Bladeren bron

feat(indexes): declare, list and drop secondary indexes

fszontagh 1 maand geleden
bovenliggende
commit
44f3ed6bc4
5 gewijzigde bestanden met toevoegingen van 116 en 0 verwijderingen
  1. 1 0
      src/CMakeLists.txt
  2. 78 0
      src/handlers/indexes.cpp
  3. 1 0
      src/server.cpp
  4. 1 0
      src/server.hpp
  5. 35 0
      tests/test_api_integration.cpp

+ 1 - 0
src/CMakeLists.txt

@@ -20,6 +20,7 @@ add_library(vectorapi_core STATIC
     handlers/projects.cpp
     handlers/keys.cpp
     handlers/collections.cpp
+    handlers/indexes.cpp
     handlers/documents.cpp
     handlers/vectors.cpp
     handlers/stats.cpp

+ 78 - 0
src/handlers/indexes.cpp

@@ -0,0 +1,78 @@
+#include "errors.hpp"
+#include "json_http.hpp"
+#include "server.hpp"
+
+namespace svapi {
+
+void registerIndexRoutes(ApiServer& s) {
+    auto& svr = s.raw(); ServerDeps* d = &s.deps();
+
+    // Declare an index. Backfills existing rows, so it is usable immediately.
+    // Idempotent: re-declaring an existing index reports already_existed.
+    svr.Post(R"(/api/v1/projects/([^/]+)/collections/([^/]+)/indexes)",
+             [d](const httplib::Request& req, httplib::Response& res) {
+        ApiKey k = requireKey(*d, req);
+        std::string project = req.matches[1], coll = req.matches[2];
+        requireProjectManage(k, project);
+        auto body = bodyJson(req);
+        // A request body of literal `null` (or any non-object JSON) parses fine but
+        // is not an object; guard before .value() so it 422s as "missing field"
+        // rather than 500ing on a type_error.
+        std::string field = body.is_object() ? body.value("field", "") : std::string();
+        if (field.empty()) throw ApiError(ErrCode::Unprocessable, "validation", "field is required");
+
+        const std::string qc = qualify(project, coll);
+        if (!d->db.client().getCollectionInfo(qc))
+            throw ApiError(ErrCode::NotFound, "not_found", "collection not found");
+
+        // createIndex() is idempotent on the DB side but does not itself report
+        // whether the index already existed, so check the current list first.
+        bool alreadyExisted = false;
+        {
+            auto existing = d->db.client().listIndexes(qc);
+            for (const auto& def : existing) if (def.field == field) { alreadyExisted = true; break; }
+        }
+
+        uint64_t rows = 0;
+        if (!d->db.client().createIndex(qc, field, rows))
+            throw ApiError(ErrCode::Unavailable, "db_error", "failed to create index");
+        sendJson(res, 201, {{"field", field}, {"unique", false},
+                            {"rows_indexed", rows}, {"already_existed", alreadyExisted}});
+    });
+
+    svr.Get(R"(/api/v1/projects/([^/]+)/collections/([^/]+)/indexes)",
+            [d](const httplib::Request& req, httplib::Response& res) {
+        ApiKey k = requireKey(*d, req);
+        std::string project = req.matches[1], coll = req.matches[2];
+        requireProjectAccess(k, project);
+
+        const std::string qc = qualify(project, coll);
+        if (!d->db.client().getCollectionInfo(qc))
+            throw ApiError(ErrCode::NotFound, "not_found", "collection not found");
+
+        std::vector<bool> uniqueFlags;
+        auto defs = d->db.client().listIndexes(qc, uniqueFlags);
+        nlohmann::json out = nlohmann::json::array();
+        for (size_t i = 0; i < defs.size(); ++i) {
+            const auto& def = defs[i];
+            const bool uniq = i < uniqueFlags.size() && uniqueFlags[i];
+            out.push_back({{"field", def.field},
+                           {"distinct_values", def.distinctValues},
+                           {"entries", def.entries},
+                           {"unique", uniq}});
+        }
+        sendJson(res, 200, {{"indexes", out}});
+    });
+
+    svr.Delete(R"(/api/v1/projects/([^/]+)/collections/([^/]+)/indexes/([^/]+))",
+               [d](const httplib::Request& req, httplib::Response& res) {
+        ApiKey k = requireKey(*d, req);
+        std::string project = req.matches[1], coll = req.matches[2], field = req.matches[3];
+        requireProjectManage(k, project);
+        if (!d->db.client().dropIndex(qualify(project, coll), field))
+            throw ApiError(ErrCode::NotFound, "not_found", "index not found");
+        sendJson(res, 200, {{"dropped", field}});
+    });
+}
+
+} // namespace svapi

+ 1 - 0
src/server.cpp

@@ -134,6 +134,7 @@ void ApiServer::registerRoutes() {
     registerProjectRoutes(*this);
     registerKeyRoutes(*this);
     registerCollectionRoutes(*this);
+    registerIndexRoutes(*this);
     registerDocumentRoutes(*this);
     registerVectorRoutes(*this);
     registerStatsRoutes(*this);

+ 1 - 0
src/server.hpp

@@ -54,6 +54,7 @@ void registerWebuiAuthRoutes(ApiServer&);
 void registerProjectRoutes(ApiServer&);
 void registerKeyRoutes(ApiServer&);
 void registerCollectionRoutes(ApiServer&);
+void registerIndexRoutes(ApiServer&);
 void registerDocumentRoutes(ApiServer&);
 void registerVectorRoutes(ApiServer&);
 void registerStatsRoutes(ApiServer&);

+ 35 - 0
tests/test_api_integration.cpp

@@ -893,3 +893,38 @@ TEST_F(ApiFixture, CaptchaGatedInsert) {
     s.captchaVerifyUrl = "";
     settings_->save(s);
 }
+
+TEST_F(ApiFixture, IndexCreateListDrop) {
+    auto c = admin();
+    std::string base = "/api/v1/projects/" + project_ + "/collections";
+    ASSERT_EQ(c.Post(base.c_str(), nlohmann::json{{"name","idxc"},{"kind","json"}}.dump(),
+                     "application/json")->status, 201);
+    std::string idx = base + "/idxc/indexes";
+
+    auto made = c.Post(idx.c_str(), nlohmann::json{{"field","status"}}.dump(), "application/json");
+    ASSERT_TRUE(made); EXPECT_EQ(made->status, 201);
+    auto madeJson = nlohmann::json::parse(made->body);
+    EXPECT_EQ(madeJson["field"], "status");
+    EXPECT_FALSE(madeJson["unique"].get<bool>());
+
+    auto list = c.Get(idx.c_str());
+    ASSERT_EQ(list->status, 200);
+    auto listJson = nlohmann::json::parse(list->body);
+    bool found = false;
+    for (auto& e : listJson["indexes"]) if (e["field"] == "status") found = true;
+    EXPECT_TRUE(found);
+
+    EXPECT_EQ(c.Delete((idx + "/status").c_str())->status, 200);
+    auto after = c.Get(idx.c_str());
+    auto afterJson = nlohmann::json::parse(after->body);
+    for (auto& e : afterJson["indexes"]) EXPECT_NE(e["field"], "status");
+}
+
+TEST_F(ApiFixture, IndexCreateRequiresField) {
+    auto c = admin();
+    std::string base = "/api/v1/projects/" + project_ + "/collections";
+    ASSERT_EQ(c.Post(base.c_str(), nlohmann::json{{"name","idxv"},{"kind","json"}}.dump(),
+                     "application/json")->status, 201);
+    auto bad = c.Post((base + "/idxv/indexes").c_str(), nlohmann::json{}.dump(), "application/json");
+    ASSERT_TRUE(bad); EXPECT_EQ(bad->status, 422);
+}