Przeglądaj źródła

test: end-to-end vector storage integration test

Adds tests/test_vector_storage.cpp covering the full pipeline:
create vector collection, insert docs with _vector fields, similarity
search with score/order validation, _vector stripping, dimension
mismatch rejection, min_score filtering, delete/update propagation
to the vector index, and non-vector collection rejection.

Wires the test into CMake via tests/CMakeLists.txt with a new
SMARTBOTIC_DB_BUILD_TESTS option (ON by default in standalone mode).
fszontagh 4 miesięcy temu
rodzic
commit
58b8acd91f
3 zmienionych plików z 209 dodań i 0 usunięć
  1. 7 0
      CMakeLists.txt
  2. 37 0
      tests/CMakeLists.txt
  3. 165 0
      tests/test_vector_storage.cpp

+ 7 - 0
CMakeLists.txt

@@ -87,12 +87,19 @@ if(SMARTBOTIC_DB_BUILD_SERVICE)
     add_subdirectory(service)
 endif()
 
+# Tests (standalone build only)
+option(SMARTBOTIC_DB_BUILD_TESTS "Build unit/integration tests" ${SMARTBOTIC_DB_STANDALONE})
+if(SMARTBOTIC_DB_BUILD_TESTS)
+    add_subdirectory(tests)
+endif()
+
 # Print configuration summary
 if(SMARTBOTIC_DB_STANDALONE)
     message(STATUS "")
     message(STATUS "Smartbotic Database ${SMARTBOTIC_DB_VERSION}")
     message(STATUS "  Build client library: ${SMARTBOTIC_DB_BUILD_CLIENT}")
     message(STATUS "  Build service:        ${SMARTBOTIC_DB_BUILD_SERVICE}")
+    message(STATUS "  Build tests:          ${SMARTBOTIC_DB_BUILD_TESTS}")
     message(STATUS "  Systemd support:      ${SYSTEMD_FOUND}")
     message(STATUS "")
 endif()

+ 37 - 0
tests/CMakeLists.txt

@@ -0,0 +1,37 @@
+# Tests for smartbotic-database
+
+# Vector storage integration test
+# Compiles memory_store.cpp directly — no dependency on the full service executable.
+set(TEST_VECTOR_SOURCES
+    test_vector_storage.cpp
+    ${CMAKE_CURRENT_SOURCE_DIR}/../service/src/memory_store.cpp
+)
+
+add_executable(test_vector_storage ${TEST_VECTOR_SOURCES})
+
+target_include_directories(test_vector_storage PRIVATE
+    ${CMAKE_CURRENT_SOURCE_DIR}/../service/src
+)
+
+# nlohmann/json
+if(TARGET nlohmann_json::nlohmann_json)
+    target_link_libraries(test_vector_storage PRIVATE nlohmann_json::nlohmann_json)
+else()
+    target_include_directories(test_vector_storage PRIVATE ${NLOHMANN_JSON_INCLUDE_DIRS})
+endif()
+
+# spdlog
+if(TARGET spdlog::spdlog)
+    target_link_libraries(test_vector_storage PRIVATE spdlog::spdlog)
+else()
+    target_link_libraries(test_vector_storage PRIVATE ${SPDLOG_LIBRARIES})
+    target_include_directories(test_vector_storage PRIVATE ${SPDLOG_INCLUDE_DIRS})
+endif()
+
+# Threads (required by MemoryStore background threads)
+find_package(Threads REQUIRED)
+target_link_libraries(test_vector_storage PRIVATE Threads::Threads)
+
+# Register with CTest
+enable_testing()
+add_test(NAME vector_storage COMMAND test_vector_storage)

+ 165 - 0
tests/test_vector_storage.cpp

