Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 8 additions & 9 deletions rust/lance-index/src/scalar/inverted/builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1254,7 +1254,7 @@ impl IndexWorker {
.filter_map(|(doc, row_id)| doc.map(|doc| (doc, *row_id)));

for (doc, row_id) in docs {
self.process_document(row_id, DocumentSource::Text(doc), false)
self.process_document(row_id, DocumentSource::Text(doc))
.await?;
}
}
Expand Down Expand Up @@ -1298,7 +1298,7 @@ impl IndexWorker {
continue;
};

self.process_document(*row_id, DocumentSource::StringList(doc.as_ref()), true)
self.process_document(*row_id, DocumentSource::StringList(doc.as_ref()))
.await?;
}

Expand All @@ -1324,12 +1324,7 @@ impl IndexWorker {
doc
}

async fn process_document(
&mut self,
row_id: u64,
document: DocumentSource<'_>,
skip_empty_document: bool,
) -> Result<()> {
async fn process_document(&mut self, row_id: u64, document: DocumentSource<'_>) -> Result<()> {
let with_position = self.has_position();
let builder_was_empty = self.builder.docs.is_empty();
let old_temporary_memory_size = self.temporary_memory_size();
Expand Down Expand Up @@ -1435,7 +1430,11 @@ impl IndexWorker {
self.builder.tokens.memory_size() as u64,
);

if skip_empty_document && token_num == 0 {
// A document that produces no tokens (e.g. an empty or whitespace-only
// string, or a list whose elements tokenize to nothing) is not indexed:
// it carries no searchable content, so counting it would only inflate
// `num_docs` and skew document-count-based statistics such as BM25.
if token_num == 0 {
self.last_token_count = 0;
self.trim_temporary_buffers();
self.adjust_tracked_memory_size(
Expand Down
30 changes: 29 additions & 1 deletion rust/lance/src/dataset/tests/dataset_index.rs
Original file line number Diff line number Diff line change
Expand Up @@ -402,10 +402,18 @@ async fn test_create_fts_index_with_empty_strings() {
false,
)]));

// Mix documents that produce tokens with ones that tokenize to nothing
// (empty and whitespace-only strings). The latter carry no searchable
// content and must not be counted in `num_docs`.
let batches: Vec<RecordBatch> = vec![
RecordBatch::try_new(
schema.clone(),
vec![Arc::new(StringArray::from(vec!["", "", ""]))],
vec![Arc::new(StringArray::from(vec![
"lance",
"",
"lance database",
" ",
]))],
Comment on lines +405 to +416

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Map the relevant test file and inspect surrounding tests for index coverage.
git ls-files rust/lance/src/dataset/tests/dataset_index.rs
wc -l rust/lance/src/dataset/tests/dataset_index.rs
sed -n '1,520p' rust/lance/src/dataset/tests/dataset_index.rs | cat -n | sed -n '1,520p'

# Search for all token/index-related assertions in this file.
rg -n "num_docs|empty|whitespace|token|search|index" rust/lance/src/dataset/tests/dataset_index.rs

Repository: lance-format/lance

Length of output: 43487


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Check whether any other test asserts FTS statistics for zero-token / empty-index behavior.
rg -n "num_docs|empty index|empty_table|empty strings|tokenize to nothing|no search hits|full_text_search" rust/lance/src/dataset/tests/dataset_index.rs

# Inspect the nearby FTS tests around the empty-index cases.
sed -n '1210,1295p' rust/lance/src/dataset/tests/dataset_index.rs | cat -n
sed -n '244,280p' rust/lance/src/dataset/tests/dataset_index.rs | cat -n

Repository: lance-format/lance

Length of output: 9394


Add an all-empty regression case.
The empty-table search path is already covered, but this fixture still doesn’t assert the num_docs == 0 BM25/statistics path. Keep a dedicated case for an index built only from empty/whitespace strings if that behavior is part of the regression target.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@rust/lance/src/dataset/tests/dataset_index.rs` around lines 405 - 416, Add a
dedicated regression case alongside the existing mixed-token fixture that builds
an index exclusively from empty and whitespace-only strings, then assert the
resulting BM25/statistics metadata reports num_docs == 0. Keep the existing
mixed-document coverage unchanged and target the dataset index test’s
index-building and statistics assertions.

)
.unwrap(),
Comment on lines 408 to 418

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Use record_batch!() for the modified fixture.

The changed test still manually constructs the schema, RecordBatch, and StringArray; repository guidance requires record_batch!() instead of this boilerplate.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@rust/lance/src/dataset/tests/dataset_index.rs` around lines 408 - 418,
Replace the manual schema, RecordBatch, and StringArray construction in the
modified test fixture with the repository’s record_batch!() helper, preserving
the existing string values and resulting batch data.

Source: Coding guidelines

];
Expand All @@ -420,13 +428,33 @@ async fn test_create_fts_index_with_empty_strings() {
.await
.unwrap();

// Only the two token-producing rows are indexed; the empty and
// whitespace-only rows are skipped so they do not inflate BM25 statistics.
let stats: serde_json::Value =
serde_json::from_str(&dataset.index_statistics("text_idx").await.unwrap()).unwrap();
assert_eq!(
stats["indices"][0]["num_docs"], 2,
"empty documents must not count: {stats}"
);

// The token-producing rows remain searchable.
let batch = dataset
.scan()
.full_text_search(FullTextSearchQuery::new("lance".to_owned()))
.unwrap()
.try_into_batch()
.await
.unwrap();
assert_eq!(batch.num_rows(), 2);

// A term present in no document returns nothing.
let batch = dataset
.scan()
.full_text_search(FullTextSearchQuery::new("missing".to_owned()))
.unwrap()
.try_into_batch()
.await
.unwrap();
assert_eq!(batch.num_rows(), 0);
}

Expand Down
Loading