Repository navigation
Add analyze(), the en_stem_keep analyzer, and multi-phrase queries - #11
Merged
Merged
Conversation
Three additions so a host can query stemmed fields with typed terms
instead of the query parser, and keep one field for exact, stemmed,
prefix and phrase search.
- `Index.analyze(_:with:)` runs a field's analyzer on a string and
returns its tokens (text, position, byte offsets), via a new
`tantivy_index_analyze` export. Resolves the index's own tokenizer
manager, so registered analyzers work as well as the built-ins.
- `Analyzer.englishKeepingSurface` (`en_stem_keep`): tantivy's
`en_stem` chain with the stemmer swapped for `KeepStemFilter`, which
emits each word and then its stem at the same position when the two
differ. One field then answers `.term("designers")`, `.term("design")`,
phrases in either form, and prefixes. The query parser requires both
same-position tokens, so `.parsed` on such a field is an exact-surface
match; the docs say to use typed queries built from `analyze`.
- `Query.multiPhrase`: a phrase where each position accepts any of
several terms (Lucene's MultiPhraseQuery), including a
`multiPhrase(_:tokens:)` form that groups `analyze` output by position.
Built on tantivy's RegexPhraseQuery with each position an escaped
literal alternation; the BM25 weight is rescaled so a position counts
as its most frequent alternative, so adding a synonym never raises a
score and one alternative per position scores exactly as `.phrase`.
Single-position queries go through a max-scored term disjunction,
wrapped so tantivy's block-WAND path does not sum the alternatives.
`rust-stemmers` and `regex-syntax` become direct dependencies, pinned to
the exact versions tantivy 0.26.1 already uses so stems and escaping stay
identical to the engine's own.
Two public enums gain a case (`Analyzer`, `Query`), so this is a minor
version bump for callers with exhaustive switches.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What changed
Three additions so a host can query stemmed fields with typed terms instead of the query parser, and collapse its
_rawmirror fields into one field that answers exact, stemmed, prefix and phrase queries.Index.analyze(_:with:)— Release ARuns a field's analyzer on a string and returns its
Tokens (text, position, UTF-8 byte offsets). Newtantivy_index_analyzeexport; reads the index's own tokenizer manager, so registered analyzers (lowercase,en_stem_keep) resolve as well as the built-ins.Tokenhas a public init so callers can add synonyms to an analyzed stream.Analyzer.englishKeepingSurface(en_stem_keep) — Release BTantivy's
en_stemchain (SimpleTokenizer→RemoveLong(40)→LowerCaser) with the stemmer swapped for a newKeepStemFilter(rust/src/keep_stem.rs): each word is emitted as itself, then as its stem at the same position when the stem differs (designers→designers+design;posteronce). Later positions are not shifted, so phrases work across the doubled position.Query.multiPhrase— synonym-aware phrasesA phrase where each position accepts any one of several terms (Lucene's
MultiPhraseQuery):.multiPhrase("f", [["graphic"], ["designers", "design"]])..multiPhrase("f", tokens:)takesanalyzeoutput directly, grouping same-position tokens into alternatives;PhrasePosition(offset:terms:)for explicit gaps.Things a reviewer should know
.parsedon anen_stem_keepfield is an exact-surface match. Tantivy's parser puts same-position tokens into onePhraseQueryrequiring all of them, sodesignersonly matches the literal word — the stemming is silently lost. The enum doc comment says to use typed queries fromanalyze;parsedQueriesLoseStemmingpins the behaviour.multiPhraseis built onRegexPhraseQuery, with each position an escaped literal alternation (designers|design). Tantivy keeps the true multi-phrase machinery crate-private. Alternatives are anchored and literal (c++,well-known,c|cxxtested).RegexPhraseQuerytakes each position's idf from the pattern text, which is never an indexed term, so every position looked maximally rare. The weight is rescaled so each position counts as its most frequent alternative (LuceneSynonymQuerysemantics): adding a synonym never raises a score, and one alternative per position scores identically to.phrase. This depends on tantivy internals — hence the=0.26.1pin — andsingleAlternativesScoreLikePhraseis the tripwire.DisjunctionMaxQueryof term queries wrapped in a ×1BoostWeight: in tantivy 0.26.1 a top-level term disjunction collects top docs via block-WAND, which sums the alternatives regardless of the max combiner. Regression-tested with a document holding both alternatives.en_stem_keephold both forms, so a terms aggregation returnsdesignersanddesignas separate buckets. BM25 field length counts both tokens where they differ. Both noted on the enum case.rust-stemmers = "=1.2.0"andregex-syntax = "=0.8.10"become direct, pinned to tantivy's exact copies (cargo treeshows one copy of each). No new code ships.AnalyzerandQueryeach gain a case, so exhaustive switches in callers need updating → minor version bump (0.8.0). Behaviour of existing analyzers, queries and indexes is unchanged; indexes usingen_stem_keepor queries usingmulti_phraseneed this library version.Tests
392 Swift tests (36 new across
AnalyzerTests,TokenizerTests,KeepStemPhraseTests,MultiPhraseTests,QueryEncodingTests) and 15 Rust tests pass. Build note for this machine:CARGO_PROFILE_RELEASE_BUILD_OVERRIDE_STRIP=false scripts/build-host.sh(cargo's release strip breaks proc-macro dylibs on rustc 1.96/macOS 27) andswift test --manifest-cache noneafter the host lib first appears.🤖 Generated with Claude Code