vd.search#
Advanced search utilities for vd.
Provides functions for multi-query search, search result merging, and other advanced search patterns.
Functions
|
Brute-force BM25 lexical search over a vd collection's stored |
|
Remove duplicate results. |
|
Hybrid (dense + lexical) search that works on any vd Collection. |
|
Search with multiple queries and combine results. |
|
Combine multiple result lists using Reciprocal Rank Fusion. |
|
Find documents similar to a specific document. |
|
Search with relevance feedback (Rocchio algorithm). |
Classes
|
A reusable Okapi BM25 index over a vd collection's stored |
- class vd.search.BM25Index(collection, *, filter=None, tokenize=<function _tokenize>)[source]#
Bases:
objectA reusable Okapi BM25 index over a vd collection’s stored
text.Builds the query-independent term statistics — per-document token lists, document frequencies, document lengths, and the mean length — once in
__init__(), then answers many queries against them viasearch(). This is the build-once / query-many companion tobm25_lexical_search()(which builds a throwaway index for a single query): for batch evaluation or any repeated querying of the same collection it turns an O(N · Q) workload (re-tokenizing every document on every query) into O(N + scoring · Q).Construction is O(N) in the collection size; each
search()is O(matching documents). Fine for prototypes and collections up to ~100k documents; for larger workloads switch to a backend with a native text index (weaviate, elasticsearch, redis, lancedb, …).- Parameters:
collection (
Collection) – Any vd Collection (or mapping-likeid -> objexposing.textand.metadata). Documents whosetextis empty contribute zero score and are dropped at build time.filter (
Optional[dict]) – Canonicalvdmetadata filter, applied once at build time (viavd.filters.matches_filter()) so the index covers only the matching documents and its statistics reflect that subset.tokenize (
Callable[[str],list[str]]) – Tokenizer (default: lowercased\w+tokens). Pass a custom one for stemming, CJK, etc.
Examples
>>> import vd >>> c = vd.connect('memory').create_collection('t', dimension=2) >>> c['a'] = vd.Document(id='a', text='the quick brown fox', vector=[1.0, 0.0]) >>> c['b'] = vd.Document(id='b', text='lazy dog sleeps', vector=[0.0, 1.0]) >>> index = vd.BM25Index(c) >>> index.search('quick fox', limit=1)[0]['id'] 'a'
- search(query_text, *, limit=10, k1=1.5, b=0.75)[source]#
Okapi BM25 scores for
query_textover the indexed documents.Returns result dicts in the same shape as
Collection.search()—{"id", "text", "score", "metadata"}— sorted by descending score.k1/bare the standard Okapi hyperparameters (scoring-time, so one index can be queried with different settings).
- vd.search.bm25_lexical_search(collection, query_text, *, limit=10, filter=None, k1=1.5, b=0.75)[source]#
Brute-force BM25 lexical search over a vd collection’s stored
text.Builds a throwaway
BM25Indexovercollectionand runs a single query against it. Used as the default lexical side ofhybrid_search()when a collection does not implementSupportsHybrid.Cost is O(N) in the collection size — fine for prototypes and collections up to ~100k documents. **For repeated queries over the same collection, build a**
BM25Indexonce and callBM25Index.search()per query instead of calling this function in a loop — the term statistics are then computed once rather than on every call. For larger workloads, switch to a backend with native hybrid search (weaviate, elasticsearch, redis, lancedb, …) or pass a customlexical_searchcallable tohybrid_search()that consults a real text index.- Parameters:
collection (
Collection) – Any vd Collection. Documents whosetextis empty contribute zero score and are filtered out of the result.query_text (
str) – The lexical query.limit (
int) – Maximum number of results.filter (
Optional[dict]) – Canonicalvdmetadata filter. Applied client-side viavd.filters.matches_filter().k1 (
float) – BM25 hyperparameters. Defaults match the standard Okapi BM25.b (
float) – BM25 hyperparameters. Defaults match the standard Okapi BM25.
- Returns:
Result dicts in the same shape as
Collection.search()—{"id", "text", "score", "metadata"}— sorted by descending score.- Return type:
Examples
>>> import vd >>> c = vd.connect('memory').create_collection('t', dimension=2) >>> c['a'] = vd.Document(id='a', text='the quick brown fox', vector=[1.0, 0.0]) >>> c['b'] = vd.Document(id='b', text='lazy dog sleeps', vector=[0.0, 1.0]) >>> hits = bm25_lexical_search(c, 'quick fox', limit=1) >>> hits[0]['id'] 'a'
- vd.search.deduplicate_results(results, *, key='id', keep='first')[source]#
Remove duplicate results.
- Parameters:
- Yields:
dict – Deduplicated results
- Return type:
Examples
>>> results = [ ... {'id': 'doc1', 'score': 0.9}, ... {'id': 'doc1', 'score': 0.8}, ... {'id': 'doc2', 'score': 0.7} ... ] >>> unique = list(deduplicate_results(iter(results))) >>> len(unique) 2
- vd.search.hybrid_search(collection, query, *, query_text=None, limit=10, filter=None, k_dense=None, k_lexical=None, rrf_k=60, lexical_search=None, egress=None, **kwargs)[source]#
Hybrid (dense + lexical) search that works on any vd Collection.
Dispatches to the collection’s native
hybrid_searchwhen it implementsSupportsHybrid(efficient, server-side). Otherwise fuses the collection’s own densesearch()with a client-side lexical scan (default:bm25_lexical_search()) via **Reciprocal Rank Fusion**.The portable contract is RRF. Backend-specific knobs (weighted blend
alpha, fusion-type variants, native ranker choices) are accepted via**kwargsand forwarded to the adapter when it has a native implementation; they are ignored by the client-side fallback.- Parameters:
collection (
Collection) – Any vd Collection — native-hybrid or not.query (
Union[str,list[float]]) – Query text (embedded by the collection if it has an embedder) or a pre-computed query vector. Whenqueryis a vector,query_textis required.query_text (
Optional[str]) – Explicit text for the lexical side. Defaults toquerywhenqueryis a string.limit (
int) – Number of fused results to return.filter (
Optional[dict]) – Canonicalvdmetadata filter, applied to both sub-searches.k_dense (
Optional[int]) – How many results to fetch from each sub-search before fusion. Default ismax(4 * limit, 50)for each side. Widen for higher recall.k_lexical (
Optional[int]) – How many results to fetch from each sub-search before fusion. Default ismax(4 * limit, 50)for each side. Widen for higher recall.rrf_k (
int) – Reciprocal Rank Fusion constant (typically 60).lexical_search (
Optional[Callable[...,list[dict[str,Any]]]]) – Customlexical_search(collection, query_text, *, limit, filter, \*\*kwargs) -> list[SearchResult]. Defaults tobm25_lexical_search(). Used only on the fallback path; on the native path it is ignored with aUserWarning.egress (
Optional[Callable[[dict[str,Any]],Any]]) – Per-result transform applied before yielding.**kwargs – Extra options. On the native path they are forwarded to the adapter (e.g.
alpha=0.7on weaviate). On the fallback path they are ignored.
- Yields:
dict – Fused result dicts.
scoreis the RRF score on the fallback path, or the adapter’s fused score on the native path.- Return type:
Examples
>>> import vd >>> client = vd.connect('memory') >>> col = client.create_collection('docs', dimension=2) >>> col['a'] = vd.Document(id='a', text='cats purr', ... vector=[1.0, 0.0]) >>> col['b'] = vd.Document(id='b', text='dogs bark', ... vector=[0.0, 1.0]) >>> hits = list(vd.hybrid_search(col, [0.9, 0.1], query_text='cats', ... limit=1)) >>> hits[0]['id'] 'a'
- vd.search.multi_query_search(collection, queries, *, limit=10, combine='interleave', filter=None, **kwargs)[source]#
Search with multiple queries and combine results.
- Parameters:
collection (
Collection) – Collection to searchlimit (
int) – Total number of results to returncombine (
str) –How to combine results:
’interleave’: Interleave results from each query
’concatenate’: Concatenate all results
’union’: Remove duplicates across queries
’best’: Take best results across all queries
**kwargs – Additional search options
- Yields:
dict – Search results
- Return type:
Examples
>>> import vd >>> client = vd.connect('memory') >>> docs = client.create_collection('test') >>> results = vd.multi_query_search( ... docs, ... ["What is AI?", "How does ML work?"], ... limit=10 ... )
- vd.search.reciprocal_rank_fusion(result_lists, *, k=60)[source]#
Combine multiple result lists using Reciprocal Rank Fusion.
RRF is a simple yet effective way to combine rankings from multiple sources.
- Parameters:
- Returns:
Combined and re-ranked results
- Return type:
Examples
>>> results1 = [{'id': 'doc1', 'score': 0.9}, {'id': 'doc2', 'score': 0.8}] >>> results2 = [{'id': 'doc2', 'score': 0.95}, {'id': 'doc3', 'score': 0.7}] >>> combined = reciprocal_rank_fusion([results1, results2])
- vd.search.search_similar_to_document(collection, doc_id, *, limit=10, exclude_self=True, filter=None, **kwargs)[source]#
Find documents similar to a specific document.
- Parameters:
- Yields:
dict – Search results
- Return type:
Examples
>>> import vd >>> client = vd.connect('memory') >>> docs = client.create_collection('test') >>> similar = vd.search_similar_to_document(docs, 'doc1', limit=5)
- vd.search.search_with_feedback(collection, query, *, relevant_ids=None, irrelevant_ids=None, alpha=1.0, beta=0.75, gamma=0.15, limit=10, **kwargs)[source]#
Search with relevance feedback (Rocchio algorithm).
Adjusts the query based on relevant and irrelevant documents.
- Parameters:
collection (
Collection) – Collection to searchquery (
str) – Original queryrelevant_ids (
Optional[list[str]]) – IDs of relevant documentsirrelevant_ids (
Optional[list[str]]) – IDs of irrelevant documentsalpha (
float) – Weight for original querybeta (
float) – Weight for relevant documentsgamma (
float) – Weight for irrelevant documents (negative)limit (
int) – Number of results**kwargs – Additional search options
- Yields:
dict – Search results
- Return type:
Examples
>>> import vd >>> client = vd.connect('memory') >>> docs = client.create_collection('test') >>> results = vd.search_with_feedback( ... docs, ... "machine learning", ... relevant_ids=['doc1', 'doc2'], ... irrelevant_ids=['doc5'] ... )