search

package
v0.16.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 5, 2026 License: MIT Imports: 14 Imported by: 0

Documentation

Index

Constants

View Source
const (
	// DefaultBM25K1 controls term-frequency saturation: how quickly extra
	// occurrences of a term stop adding to the score.
	DefaultBM25K1 = 1.2
	// DefaultBM25B controls document-length normalization, from 0 (off) to 1
	// (full): how strongly a long document is penalized for diluting a term.
	DefaultBM25B = 0.75
	// DefaultGramWeight is the production ClassWeighted.GramWeight: how much
	// a match on the auxiliary intra-word gram channel counts relative to a
	// whole-word match. 0.2 sits in the middle of the plateau the relevance
	// harness measured for #888 — every weight in [0.1, 0.3] beat the pinned
	// Lucene baselines on all corpora, with the extremes offering no durable
	// advantage over this midpoint.
	DefaultGramWeight = 0.2
)

Default BM25 parameters, matching the values most search engines ship with.

View Source
const MaxTermExpansions = 50

MaxTermExpansions caps how many dictionary terms a single word-channel query term expands to under prefix/fuzzy matching, so a hot prefix or a small fuzziness over a large dictionary cannot blow up a query. It mirrors Lucene's default multi-term rewrite cap; the exact term is always kept, then the scan fills the remaining budget in dictionary id order (deterministic).

Variables

This section is empty.

Functions

func BM25Score added in v0.14.0

func BM25Score(tf, avgLen float64, df, n, docLen int, k1, b float64) float64

BM25Score is the Okapi BM25 ranking kernel shared by every BM25 surface in the codebase: the full-text Scorer above (integer term frequencies via BM25.Score) and the graph edge-weighting path in core/graphcache (float term frequencies, since additive decaying edge weights are routinely < 1). Keeping the formula in exactly one place guarantees the two ranking surfaces stay numerically identical.

tf is the term frequency (the live edge weight for the graph path); avgLen is the mean document length; df, n, and docLen are the document frequency, corpus size, and this document's length. k1 tunes TF saturation (a non-positive value falls back to DefaultBM25K1) and b tunes length normalization (clamped to [0, 1]). It returns 0 whenever the term matches nothing useful (df, n, or tf non-positive), so empty or unmatched corpora contribute no weight.

Types

type Analyzer

type Analyzer interface {
	Analyze(text string) []Token
}

Analyzer converts raw text into the sequence of index terms used by both indexing and querying. Running the same Analyzer on documents and on queries is what keeps the two sides symmetric. Implementations in this package are stateless and safe for concurrent use by multiple goroutines.

func NewAnalyzer

func NewAnalyzer(normalizers []Normalizer, tokenizer Tokenizer, filters []TokenFilter) Analyzer

NewAnalyzer builds an Analyzer from an ordered list of normalizers (each applied in turn; pass nil or an empty slice to skip normalization), a tokenizer, and an ordered list of token filters (each applied in turn; pass nil or an empty slice to skip filtering). The tokenizer is required: NewAnalyzer panics on a nil tokenizer rather than substituting a default, so the analysis pipeline is always exactly what the caller spelled out. Pass UnicodeTokenizer{} explicitly for the language-independent default.

func NewNGramAnalyzer

func NewNGramAnalyzer(n int) Analyzer

NewNGramAnalyzer returns the batteries-included analyzer for substring and no-whitespace-script search: it lowercases the text and then emits every n-rune window with an NGramTokenizer, so a query matches a document whenever they share an n-gram. Prefer it over a word-splitting analyzer (NewAnalyzer with a Unicode or Whitespace tokenizer) when words are not whitespace delimited (e.g. CJK, where the word-splitting tokenizers emit one giant token) or when infix matches matter ("arch" finding "search"). Because the same analyzer runs on both sides, queries must use the same n: a query shorter than n runes shares no n-gram with anything and therefore matches nothing. Like NGramTokenizer, the window slides over whitespace and punctuation too, so grams may straddle word boundaries; normalize the text first if that matters. n < 1 is treated as 1 (unigrams).

func NewScriptAwareAnalyzer added in v0.16.0

func NewScriptAwareAnalyzer() Analyzer

