feat(cqld4): gate the predicated search on attainable recall and report it beside the strict measure

The workload surveys its ground truth in its derived parameters and refuses to run below min_attainable (default 0.95; 0 for a smoke run), and its verifier reports recall_attainable beside recall. The adapter docs state the regime: survey before loading, report the attainable measure beside the strict one, and choose the profile by what the survey says it can pay out, since selectivity times base count is only a first guess.
This commit is contained in:
Jonathan Shook committed 2026-09-16 07:04:49 +00:00
1 parent 6ae106a5eb
commit b4bfb22800
2 files changed
+62 -5

No files matched your search

@@ -14,6 +14,7 @@ description: |
similarity: {{similarity == dataset(spec).attributes().getOrDefault('distance_function','COSINE').toString().toLowerCase()}}
metadata columns: {{columns == recordColumns(spec, 'metadata_content')}}
predicate forms: {{forms == predicateForms(spec, 'metadata_predicates', 'metadata_content')}}
ground truth: {{coverage == requireAttainableRecall(spec, paramOr('groundtruth','prefiltered') + '_neighbor_indices', paramOr('min_attainable','0.95'))}}
The rampup phase stores each base vector with its metadata record,
bound field by field through a prepared statement: RecordField reads
@@ -44,11 +45,28 @@ description: |
exact top-k among the base vectors that satisfy the predicate — since
SAI restricts before it ranks. Pass groundtruth=postfiltered to
compare against the top-k of an unfiltered search with the
non-matching neighbors removed instead. When fewer than k base
vectors satisfy a predicate, the ground-truth row is padded with -1
sentinels, which count as misses, so recall on very selective
predicates reads low by construction. The unfiltered search block is
included for the baseline comparison.
non-matching neighbors removed instead.
Reading recall. When fewer than k base vectors satisfy a predicate,
the ground-truth row is padded with -1 sentinels, which the strict
recall counts as misses, so on a slice too small for its predicates
recall reads low by construction — not because the search missed.
Two things keep that from cascading into a false failure. First, the
ground-truth facet is surveyed before anything is loaded (the
coverage line above): the recall@k ceiling a perfect search would
score is printed, and the run is refused when it is below
min_attainable (default 0.95) — pass min_attainable=0 for a smoke
run on a padded profile. Second, the verifier reports
recall_attainable beside recall: the share of the neighbors that
exist which were found, so a padded profile still reads through.
A profile whose predicates all reach k matches reads at a ceiling of
1.0, and its strict recall is the number to hold a system to. A
uniform set with selectivity x base_count comfortably above k is the
place to look — tessera's 1m-uniform-2-1e-3 is complete, while its
300k-uniform-2-1e-3 leaves a fifth of its queries short — but the
survey, not the arithmetic, decides; a mixed ladder at a small size
is a smoke test. The unfiltered search block is included for the
baseline comparison.
Operational knobs stay conventional: keyspace, table, rf, and cl are
TEMPLATE parameters with the usual defaults. The base facet is warmed
@@ -227,6 +245,7 @@ blocks:
k={{@maxk}}
relevancy=new io.nosqlbench.nb.api.engine.metrics.wrappers.RelevancyMeasures(_parsed_op)
relevancy.addFunction(io.nosqlbench.engine.extensions.computefunctions.RelevancyFunctions.recall("recall",k));
relevancy.addFunction(io.nosqlbench.engine.extensions.computefunctions.RelevancyFunctions.attainable_recall("recall_attainable",k));
relevancy.addFunction(io.nosqlbench.engine.extensions.computefunctions.RelevancyFunctions.precision("precision",k));
relevancy.addFunction(io.nosqlbench.engine.extensions.computefunctions.RelevancyFunctions.F1("F1",k));
relevancy.addFunction(io.nosqlbench.engine.extensions.computefunctions.RelevancyFunctions.reciprocal_rank("RR",k));
@@ -145,6 +145,44 @@ console before the first cycle — the number of prepared statements the
run will hold, and how many predicates take each — so a run declares
its shapes before it starts.
### Reading recall on a predicated search
A filtered ground truth holds, per query, the top-k among the rows
that satisfy its predicate. On a slice too small for the predicate —
a selectivity of one in a million against a hundred thousand rows —
fewer than k rows satisfy it, and the row is padded with `-1`
sentinels that no search can return. The strict `recall` divides by k
regardless, so such a profile reads low by construction, and a regime
that holds a system to that number fails for a reason that has
nothing to do with the system.
Three rules keep the reading honest, and `cql_vector_predicated`
follows all three:
- **Survey before loading.** `groundTruthCoverage(spec, facet)` reads
the ground truth alone — a few megabytes — and reports how many
queries have all k neighbors, how many have none, and the recall@k
ceiling a perfect search would score. `requireAttainableRecall(spec,
facet, minimum)` refuses the run below the ceiling you will accept,
before a million rows are loaded for a number that cannot be read;
`min_attainable=0` lets a smoke run through.
- **Report the attainable measure beside the strict one.**
`RelevancyFunctions.attainable_recall("recall_attainable", k)` leaves
the sentinels out of the denominator: the share of the neighbors that
exist which were found. A padded profile whose `recall` equals its
ceiling and whose `recall_attainable` is 1.0 found everything there
was.
- **Choose the profile by what it can pay out.** A uniform predicate
set whose `selectivity x base_count` is comfortably above k is where
to look — `1m-uniform-2-1e-3` on a base of a million has a ceiling of
1.0, and its strict recall is the number to hold a system to — but
the survey decides, not the arithmetic: `300k-uniform-2-1e-3` nominally
reaches 300 matches per predicate and still leaves a fifth of its
queries short, at a ceiling of 0.988. A mixed ladder that runs far
below the slice size is a smoke test of the binding path, and the
selector says which candidates to survey:
`dataset:family=uniform,base_count>=1m`.
## CQLd4 Op Template Examples
```yaml