vicinity 0.11.0

Approximate nearest-neighbor search
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
# Changelog

All notable changes to this project are documented here. The format follows
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project
follows [Semantic Versioning](https://semver.org/spec/v2.0.0.html). The 0.x
series is unstable: minor bumps may break the public API.

## [Unreleased]

## [0.11.0] - 2026-07-09

### Added

- Added an `updatable_store` example covering add, checkpoint, delete, reopen,
  and search through the optional `store` feature.
- Added `diskann::DiskANNSearcher::search_with_diagnostics`, which reports
  per-query graph reads, vector reads, logical bytes read, and visited-node
  counts for the file-backed search path.
- Added a `store_reopen_diagnostics` example that prints first snapshot-search
  cost with persisted HNSW sidecars present versus after deleting sidecars and
  forcing source-segment rebuilds.
- Added `store::SnapshotIndex`, a read-only checkpoint view that opens the
  segstore manifest and queries persisted HNSW sidecars before falling back to
  one source-vector segment when a sidecar is missing or stale.

### Changed

- `examples/dual_branch_demo.rs` now reports standard-HNSW recall against
  metric-matched brute-force ground truth and labels Dual-Branch recall changes
  as deltas rather than assumed improvements.
- `store::UpdatableIndex` now keys its in-memory per-segment HNSW cache by
  segstore's stable segment ids instead of `Arc` pointers, and prunes stale cache
  entries when compaction/reclaim changes the segment set.
- Store writer searches now build the temporary writer-buffer HNSW from the
  buffer slice instead of cloning buffered vectors first.
- The `store` feature now requires `segstore = "0.5"` and `durability = "0.7.1"`
  for manifest-only snapshot reads and checked sidecar envelopes.

### Fixed

- Corrected the RaBitQ example command and README quantization feature note to
  match the current feature split.
- Store sidecar envelopes now include the `segstore` segment id, so a copied or
  misnamed HNSW sidecar is rejected and rebuilt instead of being accepted for a
  different segment.
- `store::UpdatableIndex::{compact, compact_tiers, reclaim}` now persist sidecars
  for newly merged segments immediately after segstore checkpoints them, instead
  of waiting for the next search to rebuild and write the sidecar lazily.

## [0.10.5] - 2026-06-30

### Added

- `HNSWIndex::to_postcard` and `HNSWIndex::from_postcard` are now available
  with the `persistence` feature, so consumers can persist graph sidecars
  without enabling the full `store` feature.
- The `store` feature persists per-segment HNSW sidecars and loads them on
  restart when the build recipe and live id set still match.

### Changed

- The `store` feature now requires `segstore = "0.4"` for persisted per-segment
  index sidecars.

### Fixed

- Stale, corrupt, or recipe-mismatched sidecars are rejected and rebuilt instead
  of being used for search.

## [0.10.4] - 2026-06-28

### Added

- `store::UpdatableIndex::extend(vectors)`: bulk ingest that syncs the write-ahead
  log once per batch instead of once per vector (each vector is dimension-checked
  and L2-normalized before any is ingested). ~3.4x faster than a loop of `add` on a
  real filesystem (bench `ingest_fs`: 12.6ms vs 3.7ms / 4000 vectors).

### Changed

- The `store` feature now requires `segstore = "0.3"`; the internal `merge_segments`
  takes `&[&Segment]` (segstore 0.3's by-reference signature).

### Fixed

- The `store` bench opts out of `clippy::unwrap_used` (benches legitimately unwrap
  on setup), fixing a pre-existing CI failure.

## [0.10.3] - 2026-06-27

### Changed

- A `delete` now invalidates only the cached HNSW of the segment that holds the
  id, not the whole cache, so one delete no longer forces every segment to
  rebuild on the next query.

## [0.10.2] - 2026-06-27

### Added

- `store::UpdatableIndex::compact_tiers()`: one round of size-tiered compaction
  (merge similarly-sized segments), keeping segment count bounded without a full
  `compact()`.

## [0.10.1] - 2026-06-27

### Added

- `store::UpdatableIndex::reclaim(min_live_ratio)` and `space_amplification()`
  (via the new `Store::live_len`): cheap tombstone reclamation, merging only the
  delete-heavy segments instead of a full compaction.

## [0.10.0] - 2026-06-26

### Changed

- `store::UpdatableIndex` now targets segstore 0.2 and caches the per-segment HNSW
  by the segment's stable `Arc` identity, so a sealed add rebuilds only the one new
  segment instead of the whole corpus. Measured: a single add after 5k vectors took
  the next search from a full ~193ms rebuild to ~5ms (40x -> 1x). Deletes still
  invalidate the cache (a tombstone changes the live filter); they are far rarer
  than adds.

## [0.9.1] - 2026-06-26

### Fixed

- `store::UpdatableIndex` caches the per-segment HNSW indexes and rebuilds them
  only on mutation (add/delete/compact), instead of rebuilding every segment's
  graph on every query.
- `store::UpdatableIndex::add` now returns an error for a vector whose dimension
  does not match the index, rather than silently dropping it from every rebuild.
- Restored the `cdylib` crate-type so maturin builds the `pyvicinity` extension
  (it reported "No Cargo targets to build" without it).

### Changed

- `store` docs no longer claim an "exact" cross-segment merge. HNSW search is
  itself approximate, so the merged result is approximate.

## [0.9.0] - 2026-06-26

### Added

- Optional `store` feature: `store::UpdatableIndex`, an updatable, durable
  multi-segment ANN index backed by
  [`segstore`]https://crates.io/crates/segstore. A per-segment HNSW is searched
  and merged (exact top-k); vectors are L2-normalized on ingest. Incremental
  add/delete plus write-ahead log, checkpoint, compaction, and crash recovery.
  Opt-in; the default build does not depend on segstore.
- `cli` feature: a `vicinity` binary that builds an HNSW index from a JSONL file
  of dense vectors and runs k-NN queries.

## [0.8.2] - 2026-06-11

### Fixed

- Corrected `HNSWBuilder::m_max` and `HNSWIndex::new` parameter docs: `m_max`
  caps neighbors on the base layer (the paper's `M_max0`, default `2 * m`).
  The previous docs claimed it applied to non-zero layers and defaulted to
  `m`; construction has always used `m_max` for layer 0 and `m` above it.
- Corrected `HNSWIndex::search_with_distance` docs: the closure's
  `internal_id` indexes the BFS-reordered vector storage after `build()`,
  not insertion order. Parallel arrays must be built from `raw_vectors()`
  after `build()`.

### Added

- Documented contracts on `HNSWIndex::build` (BFS reorder of internal IDs,
  idempotency, RNG determinism via `HNSWParams::seed`), `search` (error
  variants, `k > n` returns fewer results, `ef < k` silently clamped to `k`),
  and `raw_vectors` (post-build reorder warning, previously only on the
  legacy `vectors_raw` alias).
- Documented the segment persistence compatibility contract on the
  `persistence` module and in the README: 0.6.x legacy fallback,
  future-version rejection, corrupt/truncated input returns errors rather
  than panicking.
- Tests pinning future-format-version rejection, garbage-byte JSON load,
  `k > n` and `k = 0` search results, exact error variants for dimension
  mismatch / add-after-build / empty build, and `build()` idempotency.

## [0.8.1] - 2026-04-30

### Fixed

- `HNSWIndex::search` and `batch_search_mqo` now also normalize the query
  when `auto_normalize=true` for cosine and angular metrics. Previously the
  flag normalized inserts only, so search queries silently produced
  meaningless distances under cosine (the dot-only fast path assumes both
  sides are unit-norm). The Python binding already did this in `prep_query`;
  the Rust API now matches.
- README HNSW snippet ran but raised `InvalidParameter` on cosine because
  the constant `[0.1; 128]` isn't unit-norm. Now uses `auto_normalize(true)`
  on the builder so the literal copy-paste works.
- README IVF-PQ snippet trained 256 clusters on 2 vectors and silently
  returned distance=0 for every result. Reframed as API-shape with a note
  about training-data scale plus a pointer to `examples/ivf_pq_demo.rs`.

### Added

- `pyvicinity` Python bindings on PyPI (abi3 wheel, CPython 3.9+).
  Ships hand-written `.pyi` stubs + `py.typed` (PEP 561), exposes
  `HNSWIndex` + `DistanceMetric`. Verified in CI by `mypy.stubtest`,
  `mypy --strict`, `ruff`, and 25 pytests.
- `examples/python/`: text similarity (sentence-transformers),
  recall@k sweep, and ann-benchmarks/VIBE drop-in wrapper.
- Sentinels `pyvicinity.MISSING_LABEL` (= -1) and
  `pyvicinity.MISSING_DISTANCE` (= +inf) for masking short rows in
  `batch_search` results, matching faiss's convention.
- `auto_normalize` constructor flag normalizes vectors on **both**
  insert and query (closes the hnswlib #592 class of correctness
  bug). Rejected at construction with `L2`/`InnerProduct` since those
  metrics don't have spherical semantics.
- CI gate on `publish-pypi` workflow: aborts unless the source SHA's
  `ci.yml` run is `success`.
- musllinux x86_64 + aarch64 wheel matrix in publish-pypi.

### Changed

- All GitHub Actions in `publish-pypi.yml` pinned to specific minor
  versions instead of mutable major tags.

## [0.8.0] - 2026-04-27

### Changed (breaking)

- Renamed `esg` module to `range_filtered`. The 0.7.x `esg` name implied
  fidelity to the partition-aware structure of Yang et al. 2025
  (arXiv:2504.04018); the shipped implementation is the paper's
  strawman baseline (HNSW + attribute-range post-filter), so the rename
  reflects what the module actually does. A paper-fidelity ESG variant
  is planned as a separate module.
  - `EsgIndex` -> `RangeFilteredIndex`
  - `EsgParams` -> `RangeFilteredParams`
  - feature `esg` -> `range_filtered`
  - module path `vicinity::esg::*` -> `vicinity::range_filtered::*`
  - `RangeFilteredParams::num_checkpoints` removed (was deprecated in
    0.7.3; the partition-aware path that consumed it was never
    implemented).
- `vicinity::spectral` module visibility downgraded from `pub` to
  `pub(crate)`. The Marchenko-Pastur helpers were reachable only via
  `ADSamplingState::new_auto`, which has no production consumer. The
  `rmt-spectral` feature and the `rmt` dep remain available for the
  internal `new_auto` path; the public wrappers `mp_lambda_plus` and
  `count_mp_outliers` are no longer accessible from outside the crate.

### Migration from 0.7.x

```diff
-vicinity = { version = "0.7", features = ["esg"] }
+vicinity = { version = "0.8", features = ["range_filtered"] }

-use vicinity::esg::{EsgIndex, EsgParams};
+use vicinity::range_filtered::{RangeFilteredIndex, RangeFilteredParams};

 let params = EsgParams {
-    num_checkpoints: 16,  // remove
     hnsw_m: 16,
     hnsw_ef_construction: 200,
     ef_search: 100,
 };
```

## [0.7.2] - 2026-04-27

### Changed

- `FreshGraphIndex::insert` no longer rebuilds the per-node `inbound_count`
  array from scratch on every call. The count is maintained as a struct
  field across `add_slice` / `insert` / `build` / `compact` and updated
  per neighbor-list mutation. Per-insert cost was
  `O(max_degree²) + O(n × max_degree)` and is now `O(max_degree²)` -
  flat in `n` once every node is at cap. Measured on Apple Silicon at
  dim=32, max_degree=16: ~120 µs/op at n=10K..50K vs an extrapolated
  ~920 µs/op pre-fix at n=50K (~7x speedup at scale). Both
  reachability invariants (I1 entry-point liveness, I2 in-edge
  survival under reverse-edge prune) survive the refactor.
- `HNSWIndex::save_to_file` is now atomic and durable. Writes to a
  sibling temp file, fsyncs the temp, atomically renames into place,
  then fsyncs the parent directory so the rename survives a crash on
  filesystems that journal data and metadata independently (XFS,
  some ext4 configs, overlayfs). A crash mid-write leaves the prior
  file intact rather than truncated. P-HNSW (MDPI 2025) flagged
  partial-write corruption as a recurring failure mode; this routine
  is the in-tree counterpart to
  `durability::storage::FsDirectory::atomic_write` (which is gated
  behind the `persistence` feature). Cost: ~13 ms/op (durability
  syncs) vs ~0.5 ms/op for the prior non-atomic path; trade-off is
  correctness, dominated by typical save cadence.
- `AcornConfig` docstring now names the selectivity regimes where
  2-hop expansion is load-bearing (2-20%), where it's overhead
  (>20%), and where it hits a recall floor (<2%, fall back to
  `selectivity_search` with `matching_ids`). Doc-only change;
  `selectivity_search`'s Low branch already routes correctly.

### Added

- `acorn_search_with_stats` returns an `AcornStats { two_hop_invocations,
  two_hop_nodes_examined }` struct alongside results, so regression
  tests can assert "the 2-hop branch fired N>=1 times" rather than
  relying on a noisier black-box recall delta. The existing
  `acorn_search` becomes a thin wrapper that discards stats; signature
  unchanged. `AcornStats` is `#[non_exhaustive]` so additional
  counters can be added without bumping major. The wrapper has no
  measurable overhead in release builds (Rust inlines the
  discard-stats indirection; measured 803 ns/op for both entry
  points).
- `tests/hnsw_integration_tests.rs::test_acorn_two_hop_branch_fires_at_sparse_selectivity`
  asserts the 2-hop branch fires at least once at ~2.5% selectivity.
  Verified to catch the gate-disabled regression by toggling
  `enable_two_hop=false` and confirming the assertion fires with
  `two_hop_invocations=0`.
- `src/hnsw/graph.rs::tests::test_delete_with_repair_preserves_self_search_reachability`
  asserts every surviving doc_id is self-search-reachable after
  deleting 60% of nodes via `delete_with_repair`. Existing tests
  stop at 20-30% deletion and check result count or top-k recall;
  this guards the failure mode where many cumulative repair calls
  leave individual live nodes orphaned.
- `src/fresh_graph/mod.rs::tests::build_post_state_every_non_entry_node_has_inbound_edge`
  is a static post-build connectivity guard: every non-entry node
  has at least one inbound edge. Adapted from qdrant's
  `test_graph_connectivity` shape, parameterized over five seeds.
  Catches build-time orphans the dynamic delete-reinsert cycle
  test wouldn't surface.
- `src/hnsw/graph.rs::tests::test_hnsw_save_to_file_uses_rename_not_truncate`
  is a Unix-only inode-based gate guard: truncate-overwrite preserves
  the inode, rename-overwrite replaces it. Verified to catch the
  regression by reverting `save_to_file` to non-atomic and confirming
  this test fails (left==right) while the happy-path roundtrip tests
  still pass.
- `src/hnsw/graph.rs::tests::test_hnsw_build_save_reload_search_equality`
  end-to-end build -> save_to_file -> drop -> load_from_file -> search-
  equality round-trip via the path-based API on a fresh live index.
  Plugs the gap between the in-memory roundtrip (already covered)
  and the v0 fixture decode tests (which pin a static byte sequence
  but don't exercise the writer).
- Three `#[ignore]`'d perf probes (`orphan_protection_scaling_probe`
  extended to n=50K; `save_to_file_overhead_probe`;
  `acorn_stats_overhead_probe`) for one-shot validation of the
  changes above. Same shape as the existing scaling probe; not run
  in CI.

### Dependencies

- No upstream dependency bumps. The session also added a
  `sync_parent_of_path` helper to `durability` (0.6.4) for the
  upstream concern that motivated `save_to_file`'s atomic refactor;
  vicinity's own `save_to_file` inlines the equivalent pattern
  rather than taking the helper as a dep, since `save_to_file` is
  `serde`-gated and `durability` is `persistence`-gated.

## [0.7.1] - 2026-04-26

### Added

- Real v0.6.2-written segment fixture under `tests/fixtures/v0_segment_dim8/`
  (~2.6 KB total) and a regression test
  (`real_v0_fixture_loads_and_searches_correctly`) that loads it via
  the v1 reader and asserts the search ordering matches the original
  v0.6.2 run. Guards the legacy v0 decode path against silent
  regression and documents that 0.6.x → 0.7.x persistence migration
  works on real data. The 0.7.0 test set only proved no-panic on
  random bytes; it did not prove correctness on a real legacy file.
- `tests/edge_cases.rs::search_returns_external_doc_ids_at_high_offset`
  pins the external-`doc_id` contract at `u32::MAX - 200` (catches a
  regression where the search path returns an internal slot index).
- `tests/edge_cases.rs::zero_query_does_not_panic_with_auto_normalize`
  pins the documented zero-query path: search returns finite distances
  or an explicit error, never a panic on the `0 / ||q||` normalization
  step.
- `src/hnsw/graph.rs::tests::test_delete_with_repair_recall_floor_against_ground_truth`
  is a recall-floor companion to the existing
  `test_delete_with_repair_maintains_recall`. The original asserts
  result count and absence-of-deleted-ids; this adds a
  recall-vs-brute-force ground-truth assertion at >= 0.7 after
  deleting 20% of nodes.
- `tests/hnsw_integration_tests.rs::test_acorn_low_selectivity_returns_valid_results`
  guards ACORN at ~2.5% predicate selectivity. An earlier draft
  compared `enable_two_hop` true vs false and asserted a positive
  recall gap; in measurement the gap flipped sign at sparse
  selectivity (2-hop adds candidates that displace better ones in a
  tight beam). Reframed as a sparse-predicate regression guard with a
  0.5 recall floor.
- `src/fresh_graph/mod.rs::tests::delete_reinsert_cycles_preserve_reachability`
  guards two FreshGraph invariants that together close the
  delete-reinsert unreachability failure mode of arxiv:2407.07871:
  (I1) `entry_point` always references a non-tombstoned internal id,
  and (I2) `insert`'s reverse-edge prune never evicts a node whose
  only remaining in-edge would be the one through the neighbor being
  pruned at. Runs 200 cycles on a 60-vector graph and asserts every
  live `doc_id` is self-search-reachable. The assertion message names
  the broken invariant when it fires.
- `src/fresh_graph/mod.rs::tests::orphan_protection_scaling_probe`
  (`#[ignore]`'d, run with `--release --nocapture`) measures per-insert
  cost of the orphan-protection rebuild as `n` grows. Sample on Apple
  Silicon, dim=32: 12 µs/op below max-degree saturation, 300+ µs/op
  once every node hits cap. Validates the rebuild-per-call cost is
  acceptable at FreshGraph's target n; informs whether incremental
  inbound-count maintenance is worth designing.

### Changed

- `DistanceMetric::InnerProduct` doc comment now states that the
  caller is responsible for L2-normalization. Un-normalized
  inner-product ranking is dominated by magnitude, which is rarely
  the intended retrieval behavior (Milvus discussion #32479).
- `FreshGraph::build` and `FreshGraph::insert` now share a single
  `add_reverse_edge_protected` helper. Previously the reverse-edge
  prune was duplicated between the two paths and only `insert` had
  orphan protection. The build path now applies the same I2 invariant
  symmetrically: the initial RNG-prune pass cannot evict a node whose
  only remaining in-edge would be the one being considered.

### Fixed

- FreshGraph delete-reinsert unreachability. Two paired changes:
  - `delete()` now repromotes `entry_point` when it tombstones the
    medoid. Previously the entry-point field was left pointing at the
    deleted internal index, so subsequent searches and inserts rooted
    their beam at a dead anchor. Repromotion prefers a live neighbor
    of the stale entry (preserves graph locality) and falls back to a
    linear scan over live nodes only if the entire neighborhood is
    tombstoned.
  - `insert()`'s reverse-edge prune now computes a per-insert
    inbound-degree count, marks any candidate whose count is `<= 1`
    as orphan-protected, and runs RNG-prune only over the unprotected
    remainder. Without this, repeated cycles can evict a long-existing
    in-edge from every list that referenced it, leaving its target
    unreachable even though the node was never deleted.
  - The two changes are paired because the entry-point fix alone
    leaves the same unreachable set on the regression test (5 of 60
    live ids after 200 cycles). The orphan-protection change is the
    load-bearing one; the entry-point fix is shipped because rooting
    search at a tombstoned anchor is incorrect on its own merits.

## [0.7.0] - 2026-04-26

### Added

- HNSW segment binary persistence now starts with a magic + version header
  (`HNSW_SEGMENT_MAGIC = b"VCNHNSW\x01"` + `FORMAT_VERSION: u32 = 1`).
  Mismatched magic returns `PersistenceError::Format` instead of silently
  decoding garbage; unsupported version numbers return a descriptive error.
  Files written by 0.6.x lack the magic — the load path falls back to a
  legacy v0 decoder, so existing persisted indices round-trip transparently.
- `tests/persistence_robustness.rs::segment_binary` module:
  `loading_corrupt_magic_returns_format_error` (sanity), and
  `proptest_one_byte_corruption_never_panics` (random byte flip in the
  metadata header always produces `Result::Err`, never a panic).

### Fixed

- `docs/GUIDE.md` outlier-detection example referenced a non-existent
  `LidEstimate.category` field. Switched to `LidStats::from_estimates(&est).categorize(lid)`,
  which is what `examples/lid_outlier_detection.rs` already does.
- Three rendered intra-doc-link errors in `src/lemur/model.rs` (`[hidden_dim]`
  was parsed as a link target by rustdoc). All shape annotations now use
  backticked code so they render literally.
- `docs/datasets.md` referenced a non-existent `hdf5` Cargo feature. Updated
  to point at `scripts/download_ann_benchmarks.py` (the actual conversion path).
- `lib.rs` recommendation table claimed DiskANN persistence was mmap-based;
  `src/diskann/disk_io.rs:95` documents it as planned. Now reads "file-based
  save/load; mmap planned".
- README and benchmark-results.md called the GloVe-25 dataset "cosine"; the
  ann-benchmarks dataset and the rendered plot are angular distance.
- README's NSG row claimed a 50K hard cap; the limit is empirical, not
  enforced. Reworded to "build slows above ~50K vectors".

### Changed

- `docs/GUIDE.md` quickstart now uses `HNSWIndex::builder` to match the README
  and the bulk of the examples. The direct `HNSWIndex::new(dim, m, m_max)`
  constructor is still exposed.
- README's Supported Algorithms table now surfaces LSH (cross-polytope) and
  `LsmIndex` (LSM-tree streaming HNSW), both previously implemented but
  undocumented.
- `KD-Tree`, `Ball Tree`, and `RP-Forest` rows are flagged "(experimental)"
  per `src/classic/mod.rs`.

## [0.6.2] - 2026-04-25

### Changed

- CI tightened to `-D warnings` for clippy across the per-feature matrix.
- Fixed six pre-existing clippy issues in production code (uncovered by the
  stricter CI).
- Test code is now allowed to use `unwrap`/`expect`/`needless_update` via
  scoped `#[cfg_attr(test, allow(...))]` in `lib.rs`.

[`6b92ae9`](https://github.com/arclabs561/vicinity/commit/6b92ae9) ·
[v0.6.1...v0.6.2](https://github.com/arclabs561/vicinity/compare/v0.6.1...v0.6.2)

## [0.6.1] - 2026-04-23

### Added

- `publish-pypi` GitHub Actions workflow with OIDC trusted publishing and a
  full wheel matrix.

### Changed

- Python package renamed `vicinity` → `pyvicinity` for PyPI registration.

### Fixed

- IVF-RaBitQ cross-cluster ranking via the qntz typed-edge API (corrected a
  cross-cluster comparability bug introduced when residuals are evaluated
  against different cluster centroids).

[v0.6.0...v0.6.1](https://github.com/arclabs561/vicinity/compare/v0.6.0...v0.6.1)

## [0.6.0] - 2026-04-22

### Added

- Vamana parallel build with batched rayon and deferred pruning. Measured
  9.5x speedup on SIFT-128 (35 min → 3.7 min) and 7.1x on GIST-960; default
  build batch is 4096.
- DiskANN parallel build (same batched-rayon pattern as Vamana).
- NSW parallel build.
- Batched distance computation in HNSW beam search (Faiss pattern). +10% QPS
  at `ef=100` on SIFT-128.
- SymphonyQG-VR (vertex-relative) variant with per-parent residual encoding,
  ported to qntz's type-safe edge API.

### Changed

- Module docs across the crate were updated to drop stale version references
  and machine-specific numbers from public doc comments.
- README opening trimmed to undersell tone (one-line description; no feature
  list in the tagline).

### Fixed

- SymphonyQG-VR cross-space bias: recall jumped from 55-86% to 99.9% at ef=400
  on the standard benchmark after correcting the cross-parent distance
  comparison.
- SymphonyQG-VR cross-parent distance comparability (precondition for the
  bias fix above).

### Removed

- Per-parent VR residual (kept the simpler global-rotation variant after the
  per-parent variant showed no recall benefit).

[v0.5.0...v0.6.0](https://github.com/arclabs561/vicinity/compare/v0.5.0...v0.6.0)

## [0.5.0] - 2026-04-12

Highlights from the 0.5 line: generation-counter visited-tracking in HNSW
search (replacing per-search HashSet allocation), devirtualized distance
dispatch, SymphonyQG search variant, and KD-Tree pruning improvements. See
`git log v0.4.0..v0.5.0` for the full commit list.

[v0.4.0...v0.5.0](https://github.com/arclabs561/vicinity/compare/v0.4.0...v0.5.0)

## [0.4.0] - 2026-04-05

Highlights: ADSampling integration, SQ4 (4-bit scalar quantization) module,
and dependency bumps for `innr`, `clump`, `sbits`, `rankops`. See
`git log v0.3.6..v0.4.0` for details.

[v0.3.6...v0.4.0](https://github.com/arclabs561/vicinity/compare/v0.3.6...v0.4.0)

## [0.3.x] - 2026-03 to 2026-04

The 0.3 line covered the initial public-API stabilization, the
`MetadataValue` enum and Range filter (breaking, in 0.3.5), and a series of
CI-greening commits. Earlier than 0.3 the project was pre-public; consult
`git log v0.3.0..` for full history.

[v0.3.0...v0.3.6](https://github.com/arclabs561/vicinity/compare/v0.3.0...v0.3.6)

[Unreleased]: https://github.com/arclabs561/vicinity/compare/v0.10.5...HEAD