search

package
v0.18.1 Latest Latest
Warning

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

Go to latest
Published: Jul 19, 2026 License: MIT Imports: 19 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
	// DefaultKeyFieldWeight is the measured production key-field multiplier.
	// The structured-field qrels plateau across [1.75, 2.25]; 1.75 is the
	// lowest measured weight that lifts exact namespace evidence without
	// overwhelming value-only matches.
	DefaultKeyFieldWeight = 1.75
)

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. Selection is semantic and independent of dictionary ids: exact first, then prefix quality, then fuzzy edit distance.

Variables

View Source
var ErrBudgetExceeded = errors.New("search work budget exceeded")

ErrBudgetExceeded classifies deterministic query-work exhaustion.

View Source
var ErrIndexIncomplete = errors.New("search index is incomplete; bounded rebuild required")

ErrIndexIncomplete is returned instead of results when a convergent source mutation could not be represented within the local index budgets.

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 AnalysisLimitError added in v0.18.0

type AnalysisLimitError struct {
	Kind  AnalysisLimitKind
	Limit int64
	Got   int64
}

AnalysisLimitError reports a deterministic per-document or aggregate index budget failure. Callers should branch on Kind, not Error's prose.

func (*AnalysisLimitError) Error added in v0.18.0

func (e *AnalysisLimitError) Error() string

type AnalysisLimitKind added in v0.18.0

type AnalysisLimitKind string

AnalysisLimitKind is a stable machine-readable indexing limit name.

const (
	LimitDocumentBytes   AnalysisLimitKind = "document_bytes"
	LimitDocumentTokens  AnalysisLimitKind = "document_tokens"
	LimitDocumentTerms   AnalysisLimitKind = "document_terms"
	LimitLiveTerms       AnalysisLimitKind = "live_terms"
	LimitLivePostings    AnalysisLimitKind = "live_postings"
	LimitPositionEntries AnalysisLimitKind = "position_entries"
)

type AnalysisStats added in v0.18.0

type AnalysisStats struct {
	ProjectedBytes  int
	Tokens          int
	UniqueTerms     int
	Postings        int
	PositionEntries int
}

AnalysisStats describes the bounded representation produced for one document. ProjectedBytes counts the UTF-8 bytes handed to the analyzer; Postings is the number of distinct (class, term) pairs.

type Analyzer

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

Analyzer converts raw text into index terms. Queries use the same Analyze method unless the dynamic implementation also provides QueryAnalyzer for a documented query-only recall channel. 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, #1067): width folding, canonical normalization, full Unicode case folding, emoji-presentation folding, punctuation boundaries, and space normalization 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. Query analysis adds a ClassGram copy of a two-rune primary term, making "ar" recall "search" while the exact whole-word document still wins through the higher-weight ClassWord channel. Document postings stay unchanged.

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 Budget added in v0.18.0

type Budget struct {
	MaxQueryTerms       int64
	MaxDictionaryVisits int64
	MaxPostingVisits    int64
	MaxPositionVisits   int64
	MaxExpirationVisits int64
}

Budget caps work performed by one query. Zero means unlimited and is used by compatibility helpers; the server validates and supplies positive limits.

type BudgetExceededError added in v0.18.0

type BudgetExceededError struct {
	Kind  WorkKind
	Limit int64
}

BudgetExceededError records which stable counter exhausted its limit.

func (*BudgetExceededError) Error added in v0.18.0

func (e *BudgetExceededError) Error() string

func (*BudgetExceededError) Unwrap added in v0.18.0

func (e *BudgetExceededError) Unwrap() error

type Bytes

type Bytes []byte

Bytes adapts valid UTF-8 to text. Arbitrary binary bytes produce an empty document instead of accidentally entering the text analyzer.

func (Bytes) String

func (b Bytes) String() string

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

type CandidateSource added in v0.18.0

type CandidateSource[S comparable] func(yield func(S) bool)

CandidateSource streams a distinct set of document IDs into a bounded top-k query. A layered store can use it to push a selective secondary-index scope (for example a key-prefix radix) into retrieval instead of scoring the global posting union and filtering afterwards. The source is consumed synchronously while the index read lock is held; it must not call an index write method.

type CanonicalNormalizer added in v0.18.0

type CanonicalNormalizer struct{}

