From a9028ef44171ac55f5f7bef9517c6eb9175d474e Mon Sep 17 00:00:00 2001 From: Yang Cen Date: Fri, 6 Nov 2026 17:37:52 +0800 Subject: [PATCH] feat(search): document and validate calibrated effort tiers (#4882) Lance beta.11 calibrates lower `search_effort` tiers by metric and requested `k`. With the Rust dependency and Python/Node execution tests already on main via #4881, document the tiers across Rust, Python, Node and the search guide, extend the remote request matrix to cover `0.1` and `0.25`, and align the Python test package with beta.11. `0.1` and `0.25` target empirical mean partition-routing recall of 80% and 90% for L2, cosine and dot through `k=100000`. These are not per-query or final Recall@k guarantees. Default Auto is unchanged; Hamming and larger `k` remain uncalibrated, and remote queries require corresponding server support. This PR makes no additional performance claim. See [Lance #10695](https://github.com/lance-format/lance/pull/10695) for measurements and limitations. Validation: both strict workspace Clippy configurations, Rust formatting, 13 Rust effort tests, 208 Python query tests, 41 Node query tests, native binding builds, TypeScript, Node lint/generated docs, Python lint/format, full lockfile check and two dependency guards passed on `a8b09170`. All validated Rust/Python/Node source and dependency files are byte-identical at `2a82d07c`; the only subsequent change restores the Java POM to main. Root Ruff has 59 existing diagnostics and six formatting files, all byte-identical to the base; the Python package passes. The Java REST/Namespace client has no changed API here, and its dependency remains identical to main. The separate [Java beta publication failure](https://github.com/lance-format/lance/issues/10773) is not claimed resolved or validated. Official SDK release, automatic mirror, Sophon adoption and measured consumer acceptance remain follow-up stages. --- docs/src/js/classes/VectorQuery.md | 5 +++++ docs/web/search/vector-search.mdx | 18 ++++++++++++++++++ nodejs/lancedb/query.ts | 5 +++++ python/pyproject.toml | 2 +- python/python/lancedb/query.py | 15 +++++++++++++++ python/uv.lock | 16 ++++++++-------- rust/lancedb/src/query.rs | 6 +++++- rust/lancedb/src/remote/table.rs | 3 ++- 8 files changed, 59 insertions(+), 11 deletions(-) diff --git a/docs/src/js/classes/VectorQuery.md b/docs/src/js/classes/VectorQuery.md index e4cc0f1cc..2eef209bf 100644 --- a/docs/src/js/classes/VectorQuery.md +++ b/docs/src/js/classes/VectorQuery.md @@ -686,6 +686,11 @@ searchEffort(searchEffort): VectorQuery Set the initial IVF search effort to a finite value in [0, 1]. +Efforts 0.1 and 0.25 target empirical mean partition-routing recall of +80% and 90% for L2, cosine and dot through k=100000. The probe budget +adapts to the metric and k. Hamming and larger k remain uncalibrated; +100000 is not an API limit. Remote queries also require server support. + 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. diff --git a/docs/web/search/vector-search.mdx b/docs/web/search/vector-search.mdx index ed8e686b4..3fb0a2fe8 100644 --- a/docs/web/search/vector-search.mdx +++ b/docs/web/search/vector-search.mdx @@ -268,6 +268,24 @@ Use `search_effort` to tune how much work a search does. It accepts values from too low, or decrease it when you can trade some recall for lower latency. `search_effort(0.95)` does **not** mean 95% recall. +SDK builds using Lance 15.0.0-beta.11 or later provide these calibrated tiers: + +| Effort | Empirical mean partition-routing target | +| :--- | :--- | +| `0.1` | 80% | +| `0.25` | 90% | +| `0.5` (default) | Approximately 95%; existing Auto behavior | + +The probe budget adapts to the distance metric and requested `k`, so you can +keep the same effort when changing the result count. The lower tiers were +calibrated for `l2`, `cosine` and `dot` through `k=100000`. This is the tested +range, not a limit on `k`; Hamming and larger requests remain uncalibrated. + +These are average routing objectives, not guarantees for each query or final +Recall@k. Quantization, filtering and explicit probe bounds can lower final +recall. Remote queries require a server with the corresponding Lance version; +upgrading only the client does not change server-side routing. + {PyVectorIndexSearchEffort} diff --git a/nodejs/lancedb/query.ts b/nodejs/lancedb/query.ts index 8f16defbf..cbd927925 100644 --- a/nodejs/lancedb/query.ts +++ b/nodejs/lancedb/query.ts @@ -854,6 +854,11 @@ export class VectorQuery extends StandardQueryBase { /** * Set the initial IVF search effort to a finite value in [0, 1]. * + * Efforts 0.1 and 0.25 target empirical mean partition-routing recall of + * 80% and 90% for L2, cosine and dot through k=100000. The probe budget + * adapts to the metric and k. Hamming and larger k remain uncalibrated; + * 100000 is not an API limit. Remote queries also require server support. + * * 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. diff --git a/python/pyproject.toml b/python/pyproject.toml index 1655dedcb..3a19fd826 100644 --- a/python/pyproject.toml +++ b/python/pyproject.toml @@ -64,7 +64,7 @@ tests = [ "polars>=0.19, <=1.32.3", "pyarrow>=16", "pyarrow-stubs>=16.0", - "pylance==15.0.0b10", + "pylance==15.0.0b11", "requests>=2.31.0", "datafusion>=54,<55", "opentelemetry-sdk>=1.30.0", diff --git a/python/python/lancedb/query.py b/python/python/lancedb/query.py index ce313c610..08ea3b9bc 100644 --- a/python/python/lancedb/query.py +++ b/python/python/lancedb/query.py @@ -2060,6 +2060,11 @@ class LanceVectorQueryBuilder(LanceQueryBuilder): def search_effort(self, search_effort: Optional[float]) -> LanceVectorQueryBuilder: """Set the initial IVF search effort to a finite value in [0, 1]. + Efforts 0.1 and 0.25 target empirical mean partition-routing recall of + 80% and 90% for L2, cosine and dot through k=100000. The probe budget + adapts to the metric and k. Hamming and larger k remain uncalibrated; + 100000 is not an API limit. Remote queries also require server support. + None (the default) 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 @@ -3019,6 +3024,11 @@ class LanceHybridQueryBuilder(LanceQueryBuilder): def search_effort(self, search_effort: Optional[float]) -> LanceHybridQueryBuilder: """Set the initial IVF search effort to a finite value in [0, 1]. + Efforts 0.1 and 0.25 target empirical mean partition-routing recall of + 80% and 90% for L2, cosine and dot through k=100000. The probe budget + adapts to the metric and k. Hamming and larger k remain uncalibrated; + 100000 is not an API limit. Remote queries also require server support. + None (the default) 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 @@ -4426,6 +4436,11 @@ class AsyncVectorQueryBase: def search_effort(self, search_effort: Optional[float]) -> Self: """Set the initial IVF search effort to a finite value in [0, 1]. + Efforts 0.1 and 0.25 target empirical mean partition-routing recall of + 80% and 90% for L2, cosine and dot through k=100000. The probe budget + adapts to the metric and k. Hamming and larger k remain uncalibrated; + 100000 is not an API limit. Remote queries also require server support. + None preserves Auto (internally 0.5) and fixed nprobes. Any explicit effort, including 0.5, overrides nprobes in either call order; explicit minimum and maximum bounds remain effective. Zero starts at the caller diff --git a/python/uv.lock b/python/uv.lock index 6abf904a4..d8a1c118a 100644 --- a/python/uv.lock +++ b/python/uv.lock @@ -2147,7 +2147,7 @@ requires-dist = [ { name = "pyarrow-stubs", marker = "extra == 'tests'", specifier = ">=16.0" }, { name = "pydantic", specifier = ">=2.7.4,<3" }, { name = "pylance", marker = "extra == 'pylance'", specifier = ">=15.0.0b3" }, - { name = "pylance", marker = "extra == 'tests'", specifier = "==15.0.0b10" }, + { name = "pylance", marker = "extra == 'tests'", specifier = "==15.0.0b11" }, { name = "pyright", marker = "extra == 'dev'", specifier = ">=1.1.350" }, { name = "pytest", marker = "extra == 'tests'", specifier = ">=7.0" }, { name = "pytest-asyncio", marker = "extra == 'tests'", specifier = ">=0.21" }, @@ -4301,7 +4301,7 @@ crypto = [ [[package]] name = "pylance" -version = "15.0.0b10" +version = "15.0.0b11" source = { registry = "https://pypi.fury.io/lance-format/" } dependencies = [ { name = "lance-namespace" }, @@ -4312,12 +4312,12 @@ dependencies = [ { name = "pyarrow", version = "26.0.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, ] wheels = [ - { url = "https://pypi.fury.io/lance-format/-/ver_1aEUT6/pylance-15.0.0b10-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:fe34d42c5d58008a65f41acb659474c43848104414a2af94461454377c9f5276" }, - { url = "https://pypi.fury.io/lance-format/-/ver_1YU4YL/pylance-15.0.0b10-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1042942b4df0ea42d263bf2e12ac2fa04e24ea49482f55349687df439686f614" }, - { url = "https://pypi.fury.io/lance-format/-/ver_LnRBy/pylance-15.0.0b10-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f0954e2a3855bc161a9ffebfcdf7f7086da3fa71ef1f3a92bd38212076963b08" }, - { url = "https://pypi.fury.io/lance-format/-/ver_pjVe1/pylance-15.0.0b10-cp310-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:eb6bc476f79d4c1b786377ae8366afc1718f54a9314141e65c57ad0f47906df6" }, - { url = "https://pypi.fury.io/lance-format/-/ver_RENd/pylance-15.0.0b10-cp310-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:4dc325458b5b959bc644487c2ade6cdbcc89c4bc14ee7bb7487d9e145426207c" }, - { url = "https://pypi.fury.io/lance-format/-/ver_1I5wzV/pylance-15.0.0b10-cp310-abi3-win_amd64.whl", hash = "sha256:10226cf5c224c0a47d18d7a323ce64473026acd7a4f08e7c6a59e787ee93a73c" }, + { url = "https://pypi.fury.io/lance-format/-/ver_oRP5O/pylance-15.0.0b11-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:54524c1bd04065516aeb97c5d06e1f6c88d1b1d5191d40077b104c5879893ea2" }, + { url = "https://pypi.fury.io/lance-format/-/ver_1LdITz/pylance-15.0.0b11-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8f6b2496b4b20787ff7780907f3017cf7b17c1f2bd1fa8cb7ed4d0a450445dfc" }, + { url = "https://pypi.fury.io/lance-format/-/ver_1ONuyB/pylance-15.0.0b11-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:903283401e222dea532a36b91ffa60e7eaaf4d1fc08602cb29eef4ac2d04a8ff" }, + { url = "https://pypi.fury.io/lance-format/-/ver_2bWLjt/pylance-15.0.0b11-cp310-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:ce8bb0242b1647ce0b9355a48339b8fe8415f3556209214d0b490afb6ee60b38" }, + { url = "https://pypi.fury.io/lance-format/-/ver_168GWI/pylance-15.0.0b11-cp310-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:387ad2b455ab5b00476d7a60ce96e6014239df239691ac219f0025a29ee19f6e" }, + { url = "https://pypi.fury.io/lance-format/-/ver_aU9I7/pylance-15.0.0b11-cp310-abi3-win_amd64.whl", hash = "sha256:7022c2afcdaf6f9e9444db5addc72033ad20ad301930b38238e246687628cd75" }, ] [[package]] diff --git a/rust/lancedb/src/query.rs b/rust/lancedb/src/query.rs index 866bfbc0d..ad2dbdcbf 100644 --- a/rust/lancedb/src/query.rs +++ b/rust/lancedb/src/query.rs @@ -1329,7 +1329,11 @@ impl VectorQuery { /// /// None (the default) resolves to 0.5, preserving Auto probing. At 0 the /// search starts at the caller minimum; at 1 it searches all partitions - /// within the caller maximum. Intermediate values interpolate around Auto. + /// within the caller maximum. Efforts 0.1 and 0.25 target empirical mean + /// partition-routing recall of 80% and 90%, respectively, for L2, cosine + /// and dot with k up to 100,000. The budget adapts to the metric and k; + /// this is a calibration boundary, not a limit on k. Hamming and larger k + /// retain uncalibrated behavior. Other values interpolate between tiers. /// Later probing may continue when too few candidates are found. /// Any explicit value, including 0.5, overrides [`Self::nprobes`] regardless /// of setter order. Explicit minimum and maximum bounds still apply. diff --git a/rust/lancedb/src/remote/table.rs b/rust/lancedb/src/remote/table.rs index 895cc9f12..abba7ff94 100644 --- a/rust/lancedb/src/remote/table.rs +++ b/rust/lancedb/src/remote/table.rs @@ -7374,7 +7374,8 @@ mod tests { #[rstest] #[case::unset(None)] #[case::minimum(Some(0.0))] - #[case::low(Some(0.25))] + #[case::recall80(Some(0.1))] + #[case::recall90(Some(0.25))] #[case::auto(Some(0.5))] #[case::high(Some(0.75))] #[case::maximum(Some(1.0))]