|
@@ -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;
|
|
|
|
|
+}
|