NewScriptAwareAnalyzer returns the production analyzer for mixed-script content search (#888): the full folding pipeline — width, diacritic, lowercase, punctuation, and space normalizers — feeding a ScriptAwareTokenizer at N = 2. Space-delimited scripts index whole words as primary tokens plus intra-word bigrams as auxiliary tokens, and unbounded (CJK-like) scripts index bigrams as primary tokens, so one analyzer serves word-precise ranking for languages with word boundaries and Lucene CJKAnalyzer-style recall for those without. Pair the index with a ClassWeighted scorer so the auxiliary grams keep infix and typo recall without outranking whole-word matches. No token filter is needed: the tokenizer already drops delimiters and never emits a gram across a word boundary.

type AnalyzerFunc

type AnalyzerFunc func(text string) []Token

AnalyzerFunc adapts an ordinary function to the Analyzer interface.

func (AnalyzerFunc) Analyze

func (f AnalyzerFunc) Analyze(text string) []Token

Analyze calls f(text).

type BM25

type BM25 struct {
	// K1 tunes term-frequency saturation (typical range 1.2–2.0).
	K1 float64
	// B tunes document-length normalization, from 0 (off) to 1 (full).
	B float64
}

BM25 scores a term/document pairing with the Okapi BM25 ranking function, the modern default for keyword relevance. It improves on plain TF-IDF in two ways: term frequency saturates (the 10th occurrence of a word adds far less than the 2nd, tuned by K1), and the score is normalized by document length so a long document does not rank highly just for being wordy (tuned by B). The inverse-document-frequency factor still rewards rare, discriminating terms over common ones.

The zero value is usable but disables length normalization (B == 0); prefer the parameters installed by NewInvertedIndex when scorer is nil, namely K1 = 1.2 and B = 0.75. A non-positive K1 falls back to 1.2, and B is clamped to [0, 1]. BM25 holds no mutable state and is safe for concurrent use.

func (BM25) Score

func (s BM25) Score(stats TermStats) float64

Score returns the BM25 contribution of one term occurrence set in a document. It is 0 when the term matches nothing useful (DF or N non-positive) so unmatched or empty corpora contribute no weight.

type Bool

type Bool bool

Bool adapts a boolean to Document as "true" or "false".

func (Bool) String

func (b Bool) String() string

String returns "true" or "false", making Bool a Document.

type Bytes

type Bytes []byte

Bytes adapts a raw byte slice to Document by interpreting it as text, so byte-backed UTF-8 content is indexable just like Text. Non-text bytes yield whatever terms the Analyzer extracts from that byte interpretation.

func (Bytes) String

func (b Bytes) String() string

String returns the bytes interpreted as text, making Bytes a Document.

type ClassWeighted added in v0.16.0

type ClassWeighted struct {
	// Base scores each pairing before weighting; nil means standard BM25.
	Base Scorer
	// GramWeight multiplies ClassGram matches, clamped to [0, 1].
	GramWeight float64
}

ClassWeighted layers per-class weighting over a base Scorer (#888): a match on the auxiliary gram channel (ClassGram) is multiplied by GramWeight while primary evidence (ClassWord) passes through unchanged. With the dual-class ScriptAwareTokenizer this is what makes a whole-word match dominate the redundant intra-word fragments that only exist to keep infix recall: the fragments still surface a document, but at a fraction of the weight, so "arch" ranks the document containing the word above the ones merely containing the substring.

GramWeight is clamped to [0, 1]; 0 silences the auxiliary channel entirely and 1 restores unweighted scoring. A nil Base falls back to BM25 with the standard parameters, so the zero value scores like the default scorer with the gram channel muted. ClassWeighted holds no mutable state and is safe for concurrent use.

func (ClassWeighted) Score added in v0.16.0

func (s ClassWeighted) Score(stats TermStats) float64

Score returns the base score, scaled by GramWeight for auxiliary-channel matches.

type DiacriticNormalizer

type DiacriticNormalizer struct{}

DiacriticNormalizer folds accents and other nonspacing combining marks so matching is diacritic-insensitive: it decomposes text to Unicode NFD, drops every rune in category Mn (nonspacing mark), then recomposes to NFC. This makes "café" match "cafe", "naïve" match "naive", Greek "Ελλάδα" match "Ελλαδα", and Russian "ёж" match "еж" — across any script that encodes its accents as combining marks (Latin, Greek, Cyrillic, Vietnamese, …). It does not transliterate between scripts and never alters a base letter, so letters that are atomic rather than base-plus-mark — "ß", "ø", "ł" — are left intact.

Because it removes every nonspacing mark, it also strips the optional harakat of Arabic, the niqqud of Hebrew, and the (non-optional) vowel marks of Thai, trading some precision for recall in those scripts. It preserves spacing combining marks (category Mc), so Brahmic base+matra clusters such as Devanagari "भारत" keep their vowels, and the NFC recomposition keeps Hangul syllables and other composed forms stable. It folds independently of case, so its order relative to LowercaseNormalizer does not matter, and it is a no-op for scripts written without combining marks (CJK, Hangul).

func (DiacriticNormalizer) Normalize

func (DiacriticNormalizer) Normalize(text string) string

Normalize strips nonspacing combining marks via NFD → drop Mn → NFC.

type Document

type Document interface {
	String() string
}

Document is the indexable payload handed to an Indexer: anything that can serialize itself to the text to be analyzed. It is structurally identical to fmt.Stringer, so any existing Stringer already satisfies it. Implementing String lets a richer record decide which of its fields become searchable (for example concatenating a title and a body) instead of forcing callers to pre-flatten every document into a bare string.

type Duration

type Duration time.Duration

Duration adapts a duration to Document in its canonical Go form (e.g. "1h30m0s").

func (Duration) String

func (d Duration) String() string

String returns the canonical duration text, making Duration a Document.

type EmptyTokenFilter

type EmptyTokenFilter struct{}

EmptyTokenFilter drops tokens whose term is empty or all whitespace—cheap insurance after custom normalizers or filters.

func (EmptyTokenFilter) Filter

func (EmptyTokenFilter) Filter(tokens []Token) []Token

Filter removes blank tokens.

type Float

type Float float64

Float adapts a floating-point number to Document, using the shortest decimal that round-trips back to the same float64 (e.g. 3.14, 1e+20).

func (Float) String

func (f Float) String() string

String returns the shortest round-tripping decimal, making Float a Document.

type IndexOption added in v0.16.0

type IndexOption func(*indexConfig)

IndexOption configures an InvertedIndex at construction time.

func WithPositions added in v0.16.0

func WithPositions() IndexOption

WithPositions makes the index record each term's token positions on the primary word channel (ClassWord), which SearchPhrase and the proximity boost need to tell an exact phrase from scattered term matches. It costs one ascending []uint32 per (word term, document); an index left without it ranks by the OR-union BM25 alone and stores no positions. The auxiliary gram channel never carries positions: phrase and proximity are word-level, and a CJK run's adjacency is already encoded by its overlapping bigrams on the word channel, so pos+1 verification works there too (#889).

func WithProximityWeight added in v0.16.0

func WithProximityWeight(w float64) IndexOption

WithProximityWeight overrides the multiplier the proximity boost applies to a multi-term query's OR-union scores (default proximityBoostWeight). It is the injection point the relevance harness sweeps to justify the shipped value: with it the boost's contribution is a measured number, not an unguarded constant (#910). A weight of 0 disables the boost, making ranking pure OR-union BM25 even under WithPositions; it is only meaningful together with WithPositions, since the boost needs the positional postings.

type Indexer

type Indexer[S comparable, D Document] interface {
	Index(id S, doc D)
	Delete(id S)
}

Indexer is the write side of a keyword-search engine. Index associates the document identified by id with the searchable terms extracted from doc, and Delete removes a previously indexed document. Indexing the same id again replaces its postings, so Index doubles as update. How text is analyzed and how postings are stored is left to the implementation. S is the caller's document-identifier type and D is the indexable document type.

type Int

type Int int64

Int adapts a signed integer to Document, rendered in base 10.

func (Int) String

func (i Int) String() string

String returns the base-10 text of the integer, making Int a Document.

type InvertedIndex

type InvertedIndex[S comparable, D Document] struct {
	// contains filtered or unexported fields
}

InvertedIndex is a generic, in-memory inverted index that maps each analyzed term to the documents whose text contains it. It is the default Indexer + Searcher implementation: indexed documents (any Document, via doc.String()) and raw queries pass through the same Analyzer, so index-time and query-time terms stay symmetric.

It splits the two responsibilities of relevance search. The index owns matching and the statistics behind it — per-document term frequencies and document lengths — while a pluggable Scorer (BM25 by default) turns those statistics into the ranking. Search returns hits ordered by descending score.

Terms live in per-class tables (#888): each TokenClass has its own term dictionary, postings, and corpus statistics, so a dual-channel analyzer (ScriptAwareTokenizer's whole words + auxiliary intra-word grams) never mixes the two channels' document frequencies or lengths, and a class-aware Scorer (ClassWeighted) can weight them apart. A single-channel pipeline simply leaves the other class empty at zero cost.

InvertedIndex is safe for concurrent use by multiple goroutines: reads (Search) take a shared lock and writes (Index, Delete) take an exclusive one, and text analysis runs outside the lock to keep the critical section short.

func NewInvertedIndex

func NewInvertedIndex[S comparable, D Document](analyzer Analyzer, scorer Scorer, opts ...IndexOption) *InvertedIndex[S, D]

NewInvertedIndex returns an empty index that analyzes both documents and queries with analyzer and ranks matches with scorer. Passing a nil scorer installs BM25 with the standard parameters (K1 = 1.2, B = 0.75). D is the document type it accepts; use Text to index plain strings. Pass WithPositions to enable phrase and proximity queries (SearchPhrase).

func (*InvertedIndex[S, D]) Delete

func (idx *InvertedIndex[S, D]) Delete(id S)

Delete removes id and all of its postings from the index. It is a no-op when id was never indexed. A term whose last document is removed is dropped entirely, so a delete-heavy workload never leaks empty posting sets.

func (*InvertedIndex[S, D]) DeleteMany added in v0.11.0

func (idx *InvertedIndex[S, D]) DeleteMany(ids []S)

DeleteMany removes every id in ids and its postings under a single write lock. Ids that were never indexed are skipped. It is the batch sibling of Delete used by GraphCache's batch eviction path so a namespace-wide or TTL-flush delete pays one idx.mu acquisition instead of one per document (#738).

func (*InvertedIndex[S, D]) Index

func (idx *InvertedIndex[S, D]) Index(id S, doc D)

Index makes id discoverable under every term the Analyzer extracts from doc.String(). Indexing an id that is already present replaces its previous postings, so re-indexing a mutated document leaves no stale terms behind; Index therefore doubles as update. A document with no analyzable terms simply removes id.

Index is the analyze-then-write convenience wrapper over Prepare + IndexPrepared: analysis runs outside idx.mu and only the cheap postings mutation happens under it. Callers that want to keep analysis off a hotter lock of their own (e.g. a layered cache writing under its aggregate lock) should call Prepare before taking that lock and IndexPrepared after.

func (*InvertedIndex[S, D]) IndexManyPrepared added in v0.11.0

func (idx *InvertedIndex[S, D]) IndexManyPrepared(items []PreparedItem[S])

IndexManyPrepared applies a batch of prepared documents under a SINGLE index write lock. Items are applied in slice order, so a repeated id keeps last-write semantics. It is the batch sibling of IndexPrepared for callers (e.g. batch vertex writes) that have no other per-id work to interleave between the postings updates (#739).

func (*InvertedIndex[S, D]) IndexPrepared added in v0.11.0

func (idx *InvertedIndex[S, D]) IndexPrepared(id S, prepared PreparedDocument)

IndexPrepared records id's postings from a PreparedDocument produced by Prepare, taking idx.mu only for the postings mutation. Replace semantics match Index: any previous postings for id are dropped first, and an empty prepared document simply removes id.

func (*InvertedIndex[S, D]) Prepare added in v0.11.0

func (idx *InvertedIndex[S, D]) Prepare(doc D) PreparedDocument

Prepare analyzes doc.String() WITHOUT taking the index lock and returns the tokens IndexPrepared needs. The analyzer is immutable and stateless, so Prepare is safe to call concurrently and from outside any of the caller's locks.

func (*InvertedIndex[S, D]) Search

func (idx *InvertedIndex[S, D]) Search(query string) []Result[S]

Search returns the documents that share at least one analyzed term with query, each paired with its relevance score, ordered from most to least relevant. The set of matches is the union (OR) of the query's terms, which maximizes recall for seeding a wider traversal, while the Scorer (BM25 by default) ranks that set so the strongest seeds come first. A term repeated in the query weights a document only once; the same spelling on two token classes counts once per class, since the channels carry distinct evidence. A query with no analyzable terms, or one that matches nothing, returns nil; ties in score have an unspecified order.

Search is the MatchAny case of SearchMatch; pass a MatchOptions to require every query term (MatchAll) or a minimum number of them (MatchMinShould).

func (*InvertedIndex[S, D]) SearchMatch added in v0.16.0

func (idx *InvertedIndex[S, D]) SearchMatch(query string, opts MatchOptions) []Result[S]

SearchMatch is Search with an explicit match mode: it ranks the documents that satisfy opts by descending BM25 score. Search is exactly SearchMatch(query, MatchOptions{}) (MatchAny). MatchAll and MatchMinShould narrow the OR-union to documents that cover enough of the query's word terms, raising precision on multi-word queries; the score is unchanged (still the full word+gram BM25 sum), so ranking within the narrowed set is identical to the OR-union's ranking of the same documents. A query with no analyzable terms, or one that nothing satisfies, returns nil; ties in score have an unspecified order.

func (*InvertedIndex[S, D]) SearchMatchTopK added in v0.16.0

func (idx *InvertedIndex[S, D]) SearchMatchTopK(query string, k int, accept func(id S) bool, opts MatchOptions) []Result[S]

SearchMatchTopK is the bounded-selection sibling of SearchMatch, combining its match mode with the size-k selection and accept gating of SearchTopK. SearchTopK is SearchMatchTopK(query, k, accept, MatchOptions{}). The lock order and accept contract are identical to SearchTopK. k <= 0, an unanalyzable query, or zero accepted matches return nil.

func (*InvertedIndex[S, D]) SearchPhrase added in v0.16.0

func (idx *InvertedIndex[S, D]) SearchPhrase(query string) []Result[S]

SearchPhrase returns the documents that contain the query's word-channel terms as a consecutive phrase, ranked by BM25 over those terms. It is the precision counterpart to Search's recall-oriented OR-union: "data set" matches a document with "... data set ..." but not one that merely scatters "data" and "set" far apart. Matching is the AND-intersection of the query's primary (ClassWord) postings, refined by position — the query terms must occur at consecutive positions (p, p+1, ...) in query order.

When the index was built without WithPositions the position refinement is unavailable and SearchPhrase degrades to the pure AND-intersection (every term present, order unverified). A query with no word terms, or one that matches nothing, returns nil; ties in score have an unspecified order.

func (*InvertedIndex[S, D]) SearchPhraseTopK added in v0.16.0

func (idx *InvertedIndex[S, D]) SearchPhraseTopK(query string, k int, accept func(id S) bool) []Result[S]

SearchPhraseTopK is the bounded-selection sibling of SearchPhrase, mirroring SearchTopK (#841): it phrase-matches then returns only the k highest-scoring documents that satisfy accept, without fully sorting the match set. accept gates a document before it can occupy one of the k slots (dead vertices, out-of-scope keys) and may be nil (accept everything). The lock order and accept contract are identical to SearchTopK. k <= 0, a query with no word terms, or zero accepted matches return nil.

func (*InvertedIndex[S, D]) SearchTopK added in v0.15.0

func (idx *InvertedIndex[S, D]) SearchTopK(query string, k int, accept func(id S) bool) []Result[S]

SearchTopK is the bounded-selection sibling of Search (#841): it computes the same OR-union BM25 scores but returns only the k highest-scoring documents that satisfy accept, without materialising or fully sorting the complete match set. With a bigram analyzer a short query can match most of the corpus, so Search's O(M log M) sort over all M matches dominates broad queries; SearchTopK selects with a size-k bounded heap in O(M log k) time and O(k) result memory instead.

accept gates a document BEFORE it can occupy one of the k slots, so filtered-out documents (dead vertices, out-of-scope keys) never consume the page budget — a full page of accepted hits is returned whenever one exists. accept may be nil (accept everything). To keep accept calls off the O(M) score loop it is consulted lazily, only when a candidate's score qualifies it for the heap.

LOCK ORDER: accept runs while idx.mu is held for reading. The established order is GraphCache.mu → idx.mu → (vertex-cache inner mutex): index writes already run under GraphCache.mu, and accept's typical body (a vertex-cache liveness probe) takes only the inner cache mutex, which never acquires idx.mu — so no cycle is possible. accept must not call back into this index's write methods.

Results are ordered by descending score; ties at the k-th boundary keep an unspecified subset, matching Search's unspecified tie order. k <= 0, an unanalyzable query, or zero accepted matches return nil.

SearchTopK is the MatchAny case of SearchMatchTopK.

func (*InvertedIndex[S, D]) Stats added in v0.9.0

func (idx *InvertedIndex[S, D]) Stats() (terms, docs int)

Stats returns the current number of distinct terms in the posting lists (summed across token classes) and the number of indexed documents. The counts are approximate under concurrent load (a concurrent Index or Delete may be in progress), but are accurate enough for a Prometheus gauge sampled on a regular cadence. The call does not block writers.

type JSONStringValueNormalizer added in v0.12.0

type JSONStringValueNormalizer struct{}

JSONStringValueNormalizer rewrites text that is a JSON object or array into just the text of its string values, dropping every object field name (key) and every non-string scalar (number, boolean, null). Its purpose is to keep a full-text index free of JSON structure: when a vertex stores a serialized document such as {"role":"admin","score":9,"active":true}, only the human content ("admin") becomes searchable — not the field names ("role", "score", "active") and not the numeric or boolean scalars. String values are emitted in object-key sorted order (arrays keep their order) so the output is deterministic and re-indexing is idempotent.

Text that is not a JSON object or array passes through unchanged: a fast path returns the input verbatim when the first non-space byte is neither '{' nor '[', and any string that starts that way but fails to parse as JSON is also returned verbatim (so a value that merely looks like JSON is still indexed as raw text). A JSON object/array carrying no string values normalizes to the empty string.

Unlike the other normalizers in this package, this one is meant to run on a value in isolation — the document projection of a single stored value — not on a composed document such as "key value", because a key concatenated with a JSON blob is not itself valid JSON and the fast path would return it unchanged. Place it at the projection step, not in a shared analyzer chain.

func (JSONStringValueNormalizer) Normalize added in v0.12.0

func (JSONStringValueNormalizer) Normalize(text string) string

Normalize returns the space-joined string values of a JSON object or array, or the input unchanged when it is not such a document.

type LengthFilter

type LengthFilter struct {
	Min int
	Max int
}

LengthFilter drops tokens whose length in runes is below Min or above Max. A Max of 0 means no upper bound. It removes noise such as stray single letters.

func (LengthFilter) Filter

func (f LengthFilter) Filter(tokens []Token) []Token

Filter keeps only tokens whose rune count is within [Min, Max].

type LowercaseFilter

type LowercaseFilter struct{}

LowercaseFilter folds each token to lower case. Use it when tokenization runs before case folding; if you already lowercase with a LowercaseNormalizer it is redundant.

func (LowercaseFilter) Filter

func (LowercaseFilter) Filter(tokens []Token) []Token

Filter lower-cases every term in place.

type LowercaseNormalizer

type LowercaseNormalizer struct{}

LowercaseNormalizer folds text to lower case with Unicode case mapping, so matching is case-insensitive independent of language.

func (LowercaseNormalizer) Normalize

func (LowercaseNormalizer) Normalize(text string) string

Normalize returns the lower-cased text.

type MatchMode added in v0.16.0

type MatchMode uint8

MatchMode selects how a multi-term query's word-channel terms combine to decide which documents match. It governs membership only — the Scorer still ranks whatever matches — and counts distinct query terms on the primary (word) channel, so the auxiliary intra-word grams a script-aware analyzer emits are evidence for ranking but never change how many terms a document must satisfy. For a single-channel (word-only) analyzer every match is a word match, so the modes reduce to the textbook boolean semantics.

const (
	// MatchAny keeps every document sharing at least one query term (the
	// OR-union). It is the zero value and the package default, because the
	// graph-seeding path Search feeds wants maximal recall.
	MatchAny MatchMode = iota
	// MatchAll keeps only documents containing every distinct query word term
	// (boolean AND), the precision end of the scale.
	MatchAll
	// MatchMinShould keeps documents containing at least MinShouldMatch distinct
	// query word terms — the tunable middle ground between Any and All.
	MatchMinShould
)

type MatchOptions added in v0.16.0

type MatchOptions struct {
	// Mode selects the boolean combination of the query's word terms.
	Mode MatchMode
	// MinShouldMatch is the minimum number of distinct query word terms a
	// document must contain when Mode is MatchMinShould. It is clamped to
	// [1, number of distinct query word terms]; other modes ignore it.
	MinShouldMatch int
	// Fuzziness is the maximum edit distance (0, 1, or 2) at which a word-channel
	// query term also matches dictionary terms, so a typo like "serach" still
	// finds "search". 0 (the default) disables fuzzy matching. CJK grams are
	// exempt. Expansion per query term is capped at MaxTermExpansions.
	Fuzziness int
	// PrefixTerms, when set, also matches dictionary terms that extend a
	// word-channel query term, so "lan" finds "lantern" (a prefix query). CJK
	// grams are exempt; expansion is capped at MaxTermExpansions.
	PrefixTerms bool
}

MatchOptions configures document membership for a query. The zero value is MatchAny with no term expansion, so an empty MatchOptions reproduces Search's OR-union exactly.

type NGramTokenizer

type NGramTokenizer struct {
	N int
}

NGramTokenizer slides a fixed-width window of N runes across the whole input and emits one token per position, so "search" with N=3 yields "sea", "ear", "arc", "rch". Because it works on runes (not bytes) and never relies on word boundaries, it is the language-independent way to make substring queries and scripts with no whitespace between words (e.g. CJK) matchable, where the word-splitting tokenizers would otherwise emit one giant token.

The window moves over every rune—whitespace and punctuation included—so a caller who wants grams that do not straddle word boundaries should strip or collapse those runes with a Normalizer first. Every emitted term is exactly N runes long, and an input shorter than N yields no tokens. N < 1 is treated as 1 (unigrams), so the zero value tokenizes into single characters rather than misbehaving.

func (NGramTokenizer) Tokenize

func (t NGramTokenizer) Tokenize(text string) []Token

Tokenize emits every N-rune window of text in left-to-right order.

type Normalizer

type Normalizer interface {
	Normalize(text string) string
}

Normalizer rewrites raw text before tokenization—for example folding case or collapsing whitespace. Implementations in this package are stateless and therefore safe for concurrent use by multiple goroutines.

type NormalizerFunc

type NormalizerFunc func(text string) string

NormalizerFunc adapts an ordinary function to the Normalizer interface.

func (NormalizerFunc) Normalize

func (f NormalizerFunc) Normalize(text string) string

Normalize calls f(text).

type PreparedDocument added in v0.11.0

type PreparedDocument struct {
	// contains filtered or unexported fields
}

PreparedDocument carries the analyzed tokens of a document so the expensive analysis (doc.String() + tokenization) can run outside the index write lock. Produce one with Prepare and apply it with IndexPrepared or IndexManyPrepared. The zero value is a valid empty document: applying it removes the id, matching Index of a document with no analyzable terms.

type PreparedItem added in v0.11.0

type PreparedItem[S comparable] struct {
	ID       S
	Prepared PreparedDocument
}

PreparedItem pairs an id with its PreparedDocument for IndexManyPrepared.

type PunctuationFilter

type PunctuationFilter struct{}

PunctuationFilter strips leading and trailing punctuation and symbol runes from each token and drops tokens left empty, while preserving inner marks (so "hello," becomes "hello" but "node-1" and "u.s.a" are untouched). It is the companion to WhitespaceTokenizer; UnicodeTokenizer already drops punctuation.

func (PunctuationFilter) Filter

func (PunctuationFilter) Filter(tokens []Token) []Token

Filter trims edge punctuation/symbols and removes now-empty tokens.

type PunctuationNormalizer

type PunctuationNormalizer struct{}

PunctuationNormalizer replaces every Unicode punctuation or symbol rune with an ASCII space, turning marks such as '.', ',', '。', and '、' into word boundaries independent of language. It does not merge the spaces it introduces; chain SpaceNormalizer after it to collapse the resulting runs and trim the ends. Unlike PunctuationFilter, which only trims marks from the edges of an already-formed token (so "node-1" stays intact), this splits on every mark, so "node-1" becomes "node 1".

func (PunctuationNormalizer) Normalize

func (PunctuationNormalizer) Normalize(text string) string

Normalize replaces each punctuation or symbol rune with a space.

type Result

type Result[S comparable] struct {
	ID    S
	Score float64
}

Result is a single ranked search hit: the ID of a matching document and the relevance Score the Searcher's Scorer assigned it. Higher scores are more relevant.

type Scorer

type Scorer interface {
	Score(stats TermStats) float64
}

Scorer assigns a relevance weight to a single (query term, document) pairing from the corpus statistics in stats. The Searcher sums a document's per-term scores to rank it, so a Scorer only has to express how one term contributes. Separating this from the index keeps "which documents match" (the index's job) independent of "how relevant each match is" (the Scorer's job), and lets the ranking formula be swapped without touching the postings. A higher score means more relevant; implementations should stay non-negative so that summing across terms is well behaved.

type ScorerFunc

type ScorerFunc func(stats TermStats) float64

ScorerFunc adapts an ordinary function to the Scorer interface.

func (ScorerFunc) Score

func (f ScorerFunc) Score(stats TermStats) float64

Score calls f(stats).

type ScriptAwareTokenizer added in v0.16.0

type ScriptAwareTokenizer struct {
	// N is the gram width, for unbounded-script runs and the auxiliary
	// intra-word grams alike. N < 2 is treated as 2.
	N int
}

ScriptAwareTokenizer is the production tokenizer for mixed-script content search (#888). It splits text into script runs and picks the right unit per run, instead of forcing one strategy onto every language the way a pure word or pure n-gram tokenizer does:

  • A run of a space-delimited script (Latin, Cyrillic, Greek, digits, …) emits the whole word as a primary token (ClassWord) — the unit Lucene's StandardAnalyzer indexes, and what lets a whole-word match outrank an infix fragment. Words of at least N runes additionally emit every N-rune window as auxiliary tokens (ClassGram), which is what keeps the bigram pipeline's infix recall ("arch" finding "search") and typo tolerance; pair the index with ClassWeighted so this redundant channel stays evidence, not the ranking.
  • A run of an unbounded script — one written without spaces between words: Han, Hiragana, Katakana, Hangul, Thai, Lao, Khmer, Myanmar — emits its N-rune windows as primary tokens, exactly the strategy of Lucene's CJKAnalyzer, because there the gram is the word-level unit. A run shorter than N (a lone ideograph) is emitted whole, so single- character words stay searchable.
  • Every other rune (whitespace, punctuation, symbols) delimits runs and is dropped, so no gram ever straddles a word boundary and the WhitespaceFilter step of the plain n-gram pipeline is unnecessary.

Run it after the folding normalizers (width, diacritic, lowercase, punctuation, space) — NewScriptAwareAnalyzer wires that pipeline. In particular WidthNormalizer must run first so half-width katakana joins the katakana run it belongs to. N < 2 is treated as 2 (bigrams), so the zero value is the production configuration. It holds no state and is safe for concurrent use.

func (ScriptAwareTokenizer) Tokenize added in v0.16.0

func (t ScriptAwareTokenizer) Tokenize(text string) []Token

Tokenize splits text into script runs and emits per-run tokens as described on the type.

type Searcher

type Searcher[S comparable] interface {
	Search(query string) []Result[S]
}

Searcher is the read side of a keyword-search engine. Search turns a raw query string into the matching documents, each paired with a relevance score, ordered from most to least relevant — the ranked seed candidates a caller picks before a wider graph traversal, where the score can double as a seed's initial weight. Documents that share no analyzed term with the query are omitted, and a query with no analyzable terms yields no results. Ties in score have an unspecified relative order.

type SpaceNormalizer

type SpaceNormalizer struct{}

SpaceNormalizer collapses every run of Unicode whitespace to a single ASCII space and trims the ends. It is useful in front of a WhitespaceTokenizer when the input may contain tabs, newlines, or repeated spaces.

func (SpaceNormalizer) Normalize

func (SpaceNormalizer) Normalize(text string) string

Normalize collapses internal whitespace runs to single spaces.

type StopWordFilter

type StopWordFilter struct {
	// contains filtered or unexported fields
}

StopWordFilter removes tokens that appear in a stop-word set. Matching is case-insensitive (terms and stop words are compared lower-cased), so the filter works whether or not case folding ran earlier.

func NewStopWordFilter

func NewStopWordFilter(words ...string) *StopWordFilter

NewStopWordFilter builds a StopWordFilter from words. Duplicate or mixed-case entries are fine; each is folded to lower case.

func (*StopWordFilter) Filter

func (f *StopWordFilter) Filter(tokens []Token) []Token

Filter removes stop-word tokens.

type TermStats

type TermStats struct {
	// TF is how many times the term occurs in this document (term frequency).
	TF int
	// DF is how many documents in the corpus contain the term (document
	// frequency); it drives the inverse-document-frequency weight.
	DF int
	// N is the number of documents carrying at least one token of the term's
	// class — the corpus size of the term's channel.
	N int
	// DocLen is the length of this document in tokens of the term's class.
	DocLen int
	// AvgLen is the mean DocLen across the class's N documents, used to
	// normalize DocLen so long and short documents compete fairly.
	AvgLen float64
	// Class is the matching channel the term belongs to, so a class-aware
	// Scorer (ClassWeighted) can weight primary and auxiliary evidence apart.
	Class TokenClass
}

TermStats carries the corpus statistics a Scorer needs to weight one (query term, matching document) pairing. The Searcher fills it in from the index while computing a result; a Scorer treats it as read-only input and holds no state of its own, so a single Scorer is safe for concurrent use.

Every count is scoped to the term's token class (#888): the index keeps separate postings and statistics per class, so DF, N, DocLen, and AvgLen describe only the channel the term matched on. For a single-channel pipeline that scoping is invisible — the class's corpus is the corpus.

type Text

type Text string

Text adapts a plain string to Document so raw text can be indexed without a bespoke wrapper type: idx.Index(id, Text("some words")).

func (Text) String

func (t Text) String() string

String returns the underlying text, making Text a Document.

type Time

type Time time.Time

Time adapts a timestamp to Document, formatted as RFC3339 with nanosecond precision — the same rendering Vertex timestamps use elsewhere.

func (Time) String

func (t Time) String() string

String returns the RFC3339Nano text of the timestamp, making Time a Document.

type Token

type Token struct {
	// Term is the analyzed text of this token.
	Term string
	// Class labels the matching channel the token belongs to; the zero value
	// (ClassWord) is the primary channel, so tokenizers that predate classes
	// keep their exact behavior.
	Class TokenClass
}

Token is the unit of analyzed text that flows through the package's analysis pipeline: a Tokenizer emits Tokens, TokenFilters rewrite or drop them, and an Analyzer returns the final slice that the index records and queries against.

type TokenClass added in v0.16.0

type TokenClass uint8

TokenClass separates a token's matching channel (#888). The inverted index keys terms, corpus statistics (document frequency, document length, corpus size), and postings per class, so the two channels never share BM25 statistics, and a class-aware Scorer (ClassWeighted) can weight them apart. Only the constants below are valid: indexing a token with an undefined class panics, and a query token with an undefined class simply matches nothing.

const (
	// ClassWord is the primary channel: whole words of space-delimited
	// scripts, the n-grams of unbounded (CJK-like) script runs — where the
	// gram is the word-level unit — and every token of a single-channel
	// pipeline such as NewNGramAnalyzer. It is deliberately the zero value.
	ClassWord TokenClass = iota
	// ClassGram is the auxiliary channel: intra-word n-gram fragments emitted
	// alongside the word they came from (ScriptAwareTokenizer) so infix and
	// typo-tolerant matching keeps working. Pair with ClassWeighted so this
	// redundant evidence never outranks a whole-word match.
	ClassGram
)

type TokenFilter

type TokenFilter interface {
	Filter(tokens []Token) []Token
}

TokenFilter post-processes a token stream produced by a Tokenizer—dropping, rewriting, or compacting tokens. An Analyzer applies filters in order. Implementations in this package are stateless and safe for concurrent use, but they may reuse the input slice's backing array, so a caller must not keep using the slice it passes in after the call returns.

type TokenFilterFunc

type TokenFilterFunc func(tokens []Token) []Token

TokenFilterFunc adapts an ordinary function to the TokenFilter interface.

func (TokenFilterFunc) Filter

func (f TokenFilterFunc) Filter(tokens []Token) []Token

Filter calls f(tokens).

type Tokenizer

type Tokenizer interface {
	Tokenize(text string) []Token
}

Tokenizer splits text into tokens. Implementations in this package are stateless and therefore safe for concurrent use by multiple goroutines.

type TokenizerFunc

type TokenizerFunc func(text string) []Token

TokenizerFunc adapts an ordinary function to the Tokenizer interface.

func (TokenizerFunc) Tokenize

func (f TokenizerFunc) Tokenize(text string) []Token

Tokenize calls f(text).

type Uint

type Uint uint64

Uint adapts an unsigned integer to Document, rendered in base 10.

func (Uint) String

func (u Uint) String() string

String returns the base-10 text of the integer, making Uint a Document.

type UnicodeTokenizer

type UnicodeTokenizer struct{}

UnicodeTokenizer splits text into maximal runs of letters and digits; every other rune (whitespace, punctuation, symbols) is treated as a delimiter and dropped. Tokenizing and stripping punctuation in a single pass makes it the simplest language-independent choice, so it is the default tokenizer.

func (UnicodeTokenizer) Tokenize

func (UnicodeTokenizer) Tokenize(text string) []Token

Tokenize splits text on any rune that is neither a letter nor a digit.

type WhitespaceFilter

type WhitespaceFilter struct{}

WhitespaceFilter drops every token whose term contains a Unicode whitespace rune. Its purpose is to raise the precision of n-gram search: run an NGramTokenizer over text whose words are already separated by single spaces (normalize first with PunctuationNormalizer + SpaceNormalizer) and the only grams that can contain a space are the ones straddling a word or punctuation boundary, e.g. "search" and "engine" yield the cross-boundary gram "h e" for N=3. Dropping them leaves just the intra-word grams, so a document is indexed under its real word content instead of boundary noise: BM25 document lengths reflect the words present, and a query no longer matches on a gram that merely spans two adjacent words. It is language-independent (any whitespace, any script) and a no-op for scripts written without spaces between words (e.g. CJK, Thai), so it is safe to leave in a multilingual pipeline.

func (WhitespaceFilter) Filter

func (WhitespaceFilter) Filter(tokens []Token) []Token

Filter removes tokens that contain any whitespace rune.

type WhitespaceTokenizer

type WhitespaceTokenizer struct{}

WhitespaceTokenizer splits text on runs of Unicode whitespace, leaving every other rune—including punctuation—attached to its token (so "word." stays "word."). Pair it with a PunctuationFilter when that is not what you want.

func (WhitespaceTokenizer) Tokenize

func (WhitespaceTokenizer) Tokenize(text string) []Token

Tokenize splits text on Unicode whitespace.

type WidthNormalizer

type WidthNormalizer struct{}

WidthNormalizer folds full-width and half-width characters to their normal width with Unicode width folding, so the same word written at either width indexes and queries identically — a frequent need for CJK input. Full-width Latin letters, digits, and punctuation collapse to ASCII ("TOKYO" → "TOKYO", "2024" → "2024", "!" → "!"), and half-width katakana expands to full-width ("カタカナ" → "カタカナ"). It does not fold case, strip accents, or expand compatibility forms such as ligatures; chain LowercaseNormalizer and DiacriticNormalizer for those. Run it early — before PunctuationNormalizer — so width-folded punctuation is then treated as a word boundary. It is a no-op for text that already uses only normal-width characters, ordinary CJK ideographs ("東京") included.

func (WidthNormalizer) Normalize

func (WidthNormalizer) Normalize(text string) string

Normalize folds full-width and half-width variants to their normal width.

Directories

Path Synopsis
Command example shows the public API of the core/search package: build an inverted index, index documents, run ranked queries, and delete a document.
Command example shows the public API of the core/search package: build an inverted index, index documents, run ranked queries, and delete a document.
Package relevance is the search-quality yardstick for core/search (#887): golden corpora with graded relevance judgments, the standard IR metrics (nDCG@10, MRR, Recall@50), and a runner that evaluates any ranking function against them.
Package relevance is the search-quality yardstick for core/search (#887): golden corpora with graded relevance judgments, the standard IR metrics (nDCG@10, MRR, Recall@50), and a runner that evaluates any ranking function against them.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL