diff --git a/docs/src/js/classes/VectorQuery.md b/docs/src/js/classes/VectorQuery.md index 3f56bb600..4809ebefd 100644 --- a/docs/src/js/classes/VectorQuery.md +++ b/docs/src/js/classes/VectorQuery.md @@ -646,6 +646,38 @@ rerank(reranker): VectorQuery *** +### searchEffort() + +```ts +searchEffort(searchEffort): VectorQuery +``` + +Set the initial IVF search effort to a finite value in [0, 1]. + +The default, null, uses Auto (internally 0.5). Zero starts at the caller +minimum; one searches all partitions within the caller maximum. Later +probing may continue when too few candidates are found. +Any explicit effort, including 0.5, overrides nprobes in either call order. +Explicit minimum and maximum bounds still apply. This is not a recall +guarantee and does not change refinement or HNSW ef. The MemWAL LSM +scanner does not yet support this option. + +#### Parameters + +* **searchEffort**: `null` \| `number` + +#### Returns + +[`VectorQuery`](VectorQuery.md) + +#### Example + +```ts +table.vectorSearch([0.1, 0.2]).searchEffort(0.75).limit(10); +``` + +*** + ### select() ```ts diff --git a/python/python/lancedb/query.py b/python/python/lancedb/query.py index fc9aea9a4..eece390c2 100644 --- a/python/python/lancedb/query.py +++ b/python/python/lancedb/query.py @@ -1734,6 +1734,9 @@ class LanceVectorQueryBuilder(LanceQueryBuilder): Examples -------- + >>> import lancedb + >>> db = lancedb.connect("memory://") + >>> table = db.create_table("effort_vectors", [{"vector": [0.1, 0.2]}]) >>> query = table.search([0.1, 0.2]).search_effort(0.75) """ self._search_effort = search_effort @@ -2562,11 +2565,17 @@ class LanceHybridQueryBuilder(LanceQueryBuilder): Examples -------- - >>> query = table.search([0.1, 0.2]).search_effort(0.75) + >>> import lancedb + >>> db = lancedb.connect("memory://") + >>> table = db.create_table( + ... "effort_hybrid", [{"vector": [0.1, 0.2], "text": "dog"}] + ... ) + >>> query = ( + ... table.search(query_type="hybrid").vector([0.1, 0.2]) + ... .text("dog").search_effort(0.75) + ... ) """ self._search_effort = search_effort - if self._vector_query is not None: - self._vector_query.search_effort(search_effort) return self def minimum_nprobes(self, minimum_nprobes: int) -> LanceHybridQueryBuilder: @@ -3790,7 +3799,15 @@ class AsyncVectorQueryBase: Examples -------- - >>> query = table.vector_search([0.1, 0.2]).search_effort(0.75) + >>> import asyncio + >>> import lancedb + >>> async def build_query(): + ... db = await lancedb.connect_async("memory://") + ... table = await db.create_table( + ... "effort_async", [{"vector": [0.1, 0.2]}] + ... ) + ... return table.vector_search([0.1, 0.2]).search_effort(0.75) + >>> query = asyncio.run(build_query()) """ self._inner.search_effort(search_effort) return self diff --git a/python/python/tests/test_query.py b/python/python/tests/test_query.py index 71c293628..74a67b909 100644 --- a/python/python/tests/test_query.py +++ b/python/python/tests/test_query.py @@ -2284,6 +2284,7 @@ def test_search_effort_probe_budget(tmp_path, monkeypatch, metric, effort, probe data = pa.table( { "id": np.arange(512), + "text": ["dog"] * 512, "vector": pa.FixedSizeListArray.from_arrays(pa.array(vectors.ravel()), 16), } ) @@ -2298,7 +2299,8 @@ def test_search_effort_probe_budget(tmp_path, monkeypatch, metric, effort, probe return table.search(vector).distance_type(metric).search_effort(value).limit(8) def partitions(query): - values = re.findall(r"partitions_searched=(\d+)", query.analyze_plan()) + vector_plan = query.analyze_plan().split("FTS Search Plan:", 1)[0] + values = re.findall(r"partitions_searched=(\d+)", vector_plan) assert values return sum(map(int, values)) @@ -2335,6 +2337,19 @@ def test_search_effort_probe_budget(tmp_path, monkeypatch, metric, effort, probe assert partitions(filtered) <= 2 assert all(row["id"] % 8 == 0 for row in filtered.to_list()) + if metric == "l2" and effort == 0.5: + table.create_index("text", config=FTS()) + hybrid = ( + table.search(query_type="hybrid") + .vector(vector) + .text("dog") + .nprobes(1) + .search_effort(0.5) + .limit(8) + ) + assert partitions(hybrid) == 4 + assert partitions(hybrid.search_effort(None)) == 1 + @pytest.mark.parametrize( "effort", [-0.1, 1.1, float("nan"), float("inf"), -float("inf")]