feat: index builders for NGram, BloomFilter, RTree (#4205)

Add index builders in the lancedb library for NGram, BloomFilter, and
RTree indexes.
This commit is contained in:
Colin Patrick McCabe
2026-09-16 15:03:06 -07:00
committed by GitHub
parent 3309c71a6e
commit 04150b3b82
6 changed files with 763 additions and 2 deletions
Generated
+297
View File
@@ -3151,6 +3151,16 @@ version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e1d926b4d407d372f141f93bb444696142c29d32962ccbd3531117cf3aa0bfa9"
[[package]]
name = "earcutr"
version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "79127ed59a85d7687c409e9978547cffb7dc79675355ed22da6b66fd5f6ead01"
dependencies = [
"itertools 0.11.0",
"num-traits",
]
[[package]]
name = "ecdsa"
version = "0.14.8"
@@ -3450,6 +3460,12 @@ dependencies = [
"rand_distr 0.5.1",
]
[[package]]
name = "float_next_after"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8bf7cc16383c4b8d58b9905a8509f02926ce3058053c056376248d958c9df1e8"
[[package]]
name = "fnv"
version = "1.0.7"
@@ -3776,6 +3792,129 @@ dependencies = [
"version_check",
]
[[package]]
name = "geo"
version = "0.31.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2fc1a1678e54befc9b4bcab6cd43b8e7f834ae8ea121118b0fd8c42747675b4a"
dependencies = [
"earcutr",
"float_next_after",
"geo-types",
"geographiclib-rs",
"i_overlay",
"log",
"num-traits",
"robust",
"rstar",
"spade",
]
[[package]]
name = "geo-traits"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2e7c353d12a704ccfab1ba8bfb1a7fe6cb18b665bf89d37f4f7890edcd260206"
dependencies = [
"geo-types",
]
[[package]]
name = "geo-types"
version = "0.7.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "94776032c45f950d30a13af6113c2ad5625316c9abfbccee4dd5a6695f8fe0f5"
dependencies = [
"approx",
"num-traits",
"rayon",
"rstar",
"serde",
]
[[package]]
name = "geoarrow-array"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dafe7b7de3fab1a8b7099fd6a6434ca955fa65065f9c19f0f8a133693f3c2b0e"
dependencies = [
"arrow-array",
"arrow-buffer",
"arrow-schema",
"geo-traits",
"geoarrow-schema",
"num-traits",
"wkb",
"wkt",
]
[[package]]
name = "geoarrow-expr-geo"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e4a62ac19c86827c6ec81ea584594b3ee96db5a8119b9774d3466c6b373c434"
dependencies = [
"arrow-array",
"arrow-buffer",
"geo",
"geo-traits",
"geoarrow-array",
"geoarrow-schema",
]
[[package]]
name = "geoarrow-schema"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4d4a7edb2a1d87024a93805332a9c8184a0354836271d42c0d18cf628a5e3cd0"
dependencies = [
"arrow-schema",
"geo-traits",
"serde",
"serde_json",
"thiserror 1.0.69",
]
[[package]]
name = "geodatafusion"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fecbdd00d0fff2b04635c1b1e4129c217908f0c2d17539e0a2275308afce2552"
dependencies = [
"arrow-arith",
"arrow-array",
"arrow-buffer",
"arrow-schema",
"datafusion",
"geo",
"geo-traits",
"geoarrow-array",
"geoarrow-expr-geo",
"geoarrow-schema",
"geohash",
"thiserror 1.0.69",
"wkt",
]
[[package]]
name = "geographiclib-rs"
version = "0.2.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c5a7f08910fd98737a6eda7568e7c5e645093e073328eeef49758cfe8b0489c7"
dependencies = [
"libm",
]
[[package]]
name = "geohash"
version = "0.13.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f58890382f70caccc5fa388981f7ac80c913795042afce9f3e065695d8f7464"
dependencies = [
"geo-types",
"libm",
]
[[package]]
name = "getrandom"
version = "0.2.17"
@@ -3954,6 +4093,15 @@ dependencies = [
"zerocopy",
]
[[package]]
name = "hash32"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "47d60b12902ba28e2730cd37e95b8c9223af2808df9e902d4df49588d1470606"
dependencies = [
"byteorder",
]
[[package]]
name = "hashbrown"
version = "0.12.3"
@@ -4010,6 +4158,16 @@ version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0049b265b7f201ca9ab25475b22b47fe444060126a51abe00f77d986fc5cc52e"
[[package]]
name = "heapless"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0bfb9eb618601c89945a70e254898da93b13be0388091d42117462b265bb3fad"
dependencies = [
"hash32",
"stable_deref_trait",
]
[[package]]
name = "heck"
version = "0.4.1"
@@ -4327,6 +4485,49 @@ dependencies = [
"serde",
]
[[package]]
name = "i_float"
version = "1.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "010025c2c532c8d82e42d0b8bb5184afa449fa6f06c709ea9adcb16c49ae405b"
dependencies = [
"libm",
]
[[package]]
name = "i_key_sort"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9190f86706ca38ac8add223b2aed8b1330002b5cdbbce28fb58b10914d38fc27"
[[package]]
name = "i_overlay"
version = "4.0.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "413183068e6e0289e18d7d0a1f661b81546e6918d5453a44570b9ab30cbed1b3"
dependencies = [
"i_float",
"i_key_sort",
"i_shape",
"i_tree",
"rayon",
]
[[package]]
name = "i_shape"
version = "1.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1ea154b742f7d43dae2897fcd5ead86bc7b5eefcedd305a7ebf9f69d44d61082"
dependencies = [
"i_float",
]
[[package]]
name = "i_tree"
version = "0.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "35e6d558e6d4c7b82bc51d9c771e7a927862a161a7d87bf2b0541450e0e20915"
[[package]]
name = "iana-time-zone"
version = "0.1.65"
@@ -5061,6 +5262,7 @@ dependencies = [
"jsonb",
"lance-arrow",
"lance-core",
"lance-geo",
"log",
"pin-project",
"prost",
@@ -5164,6 +5366,21 @@ dependencies = [
"tracing",
]
[[package]]
name = "lance-geo"
version = "13.0.0-beta.3"
source = "git+https://github.com/lance-format/lance.git?tag=v13.0.0-beta.3#565808c992c3d2adf1909b9753b50a28b6bd8a19"
dependencies = [
"datafusion",
"geo-traits",
"geo-types",
"geoarrow-array",
"geoarrow-schema",
"geodatafusion",
"lance-core",
"serde",
]
[[package]]
name = "lance-index"
version = "13.0.0-beta.3"
@@ -5190,6 +5407,9 @@ dependencies = [
"dirs",
"fst",
"futures",
"geo-types",
"geoarrow-array",
"geoarrow-schema",
"half",
"itertools 0.14.0",
"jieba-rs",
@@ -5201,6 +5421,7 @@ dependencies = [
"lance-datafusion",
"lance-encoding",
"lance-file",
"lance-geo",
"lance-index-core",
"lance-io",
"lance-linalg",
@@ -6414,6 +6635,28 @@ dependencies = [
"libc",
]
[[package]]
name = "num_enum"
version = "0.7.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5d0bca838442ec211fa11de3a8b0e0e8f3a4522575b5c4c06ed722e005036f26"
dependencies = [
"num_enum_derive",
"rustversion",
]
[[package]]
name = "num_enum_derive"
version = "0.7.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "680998035259dcfcafe653688bf2aa6d3e2dc05e98be6ab46afb089dc84f1df8"
dependencies = [
"proc-macro-crate",
"proc-macro2",
"quote",
"syn 2.0.117",
]
[[package]]
name = "number_prefix"
version = "0.4.0"
@@ -8771,6 +9014,12 @@ dependencies = [
"byteorder",
]
[[package]]
name = "robust"
version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4e27ee8bb91ca0adcf0ecb116293afa12d393f9c2b9b9cd54d33e8078fe19839"
[[package]]
name = "rsa"
version = "0.9.10"
@@ -8792,6 +9041,17 @@ dependencies = [
"zeroize",
]
[[package]]
name = "rstar"
version = "0.12.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "421400d13ccfd26dfa5858199c30a5d76f9c54e0dba7575273025b43c5175dbb"
dependencies = [
"heapless",
"num-traits",
"smallvec",
]
[[package]]
name = "rstest"
version = "0.23.0"
@@ -9530,6 +9790,18 @@ dependencies = [
"winapi",
]
[[package]]
name = "spade"
version = "2.15.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9699399fd9349b00b184f5635b074f9ec93afffef30c853f8c875b32c0f8c7fa"
dependencies = [
"hashbrown 0.16.1",
"num-traits",
"robust",
"smallvec",
]
[[package]]
name = "spin"
version = "0.9.9"
@@ -11353,6 +11625,31 @@ dependencies = [
"wasmparser",
]
[[package]]
name = "wkb"
version = "0.9.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a120b336c7ad17749026d50427c23d838ecb50cd64aaea6254b5030152f890a9"
dependencies = [
"byteorder",
"geo-traits",
"num_enum",
"thiserror 1.0.69",
]
[[package]]
name = "wkt"
version = "0.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "efb2b923ccc882312e559ffaa832a055ba9d1ac0cc8e86b3e25453247e4b81d7"
dependencies = [
"geo-traits",
"geo-types",
"log",
"num-traits",
"thiserror 1.0.69",
]
[[package]]
name = "writeable"
version = "0.6.3"
+2
View File
@@ -128,6 +128,8 @@ pprof = { version = "0.14", features = ["flamegraph"] }
[features]
default = []
# Native spatial indexes and queries. Remote RTree requests do not require this feature.
geo = ["lance/geo"]
aws = [
"dep:aws-smithy-types",
"lance/aws",
+55 -2
View File
@@ -14,8 +14,8 @@ use crate::{DistanceType, Error, Result, job::Job, table::BaseTable};
use self::{
scalar::{
BTreeIndexBuilder, BitmapIndexBuilder, FmIndexBuilder, LabelListIndexBuilder,
ZoneMapIndexBuilder,
BTreeIndexBuilder, BitmapIndexBuilder, BloomFilterIndexBuilder, FmIndexBuilder,
LabelListIndexBuilder, NGramIndexBuilder, RTreeIndexBuilder, ZoneMapIndexBuilder,
},
vector::{
IvfHnswFlatIndexBuilder, IvfHnswPqIndexBuilder, IvfHnswSqIndexBuilder, IvfPqIndexBuilder,
@@ -63,6 +63,16 @@ pub enum Index {
/// prove they cannot match the predicate.
ZoneMap(ZoneMapIndexBuilder),
/// An NGram index accelerates substring and pattern filters on UTF-8 strings.
NGram(NGramIndexBuilder),
/// A Bloom filter index skips groups of scalar values that cannot match a filter.
BloomFilter(BloomFilterIndexBuilder),
/// An R-tree index accelerates spatial intersection filters on GeoArrow geometries.
/// Native creation requires the `geo` feature.
RTree(RTreeIndexBuilder),
/// Full text search index using BM25.
///
/// The posting block size defaults to 128. Supported values are 128 and 256;
@@ -352,6 +362,12 @@ pub enum IndexType {
Fm,
#[serde(alias = "ZONEMAP", alias = "ZONE_MAP")]
ZoneMap,
#[serde(alias = "NGRAM", alias = "N_GRAM")]
NGram,
#[serde(alias = "BLOOM_FILTER", alias = "BLOOMFILTER")]
BloomFilter,
#[serde(alias = "RTREE", alias = "R_TREE")]
RTree,
// FTS
#[serde(alias = "INVERTED", alias = "Inverted")]
FTS,
@@ -374,6 +390,9 @@ impl std::fmt::Display for IndexType {
Self::LabelList => write!(f, "LABEL_LIST"),
Self::Fm => write!(f, "FM"),
Self::ZoneMap => write!(f, "ZONEMAP"),
Self::NGram => write!(f, "NGRAM"),
Self::BloomFilter => write!(f, "BLOOM_FILTER"),
Self::RTree => write!(f, "RTREE"),
Self::FTS => write!(f, "FTS"),
Self::Unknown => write!(f, "UNKNOWN"),
}
@@ -390,6 +409,9 @@ impl std::str::FromStr for IndexType {
"LABEL_LIST" | "LABELLIST" => Ok(Self::LabelList),
"FM" | "FMINDEX" => Ok(Self::Fm),
"ZONEMAP" | "ZONE_MAP" => Ok(Self::ZoneMap),
"NGRAM" | "N_GRAM" => Ok(Self::NGram),
"BLOOM_FILTER" | "BLOOMFILTER" => Ok(Self::BloomFilter),
"RTREE" | "R_TREE" => Ok(Self::RTree),
"FTS" | "INVERTED" => Ok(Self::FTS),
"IVF_FLAT" => Ok(Self::IvfFlat),
"IVF_SQ" => Ok(Self::IvfSq),
@@ -495,3 +517,34 @@ pub struct IndexStatistics {
/// The number of parts this index is split into.
pub num_indices: Option<u32>,
}
#[cfg(test)]
mod tests {
use super::IndexType;
#[test]
fn builtin_scalar_index_type_names() {
for (index_type, canonical, aliases) in [
(IndexType::NGram, "NGRAM", ["NGram", "NGRAM", "N_GRAM"]),
(
IndexType::BloomFilter,
"BLOOM_FILTER",
["BloomFilter", "BLOOMFILTER", "BLOOM_FILTER"],
),
(IndexType::RTree, "RTREE", ["RTree", "RTREE", "R_TREE"]),
] {
assert_eq!(index_type.to_string(), canonical);
for alias in aliases {
assert_eq!(alias.parse::<IndexType>().unwrap(), index_type);
assert_eq!(
alias.to_lowercase().parse::<IndexType>().unwrap(),
index_type
);
assert_eq!(
serde_json::from_value::<IndexType>(serde_json::json!(alias)).unwrap(),
index_type
);
}
}
}
}
+170
View File
@@ -82,8 +82,178 @@ pub struct FmIndexBuilder {}
#[derive(Debug, Clone, Default, serde::Serialize)]
pub struct ZoneMapIndexBuilder {}
/// Builder for an NGram index over UTF-8 strings.
///
/// This index accelerates certain substring, `LIKE`, and regular-expression filters.
/// It uses Lance's default trigram parameters.
///
/// ```
/// use lancedb::index::{Index, scalar::NGramIndexBuilder};
/// # async fn example(table: &lancedb::Table) -> lancedb::Result<()> {
/// table.create_index(&["text"], Index::NGram(NGramIndexBuilder::default()))
/// .execute().await?;
/// # Ok(())
/// # }
/// ```
#[derive(Debug, Clone, Default, serde::Serialize)]
pub struct NGramIndexBuilder {}
/// Builder for a Bloom filter index on scalar values.
///
/// Bloom filters accelerate equality and membership filters by skipping groups of rows that cannot
/// match. Candidate rows are checked to remove false positives. A bloom filter is much smaller than
/// a btree or bitmap index, but not as precise. It is also limited to equality queries.
///
/// Unset parameters use Lance's defaults.
///
/// ```
/// use lancedb::index::{Index, scalar::BloomFilterIndexBuilder};
/// # async fn example(table: &lancedb::Table) -> lancedb::Result<()> {
/// let params = BloomFilterIndexBuilder::default()
/// .number_of_items(4096)?
/// .probability(0.01)?;
/// table.create_index(&["id"], Index::BloomFilter(params))
/// .execute().await?;
/// # Ok(())
/// # }
/// ```
#[derive(Debug, Clone, Default, serde::Serialize)]
pub struct BloomFilterIndexBuilder {
#[serde(skip_serializing_if = "Option::is_none")]
number_of_items: Option<u64>,
#[serde(skip_serializing_if = "Option::is_none")]
probability: Option<f64>,
}
impl BloomFilterIndexBuilder {
/// Set the number of rows covered by each Bloom filter.
///
/// Must be greater than zero. Defaults to 8192, unless overridden by Lance's
/// `LANCE_BLOOMFILTER_DEFAULT_NUMBER_OF_ITEMS` environment variable.
pub fn number_of_items(mut self, number_of_items: u64) -> crate::Result<Self> {
if number_of_items == 0 {
return Err(crate::Error::InvalidInput {
message: "BloomFilter number_of_items must be greater than zero".into(),
});
}
self.number_of_items = Some(number_of_items);
Ok(self)
}
/// Set the desired false-positive probability for each Bloom filter.
///
/// Must be finite and strictly between zero and one. Lower values use more
/// space. Defaults to 0.00057, unless overridden by Lance's
/// `LANCE_BLOOMFILTER_DEFAULT_PROBABILITY` environment variable.
pub fn probability(mut self, probability: f64) -> crate::Result<Self> {
if !probability.is_finite() || probability <= 0.0 || probability >= 1.0 {
return Err(crate::Error::InvalidInput {
message: "BloomFilter probability must be finite and strictly between zero and one"
.into(),
});
}
self.probability = Some(probability);
Ok(self)
}
}
/// Builder for an R-tree index on GeoArrow geometry columns.
///
/// This index accelerates spatial intersection filters using geometry bounding
/// boxes. Unset parameters use Lance's defaults. Native creation requires
/// the `geo` feature; remote creation requires server support.
///
/// ```
/// use lancedb::index::{Index, scalar::RTreeIndexBuilder};
/// # async fn example(table: &lancedb::Table) -> lancedb::Result<()> {
/// let params = RTreeIndexBuilder::default().page_size(1024)?;
/// table.create_index(&["geometry"], Index::RTree(params))
/// .execute().await?;
/// # Ok(())
/// # }
/// ```
#[derive(Debug, Clone, Default, serde::Serialize)]
pub struct RTreeIndexBuilder {
#[serde(skip_serializing_if = "Option::is_none")]
page_size: Option<u32>,
}
impl RTreeIndexBuilder {
/// Set the maximum number of entries in each R-tree page.
///
/// Must be at least 2. Defaults to 4096.
pub fn page_size(mut self, page_size: u32) -> crate::Result<Self> {
if page_size < 2 {
return Err(crate::Error::InvalidInput {
message: "RTree page_size must be at least 2".into(),
});
}
self.page_size = Some(page_size);
Ok(self)
}
}
pub use lance_index::scalar::FullTextSearchQuery;
pub use lance_index::scalar::InvertedIndexParams as FtsIndexBuilder;
pub use lance_index::scalar::InvertedIndexParams;
pub use lance_index::scalar::inverted::DocumentGranularity;
pub use lance_index::scalar::inverted::query::*;
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn scalar_index_parameters() {
assert_eq!(
serde_json::to_value(BloomFilterIndexBuilder::default()).unwrap(),
serde_json::json!({})
);
assert_eq!(
serde_json::to_value(RTreeIndexBuilder::default()).unwrap(),
serde_json::json!({})
);
assert_eq!(
serde_json::to_value(
BloomFilterIndexBuilder::default()
.number_of_items(1)
.unwrap()
)
.unwrap(),
serde_json::json!({"number_of_items": 1})
);
assert_eq!(
serde_json::to_value(
BloomFilterIndexBuilder::default()
.probability(0.01)
.unwrap()
)
.unwrap(),
serde_json::json!({"probability": 0.01})
);
assert!(
BloomFilterIndexBuilder::default()
.number_of_items(0)
.is_err()
);
for probability in [
f64::NAN,
f64::INFINITY,
f64::NEG_INFINITY,
-0.1,
0.0,
1.0,
1.1,
] {
assert!(
BloomFilterIndexBuilder::default()
.probability(probability)
.is_err()
);
}
for page_size in [0, 1] {
assert!(RTreeIndexBuilder::default().page_size(page_size).is_err());
}
assert!(RTreeIndexBuilder::default().page_size(2).is_ok());
}
}
+30
View File
@@ -588,6 +588,9 @@ impl<S: HttpSend> RemoteTable<S> {
Index::LabelList(p) => ("LABEL_LIST", Some(to_json(p)?)),
Index::Fm(p) => ("FM", Some(to_json(p)?)),
Index::ZoneMap(p) => ("ZONEMAP", Some(to_json(p)?)),
Index::NGram(p) => ("NGRAM", Some(to_json(p)?)),
Index::BloomFilter(p) => ("BLOOM_FILTER", Some(to_json(p)?)),
Index::RTree(p) => ("RTREE", Some(to_json(p)?)),
Index::FTS(p) => {
let mut params = to_json(p)?;
if p.get_document_granularity().is_list_element() {
@@ -6378,6 +6381,33 @@ mod tests {
("BTREE", json!({}), Index::BTree(Default::default())),
("BITMAP", json!({}), Index::Bitmap(Default::default())),
("ZONEMAP", json!({}), Index::ZoneMap(Default::default())),
("NGRAM", json!({}), Index::NGram(Default::default())),
(
"BLOOM_FILTER",
json!({}),
Index::BloomFilter(Default::default()),
),
("RTREE", json!({}), Index::RTree(Default::default())),
(
"BLOOM_FILTER",
json!({"number_of_items": 4096, "probability": 0.01}),
Index::BloomFilter(
crate::index::scalar::BloomFilterIndexBuilder::default()
.number_of_items(4096)
.unwrap()
.probability(0.01)
.unwrap(),
),
),
(
"RTREE",
json!({"page_size": 1024}),
Index::RTree(
crate::index::scalar::RTreeIndexBuilder::default()
.page_size(1024)
.unwrap(),
),
),
(
"LABEL_LIST",
json!({}),
+209
View File
@@ -265,6 +265,26 @@ impl NativeTable {
BuiltinIndexType::ZoneMap,
)))
}
Index::NGram(_) => Ok(Box::new(ScalarIndexParams::for_builtin(
BuiltinIndexType::NGram,
))),
Index::BloomFilter(params) => {
let params = serde_json::to_value(params).map_err(|e| Error::InvalidInput {
message: format!("failed to serialize index params: {e}"),
})?;
Ok(Box::new(
ScalarIndexParams::for_builtin(BuiltinIndexType::BloomFilter)
.with_params(&params),
))
}
Index::RTree(params) => {
let params = serde_json::to_value(params).map_err(|e| Error::InvalidInput {
message: format!("failed to serialize index params: {e}"),
})?;
Ok(Box::new(
ScalarIndexParams::for_builtin(BuiltinIndexType::RTree).with_params(&params),
))
}
Index::FTS(fts_opts) => {
Self::validate_index_type(field, "FTS", supported_fts_data_type)?;
Ok(Box::new(fts_opts))
@@ -425,6 +445,9 @@ impl NativeTable {
Index::LabelList(_) => IndexType::LabelList,
Index::Fm(_) => IndexType::Fm,
Index::ZoneMap(_) => IndexType::ZoneMap,
Index::NGram(_) => IndexType::NGram,
Index::BloomFilter(_) => IndexType::BloomFilter,
Index::RTree(_) => IndexType::RTree,
Index::FTS(_) => IndexType::Inverted,
Index::IvfFlat(_)
| Index::IvfSq(_)
@@ -439,6 +462,7 @@ impl NativeTable {
#[cfg(test)]
mod tests {
use lance::index::DatasetIndexExt;
use std::sync::Arc;
use std::time::Duration;
@@ -1321,6 +1345,191 @@ mod tests {
assert_eq!(null_count, 1);
}
#[tokio::test]
async fn test_create_builtin_scalar_index() {
for (index, index_type, predicate, invalid_column) in [
(
Index::NGram(Default::default()),
crate::index::IndexType::NGram,
"contains(text, 'abc')",
"id",
),
(
Index::BloomFilter(Default::default()),
crate::index::IndexType::BloomFilter,
"text = 'abc'",
"flag",
),
(
Index::BloomFilter(
crate::index::scalar::BloomFilterIndexBuilder::default()
.number_of_items(2)
.unwrap()
.probability(0.01)
.unwrap(),
),
crate::index::IndexType::BloomFilter,
"text = 'abc'",
"flag",
),
] {
let conn = connect("memory://").execute().await.unwrap();
let batch = record_batch!(
("id", Int32, [1, 2, 3, 4]),
("text", Utf8, [Some("abc"), Some("xyz"), None, Some("")]),
("flag", Boolean, [true, false, true, false])
)
.unwrap();
let table = conn
.create_table("scalar", batch.clone())
.execute()
.await
.unwrap();
table.add(batch).execute().await.unwrap();
table
.create_index(&["text"], index.clone())
.name("text_search".into())
.train(false)
.execute()
.await
.unwrap();
assert_eq!(
table.list_indices().await.unwrap()[0].index_type,
index_type
);
assert!(
table
.create_index(&["text"], index.clone())
.name("text_search".into())
.replace(false)
.execute()
.await
.is_err()
);
table
.create_index(&["text"], index.clone())
.name("text_search".into())
.execute()
.await
.unwrap();
let stats = table.index_stats("text_search").await.unwrap().unwrap();
assert_eq!(stats.index_type, index_type);
assert_eq!(stats.num_indexed_rows, 8);
if let Index::BloomFilter(params) = &index {
let expected = serde_json::to_value(params).unwrap();
let dataset = table.as_native().unwrap().dataset.get().await.unwrap();
let stats: serde_json::Value =
serde_json::from_str(&dataset.index_statistics("text_search").await.unwrap())
.unwrap();
for (key, value) in expected.as_object().unwrap() {
assert_eq!(&stats["indices"][0][key], value);
}
}
for (filter, expected_rows) in [(predicate, 2), ("text IS NULL", 2), ("text = ''", 2)] {
let query = table.query().only_if(filter);
let plan = query.explain_plan(false).await.unwrap();
if filter == predicate {
assert!(plan.contains("ScalarIndexQuery"), "{plan}");
}
let batches = query
.execute()
.await
.unwrap()
.try_collect::<Vec<_>>()
.await
.unwrap();
assert_eq!(
batches.iter().map(RecordBatch::num_rows).sum::<usize>(),
expected_rows
);
}
let error = table
.create_index(&[invalid_column], index)
.execute()
.await
.unwrap_err();
assert!(
error.to_string().to_lowercase().contains(
if index_type == crate::index::IndexType::NGram {
"ngram"
} else {
"bloom"
}
),
"{error}"
);
}
}
#[cfg(feature = "geo")]
#[tokio::test]
async fn test_create_rtree_index() {
let points = StructArray::new(
vec![
Field::new("x", DataType::Float64, false),
Field::new("y", DataType::Float64, false),
]
.into(),
vec![
Arc::new(arrow_array::Float64Array::from(vec![0.0, 10.0, 0.0])),
Arc::new(arrow_array::Float64Array::from(vec![0.0, 10.0, 0.0])),
],
Some(arrow_buffer::NullBuffer::from(vec![true, true, false])),
);
let field = Field::new("geometry", points.data_type().clone(), true).with_metadata(
std::collections::HashMap::from([
("ARROW:extension:name".into(), "geoarrow.point".into()),
("ARROW:extension:metadata".into(), "{}".into()),
]),
);
let batch =
RecordBatch::try_new(Arc::new(Schema::new(vec![field])), vec![Arc::new(points)])
.unwrap();
let conn = connect("memory://").execute().await.unwrap();
let table = conn.create_table("spatial", batch).execute().await.unwrap();
table
.create_index(
&["geometry"],
Index::RTree(
crate::index::scalar::RTreeIndexBuilder::default()
.page_size(2)
.unwrap(),
),
)
.name("spatial_idx".into())
.execute()
.await
.unwrap();
let indices = table.list_indices().await.unwrap();
assert_eq!(indices.len(), 1);
assert_eq!(indices[0].index_type, crate::index::IndexType::RTree);
let stats = table.index_stats("spatial_idx").await.unwrap().unwrap();
assert_eq!(stats.index_type, crate::index::IndexType::RTree);
assert_eq!(stats.num_indexed_rows, 3);
let dataset = table.as_native().unwrap().dataset.get().await.unwrap();
let stats: serde_json::Value =
serde_json::from_str(&dataset.index_statistics("spatial_idx").await.unwrap()).unwrap();
assert_eq!(stats["indices"][0]["page_size"], 2);
for predicate in [
"ST_Intersects(geometry, ST_GeomFromText('POLYGON ((-1 -1, 1 -1, 1 1, -1 1, -1 -1))'))",
"geometry IS NULL",
] {
let query = table.query().only_if(predicate);
let plan = query.explain_plan(false).await.unwrap();
assert!(plan.contains("ScalarIndexQuery"), "{plan}");
let batches = query
.execute()
.await
.unwrap()
.try_collect::<Vec<_>>()
.await
.unwrap();
assert_eq!(batches.iter().map(RecordBatch::num_rows).sum::<usize>(), 1);
}
}
#[tokio::test]
async fn test_create_index_nested_field_paths() {
let tmp_dir = tempdir().unwrap();