pub async fn search_vector(
conn: &Connection,
query_vec: &[f32],
model: &ModelName,
top_k: usize,
as_of_valid: Option<&str>,
half_life: Option<Duration>,
) -> Result<Vec<VectorSearchResult>>Expand description
Top-k nearest visible neighbours for query_vec under model (§5.9).
Goes through vector_top_k, which consults the DiskANN index, rather than
scanning the table and sorting: the index is what §9’s “top-10 over 100K
concepts in ≤20 ms” budget assumes, and an ORDER BY vector_distance_cos(…)
over the whole table is linear in the corpus no matter how small k is.
vector_top_k yields base-table rowids, so the distance is recomputed on the
k rows it selects — k distance evaluations, not one per concept.
§top_k is a count, and keeping it one is the whole of the loop
The index chooses its k rows before the visibility predicate can see them,
so a filter applied afterwards returns fewer than k whenever a retired
concept is among them. Letting top_k become a ceiling would be a silent
behaviour change for every existing caller, so the index is asked for a
larger k' instead — the escalation
crate::graph::FilteredVectorSearch already performs against the same
problem, and the inflation CostEstimator::k_prime computes from
selectivity.
It runs only when the first pass comes up short, which is the case where
something was actually filtered out. A corpus with nothing retired — the
overwhelmingly common one — pays one query and no count, which is why the
loop is here rather than a selectivity estimate computed up front: that
would put two COUNT(*)s on every search to serve the case that almost
never arises.
Termination is by exhaustion, not by a retry budget. k' doubles until the
index has been asked for the whole table, and a k' at or above the row
count means what came back is every visible neighbour — a complete
answer, not a truncated one. The row count is read at most once and only on
the escalating path.
§as_of_valid: what was true then, or the corpus (0.13.19, W9.4, F-32)
With an instant, a concept is a result only while its own valid interval contains it — the half-open bound the whole crate uses, and the same clause the visibility predicate already carries, so it costs no extra join. Without one, the statement is byte-for-byte what 0.13.18 issued: an absent knob leaves the mechanism alone (D-155).
It is as_of_valid and not as_of because
crate::graph::TraversalBuilder::as_of_valid split that word into two
axes in 0.13.2, and one spelling per axis is the point of having split it.
Transaction time is deliberately not offered here: reading the index as
it stood at a past recorded_at would mean searching vectors that have
since been replaced, and the DiskANN index holds one row per concept with no
history to search. A caller who wants that asks the ledger, not the index.
The escalation above needs no adjustment for it. It keys on a pass coming up short and not on why it came up short, so a corpus thinned by valid time re-asks the index exactly as one thinned by retirement does.