Repository navigation
Redis-wire TAG semantics (; in scalar values, case folding) + deferred KiviDB test/CI items from #375 #377
Description
Follow-ups from the #375 / #381 reviews: TAG semantics on the Redis-wire engines, plus the deferred KiviDB test and CI items. #381 (KiviDB 1.1.2, SEPARATOR ;) is merged; #375 adds TAG value escaping on top.
A. ; inside a scalar TAG value
Redis, Valkey, Dragonfly and KiviDB (>= 1.1.2, #381) declare every TAG field TAG SEPARATOR ; (redis.rs ~L500, valkey.rs, dragonfly.rs, kividb.rs::tag_declaration). The reason is that a bare TAG splits on ,, and real values contain commas. The cost is that a ; inside a scalar keyword/uuid value becomes a separator:
- Query side: a filter value
a;bmatches nothing (a silent under-match). On KiviDB, kividb: exact TAG filter values on 1.1.2 (escaping + live fixture), follow-up to #381 #375'skividb_caps::tag_value_problemmakes this a hard error. Redis, Valkey and Dragonfly still send it. - Corpus side: a stored
a;bindexes asa+b, so its own value can't reach it and a filter onamatches it (a silent over-match). This is open on every engine, and no query-side check can see it.
Measured exposure on shipped data (every dataset with a TAG-typed field, streamed):
| dataset | query filter values with ; |
corpus values with ; |
|---|---|---|
| h-and-m-2048-angular-filters (13 TAG fields) | 0 / ~10k | 0 |
arxiv-titles-384-angular-filters (labels, submitter) |
0 / 6.6k | 12 submitter values (HTML entities, e.g. Mikołaj Bojańczyk), never queried |
| random-match-keyword-100-angular-filters | 0 / 16.6k | 0 |
msmarco-cohere-1024-100K-cosine (docid, url, category) |
no filtered vector queries | 10 urls, never queried |
Over-match: none of arxiv's 2,928 distinct submitter query values equals any of the 20 ;-fragments of those 12 stored values (compared case-insensitively and trimmed). Nothing shipped is affected today; this is a latent hazard for new datasets.
Proposal:
- In the Redis, Valkey and Dragonfly filter builders, refuse a TAG filter value that contains the declared separator, as KiviDB now does (ROOT CAUSE: no engine distinguishes "no filter" from "filter that parsed to nothing" — silent unfiltered runs scored against filtered ground truth #219: "error rather than under-match").
- At upload, for scalar TAG fields, refuse or count stored values that contain the separator. Alternatively, pick a separator that can't occur in the data and record it in the results.
- Add a fixture with a
;-bearing scalar value for each engine, and mutation-check each guard. - Run
tests/common::write_comma_keyword_projectin the Redis, Valkey and Dragonfly suites too. No live test there goes red if one of them reverts to a bare TAG, the declaration that collapsed KiviDB recall to 0.270 in kividb: exact TAG filter values on 1.1.2 (escaping + live fixture), follow-up to #381 #375's mutation table. - Consider the
--skip-uploadFT.INFO separator read-back (kividb_caps::check_index_tag_separators) for the Redis family as well.
B. TAG matching is case-insensitive, but ground truth matches exactly
On KiviDB (SEPARATOR ;, measured on 1.0.5 and 1.1.2) and on the Redis vector layout, @t:{ginha} matches giNHA and GINHA, and stored edge whitespace is trimmed. Ground truth uses exact matching, so these filters over-match:
- h-and-m: 31 of 10,000 queries. For example,
department_name"Jersey fancy" matches 3,148 docs instead of 1,397, "Denim trousers" 900 instead of 397, andproduct_type_name"Dog wear" 27 instead of 7. - arxiv
submitter: 30 queries, covering 25 distinct values. For example, "Zhi-Zhong Xing" matches 97 docs instead of 56. - random-match-keyword and
labels: no collisions.
Possible fix: declare TAG SEPARATOR ; CASESENSITIVE on both engines. On KiviDB 1.0.5, @t:{giNHA} returned 1 and @t:{ginha} returned 0; re-measure on 1.1.2. Then require it in the reused-index FT.INFO read-back and record it in params.index_layout.
C. Deferred from the #375 final review (KiviDB tests and CI)
- Unmeasured characters inside a TAG value (f1). kividb: exact TAG filter values on 1.1.2 (escaping + live fixture), follow-up to #381 #375 hard-errors on an empty value and on interior whitespace other than a space or a control character, because the 1.1.2 probe covered only the 32 ASCII punctuation characters and space. A
zz<c>zzprobe on 1.1.2 for each of\t \n \r \x0B \x0CU+00A0 U+3000, and an empty stored TAG, would show whether any of them can be allowed. No shipped query value has any of these. - The CI log doesn't record which server answered (f2 opt 2).
kividb/kividb:v1.1.2-fullis a movable tag, and test output is captured. Pin the image by digest or printkividb_versionbefore the suite. - The mutation stopped before the per-value count loop (f2 opt 1). With escaping reverted,
test_binary_kividb_tag_values_exact_countsfailed at the engine run (--fail-on-dropped-queries), not at the exact-id loop. Add a case that runs the loop on its own. - The pre-release gate (f3 opt 1).
1.1.2-rc1passes on its core version and was never measured. Decide whether to require a release build atTAG_MIN_VERSION. - A live red test for
search()'s version check under--skip-upload(f5 opt 1). The legacy 1.0.2 job is gone. A unit test that drives the guard with aKividbServerparsed from akividb_version:1.0.5reply and a labels dataset would cover it without bringing back a legacy image. - Re-labelled comments that were never re-measured (f5 opt 2).
tests/harness_invariants.rs("kividb 1.1.2" keyspace sample) andtests/common/mod.rs("1.1.2 reportsloading"). Re-measure them on 1.1.2, or label them as 1.0.2 samples. - Docker Hub pull limits (f5 opt 3). If anonymous pulls start to bite,
ghcr.io/kividbio/kividbis a drop-in replacement. - The exact-count fixture asserts >= 1.1.2, not exactly 1.1.2 (f7 opt 2). Its panic text names 1.1.2. Make it version-neutral when the image is bumped.
Related: #376 (KiviDB TEXT queries that are still refused).
Activity
docs(kividb): measured shipped-data exposure for the TAG separator
fcostaoliveira commentedon Oct 29, 2026
Added from the #375 reviews: TAG matching is case-insensitive, but ground truth matches exactly. README.md (KiviDB note) points here.
KiviDB
TAG SEPARATOR ;(measured on 1.0.5) ignores case and trims spaces. For example,@t:{ginha}matchesgiNHAandGINHA, and@t:{ sp }matchessp. The dataset ground truth uses exact matching, so these filters return extra documents:department_name"Jersey fancy" matches 3,148 docs instead of exactly 1,397.product_type_name"Dog wear" matches 27 instead of 7.submitter: 30 queries over-match. For example, "Zhi-Zhong Xing" matches 97 docs instead of exactly 56.Not KiviDB-only. The Redis engine's
vectorlayout behaves the same way (redis.rs ~500, workload.rs ~62-66), so KiviDB and Redis stay comparable, but both report recall on these queries against exact ground truth.Possible fix: KiviDB 1.0.5 accepts
TAG SEPARATOR ; CASESENSITIVE, and it matched exactly (@t:{giNHA}→ 1,@t:{ginha}→ 0). RediSearch supportsCASESENSITIVEtoo. The fix:check_index_tag_separators/ FT.INFO);params.index_layout.Before the fix: re-measure it on KiviDB 1.1.2.
🤖 Generated with Claude Code
[-]Redis-wire engines: a `;` inside a scalar TAG value is split by `SEPARATOR ;` (query under-match, corpus over-match)[/-][+]Redis-wire TAG semantics (`;` in scalar values, case folding) + deferred KiviDB test/CI items from #375[/+]fcostaoliveira commentedon Oct 29, 2026
C, added from the final #375 bot pass (comment 6496170566):
match_anyarm differs between engines. KiviDB now refuses an empty TAG value anywhere, including one arm of amatch_any, as a hard error for the whole dataset (kividb.rs~947-949,kividb_caps::tag_value_problem). redis.rs skips the empty arm. Both are loud or harmless today, and no shipped query has an empty value. Whoever next touches redis.rs's empty-string handling should pick one behaviour for both engines.(…)=>[KNN $K @vec $vec_param]. Today the hybrid shape is covered only byrecall >= 0.95(1.0 in practice) in the same runs. Add oneFT.SEARCHper value with a KNN tail andK >= 38, since divergence 1 showed KiviDB's parser can treat the two positions differently.🤖 Generated with Claude Code
fcostaoliveira commentedon Oct 29, 2026
tag_value_problemlets them through unescaped. No shipped query value contains one. Either probe them on 1.1.2, or refuse them alongside the interior-whitespace check. (From the final kividb: exact TAG filter values on 1.1.2 (escaping + live fixture), follow-up to #381 #375 review, g1.)kividb: exact TAG filter values on 1.1.2 (escaping + live fixture), f…