fix(vectorsets): derive the vector-set key per config so concurrent runs stop clobbering each other - #278
Merged
Conversation
…uns stop clobbering each other VectorSets never got the #151-4 per-config namespacing that the rest of the Redis-wire family did. Every config addressed the literal key `idx`, and `configure()` opened with `DEL idx`, so starting a second config did not merely interleave with the first — it deleted the first's entire corpus and rebuilt the same key with its own. Two configs, two datasets, or two concurrent runs on one server were mutually destructive, and nothing named the collision so nothing could detect it. The key is now `derive_index_name("VECTORSETS_INDEX_NAME", "idx", &config.name)` — the same helper, the same `<base>:<sanitized-config-name>` shape, the same `_EXACT` escape hatch and the same defaulting as redis/valkey/dragonfly/kividb. A vector set is one key rather than an index over a keyspace, so there is no `derive_key_prefix` analogue: this name IS the namespace. Every site that named the key moved with it: VADD (batch and the mixed-workload single update), VSIM (search, the mixed path, and the connection-priming query), the `DEL` in both `configure()` and `delete()`, and the `VINFO` in `get_memory_usage()` and `server_metadata()`. `vectorsets.rs` no longer contains the string "idx" anywhere except the default-base argument. `experiment::run`'s startup collision guard now knows "vectorsets", so an exact-pinned sweep of >1 config is rejected before any data is written instead of silently re-collapsing into one shared key. Migration: a vector set left behind by an older run under the bare `idx` key is not found and not deleted. That is accepted — it is a regenerable benchmark corpus, and `--skip-upload` reuse across the boundary would in any case be reading a corpus the run cannot attribute. Delete the stale `idx` key by hand. Verification (live redis:8.8.0, two configs back-to-back on one server, dims 8 and 16 so a surviving key is attributable by content, all read back server-side with VCARD/VDIM/EXISTS rather than by recall): before (master) after config A: VCARD idx:vectorsets-iso-a=0 (VDIM 0) | VCARD idx=400 (VDIM 8) after config B: VCARD idx:vectorsets-iso-a=0 (VDIM 0), VCARD idx:vectorsets-iso-b=0 (VDIM 0) | VCARD idx=400 (VDIM 16) -> one shared key, last writer wins; A's 8-d corpus is gone. after (this branch) after config A: VCARD idx:vectorsets-iso-a=400 (VDIM 8) | VCARD idx=0 after config B: VCARD idx:vectorsets-iso-a=400 (VDIM 8), VCARD idx:vectorsets-iso-b=400 (VDIM 16) | EXISTS idx=0 As a side effect the integration suite is now parallel-safe: at default test threads it went from 0/8 passing to 9/9. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Docker Build ValidationPlatforms tested:
Tests performed:
|
…gh corpus_row_count The textual conflict was one import line. The real conflict was semantic and the merge tools could not see it: #271 added `Engine::corpus_row_count()` to `vectorsets.rs` as `VCARD idx` — the literal key — while this branch was moving every other site to the per-config `idx:<config>`. Merged naively, the corpus verification `--skip-upload` depends on would count a DIFFERENT key from the one the upload and the search use. That is not theoretical. Reproduced live on the merge, seeding the legacy `idx` with exactly the expected 400 rows: Experiment stage: Reuse check — server holds 400 of 400 expected rows → QPS: 1924.5, Recall: 0.0000, Precision@returned: 0.0000, MRR: 0.0000 exit 0 The guard passed on the wrong key, the search ran against an empty per-config key, and a recall-0.0 result file was published under a config name that claims otherwise — a brand-new silent-wrong result created by the merge itself, in the exact area both PRs are about. `corpus_row_count` now probes `self.config.key`, with a comment naming the hazard so a future merge does not re-introduce it. `vectorsets.rs` again contains the string "idx" exactly once: the default-base argument to `derive_index_name`. REVIEWER FINDING (ensure_index_exists): not fixed by adding a second mechanism, because #271 already shipped the right one. VectorSets has no `ensure_index_exists` analogue and `VSIM` on a missing key returns an empty array with no error, so "search succeeded" is not evidence. But `--skip-upload` now runs `check_corpus_reuse_precondition` → `corpus_row_count` → `VCARD <key>`, and 0 rows against a declared 400 is classified `Short` → hard error: --skip-upload: the corpus you asked to reuse is empty or missing — config 'vectorsets-migrate' holds 0 of the 400 rows dataset 'vs-migrate' declares. So the README's promise that "--skip-upload against a missing/mismatched index now hard-errors instead of silently writing a recall 0.0 file" is now TRUE for the fifth member of the family, via the mechanism the other four also route through for their row counts. A `VCARD`-based check inside `vectorsets.rs` would have been a second, redundant gate. New integration test `test_vectorsets_skip_upload_hard_errors_on_a_pre_236_corpus` pins exactly this. It seeds the legacy un-suffixed key with EXACTLY the expected row count, which is what gives it teeth: a `corpus_row_count` reading the old key sees 400 of 400, returns Ok, and the run proceeds — the RED transcript above. It carries a positive control (upload, then `--skip-upload` against the config's OWN corpus must succeed) so it cannot pass by rejecting everything, and it runs over a private base (`idx236mig`) rather than the global `idx`, because `test_vectorsets_two_configs_do_not_clobber_each_other` asserts `idx` is never written and two tests mutating it would race at default test threads — the very property #236 restored. Docs: README's "Per-config index isolation" section now names VectorSets in the heading, lists `VECTORSETS_INDEX_NAME` with the other four `*_INDEX_NAME` knobs and their `_EXACT` rule, describes the single-key shape (no doc-key prefix), gives the pre-#236 cut-off alongside the pre-#151-4 one, and explains why the hard-error promise holds for an engine whose search cannot fail. The `--skip-upload` section's destructive-configure list now reads `DEL <key>` rather than `DEL idx`. Gates on the merge commit (new baseline: master fd92603 = 872): cargo test --lib --bins → 874 passed, 0 failed lib 126 | vector-db-benchmark 746 | generate-dataset 2 | bench_hdf5/jsonl/npy 0 (+2 = the two new index_naming tests) cargo clippy --all-targets -- -D warnings → clean cargo fmt --check → clean cargo test --test integration_vectorsets → 10/10 at DEFAULT test threads (live redis:8.8.0) cargo test --test overhead_invariants → 9/9 Two-config coexistence re-verified on the merged tree, server-side: after A: VCARD idx:vectorsets-iso-a=400 (VDIM 8) | legacy VCARD idx=0 after B: VCARD idx:vectorsets-iso-a=400 (VDIM 8), VCARD idx:vectorsets-iso-b=400 (VDIM 16) | EXISTS idx=0 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…/test debt Follow-up to the merge commit, closing the three reviewer blockers. BLOCKER 2 — get_memory_usage over-reported once configs could coexist. It returns server-wide `INFO memory:used_memory`. That was CORRECT as a per-config figure before this PR: the hardcoded key plus a destructive `DEL` in `configure()` guaranteed exactly one resident corpus, so the server total WAS this config's cost. Making configs coexist silently turned it into the sum over all of them — 2× for two configs, N× for an N-config sweep, and the new coexistence test is exactly that scenario. A metric that was right became wrong as a side effect of a correctness fix, which is the worst way to lose one. `VINFO` carries no memory field (verified live), but the corpus is a single key, so `MEMORY USAGE <key>` attributes it exactly. Published as `index_memory_bytes`, the same field name redis/valkey/dragonfly/kividb already use for their per-index `FT.INFO` figure, so no consumer needs a special case. `used_memory` keeps its shape and meaning as the global secondary, with the caveat spelled out in a comment mirroring redis.rs's. Measured on the merged tree, two 400-doc configs on one server: A alone: index_memory_bytes=287,740 used_memory=1,985,664 B, with A resident: index_memory_bytes=287,740 used_memory=2,319,864 MEMORY USAGE idx:vectorsets-mem-a=287,740 idx:vectorsets-mem-b=287,740 The per-config figure is flat and matches the key; the global one grows. New test `test_vectorsets_index_memory_is_per_config_not_server_wide` pins it and is RED without the fix (`index_memory_bytes` absent → -1 for both configs). Its final assertion deliberately does NOT check that `used_memory` grew between the two runs: that is global state any concurrent test moves either way, and it flaked 1-in-3 in the full suite. It asserts a floor instead (`used_memory > 2 × index_memory_bytes`) — same point, and other corpora landing only make it safer. BLOCKER 1 leftovers — the stale docs and the missing test. `corpus_row_count`'s doc comment still said "The key is the hardcoded `idx` (issue #236): this count therefore cannot distinguish two configs sharing one server", and `engine/mod.rs`'s trait doc still listed VectorSets among the engines with "exactly ONE such object per server, shared by every config". Both were written before this PR and both now assert the opposite of the code. Rewritten: VectorSets is the fifth per-config member, and its count is what makes the `--skip-upload` promise true for an engine whose search cannot fail. `test_vectorsets_corpus_row_count_tracks_the_live_key` is the family test #271 shipped for kividb/mongodb/qdrant/valkey and not for vectorsets — which is why CI could not see the hardcoded probe. Same shape: upload, `VREM` half the corpus behind the tool's back, assert the run hard-errors with "holds 200 of the 400 rows". A constant, or a read of the old shared key, fails it. BLOCKER 3 — no new mechanism added, because #271 already provides one. With `corpus_row_count` reading the derived key, `--skip-upload` against a pre-#236 corpus classifies `Short` and hard-errors before anything is measured. An `ensure_index_exists` analogue would be a second, redundant gate on the same path. Covered by `test_vectorsets_skip_upload_hard_errors_on_a_pre_236_corpus` (added in the merge commit), which seeds the legacy key with EXACTLY the expected row count so a probe on the wrong key would certify 400-of-400 and let the run publish recall 0.0. Docs — the third stale README claim. The "#230 datetime corpora" section still read "`VCARD idx`" and "the engine uses a single hardcoded key (`idx`, issue #236), there is not even a name change for the tool to detect. A stale corpus of the right size is found, the run succeeds, and only the recall number is wrong." Every clause was falsified. It now separates the two cut-offs: a pre-#230 corpus IS found (right size, wrong encoding — the one remaining silent case), while a pre-#236 corpus is NOT found and is rejected, with the reason spelled out: `VSIM` on a missing key returns an empty array with rc=0, so `failed_queries` stays 0 and the empty search is FAST — recall 0.0 at inflated QPS is worse than a plain zero. Gates (baseline: master fd92603 = 872): cargo test --lib --bins → 874 passed, 0 failed lib 126 | vector-db-benchmark 746 | generate-dataset 2 | bench_hdf5/jsonl/npy 0 cargo clippy --all-targets -- -D warnings → clean cargo fmt --check → clean cargo test --test integration_vectorsets → 12/12 at DEFAULT test threads, three consecutive runs, no flake (master, measured the same way in a clean worktree three times: 1 passed / 6 failed) Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…claims, and correct this PR's own test-teeth story Claim accuracy only — no behaviour change. Every item below is a place where the prose asserted more than the code delivers, including two I wrote myself. BLOCKER 1 — the fourth stale README place, twin of one I had already fixed. README's "cardinality is not identity" paragraph still read: "Only the FOUR Redis-wire engines address a per-config index and keyspace … MongoDB, pgvector, Elasticsearch/OpenSearch, Qdrant, Milvus AND VECTORSETS each have ONE corpus object per server, shared by every config and every dataset — so a full-size corpus uploaded by a sibling config … passes." That is verbatim the assertion I rewrote in `engine/mod.rs`'s trait doc. I fixed the Rust twin and missed the Markdown one, then claimed in the PR body that all three stale places were fixed; there were four. Post-PR it was false three ways: VectorSets is per-config, a sibling's corpus does NOT pass (this PR's own `test_vectorsets_skip_upload_hard_errors_on_a_pre_236_corpus` proves it), and "four" is "five". It also contradicted my new text 50 lines below, which a reader reaches second. Rewritten to five/ten, with the limit that actually survives on the per-config engines spelled out: a sibling CONFIG no longer passes, but the same config re-run against a DIFFERENT DATASET of equal size still does, because the key derives from the config name alone. BLOCKER 2 — the #230 section was chronologically impossible. It said a pre-#230 corpus "is found, the run succeeds, and only the recall number is wrong … the one failure mode left here", while the next paragraph said a pre-#236 corpus is not found and is rejected. `git merge-base --is-ancestor 66fcaaa fd92603` is true: #230's fix predates this branch, so pre-#230 ⊂ pre-#236 and the two halves are mutually exclusive. After this PR the "right size, wrong encoding, silently found" case is unreachable by any released binary. Now scoped: the wrong-encoding case is reachable only by pinning `VECTORSETS_INDEX_NAME_EXACT=1` at the base `idx`, or by `RENAME idx idx:<config>` — both named, and both explicitly flagged as not migration paths this README recommends. S1 — this PR's own account of where its test's teeth are was wrong. Three places (the merge commit message, the test's doc comment, the PR body) credited "seeds the legacy key with EXACTLY the expected row count" as what makes the migration test sharp — the idea being that a probe hardcoded to `idx` would read 400 of 400 and wave the run through. It would not. That test runs over a PRIVATE base (`idx236mig`, chosen so it cannot race the coexistence test over the global `idx`), so a hardcoded probe reads an empty `idx`, gets 0 of 400, and hard-errors — passing the negative half for the wrong reason. Verified by counterfactual on this tree, `corpus_row_count` patched back to `.arg("idx")`, live, `--test-threads=1`: test_vectorsets_corpus_row_count_tracks_the_live_key ... FAILED (:869) test_vectorsets_skip_upload_hard_errors_on_a_pre_236_corpus ... FAILED (:777) 10 passed; 2 failed `:869` is `holds 200 of the 400 rows` — a direct wrong-key detector. `:777` is the POSITIVE CONTROL ("--skip-upload against this config's OWN corpus must succeed"), not any seeded-negative assertion. So the merge hazard is caught by CI, by both tests, but not by the mechanism I named. The doc comment now says so explicitly, including why the seeding still earns its place (it makes the scenario realistic — rejected even with a full-size corpus sitting on the server — rather than merely absent). S3 — the headline new metric was undocumented, under a heading that contradicted it. The "Per-config index isolation" heading gained VectorSets, but the line under it still said memory "is reported per-index via FT.INFO", which VectorSets does not use; and `index_memory_bytes` appeared nowhere in any .md. Added a table giving both fields, their sources (`FT.INFO` vs `MEMORY USAGE <key>`) and their scopes, plus the two consequences a reader needs: `used_memory` from a pre-#236 run and from a current sweep are not comparable quantities, and an orphaned legacy `idx` keeps inflating `used_memory` in every later result file until it is deleted. S4 — `experiment.rs`: both stale comments fixed after all. `:734`'s historical measurement record (`VCARD idx`) and `:797`'s destructive-call list (`DEL idx`) are now `VCARD idx:<config>` and `DEL <key>`, matching the README line I had already updated. Comment-only, two words; #264 merges clean over them (re-checked with `git merge-tree`). My earlier disclosure said `:796` — the `DEL idx` line is `:797`. Test: the memory test now asserts SCALING, not just presence. Every previous assertion was satisfied by a fixed per-key overhead figure, which is the plausible way `MEMORY USAGE` could be meaningless (a module type with no `mem_usage` callback reports bare key overhead). It now compares the 400-element corpus against a 1-element vector set of the same dimensionality created on the same server, and requires >10×. Artifact for a claim that had none. The README asserted "measured 437 → 3413 QPS" for the empty-corpus case with no transcript in the PR. Re-measured on this tree, 400-vector corpus, ef=400 parallel=1, same server, via `--skip-upload --allow-partial-corpus` after deleting the key: FULL VCARD=400 rps=637.55 recall=1.0 failed_queries=0 EMPTY VCARD=0 rps=2679.26 recall=0.0 failed_queries=0 4.2× faster for measuring nothing, `failed_queries: 0` in both. README now carries these numbers instead of the unsourced pair. Master baseline is a RACE, not a number. I reported "1 passed / 6 failed, three times". Re-measured 11 times at default threads, dedicated container, FLUSHALL between runs, at `fd92603`: 0 passed / 7 failed ×3 1 passed / 6 failed ×7 2 passed / 5 failed ×1 So it is 0–2 of 7 depending on interleaving. `mixed_benchmark` is the most frequent survivor but not the only one (`datetime_keyword_schema` also survived once), so "the one that survives" was wrong twice over. This branch: 12/12, three consecutive runs, no variance. Gates (baseline: master fd92603 = 872): cargo test --lib --bins → 874 passed, 0 failed cargo clippy --all-targets -- -D warnings → clean cargo fmt --check → clean cargo test --test integration_vectorsets → 12/12 ×3 at default threads cargo test --test overhead_invariants → 9/9 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Docker Build ValidationPlatforms tested:
Tests performed:
|
Docker Build ValidationPlatforms tested:
Tests performed:
|
1 similar comment
Docker Build ValidationPlatforms tested:
Tests performed:
|
fcostaoliveira
added a commit
that referenced
this pull request
Aug 8, 2026
Merged, not rebased: the branch is published and the standing rule here is to add a commit rather than force-push.
fcostaoliveira
added a commit
that referenced
this pull request
Aug 8, 2026
…corder #278's two new `index_naming` tests resolve through the recorder (`derive_index_name` -> `env_or` / `env_flag`) without holding `test_lock`, so an observation can land between another test's `begin_experiment` and its `snapshot()`. Six consecutive green suites did not disprove it — the window is a few instructions wide — and "measured green" is precisely the evidence that was wrong about this test last time. Closed two ways rather than one: * those tests now take `effective_config::test_lock()`, which any test driving the global recorder must; * `begin_experiment_clears_the_previous_experiments_accumulation` now asserts the keys it SEEDED are gone rather than asserting total emptiness, so a concurrent writer cannot fail it. Equally strong against the mutation, since deleting `reset()` leaves exactly those keys behind — re-verified 5/5 red (3/3 alone, 1/1 in-suite, 1/1 `--test-threads=1`). Total clearing stays covered deterministically by `begin_clears_every_accumulating_field`, which owns a private `Recorder`. These changes were written before the merge commit and lost to a `git checkout HEAD -- src/` used to measure the master baseline; recommitted here rather than amending, since the branch is published. Refs #212
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The bug
vectorsets.rshardcoded the keyidxat every site. It never got the #151-4 /PR #153 per-config namespacing that redis / valkey / dragonfly / kividb have, and
because there was no derived name, there was nothing for the startup collision
guard to detect either.
This is worse than the Redis-family version. VectorSets' whole corpus is one
key, and
configure()opened withDEL idx— so starting config B did notinterleave with config A's data, it deleted config A's corpus outright and
rebuilt the same key with its own.
Scope of the fix, precisely.
derive_index_namekeys offengine_config.namealone, so this fixes the two-configs case. One config across two datasets,
and two concurrent runs of the same config, still share a key — identical to the
Redis family's design, so not a regression, but not covered either.
And even for two configs it is data isolation, not measurement isolation:
configure()calls server-globalCONFIG RESETSTAT, so two concurrent runs nolonger destroy each other's corpus but do still corrupt each other's commandstats
baseline — blinding the failed-call guard that catches a silently-failing
VADD/VSIM. Concurrency was impossible before this PR and is newly reachable, so
the caveat is newly relevant; it is documented at the derivation site. The
used_memoryhalf of that problem is fixed below.The fix
Same helper, same
<base>:<sanitized-config-name>shape, sameVECTORSETS_INDEX_NAME_EXACTescape hatch, same defaulting as the four siblings.No
derive_key_prefixanalogue — a vector set is a single key, not an index overa keyspace of doc hashes, so this name is the namespace.
All eleven sites that named the key moved, since a half-migration is worse than
none:
configure(DEL),upload→vadd_batch(VADD),search(VSIM, queryand the priming query),
search_mixed(VSIM+vadd_single'sVADD),delete(DEL),get_memory_usage+server_metadata(VINFO), andcorpus_row_count(VCARD).vectorsets.rsno longer contains the string"idx"anywhere except the default-base argument.
experiment::run's collision guard now maps"vectorsets" => "VECTORSETS_INDEX_NAME".Two things the merge with #271 forced, and one it created
corpus_row_count— a silent-wrong born from a clean merge. #271 landedEngine::corpus_row_count()for vectorsets asVCARD idx, hardcoded, with a doccomment reading "The key is the hardcoded
idx(issue #236)" — written beforethis PR existed. Git conflicts only on the import line. Resolved naively you get
writes to
idx:<config>and a reuse probe readingidx. Reproduced live on themerge, seeding the legacy key with exactly the expected 400 rows:
Fixed in the merge commit, with a comment naming the hazard. #271 shipped
corpus_row_counttests for redis/kividb/mongodb/qdrant/valkey but notvectorsets, which is why CI could not see this — that test is added here (
VREMhalf the corpus, assert
holds 200 of the 400 rows).get_memory_usage— a metric that was right, made wrong by coexistence. Itreturns server-wide
used_memory. That was correct per-config before this PR:the hardcoded key plus the destructive
DELguaranteed exactly one residentcorpus. Now every config after the first reports the sum.
VINFOhas no memoryfield, but the corpus is one key, so
MEMORY USAGE <key>attributes it exactly —published as
index_memory_bytes, the same field the four FT engines already use.Joining the family's guard — nothing new added, because #271 already gives it.
VectorSets has no
ensure_index_existsanalogue, andVSIMon a missing keyreturns an empty array with rc=0, so
failed_queriesstays 0 and the empty searchreturns fast. Measured on this tree (400 vectors,
ef=400 parallel=1, sameserver, key deleted then
--skip-upload --allow-partial-corpus):4.2× faster for measuring nothing. With
corpus_row_countreading the derivedkey,
--skip-uploadnow routes throughcheck_corpus_reuse_precondition→Short→ hard error, so that run is rejected instead of published. AVCARD-in-searchgate would be a second, redundant mechanism on the same path.The guarantee is conditional, and the README now says so. When the expected row
count cannot be determined (sparse/
h5-multilayouts, unresolvable dataset path),the verdict is
Unverifiable— printsSKIPPED, recordsparams.corpus_reuse.status = "unverified", and lets the run proceed. Inheritedfrom #238/#271, filed separately as #290; scoped here, not fixed here.
Migration
An
idxkey from a pre-#236 binary is not found and not deleted. Accepted andnow loud: the reuse check rejects the run rather than measuring nothing.
DEL idxto reclaim the memory and re-upload. Note it also keeps inflating
used_memoryinevery later result file until deleted.
Verification — server-side, not recall
Two configs back-to-back on one live
redis:8.8.0. All assertions areVCARD/VDIM/EXISTSread off the server; a recall assertion cannot catch this,because two same-shaped corpora produce a plausible recall whichever one answered.
RED on master (test file only,
src/reverted):One key. After A it holds 400 8-d vectors; after B the same key holds 400 16-d
vectors. A's corpus is gone.
GREEN on this branch (verbatim):
(
EXISTS idx == 0is asserted separately.)The load-bearing assertion is
card_a_after_a == n_a— config A's countsnapshotted before B runs. The two fixtures upload at different
dimensionalities (8 and 16) and the
VDIMassertions check them, but that isbelt-and-braces, not what makes the test work: rebuilding both fixtures at dim 8
and relaxing both
VDIMassertions leaves master RED with an identicalleft: 0 / right: 400. Earlier revisions of this description claimed the differingdims were what made a surviving key attributable; they are not.
Five new integration tests plus two
index_namingunit tests. Where the teethactually are, since an earlier version of this body got it wrong: the migration
test runs over a private base, so its exact-count seeding is not what catches a
wrong-key probe. Counterfactual with
corpus_row_countpatched back to.arg("idx"), live:Both catch it; the seeded-negative half does not. Corrected in the test's doc
comment.
Two of the new tests are the unique killer of their mutant:
exact_pin_drops_the_config_suffixfor removing the_EXACThatch (which had zerocoverage anywhere in the repo before this PR), and the startup-rejection test for
deleting the
"vectorsets" => "VECTORSETS_INDEX_NAME"guard row. The memory flooris the sole assertion that kills
index_memory_bytes = used_memory, panicking withserver-wide used_memory (2312344) should dwarf this config's own corpus (2312344). Honest counterweights: the 10× scaling assertion added in the lastrevision kills no mutant the suite did not already kill — the constant-1 case dies
one assertion earlier — so it is defence-in-depth against a server-side property
that cannot be expressed as a source mutation, not a new killer; and
vectorsets_configs_derive_distinct_keysis redundant with the pre-existingderive_index_name_appends_sanitized_configand uses a fake env name, so theliteral
format!("idx:{name}")in the integration tests is what actually pins thewire key.
Side effect: the integration suite is parallel-safe
Every test wrote to the same
idxand eachconfigure()wiped its neighboursmid-flight, which is why CI pins
--test-threads=1. At default test threads onmaster this is a race, not a number — 11 runs at
fd92603, dedicatedcontainer,
FLUSHALLbetween:fd92603, 7 tests)So 0–2 of 7 pass depending on interleaving. This branch: 12/12 at default threads
×3, and 12/12 at
--test-threads=1, withKEYS *empty after every run — theorder-independence is real, not a lucky interleaving. Master carrying this same
test file scores 0/12 at default threads.
The
--test-threads=1flag in.github/workflows/ci.ymlis left alone — stillcorrect, just no longer load-bearing.
Docs
README.mdasserted the opposite of the code in four places, all now fixed:the "Per-config index isolation" heading + knob list; the
--skip-uploaddestructive-configure list (
DEL <key>); the #230 datetime section; and the"cardinality is not identity" paragraph, which still said "only the four
Redis-wire engines" and listed VectorSets among the one-object-per-server engines
— the exact twin of the
engine/mod.rstrait doc, fixed in Rust and initiallymissed in Markdown.
Also: the #230 section was chronologically impossible (it claimed a pre-#230
corpus is silently found, while the next paragraph said pre-#236 corpora are
rejected — and pre-#230 ⊂ pre-#236, since
66fcaaapredates this branch). Nowscoped to the only two things that reach it:
VECTORSETS_INDEX_NAME_EXACT=1atbase
idx, orRENAME idx idx:<config>.index_memory_byteswas documented nowhere outside code comments and the sectionthat now owns it still said
FT.INFO; there is now a table giving both memoryfields, their sources and their scopes, and noting that a pre-#236
used_memoryand a current one are not comparable quantities.
experiment.rs's two stale comments (VCARD idxat :734,DEL idxat :797) arefixed — comment-only; re-checked that #264 still merges clean over them.
Merge state
git merge-treeagainst #264 (feat/record-effective-config-212): zero conflictmarkers. #264 reroutes the
*_INDEX_NAMEreads througheffective_config::env_or/env_flagwhile myexact_pin_drops_the_config_suffixdrives them via
std::env::set_var. Verified rather than assumed — performedthe merge in a scratch worktree and ran it live: 5/5
index_namingunit tests,12/12 integration tests, 931 unit tests total.
Gates
Baseline: master
fd92603= 872.cargo test --lib --bins— 874 passed, 0 failed (lib 126,vector-db-benchmark746,generate-dataset2, threebench_*0). +2 = the newindex_namingtests; the 5 new integration tests are docker-gated.cargo clippy --all-targets -- -D warnings— cleancargo fmt --check— cleancargo test --test integration_vectorsets— 12/12 at default threads ×3, and12/12 at
--test-threads=1cargo test --test overhead_invariants— 9/9Deliberately out of scope
experiment.rs/effective_config.rsbeyond the guard registration and twocomments (feat(results): record the environment knobs and declared config a run actually used #264).
Unverifiablereuse-guard hole — filed as --skip-upload reuse guard is inert when the dataset's row count can't be determined, publishing recall 0.0 at inflated QPS #290.update_countcounting client-side attempts rather than server acceptance —filed as Mixed-workload updates can all land on a key nobody searches, with recall, update_count and update_rps all green #293.
src/vectorsets/{search,upload,configure}.rsstill hardcode"idx". That moduleis dead code that never compiles (
src/lib.rsdoes not declare it;engine/mod.rs:25resolves toengine/vectorsets.rs), and two open PRs areediting it. Filed as src/vectorsets/ is dead code that never compiles — and two open PRs are editing it #291 rather than deleted here.
distinct names collapsing to one token) for any engine — only the
_EXACTpath,which this PR adds. Filed as The #151-4 collision guard's sanitize-collision path is untested for every engine (only the _EXACT path is covered) #294.
the server total between its 1.5× ceiling and its 2× floor (
used_memory/3is2.7× the true value and passes everything). Known, not fixed.
v0/engine/clients/vectorsets/*.pystill hardcode"idx"; a mixed v0/Rustworkflow no longer shares a corpus.
sanitize_tokenis non-injective, and its guard is per-invocation: twosequential single-config runs whose names sanitise together still clobber, as
does
_EXACTplus two sequential runs. Pre-existing and family-wide; all 19shipped
vectorsets-*configs are sanitize-clean.(see Scope above).
Closes #236
Co-Authored-By: Claude Opus 5 (1M context) noreply@anthropic.com