a06f331eb8
CI / benchmark (push) Has been skipped
install-script / posix-syntax (push) Successful in 6m1s
CI / build-onnx (push) Failing after 6m43s
init-smoke / dry-run (push) Failing after 15m57s
security / govulncheck (push) Has been cancelled
security / trivy-fs (push) Has been cancelled
CI / test (1.26, ubuntu-latest) (push) Has been cancelled
Scorecard supply-chain security / Scorecard analysis (push) Has been cancelled
CI / test (1.26, macos-latest) (push) Has been cancelled
CI / build-windows (push) Has been cancelled
CI / lint (push) Has been cancelled
install-script / powershell-syntax (push) Has been cancelled
install-script / install (macos-14) (push) Has been cancelled
install-script / install (ubuntu-latest) (push) Has been cancelled
92 lines
3.5 KiB
Go
92 lines
3.5 KiB
Go
package search
|
||
|
||
import (
|
||
"os"
|
||
"strings"
|
||
"unicode"
|
||
|
||
porterstemmer "github.com/blevesearch/go-porterstemmer"
|
||
)
|
||
|
||
// ftsStemmingEnabled gates the token-normalization pass — stopword
|
||
// removal plus Porter stemming — applied to the full-text-search index
|
||
// and query paths. Default OFF: on the recall fixture stemming trades
|
||
// exact-symbol-lookup precision (exact-tier R@5 −3.1pp) for broader
|
||
// recall (R@20 +5.7pp), so it ships as an opt-in rather than quietly
|
||
// reranking every identifier query. Enable it with
|
||
// GORTEX_FTS_STEMMING=1 (also true / yes / on).
|
||
//
|
||
// Read once at process start, like the bigram-typo flag: the index
|
||
// built during a daemon's lifetime and every query against it share a
|
||
// single setting, so a mid-session toggle can't desynchronise stemmed
|
||
// postings from stemmed query terms. When enabled, the same
|
||
// normalization runs on both the posting list and the query, so the
|
||
// two never disagree.
|
||
var ftsStemmingEnabled = ftsStemmingFromEnv()
|
||
|
||
func ftsStemmingFromEnv() bool {
|
||
switch strings.ToLower(strings.TrimSpace(os.Getenv("GORTEX_FTS_STEMMING"))) {
|
||
case "1", "true", "yes", "on", "y":
|
||
return true
|
||
}
|
||
return false
|
||
}
|
||
|
||
// ftsStopWords is the stopword set: English glue words that carry
|
||
// no code-search signal. Deliberately tight — it holds only unambiguous
|
||
// function words, never code keywords (for / if / case / switch / type)
|
||
// or code-meaningful nouns (get / set / new / value / key), so dropping
|
||
// a member can never erase intent from a query. Tokens arrive already
|
||
// lowercased from Tokenize / TokenizeQuery.
|
||
var ftsStopWords = map[string]struct{}{
|
||
"the": {}, "a": {}, "an": {}, "and": {}, "or": {}, "but": {},
|
||
"of": {}, "to": {}, "in": {}, "on": {}, "at": {}, "by": {},
|
||
"with": {}, "as": {}, "is": {}, "are": {}, "was": {}, "were": {},
|
||
"be": {}, "been": {}, "that": {}, "this": {}, "these": {},
|
||
"those": {}, "from": {}, "into": {}, "than": {}, "then": {},
|
||
"it": {}, "its": {}, "so": {}, "such": {}, "via": {}, "per": {},
|
||
}
|
||
|
||
// NormalizeFTSTokens applies the FR63 stopword filter and Porter stemmer
|
||
// to a token list produced by Tokenize / TokenizeQuery. The index path
|
||
// (BM25Backend.Add, BleveBackend.Add) and the query path
|
||
// (BM25Backend.Search, BleveBackend.Search) both call it, so a stemmed
|
||
// posting list is always probed with stemmed query terms.
|
||
//
|
||
// Stopwords are dropped before stemming so a stemmed form can never
|
||
// collide with a stopword entry. The result is a freshly allocated
|
||
// slice; the input is left untouched. When stemming is disabled the
|
||
// input slice is returned unchanged.
|
||
func NormalizeFTSTokens(tokens []string) []string {
|
||
if !ftsStemmingEnabled || len(tokens) == 0 {
|
||
return tokens
|
||
}
|
||
out := make([]string, 0, len(tokens))
|
||
for _, t := range tokens {
|
||
if _, stop := ftsStopWords[t]; stop {
|
||
continue
|
||
}
|
||
out = append(out, stemFTSToken(t))
|
||
}
|
||
return out
|
||
}
|
||
|
||
// stemFTSToken returns the Porter stem of one lowercase token. Tokens
|
||
// shorter than four runes are returned unchanged — Porter's measure-
|
||
// gated rules almost never fire that low, and the trailing-"s" rule
|
||
// that does would conflate distinct short code fragments (ids vs id).
|
||
// Tokens carrying a digit or any non-ASCII rune are returned verbatim:
|
||
// the Porter algorithm is defined over the English letter alphabet, so
|
||
// "sha256" or "utf8" must not be mangled.
|
||
func stemFTSToken(t string) string {
|
||
if len(t) < 4 {
|
||
return t
|
||
}
|
||
for _, r := range t {
|
||
if r > unicode.MaxASCII || !unicode.IsLetter(r) {
|
||
return t
|
||
}
|
||
}
|
||
return porterstemmer.StemString(t)
|
||
}
|