CanonicalNormalizer converts canonically equivalent spellings to NFC while preserving every combining mark. It is the conservative production choice: composed/decomposed input matches, but Thai tones, Indic viramas/matras, accents, niqqud, and harakat are never erased as a recall shortcut.

func (CanonicalNormalizer) Normalize added in v0.18.0

func (CanonicalNormalizer) Normalize(text string) string

Normalize returns text in Unicode NFC.

type CaseFoldNormalizer added in v0.18.0

type CaseFoldNormalizer struct{}

CaseFoldNormalizer applies Unicode's language-neutral full case fold. Unlike lowercasing, case folding expands equivalences such as German sharp-s to "ss" and maps both Greek sigma forms to sigma. It deliberately does not apply locale-specific mappings (for example Turkish dotted/dotless I).

func (CaseFoldNormalizer) Normalize added in v0.18.0

func (CaseFoldNormalizer) Normalize(text string) string

Normalize returns the Unicode case-folded text.

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 DocumentField added in v0.18.0

type DocumentField struct {
	ID   FieldID
	Text string
}

DocumentField is one independently analyzed field instance. Multiple FieldValue entries are allowed (for example JSON string leaves); phrase and proximity never cross from one instance to another.

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 EmojiPresentationNormalizer added in v0.18.0

type EmojiPresentationNormalizer struct{}

EmojiPresentationNormalizer removes the text/emoji presentation selectors U+FE0E and U+FE0F. Presentation is a rendering preference rather than lexical identity, so "❤", "❤︎", and "❤️" share one searchable term. ZWJ and skin-tone modifiers remain significant.

func (EmojiPresentationNormalizer) Normalize added in v0.18.0

func (EmojiPresentationNormalizer) Normalize(text string) string

Normalize removes Unicode variation selectors 15 and 16.

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 FieldID added in v0.18.0

type FieldID uint8

FieldID is a stable semantic search field. FieldDefault preserves the single-text Document contract; FieldKey and FieldValue let layered stores keep identifier and content evidence separate without importing their domain types into core.

const (
	FieldDefault FieldID = iota
	FieldKey
	FieldValue
)

type FieldWeighted added in v0.18.0

type FieldWeighted struct {
	Base        Scorer
	KeyWeight   float64
	ValueWeight float64
}

FieldWeighted layers stable key/value weights over a base scorer. Default text and value evidence use weight 1 unless explicitly overridden.

func (FieldWeighted) Score added in v0.18.0

func (s FieldWeighted) Score(stats TermStats) float64

type FieldedDocument added in v0.18.0

type FieldedDocument interface {
	Document
	SearchFields() []DocumentField
}

FieldedDocument exposes structured search fields. It remains a Document so existing generic index bounds and diagnostics keep a plain-text fallback, but Prepare uses SearchFields directly when this interface is present.

type Fields added in v0.18.0

type Fields []DocumentField

Fields is a convenient immutable FieldedDocument for callers that already have projected text instances.

func (Fields) SearchFields added in v0.18.0

func (f Fields) SearchFields() []DocumentField

func (Fields) String added in v0.18.0

func (f Fields) String() string

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 IndexHealth added in v0.18.0

type IndexHealth string

IndexHealth is the search index consistency state.

const (
	IndexHealthy    IndexHealth = "healthy"
	IndexIncomplete IndexHealth = "incomplete"
)

type IndexMemoryStats added in v0.18.0

type IndexMemoryStats struct {
	Documents              int
	PhysicalDocuments      int
	ExpiredDocuments       int
	ExpirationQueueEntries int
	LiveTerms              int
	RetainedTermSlots      int
	RetainedOrdinals       int
	Postings               int64
	PositionEntries        int64
	EstimatedLiveBytes     int64
	EstimatedRetainedBytes int64
	RebuildCount           uint64
	LastRebuildDuration    time.Duration
	WriteLockAcquisitions  uint64
	ExpirationPurged       uint64
	LastExpirationPurge    time.Duration
	Generation             uint64
	Health                 IndexHealth
}

IndexMemoryStats is an observable snapshot of live and retained index state.

type IndexOption added in v0.16.0

type IndexOption func(*indexConfig)

IndexOption configures an InvertedIndex at construction time.

func WithAnalysisLimits added in v0.18.0

func WithAnalysisLimits(limits SearchAnalysisLimits) IndexOption

