mirror of
https://github.com/lancedb/lancedb.git
synced 2026-10-14 15:52:26 +00:00
`Index.fts({ language: "english" })` and other unsupported language
values abort the Node process because the native factory unwraps the
error returned by `FtsIndexBuilder::language`.
Convert that error into a catchable JavaScript `Error`, using the same
message as `tokenize` and Python. Add regression coverage for
`"Klingon"` and lowercase `"english"`, plus a valid English stemming and
search case across Arrow 15–18.
Validation:
- Native and TypeScript builds: `pnpm build:debug --profile ci` and
`pnpm tsc`.
- Full-text and tokenization tests: `pnpm test __test__/table.test.ts
--runInBand --testNamePattern 'full text|FTS|tokeniz'` — 69 passed.
- Rust formatting and lint: `cargo fmt --all`; `cargo clippy --profile
ci --target x86_64-unknown-linux-gnu -p lancedb-nodejs -- -D warnings`.
- TypeScript formatting and lint: `pnpm exec biome format --write
__test__/table.test.ts` and `pnpm lint`; `pnpm run docs` passed without
generated changes.
Fixes #4367
<!-- lance-gatekeeper-fix:v1 agent=271bb96150580678f4e03b8e36beca03
generation=1 -->
Co-authored-by: Gatefixer <313497061+lancedb-gatefixer[bot]@users.noreply.github.com>
357 lines
12 KiB
Rust
357 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).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);
|
|
}
|
|
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))),
|
|
})
|
|
}
|
|
}
|