mirror of
https://github.com/lancedb/lancedb.git
synced 2026-10-13 23:32:27 +00:00
Merge main and retain sliced Arrow validity fix
This commit is contained in:
+1
-1
@@ -1,5 +1,5 @@
|
||||
[tool.bumpversion]
|
||||
current_version = "0.41.0-beta.5"
|
||||
current_version = "0.41.0-beta.6"
|
||||
parse = """(?x)
|
||||
(?P<major>0|[1-9]\\d*)\\.
|
||||
(?P<minor>0|[1-9]\\d*)\\.
|
||||
|
||||
Generated
+48
-48
@@ -3584,8 +3584,8 @@ checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c"
|
||||
|
||||
[[package]]
|
||||
name = "fsst"
|
||||
version = "14.0.0-beta.13"
|
||||
source = "git+https://github.com/lance-format/lance.git?tag=v14.0.0-beta.13#145fd4b933fb4a9a45831f0ae9185c88e908fe6a"
|
||||
version = "14.0.0-beta.15"
|
||||
source = "git+https://github.com/lance-format/lance.git?tag=v14.0.0-beta.15#765641dc0d7100f032fdebc00d73eef49711c4d4"
|
||||
dependencies = [
|
||||
"arrow-array",
|
||||
"rand 0.9.5",
|
||||
@@ -5059,8 +5059,8 @@ checksum = "e037a2e1d8d5fdbd49b16a4ea09d5d6401c1f29eca5ff29d03d3824dba16256a"
|
||||
|
||||
[[package]]
|
||||
name = "lance"
|
||||
version = "14.0.0-beta.13"
|
||||
source = "git+https://github.com/lance-format/lance.git?tag=v14.0.0-beta.13#145fd4b933fb4a9a45831f0ae9185c88e908fe6a"
|
||||
version = "14.0.0-beta.15"
|
||||
source = "git+https://github.com/lance-format/lance.git?tag=v14.0.0-beta.15#765641dc0d7100f032fdebc00d73eef49711c4d4"
|
||||
dependencies = [
|
||||
"arc-swap",
|
||||
"arrow",
|
||||
@@ -5133,7 +5133,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "lance-arrow"
|
||||
version = "14.0.0-beta.13"
|
||||
version = "14.0.0-beta.15"
|
||||
dependencies = [
|
||||
"arrow-array",
|
||||
"arrow-buffer",
|
||||
@@ -5155,7 +5155,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "lance-arrow-scalar"
|
||||
version = "58.0.0"
|
||||
source = "git+https://github.com/lance-format/lance.git?tag=v14.0.0-beta.13#145fd4b933fb4a9a45831f0ae9185c88e908fe6a"
|
||||
source = "git+https://github.com/lance-format/lance.git?tag=v14.0.0-beta.15#765641dc0d7100f032fdebc00d73eef49711c4d4"
|
||||
dependencies = [
|
||||
"arrow-array",
|
||||
"arrow-buffer",
|
||||
@@ -5169,7 +5169,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "lance-arrow-stats"
|
||||
version = "58.0.0"
|
||||
source = "git+https://github.com/lance-format/lance.git?tag=v14.0.0-beta.13#145fd4b933fb4a9a45831f0ae9185c88e908fe6a"
|
||||
source = "git+https://github.com/lance-format/lance.git?tag=v14.0.0-beta.15#765641dc0d7100f032fdebc00d73eef49711c4d4"
|
||||
dependencies = [
|
||||
"arrow-array",
|
||||
"arrow-schema",
|
||||
@@ -5178,8 +5178,8 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "lance-bitpacking"
|
||||
version = "14.0.0-beta.13"
|
||||
source = "git+https://github.com/lance-format/lance.git?tag=v14.0.0-beta.13#145fd4b933fb4a9a45831f0ae9185c88e908fe6a"
|
||||
version = "14.0.0-beta.15"
|
||||
source = "git+https://github.com/lance-format/lance.git?tag=v14.0.0-beta.15#765641dc0d7100f032fdebc00d73eef49711c4d4"
|
||||
dependencies = [
|
||||
"arrayref",
|
||||
"crunchy",
|
||||
@@ -5189,8 +5189,8 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "lance-core"
|
||||
version = "14.0.0-beta.13"
|
||||
source = "git+https://github.com/lance-format/lance.git?tag=v14.0.0-beta.13#145fd4b933fb4a9a45831f0ae9185c88e908fe6a"
|
||||
version = "14.0.0-beta.15"
|
||||
source = "git+https://github.com/lance-format/lance.git?tag=v14.0.0-beta.15#765641dc0d7100f032fdebc00d73eef49711c4d4"
|
||||
dependencies = [
|
||||
"arrow-array",
|
||||
"arrow-buffer",
|
||||
@@ -5227,8 +5227,8 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "lance-datafusion"
|
||||
version = "14.0.0-beta.13"
|
||||
source = "git+https://github.com/lance-format/lance.git?tag=v14.0.0-beta.13#145fd4b933fb4a9a45831f0ae9185c88e908fe6a"
|
||||
version = "14.0.0-beta.15"
|
||||
source = "git+https://github.com/lance-format/lance.git?tag=v14.0.0-beta.15#765641dc0d7100f032fdebc00d73eef49711c4d4"
|
||||
dependencies = [
|
||||
"arrow",
|
||||
"arrow-array",
|
||||
@@ -5259,8 +5259,8 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "lance-datagen"
|
||||
version = "14.0.0-beta.13"
|
||||
source = "git+https://github.com/lance-format/lance.git?tag=v14.0.0-beta.13#145fd4b933fb4a9a45831f0ae9185c88e908fe6a"
|
||||
version = "14.0.0-beta.15"
|
||||
source = "git+https://github.com/lance-format/lance.git?tag=v14.0.0-beta.15#765641dc0d7100f032fdebc00d73eef49711c4d4"
|
||||
dependencies = [
|
||||
"arrow",
|
||||
"arrow-array",
|
||||
@@ -5277,8 +5277,8 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "lance-derive"
|
||||
version = "14.0.0-beta.13"
|
||||
source = "git+https://github.com/lance-format/lance.git?tag=v14.0.0-beta.13#145fd4b933fb4a9a45831f0ae9185c88e908fe6a"
|
||||
version = "14.0.0-beta.15"
|
||||
source = "git+https://github.com/lance-format/lance.git?tag=v14.0.0-beta.15#765641dc0d7100f032fdebc00d73eef49711c4d4"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@@ -5287,8 +5287,8 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "lance-encoding"
|
||||
version = "14.0.0-beta.13"
|
||||
source = "git+https://github.com/lance-format/lance.git?tag=v14.0.0-beta.13#145fd4b933fb4a9a45831f0ae9185c88e908fe6a"
|
||||
version = "14.0.0-beta.15"
|
||||
source = "git+https://github.com/lance-format/lance.git?tag=v14.0.0-beta.15#765641dc0d7100f032fdebc00d73eef49711c4d4"
|
||||
dependencies = [
|
||||
"arrow-arith",
|
||||
"arrow-array",
|
||||
@@ -5321,8 +5321,8 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "lance-file"
|
||||
version = "14.0.0-beta.13"
|
||||
source = "git+https://github.com/lance-format/lance.git?tag=v14.0.0-beta.13#145fd4b933fb4a9a45831f0ae9185c88e908fe6a"
|
||||
version = "14.0.0-beta.15"
|
||||
source = "git+https://github.com/lance-format/lance.git?tag=v14.0.0-beta.15#765641dc0d7100f032fdebc00d73eef49711c4d4"
|
||||
dependencies = [
|
||||
"arrow-arith",
|
||||
"arrow-array",
|
||||
@@ -5354,8 +5354,8 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "lance-geo"
|
||||
version = "14.0.0-beta.13"
|
||||
source = "git+https://github.com/lance-format/lance.git?tag=v14.0.0-beta.13#145fd4b933fb4a9a45831f0ae9185c88e908fe6a"
|
||||
version = "14.0.0-beta.15"
|
||||
source = "git+https://github.com/lance-format/lance.git?tag=v14.0.0-beta.15#765641dc0d7100f032fdebc00d73eef49711c4d4"
|
||||
dependencies = [
|
||||
"datafusion",
|
||||
"geo-traits",
|
||||
@@ -5369,8 +5369,8 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "lance-index"
|
||||
version = "14.0.0-beta.13"
|
||||
source = "git+https://github.com/lance-format/lance.git?tag=v14.0.0-beta.13#145fd4b933fb4a9a45831f0ae9185c88e908fe6a"
|
||||
version = "14.0.0-beta.15"
|
||||
source = "git+https://github.com/lance-format/lance.git?tag=v14.0.0-beta.15#765641dc0d7100f032fdebc00d73eef49711c4d4"
|
||||
dependencies = [
|
||||
"arc-swap",
|
||||
"arrow",
|
||||
@@ -5441,8 +5441,8 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "lance-index-core"
|
||||
version = "14.0.0-beta.13"
|
||||
source = "git+https://github.com/lance-format/lance.git?tag=v14.0.0-beta.13#145fd4b933fb4a9a45831f0ae9185c88e908fe6a"
|
||||
version = "14.0.0-beta.15"
|
||||
source = "git+https://github.com/lance-format/lance.git?tag=v14.0.0-beta.15#765641dc0d7100f032fdebc00d73eef49711c4d4"
|
||||
dependencies = [
|
||||
"arrow-array",
|
||||
"arrow-schema",
|
||||
@@ -5465,8 +5465,8 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "lance-io"
|
||||
version = "14.0.0-beta.13"
|
||||
source = "git+https://github.com/lance-format/lance.git?tag=v14.0.0-beta.13#145fd4b933fb4a9a45831f0ae9185c88e908fe6a"
|
||||
version = "14.0.0-beta.15"
|
||||
source = "git+https://github.com/lance-format/lance.git?tag=v14.0.0-beta.15#765641dc0d7100f032fdebc00d73eef49711c4d4"
|
||||
dependencies = [
|
||||
"arrow",
|
||||
"arrow-array",
|
||||
@@ -5508,8 +5508,8 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "lance-jsonb"
|
||||
version = "14.0.0-beta.13"
|
||||
source = "git+https://github.com/lance-format/lance.git?tag=v14.0.0-beta.13#145fd4b933fb4a9a45831f0ae9185c88e908fe6a"
|
||||
version = "14.0.0-beta.15"
|
||||
source = "git+https://github.com/lance-format/lance.git?tag=v14.0.0-beta.15#765641dc0d7100f032fdebc00d73eef49711c4d4"
|
||||
dependencies = [
|
||||
"byteorder",
|
||||
"ethnum",
|
||||
@@ -5526,8 +5526,8 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "lance-linalg"
|
||||
version = "14.0.0-beta.13"
|
||||
source = "git+https://github.com/lance-format/lance.git?tag=v14.0.0-beta.13#145fd4b933fb4a9a45831f0ae9185c88e908fe6a"
|
||||
version = "14.0.0-beta.15"
|
||||
source = "git+https://github.com/lance-format/lance.git?tag=v14.0.0-beta.15#765641dc0d7100f032fdebc00d73eef49711c4d4"
|
||||
dependencies = [
|
||||
"arrow-array",
|
||||
"arrow-schema",
|
||||
@@ -5541,8 +5541,8 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "lance-namespace"
|
||||
version = "14.0.0-beta.13"
|
||||
source = "git+https://github.com/lance-format/lance.git?tag=v14.0.0-beta.13#145fd4b933fb4a9a45831f0ae9185c88e908fe6a"
|
||||
version = "14.0.0-beta.15"
|
||||
source = "git+https://github.com/lance-format/lance.git?tag=v14.0.0-beta.15#765641dc0d7100f032fdebc00d73eef49711c4d4"
|
||||
dependencies = [
|
||||
"arrow",
|
||||
"async-trait",
|
||||
@@ -5556,8 +5556,8 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "lance-namespace-impls"
|
||||
version = "14.0.0-beta.13"
|
||||
source = "git+https://github.com/lance-format/lance.git?tag=v14.0.0-beta.13#145fd4b933fb4a9a45831f0ae9185c88e908fe6a"
|
||||
version = "14.0.0-beta.15"
|
||||
source = "git+https://github.com/lance-format/lance.git?tag=v14.0.0-beta.15#765641dc0d7100f032fdebc00d73eef49711c4d4"
|
||||
dependencies = [
|
||||
"arc-swap",
|
||||
"arrow",
|
||||
@@ -5611,8 +5611,8 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "lance-select"
|
||||
version = "14.0.0-beta.13"
|
||||
source = "git+https://github.com/lance-format/lance.git?tag=v14.0.0-beta.13#145fd4b933fb4a9a45831f0ae9185c88e908fe6a"
|
||||
version = "14.0.0-beta.15"
|
||||
source = "git+https://github.com/lance-format/lance.git?tag=v14.0.0-beta.15#765641dc0d7100f032fdebc00d73eef49711c4d4"
|
||||
dependencies = [
|
||||
"arrow-array",
|
||||
"arrow-buffer",
|
||||
@@ -5626,8 +5626,8 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "lance-table"
|
||||
version = "14.0.0-beta.13"
|
||||
source = "git+https://github.com/lance-format/lance.git?tag=v14.0.0-beta.13#145fd4b933fb4a9a45831f0ae9185c88e908fe6a"
|
||||
version = "14.0.0-beta.15"
|
||||
source = "git+https://github.com/lance-format/lance.git?tag=v14.0.0-beta.15#765641dc0d7100f032fdebc00d73eef49711c4d4"
|
||||
dependencies = [
|
||||
"arrow",
|
||||
"arrow-array",
|
||||
@@ -5667,8 +5667,8 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "lance-testing"
|
||||
version = "14.0.0-beta.13"
|
||||
source = "git+https://github.com/lance-format/lance.git?tag=v14.0.0-beta.13#145fd4b933fb4a9a45831f0ae9185c88e908fe6a"
|
||||
version = "14.0.0-beta.15"
|
||||
source = "git+https://github.com/lance-format/lance.git?tag=v14.0.0-beta.15#765641dc0d7100f032fdebc00d73eef49711c4d4"
|
||||
dependencies = [
|
||||
"arrow-array",
|
||||
"arrow-schema",
|
||||
@@ -5681,8 +5681,8 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "lance-tokenizer"
|
||||
version = "14.0.0-beta.13"
|
||||
source = "git+https://github.com/lance-format/lance.git?tag=v14.0.0-beta.13#145fd4b933fb4a9a45831f0ae9185c88e908fe6a"
|
||||
version = "14.0.0-beta.15"
|
||||
source = "git+https://github.com/lance-format/lance.git?tag=v14.0.0-beta.15#765641dc0d7100f032fdebc00d73eef49711c4d4"
|
||||
dependencies = [
|
||||
"frostem",
|
||||
"icu_segmenter",
|
||||
@@ -5695,7 +5695,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "lancedb"
|
||||
version = "0.41.0-beta.5"
|
||||
version = "0.41.0-beta.6"
|
||||
dependencies = [
|
||||
"ahash",
|
||||
"anyhow",
|
||||
@@ -5792,7 +5792,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "lancedb-nodejs"
|
||||
version = "0.41.0-beta.5"
|
||||
version = "0.41.0-beta.6"
|
||||
dependencies = [
|
||||
"arrow-array",
|
||||
"arrow-buffer",
|
||||
@@ -5817,7 +5817,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "lancedb-python"
|
||||
version = "0.41.0-beta.5"
|
||||
version = "0.41.0-beta.6"
|
||||
dependencies = [
|
||||
"arc-swap",
|
||||
"arrow",
|
||||
|
||||
+15
-15
@@ -13,21 +13,21 @@ categories = ["database-implementations"]
|
||||
rust-version = "1.94.0"
|
||||
|
||||
[workspace.dependencies]
|
||||
lance = { "version" = "=14.0.0-beta.13", default-features = false, "tag" = "v14.0.0-beta.13", "git" = "https://github.com/lance-format/lance.git" }
|
||||
lance-core = { "version" = "=14.0.0-beta.13", "tag" = "v14.0.0-beta.13", "git" = "https://github.com/lance-format/lance.git" }
|
||||
lance-datagen = { "version" = "=14.0.0-beta.13", "tag" = "v14.0.0-beta.13", "git" = "https://github.com/lance-format/lance.git" }
|
||||
lance-file = { "version" = "=14.0.0-beta.13", "tag" = "v14.0.0-beta.13", "git" = "https://github.com/lance-format/lance.git" }
|
||||
lance-io = { "version" = "=14.0.0-beta.13", default-features = false, "tag" = "v14.0.0-beta.13", "git" = "https://github.com/lance-format/lance.git" }
|
||||
lance-index = { "version" = "=14.0.0-beta.13", "tag" = "v14.0.0-beta.13", "git" = "https://github.com/lance-format/lance.git" }
|
||||
lance-jsonb = { version = "=14.0.0-beta.13", tag = "v14.0.0-beta.13", git = "https://github.com/lance-format/lance.git" }
|
||||
lance-linalg = { "version" = "=14.0.0-beta.13", "tag" = "v14.0.0-beta.13", "git" = "https://github.com/lance-format/lance.git" }
|
||||
lance-namespace = { "version" = "=14.0.0-beta.13", "tag" = "v14.0.0-beta.13", "git" = "https://github.com/lance-format/lance.git" }
|
||||
lance-namespace-impls = { "version" = "=14.0.0-beta.13", default-features = false, "tag" = "v14.0.0-beta.13", "git" = "https://github.com/lance-format/lance.git" }
|
||||
lance-table = { "version" = "=14.0.0-beta.13", "tag" = "v14.0.0-beta.13", "git" = "https://github.com/lance-format/lance.git" }
|
||||
lance-testing = { "version" = "=14.0.0-beta.13", "tag" = "v14.0.0-beta.13", "git" = "https://github.com/lance-format/lance.git" }
|
||||
lance-datafusion = { "version" = "=14.0.0-beta.13", "tag" = "v14.0.0-beta.13", "git" = "https://github.com/lance-format/lance.git" }
|
||||
lance-encoding = { "version" = "=14.0.0-beta.13", "tag" = "v14.0.0-beta.13", "git" = "https://github.com/lance-format/lance.git" }
|
||||
lance-arrow = { version = "=14.0.0-beta.13", path = "python/vendor/lance-arrow" }
|
||||
lance = { "version" = "=14.0.0-beta.15", default-features = false, "tag" = "v14.0.0-beta.15", "git" = "https://github.com/lance-format/lance.git" }
|
||||
lance-core = { "version" = "=14.0.0-beta.15", "tag" = "v14.0.0-beta.15", "git" = "https://github.com/lance-format/lance.git" }
|
||||
lance-datagen = { "version" = "=14.0.0-beta.15", "tag" = "v14.0.0-beta.15", "git" = "https://github.com/lance-format/lance.git" }
|
||||
lance-file = { "version" = "=14.0.0-beta.15", "tag" = "v14.0.0-beta.15", "git" = "https://github.com/lance-format/lance.git" }
|
||||
lance-io = { "version" = "=14.0.0-beta.15", default-features = false, "tag" = "v14.0.0-beta.15", "git" = "https://github.com/lance-format/lance.git" }
|
||||
lance-index = { "version" = "=14.0.0-beta.15", "tag" = "v14.0.0-beta.15", "git" = "https://github.com/lance-format/lance.git" }
|
||||
lance-jsonb = { version = "=14.0.0-beta.15", tag = "v14.0.0-beta.15", git = "https://github.com/lance-format/lance.git" }
|
||||
lance-linalg = { "version" = "=14.0.0-beta.15", "tag" = "v14.0.0-beta.15", "git" = "https://github.com/lance-format/lance.git" }
|
||||
lance-namespace = { "version" = "=14.0.0-beta.15", "tag" = "v14.0.0-beta.15", "git" = "https://github.com/lance-format/lance.git" }
|
||||
lance-namespace-impls = { "version" = "=14.0.0-beta.15", default-features = false, "tag" = "v14.0.0-beta.15", "git" = "https://github.com/lance-format/lance.git" }
|
||||
lance-table = { "version" = "=14.0.0-beta.15", "tag" = "v14.0.0-beta.15", "git" = "https://github.com/lance-format/lance.git" }
|
||||
lance-testing = { "version" = "=14.0.0-beta.15", "tag" = "v14.0.0-beta.15", "git" = "https://github.com/lance-format/lance.git" }
|
||||
lance-datafusion = { "version" = "=14.0.0-beta.15", "tag" = "v14.0.0-beta.15", "git" = "https://github.com/lance-format/lance.git" }
|
||||
lance-encoding = { "version" = "=14.0.0-beta.15", "tag" = "v14.0.0-beta.15", "git" = "https://github.com/lance-format/lance.git" }
|
||||
lance-arrow = { version = "=14.0.0-beta.15", path = "python/vendor/lance-arrow" }
|
||||
lancedb = { path = "rust/lancedb", default-features = false }
|
||||
ahash = "0.8"
|
||||
# Note that this one does not include pyarrow
|
||||
|
||||
+7
-4
@@ -15,18 +15,21 @@ unreleased features.
|
||||
|
||||
### Preview the docs
|
||||
|
||||
Run these commands from the LanceDB repository root. The subshell changes into
|
||||
`docs` to load `mkdocs.yml`, while leaving your shell in the repository root.
|
||||
|
||||
```shell
|
||||
cd docs
|
||||
mkdocs serve
|
||||
(cd docs && PYTHONPATH=. mkdocs serve)
|
||||
```
|
||||
|
||||
If you want to just generate the HTML files:
|
||||
|
||||
```shell
|
||||
PYTHONPATH=. mkdocs build -f docs/mkdocs.yml
|
||||
(cd docs && PYTHONPATH=. mkdocs build)
|
||||
```
|
||||
|
||||
If successful, you should see a `docs/site` directory that you can verify locally.
|
||||
If successful, you should see a `docs/site` directory in the repository root that
|
||||
you can verify locally.
|
||||
|
||||
## Adding examples
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ Add the following dependency to your `pom.xml`:
|
||||
<dependency>
|
||||
<groupId>com.lancedb</groupId>
|
||||
<artifactId>lancedb-core</artifactId>
|
||||
<version>0.41.0-beta.5</version>
|
||||
<version>0.41.0-beta.6</version>
|
||||
</dependency>
|
||||
```
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
<parent>
|
||||
<groupId>com.lancedb</groupId>
|
||||
<artifactId>lancedb-parent</artifactId>
|
||||
<version>0.41.0-beta.5</version>
|
||||
<version>0.41.0-beta.6</version>
|
||||
<relativePath>../pom.xml</relativePath>
|
||||
</parent>
|
||||
|
||||
|
||||
+2
-2
@@ -6,7 +6,7 @@
|
||||
|
||||
<groupId>com.lancedb</groupId>
|
||||
<artifactId>lancedb-parent</artifactId>
|
||||
<version>0.41.0-beta.5</version>
|
||||
<version>0.41.0-beta.6</version>
|
||||
<packaging>pom</packaging>
|
||||
<name>${project.artifactId}</name>
|
||||
<description>LanceDB Java SDK Parent POM</description>
|
||||
@@ -28,7 +28,7 @@
|
||||
<properties>
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
<arrow.version>15.0.0</arrow.version>
|
||||
<lance-core.version>14.0.0-beta.13</lance-core.version>
|
||||
<lance-core.version>14.0.0-beta.15</lance-core.version>
|
||||
<jackson.version>2.18.11</jackson.version>
|
||||
<spotless.skip>false</spotless.skip>
|
||||
<spotless.version>2.30.0</spotless.version>
|
||||
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
[package]
|
||||
name = "lancedb-nodejs"
|
||||
edition.workspace = true
|
||||
version = "0.41.0-beta.5"
|
||||
version = "0.41.0-beta.6"
|
||||
publish = false
|
||||
license.workspace = true
|
||||
description.workspace = true
|
||||
|
||||
@@ -527,3 +527,106 @@ describe("Query orderBy", () => {
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("IVF search effort", () => {
|
||||
let directory: tmp.DirResult;
|
||||
let table: Table;
|
||||
const vector = Array.from({ length: 16 }, (_, i) => (i === 0 ? 1 : 0));
|
||||
|
||||
beforeEach(async () => {
|
||||
directory = tmp.dirSync({ unsafeCleanup: true });
|
||||
const db = await connect(directory.name);
|
||||
const rows = Array.from({ length: 512 }, (_, id) => ({
|
||||
id,
|
||||
vector: Array.from(
|
||||
{ length: 16 },
|
||||
(_, j) =>
|
||||
(Math.floor(id / 32) === j ? 1 : 0) + Math.sin(id * 17 + j) * 0.001,
|
||||
),
|
||||
}));
|
||||
table = await db.createTable("effort", rows.slice(0, 256));
|
||||
await table.add(rows.slice(256));
|
||||
await table.createIndex("vector", {
|
||||
config: Index.ivfFlat({ numPartitions: 16 }),
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
directory.removeCallback();
|
||||
});
|
||||
|
||||
it.each([
|
||||
[null, 6],
|
||||
[0, 1],
|
||||
[0.25, 3],
|
||||
[0.5, 6],
|
||||
[0.75, 10],
|
||||
[1, 16],
|
||||
])(
|
||||
"preserves effort %s through execution and probe precedence",
|
||||
async (effort, expected) => {
|
||||
const partitions = async (query: ReturnType<Table["vectorSearch"]>) => {
|
||||
const plan = await query.limit(8).analyzePlan();
|
||||
const matches = [...plan.matchAll(/partitions_searched=(\d+)/g)];
|
||||
expect(matches.length).toBeGreaterThan(0);
|
||||
return matches.reduce((total, match) => total + Number(match[1]), 0);
|
||||
};
|
||||
expect(
|
||||
await partitions(table.vectorSearch(vector).searchEffort(effort)),
|
||||
).toBe(expected);
|
||||
expect(
|
||||
await partitions(
|
||||
table.vectorSearch(vector).nprobes(1).searchEffort(effort),
|
||||
),
|
||||
).toBe(effort === null ? 1 : expected);
|
||||
expect(
|
||||
await partitions(
|
||||
table.vectorSearch(vector).searchEffort(effort).nprobes(1),
|
||||
),
|
||||
).toBe(effort === null ? 1 : expected);
|
||||
expect(
|
||||
await partitions(
|
||||
table
|
||||
.vectorSearch(vector)
|
||||
.searchEffort(effort)
|
||||
.nprobes(1)
|
||||
.searchEffort(null),
|
||||
),
|
||||
).toBe(1);
|
||||
expect(
|
||||
await partitions(
|
||||
table.vectorSearch(vector).searchEffort(effort).maximumNprobes(2),
|
||||
),
|
||||
).toBeLessThanOrEqual(2);
|
||||
const actual = await table
|
||||
.vectorSearch(vector)
|
||||
.searchEffort(effort)
|
||||
.limit(8)
|
||||
.toArray();
|
||||
const exact = await table
|
||||
.vectorSearch(vector)
|
||||
.bypassVectorIndex()
|
||||
.limit(8)
|
||||
.toArray();
|
||||
const ids = new Set(exact.map((row) => row.id));
|
||||
expect(
|
||||
actual.filter((row) => ids.has(row.id)).length / 8,
|
||||
).toBeGreaterThanOrEqual(0.5);
|
||||
},
|
||||
);
|
||||
|
||||
it.each([
|
||||
-0.1,
|
||||
1.1,
|
||||
Number.NaN,
|
||||
Number.POSITIVE_INFINITY,
|
||||
Number.NEGATIVE_INFINITY,
|
||||
])("rejects invalid effort %s before execution", (effort) => {
|
||||
expect(() => table.vectorSearch(vector).searchEffort(effort)).toThrow(
|
||||
"search_effort must be finite and in [0, 1]",
|
||||
);
|
||||
expect(() =>
|
||||
table.vectorSearch(vector).fullTextSearch("dog").searchEffort(effort),
|
||||
).toThrow("search_effort must be finite and in [0, 1]");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -549,6 +549,27 @@ export class VectorQuery extends StandardQueryBase<NativeVectorQuery> {
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* table.vectorSearch([0.1, 0.2]).searchEffort(0.75).limit(10);
|
||||
* ```
|
||||
*/
|
||||
searchEffort(searchEffort: number | null): VectorQuery {
|
||||
this.doVectorCall((inner) => inner.searchEffort(searchEffort));
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the minimum number of probes used.
|
||||
*
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@lancedb/lancedb-darwin-arm64",
|
||||
"version": "0.41.0-beta.5",
|
||||
"version": "0.41.0-beta.6",
|
||||
"os": ["darwin"],
|
||||
"cpu": ["arm64"],
|
||||
"main": "lancedb.darwin-arm64.node",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@lancedb/lancedb-linux-arm64-gnu",
|
||||
"version": "0.41.0-beta.5",
|
||||
"version": "0.41.0-beta.6",
|
||||
"os": ["linux"],
|
||||
"cpu": ["arm64"],
|
||||
"main": "lancedb.linux-arm64-gnu.node",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@lancedb/lancedb-linux-arm64-musl",
|
||||
"version": "0.41.0-beta.5",
|
||||
"version": "0.41.0-beta.6",
|
||||
"os": ["linux"],
|
||||
"cpu": ["arm64"],
|
||||
"main": "lancedb.linux-arm64-musl.node",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@lancedb/lancedb-linux-x64-gnu",
|
||||
"version": "0.41.0-beta.5",
|
||||
"version": "0.41.0-beta.6",
|
||||
"os": ["linux"],
|
||||
"cpu": ["x64"],
|
||||
"main": "lancedb.linux-x64-gnu.node",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@lancedb/lancedb-linux-x64-musl",
|
||||
"version": "0.41.0-beta.5",
|
||||
"version": "0.41.0-beta.6",
|
||||
"os": ["linux"],
|
||||
"cpu": ["x64"],
|
||||
"main": "lancedb.linux-x64-musl.node",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@lancedb/lancedb-win32-arm64-msvc",
|
||||
"version": "0.41.0-beta.5",
|
||||
"version": "0.41.0-beta.6",
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@lancedb/lancedb-win32-x64-msvc",
|
||||
"version": "0.41.0-beta.5",
|
||||
"version": "0.41.0-beta.6",
|
||||
"os": ["win32"],
|
||||
"cpu": ["x64"],
|
||||
"main": "lancedb.win32-x64-msvc.node",
|
||||
|
||||
+1
-1
@@ -11,7 +11,7 @@
|
||||
"ann"
|
||||
],
|
||||
"private": false,
|
||||
"version": "0.41.0-beta.5",
|
||||
"version": "0.41.0-beta.6",
|
||||
"main": "dist/index.js",
|
||||
"exports": {
|
||||
".": "./dist/index.js",
|
||||
|
||||
@@ -298,6 +298,16 @@ impl VectorQuery {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[napi]
|
||||
pub fn search_effort(&mut self, search_effort: Option<f64>) -> napi::Result<()> {
|
||||
self.inner = self
|
||||
.inner
|
||||
.clone()
|
||||
.search_effort(search_effort)
|
||||
.default_error()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[napi]
|
||||
pub fn minimum_nprobes(&mut self, minimum_nprobe: u32) -> napi::Result<()> {
|
||||
self.inner = self
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "lancedb-python"
|
||||
version = "0.41.0-beta.5"
|
||||
version = "0.41.0-beta.6"
|
||||
publish = false
|
||||
edition.workspace = true
|
||||
description = "Python bindings for LanceDB"
|
||||
|
||||
@@ -63,7 +63,7 @@ tests = [
|
||||
"polars>=0.19, <=1.32.3",
|
||||
"pyarrow<25",
|
||||
"pyarrow-stubs>=16.0",
|
||||
"pylance==14.0.0b10",
|
||||
"pylance==14.0.0b12",
|
||||
"requests>=2.31.0",
|
||||
"datafusion>=54,<55",
|
||||
"opentelemetry-sdk>=1.30.0",
|
||||
@@ -73,6 +73,7 @@ dev = [
|
||||
"pre-commit>=3.5.0",
|
||||
"pyright>=1.1.350",
|
||||
'typing-extensions>=4.0.0; python_version < "3.11"',
|
||||
"maturin>=1.10",
|
||||
]
|
||||
docs = ["mkdocs", "mkdocs-jupyter", "mkdocs-material", "mkdocstrings-python"]
|
||||
clip = ["torch", "pillow>=12.1.1", "open-clip-torch"]
|
||||
|
||||
@@ -110,7 +110,9 @@ def projection_includes_blob_column(
|
||||
if projection is None:
|
||||
return True
|
||||
for output, source in _iter_projection_pairs(projection):
|
||||
if output in columns or source in columns:
|
||||
if output in columns or any(
|
||||
column == source or column.startswith(f"{source}.") for column in columns
|
||||
):
|
||||
return True
|
||||
return False
|
||||
|
||||
@@ -122,14 +124,16 @@ def blob_v2_projection_sources(
|
||||
blob_columns = row_addressable_blob_v2_paths(schema)
|
||||
if not blob_columns:
|
||||
return {}
|
||||
columns = set(blob_columns)
|
||||
if projection is None:
|
||||
return {column: column for column in blob_columns}
|
||||
return {
|
||||
output: source
|
||||
for output, source in _iter_projection_pairs(projection)
|
||||
if source in columns
|
||||
}
|
||||
sources = {}
|
||||
for output, source in _iter_projection_pairs(projection):
|
||||
for column in blob_columns:
|
||||
if column == source:
|
||||
sources[output] = column
|
||||
elif column.startswith(f"{source}."):
|
||||
sources[f"{output}{column[len(source) :]}"] = column
|
||||
return sources
|
||||
|
||||
|
||||
def v2_projection_needs_row_id(
|
||||
@@ -180,7 +184,7 @@ async def replace_v2_blob_columns_with_bytes(
|
||||
fetch_blobs: FetchBlobsAsync,
|
||||
) -> pa.Table:
|
||||
for output_name, source_name in blob_sources.items():
|
||||
if output_name not in tbl.column_names:
|
||||
if not _has_column_path(tbl.schema, output_name):
|
||||
continue
|
||||
blobs = await fetch_blobs(source_name, tbl)
|
||||
tbl = _set_blob_column(tbl, output_name, blobs)
|
||||
@@ -193,7 +197,7 @@ def replace_v2_blob_columns_with_bytes_sync(
|
||||
fetch_blobs: FetchBlobsSync,
|
||||
) -> pa.Table:
|
||||
for output_name, source_name in blob_sources.items():
|
||||
if output_name not in tbl.column_names:
|
||||
if not _has_column_path(tbl.schema, output_name):
|
||||
continue
|
||||
blobs = fetch_blobs(source_name, tbl)
|
||||
tbl = _set_blob_column(tbl, output_name, blobs)
|
||||
@@ -287,9 +291,46 @@ def _iter_projection_pairs(
|
||||
yield name, source if source is not None else expr.to_sql()
|
||||
|
||||
|
||||
def _set_blob_column(tbl: pa.Table, output_name: str, blobs: pa.Array) -> pa.Table:
|
||||
def _has_column_path(schema: pa.Schema, path: str) -> bool:
|
||||
if schema.get_field_index(path) >= 0:
|
||||
return True
|
||||
top_name, *rest = path.split(".")
|
||||
index = schema.get_field_index(top_name)
|
||||
if index < 0:
|
||||
return False
|
||||
field = schema.field(index)
|
||||
for name in rest:
|
||||
if not pa.types.is_struct(field.type):
|
||||
return False
|
||||
index = field.type.get_field_index(name)
|
||||
if index < 0:
|
||||
return False
|
||||
field = field.type.field(index)
|
||||
return True
|
||||
|
||||
|
||||
def _set_blob_column(
|
||||
tbl: pa.Table, output_name: str, blobs: pa.Array | pa.ChunkedArray
|
||||
) -> pa.Table:
|
||||
index = tbl.schema.get_field_index(output_name)
|
||||
return tbl.set_column(index, pa.field(output_name, blobs.type), [blobs])
|
||||
if index >= 0:
|
||||
return tbl.set_column(index, pa.field(output_name, blobs.type), [blobs])
|
||||
|
||||
parent_path, leaf_name = output_name.rsplit(".", 1)
|
||||
if isinstance(blobs, pa.ChunkedArray):
|
||||
blobs = blobs.combine_chunks()
|
||||
|
||||
def replace_blob(children: list, child_fields: list) -> None:
|
||||
for i, field in enumerate(child_fields):
|
||||
if field.name == leaf_name:
|
||||
children[i] = blobs
|
||||
child_fields[i] = pa.field(
|
||||
leaf_name, blobs.type, nullable=field.nullable
|
||||
)
|
||||
return
|
||||
raise KeyError(f"Blob field '{leaf_name}' not found in struct '{parent_path}'")
|
||||
|
||||
return _transform_struct_column(tbl, parent_path, replace_blob)
|
||||
|
||||
|
||||
def _embed_row_id_in_column(tbl: pa.Table, path: str, row_ids: pa.Array) -> pa.Table:
|
||||
|
||||
@@ -727,6 +727,7 @@ class VectorQuery:
|
||||
def use_lsm(self, enable: bool): ...
|
||||
def refine_factor(self, refine_factor: int): ...
|
||||
def nprobes(self, nprobes: int): ...
|
||||
def search_effort(self, search_effort: Optional[float]): ...
|
||||
def minimum_nprobes(self, minimum_nprobes: int): ...
|
||||
def maximum_nprobes(self, maximum_nprobes: int): ...
|
||||
def bypass_vector_index(self): ...
|
||||
@@ -752,6 +753,7 @@ class HybridQuery:
|
||||
def distance_type(self, distance_type: str): ...
|
||||
def refine_factor(self, refine_factor: int): ...
|
||||
def nprobes(self, nprobes: int): ...
|
||||
def search_effort(self, search_effort: Optional[float]): ...
|
||||
def minimum_nprobes(self, minimum_nprobes: int): ...
|
||||
def maximum_nprobes(self, maximum_nprobes: int): ...
|
||||
def bypass_vector_index(self): ...
|
||||
@@ -781,6 +783,7 @@ class PyQueryRequest:
|
||||
nprobes: Optional[int]
|
||||
minimum_nprobes: Optional[int]
|
||||
maximum_nprobes: Optional[int]
|
||||
search_effort: Optional[float]
|
||||
lower_bound: Optional[float]
|
||||
upper_bound: Optional[float]
|
||||
ef: Optional[int]
|
||||
|
||||
@@ -105,6 +105,11 @@ def _query_to_namespace_request(
|
||||
|
||||
if not isinstance(query, Query):
|
||||
raise TypeError(f"Expected Query, got {type(query)}")
|
||||
if query.search_effort is not None:
|
||||
raise ValueError(
|
||||
"namespace QueryTable does not support search_effort; "
|
||||
"execute the query locally"
|
||||
)
|
||||
|
||||
vector_request = None
|
||||
if query.vector is not None:
|
||||
|
||||
@@ -475,8 +475,8 @@ class MatchQuery(FullTextQuery):
|
||||
----------
|
||||
query : str
|
||||
The query string to match against.
|
||||
column : str
|
||||
The name of the column to match against.
|
||||
column : str, optional
|
||||
The column to match against. None leaves column selection to query planning.
|
||||
boost : float, default 1.0
|
||||
The boost factor for the query.
|
||||
The score of each matching document is multiplied by this value.
|
||||
@@ -505,7 +505,7 @@ class MatchQuery(FullTextQuery):
|
||||
"""
|
||||
|
||||
query: str
|
||||
column: str
|
||||
column: Optional[str] = None
|
||||
boost: float = pydantic.Field(1.0, kw_only=True)
|
||||
fuzziness: Optional[int] = pydantic.Field(0, kw_only=True)
|
||||
max_expansions: int = pydantic.Field(50, kw_only=True)
|
||||
@@ -793,6 +793,9 @@ class Query(pydantic.BaseModel):
|
||||
# and all partitions could be searched if needed to satisfy the limit.
|
||||
maximum_nprobes: Optional[int] = None
|
||||
|
||||
# Optional initial IVF effort; None preserves Auto and explicit nprobes.
|
||||
search_effort: Optional[float] = None
|
||||
|
||||
# lower bound for distance search
|
||||
lower_bound: Optional[float] = None
|
||||
|
||||
@@ -854,6 +857,7 @@ class Query(pydantic.BaseModel):
|
||||
query.nprobes = req.nprobes
|
||||
query.minimum_nprobes = req.minimum_nprobes
|
||||
query.maximum_nprobes = req.maximum_nprobes
|
||||
query.search_effort = req.search_effort
|
||||
query.lower_bound = req.lower_bound
|
||||
query.upper_bound = req.upper_bound
|
||||
query.ef = req.ef
|
||||
@@ -1635,6 +1639,7 @@ class LanceVectorQueryBuilder(LanceQueryBuilder):
|
||||
self._nprobes = None
|
||||
self._minimum_nprobes = None
|
||||
self._maximum_nprobes = None
|
||||
self._search_effort = None
|
||||
self._lower_bound = None
|
||||
self._upper_bound = None
|
||||
self._refine_factor = None
|
||||
@@ -1716,6 +1721,27 @@ class LanceVectorQueryBuilder(LanceQueryBuilder):
|
||||
self._nprobes = nprobes
|
||||
return self
|
||||
|
||||
def search_effort(self, search_effort: Optional[float]) -> LanceVectorQueryBuilder:
|
||||
"""Set the initial IVF search effort to a finite value in [0, 1].
|
||||
|
||||
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
|
||||
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. MemWAL LSM queries do not
|
||||
yet support this option.
|
||||
|
||||
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
|
||||
return self
|
||||
|
||||
def minimum_nprobes(self, minimum_nprobes: int) -> LanceVectorQueryBuilder:
|
||||
"""Set the minimum number of probes to use.
|
||||
|
||||
@@ -1858,6 +1884,7 @@ class LanceVectorQueryBuilder(LanceQueryBuilder):
|
||||
nprobes=self._nprobes,
|
||||
minimum_nprobes=self._minimum_nprobes,
|
||||
maximum_nprobes=self._maximum_nprobes,
|
||||
search_effort=self._search_effort,
|
||||
lower_bound=self._lower_bound,
|
||||
upper_bound=self._upper_bound,
|
||||
refine_factor=self._refine_factor,
|
||||
@@ -2217,6 +2244,7 @@ class LanceHybridQueryBuilder(LanceQueryBuilder):
|
||||
self._nprobes = None
|
||||
self._minimum_nprobes = None
|
||||
self._maximum_nprobes = None
|
||||
self._search_effort = None
|
||||
self._refine_factor = None
|
||||
self._distance_type = None
|
||||
self._phrase_query = None
|
||||
@@ -2524,6 +2552,32 @@ class LanceHybridQueryBuilder(LanceQueryBuilder):
|
||||
self._nprobes = nprobes
|
||||
return self
|
||||
|
||||
def search_effort(self, search_effort: Optional[float]) -> LanceHybridQueryBuilder:
|
||||
"""Set the initial IVF search effort to a finite value in [0, 1].
|
||||
|
||||
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
|
||||
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. MemWAL LSM queries do not
|
||||
yet support this option.
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> 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
|
||||
return self
|
||||
|
||||
def minimum_nprobes(self, minimum_nprobes: int) -> LanceHybridQueryBuilder:
|
||||
"""Set the minimum number of probes to use.
|
||||
|
||||
@@ -2802,6 +2856,8 @@ class LanceHybridQueryBuilder(LanceQueryBuilder):
|
||||
self._vector_query.metric(self._distance_type)
|
||||
if self._nprobes is not None:
|
||||
self._vector_query.nprobes(self._nprobes)
|
||||
if self._search_effort is not None:
|
||||
self._vector_query.search_effort(self._search_effort)
|
||||
if self._minimum_nprobes is not None:
|
||||
self._vector_query.minimum_nprobes(self._minimum_nprobes)
|
||||
if self._maximum_nprobes is not None:
|
||||
@@ -3730,6 +3786,32 @@ class AsyncVectorQueryBase:
|
||||
self._inner.nprobes(nprobes)
|
||||
return self
|
||||
|
||||
def search_effort(self, search_effort: Optional[float]) -> Self:
|
||||
"""Set the initial IVF search effort to a finite value in [0, 1].
|
||||
|
||||
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
|
||||
minimum and one searches all partitions within the maximum. Later
|
||||
probing may continue to obtain enough candidates. This is not a recall
|
||||
guarantee and does not change refinement or HNSW ef. MemWAL LSM queries
|
||||
do not yet support this option.
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> 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
|
||||
|
||||
def minimum_nprobes(self, minimum_nprobes: int) -> Self:
|
||||
"""Set the minimum number of probes to use.
|
||||
|
||||
|
||||
@@ -4440,6 +4440,7 @@ class LanceTable(Table):
|
||||
and not self._route_pushdown_to_rust
|
||||
and self.current_branch() is None
|
||||
and query.take_offsets is None
|
||||
and query.search_effort is None
|
||||
):
|
||||
from lancedb.namespace import _execute_server_side_query
|
||||
|
||||
@@ -6441,6 +6442,8 @@ class AsyncTable:
|
||||
async_query = async_query.distance_type(query.distance_type)
|
||||
if query.nprobes is not None:
|
||||
async_query = async_query.nprobes(query.nprobes)
|
||||
if query.search_effort is not None:
|
||||
async_query = async_query.search_effort(query.search_effort)
|
||||
if query.minimum_nprobes is not None and query.maximum_nprobes is not None:
|
||||
async_query = async_query.minimum_nprobes(
|
||||
query.minimum_nprobes
|
||||
@@ -6481,6 +6484,7 @@ class AsyncTable:
|
||||
)
|
||||
and not self._route_pushdown_to_rust
|
||||
and query.take_offsets is None
|
||||
and query.search_effort is None
|
||||
):
|
||||
from lancedb.namespace import _execute_server_side_query
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@ from lance.blob import Blob, BlobType as LanceBlobType
|
||||
|
||||
import lancedb
|
||||
from lancedb._blob import (
|
||||
_set_blob_column,
|
||||
blob_v2_projection_sources,
|
||||
read_row_ids_from_hits,
|
||||
stash_auto_row_ids,
|
||||
@@ -297,6 +298,121 @@ def test_blob_v2_projection_sources_use_typed_column_name():
|
||||
}
|
||||
|
||||
|
||||
def test_blob_v2_projection_sources_include_selected_struct():
|
||||
schema = pa.schema(
|
||||
[
|
||||
pa.field(
|
||||
"info", pa.struct([pa.field("name", pa.string()), lancedb.blob("blob")])
|
||||
)
|
||||
]
|
||||
)
|
||||
|
||||
assert blob_v2_projection_sources(schema, ["info"]) == {"info.blob": "info.blob"}
|
||||
assert blob_v2_projection_sources(schema, {"renamed": "info"}) == {
|
||||
"renamed.blob": "info.blob"
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.parametrize("parent_path", ["info", "outer.info"])
|
||||
def test_set_blob_column_missing_nested_field_raises(parent_path):
|
||||
row = {"name": "first"}
|
||||
for name in reversed(parent_path.split(".")):
|
||||
row = {name: row}
|
||||
table = pa.Table.from_pylist([row])
|
||||
blobs = pa.array([b"hello"], type=pa.large_binary())
|
||||
|
||||
with pytest.raises(KeyError) as exc_info:
|
||||
_set_blob_column(table, f"{parent_path}.blob", blobs)
|
||||
|
||||
assert parent_path in str(exc_info.value)
|
||||
assert "'blob'" in str(exc_info.value)
|
||||
|
||||
|
||||
def _nested_blob_table(db, name):
|
||||
schema = pa.schema(
|
||||
[
|
||||
pa.field("id", pa.int64()),
|
||||
pa.field(
|
||||
"info", pa.struct([pa.field("name", pa.string()), lancedb.blob("blob")])
|
||||
),
|
||||
]
|
||||
)
|
||||
table = db.create_table(name, schema=schema)
|
||||
info = pa.StructArray.from_arrays(
|
||||
[
|
||||
pa.array(["first", "second", "third"]),
|
||||
pa.array([b"hello", None, b"hidden"], type=pa.large_binary()),
|
||||
],
|
||||
names=["name", "blob"],
|
||||
mask=pa.array([False, False, True]),
|
||||
)
|
||||
table.add(pa.table({"id": pa.array([0, 1, 2]), "info": info}))
|
||||
return table
|
||||
|
||||
|
||||
def test_nested_blob_to_pandas_bytes(tmp_path):
|
||||
table = _nested_blob_table(lancedb.connect(tmp_path), "nested_pandas_bytes")
|
||||
expected = [
|
||||
{"name": "first", "blob": b"hello"},
|
||||
{"name": "second", "blob": None},
|
||||
None,
|
||||
]
|
||||
|
||||
assert table.to_pandas(blob_mode="bytes")["info"].tolist() == expected
|
||||
assert table.search().to_pandas(blob_mode="bytes")["info"].tolist() == expected
|
||||
assert (
|
||||
table.search().select(["info"]).to_pandas(blob_mode="bytes")["info"].tolist()
|
||||
== expected
|
||||
)
|
||||
assert (
|
||||
table.search()
|
||||
.select({"renamed": "info"})
|
||||
.to_pandas(blob_mode="bytes")["renamed"]
|
||||
.tolist()
|
||||
== expected
|
||||
)
|
||||
assert table.search().select({"b": "info.blob"}).to_pandas(blob_mode="bytes")[
|
||||
"b"
|
||||
].tolist() == [b"hello", None, None]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_nested_blob_to_pandas_bytes_async(tmp_path):
|
||||
db = await lancedb.connect_async(tmp_path)
|
||||
schema = pa.schema(
|
||||
[
|
||||
pa.field("id", pa.int64()),
|
||||
pa.field(
|
||||
"info", pa.struct([pa.field("name", pa.string()), lancedb.blob("blob")])
|
||||
),
|
||||
]
|
||||
)
|
||||
table = await db.create_table("nested_pandas_bytes_async", schema=schema)
|
||||
await table.add(
|
||||
pa.table(
|
||||
{
|
||||
"id": pa.array([0, 1]),
|
||||
"info": pa.StructArray.from_arrays(
|
||||
[
|
||||
pa.array(["first", "second"]),
|
||||
pa.array([b"hello", None], type=pa.large_binary()),
|
||||
],
|
||||
names=["name", "blob"],
|
||||
),
|
||||
}
|
||||
)
|
||||
)
|
||||
expected = [
|
||||
{"name": "first", "blob": b"hello"},
|
||||
{"name": "second", "blob": None},
|
||||
]
|
||||
|
||||
assert (await table.to_pandas(blob_mode="bytes"))["info"].tolist() == expected
|
||||
assert (await table.query().to_pandas(blob_mode="bytes"))[
|
||||
"info"
|
||||
].tolist() == expected
|
||||
|
||||
|
||||
def _legacy_v1_table(name):
|
||||
# Legacy v1 blob columns are only writable at file version <= 2.1.
|
||||
db = lancedb.connect(
|
||||
|
||||
@@ -1209,3 +1209,15 @@ def test_local_table_to_arrow_and_to_pandas_are_unchanged(tmp_path):
|
||||
|
||||
assert table.to_arrow().column("id").to_pylist() == [1, 2]
|
||||
assert table.to_pandas()["id"].tolist() == [1, 2]
|
||||
|
||||
|
||||
@pytest.mark.parametrize("effort", [0.0, 0.5, 1.0])
|
||||
def test_namespace_request_rejects_search_effort(effort):
|
||||
from lancedb.query import Query
|
||||
|
||||
with pytest.raises(
|
||||
ValueError, match="namespace QueryTable does not support search_effort"
|
||||
):
|
||||
_query_to_namespace_request(
|
||||
["test"], Query(vector=[1.0, 0.0], search_effort=effort)
|
||||
)
|
||||
|
||||
@@ -2265,3 +2265,127 @@ def test_blob_v2_to_batches_row_id(tmp_db):
|
||||
assert "_rowid" in hits.column_names
|
||||
blobs = table.fetch_blobs("blob", hits)
|
||||
assert [blobs[i].as_py() for i in range(len(blobs))] == [b"one", b"two"]
|
||||
|
||||
|
||||
@pytest.mark.parametrize("metric", ["l2", "cosine", "dot"])
|
||||
@pytest.mark.parametrize(
|
||||
"effort,probes", [(None, 4), (0, 1), (0.25, 2), (0.5, 4), (0.75, 8), (1, 16)]
|
||||
)
|
||||
def test_search_effort_probe_budget(tmp_path, monkeypatch, metric, effort, probes):
|
||||
import re
|
||||
from lancedb.index import IvfFlat
|
||||
|
||||
monkeypatch.setenv("LANCE_AUTO_PROBE_MARGIN", "0")
|
||||
monkeypatch.setenv("LANCE_AUTO_MIN_INITIAL_NPROBES", "4")
|
||||
monkeypatch.setenv("LANCE_AUTO_MAX_INITIAL_NPROBES", "4")
|
||||
rng = np.random.default_rng(42)
|
||||
vectors = np.repeat(np.eye(16, dtype=np.float32), 32, axis=0)
|
||||
vectors += rng.normal(0, 0.001, vectors.shape).astype(np.float32)
|
||||
data = pa.table(
|
||||
{
|
||||
"id": np.arange(512),
|
||||
"text": ["dog"] * 512,
|
||||
"vector": pa.FixedSizeListArray.from_arrays(pa.array(vectors.ravel()), 16),
|
||||
}
|
||||
)
|
||||
table = lancedb.connect(tmp_path).create_table("effort", data.slice(0, 256))
|
||||
table.add(data.slice(256))
|
||||
table.create_index(
|
||||
"vector", config=IvfFlat(distance_type=metric, num_partitions=16)
|
||||
)
|
||||
vector = np.eye(16, dtype=np.float32)[0]
|
||||
|
||||
def search(value=effort):
|
||||
return table.search(vector).distance_type(metric).search_effort(value).limit(8)
|
||||
|
||||
def partitions(query):
|
||||
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))
|
||||
|
||||
assert partitions(search()) == probes
|
||||
exact = set(
|
||||
table.search(vector)
|
||||
.distance_type(metric)
|
||||
.bypass_vector_index()
|
||||
.limit(8)
|
||||
.to_arrow()["id"]
|
||||
.to_pylist()
|
||||
)
|
||||
actual = set(search().to_arrow()["id"].to_pylist())
|
||||
assert len(actual & exact) / 8 >= 0.5
|
||||
expected = 1 if effort is None else probes
|
||||
assert partitions(search().nprobes(1)) == expected
|
||||
assert (
|
||||
partitions(
|
||||
table.search(vector)
|
||||
.distance_type(metric)
|
||||
.nprobes(1)
|
||||
.search_effort(effort)
|
||||
.limit(8)
|
||||
)
|
||||
== expected
|
||||
)
|
||||
assert partitions(search().nprobes(1).search_effort(None)) == 1
|
||||
assert partitions(search().maximum_nprobes(2)) <= 2
|
||||
assert partitions(search().minimum_nprobes(3).maximum_nprobes(3)) == 3
|
||||
assert partitions(search(0).nprobes(16).maximum_nprobes(0)) == 1
|
||||
if effort is None:
|
||||
assert search().to_arrow() == search(0.5).to_arrow()
|
||||
filtered = search(0).maximum_nprobes(2).where("id % 8 = 0", prefilter=True)
|
||||
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")]
|
||||
)
|
||||
def test_search_effort_invalid_sync(table, effort):
|
||||
with pytest.raises(ValueError, match="search_effort must be finite and in"):
|
||||
table.search([0, 0]).search_effort(effort).to_list()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.parametrize("hybrid", [False, True])
|
||||
@pytest.mark.parametrize("effort", [None, 0, 0.25, 0.5, 0.75, 1])
|
||||
async def test_search_effort_async_roundtrip(table_async, hybrid, effort):
|
||||
query = table_async.vector_search([0, 0])
|
||||
if hybrid:
|
||||
query = query.nearest_to_text("dog")
|
||||
query = query.nprobes(5).search_effort(effort).minimum_nprobes(2).maximum_nprobes(3)
|
||||
request = query.to_query_object()
|
||||
assert request.search_effort == effort
|
||||
assert request.nprobes == 5
|
||||
assert request.minimum_nprobes == 2
|
||||
assert request.maximum_nprobes == 3
|
||||
assert query.search_effort(None).to_query_object().search_effort is None
|
||||
query.search_effort(effort)
|
||||
if not hybrid:
|
||||
assert len(await query.limit(1).to_list()) == 1
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.parametrize("hybrid", [False, True])
|
||||
@pytest.mark.parametrize(
|
||||
"effort", [-0.1, 1.1, float("nan"), float("inf"), -float("inf")]
|
||||
)
|
||||
async def test_search_effort_invalid_async(table_async, hybrid, effort):
|
||||
query = table_async.vector_search([0, 0])
|
||||
if hybrid:
|
||||
query = query.nearest_to_text("dog")
|
||||
with pytest.raises(ValueError, match="search_effort must be finite and in"):
|
||||
query.search_effort(effort)
|
||||
|
||||
+31
-3
@@ -71,7 +71,7 @@ impl<'a, 'py> FromPyObject<'a, 'py> for PyLanceDB<FtsQuery> {
|
||||
match class_name(&ob)?.as_str() {
|
||||
"MatchQuery" => {
|
||||
let query = ob.getattr("query")?.extract()?;
|
||||
let column = ob.getattr("column")?.extract()?;
|
||||
let column = ob.getattr("column")?.extract::<Option<String>>()?;
|
||||
let boost = ob.getattr("boost")?.extract()?;
|
||||
let fuzziness = ob.getattr("fuzziness")?.extract()?;
|
||||
let max_expansions = ob.getattr("max_expansions")?.extract()?;
|
||||
@@ -88,7 +88,7 @@ impl<'a, 'py> FromPyObject<'a, 'py> for PyLanceDB<FtsQuery> {
|
||||
|
||||
let mut query =
|
||||
MatchQuery::new(query)
|
||||
.with_column(Some(column))
|
||||
.with_column(column)
|
||||
.with_boost(boost)
|
||||
.with_fuzziness(fuzziness)
|
||||
.with_max_expansions(max_expansions)
|
||||
@@ -199,7 +199,7 @@ impl<'py> IntoPyObject<'py> for PyLanceDB<FtsQuery> {
|
||||
}
|
||||
namespace
|
||||
.getattr(intern!(py, "MatchQuery"))?
|
||||
.call((query.terms, query.column.unwrap()), Some(&kwargs))
|
||||
.call((query.terms, query.column), Some(&kwargs))
|
||||
}
|
||||
FtsQuery::Phrase(query) => {
|
||||
let kwargs = PyDict::new(py);
|
||||
@@ -339,6 +339,7 @@ pub struct PyQueryRequest {
|
||||
pub nprobes: Option<usize>,
|
||||
pub minimum_nprobes: Option<usize>,
|
||||
pub maximum_nprobes: Option<usize>,
|
||||
pub search_effort: Option<f64>,
|
||||
pub lower_bound: Option<f32>,
|
||||
pub upper_bound: Option<f32>,
|
||||
pub ef: Option<usize>,
|
||||
@@ -371,6 +372,7 @@ impl From<AnyQuery> for PyQueryRequest {
|
||||
nprobes: None,
|
||||
minimum_nprobes: None,
|
||||
maximum_nprobes: None,
|
||||
search_effort: None,
|
||||
lower_bound: None,
|
||||
upper_bound: None,
|
||||
ef: None,
|
||||
@@ -399,6 +401,7 @@ impl From<AnyQuery> for PyQueryRequest {
|
||||
nprobes: vector_query.nprobes,
|
||||
minimum_nprobes: vector_query.minimum_nprobes,
|
||||
maximum_nprobes: vector_query.maximum_nprobes,
|
||||
search_effort: vector_query.search_effort,
|
||||
lower_bound: vector_query.lower_bound,
|
||||
upper_bound: vector_query.upper_bound,
|
||||
ef: vector_query.ef,
|
||||
@@ -998,6 +1001,15 @@ impl VectorQuery {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn search_effort(&mut self, search_effort: Option<f64>) -> PyResult<()> {
|
||||
self.inner = self
|
||||
.inner
|
||||
.clone()
|
||||
.search_effort(search_effort)
|
||||
.infer_error()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn minimum_nprobes(&mut self, minimum_nprobes: u32) -> PyResult<()> {
|
||||
self.inner = self
|
||||
.inner
|
||||
@@ -1191,6 +1203,18 @@ impl HybridQuery {
|
||||
self.inner_vec.nprobes(nprobe)
|
||||
}
|
||||
|
||||
pub fn search_effort(&mut self, search_effort: Option<f64>) -> PyResult<()> {
|
||||
self.inner_vec.search_effort(search_effort)
|
||||
}
|
||||
|
||||
pub fn minimum_nprobes(&mut self, minimum_nprobes: u32) -> PyResult<()> {
|
||||
self.inner_vec.minimum_nprobes(minimum_nprobes)
|
||||
}
|
||||
|
||||
pub fn maximum_nprobes(&mut self, maximum_nprobes: u32) -> PyResult<()> {
|
||||
self.inner_vec.maximum_nprobes(maximum_nprobes)
|
||||
}
|
||||
|
||||
pub fn ef(&mut self, ef: u32) {
|
||||
self.inner_vec.ef(ef);
|
||||
}
|
||||
@@ -1235,6 +1259,10 @@ impl HybridQuery {
|
||||
req.query_vector = vec_req.query_vector;
|
||||
req.column = vec_req.column;
|
||||
req.distance_type = vec_req.distance_type;
|
||||
req.nprobes = vec_req.nprobes;
|
||||
req.minimum_nprobes = vec_req.minimum_nprobes;
|
||||
req.maximum_nprobes = vec_req.maximum_nprobes;
|
||||
req.search_effort = vec_req.search_effort;
|
||||
req.ef = vec_req.ef;
|
||||
req.refine_factor = vec_req.refine_factor;
|
||||
req.lower_bound = vec_req.lower_bound;
|
||||
|
||||
Generated
+34
-8
@@ -1900,6 +1900,7 @@ clip = [
|
||||
{ name = "torch" },
|
||||
]
|
||||
dev = [
|
||||
{ name = "maturin" },
|
||||
{ name = "pre-commit" },
|
||||
{ name = "pyright" },
|
||||
{ name = "ruff" },
|
||||
@@ -1981,6 +1982,7 @@ requires-dist = [
|
||||
{ name = "ibm-watsonx-ai", marker = "python_full_version >= '3.10' and extra == 'embeddings'", specifier = ">=1.1.2" },
|
||||
{ name = "instructorembedding", marker = "extra == 'embeddings'", specifier = ">=1.0.1" },
|
||||
{ name = "lance-namespace", specifier = ">=0.13.0" },
|
||||
{ name = "maturin", marker = "extra == 'dev'", specifier = ">=1.10" },
|
||||
{ name = "mkdocs", marker = "extra == 'docs'" },
|
||||
{ name = "mkdocs-jupyter", marker = "extra == 'docs'" },
|
||||
{ name = "mkdocs-material", marker = "extra == 'docs'" },
|
||||
@@ -2005,7 +2007,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 = ">=5.0.0b5" },
|
||||
{ name = "pylance", marker = "extra == 'tests'", specifier = "==14.0.0b10" },
|
||||
{ name = "pylance", marker = "extra == 'tests'", specifier = "==14.0.0b12" },
|
||||
{ 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" },
|
||||
@@ -2156,6 +2158,30 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/41/09/5b161152e2d90f7b87f781c2e1267494aef9c32498df793f73ad0a0a494a/matplotlib_inline-0.2.2-py3-none-any.whl", hash = "sha256:3c821cf1c209f59fb2d2d64abbf5b23b67bcb2210d663f9918dd851c6da1fcf6", size = 9534, upload-time = "2026-05-08T17:33:32.055Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "maturin"
|
||||
version = "1.15.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "tomli", marker = "python_full_version < '3.11'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/b9/c8/22e5e21b2679c9bce6415ca578034ca2cc9316be0642ae21e051a2d5198c/maturin-1.15.0.tar.gz", hash = "sha256:94b26cc8e8aba61a5f2099715fe640e18c5f678e9a500408b38761263954228a", size = 385504, upload-time = "2026-08-24T12:11:22.665Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/14/69/5c01b461044eb1f45ddcce006706eb88110c793cdb11c7ae0b5e08492e94/maturin-1.15.0-py3-none-linux_armv6l.whl", hash = "sha256:6bf6dc62e22d4dcfd5a51244ff0d58975fa4979c48209fe84159617648956d82", size = 10206220, upload-time = "2026-08-24T12:10:53.327Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/eb/1f/2b431554e11687cdb1077e0cdadcc118c53f611086b3af00c8545a67c6a5/maturin-1.15.0-py3-none-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:cd35772633f489841132bc8e71d6fc7f842df30b9c05cd5cdf1ee1ddcb744cc7", size = 19416513, upload-time = "2026-08-24T12:10:56.126Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/51/36/e23a21cb34a648b711036b9b2fe1d4f3f4ee24f8db54215d73f1a9a3a3ec/maturin-1.15.0-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:c40b4eae7bf5ef1f4b1af8d623fe4105016f93578fb15b764e741d08ec3b92dd", size = 10014962, upload-time = "2026-08-24T12:10:58.486Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8c/80/33b15cb2d8f30f12c807955e8f2fd775027692904e30ec0784744ce8cd83/maturin-1.15.0-py3-none-manylinux_2_12_i686.manylinux2010_i686.musllinux_1_1_i686.whl", hash = "sha256:7eb066372f541f8eb4909c79c5d9bd0b9e8125980bdf1ec9e8aba23c6c8d6c55", size = 10196223, upload-time = "2026-08-24T12:11:00.696Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fe/91/b495e19e2f5c503b540452b2039115e7b2363867e8c5ad4179eb752fa92c/maturin-1.15.0-py3-none-manylinux_2_12_x86_64.manylinux2010_x86_64.musllinux_1_1_x86_64.whl", hash = "sha256:653020a63525bb224e5ab0adf02e17a2e08bc86dbea7fc1399c9a56d7529b99e", size = 10541186, upload-time = "2026-08-24T12:11:02.857Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5d/2b/2abff58037188d852b124871b1f0d720e1c2bfb3d4f1b03d87c52cd66488/maturin-1.15.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.musllinux_1_1_aarch64.whl", hash = "sha256:0ebf9767892725083138e671c34482c660317a2f3d6a29fc0e0f34e9d8c99136", size = 10083468, upload-time = "2026-08-24T12:11:05.012Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3f/07/b7e9f8be99a6627849e81ac7b6694876bce8f50a92995fe17e3cf2610f0a/maturin-1.15.0-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.musllinux_1_1_armv7l.whl", hash = "sha256:7ab7eebffd7b8debca2265985de4eaeb332141276d24b9560b5ad484d4b3add1", size = 10047786, upload-time = "2026-08-24T12:11:07.042Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ca/ab/167e3cb7accee11b507dbe53e0e87aeccb376d44ae66284c96ee4df3a9fd/maturin-1.15.0-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.musllinux_1_1_ppc64le.whl", hash = "sha256:126e12e618b4db42f68c779a56d41f82a390145ba36ac3f621d057eb34f5ad9d", size = 13315332, upload-time = "2026-08-24T12:11:09.433Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/14/4d/801379f646cbc6b00998e5289b0630a886be3a4ee4c75b6bc9b87478a7f1/maturin-1.15.0-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4f9d33e6c3f9615c8caceecbbbd440f8eb25a3ddeb687077682cd5eca2e9ae15", size = 10807183, upload-time = "2026-08-24T12:11:11.73Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/89/27/2e612e1cbd1580e9e94d4722c227b5180dca27b32b955a34b79918aa1292/maturin-1.15.0-py3-none-manylinux_2_31_riscv64.musllinux_1_1_riscv64.whl", hash = "sha256:bf29beddd0c6708f112db51d5275fc28b28b9e9c9c5faae387eaef662918b176", size = 10413274, upload-time = "2026-08-24T12:11:14.04Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/70/d8/202a7b4d75a51f20f84ec9ce3b7345b12b822207072164e2b1c6ef665125/maturin-1.15.0-py3-none-win32.whl", hash = "sha256:da649988be98e87e009e51b1bf0d301b6a301bc0cecbdd60d40d8ba60748d1ca", size = 8928744, upload-time = "2026-08-24T12:11:16.269Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/40/dc/4e90da594986ba78dd3bc8a5921ecdcdb11085b22b02a412caab3b225601/maturin-1.15.0-py3-none-win_amd64.whl", hash = "sha256:552c2be4afd43fe8d5c9f3ec8d4c4756d973b8dcbe94c14084390301f50243e1", size = 10335085, upload-time = "2026-08-24T12:11:18.326Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8b/10/15d4314edf130955edf2dc237aa393a8a7c10f2b9b57b89fa2f61f915659/maturin-1.15.0-py3-none-win_arm64.whl", hash = "sha256:c7dc0c66c78d3debdd9c5aa807e861fbcbf07f3505d34b125df74c03986b0f48", size = 9713795, upload-time = "2026-08-24T12:11:20.83Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "mdit-py-plugins"
|
||||
version = "0.6.1"
|
||||
@@ -3912,7 +3938,7 @@ crypto = [
|
||||
|
||||
[[package]]
|
||||
name = "pylance"
|
||||
version = "14.0.0b10"
|
||||
version = "14.0.0b12"
|
||||
source = { registry = "https://pypi.fury.io/lance-format/" }
|
||||
dependencies = [
|
||||
{ name = "lance-namespace" },
|
||||
@@ -3922,12 +3948,12 @@ dependencies = [
|
||||
{ name = "pyarrow" },
|
||||
]
|
||||
wheels = [
|
||||
{ url = "https://pypi.fury.io/lance-format/-/ver_4hvE/pylance-14.0.0b10-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:415b6b1b9fe735fbae7c755eb9774ae2fe0feddc90bf80bee24087cbfbfdf8c1" },
|
||||
{ url = "https://pypi.fury.io/lance-format/-/ver_pM1km/pylance-14.0.0b10-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:87317cad22f4d35fea11eb70aa5c0d71a0e1ea3b96de38f3deca933daa749fff" },
|
||||
{ url = "https://pypi.fury.io/lance-format/-/ver_24jdyj/pylance-14.0.0b10-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6da554a2fc2197ba254676b12824d8120b48d65937697e0661a61302458a4740" },
|
||||
{ url = "https://pypi.fury.io/lance-format/-/ver_XclaZ/pylance-14.0.0b10-cp310-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:36c80a801675bb9dcbd681fa99d0cdd5ca642dfb3ca08d71ac980278a2f7748b" },
|
||||
{ url = "https://pypi.fury.io/lance-format/-/ver_L0yh/pylance-14.0.0b10-cp310-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:e50ad0b866faefb2fa3ae87328b93fa0d06b2bdd5bb5d0c1a6c74b907cc2fd93" },
|
||||
{ url = "https://pypi.fury.io/lance-format/-/ver_2gbNr8/pylance-14.0.0b10-cp310-abi3-win_amd64.whl", hash = "sha256:3d86451fe00f71510005bb700737fb1a4f9361554541cbf802f3c5072554d014" },
|
||||
{ url = "https://pypi.fury.io/lance-format/-/ver_1Ixasc/pylance-14.0.0b12-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:84350a3c5c1eea3398f4c4fad7cfe85b831d831a5e43b5ca2a783dbdb0e11969" },
|
||||
{ url = "https://pypi.fury.io/lance-format/-/ver_sCEkG/pylance-14.0.0b12-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:76b386c182a6a8e7b86fbef28f84d1dd39e4626ac8681a0cf36f40a45dd7cb87" },
|
||||
{ url = "https://pypi.fury.io/lance-format/-/ver_1vdT1C/pylance-14.0.0b12-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6aa548c7937fcdfa8f1943a2d59041a42f8b500199880dbb509c7195fa253381" },
|
||||
{ url = "https://pypi.fury.io/lance-format/-/ver_2fXu1V/pylance-14.0.0b12-cp310-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:79f82d6a6c50f37aab338470e3733436e6a27fbb5972d29fcf453ece56d3b3c6" },
|
||||
{ url = "https://pypi.fury.io/lance-format/-/ver_24UavN/pylance-14.0.0b12-cp310-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:de0305999257002785f60169e6926896584d66f1da050bb5afd1d48c6e3d9bb9" },
|
||||
{ url = "https://pypi.fury.io/lance-format/-/ver_XNynX/pylance-14.0.0b12-cp310-abi3-win_amd64.whl", hash = "sha256:4d0cdb689e898ce71e8e434d00a3b97639cea8fb97da21f1c921543be363268f" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
||||
Vendored
+1
-1
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "lance-arrow"
|
||||
version = "14.0.0-beta.13"
|
||||
version = "14.0.0-beta.15"
|
||||
edition = "2024"
|
||||
authors = ["Lance Devs <dev@lance.org>"]
|
||||
license = "Apache-2.0"
|
||||
|
||||
+2
-2
@@ -1,5 +1,5 @@
|
||||
This directory contains `rust/lance-arrow` from Lance tag `v14.0.0-beta.13`
|
||||
(commit `145fd4b933fb4a9a45831f0ae9185c88e908fe6a`), licensed under Apache-2.0.
|
||||
This directory contains `rust/lance-arrow` from Lance tag `v14.0.0-beta.15`
|
||||
(commit `765641dc0d7100f032fdebc00d73eef49711c4d4`), licensed under Apache-2.0.
|
||||
|
||||
LanceDB carries a local fix in `src/lib.rs` for sliced child validity bitmaps
|
||||
until a Lance release includes the fix. The patch is selected in the root
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "lancedb"
|
||||
version = "0.41.0-beta.5"
|
||||
version = "0.41.0-beta.6"
|
||||
edition.workspace = true
|
||||
description = "LanceDB: A serverless, low-latency vector database for AI applications"
|
||||
license.workspace = true
|
||||
|
||||
@@ -1127,6 +1127,11 @@ pub struct VectorQueryRequest {
|
||||
/// If unset, Lance's default is used. Zero represents an explicitly unbounded
|
||||
/// maximum on request and wire representations.
|
||||
pub maximum_nprobes: Option<usize>,
|
||||
/// Optional initial IVF search effort in [0, 1].
|
||||
///
|
||||
/// None preserves Auto and fixed `nprobes`; any explicit value overrides
|
||||
/// `nprobes` while retaining explicit minimum and maximum bounds.
|
||||
pub search_effort: Option<f64>,
|
||||
/// The lower bound (inclusive) of the distance to search for.
|
||||
pub lower_bound: Option<f32>,
|
||||
/// The upper bound (exclusive) of the distance to search for.
|
||||
@@ -1153,6 +1158,7 @@ impl Default for VectorQueryRequest {
|
||||
nprobes: None,
|
||||
minimum_nprobes: None,
|
||||
maximum_nprobes: None,
|
||||
search_effort: None,
|
||||
lower_bound: None,
|
||||
upper_bound: None,
|
||||
ef: None,
|
||||
@@ -1165,6 +1171,17 @@ impl Default for VectorQueryRequest {
|
||||
}
|
||||
|
||||
impl VectorQueryRequest {
|
||||
pub(crate) fn validate_search_effort(&self) -> Result<()> {
|
||||
if let Some(effort) = self.search_effort
|
||||
&& (!effort.is_finite() || !(0.0..=1.0).contains(&effort))
|
||||
{
|
||||
return Err(Error::InvalidInput {
|
||||
message: format!("search_effort must be finite and in [0, 1], got {effort}"),
|
||||
});
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn add_query_vector(&mut self, vector: impl IntoQueryVector) -> Result<()> {
|
||||
let vector = vector.to_query_vector(&DataType::Float32, "default")?;
|
||||
let array =
|
||||
@@ -1279,7 +1296,8 @@ impl VectorQuery {
|
||||
/// you the desired recall.
|
||||
///
|
||||
/// LanceDB retains this as `nprobes` through local and remote request
|
||||
/// construction. Lance sets both probe bounds to this value. For more
|
||||
/// construction. Lance sets both probe bounds to this value only when
|
||||
/// [`Self::search_effort`] is unset. For more
|
||||
/// fine-grained control see [`VectorQuery::minimum_nprobes`] and
|
||||
/// [`VectorQuery::maximum_nprobes`].
|
||||
///
|
||||
@@ -1294,6 +1312,31 @@ impl VectorQuery {
|
||||
Ok(self)
|
||||
}
|
||||
|
||||
/// Set the initial IVF search effort to a finite value in [0, 1].
|
||||
///
|
||||
/// 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.
|
||||
/// 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.
|
||||
/// This does not change refinement or HNSW ef and is not a recall guarantee.
|
||||
/// Lance's MemWAL LSM scanner does not yet support this option.
|
||||
///
|
||||
/// ```
|
||||
/// # use lancedb::{Table, query::{QueryBase, ExecutableQuery}};
|
||||
/// # async fn example(table: &Table) -> lancedb::Result<()> {
|
||||
/// let results = table.query().nearest_to([0.1, 0.2].as_slice())?
|
||||
/// .search_effort(0.75)?.execute().await?;
|
||||
/// # Ok(())
|
||||
/// # }
|
||||
/// ```
|
||||
pub fn search_effort(mut self, search_effort: impl Into<Option<f64>>) -> Result<Self> {
|
||||
self.request.search_effort = search_effort.into();
|
||||
self.request.validate_search_effort()?;
|
||||
Ok(self)
|
||||
}
|
||||
|
||||
/// Set the minimum number of partitions to search
|
||||
///
|
||||
/// This argument is only used when the vector column has an IVF PQ index.
|
||||
@@ -1333,6 +1376,8 @@ impl VectorQuery {
|
||||
///
|
||||
/// If this value is greater than minimum_nprobes then the excess partitions will
|
||||
/// only be searched if the initial search does not return enough results.
|
||||
/// The bound applies per index segment to initial and later probing without
|
||||
/// disabling Auto probing.
|
||||
///
|
||||
/// This can be useful when there is a narrow filter to allow these queries to
|
||||
/// spend more time searching and avoid potential false negatives.
|
||||
@@ -2448,6 +2493,26 @@ mod tests {
|
||||
assert_eq!(query.request.nprobes, None);
|
||||
assert_eq!(query.request.minimum_nprobes, None);
|
||||
assert_eq!(query.request.maximum_nprobes, None);
|
||||
assert_eq!(query.request.search_effort, None);
|
||||
|
||||
let effort_query = query
|
||||
.clone()
|
||||
.nprobes(20)
|
||||
.unwrap()
|
||||
.search_effort(0.5)
|
||||
.unwrap();
|
||||
assert_eq!(effort_query.request.search_effort, Some(0.5));
|
||||
assert_eq!(effort_query.request.nprobes, Some(20));
|
||||
assert_eq!(effort_query.request.minimum_nprobes, None);
|
||||
assert_eq!(effort_query.request.maximum_nprobes, None);
|
||||
assert_eq!(
|
||||
effort_query
|
||||
.search_effort(None)
|
||||
.unwrap()
|
||||
.request
|
||||
.search_effort,
|
||||
None
|
||||
);
|
||||
assert_eq!(
|
||||
*query
|
||||
.request
|
||||
|
||||
@@ -1137,6 +1137,7 @@ impl<S: HttpSend> RemoteTable<S> {
|
||||
mut body: serde_json::Value,
|
||||
query: &VectorQueryRequest,
|
||||
) -> Result<Vec<serde_json::Value>> {
|
||||
query.validate_search_effort()?;
|
||||
self.apply_query_params(&mut body, &query.base)?;
|
||||
|
||||
// Apply general parameters, before we dispatch based on number of query vectors.
|
||||
@@ -1155,6 +1156,9 @@ impl<S: HttpSend> RemoteTable<S> {
|
||||
if let Some(maximum_nprobes) = query.maximum_nprobes {
|
||||
body["maximum_nprobes"] = maximum_nprobes.into();
|
||||
}
|
||||
if let Some(search_effort) = query.search_effort {
|
||||
body["search_effort"] = search_effort.into();
|
||||
}
|
||||
body["lower_bound"] = query.lower_bound.into();
|
||||
body["upper_bound"] = query.upper_bound.into();
|
||||
body["ef"] = query.ef.into();
|
||||
@@ -6269,6 +6273,77 @@ mod tests {
|
||||
));
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
#[case::unset(None)]
|
||||
#[case::minimum(Some(0.0))]
|
||||
#[case::low(Some(0.25))]
|
||||
#[case::auto(Some(0.5))]
|
||||
#[case::high(Some(0.75))]
|
||||
#[case::maximum(Some(1.0))]
|
||||
#[tokio::test]
|
||||
async fn test_query_vector_search_effort_wire(#[case] effort: Option<f64>) {
|
||||
let table = Table::new_with_handler("my_table", move |request| {
|
||||
let body = request_body_json(&request);
|
||||
assert_eq!(body["nprobes"], 7);
|
||||
assert_eq!(body["minimum_nprobes"], 2);
|
||||
assert_eq!(body["maximum_nprobes"], 8);
|
||||
if let Some(effort) = effort {
|
||||
assert_eq!(body["search_effort"], effort);
|
||||
} else {
|
||||
assert!(body.get("search_effort").is_none());
|
||||
}
|
||||
let batch = RecordBatch::try_new(
|
||||
Arc::new(Schema::new(vec![Field::new("id", DataType::Int32, false)])),
|
||||
vec![Arc::new(Int32Array::from(vec![1]))],
|
||||
)
|
||||
.unwrap();
|
||||
http::Response::builder()
|
||||
.status(200)
|
||||
.header(CONTENT_TYPE, ARROW_FILE_CONTENT_TYPE)
|
||||
.body(write_ipc_file(&batch))
|
||||
.unwrap()
|
||||
});
|
||||
let batches = table
|
||||
.query()
|
||||
.nearest_to(vec![0.1, 0.2])
|
||||
.unwrap()
|
||||
.search_effort(effort)
|
||||
.unwrap()
|
||||
.nprobes(7)
|
||||
.unwrap()
|
||||
.minimum_nprobes(2)
|
||||
.unwrap()
|
||||
.maximum_nprobes(Some(8))
|
||||
.unwrap()
|
||||
.execute()
|
||||
.await
|
||||
.unwrap()
|
||||
.try_collect::<Vec<_>>()
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(batches[0].num_rows(), 1);
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
#[case::negative(-0.1)]
|
||||
#[case::too_large(1.1)]
|
||||
#[case::nan(f64::NAN)]
|
||||
#[case::infinity(f64::INFINITY)]
|
||||
#[case::negative_infinity(f64::NEG_INFINITY)]
|
||||
#[tokio::test]
|
||||
async fn test_query_vector_search_effort_invalid(#[case] effort: f64) {
|
||||
let table = Table::new_with_handler::<&str>("my_table", |_| {
|
||||
panic!("invalid search_effort must not send a request")
|
||||
});
|
||||
let result = table
|
||||
.query()
|
||||
.nearest_to(vec![0.1, 0.2])
|
||||
.unwrap()
|
||||
.search_effort(effort);
|
||||
assert!(matches!(result, Err(Error::InvalidInput { message })
|
||||
if message.contains("search_effort must be finite and in [0, 1]")));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_query_vector_all_params() {
|
||||
let table = Table::new_with_handler("my_table", |request| {
|
||||
|
||||
@@ -19,6 +19,9 @@ pub use lance::dataset::ReadParams;
|
||||
pub use lance::dataset::Version;
|
||||
use lance::dataset::WriteMode;
|
||||
use lance::dataset::builder::DatasetBuilder;
|
||||
pub use lance::dataset::mem_wal::MemIndexRegistry;
|
||||
/// The traits a memtable index plugin implements; see [`MemIndexRegistry`].
|
||||
pub use lance::dataset::mem_wal::index as mem_index;
|
||||
use lance::dataset::{InsertBuilder, WriteParams};
|
||||
use lance::index::DatasetIndexExt;
|
||||
use lance::index::scalar::load_segment_params;
|
||||
@@ -424,7 +427,8 @@ impl LsmWriteSpec {
|
||||
/// changes, so an index created later is maintained too; one of a kind the
|
||||
/// MemWAL cannot mirror is skipped rather than failing the table. A list is
|
||||
/// verbatim: each name must already exist and be maintainable, and an empty
|
||||
/// list maintains nothing.
|
||||
/// list maintains nothing. The maintainable kinds are Lance's built-ins plus
|
||||
/// any added with [`NativeTable::set_mem_index_registry`].
|
||||
///
|
||||
/// ```
|
||||
/// # use lancedb::table::LsmWriteSpec;
|
||||
@@ -2590,6 +2594,9 @@ pub struct NativeTable {
|
||||
pub(crate) pushdown_operations: HashSet<NamespaceClientPushdownOperation>,
|
||||
// Read-freshness baseline; `Some` only for namespace-backed tables.
|
||||
freshness: Option<TableFreshness>,
|
||||
// Memtable index kinds this handle's LSM writes maintain, shared with its
|
||||
// clones and branch handles.
|
||||
mem_index_registry: Arc<std::sync::RwLock<MemIndexRegistry>>,
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for NativeTable {
|
||||
@@ -2756,9 +2763,51 @@ impl NativeTable {
|
||||
namespace_client,
|
||||
pushdown_operations,
|
||||
freshness: None,
|
||||
mem_index_registry: Default::default(),
|
||||
})
|
||||
}
|
||||
|
||||
/// Maintain the memtable index kinds in `registry` when this table writes
|
||||
/// through its [`LsmWriteSpec`], and accept them when
|
||||
/// [`LsmWriteSpec::with_maintained_indexes`] names one.
|
||||
///
|
||||
/// The registry belongs to this handle, its clones and its branch handles,
|
||||
/// and is not stored with the table: a handle opened separately starts from
|
||||
/// Lance's built-ins. Set it before the handle's first LSM write; writers
|
||||
/// already open keep the registry they were opened with. Local tables only,
|
||||
/// reached through [`Table::as_native`].
|
||||
///
|
||||
/// ```
|
||||
/// # use std::sync::Arc;
|
||||
/// # use lancedb::Table;
|
||||
/// # use lancedb::table::{LsmWriteSpec, MemIndexRegistry, mem_index::MemIndexPlugin};
|
||||
/// # async fn example(
|
||||
/// # table: &Table,
|
||||
/// # plugin: Arc<dyn MemIndexPlugin>,
|
||||
/// # ) -> Result<(), Box<dyn std::error::Error>> {
|
||||
/// let registry = MemIndexRegistry::default().with_plugin(plugin)?;
|
||||
/// if let Some(native) = table.as_native() {
|
||||
/// native.set_mem_index_registry(registry);
|
||||
/// }
|
||||
/// let spec = LsmWriteSpec::unsharded().with_maintained_indexes(vec!["region_bitmap".to_string()]);
|
||||
/// table.set_lsm_write_spec(spec).await?;
|
||||
/// # Ok(())
|
||||
/// # }
|
||||
/// ```
|
||||
pub fn set_mem_index_registry(&self, registry: MemIndexRegistry) {
|
||||
*self
|
||||
.mem_index_registry
|
||||
.write()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner) = registry;
|
||||
}
|
||||
|
||||
pub(crate) fn mem_index_registry(&self) -> MemIndexRegistry {
|
||||
self.mem_index_registry
|
||||
.read()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner)
|
||||
.clone()
|
||||
}
|
||||
|
||||
/// Set the namespace client for server-side query execution.
|
||||
///
|
||||
/// When set, queries will be executed on the namespace server instead of locally.
|
||||
@@ -2786,6 +2835,7 @@ impl NativeTable {
|
||||
namespace_client: self.namespace_client.clone(),
|
||||
pushdown_operations: self.pushdown_operations.clone(),
|
||||
freshness: self.freshness.clone(),
|
||||
mem_index_registry: self.mem_index_registry.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2893,6 +2943,7 @@ impl NativeTable {
|
||||
namespace_client: stored_namespace_client,
|
||||
pushdown_operations,
|
||||
freshness: None,
|
||||
mem_index_registry: Default::default(),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -2984,6 +3035,7 @@ impl NativeTable {
|
||||
namespace_client,
|
||||
pushdown_operations,
|
||||
freshness: None,
|
||||
mem_index_registry: Default::default(),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -3117,6 +3169,7 @@ impl NativeTable {
|
||||
namespace_client: stored_namespace_client,
|
||||
pushdown_operations,
|
||||
freshness: None,
|
||||
mem_index_registry: Default::default(),
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -166,6 +166,7 @@ pub struct BaseTableAdapter {
|
||||
table: Arc<dyn BaseTable>,
|
||||
schema: Arc<ArrowSchema>,
|
||||
fts_query: Option<FullTextSearchQuery>,
|
||||
use_lsm: Option<bool>,
|
||||
}
|
||||
|
||||
impl BaseTableAdapter {
|
||||
@@ -181,9 +182,16 @@ impl BaseTableAdapter {
|
||||
table,
|
||||
schema: Arc::new(schema),
|
||||
fts_query: None,
|
||||
use_lsm: None,
|
||||
})
|
||||
}
|
||||
|
||||
/// Set the MemWAL read routing of every scan; see [`QueryRequest::use_lsm`].
|
||||
pub fn with_use_lsm(mut self, use_lsm: Option<bool>) -> Self {
|
||||
self.use_lsm = use_lsm;
|
||||
self
|
||||
}
|
||||
|
||||
/// Create a new adapter with an FTS query applied.
|
||||
pub fn with_fts_query(&self, fts_query: FullTextSearchQuery) -> Self {
|
||||
// Add _score column to the schema
|
||||
@@ -196,6 +204,7 @@ impl BaseTableAdapter {
|
||||
table: self.table.clone(),
|
||||
schema,
|
||||
fts_query: Some(fts_query),
|
||||
use_lsm: self.use_lsm,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -223,6 +232,7 @@ impl TableProvider for BaseTableAdapter {
|
||||
let mut query = QueryRequest {
|
||||
full_text_search: self.fts_query.clone(),
|
||||
disable_scoring_autoprojection: disable_scoring,
|
||||
use_lsm: self.use_lsm,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
|
||||
+472
-21
@@ -1349,6 +1349,41 @@ mod lsm_tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn base_table_adapter_use_lsm_false_scans_the_base_only() {
|
||||
use crate::table::datafusion::BaseTableAdapter;
|
||||
use datafusion::prelude::SessionContext;
|
||||
|
||||
let dir = tempdir().unwrap();
|
||||
let table = id_value_table(&dir).await; // base: ids 1,2,3
|
||||
table
|
||||
.set_lsm_write_spec(LsmWriteSpec::unsharded())
|
||||
.await
|
||||
.unwrap();
|
||||
// Ids 4,5 land in the active memtable, not the base table.
|
||||
lsm_upsert(&table, vec![4, 5]).await;
|
||||
|
||||
let adapter = BaseTableAdapter::try_new(table.base_table().clone())
|
||||
.await
|
||||
.unwrap()
|
||||
.with_use_lsm(Some(false));
|
||||
let ctx = SessionContext::new();
|
||||
ctx.register_table("base_only", Arc::new(adapter)).unwrap();
|
||||
let batches = ctx
|
||||
.sql("SELECT id FROM base_only")
|
||||
.await
|
||||
.unwrap()
|
||||
.collect()
|
||||
.await
|
||||
.unwrap();
|
||||
let mut ids: Vec<i64> = batches
|
||||
.iter()
|
||||
.flat_map(|b| b.column(0).as_primitive::<Int64Type>().values().to_vec())
|
||||
.collect();
|
||||
ids.sort();
|
||||
assert_eq!(ids, vec![1, 2, 3]);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn query_snapshot_preserves_lsm_read_semantics() {
|
||||
let dir = tempdir().unwrap();
|
||||
@@ -1782,8 +1817,13 @@ mod lsm_tests {
|
||||
assert_eq!(rows, 1, "LSM FTS must still see base rows");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn lsm_read_vector_search() {
|
||||
/// A table whose vector index the LSM maintains: base rows filled with
|
||||
/// their id, and one memtable row filled with 1000, nearest to `[1000; 8]`.
|
||||
/// `registry` replaces the table's memtable kinds.
|
||||
async fn lsm_vector_table(
|
||||
dir: &std::path::Path,
|
||||
registry: Option<lance::dataset::mem_wal::MemIndexRegistry>,
|
||||
) -> Table {
|
||||
use crate::index::Index;
|
||||
use crate::index::vector::IvfPqIndexBuilder;
|
||||
use arrow::array::{FixedSizeListBuilder, Float32Builder};
|
||||
@@ -1815,12 +1855,7 @@ mod lsm_tests {
|
||||
.unwrap()
|
||||
};
|
||||
|
||||
let dir = tempdir().unwrap();
|
||||
let conn = connect(dir.path().to_str().unwrap())
|
||||
.execute()
|
||||
.await
|
||||
.unwrap();
|
||||
// Base rows fill each vector with its own id (0..256); all far from 1000.
|
||||
let conn = connect(dir.to_str().unwrap()).execute().await.unwrap();
|
||||
let base = make_batch((0..N).map(|i| (i, i as f32)).collect());
|
||||
let base_reader: Box<dyn RecordBatchReader + Send> =
|
||||
Box::new(RecordBatchIterator::new(vec![Ok(base)], schema.clone()));
|
||||
@@ -1838,13 +1873,15 @@ mod lsm_tests {
|
||||
.execute()
|
||||
.await
|
||||
.unwrap();
|
||||
if let Some(registry) = registry {
|
||||
table.as_native().unwrap().set_mem_index_registry(registry);
|
||||
}
|
||||
let vec_index = table.list_indices().await.unwrap()[0].name.clone();
|
||||
table
|
||||
.set_lsm_write_spec(LsmWriteSpec::unsharded().with_maintained_indexes(vec![vec_index]))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Insert a vector (filled with 1000) that is nearest to the query.
|
||||
let mut builder = table.merge_insert(&[]);
|
||||
builder
|
||||
.when_matched_update_all(None)
|
||||
@@ -1854,20 +1891,21 @@ mod lsm_tests {
|
||||
schema.clone(),
|
||||
));
|
||||
builder.execute(insert_reader).await.unwrap();
|
||||
table
|
||||
}
|
||||
|
||||
// KNN near [1000; DIM]: the default (auto-routed) read surfaces the
|
||||
// memtable row.
|
||||
let stream = table
|
||||
.query()
|
||||
.nearest_to(&[1000.0_f32; 8])
|
||||
.unwrap()
|
||||
.limit(1)
|
||||
.execute()
|
||||
.await
|
||||
.unwrap();
|
||||
let ids = collect_ids(stream).await;
|
||||
/// The ids of the nearest row to `[1000; 8]` through the default read.
|
||||
async fn nearest_ids(table: &Table) -> Vec<i64> {
|
||||
let query = table.query().nearest_to(&[1000.0_f32; 8]).unwrap().limit(1);
|
||||
collect_ids(query.execute().await.unwrap()).await
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn lsm_read_vector_search() {
|
||||
let dir = tempdir().unwrap();
|
||||
let table = lsm_vector_table(dir.path(), None).await;
|
||||
assert_eq!(
|
||||
ids,
|
||||
nearest_ids(&table).await,
|
||||
vec![9999],
|
||||
"LSM vector search must rank the memtable row first"
|
||||
);
|
||||
@@ -1893,4 +1931,417 @@ mod lsm_tests {
|
||||
assert_eq!(ids, vec![255]);
|
||||
}
|
||||
}
|
||||
|
||||
/// The built-in vector plugin, except that its index answers no search.
|
||||
#[derive(Debug)]
|
||||
struct AnswersNothing;
|
||||
|
||||
#[derive(Debug)]
|
||||
struct AnswersNothingIndex(Arc<dyn lance::dataset::mem_wal::index::MemIndex>);
|
||||
|
||||
fn vector_plugin() -> Arc<dyn lance::dataset::mem_wal::index::MemIndexPlugin> {
|
||||
Arc::new(lance::dataset::mem_wal::index::HnswMemIndexPlugin)
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl lance::dataset::mem_wal::index::MemIndexPlugin for AnswersNothing {
|
||||
fn name(&self) -> &str {
|
||||
"AnswersNothing"
|
||||
}
|
||||
fn details_message(&self) -> &str {
|
||||
"VectorIndexDetails"
|
||||
}
|
||||
fn flush_index_type(&self) -> lance_index::IndexType {
|
||||
vector_plugin().flush_index_type()
|
||||
}
|
||||
fn training_criteria(&self) -> lance_index::scalar::registry::TrainingCriteria {
|
||||
vector_plugin().training_criteria()
|
||||
}
|
||||
async fn resolve(
|
||||
&self,
|
||||
ctx: &lance::dataset::mem_wal::index::ResolveContext<'_>,
|
||||
) -> lance_core::Result<lance::dataset::mem_wal::index::ResolvedIndex> {
|
||||
vector_plugin().resolve(ctx).await
|
||||
}
|
||||
fn validate(
|
||||
&self,
|
||||
ctx: &lance::dataset::mem_wal::index::MemIndexBuildContext<'_>,
|
||||
) -> lance_core::Result<()> {
|
||||
vector_plugin().validate(ctx)
|
||||
}
|
||||
fn create(
|
||||
&self,
|
||||
ctx: &lance::dataset::mem_wal::index::MemIndexBuildContext<'_>,
|
||||
) -> lance_core::Result<Arc<dyn lance::dataset::mem_wal::index::MemIndex>> {
|
||||
Ok(Arc::new(AnswersNothingIndex(vector_plugin().create(ctx)?)))
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl lance::dataset::mem_wal::index::MemIndex for AnswersNothingIndex {
|
||||
fn columns(&self) -> &[String] {
|
||||
self.0.columns()
|
||||
}
|
||||
fn can_answer(&self, _query: &dyn lance::dataset::mem_wal::index::MemQuery) -> bool {
|
||||
false
|
||||
}
|
||||
fn insert(&self, batch: &RecordBatch, row_offset: u64) -> lance_core::Result<()> {
|
||||
self.0.insert(batch, row_offset)
|
||||
}
|
||||
fn insert_batches(
|
||||
&self,
|
||||
batches: &[lance::dataset::mem_wal::write::StoredBatch],
|
||||
) -> lance_core::Result<()> {
|
||||
self.0.insert_batches(batches)
|
||||
}
|
||||
fn resident_bytes(&self) -> usize {
|
||||
self.0.resident_bytes()
|
||||
}
|
||||
fn search(
|
||||
&self,
|
||||
_query: &dyn lance::dataset::mem_wal::index::MemQuery,
|
||||
_ctx: &lance::dataset::mem_wal::index::SearchContext,
|
||||
) -> lance_core::Result<Option<lance::dataset::mem_wal::index::MemMatches>> {
|
||||
Ok(None)
|
||||
}
|
||||
async fn flush(
|
||||
&self,
|
||||
ctx: &lance::dataset::mem_wal::index::FlushContext<'_>,
|
||||
) -> lance_core::Result<lance::dataset::mem_wal::index::FlushOutcome> {
|
||||
self.0.flush(ctx).await
|
||||
}
|
||||
}
|
||||
|
||||
/// A resident memtable whose vector index answers nothing still serves an
|
||||
/// LSM vector search: Lance compares every row instead.
|
||||
#[tokio::test]
|
||||
async fn lsm_vector_search_compares_every_row_when_no_resident_index_answers() {
|
||||
let dir = tempdir().unwrap();
|
||||
let mut registry = lance::dataset::mem_wal::MemIndexRegistry::default();
|
||||
registry.replace_plugin(Arc::new(AnswersNothing)).unwrap();
|
||||
let table = lsm_vector_table(dir.path(), Some(registry)).await;
|
||||
assert_eq!(nearest_ids(&table).await, vec![9999]);
|
||||
|
||||
// The second page starts after the memtable row.
|
||||
let page = table
|
||||
.query()
|
||||
.nearest_to(&[1000.0_f32; 8])
|
||||
.unwrap()
|
||||
.limit(2)
|
||||
.offset(1);
|
||||
assert_eq!(
|
||||
collect_ids(page.execute().await.unwrap()).await,
|
||||
vec![255, 254]
|
||||
);
|
||||
}
|
||||
|
||||
/// Claims the base table's bitmap index and keeps it as a B-tree, whose
|
||||
/// flush rows are in the shape the bitmap trainer takes.
|
||||
#[derive(Debug)]
|
||||
struct BitmapAsBTree;
|
||||
|
||||
fn btree_plugin() -> Arc<dyn lance::dataset::mem_wal::index::MemIndexPlugin> {
|
||||
lance::dataset::mem_wal::MemIndexRegistry::default()
|
||||
.plugin_for_details_url("/lance.table.BTreeIndexDetails")
|
||||
.unwrap()
|
||||
.clone()
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl lance::dataset::mem_wal::index::MemIndexPlugin for BitmapAsBTree {
|
||||
fn name(&self) -> &str {
|
||||
"BitmapAsBTree"
|
||||
}
|
||||
fn details_message(&self) -> &str {
|
||||
"BitmapIndexDetails"
|
||||
}
|
||||
fn flush_index_type(&self) -> lance_index::IndexType {
|
||||
lance_index::IndexType::Bitmap
|
||||
}
|
||||
fn training_criteria(&self) -> lance_index::scalar::registry::TrainingCriteria {
|
||||
btree_plugin().training_criteria()
|
||||
}
|
||||
fn validate(
|
||||
&self,
|
||||
ctx: &lance::dataset::mem_wal::index::MemIndexBuildContext<'_>,
|
||||
) -> lance_core::Result<()> {
|
||||
btree_plugin().validate(ctx)
|
||||
}
|
||||
fn create(
|
||||
&self,
|
||||
ctx: &lance::dataset::mem_wal::index::MemIndexBuildContext<'_>,
|
||||
) -> lance_core::Result<Arc<dyn lance::dataset::mem_wal::index::MemIndex>> {
|
||||
btree_plugin().create(ctx)
|
||||
}
|
||||
}
|
||||
|
||||
/// A kind Lance has no memtable index for is refused when named until the
|
||||
/// table's registry maintains it; then it is maintained, flushed with each
|
||||
/// generation, and answers searches on the flushed generations.
|
||||
#[tokio::test]
|
||||
async fn lsm_maintains_an_index_kind_from_the_tables_registry() {
|
||||
let dir = tempdir().unwrap();
|
||||
let conn = connect(dir.path().to_str().unwrap())
|
||||
.execute()
|
||||
.await
|
||||
.unwrap();
|
||||
let table = conn
|
||||
.create_table("t", id_region_reader(vec![(1, "us"), (2, "eu"), (3, "us")]))
|
||||
.execute()
|
||||
.await
|
||||
.unwrap();
|
||||
table.set_unenforced_primary_key(["id"]).await.unwrap();
|
||||
table
|
||||
.create_index(&["region"], crate::index::Index::Bitmap(Default::default()))
|
||||
.name("region_bitmap".to_string())
|
||||
.execute()
|
||||
.await
|
||||
.unwrap();
|
||||
let spec =
|
||||
|| LsmWriteSpec::unsharded().with_maintained_indexes(vec!["region_bitmap".to_string()]);
|
||||
|
||||
let refused = table.set_lsm_write_spec(spec()).await.unwrap_err();
|
||||
assert!(
|
||||
refused.to_string().contains("region_bitmap")
|
||||
&& refused
|
||||
.to_string()
|
||||
.contains("no registered plugin maintains"),
|
||||
"{refused:?}"
|
||||
);
|
||||
|
||||
let registry = lance::dataset::mem_wal::MemIndexRegistry::default()
|
||||
.with_plugin(Arc::new(BitmapAsBTree))
|
||||
.unwrap();
|
||||
table.as_native().unwrap().set_mem_index_registry(registry);
|
||||
table.set_lsm_write_spec(spec()).await.unwrap();
|
||||
|
||||
// The registry is not stored with the table.
|
||||
let other = conn.open_table("t").execute().await.unwrap();
|
||||
let mut builder = other.merge_insert(&["id"]);
|
||||
builder
|
||||
.when_matched_update_all(None)
|
||||
.when_not_matched_insert_all();
|
||||
let refused = builder
|
||||
.execute(id_region_reader(vec![(20, "us")]))
|
||||
.await
|
||||
.unwrap_err();
|
||||
assert!(
|
||||
refused
|
||||
.to_string()
|
||||
.contains("no registered plugin maintains"),
|
||||
"{refused:?}"
|
||||
);
|
||||
|
||||
let mut builder = table.merge_insert(&["id"]);
|
||||
builder
|
||||
.when_matched_update_all(None)
|
||||
.when_not_matched_insert_all();
|
||||
builder
|
||||
.execute(id_region_reader(vec![
|
||||
(10, "fresh"),
|
||||
(11, "fresh"),
|
||||
(12, "eu"),
|
||||
]))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let fresh = || async {
|
||||
let query = table.query().only_if("region = 'fresh'");
|
||||
let mut ids = collect_ids(query.execute().await.unwrap()).await;
|
||||
ids.sort_unstable();
|
||||
ids
|
||||
};
|
||||
assert_eq!(fresh().await, vec![10, 11]);
|
||||
let (_, _, memtables) = table
|
||||
.as_native()
|
||||
.unwrap()
|
||||
.dataset
|
||||
.shard_writer()
|
||||
.read_snapshot()
|
||||
.await
|
||||
.unwrap()
|
||||
.expect("the merge opened a writer");
|
||||
let active = memtables.expect("the writer keeps memtables").active;
|
||||
assert!(
|
||||
active
|
||||
.index_store
|
||||
.index_names()
|
||||
.contains(&"region_bitmap".to_string()),
|
||||
"the memtable maintains the plugin's index: {:?}",
|
||||
active.index_store.index_names()
|
||||
);
|
||||
|
||||
table.close_lsm_writers().await.unwrap();
|
||||
assert_eq!(fresh().await, vec![10, 11]);
|
||||
assert_generations_answer_through_region_bitmap(&table, vec![10, 11]).await;
|
||||
}
|
||||
|
||||
/// The generations each shard's manifest publishes.
|
||||
async fn published_generations(table: &Table) -> Vec<lance::Dataset> {
|
||||
use lance::dataset::mem_wal::ShardManifestStore;
|
||||
|
||||
let uri = table.uri().await.unwrap();
|
||||
let (store, base) = lance_io::object_store::ObjectStore::from_uri(&uri)
|
||||
.await
|
||||
.unwrap();
|
||||
let mem_wal = std::path::Path::new(&uri).join("_mem_wal");
|
||||
let mut generations = Vec::new();
|
||||
for shard in std::fs::read_dir(&mem_wal).unwrap() {
|
||||
let Ok(shard_id) = shard.unwrap().file_name().to_string_lossy().parse() else {
|
||||
continue;
|
||||
};
|
||||
let manifest = ShardManifestStore::new(store.clone(), &base, shard_id, 2)
|
||||
.latest()
|
||||
.await
|
||||
.unwrap()
|
||||
.expect("a shard directory has a manifest");
|
||||
for sstable in manifest.sstables {
|
||||
let path = mem_wal.join(shard_id.to_string()).join(&sstable.path);
|
||||
generations.push(lance::Dataset::open(path.to_str().unwrap()).await.unwrap());
|
||||
}
|
||||
}
|
||||
generations
|
||||
}
|
||||
|
||||
/// Every published generation carries `region_bitmap` as a bitmap index on
|
||||
/// `region`, and a search for `region = 'fresh'` on it uses that index.
|
||||
async fn assert_generations_answer_through_region_bitmap(table: &Table, fresh: Vec<i64>) {
|
||||
use lance::index::DatasetIndexExt;
|
||||
|
||||
let generations = published_generations(table).await;
|
||||
assert!(
|
||||
!generations.is_empty(),
|
||||
"closing the writer flushed a generation"
|
||||
);
|
||||
let mut found = Vec::new();
|
||||
for generation in generations {
|
||||
let region = generation.schema().field("region").unwrap().id;
|
||||
let indices = generation.load_indices().await.unwrap();
|
||||
let index = indices
|
||||
.iter()
|
||||
.find(|index| index.name == "region_bitmap")
|
||||
.expect("the generation carries the index the plugin trained");
|
||||
assert_eq!(index.fields, vec![region]);
|
||||
assert!(
|
||||
index
|
||||
.index_details
|
||||
.as_ref()
|
||||
.is_some_and(|details| details.type_url.ends_with("BitmapIndexDetails")),
|
||||
"{:?}",
|
||||
index.index_details
|
||||
);
|
||||
|
||||
let mut scan = generation.scan();
|
||||
scan.filter("region = 'fresh'").unwrap();
|
||||
let plan = scan.explain_plan(false).await.unwrap();
|
||||
assert!(plan.contains("ScalarIndexQuery"), "{plan}");
|
||||
let batch = scan.try_into_batch().await.unwrap();
|
||||
found.extend(
|
||||
batch["id"]
|
||||
.as_primitive::<Int64Type>()
|
||||
.values()
|
||||
.iter()
|
||||
.copied(),
|
||||
);
|
||||
}
|
||||
found.sort_unstable();
|
||||
assert_eq!(found, fresh);
|
||||
}
|
||||
|
||||
/// Under the default set, a kind only the registry maintains is picked up
|
||||
/// when its index is created after the writer opened, and the table's
|
||||
/// clones share the registry.
|
||||
#[tokio::test]
|
||||
async fn lsm_default_set_maintains_a_registry_kind_created_while_writing() {
|
||||
let dir = tempdir().unwrap();
|
||||
let conn = connect(dir.path().to_str().unwrap())
|
||||
.execute()
|
||||
.await
|
||||
.unwrap();
|
||||
let table = conn
|
||||
.create_table("t", id_region_reader(vec![(1, "us"), (2, "eu")]))
|
||||
.execute()
|
||||
.await
|
||||
.unwrap();
|
||||
table.set_unenforced_primary_key(["id"]).await.unwrap();
|
||||
let registry = lance::dataset::mem_wal::MemIndexRegistry::default()
|
||||
.with_plugin(Arc::new(BitmapAsBTree))
|
||||
.unwrap();
|
||||
table.as_native().unwrap().set_mem_index_registry(registry);
|
||||
let clone = table.clone();
|
||||
assert!(
|
||||
clone
|
||||
.as_native()
|
||||
.unwrap()
|
||||
.mem_index_registry()
|
||||
.plugin_for_details_url("/lance.table.BitmapIndexDetails")
|
||||
.is_some(),
|
||||
"a clone shares the handle's registry"
|
||||
);
|
||||
table
|
||||
.set_lsm_write_spec(LsmWriteSpec::unsharded())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let upsert = |rows: Vec<(i64, &'static str)>| {
|
||||
let table = table.clone();
|
||||
async move {
|
||||
let mut builder = table.merge_insert(&["id"]);
|
||||
builder
|
||||
.when_matched_update_all(None)
|
||||
.when_not_matched_insert_all();
|
||||
builder.execute(id_region_reader(rows)).await.unwrap();
|
||||
}
|
||||
};
|
||||
// Opens the writer before the table has a bitmap index.
|
||||
upsert(vec![(3, "us")]).await;
|
||||
table
|
||||
.create_index(&["region"], crate::index::Index::Bitmap(Default::default()))
|
||||
.name("region_bitmap".to_string())
|
||||
.execute()
|
||||
.await
|
||||
.unwrap();
|
||||
upsert(vec![(10, "fresh"), (11, "fresh")]).await;
|
||||
|
||||
let (_, _, memtables) = table
|
||||
.as_native()
|
||||
.unwrap()
|
||||
.dataset
|
||||
.shard_writer()
|
||||
.read_snapshot()
|
||||
.await
|
||||
.unwrap()
|
||||
.expect("the merge opened a writer");
|
||||
let active = memtables.expect("the writer keeps memtables").active;
|
||||
assert!(
|
||||
active
|
||||
.index_store
|
||||
.index_names()
|
||||
.contains(&"region_bitmap".to_string()),
|
||||
"the open writer picked up the new index: {:?}",
|
||||
active.index_store.index_names()
|
||||
);
|
||||
|
||||
table.close_lsm_writers().await.unwrap();
|
||||
// Only the generation written after the index exists carries it.
|
||||
let generations = published_generations(&table).await;
|
||||
let mut carrying = 0;
|
||||
for generation in &generations {
|
||||
use lance::index::DatasetIndexExt;
|
||||
if generation
|
||||
.load_indices()
|
||||
.await
|
||||
.unwrap()
|
||||
.iter()
|
||||
.any(|index| index.name == "region_bitmap")
|
||||
{
|
||||
carrying += 1;
|
||||
}
|
||||
}
|
||||
assert!(
|
||||
carrying >= 1,
|
||||
"{} generations, none carry the index",
|
||||
generations.len()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,7 +29,7 @@ use arrow_schema::{DataType, Schema as ArrowSchema, SchemaRef};
|
||||
use lance::Dataset;
|
||||
use lance::dataset::mem_wal::{
|
||||
DatasetMemWalExt, ShardWriter, ShardWriterConfig, evaluate_sharding_spec,
|
||||
validate_maintained_indexes,
|
||||
validate_maintained_indexes_with,
|
||||
};
|
||||
use lance::index::DatasetIndexExt;
|
||||
use lance_core::datatypes::Schema as LanceSchema;
|
||||
@@ -103,7 +103,8 @@ pub(crate) async fn set_lsm_write_spec(table: &NativeTable, spec: LsmWriteSpec)
|
||||
}
|
||||
}
|
||||
let dataset = table.dataset.get().await?;
|
||||
validate_maintained_indexes(&dataset, requested).await?;
|
||||
validate_maintained_indexes_with(&dataset, requested, &table.mem_index_registry())
|
||||
.await?;
|
||||
Some(requested.to_vec())
|
||||
}
|
||||
None => None,
|
||||
@@ -126,7 +127,9 @@ pub(crate) async fn set_lsm_write_spec(table: &NativeTable, spec: LsmWriteSpec)
|
||||
.into(),
|
||||
});
|
||||
}
|
||||
let mut builder = dataset.initialize_mem_wal();
|
||||
let mut builder = dataset
|
||||
.initialize_mem_wal()
|
||||
.mem_index_registry(table.mem_index_registry());
|
||||
let writer_config_defaults = match spec {
|
||||
LsmWriteSpec::Bucket {
|
||||
column,
|
||||
@@ -725,7 +728,8 @@ pub(crate) async fn execute_lsm_merge_insert(
|
||||
return Ok(lsm_merge_result(0));
|
||||
};
|
||||
|
||||
let config = shard_writer_config_from_defaults(&plan.writer_config_defaults);
|
||||
let mut config = shard_writer_config_from_defaults(&plan.writer_config_defaults);
|
||||
config.mem_index_registry = table.mem_index_registry();
|
||||
let writer = table
|
||||
.dataset
|
||||
.shard_writer()
|
||||
|
||||
@@ -106,7 +106,7 @@ async fn can_execute_namespace_query(table: &NativeTable, query: &AnyQuery) -> R
|
||||
}
|
||||
|
||||
fn requires_local_namespace_execution(query: &AnyQuery) -> bool {
|
||||
// The namespace QueryTable request cannot represent approx_mode or use_lsm.
|
||||
// The namespace QueryTable request cannot represent approx_mode, search_effort or use_lsm.
|
||||
// For use_lsm that is worse than a tuning miss: MemWAL read routing lives only
|
||||
// in `create_plan`, so a pushed-down query would return stale base-only data.
|
||||
if query.base().use_lsm.is_some() || query.base().take_offsets.is_some() {
|
||||
@@ -115,7 +115,7 @@ fn requires_local_namespace_execution(query: &AnyQuery) -> bool {
|
||||
matches!(
|
||||
query,
|
||||
AnyQuery::VectorQuery(vector_query)
|
||||
if vector_query.approx_mode.is_some()
|
||||
if vector_query.approx_mode.is_some() || vector_query.search_effort.is_some()
|
||||
)
|
||||
}
|
||||
|
||||
@@ -163,6 +163,7 @@ pub async fn create_plan(
|
||||
AnyQuery::Query(query) => VectorQueryRequest::from_plain_query(query),
|
||||
};
|
||||
query.base.check_filter()?;
|
||||
query.validate_search_effort()?;
|
||||
|
||||
let ds_ref = table.dataset.get().await?;
|
||||
|
||||
@@ -293,7 +294,9 @@ pub async fn create_plan(
|
||||
scanner.approx_mode(approx_mode.into());
|
||||
}
|
||||
|
||||
if let Some(nprobes) = query.nprobes {
|
||||
if query.search_effort.is_none()
|
||||
&& let Some(nprobes) = query.nprobes
|
||||
{
|
||||
if query.maximum_nprobes == Some(0) {
|
||||
scanner.minimum_nprobes(nprobes);
|
||||
} else {
|
||||
@@ -306,6 +309,9 @@ pub async fn create_plan(
|
||||
if let Some(maximum_nprobes) = query.maximum_nprobes.filter(|nprobes| *nprobes != 0) {
|
||||
scanner.maximum_nprobes(maximum_nprobes);
|
||||
}
|
||||
if let Some(search_effort) = query.search_effort {
|
||||
scanner.search_effort(search_effort)?;
|
||||
}
|
||||
}
|
||||
|
||||
// For a batch query, `nearest` already applies k to each query vector.
|
||||
@@ -604,6 +610,11 @@ fn convert_to_namespace_query(query: &AnyQuery) -> Result<NsQueryTableRequest> {
|
||||
};
|
||||
|
||||
// Check for unsupported features
|
||||
if vq.search_effort.is_some() {
|
||||
return Err(Error::NotSupported {
|
||||
message: "namespace QueryTable does not support search_effort; execute the query locally".to_string(),
|
||||
});
|
||||
}
|
||||
if vq.base.reranker.is_some() {
|
||||
return Err(Error::NotSupported {
|
||||
message: "Reranker is not supported for server-side queries".to_string(),
|
||||
@@ -962,6 +973,19 @@ mod tests {
|
||||
let default_query = AnyQuery::VectorQuery(VectorQueryRequest::default());
|
||||
assert!(!requires_local_namespace_execution(&default_query));
|
||||
|
||||
for effort in [0.0, 0.5, 1.0] {
|
||||
let effort_query = AnyQuery::VectorQuery(VectorQueryRequest {
|
||||
query_vector: vec![Arc::new(Float32Array::from(vec![1.0, 2.0]))],
|
||||
search_effort: Some(effort),
|
||||
..Default::default()
|
||||
});
|
||||
assert!(requires_local_namespace_execution(&effort_query));
|
||||
assert!(matches!(
|
||||
convert_to_namespace_query(&effort_query),
|
||||
Err(Error::NotSupported { message }) if message.contains("search_effort")
|
||||
));
|
||||
}
|
||||
|
||||
let nprobes_query = AnyQuery::VectorQuery(VectorQueryRequest {
|
||||
query_vector: vec![Arc::new(Float32Array::from(vec![1.0, 2.0]))],
|
||||
nprobes: Some(20),
|
||||
|
||||
@@ -33,7 +33,6 @@ use datafusion_physical_plan::expressions::Column;
|
||||
use datafusion_physical_plan::projection::ProjectionExec;
|
||||
use datafusion_physical_plan::{ExecutionPlan, PhysicalExpr};
|
||||
use lance::Dataset;
|
||||
use lance::dataset::mem_wal::index::{IndexStore, VectorMemQuery};
|
||||
use lance::dataset::mem_wal::scanner::{InMemoryMemTableRef, InMemoryMemTables};
|
||||
use lance::dataset::mem_wal::{
|
||||
DatasetMemWalExt, LsmScanner, ShardManifestStore, ShardSnapshot, ShardWriterConfig,
|
||||
@@ -174,6 +173,9 @@ fn reject_unsupported(query: &VectorQueryRequest) -> Result<()> {
|
||||
// brute-force search the index-only base arm can't do. (ef / approx_mode are
|
||||
// recall/speed knobs and are left to no-op.)
|
||||
if !query.query_vector.is_empty() {
|
||||
if query.search_effort.is_some() {
|
||||
return unsupported("search_effort on vector search");
|
||||
}
|
||||
if query.lower_bound.is_some() || query.upper_bound.is_some() {
|
||||
return unsupported("distance_range on vector search");
|
||||
}
|
||||
@@ -708,17 +710,14 @@ async fn vector_plan(
|
||||
|
||||
// The base arm relies on the column's vector index (`fast_search`). Unmaintained,
|
||||
// its catch-up is untracked and exclusion falls back to the compaction watermark,
|
||||
// dropping compacted SSTables the lagging base index has not re-indexed; and a
|
||||
// resident MemTable built before the index joined the set carries none either.
|
||||
// dropping compacted SSTables the lagging base index has not re-indexed.
|
||||
// Reject rather than silently omit rows, mirroring the FTS arm.
|
||||
if !index_maintained(dataset, &column, details, "VectorIndexDetails").await?
|
||||
|| !resident_memtables_carry(&in_memory, |memtable| {
|
||||
carries_vector_index(&memtable.index_store, &column)
|
||||
})
|
||||
{
|
||||
// A resident MemTable needs no index of its own: one that cannot answer
|
||||
// compares every row.
|
||||
if !index_maintained(dataset, &column, details, "VectorIndexDetails").await? {
|
||||
return Err(Error::NotSupported {
|
||||
message: format!(
|
||||
"the MemWAL LSM scanner requires the vector index on '{column}' to be maintained by the write spec (LsmWriteSpec::with_maintained_indexes) and carried by every resident MemTable; otherwise compacted rows not yet re-indexed are omitted. set use_lsm(false) to read the base table only"
|
||||
"the MemWAL LSM scanner requires the vector index on '{column}' to be maintained by the write spec (LsmWriteSpec::with_maintained_indexes); otherwise compacted rows not yet re-indexed are omitted. set use_lsm(false) to read the base table only"
|
||||
),
|
||||
});
|
||||
}
|
||||
@@ -761,22 +760,6 @@ async fn vector_plan(
|
||||
Ok(scanner.create_plan().await?)
|
||||
}
|
||||
|
||||
/// Probe for a resident vector index without imposing a distance metric, matching
|
||||
/// the presence check used before Lance introduced query-based index lookup.
|
||||
fn carries_vector_index(store: &IndexStore, column: &str) -> bool {
|
||||
let probe = VectorMemQuery {
|
||||
vector: arrow_array::FixedSizeListArray::new_null(
|
||||
Arc::new(arrow_schema::Field::new("item", DataType::Float32, true)),
|
||||
1,
|
||||
1,
|
||||
),
|
||||
k: 1,
|
||||
ef: None,
|
||||
distance_type: None,
|
||||
};
|
||||
store.index_answering(column, &probe).is_some()
|
||||
}
|
||||
|
||||
/// Whether `column` stores binary (uint8) vectors, which the LSM vector planner
|
||||
/// does not support.
|
||||
fn is_binary_vector_column(schema: &ArrowSchema, column: &str) -> bool {
|
||||
@@ -822,9 +805,22 @@ mod tests {
|
||||
use lance_index::mem_wal::{CompactedSsTable, IndexCatchupProgress};
|
||||
|
||||
#[test]
|
||||
fn resident_index_checks_distinguish_columns_and_query_types() {
|
||||
fn search_effort_is_not_silently_ignored() {
|
||||
let query = VectorQueryRequest {
|
||||
query_vector: vec![Arc::new(arrow_array::Float32Array::from(vec![1.0, 0.0]))],
|
||||
search_effort: Some(0.5),
|
||||
..Default::default()
|
||||
};
|
||||
assert!(
|
||||
matches!(reject_unsupported(&query), Err(Error::NotSupported { message })
|
||||
if message.contains("search_effort") && message.contains("un-compacted MemWAL"))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resident_fts_check_distinguishes_columns() {
|
||||
use arrow_schema::Field;
|
||||
use lance::dataset::mem_wal::index::MemIndexSpec;
|
||||
use lance::dataset::mem_wal::index::{IndexStore, MemIndexSpec};
|
||||
|
||||
let schema = ArrowSchema::new(vec![
|
||||
Field::new("id", DataType::Int32, false),
|
||||
@@ -847,16 +843,11 @@ mod tests {
|
||||
),
|
||||
];
|
||||
let store = IndexStore::from_specs(&specs, &schema, 1_000, 16).unwrap();
|
||||
assert!(carries_vector_index(&store, "vector"));
|
||||
for column in ["id", "text", "missing"] {
|
||||
assert!(!carries_vector_index(&store, column));
|
||||
}
|
||||
assert!(!store.fts_granularities_on("text").is_empty());
|
||||
for column in ["id", "vector", "missing"] {
|
||||
assert!(store.fts_granularities_on(column).is_empty());
|
||||
}
|
||||
let empty = IndexStore::from_specs(&[], &schema, 1_000, 16).unwrap();
|
||||
assert!(!carries_vector_index(&empty, "vector"));
|
||||
assert!(empty.fts_granularities_on("text").is_empty());
|
||||
}
|
||||
|
||||
|
||||
@@ -1847,8 +1847,8 @@ mod tests {
|
||||
assert_eq!(
|
||||
layouts,
|
||||
vec![
|
||||
(1, Some(BlobKind::Managed)),
|
||||
(2, Some(BlobKind::Managed)),
|
||||
(1, Some(BlobKind::Dedicated)),
|
||||
(2, Some(BlobKind::Packed)),
|
||||
(3, Some(BlobKind::Inline)),
|
||||
(4, None),
|
||||
]
|
||||
|
||||
Reference in New Issue
Block a user