@@ -0,0 +1,165 @@
+/**
+ * End-to-end integration test for vector storage in MemoryStore.
+ *
+ * Tests the full pipeline:
+ *   1. Create vector collection (dim=3)
+ *   2. Insert documents with _vector fields
+ *   3. Similarity search — verify order and scores
+ *   4. _vector is stripped from stored document data
+ *   5. Dimension mismatch is rejected
+ *   6. min_score filter
+ *   7. Delete removes vector from index
+ *   8. Update replaces vector
+ *   9. Non-vector collection rejects similaritySearch
+ */
+
+#include "../service/src/memory_store.hpp"
+
+#include <cassert>
+#include <cmath>
+#include <iostream>
+#include <stdexcept>
+#include <vector>
+
+using namespace smartbotic::database;
+
+static void check(bool condition, const char* msg) {
+    if (!condition) {
+        std::cerr << "FAIL: " << msg << "\n";
+        std::exit(1);
+    }
+}
+
+int main() {
+    MemoryStore::Config config;
+    config.nodeId = "test";
+    // Use a low memory limit so the test stays lightweight; eviction not triggered.
+    config.maxMemoryBytes = 256ULL * 1024 * 1024;
+    MemoryStore store(config);
+    store.start();
+
+    // ---------------------------------------------------------------
+    // 1. Create vector collection (dim=3)
+    // ---------------------------------------------------------------
+    CollectionOptions opts;
+    opts.vectorDimension = 3;
+    bool created = store.createCollection("test_vectors", opts);
+    check(created, "createCollection should succeed on first call");
+
+    // Creating the same collection again should return false (already exists)
+    bool created_again = store.createCollection("test_vectors", opts);
+    check(!created_again, "createCollection should return false if already exists");
+
+    // ---------------------------------------------------------------
+    // 2. Insert documents with _vector fields
+    // ---------------------------------------------------------------
+    Document doc1;
+    doc1.id         = "doc1";
+    doc1.collection = "test_vectors";
+    doc1.data       = {{"content", "hello"}, {"_vector", {1.0f, 0.0f, 0.0f}}};
+    store.insert("test_vectors", doc1);
+
+    Document doc2;
+    doc2.id         = "doc2";
+    doc2.collection = "test_vectors";
+    doc2.data       = {{"content", "world"}, {"_vector", {0.0f, 1.0f, 0.0f}}};
+    store.insert("test_vectors", doc2);
+
+    Document doc3;
+    doc3.id         = "doc3";
+    doc3.collection = "test_vectors";
+    doc3.data       = {{"content", "similar"}, {"_vector", {0.9f, 0.1f, 0.0f}}};
+    store.insert("test_vectors", doc3);
+
+    // ---------------------------------------------------------------
+    // 3. Similarity search — query [1,0,0] should find doc1 (exact)
+    //    and doc3 (most similar), in that order
+    // ---------------------------------------------------------------
+    auto results = store.similaritySearch("test_vectors", {1.0f, 0.0f, 0.0f}, 2);
+    check(results.size() == 2,      "similarity search should return exactly 2 results");
+    check(results[0].id == "doc1",  "first result should be doc1 (exact match)");
+    check(results[0].score > 0.99f, "doc1 score should be near 1.0");
+    check(results[1].id == "doc3",  "second result should be doc3 (most similar)");
+    check(results[1].score > 0.9f,  "doc3 score should be > 0.9");
+    std::cout << "PASS: similarity search returns correct order and scores\n";
+
+    // ---------------------------------------------------------------
+    // 4. Verify _vector is stripped from stored document data
+    // ---------------------------------------------------------------
+    auto stored = store.get("test_vectors", "doc1");
+    check(stored.has_value(),                   "doc1 should exist after insert");
+    check(!stored->data.contains("_vector"),    "_vector should be stripped from stored doc");
+    check(stored->data["content"] == "hello",   "doc1 content should be 'hello'");
+    std::cout << "PASS: _vector stripped from document data\n";
+
+    // ---------------------------------------------------------------
+    // 5. Dimension mismatch should throw
+    // ---------------------------------------------------------------
+    try {
+        Document bad;
+        bad.id         = "bad";
+        bad.collection = "test_vectors";
+        bad.data       = {{"_vector", {1.0f, 2.0f}}};  // wrong dim (2 instead of 3)
+        store.insert("test_vectors", bad);
+        check(false, "dimension mismatch should have thrown");
+    } catch (const std::invalid_argument& e) {
+        std::cout << "PASS: dimension mismatch rejected: " << e.what() << "\n";
+    } catch (const std::exception& e) {
+        // Some implementations may throw runtime_error or similar
+        std::cout << "PASS: dimension mismatch rejected (exception): " << e.what() << "\n";
+    }
+
+    // ---------------------------------------------------------------
+    // 6. min_score filter
+    //    doc1 = [1,0,0] has cosine score 1.0 (exact match)
+    //    doc3 = [0.9,0.1,0] has cosine score ~0.994 with query [1,0,0]
+    //    Using min_score=0.999 keeps only doc1.
+    // ---------------------------------------------------------------
+    auto filtered = store.similaritySearch("test_vectors", {1.0f, 0.0f, 0.0f}, 10, 0.999f);
+    check(filtered.size() == 1,    "min_score=0.999 should return exactly 1 result");
+    check(filtered[0].id == "doc1","min_score filtered result should be doc1");
+    std::cout << "PASS: min_score filter works\n";
+
+    // ---------------------------------------------------------------
+    // 7. Delete removes vector from index
+    // ---------------------------------------------------------------
+    store.remove("test_vectors", "doc1");
+    auto after_delete = store.similaritySearch("test_vectors", {1.0f, 0.0f, 0.0f}, 10);
+    for (const auto& r : after_delete) {
+        check(r.id != "doc1", "doc1 should not appear after delete");
+    }
+    std::cout << "PASS: delete removes vector\n";
+
+    // ---------------------------------------------------------------
+    // 8. Update replaces vector
+    //    Move doc2 from [0,1,0] to [1,0,0] — it should now rank first
+    // ---------------------------------------------------------------
+    Document doc2_updated;
+    doc2_updated.id         = "doc2";
+    doc2_updated.collection = "test_vectors";
+    doc2_updated.data       = {{"content", "updated"}, {"_vector", {1.0f, 0.0f, 0.0f}}};
+    store.update("test_vectors", "doc2", doc2_updated);
+
+    auto after_update = store.similaritySearch("test_vectors", {1.0f, 0.0f, 0.0f}, 1);
+    check(!after_update.empty(),           "search after update should return results");
+    check(after_update[0].id == "doc2",    "after update, doc2 should be top result");
+    check(after_update[0].score > 0.99f,   "updated doc2 score should be near 1.0");
+    std::cout << "PASS: update replaces vector\n";
+
+    // ---------------------------------------------------------------
+    // 9. Non-vector collection rejects similaritySearch
+    // ---------------------------------------------------------------
+    store.createCollection("no_vectors", {});
+    try {
+        store.similaritySearch("no_vectors", {1.0f, 0.0f, 0.0f}, 1);
+        check(false, "non-vector collection should have thrown on similaritySearch");
+    } catch (const std::invalid_argument&) {
+        std::cout << "PASS: non-vector collection rejects search\n";
+    } catch (const std::exception& e) {
+        std::cout << "PASS: non-vector collection rejects search (exception): " << e.what() << "\n";
+    }
+
+    store.stop();
+    std::cout << "\nAll vector storage tests PASSED!\n";
+    return 0;
+}