Skip to main content

vecdiff

Diff two embedding-index snapshots and get graded, evidence-first findings — for codebase vector-DB migrations (model swaps, re-chunking) and index rot audits (orphans, duplicates).

Fully local, deterministic, numpy-only. vecdiff never needs your original vector DB and never touches a network.

한국어 문서

The standard advice for re-embedding is "run the new index side by side (blue/green), compare, then cut over." Nobody ships the compare step — teams throw a few queries at both indexes and go by feel. vecdiff mechanizes that comparison.

$ pip install vecdiff
$ vecdiff old-snapshot/ new-snapshot/ --gate

Why

A code-embedding index has three chronic anxieties:

  1. Model swap — embedding spaces are not comparable vector-to-vector, so "did retrieval quality survive?" was unanswerable by machine.
  2. Re-chunking / re-indexingwhat exactly changed? had no answer at all.
  3. Rot — code moves, indexes sit still. Chunks pointing at dead symbols and accidental duplicates accumulate quietly.

vecdiff answers all three with the same primitive: index-vs-index diff plus per-chunk health checks.

Quickstart

# 1. install (numpy is the only runtime dependency)
pip install vecdiff        # or: uv tool install vecdiff

# 2. generate two demo snapshots (deterministic; B simulates a sloppy re-embed)
python scripts/make_demo_snapshots.py /tmp/vecdemo

# 3. diff them
vecdiff /tmp/vecdemo/snapA /tmp/vecdemo/snapB --markdown report.md --gate
echo $?

You get console findings plus report.md, and --gate turns them into an exit code for CI (see below).

What it checks

# Check What it catches
N1 Neighbor stability Model-swap / re-index regressions: per-chunk top-k neighbor sets compared across snapshots (Jaccard + rank inversions), heavy-loss chunks grouped by directory
N2 Population stats Pipeline breakage early warnings: norm distribution shift, extreme-norm outliers, dimension mismatch (hard error)
N4 Duplicates Re-chunking accidents and boilerplate floods: within-index pairs at cosine ≥ threshold
N5 Constant vectors Pipeline bugs (cached API response, constant fallback, broken batch): one bit-identical embedding reused across many chunk ids
Q1 (--queries-a/b) Canonical queries (supervised) What real retrieval traffic would see: the same query set run through both indexes, per-query top-k overlap + rank inversions
N3 (--paths-manifest) Orphans + ghosts Rot: chunks whose source file no longer exists (file-level: a plain path list); with a symbol-graph manifest (e.g. cartograph for Swift/iOS, kartograph for Kotlin/Android), chunks whose file survives but whose declared symbols are gone

Signal thresholds

vecdiff reports graded signals, never a verdict like "model B is better". Thresholds are stated inline in every finding:

Signal Green Yellow Red
N1 mean neighbor Jaccard ≥ 0.90 ≥ 0.70 < 0.70
N1 heavy-loss chunks (Jaccard ≤ 0.30, i.e. ≥ 70% of top-k lost) < 2% of sampled < 10% ≥ 10%
N2 norm mean shift A→B ≤ 5% ≤ 20% > 20%
N2 extreme-norm outliers (|z| > 3) ≤ 1% < 5% ≥ 5%
N4 duplicate pairs (cosine ≥ threshold) / n 0 < 1% ≥ 1%
N5 largest bit-identical group < 5 members ≥ 5 members ≥ 5% of index
Q1 mean query Jaccard ≥ 0.90 ≥ 0.70 < 0.70
Q1 heavy-loss queries (Jaccard ≤ 0.30) < 2% < 10% ≥ 10%
N3 rot chunks (orphans + ghosts) / path-reported chunks 0 < 5% ≥ 5%

The N2 outlier check is skipped (reported green, with the reason inline) when norm variance is ≈ 0 — e.g. embedders that return pre-normalized unit vectors, where z-scores would be float rounding noise.

--gate exit codes: 0 all green, 1 any yellow, 2 any red. (Note: argparse usage errors — a mistyped flag — also exit 2; check stderr to tell them apart from a red verdict. Hard errors — unreadable snapshot, dimension mismatch — exit 3.)

Sampling

N1 is exact per queried chunk but samples shared ids by default (--sample 0.2, --seed 0 — same seed, same sample, reproducible reports). Use --full for an exact run; cost is O(queried × n × dim).

Case study — model swap over a real code index

371 chunks (~12k lines of real Python/Swift code), same chunks embedded with bge-small-en-v1.5 vs all-MiniLM-L6-v2:

  • mean neighbor Jaccard 0.33; 42.6% of chunks lost ≥ 70% of their top-10 neighbors
  • loss concentrated by directory (e.g. 32/50 chunks in one Swift core module, 22/59 in vecdiff itself) — the spot-check list for cutover
  • N2/N4 confirmed both pipelines mechanically healthy (no scaling bug, no duplicates); gate exit 2 = do not cut over blind

Full story + reproducible commands: docs/case_study. Dogfooding this run also fixed a real tool bug (N2 now skips its norm-outlier check for pre-normalized embedders, with the reason inline).

Snapshot formats

A snapshot is a model-agnostic dump: chunk ids + float32 vectors + metadata (model, dim, chunk_paths, created_at). Loading validates the dump strictly and rejects duplicate ids, dimension/length mismatches, and non-finite (NaN/inf) vectors — a poisoned row would make cosine top-k silently arbitrary, so it fails at load instead of mid-report.