WithAnalysisLimits installs hard per-document and aggregate index budgets.

func WithIndexClock added in v0.18.0

func WithIndexClock(clock func() time.Time) IndexOption

WithIndexClock overrides the wall clock used for expiration decisions. It is primarily useful for deterministic tests; production callers normally use the default time.Now clock.

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) error
	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. Index returns a typed AnalysisLimitError when configured document or aggregate budgets reject the final state. 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 configured Analyzer. An optional QueryAnalyzer may add query-only evidence; otherwise 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 in the stable total order (score descending, caller-provided typed ID comparator ascending).

Terms live in per-class tables (#888): each TokenClass has its own term dictionary and postings. Corpus statistics and posting payloads are further scoped by semantic Document field (#1061), 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, compareID func(S, S) int, 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). compareID is the mandatory ascending total order for equal-score document IDs; it must be stable across processes/replicas. 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]) Compact added in v0.18.0

func (idx *InvertedIndex[S, D]) Compact()

Compact atomically rebuilds dictionaries, ordinals, postings, and document maps from the live logical corpus. Searches observe either complete state.

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]) Health added in v0.18.0

func (idx *InvertedIndex[S, D]) Health() IndexHealth

Health returns the current consistency state.

func (*InvertedIndex[S, D]) Index

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

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. Configured analysis/index limits are checked before mutation.

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]) error

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]) IndexManyPreparedValidated added in v0.18.0

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

IndexManyPreparedValidated applies a batch previously accepted by ValidateManyPrepared. The caller must serialize intervening writers.

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

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

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]) IndexPreparedWithExpiration added in v0.18.0

func (idx *InvertedIndex[S, D]) IndexPreparedWithExpiration(id S, prepared PreparedDocument, expiration time.Time) error

IndexPreparedWithExpiration applies a prepared document with an absolute expiration deadline.

func (*InvertedIndex[S, D]) IndexWithExpiration added in v0.18.0

func (idx *InvertedIndex[S, D]) IndexWithExpiration(id S, doc D, expiration time.Time) error

IndexWithExpiration is Index with an absolute TTL deadline. Zero time and Unix epoch-or-earlier mean no expiration. A born-expired document behaves as a delete and never enters postings or the expiration heap.

func (*InvertedIndex[S, D]) MarkIncomplete added in v0.18.0

func (idx *InvertedIndex[S, D]) MarkIncomplete()

MarkIncomplete makes all subsequent context-aware searches fail closed.

func (*InvertedIndex[S, D]) MemoryStats added in v0.18.0

func (idx *InvertedIndex[S, D]) MemoryStats() IndexMemoryStats

MemoryStats returns live/retained cardinalities, a conservative byte estimate, compaction history, and consistency health.

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

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

Prepare analyzes a FieldedDocument's field instances (or doc.String() as one default field) WITHOUT taking the index lock and returns the bounded tokens IndexPrepared needs, their AnalysisStats, or a typed AnalysisLimitError. 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]) RebuildDocuments added in v0.18.1

func (idx *InvertedIndex[S, D]) RebuildDocuments(visit func(yield func(S, D, time.Time) error) error) error

RebuildDocuments constructs a complete replacement through bounded batches and publishes it atomically. visit must call yield for each document in the desired final corpus. Analysis and aggregate-limit validation happen against the replacement; an error discards it and leaves the prior index unchanged.

Unlike RebuildPrepared, this path never retains the analyzed representation of the whole corpus. It is intended for recovery code that already owns the source-of-truth graph and can enumerate it while searches fail closed.

func (*InvertedIndex[S, D]) RebuildPrepared added in v0.18.0

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

RebuildPrepared constructs a complete bounded replacement off to the side and swaps it in only after every aggregate limit succeeds. A failed rebuild leaves the prior (possibly incomplete) index untouched and unhealthy.

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; equal scores use the required typed document-ID comparator ascending.

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. Equal scores use the index's required typed document-ID comparator ascending.

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-execution sibling of SearchMatch, combining its match mode with the streaming candidate execution and size-k heap 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]) SearchMatchTopKCandidatesContextAt added in v0.18.0

func (idx *InvertedIndex[S, D]) SearchMatchTopKCandidatesContextAt(ctx context.Context, query string, k int, accept func(id S) bool, opts MatchOptions, budget Budget, now time.Time, candidates CandidateSource[S]) ([]Result[S], Stats, error)

