Files
wehub-resource-sync 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
chore: import upstream snapshot with attribution
2026-07-13 12:33:42 +08:00

92 lines
3.5 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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)
}