@@ -3678,3 +3678,125 @@ TYPED_TEST(HNSWTieredIndexTestBasic, deleteInplaceMultiSwapId) {
36783678 ASSERT_EQ (tiered_index->deleteVector (0 ), 2 );
36793679 ASSERT_EQ (tiered_index->getHNSWIndex ()->safeGetEntryPointState ().first , 0 );
36803680}
3681+
3682+ TYPED_TEST (HNSWTieredIndexTestBasic, deleteInplaceAvoidUpdatedMarkedDeleted) {
3683+ // Create TieredHNSW index instance with a mock queue.
3684+ size_t dim = 4 ;
3685+ HNSWParams params = {
3686+ .type = TypeParam::get_index_type (), .dim = dim, .metric = VecSimMetric_L2};
3687+ VecSimParams hnsw_params = CreateParams (params);
3688+
3689+ auto mock_thread_pool = tieredIndexMock ();
3690+
3691+ auto *tiered_index = this ->CreateTieredHNSWIndex (hnsw_params, mock_thread_pool);
3692+ auto allocator = tiered_index->getAllocator ();
3693+
3694+ // Insert three vector to HNSW, expect a full graph to be created
3695+ GenerateAndAddVector<TEST_DATA_T>(tiered_index->backendIndex , dim, 0 , 0 );
3696+ GenerateAndAddVector<TEST_DATA_T>(tiered_index->backendIndex , dim, 1 , 1 );
3697+ GenerateAndAddVector<TEST_DATA_T>(tiered_index->backendIndex , dim, 2 , 2 );
3698+
3699+ // Delete vector with id=0 asynchronously, expect to have a repair job for the other vectors.
3700+ ASSERT_EQ (tiered_index->deleteVector (0 ), 1 );
3701+ ASSERT_EQ (tiered_index->idToRepairJobs .size (), 2 );
3702+ ASSERT_EQ (tiered_index->idToRepairJobs .at (1 )[0 ]->associatedSwapJobs .size (), 1 );
3703+ ASSERT_EQ (tiered_index->idToRepairJobs .at (2 )[0 ]->associatedSwapJobs .size (), 1 );
3704+
3705+ // Execute the repair job for 1->0. Now, 0->1 is unidirectional edge
3706+ ASSERT_TRUE (mock_thread_pool.jobQ .front ().job ->isValid );
3707+ mock_thread_pool.thread_iteration ();
3708+ ASSERT_EQ (tiered_index->idToRepairJobs .size (), 1 );
3709+ ASSERT_EQ (tiered_index->idToRepairJobs .at (2 )[0 ]->associatedSwapJobs .size (), 1 );
3710+
3711+ // Insert another vector with id=3, that should be connected to both 1 and 2.
3712+ GenerateAndAddVector<TEST_DATA_T>(tiered_index->backendIndex , dim, 3 , -1 );
3713+
3714+ // Delete in-place id=2, expect that upon repairing inplace 1 due to 1->2, there will *not* be
3715+ // a new edge 1->0 since 0 is deleted. Also the other repair job 2->0 should be invalidated.
3716+ // Also, expect that repairing 3 in-place will not create a new edge to marked deleted 0 and
3717+ // vice versa.
3718+ tiered_index->setWriteMode (VecSim_WriteInPlace);
3719+ ASSERT_EQ (tiered_index->deleteVector (2 ), 1 );
3720+ ASSERT_FALSE (mock_thread_pool.jobQ .front ().job ->isValid );
3721+ int **neighbours;
3722+ ASSERT_EQ (tiered_index->getHNSWIndex ()->getHNSWElementNeighbors (1 , &neighbours),
3723+ VecSimDebugCommandCode_OK);
3724+ // Expect 1 neighbors at level 0 (id=3) and that 0 is NOT a new neighbor for 1.
3725+ ASSERT_EQ (neighbours[0 ][0 ], 1 );
3726+ ASSERT_EQ (neighbours[0 ][1 ], 3 );
3727+ VecSimDebug_ReleaseElementNeighborsInHNSWGraph (neighbours);
3728+
3729+ ASSERT_EQ (tiered_index->getHNSWIndex ()->getHNSWElementNeighbors (3 , &neighbours),
3730+ VecSimDebugCommandCode_OK);
3731+ ASSERT_EQ (neighbours[0 ][0 ], 1 );
3732+ // Expect 1 neighbors at level 0 (id=1) and that 0 is NOT a new neighbor for 3.
3733+ ASSERT_EQ (neighbours[0 ][1 ], 1 );
3734+ VecSimDebug_ReleaseElementNeighborsInHNSWGraph (neighbours);
3735+
3736+ auto &level_data = tiered_index->getHNSWIndex ()->getElementLevelData ((idType)0 , 0 );
3737+ // Expect 1 neighbors at level 0 (id=1) and that 3 is NOT a new neighbor for 0.
3738+ ASSERT_EQ (level_data.getNumLinks (), 1 );
3739+ ASSERT_EQ (level_data.getLinkAtPos (0 ), 1 );
3740+
3741+ // Expect that id=0 is a ready swap job and execute it.
3742+ ASSERT_EQ (tiered_index->readySwapJobs , 1 );
3743+ ASSERT_TRUE (tiered_index->idToSwapJob .contains (0 ));
3744+ tiered_index->runGC ();
3745+ }
3746+
3747+ TYPED_TEST (HNSWTieredIndexTestBasic, switchDeleteModes) {
3748+ // Create TieredHNSW index instance with a mock queue.
3749+ size_t dim = 16 ;
3750+ size_t n = 1000 ;
3751+ size_t swap_job_threshold = 10 ;
3752+ HNSWParams params = {
3753+ .type = TypeParam::get_index_type (),
3754+ .dim = dim,
3755+ .metric = VecSimMetric_L2,
3756+ .blockSize = 100 ,
3757+ };
3758+ VecSimParams hnsw_params = CreateParams (params);
3759+ auto mock_thread_pool = tieredIndexMock ();
3760+
3761+ auto *tiered_index =
3762+ this ->CreateTieredHNSWIndex (hnsw_params, mock_thread_pool, swap_job_threshold);
3763+ auto allocator = tiered_index->getAllocator ();
3764+
3765+ // Launch the BG threads loop that takes jobs from the queue and executes them.
3766+ mock_thread_pool.init_threads ();
3767+
3768+ // Create and insert vectors one by one inplace.
3769+ VecSim_SetWriteMode (VecSim_WriteInPlace);
3770+ std::srand (10 ); // create pseudo random generator with any arbitrary seed.
3771+ for (size_t i = 0 ; i < n; i++) {
3772+ TEST_DATA_T vector[dim];
3773+ for (size_t j = 0 ; j < dim; j++) {
3774+ vector[j] = std::rand () / (TEST_DATA_T)RAND_MAX;
3775+ }
3776+ VecSimIndex_AddVector (tiered_index, vector, i);
3777+ }
3778+ EXPECT_EQ (tiered_index->backendIndex ->indexSize (), n);
3779+
3780+ // Update vectors while changing the write mode.
3781+ for (size_t i = 0 ; i < n; i++) {
3782+ TEST_DATA_T vector[dim];
3783+ for (size_t j = 0 ; j < dim; j++) {
3784+ vector[j] = std::rand () / (TEST_DATA_T)RAND_MAX;
3785+ }
3786+ EXPECT_EQ (tiered_index->addVector (vector, i), 0 );
3787+ if (i % 10 == 0 ) {
3788+ // Change mode every 10 vectors.
3789+ auto next_mode = tiered_index->getWriteMode () == VecSim_WriteInPlace
3790+ ? VecSim_WriteAsync
3791+ : VecSim_WriteInPlace;
3792+ VecSim_SetWriteMode (next_mode);
3793+ }
3794+ }
3795+
3796+ mock_thread_pool.thread_pool_join ();
3797+ ASSERT_EQ (tiered_index->frontendIndex ->indexSize (), 0 );
3798+ ASSERT_EQ (tiered_index->backendIndex ->indexLabelCount (), n);
3799+ auto state = tiered_index->getHNSWIndex ()->checkIntegrity ();
3800+ ASSERT_EQ (state.valid_state , 1 );
3801+ ASSERT_EQ (state.connections_to_repair , 0 );
3802+ }
0 commit comments