foley.index.search#

Hybrid retrieval: reciprocal rank fusion over a vector list and a keyword list.

Pure orchestration + fusion. This module depends only on the small index protocols (foley.index.protocols) — never on torch/lancedb/ sqlite_vec — so the fusion rule is one SSOT exercised identically by every backend and directly unit-testable.

Why rank fusion (report 04 §3.2): dense CLAP cosine lives in [-1, 1] while BM25 scores are unbounded, so averaging them lets one drown the other. Reciprocal Rank Fusion (RRF; Cormack, Clarke & Büttcher, SIGIR 2009) instead fuses on rank position:

score(d) = Σ_rankers  1 / (k + rank_r(d))          # k = 60 (standard)

A document ranked high by either the vector list or the BM25 list floats up; k=60 damps the long tail. It is one line, parameter-light, and robust — the right first-stage fusion for a mix of semantic captions and terse literal tags.

The concrete backends (LanceDB, sqlite-vec) can fuse natively in-engine; foley deliberately fuses here instead so the ranking is byte-identical across backends and gated by the eval harness (report 08) rather than an engine’s internals.

Module Attributes

RRF_K

Standard RRF damping constant (Cormack et al., SIGIR 2009).

DEFAULT_CANDIDATE_K

Per-ranker shortlist depth pulled from each index before fusion.

Functions

fuse_hits(vector_hits, keyword_hits, *, k[, ...])

RRF-fuse a vector ranker's hits with a keyword ranker's hits.

hybrid_search(query, *, embedder, vindex, kindex)

Embed query, run the vector + keyword rankers, and RRF-fuse them.

reciprocal_rank_fusion(ranked_id_lists, *[, k])

Fuse several ranked id lists into one, by reciprocal rank.

vector_search(qvec, *, vindex[, k, where])

Pure audio<->audio (or clip->library) vector search — no keyword leg.

Classes

FusedHit(id[, rrf_score, clap_score, bm25_score])

One fused retrieval hit: an id plus the scores that produced it.

foley.index.search.DEFAULT_CANDIDATE_K: int = 50#

Per-ranker shortlist depth pulled from each index before fusion. Fusing deeper lists than the requested k lets a doc ranked mid-list by one ranker but top by the other still surface.

class foley.index.search.FusedHit(id, rrf_score=None, clap_score=None, bm25_score=None)[source]#

Bases: object

One fused retrieval hit: an id plus the scores that produced it.

The raw component scores are carried through (not just the fused rank score) so the façade can stamp them onto a Candidate (clap_score / bm25_score / rrf_score) for display and debugging.

id#

The sound id.

rrf_score#

The fused RRF score (None for a pure-vector search).

clap_score#

Cosine similarity from the vector ranker (None if the id appeared only in the keyword list).

bm25_score#

BM25 score from the keyword ranker (None if the id appeared only in the vector list).

foley.index.search.RRF_K: int = 60#

Standard RRF damping constant (Cormack et al., SIGIR 2009). Larger => flatter.

foley.index.search.fuse_hits(vector_hits, keyword_hits, *, k, rrf_k=60)[source]#

RRF-fuse a vector ranker’s hits with a keyword ranker’s hits.

Parameters:
  • vector_hits (list[tuple[str, float]]) – [(id, cosine_similarity), ...] best-first (from knn()).

  • keyword_hits (list[tuple[str, float]]) – [(id, bm25_score), ...] best-first (from bm25()).

  • k (int) – Number of fused hits to return.

  • rrf_k (int) – The RRF damping constant.

Return type:

list[FusedHit]

Returns:

The top-k :class:`FusedHit`s, each carrying its raw component scores.

Embed query, run the vector + keyword rankers, and RRF-fuse them.

Parameters:
  • query (str) – The natural-language query.

  • embedder (Embedder) – Text<->audio embedder (its embed_text produces the query vector).

  • vindex (VectorIndex) – The vector index (CLAP KNN).

  • kindex (KeywordIndex) – The keyword index (BM25).

  • k (int) – Number of fused results to return.

  • candidate_k (int) – Shortlist depth pulled from each ranker before fusion.

  • rrf_k (int) – The RRF damping constant.

  • where (Optional[dict]) – Optional metadata push-down passed to both rankers.

Return type:

list[FusedHit]

Returns:

The top-k fused :class:`FusedHit`s.

foley.index.search.reciprocal_rank_fusion(ranked_id_lists, *, k=60)[source]#

Fuse several ranked id lists into one, by reciprocal rank.

Parameters:
  • ranked_id_lists (list[list[str]]) – Each element is a list of ids in descending-relevance order (best first). Lists may overlap and may differ in length.

  • k (int) – The RRF damping constant (default RRF_K = 60).

Return type:

list[tuple[str, float]]

Returns:

[(id, fused_score), ...] sorted by fused score descending, ties broken by id ascending (so the fusion is fully deterministic).

Pure audio<->audio (or clip->library) vector search — no keyword leg.

Used by SoundLibrary.similar and by searching with a reference clip. Hits keep their cosine similarity in clap_score and preserve the index’s own descending-similarity order (rrf_score is left None — there is no fusion).

Parameters:
  • qvec (ndarray) – An already-L2-normalized (dim,) query vector.

  • vindex (VectorIndex) – The vector index.

  • k (int) – Number of neighbours to return.

  • where (Optional[dict]) – Optional metadata push-down.

Return type:

list[FusedHit]

Returns:

Up to k :class:`FusedHit`s in descending-similarity order.