SearchMatchTopKCandidatesContextAt is SearchMatchTopKContextAt with an optional streaming candidate scope. Supplying a source makes retrieval score only those IDs while retaining global corpus statistics, so filtering does not silently change BM25 IDF or length normalization.

func (*InvertedIndex[S, D]) SearchMatchTopKContext added in v0.18.0

func (idx *InvertedIndex[S, D]) SearchMatchTopKContext(ctx context.Context, query string, k int, accept func(id S) bool, opts MatchOptions, budget Budget) ([]Result[S], Stats, error)

SearchMatchTopKContext is SearchMatchTopK with cancellation and deterministic work accounting. It never returns partial results on error.

func (*InvertedIndex[S, D]) SearchMatchTopKContextAt added in v0.18.0

func (idx *InvertedIndex[S, D]) SearchMatchTopKContextAt(ctx context.Context, query string, k int, accept func(id S) bool, opts MatchOptions, budget Budget, now time.Time) ([]Result[S], Stats, error)

SearchMatchTopKContextAt is SearchMatchTopKContext with an explicit liveness instant. Layered stores use it to share one sampled now between index expiration and their accept callback.

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. Equal scores use the index's required typed document-ID comparator ascending.

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-execution sibling of SearchPhrase, mirroring SearchTopK (#841/#1060): it streams the rarest posting, verifies and scores one phrase candidate at a time, and retains only the k highest-scoring documents that satisfy accept. 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]) SearchPhraseTopKCandidatesContextAt added in v0.18.0

func (idx *InvertedIndex[S, D]) SearchPhraseTopKCandidatesContextAt(ctx context.Context, query string, k int, accept func(id S) bool, budget Budget, now time.Time, candidates CandidateSource[S]) ([]Result[S], Stats, error)

SearchPhraseTopKCandidatesContextAt is SearchPhraseTopKContextAt with an optional streaming candidate scope. It keeps the same global BM25 corpus statistics while avoiding work outside a selective layered-store scope.

func (*InvertedIndex[S, D]) SearchPhraseTopKContext added in v0.18.0

func (idx *InvertedIndex[S, D]) SearchPhraseTopKContext(ctx context.Context, query string, k int, accept func(id S) bool, budget Budget) ([]Result[S], Stats, error)

SearchPhraseTopKContext is SearchPhraseTopK with cancellation and deterministic work accounting. It never returns partial results on error.

func (*InvertedIndex[S, D]) SearchPhraseTopKContextAt added in v0.18.0

func (idx *InvertedIndex[S, D]) SearchPhraseTopKContextAt(ctx context.Context, query string, k int, accept func(id S) bool, budget Budget, now time.Time) ([]Result[S], Stats, error)

SearchPhraseTopKContextAt is SearchPhraseTopKContext with an explicit liveness instant shared with a layered store's accept callback.

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-execution sibling of Search (#841/#1060): it multiway-merges sorted postings, scores one document at a time, and retains only the k highest-scoring accepted documents. It therefore avoids both the exhaustive ordinal-to-score map and the complete result sort. Working memory is O(k + query clauses), independent of the total match count M.

accept gates a document before scoring and before it can occupy one of the k slots, so filtered-out documents (for example dead vertices) consume neither scoring work nor page budget. accept may be nil (accept everything). A layered store with a selective secondary index should use CandidateSource through SearchMatchTopKCandidatesContextAt instead of expressing that scope as an O(M) accept predicate.

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 use the index's total order (score DESC, typed ID comparator ASC), including at the k-th boundary. 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.

func (*InvertedIndex[S, D]) ValidateManyPrepared added in v0.18.0

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

ValidateManyPrepared checks aggregate limits against the batch's final replace state without mutating the index. A caller that serializes all index writers with an outer lock may use this to preserve a larger transaction's atomicity before applying the same batch.

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.

func (JSONStringValueNormalizer) Values added in v0.18.0

func (JSONStringValueNormalizer) Values(text string) (values []string, structured bool)

Values returns each JSON string leaf as a separate deterministic field instance. structured is false for plain text and malformed JSON, whose caller should preserve the original input as one field.

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 an already grouped document representation so all analysis, partitioning, sorting, frequency aggregation, and position collection complete before the index write lock is acquired. 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
	Expiration time.Time
}

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 QueryAnalyzer added in v0.18.0

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

QueryAnalyzer is an optional asymmetric analysis extension. An index always calls Analyze for documents; when the dynamic Analyzer also implements this interface it calls AnalyzeQuery for search input. Match-mode coverage remains defined solely by ClassWord; extra ClassGram terms are ranking evidence.

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 CJKBigram run — Unicode Ideographic characters, Hiragana, Katakana, and Hangul, following Lucene's documented CJKBigramFilter script set — 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.
  • Combining marks continue the preceding lexical run, preserving Thai and Indic identity. Each Unicode symbol is searchable as one primary token; ZWJ-linked symbols form one token and adjacent unjoined symbols do not.
  • Every other rune (whitespace and punctuation) 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 production normalizers (width, canonical composition, full case fold, emoji presentation, 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.

func (ScriptAwareTokenizer) TokenizeBounded added in v0.18.0

func (t ScriptAwareTokenizer) TokenizeBounded(text string, maxTokens int) ([]Token, bool)

TokenizeBounded stops before appending token maxTokens+1. The boolean is true when more output exists, allowing callers to reject a large document without retaining its complete token stream.

type SearchAnalysisLimits added in v0.18.0

type SearchAnalysisLimits struct {
	MaxDocumentBytes     int
	MaxDocumentTokens    int
	MaxDocumentTerms     int
	MaxLiveTerms         int
	MaxLivePostings      int64
	MaxPositionEntries   int64
	CompactionRatio      float64
	CompactionMinRetired int
}

SearchAnalysisLimits bounds the memory amplification of indexing. A zero field disables that individual limit. CompactionRatio and CompactionMinRetired control reclamation of dictionary/ordinal high-water storage after delete or TTL churn.

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. Implementations provide a stable document-ID order for equal scores; InvertedIndex requires a typed ascending ID comparator at construction.

type SizedDocument added in v0.18.0

type SizedDocument interface {
	Document
	SizeHint() int
}

SizedDocument can expose a cheap upper bound before String performs an expensive projection. The production vertex projection uses it to reject a large JSON/string payload before parsing or projecting its fields.

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 Stats added in v0.18.0

type Stats struct {
	QueryBytes        int64
	QueryTokens       int64
	QueryClauses      int64
	QueryTerms        int64
	DictionaryVisits  int64
	ExpansionRetained int64
	PostingVisits     int64
	PositionVisits    int64
	ExpirationVisits  int64
	CandidateVisits   int64
	CandidateSkips    int64
	AnalysisDuration  time.Duration
	ExpansionDuration time.Duration
	SelectionDuration time.Duration
}

Stats captures exact deterministic work plus coarse executor phase timing for one search attempt.

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 SymbolPreservingPunctuationNormalizer added in v0.18.0

type SymbolPreservingPunctuationNormalizer struct{}

SymbolPreservingPunctuationNormalizer replaces Unicode punctuation with a boundary while retaining symbols as searchable content. Production search uses it so emoji and intentional mathematical/currency symbols do not vanish; callers that want the older punctuation-and-symbol boundary policy can keep using PunctuationNormalizer.

func (SymbolPreservingPunctuationNormalizer) Normalize added in v0.18.0

Normalize replaces punctuation, but not symbols, with an ASCII space.

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
	// Field identifies the semantic document field supplying this evidence.
	Field FieldID
}

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 and semantic field (#888/#1061): DF, N, DocLen, and AvgLen describe only that channel/field. For a single-channel, single-field 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.

type WorkKind added in v0.18.0

type WorkKind string

WorkKind identifies one deterministic search-work counter.

const (
	WorkQueryBytes        WorkKind = "query_bytes"
	WorkQueryTokens       WorkKind = "query_tokens"
	WorkQueryClauses      WorkKind = "query_clauses"
	WorkQueryTerms        WorkKind = "query_terms"
	WorkDictionaryVisits  WorkKind = "dictionary_visits"
	WorkExpansionRetained WorkKind = "expansions_retained"
	WorkPostingVisits     WorkKind = "posting_visits"
	WorkPositionVisits    WorkKind = "position_visits"
	WorkExpirationVisits  WorkKind = "expiration_visits"
	WorkCandidateVisits   WorkKind = "candidate_visits"
	WorkCandidateSkips    WorkKind = "candidate_skips"
)

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