-
Notifications
You must be signed in to change notification settings - Fork 767
fix: skip empty string docs in FTS stats #7699
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
cc50dfd
8a86ade
acbe395
58120c1
5fbdb7f
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -6784,12 +6784,13 @@ async fn tokenize_and_count( | |
| .extend(std::iter::repeat_n(0, query_tokens.len())); | ||
|
|
||
| let Some(doc) = doc else { | ||
| append_counts(*row_id, 0, &temp_query_token_counts); | ||
| continue; | ||
| }; | ||
|
|
||
| let all_tokens = count_text(doc, &mut temp_query_token_counts); | ||
| append_counts(*row_id, all_tokens, &temp_query_token_counts); | ||
| if all_tokens > 0 { | ||
| append_counts(*row_id, all_tokens, &temp_query_token_counts); | ||
| } | ||
| } | ||
| } | ||
| DataType::List(_) => { | ||
|
|
@@ -6981,12 +6982,6 @@ fn flat_bm25_score( | |
| for _ in 0..counted_input.num_rows() { | ||
| let num_tokens_in_doc = all_token_counts_iter.next().expect_ok()?; | ||
| let row_id = row_ids_iter.next().expect_ok()?; | ||
| if num_tokens_in_doc == 0 { | ||
| for _ in query_tokens { | ||
| query_token_counts_iter.next().expect_ok()?; | ||
| } | ||
| continue; | ||
| } | ||
| let doc_norm = K1 * (1.0 - B + B * num_tokens_in_doc as f32 / scorer.avg_doc_length()); | ||
| let mut score = 0.0; | ||
| for token in query_tokens { | ||
|
|
@@ -11129,6 +11124,60 @@ mod tests { | |
| ); | ||
| } | ||
|
|
||
| #[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(); | ||
|
Comment on lines
+11129
to
+11147
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win 🧩 Analysis chain🏁 Script executed: #!/bin/bash
set -euo pipefail
# Inspect the target area and nearby test context.
sed -n '9440,9525p' rust/lance-index/src/scalar/inverted/index.rs
# Find existing uses of record_batch!() in Rust tests for repo-local style.
rg -n --glob '*.rs' 'record_batch!\(' rust | head -n 80
# Check the Arrow crate version in the workspace lockfile / manifests.
rg -n --glob 'Cargo.lock' 'name = "(arrow-array|arrow-ar|arrow)"|version = ' Cargo.lock
rg -n --glob 'Cargo.toml' 'arrow-array|arrow' Cargo.toml rust/**/Cargo.tomlRepository: lance-format/lance Length of output: 39859 Use Replace the manual 🤖 Prompt for AI AgentsSource: Coding guidelines |
||
| 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, | ||
| ) | ||
| .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_search_uses_full_document_length_for_normalization() { | ||
| let schema = Arc::new(Schema::new(vec![ | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could we apply this zero-token policy consistently across the other FTS paths? The flat/unindexed Utf8 path still appends every row and adds
counted_input.num_rows()tonum_docs, while MemWAL keeps zero-length documents and flushes them into theDocSet. A whitespace-only, stop-word-only, or overlength document is therefore excluded after a normal index build but counted while it is unindexed or flushed through MemWAL, so avgdl/IDF and potentially top-k ordering can change as the same data moves between these paths. Please update those paths as part of this change and add cross-path coverage.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for pointing this out. I understood the consistency issue at a high level, but I wasn’t sure about the best way to implement the policy across the flat/unindexed and MemWAL paths.
This follow-up commit was implemented mostly with assistance from GPT-5.6-sol. It now excludes zero-token documents from the flat counted input, MemWAL corpus statistics, frozen DocSets, and the flush builder. The added coverage uses whitespace-only, stop-word-only, overlength, null, and normal documents, and checks that
num_docs,avgdl, and the IDF/query weight match the normal-build baseline through the flat path and the MemWAL tail-to-frozen/flush transitions. It also checks that the MemWAL score remains unchanged across freeze.Thanks again for the detailed review—it clarified the invariant that needs to hold across the full FTS lifecycle.