@@ -161,7 +161,7 @@ TEST_F(HnswTest, BruteForceRecall) {
161161 const auto points =
162162 make_pseudo_random_points (kNumPoints , kRecallDims , &state);
163163 for (size_t i = 0 ; i < kNumPoints ; ++i) {
164- index.insert (i, /* base_pk=*/ i, as_bytes (points[i]));
164+ index.insert (i + 1 , /* base_pk=*/ i, as_bytes (points[i]));
165165 }
166166
167167 const auto queries =
@@ -257,7 +257,7 @@ TEST_F(HnswTest, StreamMultipleBatchesNoDuplicates) {
257257 TestHnsw index (kDims , euclidean, kM , kEfConstruction );
258258 for (uint64_t i = 0 ; i < 30 ; ++i) {
259259 const auto v = make_vec ({static_cast <float >(i), 0 .0f });
260- index.insert (i, 1000 + i, as_bytes (v));
260+ index.insert (i + 1 , 1000 + i, as_bytes (v));
261261 }
262262 const auto query = make_vec ({0 .0f , 0 .0f });
263263 // Small batch/ef forces continuation refills across batches.
@@ -275,7 +275,7 @@ TEST_F(HnswTest, StreamDistancesNonDecreasingAcrossBatches) {
275275 std::vector<std::vector<float >> points;
276276 for (uint64_t i = 0 ; i < 40 ; ++i) {
277277 points.push_back (make_vec ({static_cast <float >(i), 0 .0f }));
278- index.insert (i, /* base_pk=*/ i, as_bytes (points.back ()));
278+ index.insert (i + 1 , /* base_pk=*/ i, as_bytes (points.back ()));
279279 }
280280
281281 const auto query = make_vec ({0 .0f , 0 .0f });
@@ -302,7 +302,7 @@ TEST_F(HnswTest, StreamDrainsEntireGraph) {
302302
303303 for (uint64_t i = 0 ; i < kNumPoints ; ++i) {
304304 const auto v = make_vec ({static_cast <float >(i), 0 .0f });
305- index.insert (i, /* base_pk=*/ 1000 + i, as_bytes (v));
305+ index.insert (i + 1 , /* base_pk=*/ 1000 + i, as_bytes (v));
306306 }
307307
308308 const auto query = make_vec ({0 .0f , 0 .0f });
@@ -326,7 +326,7 @@ TEST_F(HnswTest, StreamDrainsEntireGraphWithEfSmallerThanGraph) {
326326
327327 for (uint64_t i = 0 ; i < kNumPoints ; ++i) {
328328 const auto v = make_vec ({static_cast <float >(i), 0 .0f });
329- index.insert (i, /* base_pk=*/ 1000 + i, as_bytes (v));
329+ index.insert (i + 1 , /* base_pk=*/ 1000 + i, as_bytes (v));
330330 }
331331
332332 const auto query = make_vec ({0 .0f , 0 .0f });
@@ -354,7 +354,7 @@ TEST_F(HnswTest, StreamNoDuplicatesInMultiDimensionalGraph) {
354354 const auto points =
355355 make_pseudo_random_points (kNumPoints , kStreamDims , &state);
356356 for (size_t i = 0 ; i < kNumPoints ; ++i) {
357- index.insert (i, /* base_pk=*/ i, as_bytes (points[i]));
357+ index.insert (i + 1 , /* base_pk=*/ i, as_bytes (points[i]));
358358 }
359359
360360 const auto queries =
@@ -382,30 +382,33 @@ TEST_F(HnswTest, StreamYieldsEachNodeAtMostOnce) {
382382
383383 for (uint64_t i = 0 ; i < 5 ; ++i) {
384384 index.insert (
385- i, i,
385+ i + 1 , i + 1 ,
386386 as_bytes (make_vec ({100 .0f + 0 .5f * static_cast <float >(i), 0 .0f })));
387387 }
388388 for (uint64_t i = 5 ; i < 10 ; ++i) {
389389 index.insert (
390- i, i,
390+ i + 1 , i + 1 ,
391391 as_bytes (make_vec ({100 .0f + 0 .5f * static_cast <float >(i - 5 ), 1 .0f })));
392392 }
393393 for (uint64_t i = 10 ; i < 15 ; ++i) {
394394 index.insert (
395- i, i,
395+ i + 1 , i + 1 ,
396396 as_bytes (make_vec ({100 .0f + 0 .5f * static_cast <float >(i - 10 ), 2 .0f })));
397397 }
398398 for (uint64_t i = 100 ; i < 105 ; ++i) {
399399 index.insert (
400- i, i, as_bytes (make_vec ({0 .5f * static_cast <float >(i - 100 ), 0 .0f })));
400+ i + 1 , i + 1 ,
401+ as_bytes (make_vec ({0 .5f * static_cast <float >(i - 100 ), 0 .0f })));
401402 }
402403 for (uint64_t i = 105 ; i < 110 ; ++i) {
403404 index.insert (
404- i, i, as_bytes (make_vec ({0 .5f * static_cast <float >(i - 105 ), 1 .0f })));
405+ i + 1 , i + 1 ,
406+ as_bytes (make_vec ({0 .5f * static_cast <float >(i - 105 ), 1 .0f })));
405407 }
406408 for (uint64_t i = 110 ; i < 115 ; ++i) {
407409 index.insert (
408- i, i, as_bytes (make_vec ({0 .5f * static_cast <float >(i - 110 ), 2 .0f })));
410+ i + 1 , i + 1 ,
411+ as_bytes (make_vec ({0 .5f * static_cast <float >(i - 110 ), 2 .0f })));
409412 }
410413
411414 const auto query = make_vec ({0 .0f , 0 .0f });
@@ -466,7 +469,7 @@ TEST_F(HnswTest, StreamBruteForceRecall) {
466469 const auto points =
467470 make_pseudo_random_points (kNumPoints , kRecallDims , &state);
468471 for (size_t i = 0 ; i < kNumPoints ; ++i) {
469- index.insert (i, /* base_pk=*/ i, as_bytes (points[i]));
472+ index.insert (i + 1 , /* base_pk=*/ i, as_bytes (points[i]));
470473 }
471474
472475 const auto queries =
@@ -506,14 +509,153 @@ TEST_F(HnswTest, StreamBruteForceRecall) {
506509 << " avg stream recall@125=" << avg_recall;
507510}
508511
512+ namespace {
513+
514+ void assert_round_trip_knn (RoundTripFixture *fixture, size_t dims, size_t M,
515+ size_t ef_construction, size_t k, size_t ef_search,
516+ bool validate_built [[maybe_unused]] ) {
517+ LoadTestHnsw built (dims, euclidean, M, ef_construction);
518+ populate_round_trip_index (built, fixture);
519+ ASSERT_GT (fixture->store .entry_point , 0U );
520+ #ifndef NDEBUG
521+ if (validate_built) {
522+ EXPECT_TRUE (built.validate ());
523+ }
524+ #endif
525+
526+ LoadTestHnsw reloaded (dims, euclidean, M, ef_construction);
527+ reloaded.init_from_entry_point (fixture->store .entry_point , &fixture->store );
528+
529+ const auto from_built = built.k_nn_search (as_bytes (fixture->query ), k,
530+ ef_search, &fixture->store );
531+ const auto from_reloaded = reloaded.k_nn_search (as_bytes (fixture->query ), k,
532+ ef_search, &fixture->store );
533+
534+ ASSERT_EQ (from_built.size (), from_reloaded.size ());
535+ EXPECT_EQ (from_built, from_reloaded);
536+ }
537+
538+ void assert_round_trip_stream (RoundTripFixture *fixture, size_t dims, size_t M,
539+ size_t ef_construction, size_t batch_size,
540+ size_t ef_search, size_t max_results) {
541+ LoadTestHnsw built (dims, euclidean, M, ef_construction);
542+ populate_round_trip_index (built, fixture);
543+ ASSERT_GT (fixture->store .entry_point , 0U );
544+
545+ LoadTestHnsw reloaded (dims, euclidean, M, ef_construction);
546+ reloaded.init_from_entry_point (fixture->store .entry_point , &fixture->store );
547+
548+ const auto stream_built = drain_stream (built, as_bytes (fixture->query ),
549+ batch_size, ef_search, max_results);
550+ const auto stream_reloaded =
551+ drain_stream (reloaded, as_bytes (fixture->query ), batch_size, ef_search,
552+ max_results, &fixture->store );
553+
554+ ASSERT_EQ (stream_built.size (), stream_reloaded.size ());
555+ EXPECT_EQ (stream_built, stream_reloaded);
556+ }
557+
558+ } // namespace
559+
560+ TEST_F (HnswTest, RoundTripSmallGraph) {
561+ RoundTripFixture fixture = make_fixed_round_trip_fixture (kDims );
562+ assert_round_trip_knn (&fixture, kDims , kM , kEfConstruction ,
563+ /* k=*/ 3 , /* ef_search=*/ 16 , /* validate_built=*/ true );
564+ }
565+
566+ TEST_F (HnswTest, RoundTripRandomGraph) {
567+ constexpr size_t kNumPoints = 1000 ;
568+ constexpr uint64_t kSeed = 4242 ;
569+ RoundTripFixture fixture =
570+ make_random_round_trip_fixture (kDims , kNumPoints , kSeed );
571+ assert_round_trip_knn (&fixture, kDims , kM , kEfConstruction ,
572+ /* k=*/ 10 , /* ef_search=*/ 50 , /* validate_built=*/ false );
573+ }
574+
575+ TEST_F (HnswTest, RoundTripStreamSearch) {
576+ RoundTripFixture fixture = make_fixed_round_trip_fixture (kDims );
577+ assert_round_trip_stream (&fixture, kDims , kM , kEfConstruction ,
578+ /* batch_size=*/ 2 , /* ef_search=*/ 16 ,
579+ /* max_results=*/ 3 );
580+ }
581+
582+ TEST_F (HnswTest, RoundTripStreamRandomGraph) {
583+ constexpr size_t kNumPoints = 1000 ;
584+ constexpr uint64_t kSeed = 5150 ;
585+ RoundTripFixture fixture =
586+ make_random_round_trip_fixture (kDims , kNumPoints , kSeed );
587+ assert_round_trip_stream (&fixture, kDims , kM , kEfConstruction ,
588+ /* batch_size=*/ 5 , /* ef_search=*/ 50 ,
589+ /* max_results=*/ 25 );
590+ }
591+
592+ TEST_F (HnswTest, InitFromEntryPointLoadsEP) {
593+ RoundTripFixture fixture = make_fixed_round_trip_fixture (kDims );
594+ LoadTestHnsw built (kDims , euclidean, kM , kEfConstruction );
595+ populate_round_trip_index (built, &fixture);
596+ ASSERT_GT (fixture.store .entry_point , 0U );
597+
598+ LoadTestHnsw reloaded (kDims , euclidean, kM , kEfConstruction );
599+ reloaded.init_from_entry_point (fixture.store .entry_point , &fixture.store );
600+
601+ EXPECT_EQ (1U , fixture.store .load_counts .size ());
602+ EXPECT_EQ (1U , fixture.store .load_counts .at (fixture.store .entry_point ));
603+ for (uint64_t id : fixture.graph_ids ) {
604+ if (id == fixture.store .entry_point ) {
605+ continue ;
606+ }
607+ EXPECT_EQ (0U , fixture.store .load_counts .count (id));
608+ }
609+ }
610+
611+ TEST_F (HnswTest, SearchLoadsNeighborsOnDemand) {
612+ constexpr size_t kNumPoints = 1000 ;
613+ constexpr uint64_t kSeed = 7777 ;
614+ RoundTripFixture fixture =
615+ make_random_round_trip_fixture (kDims , kNumPoints , kSeed );
616+ LoadTestHnsw built (kDims , euclidean, kM , kEfConstruction );
617+ populate_round_trip_index (built, &fixture);
618+ ASSERT_GT (fixture.store .entry_point , 0U );
619+
620+ LoadTestHnsw reloaded (kDims , euclidean, kM , kEfConstruction );
621+ reloaded.init_from_entry_point (fixture.store .entry_point , &fixture.store );
622+ ASSERT_EQ (1U , fixture.store .load_counts .size ());
623+
624+ const auto hits =
625+ reloaded.k_nn_search (as_bytes (fixture.query ), 10 , 50 , &fixture.store );
626+ ASSERT_GE (hits.size (), 1U );
627+ EXPECT_GT (fixture.store .load_counts .size (), 1U );
628+ EXPECT_LT (fixture.store .load_counts .size (), fixture.graph_ids .size ());
629+ }
630+
631+ TEST_F (HnswTest, LoadNodeIdempotent) {
632+ constexpr size_t kNumPoints = 1000 ;
633+ constexpr uint64_t kSeed = 8888 ;
634+ RoundTripFixture fixture =
635+ make_random_round_trip_fixture (kDims , kNumPoints , kSeed );
636+ LoadTestHnsw built (kDims , euclidean, kM , kEfConstruction );
637+ populate_round_trip_index (built, &fixture);
638+ ASSERT_GT (fixture.store .entry_point , 0U );
639+
640+ LoadTestHnsw reloaded (kDims , euclidean, kM , kEfConstruction );
641+ reloaded.init_from_entry_point (fixture.store .entry_point , &fixture.store );
642+
643+ const auto query = as_bytes (fixture.query );
644+ (void )reloaded.k_nn_search (query, 10 , 50 , &fixture.store );
645+ const auto counts_after_first = fixture.store .load_counts ;
646+
647+ (void )reloaded.k_nn_search (query, 10 , 50 , &fixture.store );
648+ EXPECT_EQ (counts_after_first, fixture.store .load_counts );
649+ }
650+
509651#ifndef NDEBUG
510652TEST_F (HnswTest, GraphInvariants) {
511653 TestHnsw index (kDims , euclidean, kM , kEfConstruction );
512654 EXPECT_TRUE (index.validate ());
513655
514656 for (uint64_t i = 0 ; i < 50 ; ++i) {
515657 const auto v = make_vec ({static_cast <float >(i), static_cast <float >(i % 7 )});
516- index.insert (i, 1000 + i, as_bytes (v));
658+ index.insert (i + 1 , 1000 + i, as_bytes (v));
517659 EXPECT_TRUE (index.validate ()) << " after insert i=" << i;
518660 }
519661}
@@ -525,6 +667,12 @@ TEST(HnswDeathTest, SearchKZeroAsserts) {
525667 EXPECT_DEATH_IF_SUPPORTED (index.k_nn_search (as_bytes (v), /* k=*/ 0 , 16 ), " " );
526668}
527669
670+ TEST (HnswDeathTest, ZeroIdAsserts) {
671+ TestHnsw index (2 , euclidean, 4 , 16 );
672+ const auto v = make_vec ({1 .0f , 2 .0f });
673+ EXPECT_DEATH_IF_SUPPORTED (index.insert (0 , 100 , as_bytes (v)), " " );
674+ }
675+
528676TEST (HnswDeathTest, DuplicateIdAsserts) {
529677 TestHnsw index (2 , euclidean, 4 , 16 );
530678 const auto v = make_vec ({1 .0f , 2 .0f });
0 commit comments