Skip to content

Search & Fusion

The Search builder assembles a hybrid query — a lexical (text) channel, one or more vector channels, or both — and a fusion strategy that combines them. Every builder method forwards exactly one native call and returns self for chaining; the facade never re-walks the query in Python.

from valise import Search

q = (
    Search()
    .text("body", "quick fox")        # scorer defaults to Bm25(k1=1.2, b=0.75)
    .vector("dense", query_vec)       # rerank defaults to Rerank.ACCURATE
    .top_k(10)                        # default 10
)
result = store.search("kb", q)

Defaults live in the Rust core (the single source of truth): BM25 text scoring, accurate vector rerank, and Rrf(60) fusion when two or more channels are present. Override any of them explicitly:

from valise import Search, Bm25, Rerank, Rrf, Weighted

Search().text("body", "quick fox", Bm25(k1=0.9, b=0.4))
Search().vector("dense", query_vec, Rerank.FAST)   # keep quantized scores
Search().text(...).vector(...).fuse(Weighted(text=0.3, vector=0.7))

.vector(...) may be called multiple times for multi-vector / multimodal search; each call becomes a fusion channel. At most one text channel in v1.

Strict typed value objects — raw strings are rejected

The text scorer, fusion strategy, and rerank precision are typed value objects / enums. Passing a raw string raises TypeError.

Text scorers (the TextScorer union — referenced inline here, not a public class): Bm25(k1, b), TfidfCosine(tf_mode), TfidfCosineApprox(tf_mode), CountCosine(), CountCosineApprox(), Dice(), Overlap(), Containment().

Fusion (the Fusion union): Rrf(k) for reciprocal-rank fusion, or Weighted(text, vector) for a linear weighted blend of the two channel kinds.

Rerank precision: the Rerank.FAST / Rerank.ACCURATE enum on the vector channel. ACCURATE (the default) reranks the candidate set with full-precision vectors; FAST keeps the quantized scores.

# Correct — typed objects:
Search().text("body", "fox", Bm25()).fuse(Rrf(60))

# Rejected — raw strings raise TypeError:
# Search().text("body", "fox", "bm25").fuse("rrf")

Searching a vector field before its first vector commit raises NotCalibratedError — see Schemas & Spaces.

SearchResult

search returns a SearchResult: parallel keys and scores built from the native (keys, scores) tuple in one native call. The scores ndarray is the moved float32 array, never copied.

len(result)        # number of hits
result.keys        # list of keys (native list, as-is)
result.scores      # float32 ndarray (moved, zero-copy)
result[0]          # a Hit(key, score, collection)
result[:3]         # `SearchResult` (sliceable; slicing returns another `SearchResult`) (slicing supported)
for hit in result: # lazy iteration; Hit objects materialized on access
    print(hit.key, hit.score)

Each Hit is a frozen dataclass with key, score, and collection. Higher score is better. Hits are materialized lazily on indexing/iteration — there is no eager per-hit loop over the result set.

Scores are not reproducible

When a vector channel is present, fused scores depend on the lossy codec and an RNG, so exact score values are not reproducible. Assert on counts and keys, not on score equality.