Quellcode durchsuchen

feat(registry): project-scoped collection registry

Fszontagh vor 2 Monaten
Ursprung
Commit
a37bb7efd3
5 geänderte Dateien mit 141 neuen und 0 gelöschten Zeilen
  1. 1 0
      src/CMakeLists.txt
  2. 56 0
      src/collection_registry.cpp
  3. 52 0
      src/collection_registry.hpp
  4. 4 0
      tests/CMakeLists.txt
  5. 28 0
      tests/test_registry.cpp

+ 1 - 0
src/CMakeLists.txt

@@ -8,6 +8,7 @@ add_library(vectorapi_core STATIC
     apikey.cpp
     key_store.cpp
     settings_store.cpp
+    collection_registry.cpp
 )
 target_include_directories(vectorapi_core PUBLIC
     ${CMAKE_CURRENT_SOURCE_DIR}

+ 56 - 0
src/collection_registry.cpp

@@ -0,0 +1,56 @@
+#include "collection_registry.hpp"
+#include <ctime>
+
+namespace svapi {
+
+CollectionMeta CollectionMeta::fromJson(const nlohmann::json& j) {
+    CollectionMeta m;
+    m.name            = j.value("name", "");
+    m.kind            = j.value("kind", "json");
+    m.vectorDimension = j.value("vector_dimension", 0u);
+    m.embeddingModel  = j.value("embedding_model", "");
+    m.createdAt       = j.value("created_at", 0ull);
+    return m;
+}
+
+nlohmann::json CollectionMeta::toJson() const {
+    return {{"name", name}, {"kind", kind}, {"vector_dimension", vectorDimension},
+            {"embedding_model", embeddingModel}, {"created_at", createdAt}};
+}
+
+CollectionRegistry::CollectionRegistry(DbGateway& db) : db_(db) {}
+
+void CollectionRegistry::ensure(const std::string& project) {
+    const std::string c = coll(project);
+    if (!db_.client().getCollectionInfo(c).has_value())
+        db_.client().createCollection(c, 0, false, 0, 0);
+}
+
+bool CollectionRegistry::add(const std::string& project, const CollectionMeta& meta) {
+    CollectionMeta m = meta;
+    if (m.createdAt == 0) m.createdAt = static_cast<uint64_t>(std::time(nullptr));
+    auto [id, isNew] = db_.client().upsert(coll(project), m.toJson(), m.name);
+    return !id.empty();
+}
+
+std::optional<CollectionMeta> CollectionRegistry::get(const std::string& project,
+                                                       const std::string& name) {
+    if (auto doc = db_.client().get(coll(project), name))
+        return CollectionMeta::fromJson(*doc);
+    return std::nullopt;
+}
+
+std::vector<CollectionMeta> CollectionRegistry::list(const std::string& project) {
+    std::vector<CollectionMeta> out;
+    smartbotic::database::Client::QueryOptions opts;
+    opts.limit = 100000;
+    for (const auto& doc : db_.client().find(coll(project), opts))
+        out.push_back(CollectionMeta::fromJson(doc));
+    return out;
+}
+
+bool CollectionRegistry::remove(const std::string& project, const std::string& name) {
+    return db_.client().remove(coll(project), name);
+}
+
+} // namespace svapi

+ 52 - 0
src/collection_registry.hpp

@@ -0,0 +1,52 @@
+#pragma once
+#include "db_gateway.hpp"
+#include <nlohmann/json.hpp>
+#include <optional>
+#include <string>
+#include <vector>
+
+namespace svapi {
+
+/// Metadata describing a managed collection.
+struct CollectionMeta {
+    std::string name;
+    std::string kind;
+    uint32_t    vectorDimension = 0;
+    std::string embeddingModel;
+    uint64_t    createdAt = 0;
+
+    static CollectionMeta fromJson(const nlohmann::json&);
+    nlohmann::json toJson() const;
+};
+
+/// Project-scoped registry: one document per managed collection stored in
+/// qualify(project, "vectorapi_collections").
+class CollectionRegistry {
+public:
+    explicit CollectionRegistry(DbGateway& db);
+
+    /// Ensure the registry collection for `project` exists (idempotent).
+    void ensure(const std::string& project);
+
+    /// Add (or overwrite) a collection entry.  Returns true on success.
+    bool add(const std::string& project, const CollectionMeta& meta);
+
+    /// Look up a single collection entry by name.
+    std::optional<CollectionMeta> get(const std::string& project, const std::string& name);
+
+    /// List all collection entries for the project.
+    std::vector<CollectionMeta> list(const std::string& project);
+
+    /// Remove a collection entry.  Returns true if the document was present.
+    bool remove(const std::string& project, const std::string& name);
+
+private:
+    DbGateway& db_;
+
+    /// Qualified registry collection name for the given project.
+    std::string coll(const std::string& project) const {
+        return qualify(project, "vectorapi_collections");
+    }
+};
+
+} // namespace svapi

+ 4 - 0
tests/CMakeLists.txt

@@ -28,3 +28,7 @@ gtest_discover_tests(test_settings)
 add_executable(test_stores test_stores.cpp)
 target_link_libraries(test_stores PRIVATE vectorapi_core GTest::gtest_main)
 gtest_discover_tests(test_stores)
+
+add_executable(test_registry test_registry.cpp)
+target_link_libraries(test_registry PRIVATE vectorapi_core GTest::gtest_main)
+gtest_discover_tests(test_registry)

+ 28 - 0
tests/test_registry.cpp

@@ -0,0 +1,28 @@
+#include "db_test_util.hpp"
+#include "collection_registry.hpp"
+using namespace svapi;
+
+TEST(CollectionMeta, JsonRoundTrip) {
+    CollectionMeta m{"docs", "vector", 1536, "text-embedding-3-small", 123};
+    CollectionMeta r = CollectionMeta::fromJson(m.toJson());
+    EXPECT_EQ(r.name, "docs"); EXPECT_EQ(r.kind, "vector");
+    EXPECT_EQ(r.vectorDimension, 1536u); EXPECT_EQ(r.embeddingModel, "text-embedding-3-small");
+}
+
+TEST(CollectionRegistry, IsolatedPerProject) {
+    SVAPI_REQUIRE_DB(db);
+    const std::string pa = testutil::tmpName("rega"), pb = testutil::tmpName("regb");
+    db->ensureProject(pa); db->ensureProject(pb);
+    CollectionRegistry reg(*db);
+    reg.ensure(pa); reg.ensure(pb);
+
+    EXPECT_TRUE(reg.add(pa, {"users", "json", 0, "", 0}));
+    EXPECT_TRUE(reg.get(pa, "users").has_value());
+    EXPECT_FALSE(reg.get(pb, "users").has_value());      // isolation across projects
+    EXPECT_EQ(reg.list(pa).size(), 1u);
+    EXPECT_EQ(reg.list(pb).size(), 0u);
+    EXPECT_TRUE(reg.remove(pa, "users"));
+    EXPECT_FALSE(reg.get(pa, "users").has_value());
+
+    db->dropProject(pa); db->dropProject(pb);
+}