|
|
@@ -173,6 +173,42 @@ struct DirChecksum {
|
|
|
bool operator!=(const DirChecksum& other) const { return !(*this == other); }
|
|
|
};
|
|
|
|
|
|
+// Read the raw float32 bytes from `_vectors_<collection>` sub-db for a doc.
|
|
|
+std::vector<float> read_vector_from_env(LmdbEnv& env, const std::string& collection,
|
|
|
+ const std::string& doc_id) {
|
|
|
+ using smartbotic::db::storage::ReadTxn;
|
|
|
+ ReadTxn rtxn(env);
|
|
|
+ MDB_dbi dbi = 0;
|
|
|
+ std::string name = "_vectors_" + collection;
|
|
|
+ int rc = mdb_dbi_open(rtxn.raw(), name.c_str(), 0, &dbi);
|
|
|
+ if (rc == MDB_NOTFOUND) return {};
|
|
|
+ if (rc != MDB_SUCCESS) {
|
|
|
+ std::cerr << "FAIL: read_vector dbi_open: " << mdb_strerror(rc) << "\n";
|
|
|
+ std::abort();
|
|
|
+ }
|
|
|
+ MDB_val k{doc_id.size(), const_cast<char*>(doc_id.data())};
|
|
|
+ MDB_val v{0, nullptr};
|
|
|
+ rc = mdb_get(rtxn.raw(), dbi, &k, &v);
|
|
|
+ if (rc == MDB_NOTFOUND) return {};
|
|
|
+ if (rc != MDB_SUCCESS) {
|
|
|
+ std::cerr << "FAIL: read_vector mdb_get: " << mdb_strerror(rc) << "\n";
|
|
|
+ std::abort();
|
|
|
+ }
|
|
|
+ size_t count = v.mv_size / sizeof(float);
|
|
|
+ std::vector<float> out(count);
|
|
|
+ std::memcpy(out.data(), v.mv_data, v.mv_size);
|
|
|
+ return out;
|
|
|
+}
|
|
|
+
|
|
|
+// True if a sub-db with the given name exists in env.
|
|
|
+bool sub_db_exists_in_env(LmdbEnv& env, const std::string& name) {
|
|
|
+ using smartbotic::db::storage::ReadTxn;
|
|
|
+ ReadTxn rtxn(env);
|
|
|
+ MDB_dbi dbi = 0;
|
|
|
+ int rc = mdb_dbi_open(rtxn.raw(), name.c_str(), 0, &dbi);
|
|
|
+ return rc == MDB_SUCCESS;
|
|
|
+}
|
|
|
+
|
|
|
} // anonymous namespace
|
|
|
|
|
|
// -------------------------------------------------------------------------
|
|
|
@@ -510,6 +546,116 @@ void test_already_migrated_short_circuit() {
|
|
|
std::cout << "PASS: already-migrated short-circuits\n";
|
|
|
}
|
|
|
|
|
|
+// -------------------------------------------------------------------------
|
|
|
+// Task 3.3: Vector migration
|
|
|
+// -------------------------------------------------------------------------
|
|
|
+
|
|
|
+void test_migrate_collection_with_vectors() {
|
|
|
+ auto dir = make_tmpdir("vec");
|
|
|
+ {
|
|
|
+ V1Builder b(dir);
|
|
|
+ CollectionOptions opts;
|
|
|
+ opts.vectorDimension = 8;
|
|
|
+ b.createCollection("vecs", opts);
|
|
|
+ for (int i = 0; i < 50; i++) {
|
|
|
+ // Insert via the public path so _vector goes through v1.x's
|
|
|
+ // strip-and-stash flow (the JSON _vector field is moved into
|
|
|
+ // the parallel vectors map and stripped from doc data).
|
|
|
+ nlohmann::json data = {
|
|
|
+ {"content", "doc " + std::to_string(i)},
|
|
|
+ {"_vector", {float(i), float(i+1), float(i+2), float(i+3),
|
|
|
+ float(i+4), float(i+5), float(i+6), float(i+7)}}
|
|
|
+ };
|
|
|
+ Document d = makeDoc("v" + std::to_string(i), data);
|
|
|
+ b.insert("vecs", d);
|
|
|
+ }
|
|
|
+ b.writeSnapshot();
|
|
|
+ }
|
|
|
+
|
|
|
+ TmpEnv env_dir("vec-env");
|
|
|
+ LmdbDocumentStore target(env_dir.env);
|
|
|
+ auto r = migrate_v1_to_v2(dir, target, env_dir.env);
|
|
|
+ check(r.success, "vector migration succeeds");
|
|
|
+ check(r.docs_migrated == 50, "50 vector docs migrated");
|
|
|
+
|
|
|
+ // Spot-check entries.
|
|
|
+ for (int i : {0, 17, 49}) {
|
|
|
+ auto v = read_vector_from_env(env_dir.env, "vecs", "v" + std::to_string(i));
|
|
|
+ check(v.size() == 8, "vector dim=8 round-trip");
|
|
|
+ for (int j = 0; j < 8; j++) {
|
|
|
+ check(v[j] == float(i + j), "vector value matches v1.x source");
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ std::error_code ec;
|
|
|
+ fs::remove_all(dir, ec);
|
|
|
+ std::cout << "PASS: collection with vectors migrates\n";
|
|
|
+}
|
|
|
+
|
|
|
+void test_migrate_collection_with_partial_vectors() {
|
|
|
+ auto dir = make_tmpdir("partial-vec");
|
|
|
+ {
|
|
|
+ V1Builder b(dir);
|
|
|
+ CollectionOptions opts;
|
|
|
+ opts.vectorDimension = 4;
|
|
|
+ b.createCollection("vecs", opts);
|
|
|
+ // Half the docs have _vector, half don't.
|
|
|
+ for (int i = 0; i < 10; i++) {
|
|
|
+ nlohmann::json data;
|
|
|
+ data["content"] = "doc " + std::to_string(i);
|
|
|
+ if (i % 2 == 0) {
|
|
|
+ data["_vector"] = {float(i), float(i+1), float(i+2), float(i+3)};
|
|
|
+ }
|
|
|
+ Document d = makeDoc("d" + std::to_string(i), data);
|
|
|
+ b.insert("vecs", d);
|
|
|
+ }
|
|
|
+ b.writeSnapshot();
|
|
|
+ }
|
|
|
+
|
|
|
+ TmpEnv env_dir("partial-vec-env");
|
|
|
+ LmdbDocumentStore target(env_dir.env);
|
|
|
+ auto r = migrate_v1_to_v2(dir, target, env_dir.env);
|
|
|
+ check(r.success, "partial-vector migration succeeds");
|
|
|
+ check(r.docs_migrated == 10, "10 docs total migrated");
|
|
|
+
|
|
|
+ int vec_count = 0;
|
|
|
+ for (int i = 0; i < 10; i++) {
|
|
|
+ auto v = read_vector_from_env(env_dir.env, "vecs", "d" + std::to_string(i));
|
|
|
+ if (!v.empty()) {
|
|
|
+ ++vec_count;
|
|
|
+ check(v.size() == 4, "dim=4 round-trip");
|
|
|
+ check(v[0] == float(i), "first elem matches");
|
|
|
+ }
|
|
|
+ }
|
|
|
+ check(vec_count == 5, "5 vectors actually written (only even-indexed docs)");
|
|
|
+
|
|
|
+ std::error_code ec;
|
|
|
+ fs::remove_all(dir, ec);
|
|
|
+ std::cout << "PASS: collection with partial vectors\n";
|
|
|
+}
|
|
|
+
|
|
|
+void test_migrate_non_vector_collection_no_sub_db() {
|
|
|
+ auto dir = make_tmpdir("no-vec");
|
|
|
+ {
|
|
|
+ V1Builder b(dir);
|
|
|
+ // vector_dimension = 0 → no vector sub-db expected.
|
|
|
+ b.createCollection("plain");
|
|
|
+ b.insert("plain", makeDoc("d1", {{"v", 1}}));
|
|
|
+ b.writeSnapshot();
|
|
|
+ }
|
|
|
+
|
|
|
+ TmpEnv env_dir("no-vec-env");
|
|
|
+ LmdbDocumentStore target(env_dir.env);
|
|
|
+ auto r = migrate_v1_to_v2(dir, target, env_dir.env);
|
|
|
+ check(r.success, "non-vector migration succeeds");
|
|
|
+ check(!sub_db_exists_in_env(env_dir.env, "_vectors_plain"),
|
|
|
+ "no _vectors_plain sub-db created");
|
|
|
+
|
|
|
+ std::error_code ec;
|
|
|
+ fs::remove_all(dir, ec);
|
|
|
+ std::cout << "PASS: non-vector collection has no vector sub-db\n";
|
|
|
+}
|
|
|
+
|
|
|
// -------------------------------------------------------------------------
|
|
|
// main()
|
|
|
// -------------------------------------------------------------------------
|
|
|
@@ -532,6 +678,11 @@ int main() {
|
|
|
test_force_overrides_no_v1_check();
|
|
|
test_already_migrated_short_circuit();
|
|
|
|
|
|
+ // Task 3.3: Vector migration
|
|
|
+ test_migrate_collection_with_vectors();
|
|
|
+ test_migrate_collection_with_partial_vectors();
|
|
|
+ test_migrate_non_vector_collection_no_sub_db();
|
|
|
+
|
|
|
std::cout << "\nAll migrate_v1_to_v2 tests passed.\n";
|
|
|
return 0;
|
|
|
}
|