#2906·LiteDB

Flaky test: VectorIndex_Search_Prunes_Node_Visits can return zero matches because HNSW levels use an unseeded Random

Author: JKamskerCreated Sep 16, 2026Updated Sep 16, 2026
Labelsbugseverity: lowpriority: P3

LiteDB.Tests.QueryTest.VectorIndex_Tests.VectorIndex_Search_Prunes_Node_Visits failed on dev at 31f9a7f4 in the Test (Windows net481) job with:

Expected stats.Matches to contain only items matching (id <= 64), but the collection is empty.
  at LiteDB.Tests.Query.VectorIndex_Tests.cs(591)

The identical tree passed the same net481 job 20 minutes earlier on PR #2755 (run 35152785054), and net462 passed in the failing run. The test data is fully deterministic (two clusters of 64 fixed vectors), so the variation is inside the index.

Cause

LiteDB/Engine/Services/VectorIndexService.cs:16:

csharp
private readonly Random _random = new Random();

SampleLevel() uses it to pick the HNSW level of every inserted node, so the graph topology differs on every process start. The test searches with maxDistance: 0.25, limit: 5, and Search tightens pruneDistance to the current worst result as it goes. With an unlucky level assignment the greedy descent can enter the far cluster and prune every near-cluster node before visiting it, returning no matches. This is a property of approximate search with pruning; the test asserts an exact outcome.

Options

  1. Make the level sampler deterministic for tests, for example an internal seed hook or a Random injected through VectorIndexOptions (a fixed seed also makes index builds reproducible, which helps debugging).
  2. Keep the pruning assertion (Total > Visited) but relax the match assertion to tolerate an empty result, or search with a larger maxDistance/limit so pruning cannot exclude the whole cluster.
  3. Seed _random from something stable per index (for example the collection name hash) so the same insert sequence always yields the same graph.

Option 1 fixes the flake without changing production behaviour. Surfaced by the new .NET Framework test job from #2903, which is simply one more process in which the dice are rolled; nothing suggests it is Framework-specific.