Skip to content

feat: add code analyzer for FTS#7681

Open
Xuanwo wants to merge 11 commits into
mainfrom
xuanwo/fts-code-analyzer
Open

feat: add code analyzer for FTS#7681
Xuanwo wants to merge 11 commits into
mainfrom
xuanwo/fts-code-analyzer

Conversation

@Xuanwo

@Xuanwo Xuanwo commented Jul 8, 2026

Copy link
Copy Markdown
Collaborator

This PR adds a first-slice code analyzer profile for FTS so code-like text can be searched by identifiers and subwords instead of natural-language defaults.

The current text analyzer treats code poorly: camelCase/acronyms/numeric boundaries are not split as code identifiers, and stemming / stop-word removal can rewrite code tokens. The new analyzer="code" profile resolves to a persisted tokenizer configuration, adds code lexical tokenization and identifier splitting, and wires the same analyzer through indexed, flat, MemWAL, and Python paths.

The implementation also canonicalizes FTS segment details before comparing them, so existing text FTS segments that omit newly added default fields remain compatible with newly written text FTS segments.

Validation covered targeted Rust tokenizer/index/MemWAL/details tests, targeted Python FTS tests, cargo fmt --all --check, cargo clippy --all --tests --benches -- -D warnings, and Python ruff checks. uv run make lint still fails at pyright because this local environment does not provide optional tensorflow, which affects existing python/lance/arrow.py and python/tests/test_arrow.py imports outside this change.

Summary by CodeRabbit

  • New Features
    • Added code-aware full-text indexing configuration (analyzer/tokenizer profiles), including optional identifier splitting, numeric splitting, preserving original tokens, and operator indexing.
    • Improved search semantics to match position alternatives and apply stricter operator-aware behavior for flat BM25.
  • Bug Fixes
    • Now fails fast when in-memory full-text index metadata cannot be decoded.
    • Canonicalizes legacy inverted index details and infers missing tokenizer defaults for better compatibility.
  • Tests
    • Added regression coverage for code queries, repeated subwords, grouped AND/phrase matching, and invalid parameter combinations.

@github-actions github-actions Bot added A-python Python bindings A-index Vector index, linalg, tokenizer A-format On-disk format: protos and format spec docs labels Jul 8, 2026
@github-actions

github-actions Bot commented Jul 8, 2026

Copy link
Copy Markdown
Contributor

Important

This PR touches the Lance format specification.

Substantive changes to the format specification — the .proto definitions
and the spec docs under docs/src/format/ — require a PMC vote before merge.
Minor edits such as typo fixes, wording, or formatting are excluded; use your
judgment.

If this is a meaningful format change:

  • Start a vote following the Lance community voting process.
    Format specification modifications need 3 binding +1 votes (excluding the
    proposer), held on GitHub Discussions, with a minimum voting period of 1 week.
  • Once the vote passes, link the completed vote in this PR. It should not be
    merged until the vote is linked.

@github-actions github-actions Bot added the enhancement New feature or request label Jul 8, 2026

@BubbleCal BubbleCal left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Two concise inline comments on the correctness issues found.

})
}

