mirror of
https://github.com/lancedb/lancedb.git
synced 2026-08-18 12:08:35 +00:00
f7feed48c3
## What Expose custom FTS stop-word lists in the Python and TypeScript public APIs, including their standalone tokenize helpers and remote index creation. This PR supports concrete string lists only. It does not add file or LanceDB-table stop-word sources. ## Why Rust already exposes Lance's custom stop-word list option. The Python and TypeScript APIs did not pass it through, and local index details did not retain the full tokenizer parameters needed by index-backed tokenization after reopening a table. ## How - Add `custom_stop_words` / `customStopWords` to the Python and TypeScript FTS and tokenize options. - Preserve `None` / `undefined`, empty lists, and list contents without normalization. - Load the persisted FTS segment parameters when returning local index details. - Serialize the concrete list in remote create-index requests. - Keep Python and TypeScript tests thin; behavior, persistence, query tokenization, and remote JSON coverage live primarily in Rust. ## Validation - `cargo check --quiet --features remote --tests --examples` - `cargo clippy --quiet --features remote --tests --examples` - `cargo test --quiet --features remote --tests` - Python extension rebuild with `uv` and `maturin` - Targeted Python tests: 4 passed - Python `ruff format --check` and `ruff check` - TypeScript build, typecheck, Biome lint, generated docs, and targeted tests --------- Co-authored-by: Yang Cen <yangcen@Yangs-Mac-mini.local>
352 lines
12 KiB
Rust
352 lines
12 KiB
Rust
// SPDX-License-Identifier: Apache-2.0
|
|
// SPDX-FileCopyrightText: Copyright The LanceDB Authors
|
|
|
|
use std::sync::Mutex;
|
|
|
|
use lancedb::index::Index as LanceDbIndex;
|
|
use lancedb::index::scalar::{BTreeIndexBuilder, FmIndexBuilder, FtsIndexBuilder};
|
|
use lancedb::index::vector::{
|
|
IvfFlatIndexBuilder, IvfHnswPqIndexBuilder, IvfHnswSqIndexBuilder, IvfPqIndexBuilder,
|
|
IvfRqIndexBuilder,
|
|
};
|
|
use lancedb::tokenize as lancedb_tokenize;
|
|
use napi_derive::napi;
|
|
|
|
use crate::error::NapiErrorExt;
|
|
use crate::table::FtsToken;
|
|
use crate::util::parse_distance_type;
|
|
|
|
#[napi]
|
|
pub struct Index {
|
|
inner: Mutex<Option<LanceDbIndex>>,
|
|
}
|
|
|
|
impl Index {
|
|
pub fn consume(&self) -> napi::Result<LanceDbIndex> {
|
|
self.inner
|
|
.lock()
|
|
.unwrap()
|
|
.take()
|
|
.ok_or(napi::Error::from_reason(
|
|
"attempt to use an index more than once",
|
|
))
|
|
}
|
|
}
|
|
|
|
#[napi(catch_unwind)]
|
|
#[allow(dead_code, clippy::too_many_arguments)]
|
|
pub fn tokenize(
|
|
query: String,
|
|
base_tokenizer: Option<String>,
|
|
language: Option<String>,
|
|
max_token_length: Option<u32>,
|
|
lower_case: Option<bool>,
|
|
stem: Option<bool>,
|
|
remove_stop_words: Option<bool>,
|
|
custom_stop_words: Option<Vec<String>>,
|
|
ascii_folding: Option<bool>,
|
|
ngram_min_length: Option<u32>,
|
|
ngram_max_length: Option<u32>,
|
|
prefix_only: Option<bool>,
|
|
) -> napi::Result<Vec<FtsToken>> {
|
|
let mut opts = FtsIndexBuilder::default();
|
|
if let Some(base_tokenizer) = base_tokenizer {
|
|
opts = opts.base_tokenizer(base_tokenizer);
|
|
}
|
|
if let Some(language) = language {
|
|
opts = opts.language(&language).map_err(|_| {
|
|
napi::Error::from_reason(format!(
|
|
"LanceDB does not support the requested language: '{}'",
|
|
language
|
|
))
|
|
})?;
|
|
}
|
|
if let Some(max_token_length) = max_token_length {
|
|
opts = opts.max_token_length(Some(max_token_length as usize));
|
|
}
|
|
if let Some(lower_case) = lower_case {
|
|
opts = opts.lower_case(lower_case);
|
|
}
|
|
if let Some(stem) = stem {
|
|
opts = opts.stem(stem);
|
|
}
|
|
if let Some(remove_stop_words) = remove_stop_words {
|
|
opts = opts.remove_stop_words(remove_stop_words);
|
|
}
|
|
opts = opts.custom_stop_words(custom_stop_words);
|
|
if let Some(ascii_folding) = ascii_folding {
|
|
opts = opts.ascii_folding(ascii_folding);
|
|
}
|
|
if let Some(ngram_min_length) = ngram_min_length {
|
|
opts = opts.ngram_min_length(ngram_min_length);
|
|
}
|
|
if let Some(ngram_max_length) = ngram_max_length {
|
|
opts = opts.ngram_max_length(ngram_max_length);
|
|
}
|
|
if let Some(prefix_only) = prefix_only {
|
|
opts = opts.ngram_prefix_only(prefix_only);
|
|
}
|
|
|
|
Ok(lancedb_tokenize(&query, &opts)
|
|
.default_error()?
|
|
.into_iter()
|
|
.map(FtsToken::from)
|
|
.collect())
|
|
}
|
|
|
|
#[napi]
|
|
impl Index {
|
|
#[napi(factory)]
|
|
pub fn ivf_pq(
|
|
distance_type: Option<String>,
|
|
num_partitions: Option<u32>,
|
|
num_sub_vectors: Option<u32>,
|
|
num_bits: Option<u32>,
|
|
max_iterations: Option<u32>,
|
|
sample_rate: Option<u32>,
|
|
) -> napi::Result<Self> {
|
|
let mut ivf_pq_builder = IvfPqIndexBuilder::default();
|
|
if let Some(distance_type) = distance_type {
|
|
let distance_type = parse_distance_type(distance_type)?;
|
|
ivf_pq_builder = ivf_pq_builder.distance_type(distance_type);
|
|
}
|
|
if let Some(num_partitions) = num_partitions {
|
|
ivf_pq_builder = ivf_pq_builder.num_partitions(num_partitions);
|
|
}
|
|
if let Some(num_sub_vectors) = num_sub_vectors {
|
|
ivf_pq_builder = ivf_pq_builder.num_sub_vectors(num_sub_vectors);
|
|
}
|
|
if let Some(num_bits) = num_bits {
|
|
ivf_pq_builder = ivf_pq_builder.num_bits(num_bits);
|
|
}
|
|
if let Some(max_iterations) = max_iterations {
|
|
ivf_pq_builder = ivf_pq_builder.max_iterations(max_iterations);
|
|
}
|
|
if let Some(sample_rate) = sample_rate {
|
|
ivf_pq_builder = ivf_pq_builder.sample_rate(sample_rate);
|
|
}
|
|
Ok(Self {
|
|
inner: Mutex::new(Some(LanceDbIndex::IvfPq(ivf_pq_builder))),
|
|
})
|
|
}
|
|
|
|
#[napi(factory)]
|
|
pub fn ivf_rq(
|
|
distance_type: Option<String>,
|
|
num_partitions: Option<u32>,
|
|
num_bits: Option<u32>,
|
|
max_iterations: Option<u32>,
|
|
sample_rate: Option<u32>,
|
|
) -> napi::Result<Self> {
|
|
let mut ivf_rq_builder = IvfRqIndexBuilder::default();
|
|
if let Some(distance_type) = distance_type {
|
|
let distance_type = parse_distance_type(distance_type)?;
|
|
ivf_rq_builder = ivf_rq_builder.distance_type(distance_type);
|
|
}
|
|
if let Some(num_partitions) = num_partitions {
|
|
ivf_rq_builder = ivf_rq_builder.num_partitions(num_partitions);
|
|
}
|
|
if let Some(num_bits) = num_bits {
|
|
ivf_rq_builder = ivf_rq_builder.num_bits(num_bits);
|
|
}
|
|
if let Some(max_iterations) = max_iterations {
|
|
ivf_rq_builder = ivf_rq_builder.max_iterations(max_iterations);
|
|
}
|
|
if let Some(sample_rate) = sample_rate {
|
|
ivf_rq_builder = ivf_rq_builder.sample_rate(sample_rate);
|
|
}
|
|
Ok(Self {
|
|
inner: Mutex::new(Some(LanceDbIndex::IvfRq(ivf_rq_builder))),
|
|
})
|
|
}
|
|
|
|
#[napi(factory)]
|
|
pub fn ivf_flat(
|
|
distance_type: Option<String>,
|
|
num_partitions: Option<u32>,
|
|
max_iterations: Option<u32>,
|
|
sample_rate: Option<u32>,
|
|
) -> napi::Result<Self> {
|
|
let mut ivf_flat_builder = IvfFlatIndexBuilder::default();
|
|
if let Some(distance_type) = distance_type {
|
|
let distance_type = parse_distance_type(distance_type)?;
|
|
ivf_flat_builder = ivf_flat_builder.distance_type(distance_type);
|
|
}
|
|
if let Some(num_partitions) = num_partitions {
|
|
ivf_flat_builder = ivf_flat_builder.num_partitions(num_partitions);
|
|
}
|
|
if let Some(max_iterations) = max_iterations {
|
|
ivf_flat_builder = ivf_flat_builder.max_iterations(max_iterations);
|
|
}
|
|
if let Some(sample_rate) = sample_rate {
|
|
ivf_flat_builder = ivf_flat_builder.sample_rate(sample_rate);
|
|
}
|
|
Ok(Self {
|
|
inner: Mutex::new(Some(LanceDbIndex::IvfFlat(ivf_flat_builder))),
|
|
})
|
|
}
|
|
|
|
#[napi(factory)]
|
|
pub fn btree() -> Self {
|
|
Self {
|
|
inner: Mutex::new(Some(LanceDbIndex::BTree(BTreeIndexBuilder::default()))),
|
|
}
|
|
}
|
|
|
|
#[napi(factory)]
|
|
pub fn bitmap() -> Self {
|
|
Self {
|
|
inner: Mutex::new(Some(LanceDbIndex::Bitmap(Default::default()))),
|
|
}
|
|
}
|
|
|
|
#[napi(factory)]
|
|
pub fn label_list() -> Self {
|
|
Self {
|
|
inner: Mutex::new(Some(LanceDbIndex::LabelList(Default::default()))),
|
|
}
|
|
}
|
|
|
|
#[napi(factory)]
|
|
pub fn fm() -> Self {
|
|
Self {
|
|
inner: Mutex::new(Some(LanceDbIndex::Fm(FmIndexBuilder::default()))),
|
|
}
|
|
}
|
|
|
|
#[napi(factory)]
|
|
#[allow(clippy::too_many_arguments)]
|
|
pub fn fts(
|
|
with_position: Option<bool>,
|
|
base_tokenizer: Option<String>,
|
|
language: Option<String>,
|
|
max_token_length: Option<u32>,
|
|
lower_case: Option<bool>,
|
|
stem: Option<bool>,
|
|
remove_stop_words: Option<bool>,
|
|
custom_stop_words: Option<Vec<String>>,
|
|
ascii_folding: Option<bool>,
|
|
ngram_min_length: Option<u32>,
|
|
ngram_max_length: Option<u32>,
|
|
prefix_only: Option<bool>,
|
|
block_size: Option<u32>,
|
|
) -> napi::Result<Self> {
|
|
let mut opts = FtsIndexBuilder::default();
|
|
if let Some(with_position) = with_position {
|
|
opts = opts.with_position(with_position);
|
|
}
|
|
if let Some(base_tokenizer) = base_tokenizer {
|
|
opts = opts.base_tokenizer(base_tokenizer);
|
|
}
|
|
if let Some(language) = language {
|
|
opts = opts.language(&language).unwrap();
|
|
}
|
|
if let Some(max_token_length) = max_token_length {
|
|
opts = opts.max_token_length(Some(max_token_length as usize));
|
|
}
|
|
if let Some(lower_case) = lower_case {
|
|
opts = opts.lower_case(lower_case);
|
|
}
|
|
if let Some(stem) = stem {
|
|
opts = opts.stem(stem);
|
|
}
|
|
if let Some(remove_stop_words) = remove_stop_words {
|
|
opts = opts.remove_stop_words(remove_stop_words);
|
|
}
|
|
opts = opts.custom_stop_words(custom_stop_words);
|
|
if let Some(ascii_folding) = ascii_folding {
|
|
opts = opts.ascii_folding(ascii_folding);
|
|
}
|
|
if let Some(ngram_min_length) = ngram_min_length {
|
|
opts = opts.ngram_min_length(ngram_min_length);
|
|
}
|
|
if let Some(ngram_max_length) = ngram_max_length {
|
|
opts = opts.ngram_max_length(ngram_max_length);
|
|
}
|
|
if let Some(prefix_only) = prefix_only {
|
|
opts = opts.ngram_prefix_only(prefix_only);
|
|
}
|
|
if let Some(block_size) = block_size {
|
|
opts = opts
|
|
.block_size(block_size as usize)
|
|
.map_err(|err| napi::Error::from_reason(err.to_string()))?;
|
|
}
|
|
|
|
Ok(Self {
|
|
inner: Mutex::new(Some(LanceDbIndex::FTS(opts))),
|
|
})
|
|
}
|
|
|
|
#[napi(factory)]
|
|
pub fn hnsw_pq(
|
|
distance_type: Option<String>,
|
|
num_partitions: Option<u32>,
|
|
num_sub_vectors: Option<u32>,
|
|
max_iterations: Option<u32>,
|
|
sample_rate: Option<u32>,
|
|
m: Option<u32>,
|
|
ef_construction: Option<u32>,
|
|
) -> napi::Result<Self> {
|
|
let mut hnsw_pq_builder = IvfHnswPqIndexBuilder::default();
|
|
if let Some(distance_type) = distance_type {
|
|
let distance_type = parse_distance_type(distance_type)?;
|
|
hnsw_pq_builder = hnsw_pq_builder.distance_type(distance_type);
|
|
}
|
|
if let Some(num_partitions) = num_partitions {
|
|
hnsw_pq_builder = hnsw_pq_builder.num_partitions(num_partitions);
|
|
}
|
|
if let Some(num_sub_vectors) = num_sub_vectors {
|
|
hnsw_pq_builder = hnsw_pq_builder.num_sub_vectors(num_sub_vectors);
|
|
}
|
|
if let Some(max_iterations) = max_iterations {
|
|
hnsw_pq_builder = hnsw_pq_builder.max_iterations(max_iterations);
|
|
}
|
|
if let Some(sample_rate) = sample_rate {
|
|
hnsw_pq_builder = hnsw_pq_builder.sample_rate(sample_rate);
|
|
}
|
|
if let Some(m) = m {
|
|
hnsw_pq_builder = hnsw_pq_builder.num_edges(m);
|
|
}
|
|
if let Some(ef_construction) = ef_construction {
|
|
hnsw_pq_builder = hnsw_pq_builder.ef_construction(ef_construction);
|
|
}
|
|
Ok(Self {
|
|
inner: Mutex::new(Some(LanceDbIndex::IvfHnswPq(hnsw_pq_builder))),
|
|
})
|
|
}
|
|
|
|
#[napi(factory)]
|
|
pub fn hnsw_sq(
|
|
distance_type: Option<String>,
|
|
num_partitions: Option<u32>,
|
|
max_iterations: Option<u32>,
|
|
sample_rate: Option<u32>,
|
|
m: Option<u32>,
|
|
ef_construction: Option<u32>,
|
|
) -> napi::Result<Self> {
|
|
let mut hnsw_sq_builder = IvfHnswSqIndexBuilder::default();
|
|
if let Some(distance_type) = distance_type {
|
|
let distance_type = parse_distance_type(distance_type)?;
|
|
hnsw_sq_builder = hnsw_sq_builder.distance_type(distance_type);
|
|
}
|
|
if let Some(num_partitions) = num_partitions {
|
|
hnsw_sq_builder = hnsw_sq_builder.num_partitions(num_partitions);
|
|
}
|
|
if let Some(max_iterations) = max_iterations {
|
|
hnsw_sq_builder = hnsw_sq_builder.max_iterations(max_iterations);
|
|
}
|
|
if let Some(sample_rate) = sample_rate {
|
|
hnsw_sq_builder = hnsw_sq_builder.sample_rate(sample_rate);
|
|
}
|
|
if let Some(m) = m {
|
|
hnsw_sq_builder = hnsw_sq_builder.num_edges(m);
|
|
}
|
|
if let Some(ef_construction) = ef_construction {
|
|
hnsw_sq_builder = hnsw_sq_builder.ef_construction(ef_construction);
|
|
}
|
|
Ok(Self {
|
|
inner: Mutex::new(Some(LanceDbIndex::IvfHnswSq(hnsw_sq_builder))),
|
|
})
|
|
}
|
|
}
|