Adapter Input Notes
native (built-in) directory with vectors.npy + meta.json, or a single self-describing .npz the interchange format — export once, diff forever
jsonl (built-in) .jsonl / .ndjson (optionally .gz): {"id", "vector", "path"?, "symbols"?} per line; optional <stem>.meta.json sidecar the universal escape hatch — any vector DB can dump this in a few lines of client code
sqlite (built-in) chunks(id TEXT PRIMARY KEY, vec BLOB) float32 little-endian; optional meta(key, value) table stdlib only
faiss (optional) .index / .faiss requires pip install faiss-cpu; only flat-style indexes whose vectors can be reconstructed

Format is auto-detected from the path; override with --format.

Any other vector DB (Qdrant, Chroma, LanceDB, pgvector, …): dump a JSONL snapshot with your DB's own client, or build one in memory with snapshot_from_arrays(ids=..., vectors=..., model=...) — vecdiff stays numpy-only and never talks to your database. Ready-to-run snippets live in docs/export_recipes.md. One rule above all: ids must be stable across the two snapshots (N1 pairs chunks by id), so never export row numbers.

CI migration gate

# .github/workflows/reindex-gate.yml — run before cutting traffic to the new index
- run: vecdiff snapshots/blue/ snapshots/green/ --full --gate
  # exit 2 (red) blocks the cutover step

vecdiff collects evidence for a human decision; the gate just makes "nobody looked" impossible.

Exit codes: 0 all green, 1 any yellow, 2 any red — and 3 for hard errors (bad snapshot, dimension mismatch, I/O failure), distinct from gate verdicts so CI can tell "the comparison failed" from "the comparison said no".

Method notes & honesty

  • Cross-model comparison is impossible vector-to-vector — embedding spaces are unrelated. That is precisely why N1 compares neighbor-graph structure (per-chunk kNN Jaccard, Vectory-style) instead of coordinates. The method's prior art: Vectory (pentoai), which established kNN-IoU for embedding-space comparison in an ML-experiment-tracking frame; vecdiff operationalizes it for production code-index migrations and adds health checks Vectory does not have.
  • N1 is a function of the candidate pool: neighbor identity depends on the whole index, so removing chunks changes even "clean" chunks' neighbor sets. Heavy-loss concentration by directory (chunk_paths) is where you should look first.
  • N2 is an early-warning system, not a quality metric: a shifted norm distribution usually means pipeline scaling changed, not that retrieval got worse.
  • Judgment stays human. vecdiff's job is findings like "14 chunks lost ≥70% of their top-10 neighbors, concentrated in src/auth (11)" — not scores.

Differentiation

Adjacent tool Difference
Vectory Established the kNN-IoU method (cited). It is an ML experiment tracking toolkit (SQLite + Elasticsearch frame, image/IMDB demos); vecdiff is a production index operations tool: DB adapters, path-concentrated heavy-loss findings, CI gate, rot checks.
Ragas / RAG eval frameworks Evaluate end-task answer quality; do not diff index-vs-index.
MTEB Benchmark leaderboards for models; unrelated to your index.
Vendor migration guides / dual-index blog posts Describe the pattern; none ship the comparison tool.
Vector-index visualizers (e.g. zilliztech's) Visualize ANN search internals; not migration verdicts.

Privacy

Everything runs locally: no network calls, no telemetry, snapshots stay on your machine. Reports contain chunk ids, paths, and similarity numbers — check them into the repo only if you're comfortable with that.

License

Apache-2.0

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

vecdiff-0.4.2.tar.gz (55.8 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

vecdiff-0.4.2-py3-none-any.whl (40.5 kB view details)

Uploaded Python 3

File details

Details for the file vecdiff-0.4.2.tar.gz.

File metadata

  • Download URL: vecdiff-0.4.2.tar.gz
  • Upload date:
  • Size: 55.8 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for vecdiff-0.4.2.tar.gz
Algorithm Hash digest
SHA256 7a5fe34161396b040dc78143bd97b1ece752ac17fff42a66e1413b93a42a57bb
MD5 80cd082e576636b7c5e2b949c161ff8c
BLAKE2b-256 85986706652bd36e39cdfaa8f5b49939455fb28cac387ab66255484c7b4cda29

See more details on using hashes here.

Provenance

The following attestation bundles were made for vecdiff-0.4.2.tar.gz:

Publisher: pypi.yml on ictechgy/vecdiff

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file vecdiff-0.4.2-py3-none-any.whl.

File metadata

  • Download URL: vecdiff-0.4.2-py3-none-any.whl
  • Upload date:
  • Size: 40.5 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for vecdiff-0.4.2-py3-none-any.whl
Algorithm Hash digest
SHA256 721bcd6032c8392f37cdbc551be08e662093c61f1d79372fe429f864aa738ec7
MD5 b08910edcf304afa35dcc9550b1b5079
BLAKE2b-256 e93a6b069291104ad4e67d4464dc6300a08840f7d3ce6885d8cda11efaf5db8b

See more details on using hashes here.

Provenance

The following attestation bundles were made for vecdiff-0.4.2-py3-none-any.whl:

Publisher: pypi.yml on ictechgy/vecdiff

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Release history Release notifications | RSS feed

This release

0.4.2 This release

2 files

0.4.1

2 files

0.4.0

2 files

0.3.0

2 files

0.2.0

2 files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page