use super::*;
#[tokio::test]
async fn flat_bm25_search_stream_with_metrics_records_elapsed_compute() {
use crate::scalar::inverted::tokenizer::document_tokenizer::TextTokenizer;
use arrow_array::{StringArray, UInt64Array};
use lance_tokenizer::{SimpleTokenizer, TextAnalyzer};
let schema = Arc::new(Schema::new(vec![
ROW_ID_FIELD.clone(),
Field::new("text", DataType::Utf8, false),
]));
let batch = RecordBatch::try_new(
schema.clone(),
vec![
Arc::new(UInt64Array::from(vec![0u64, 1, 2, 3])),
Arc::new(StringArray::from(vec![
"the quick brown fox",
"lazy dog sleeps",
"the brown fox jumps over",
"completely unrelated text",
])),
],
)
.unwrap();
let input: SendableRecordBatchStream = Box::pin(RecordBatchStreamAdapter::new(
schema.clone(),
stream::iter(vec![Ok(batch)]),
));
let tokenizer: Box<dyn LanceTokenizer> = Box::new(TextTokenizer::new(
TextAnalyzer::builder(SimpleTokenizer::default()).build(),
));
let elapsed_compute = Time::default();
let result_stream = flat_bm25_search_stream_with_metrics(
input,
"text".to_string(),
"fox".to_string(),
tokenizer,
None,
100,
Some(elapsed_compute.clone()),
)
.await
.unwrap();
let batches: Vec<_> = result_stream.try_collect().await.unwrap();
assert!(!batches.is_empty(), "expected at least one scored batch");
assert!(
elapsed_compute.value() > 0,
"elapsed_compute should have been populated; got 0"
);
}
#[tokio::test]
async fn flat_bm25_phrase_honors_positions_slop_and_repeated_terms() {
async fn search(query: &str, slop: u32) -> Vec<u64> {
let schema = Arc::new(Schema::new(vec![
ROW_ID_FIELD.clone(),
Field::new("text", DataType::Utf8, false),
]));
let batch = RecordBatch::try_new(
schema.clone(),
vec![
Arc::new(UInt64Array::from_iter_values(0..5)),
Arc::new(StringArray::from(vec![
"alpha beta",
"alpha gap beta",
"alpha gap gap beta",
"alpha alpha beta",
"beta alpha",
])),
],
)
.unwrap();
let input: SendableRecordBatchStream = Box::pin(RecordBatchStreamAdapter::new(
schema,
stream::iter(vec![Ok(batch)]),
));
let tokenizer: Box<dyn LanceTokenizer> = Box::new(TextTokenizer::new(
TextAnalyzer::builder(SimpleTokenizer::default()).build(),
));
let (stream, _) = flat_bm25_search_stream_with_options_and_scorer(
input,
"text".to_string(),
query.to_string(),
tokenizer,
None,
FlatBm25SearchOptions {
target_batch_size: 100,
elapsed_compute: None,
document_granularity: DocumentGranularity::Row,
operator: Operator::And,
boost: 1.0,
phrase_slop: Some(slop),
},
)
.await
.unwrap();
stream
.try_collect::<Vec<_>>()
.await
.unwrap()
.iter()
.flat_map(|batch| batch[ROW_ID].as_primitive::<UInt64Type>().values())
.copied()
.collect()
}
assert_eq!(search("alpha beta", 0).await, vec![0, 3]);
assert_eq!(search("alpha beta", 1).await, vec![0, 1, 3]);
assert_eq!(search("alpha alpha", 0).await, vec![3]);
}
#[test]
fn flat_full_text_search_supports_phrase_queries() {
let schema = Arc::new(Schema::new(vec![
ROW_ID_FIELD.clone(),
Field::new("text", DataType::Utf8, false),
]));
let batch = RecordBatch::try_new(
schema,
vec![
Arc::new(UInt64Array::from_iter_values(0..4)),
Arc::new(StringArray::from(vec![
"alpha beta",
"alpha gap beta",
"alpha alpha beta",
"beta alpha",
])),
],
)
.unwrap();
assert_eq!(
flat_full_text_search(&[&batch], "text", "\"alpha beta\"", None).unwrap(),
vec![0, 2]
);
assert_eq!(
flat_full_text_search(&[&batch], "text", "\"alpha alpha\"", None).unwrap(),
vec![2]
);
assert!(!is_phrase_query("\""));
}
#[tokio::test]
async fn flat_bm25_skips_zero_token_documents_from_corpus_stats() {
let schema = Arc::new(Schema::new(vec![
ROW_ID_FIELD.clone(),
Field::new("text", DataType::Utf8, true),
]));
let batch = RecordBatch::try_new(
schema,
vec![
Arc::new(UInt64Array::from(vec![0_u64, 1, 2, 3, 4, 5])) as ArrayRef,
Arc::new(StringArray::from(vec![
Some(""),
Some(" "),
Some("the"),
Some("overlength"),
None,
Some("hello"),
])) as ArrayRef,
],
)
.unwrap();
let params = InvertedIndexParams::new("whitespace".to_string(), Language::English)
.remove_stop_words(true)
.stem(false)
.max_token_length(Some(6));
let query_tokens = Arc::new(Tokens::new(vec!["hello".to_string()], DocType::Text));
let counted_input = tokenize_and_count(
stream::iter(vec![Ok(batch)]),
params.build().unwrap(),
query_tokens.clone(),
1,
None,
0,
None,
)
.await
.unwrap();
assert_eq!(counted_input.num_rows(), 1);
assert_eq!(
counted_input[ROW_ID].as_primitive::<UInt64Type>().values(),
&[5]
);
let scorer = initialize_scorer(None, query_tokens.as_ref(), &counted_input);
let expected_scorer = MemBM25Scorer::new(1, 1, HashMap::from([("hello".to_string(), 1)]));
assert_eq!(scorer.total_tokens, 1);
assert_eq!(scorer.num_docs(), 1);
assert_eq!(scorer.num_docs_containing_token("hello"), 1);
assert_eq!(scorer.avg_doc_length(), expected_scorer.avg_doc_length());
assert_eq!(
scorer.query_weight("hello"),
expected_scorer.query_weight("hello")
);
}
#[tokio::test]
async fn flat_bm25_preserves_zero_token_list_element_documents() {
let coordinate_column = doc_index_storage_column(0);
let schema = Arc::new(Schema::new(vec![
ROW_ID_FIELD.clone(),
Field::new(&coordinate_column, DataType::UInt32, false),
Field::new("text", DataType::Utf8, true),
]));
let batch = RecordBatch::try_new(
schema,
vec![
Arc::new(UInt64Array::from(vec![7_u64; 6])) as ArrayRef,
Arc::new(UInt32Array::from(vec![0, 1, 2, 3, 4, 5])) as ArrayRef,
Arc::new(StringArray::from(vec![
None,
Some(""),
Some(" "),
Some("the"),
Some("overlength"),
Some("hello"),
])) as ArrayRef,
],
)
.unwrap();
let params = InvertedIndexParams::new("whitespace".to_string(), Language::English)
.remove_stop_words(true)
.stem(false)
.max_token_length(Some(6));
let query_tokens = Arc::new(Tokens::new(vec!["hello".to_string()], DocType::Text));
let counted_input = tokenize_and_count(
stream::iter(vec![Ok(batch)]),
params.build().unwrap(),
query_tokens.clone(),
2,
None,
1,
None,
)
.await
.unwrap();
assert_eq!(counted_input.num_rows(), 6);
assert_eq!(
counted_input[&coordinate_column]
.as_primitive::<UInt32Type>()
.values(),
&[0, 1, 2, 3, 4, 5]
);
let scorer = initialize_scorer(None, query_tokens.as_ref(), &counted_input);
assert_eq!(scorer.total_tokens, 1);
assert_eq!(scorer.num_docs(), 6);
assert_eq!(scorer.num_docs_containing_token("hello"), 1);
}
#[tokio::test]
async fn flat_bm25_search_uses_full_document_length_for_normalization() {
let schema = Arc::new(Schema::new(vec![
ROW_ID_FIELD.clone(),
Field::new("text", DataType::Utf8, false),
]));
let batch = RecordBatch::try_new(
schema.clone(),
vec![
Arc::new(UInt64Array::from(vec![0u64, 1])),
Arc::new(StringArray::from(vec![
"alpha",
"alpha filler filler filler filler filler filler filler filler filler",
])),
],
)
.unwrap();
let input: SendableRecordBatchStream = Box::pin(RecordBatchStreamAdapter::new(
schema.clone(),
stream::iter(vec![Ok(batch)]),
));
let tokenizer: Box<dyn LanceTokenizer> = Box::new(TextTokenizer::new(
TextAnalyzer::builder(SimpleTokenizer::default()).build(),
));
let result_stream = flat_bm25_search_stream_with_metrics(
input,
"text".to_string(),
"alpha".to_string(),
tokenizer,
None,
100,
None,
)
.await
.unwrap();
let batches: Vec<_> = result_stream.try_collect().await.unwrap();
let scored = arrow::compute::concat_batches(&FTS_SCHEMA, &batches).unwrap();
let row_ids = scored[ROW_ID].as_primitive::<UInt64Type>();
let scores = scored[SCORE_COL].as_primitive::<Float32Type>();
assert_eq!(row_ids.values(), &[0, 1]);
assert!(
scores.value(0) > scores.value(1),
"same term frequency should score shorter document higher; short={}, long={}",
scores.value(0),
scores.value(1)
);
}
#[tokio::test]
async fn flat_bm25_search_treats_string_lists_as_row_documents() {
let mut docs_builder = GenericListBuilder::<i32, _>::new(GenericStringBuilder::<i32>::new());
docs_builder.values().append_value("alpha");
docs_builder.values().append_value("alpha beta");
docs_builder.append(true);
docs_builder.values().append_value("beta");
docs_builder.append(true);
docs_builder.append(true);
docs_builder.values().append_null();
docs_builder.append(true);
docs_builder.append(false);
let docs = Arc::new(docs_builder.finish()) as ArrayRef;
let schema = Arc::new(Schema::new(vec![
ROW_ID_FIELD.clone(),
Field::new("text", docs.data_type().clone(), true),
]));
let batch = RecordBatch::try_new(
schema.clone(),
vec![
Arc::new(UInt64Array::from(vec![0u64, 1, 2, 3, 4])) as ArrayRef,
docs,
],
)
.unwrap();
let input: SendableRecordBatchStream = Box::pin(RecordBatchStreamAdapter::new(
schema.clone(),
stream::iter(vec![Ok(batch)]),
));
let tokenizer: Box<dyn LanceTokenizer> = Box::new(TextTokenizer::new(
TextAnalyzer::builder(SimpleTokenizer::default()).build(),
));
let result_stream = flat_bm25_search_stream_with_metrics(
input,
"text".to_string(),
"alpha".to_string(),
tokenizer,
None,
100,
None,
)
.await
.unwrap();
let batches: Vec<_> = result_stream.try_collect().await.unwrap();
let scored = arrow::compute::concat_batches(&FTS_SCHEMA, &batches).unwrap();
let row_ids = scored[ROW_ID].as_primitive::<UInt64Type>();
assert_eq!(row_ids.values(), &[0]);
}
#[tokio::test]
async fn flat_bm25_search_code_and_uses_position_groups() {
let schema = Arc::new(Schema::new(vec![
ROW_ID_FIELD.clone(),
Field::new("code", DataType::Utf8, false),
]));
let batch = RecordBatch::try_new(
schema.clone(),
vec![
Arc::new(UInt64Array::from(vec![0u64, 1, 2, 3])),
Arc::new(StringArray::from(vec![
"get user name",
"getUserName",
"get user",
"username",
])),
],
)
.unwrap();
let input: SendableRecordBatchStream = Box::pin(RecordBatchStreamAdapter::new(
schema.clone(),
stream::iter(vec![Ok(batch)]),
));
let tokenizer = InvertedIndexParams::code()
.split_identifiers(true)
.build()
.unwrap();
let result_stream = flat_bm25_search_stream_with_metrics_and_operator(
input,
"code".to_string(),
"getUserName".to_string(),
tokenizer,
None,
100,
Operator::And,
None,
)
.await
.unwrap();
let batches: Vec<_> = result_stream.try_collect().await.unwrap();
let scored = arrow::compute::concat_batches(&FTS_SCHEMA, &batches).unwrap();
let mut row_ids = scored[ROW_ID]
.as_primitive::<UInt64Type>()
.values()
.to_vec();
row_ids.sort_unstable();
assert_eq!(row_ids, vec![0, 1]);
}
#[tokio::test]
async fn flat_bm25_search_code_and_counts_repeated_subwords() {
let schema = Arc::new(Schema::new(vec![
ROW_ID_FIELD.clone(),
Field::new("code", DataType::Utf8, false),
]));
let batch = RecordBatch::try_new(
schema.clone(),
vec![
Arc::new(UInt64Array::from(vec![0u64, 1])),
Arc::new(StringArray::from(vec![
"pub fn edge_flat_generic_return<T>() -> Result<T, EdgeFlatError> where T: TryFrom<String> { todo!() }",
"pub fn edge_flat_generic_return<T>() -> Result<T> { todo!() }",
])),
],
)
.unwrap();
let input: SendableRecordBatchStream = Box::pin(RecordBatchStreamAdapter::new(
schema.clone(),
stream::iter(vec![Ok(batch)]),
));
let tokenizer = InvertedIndexParams::code().build().unwrap();
let result_stream = flat_bm25_search_stream_with_metrics_and_operator(
input,
"code".to_string(),
"edge_flat_generic_return TryFrom EdgeFlatError Result".to_string(),
tokenizer,
None,
100,
Operator::And,
None,
)
.await
.unwrap();
let batches: Vec<_> = result_stream.try_collect().await.unwrap();
let scored = arrow::compute::concat_batches(&FTS_SCHEMA, &batches).unwrap();
let row_ids = scored[ROW_ID].as_primitive::<UInt64Type>().values();
assert_eq!(row_ids, &[0]);
}