fn canonicalize_inverted_index_details(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This roundtrip still preserves a missing lance_tokenizer instead of canonicalizing it to "text". A text segment with None can be compared against a later segment with Some("text"), causing multi-segment FTS to fail as inconsistent.

@@ -1895,12 +1858,26 @@

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This grouped-query path unions posting lists without preserving positions. Code analyzer phrase queries can hit it when a full identifier and subword share a position, and WAND later expects positions to exist.

@Xuanwo Xuanwo marked this pull request as ready for review July 8, 2026 15:52
@BubbleCal

Copy link
Copy Markdown
Contributor

I'd think we should disable split_identifiers by default, GetUserEmail can match GetUserName seems unexpected in most cases, how do you think?

@Xuanwo

Xuanwo commented Jul 8, 2026

Copy link
Copy Markdown
Collaborator Author

I'd think we should disable split_identifiers by default, GetUserEmail can match GetUserName seems unexpected in most cases, how do you think?

Makes sense to me

@coderabbitai

coderabbitai Bot commented Jul 9, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

This PR adds code-aware full-text indexing and search support across schema, tokenizer construction, query execution, mem-WAL search, and legacy segment-detail handling.

Changes

Code Analyzer FTS Pipeline

Layer / File(s) Summary
Code tokenizer and word delimiter filter primitives
rust/lance-tokenizer/src/code_tokenizer.rs, rust/lance-tokenizer/src/word_delimiter_filter.rs, rust/lance-tokenizer/src/lib.rs
Adds CodeLexTokenizer for identifier/operator tokenization and WordDelimiterFilter for subword splitting, and exports both from the crate.
Proto schema and InvertedIndexParams resolution
protos/index_old.proto, rust/lance-index/src/scalar/inverted/tokenizer.rs
Adds analyzer, lance_tokenizer, and code-flag fields to InvertedIndexDetails; rewrites InvertedIndexParams deserialization, validation, and tokenizer construction to resolve analyzer profiles and build the new tokenizers.
Tokenizer parameter tests
rust/lance-index/src/scalar/inverted/tokenizer.rs
Adds unit tests for JSON null preservation, code-profile defaults, base-tokenizer aliasing, invalid combinations, code identifier splitting, and inverted-details round-tripping.
Python create_index kwargs wiring for FTS analyzer
python/src/dataset.rs
Validates FTS kwargs, parses analyzer and base_tokenizer compatibility, forwards stop-word and code-flag options into InvertedIndexParams, and adjusts the dataset import layout.
Python code analyzer end-to-end tests
python/python/tests/test_scalar_index.py
Adds Python tests for code analyzer defaults, identifier splitting, complex code constructs, and operator-mode differences.
Position-aware posting union and grouped AND/phrase query weighting
rust/lance-index/src/scalar/inverted/index.rs
Adds positions-aware posting-list union helpers, changes grouped candidate deduplication and weighting, and short-circuits empty grouped unions for phrase and AND semantics.
Flat BM25 operator-aware scoring
rust/lance-index/src/scalar/inverted/index.rs, rust/lance/src/io/exec/fts.rs
Adds operator-aware flat BM25 scoring, query-position grouping, repeated-token counting, a new public search helper, and the exec-site call update.
Mem-WAL analysis-based grouped AND search
rust/lance/src/dataset/mem_wal/index.rs, rust/lance/src/dataset/mem_wal/index/fts.rs
Switches mem-WAL FTS search to analyzed tokens with positions, adds grouped AND intersection logic, and turns metadata decode failures into errors.
Segment detail canonicalization for legacy compatibility
rust/lance/src/index/scalar/inverted.rs
Adds canonicalization of InvertedIndexDetails before segment comparison, inferring missing lance_tokenizer values and updating the associated tests.

Estimated code review effort: 4 (Complex) | ~75 minutes

Sequence Diagram(s)

sequenceDiagram
  participant PythonDataset
  participant InvertedIndexParams
  participant CodeLexTokenizer
  participant WordDelimiterFilter
  PythonDataset->>InvertedIndexParams: parse analyzer, base_tokenizer, and code flags
  InvertedIndexParams->>CodeLexTokenizer: build code tokenizer for indexed text
  InvertedIndexParams->>WordDelimiterFilter: wrap identifier splitting when enabled
  InvertedIndexParams-->>PythonDataset: validated index parameters
Loading
sequenceDiagram
  participant MemWALFTS
  participant Tokens
  participant GroupedSearch
  participant PhraseSearch
  MemWALFTS->>Tokens: analyze query text with positions
  Tokens->>GroupedSearch: grouped AND token positions
  Tokens->>PhraseSearch: grouped phrase alternatives
  GroupedSearch-->>MemWALFTS: intersected row matches
  PhraseSearch-->>MemWALTS: phrase matches by group
Loading
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: adding a code analyzer for full-text search.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch xuanwo/fts-code-analyzer

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 4

🧹 Nitpick comments (6)
rust/lance-tokenizer/src/word_delimiter_filter.rs (2)

8-61: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Document the public filter API with an example.

WordDelimiterFilter is publicly re-exported, and the wrapper/stream types are public; add example docs that show how it composes with [CodeLexTokenizer] / [TextAnalyzer].

As per coding guidelines, "All public APIs must have documentation with examples, and documentation should link to relevant structs and methods."

🤖 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-tokenizer/src/word_delimiter_filter.rs` around lines 8 - 61, Add
documentation comments with a runnable example for the public
WordDelimiterFilter API, since WordDelimiterFilter, WordDelimiterFilterWrapper,
and WordDelimiterTokenStream are publicly exposed. Update the docs on
WordDelimiterFilter and its constructor/transform path to show how it composes
with CodeLexTokenizer and TextAnalyzer, and include proper intra-doc links to
the relevant types and methods so the public API is fully documented.

Source: Coding guidelines


200-203: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Avoid expect in library token-stream code.

refill() establishes the invariant, but this is still a library panic path. Prefer a non-panicking invariant check.

Suggested refactor
-        self.current = self
-            .pending
-            .pop_front()
-            .expect("pending token should be available after refill");
+        let Some(token) = self.pending.pop_front() else {
+            debug_assert!(false, "pending token should be available after refill");
+            return false;
+        };
+        self.current = token;

As per coding guidelines, "Never use .unwrap(), .expect(), panic!(), or assert!() in library code for fallible operations."

🤖 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-tokenizer/src/word_delimiter_filter.rs` around lines 200 - 203,
Replace the `expect` in `WordDelimiterFilter::refill`/token advancement with a
non-panicking invariant check so library code never panics on fallible state.
Use the existing `pending` queue and `self.current` assignment path to handle
the empty case gracefully, returning an appropriate error/option-like flow
instead of `expect("pending token should be available after refill")`. Keep the
fix localized to `word_delimiter_filter.rs` and preserve the refill invariant
without introducing `panic!`, `assert!`, or `unwrap`-style behavior.

Source: Coding guidelines

rust/lance-index/src/scalar/inverted/tokenizer.rs (2)

510-545: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Remove expect from public builder helpers.

These calls are currently invariant-based, but public library code should avoid panic paths. A small private apply_code_defaults() helper can set the fields directly.

Suggested direction
+    fn apply_code_defaults(&mut self) {
+        self.analyzer = "code".to_string();
+        self.base_tokenizer = "code".to_string();
+        self.split_identifiers = false;
+        self.split_on_numerics = true;
+        self.preserve_original = true;
+        self.lower_case = true;
+        self.ascii_folding = true;
+        self.stem = false;
+        self.remove_stop_words = false;
+        self.index_operators = false;
+    }
+
     /// Create parameters for the code analyzer profile.
     pub fn code() -> Self {
-        Self::new("code".to_string(), Language::English)
-            .analyzer("code")
-            .expect("code analyzer should be valid")
+        let mut params = Self::new("code".to_string(), Language::English);
+        params.apply_code_defaults();
+        params
     }

As per coding guidelines, "Never use .unwrap(), .expect(), panic!(), or assert!() in library code for fallible operations."

🤖 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-index/src/scalar/inverted/tokenizer.rs` around lines 510 - 545,
Remove the panic-based `expect` calls from the public builder helpers in
`TokenizerOptions::code()` and `TokenizerOptions::base_tokenizer()`. Replace the
`analyzer("code")` round-trip with a private helper such as
`apply_code_defaults()` that sets the code profile fields directly, and call
that helper from both places so these library APIs remain fallible-free.

Source: Coding guidelines


510-619: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add examples for the new analyzer builder API.

The new public code(), analyzer(...), and code-flag setters should document valid combinations and include examples for default code analysis and identifier splitting.

As per coding guidelines, "All public APIs must have documentation with examples, and documentation should link to relevant structs and methods."

🤖 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-index/src/scalar/inverted/tokenizer.rs` around lines 510 - 619,
The public builder API in TokenizerConfig is missing the required docs/examples
for the new analyzer flow. Add documentation examples to the relevant public
methods in tokenizer.rs, especially code(), analyzer(...), and the code-related
setters like base_tokenizer(), split_identifiers(), split_on_numerics(),
preserve_original(), and index_operators(), showing valid combinations for
default code analysis and identifier splitting. Make sure the doc comments link
to the TokenizerConfig type and the related builder methods so the public API
docs are complete.

Source: Coding guidelines

rust/lance-tokenizer/src/code_tokenizer.rs (1)

8-28: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add example docs for the exported tokenizer API.

CodeLexTokenizer is publicly re-exported, and CodeLexTokenStream is public, but the public docs do not include an example or cross-links.

Suggested doc shape
 /// Tokenizer for code-like text.
 ///
 /// Identifiers are Unicode alphanumeric characters plus `_`. Other characters
 /// are lexical boundaries. When operator indexing is enabled, contiguous
 /// operator characters are emitted as tokens too.
+///
+/// # Examples
+///
+/// ```
+/// use lance_tokenizer::{CodeLexTokenizer, TextAnalyzer, TokenStream};
+///
+/// let mut analyzer = TextAnalyzer::builder(CodeLexTokenizer::new(true)).build();
+/// let mut stream = analyzer.token_stream("a::b");
+///
+/// assert!(stream.advance());
+/// assert_eq!(stream.token().text, "a");
+/// ```
 #[derive(Clone, Default)]
 pub struct CodeLexTokenizer {
@@
-pub struct CodeLexTokenStream<'a> {
+/// Token stream produced by [`CodeLexTokenizer`].
+pub struct CodeLexTokenStream<'a> {

As per coding guidelines, "All public APIs must have documentation with examples, and documentation should link to relevant structs and methods."

🤖 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-tokenizer/src/code_tokenizer.rs` around lines 8 - 28, Add
documentation for the exported tokenizer API by expanding the doc comment on
CodeLexTokenizer with a runnable usage example and relevant cross-links. Update
the public docs so CodeLexTokenizer references TextAnalyzer and TokenStream, and
add a short summary doc comment on CodeLexTokenStream that links back to
CodeLexTokenizer. Keep the example aligned with the existing constructor and
token stream flow so the public API is fully documented.

Source: Coding guidelines

python/python/tests/test_scalar_index.py (1)

995-1052: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Parameterize the repeated search cases.

These assertions differ only by column, query, and expected_path; converting them to @pytest.mark.parametrize will give each analyzer scenario an independent failure. While doing that, consider asserting the expected result set instead of only membership so an AND-to-OR regression cannot still pass. As per coding guidelines, “Use @pytest.mark.parametrize for Python tests when cases differ only by inputs.”

🤖 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 `@python/python/tests/test_scalar_index.py` around lines 995 - 1052, The
repeated search assertions in the test around assert_search should be converted
into a single `@pytest.mark.parametrize-driven` test so each
column/query/expected_path case runs independently. Refactor the helper or
inline the scanner call under a parametrized test using the existing
assert_search logic, and strengthen the check to verify the full expected path
set rather than only membership so an AND-to-OR regression cannot slip through.

Source: Coding guidelines

🤖 Prompt for all review comments with 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.

Inline comments:
In `@protos/index_old.proto`:
- Around line 59-66: The code analyzer flags in the proto need to be
presence-aware because bare proto3 bools treat omitted legacy values as false,
which can override the defaults used by InvertedIndexParams::code(). Update
split_on_numerics and preserve_original in the InvertedIndexDetails schema to
optional bool, then adjust the Rust conversion logic for
InvertedIndexParams::code() so None maps back to the code analyzer defaults
instead of false.

In `@python/src/dataset.rs`:
- Around line 2451-2464: The current analyzer/base_tokenizer validation in the
dataset setup still allows code-only flags to be enabled when the effective
analyzer is not code. Update the validation around the analyzer resolution path
and the setters for split_identifiers, split_on_numerics, preserve_original, and
index_operators so they reject any of these options unless analyzer resolves to
code, matching the upstream contract and the existing PyValueError style used in
this block. Use the existing analyzer/base_tokenizer checks in dataset
configuration to centralize the guard before applying those code analyzer flags.

In `@rust/lance-index/src/scalar/inverted/index.rs`:
- Around line 6030-6041: The new public API
flat_bm25_search_stream_with_metrics_and_operator has documentation but no usage
example, so add a minimal doc example to its Rustdoc showing how to call it with
a stream, tokenizer, scorer, and Operator, and link to the relevant
types/methods used. If this function is not meant to be public, reduce its
visibility instead of leaving an undocumented public entrypoint.
- Around line 1960-1963: The union-docs lookup in the grouped-query path is
using an expect that can panic in library code if the invariant is broken.
Update the code in the inverted index query flow around docs_for_union to return
a proper Result error instead of calling expect, and propagate it with ? through
the surrounding function that builds the grouped query terms. Use the existing
query-building symbols in index.rs so the failure becomes a fallible error path
rather than a process panic.

---

Nitpick comments:
In `@python/python/tests/test_scalar_index.py`:
- Around line 995-1052: The repeated search assertions in the test around
assert_search should be converted into a single `@pytest.mark.parametrize-driven`
test so each column/query/expected_path case runs independently. Refactor the
helper or inline the scanner call under a parametrized test using the existing
assert_search logic, and strengthen the check to verify the full expected path
set rather than only membership so an AND-to-OR regression cannot slip through.

In `@rust/lance-index/src/scalar/inverted/tokenizer.rs`:
- Around line 510-545: Remove the panic-based `expect` calls from the public
builder helpers in `TokenizerOptions::code()` and
`TokenizerOptions::base_tokenizer()`. Replace the `analyzer("code")` round-trip
with a private helper such as `apply_code_defaults()` that sets the code profile
fields directly, and call that helper from both places so these library APIs
remain fallible-free.
- Around line 510-619: The public builder API in TokenizerConfig is missing the
required docs/examples for the new analyzer flow. Add documentation examples to
the relevant public methods in tokenizer.rs, especially code(), analyzer(...),
and the code-related setters like base_tokenizer(), split_identifiers(),
split_on_numerics(), preserve_original(), and index_operators(), showing valid
combinations for default code analysis and identifier splitting. Make sure the
doc comments link to the TokenizerConfig type and the related builder methods so
the public API docs are complete.

In `@rust/lance-tokenizer/src/code_tokenizer.rs`:
- Around line 8-28: Add documentation for the exported tokenizer API by
expanding the doc comment on CodeLexTokenizer with a runnable usage example and
relevant cross-links. Update the public docs so CodeLexTokenizer references
TextAnalyzer and TokenStream, and add a short summary doc comment on
CodeLexTokenStream that links back to CodeLexTokenizer. Keep the example aligned
with the existing constructor and token stream flow so the public API is fully
documented.

In `@rust/lance-tokenizer/src/word_delimiter_filter.rs`:
- Around line 8-61: Add documentation comments with a runnable example for the
public WordDelimiterFilter API, since WordDelimiterFilter,
WordDelimiterFilterWrapper, and WordDelimiterTokenStream are publicly exposed.
Update the docs on WordDelimiterFilter and its constructor/transform path to
show how it composes with CodeLexTokenizer and TextAnalyzer, and include proper
intra-doc links to the relevant types and methods so the public API is fully
documented.
- Around line 200-203: Replace the `expect` in
`WordDelimiterFilter::refill`/token advancement with a non-panicking invariant
check so library code never panics on fallible state. Use the existing `pending`
queue and `self.current` assignment path to handle the empty case gracefully,
returning an appropriate error/option-like flow instead of `expect("pending
token should be available after refill")`. Keep the fix localized to
`word_delimiter_filter.rs` and preserve the refill invariant without introducing
`panic!`, `assert!`, or `unwrap`-style behavior.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 7095279a-fa1f-4d0e-be47-9cd5f7bb0d7a

📥 Commits

Reviewing files that changed from the base of the PR and between d581bb9 and a6dd5ef.

📒 Files selected for processing (12)
  • protos/index_old.proto
  • python/python/tests/test_scalar_index.py
  • python/src/dataset.rs
  • rust/lance-index/src/scalar/inverted/index.rs
  • rust/lance-index/src/scalar/inverted/tokenizer.rs
  • rust/lance-tokenizer/src/code_tokenizer.rs
  • rust/lance-tokenizer/src/lib.rs
  • rust/lance-tokenizer/src/word_delimiter_filter.rs
  • rust/lance/src/dataset/mem_wal/index.rs
  • rust/lance/src/dataset/mem_wal/index/fts.rs
  • rust/lance/src/index/scalar/inverted.rs
  • rust/lance/src/io/exec/fts.rs

Comment thread protos/index_old.proto Outdated
Comment thread python/src/dataset.rs
Comment on lines +2451 to +2464
match (analyzer.as_deref(), base_tokenizer.as_deref()) {
(Some("text"), Some("code")) => {
return Err(PyValueError::new_err(
"base_tokenizer='code' requires analyzer='code'",
));
}
(Some("code"), Some(base_tokenizer)) if base_tokenizer != "code" => {
return Err(PyValueError::new_err(format!(
"analyzer='code' requires base_tokenizer='code', got '{}'",
base_tokenizer
)));
}
_ => {}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Reject code-only flags unless the effective analyzer is code.

The new validation handles analyzer/base_tokenizer conflicts, but split_identifiers, split_on_numerics, preserve_original, and index_operators can still be set while using the default/text analyzer. The upstream resolver rejects this combination as "code analyzer flags require analyzer='code'", so mirror that contract here before applying these setters.

Suggested fix
                     match (analyzer.as_deref(), base_tokenizer.as_deref()) {
                         (Some("text"), Some("code")) => {
                             return Err(PyValueError::new_err(
                                 "base_tokenizer='code' requires analyzer='code'",
                             ));
@@
                         }
                         _ => {}
                     }
+
+                    let uses_code_analyzer =
+                        analyzer.as_deref() == Some("code")
+                            || base_tokenizer.as_deref() == Some("code");
+                    if !uses_code_analyzer {
+                        for flag in [
+                            "split_identifiers",
+                            "split_on_numerics",
+                            "preserve_original",
+                            "index_operators",
+                        ] {
+                            if kwargs
+                                .get_item(flag)?
+                                .map(|value| value.extract::<bool>())
+                                .transpose()?
+                                .unwrap_or(false)
+                            {
+                                return Err(PyValueError::new_err(
+                                    "code analyzer flags require analyzer='code'",
+                                ));
+                            }
+                        }
+                    }
 
                     if let Some(analyzer) = analyzer {
                         params = params
                             .analyzer(&analyzer)

Also applies to: 2514-2525

🤖 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 `@python/src/dataset.rs` around lines 2451 - 2464, The current
analyzer/base_tokenizer validation in the dataset setup still allows code-only
flags to be enabled when the effective analyzer is not code. Update the
validation around the analyzer resolution path and the setters for
split_identifiers, split_on_numerics, preserve_original, and index_operators so
they reject any of these options unless analyzer resolves to code, matching the
upstream contract and the existing PyValueError style used in this block. Use
the existing analyzer/base_tokenizer checks in dataset configuration to
centralize the guard before applying those code analyzer flags.

Comment thread rust/lance-index/src/scalar/inverted/index.rs Outdated
Comment thread rust/lance-index/src/scalar/inverted/index.rs
@codecov

codecov Bot commented Jul 9, 2026

Copy link
Copy Markdown

Comment thread protos/index_old.proto
// e.g. "text" for a plain string or "json" for JSON content. The extracted
// text is then passed to base_tokenizer. Absent means text / auto-inferred
// legacy behavior.
optional string lance_tokenizer = 13;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

can we just reuse lance_tokenizer?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

My current understanding:

  • lance_tokenizer: document-level extractor, text, json
  • base_tokenizer: lexical tokenizer, like simple, icu
  • analyzer: the high-level profile

For example, if we support code in json, we will have lance_tokenizer=json + analyzer=code. Do you think this makes sense?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

should we use token.position

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick comments (2)
rust/lance/src/dataset/mem_wal/index/fts.rs (2)

2490-2528: 🩺 Stability & Availability | 🔵 Trivial | 💤 Low value

Silent guards against conditions that cannot occur on this path.

Grouped phrase search is only reached when self.params.has_positions() is true (guarded in search_phrase_tokens, Line 1531), and any chunk with batch_position < snap.visible_count is by construction backed by a visible batch. So both let Some(meta) = snap.batch_for(...) else { continue; } and let Some(positions) = &chunk.positions else { continue; } silently skip states that should be impossible here, which can mask a real regression by dropping a doc instead of surfacing it. Prefer a debug_assert!/explicit error, or a comment stating when the branch legitimately triggers.

As per coding guidelines: "Do not silently guard against impossible conditions; use debug_assert!, return an explicit error, or remove the check." and "Comment fallback or guard code paths with when they trigger and why they exist."

🤖 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/mem_wal/index/fts.rs` around lines 2490 - 2528, The
`tail_phrase_group_docs` path is silently continuing on states that should be
impossible after `search_phrase_tokens` and the visible-batch check, which can
hide regressions. Update the `snap.batch_for(...)` and `chunk.positions`
branches to use a `debug_assert!`, return an explicit error, or add a clear
comment explaining the legitimate fallback condition, rather than silently
skipping the document.

Source: Coding guidelines


1879-1889: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

analyze_for_search truncates t.position with an unchecked as u32, unlike the insert path.

The writer path now validates the usize → u32 position conversion via checked_token_position (Line 1180), but the search path silently truncates with positions.push(t.position as u32). In practice a search string can't realistically exceed u32::MAX tokens, so this is a consistency concern rather than a live bug. Consider reusing checked_token_position here (or documenting why the search side is exempt) so both position pipelines behave identically.

🤖 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/mem_wal/index/fts.rs` around lines 1879 - 1889,
analyze_for_search currently converts token positions with an unchecked
usize-to-u32 cast, unlike the insert path. Update the search token collection in
analyze_for_search to use the same checked conversion helper as the writer path
(checked_token_position) or otherwise handle overflow consistently, so token
position handling matches across both pipelines. Reference analyze_for_search,
token_stream_for_search, and checked_token_position while making the change.
🤖 Prompt for all review comments with 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.

Nitpick comments:
In `@rust/lance/src/dataset/mem_wal/index/fts.rs`:
- Around line 2490-2528: The `tail_phrase_group_docs` path is silently
continuing on states that should be impossible after `search_phrase_tokens` and
the visible-batch check, which can hide regressions. Update the
`snap.batch_for(...)` and `chunk.positions` branches to use a `debug_assert!`,
return an explicit error, or add a clear comment explaining the legitimate
fallback condition, rather than silently skipping the document.
- Around line 1879-1889: analyze_for_search currently converts token positions
with an unchecked usize-to-u32 cast, unlike the insert path. Update the search
token collection in analyze_for_search to use the same checked conversion helper
as the writer path (checked_token_position) or otherwise handle overflow
consistently, so token position handling matches across both pipelines.
Reference analyze_for_search, token_stream_for_search, and
checked_token_position while making the change.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: ab0ee6a2-1de4-49bb-bcc4-34d7cc765e5d

📥 Commits

Reviewing files that changed from the base of the PR and between 2fbf49b and 6374862.

📒 Files selected for processing (1)
  • rust/lance/src/dataset/mem_wal/index/fts.rs

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

A-format On-disk format: protos and format spec docs A-index Vector index, linalg, tokenizer A-python Python bindings enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants