Anydoc / anydoc parser engine (push) Canceled after 0s
App / Format, vet, test, and build (push) Canceled after 0s
CLI / test (macos-latest / go 1.26) (push) Canceled after 0s
CLI / test (ubuntu-latest / go 1.26) (push) Canceled after 0s
CLI / test (windows-latest / go 1.26) (push) Canceled after 0s
Build and Push Docker Image / build-ui (push) Canceled after 0s
Build and Push Docker Image / build-docreader (push) Canceled after 0s
Build and Push Docker Image / build-sandbox (push) Canceled after 0s
Build and Push Docker Image / build-app (amd64, linux/amd64, ubuntu-latest) (push) Canceled after 0s
Build and Push Docker Image / build-app (arm64, linux/arm64, ubuntu-24.04-arm) (push) Canceled after 0s
DocReader / Python tests and Go client tests (push) Canceled after 0s
Build & Publish (dsh-weknora) / Typecheck, test, pack (push) Canceled after 0s
Build & Publish (dsh-weknora) / End-to-end inside dsh (pinned) (push) Canceled after 0s
Build & Publish (dsh-weknora) / End-to-end inside dsh (latest, advisory) (push) Canceled after 0s
Frontend / Test, type-check, and build (push) Canceled after 0s
Go Lint Cache / warm lint cache (.) (push) Canceled after 0s
Go Lint Cache / warm lint cache (cli) (push) Canceled after 0s
Go Lint Cache / warm lint cache (client) (push) Canceled after 0s
Build & Publish (mcp-server) / Test (Python 3.10) (push) Canceled after 0s
Build & Publish (mcp-server) / Test (Python 3.11) (push) Canceled after 0s
Build & Publish (mcp-server) / Test (Python 3.12) (push) Canceled after 0s
Build & Publish (mcp-server) / Test (Python 3.13) (push) Canceled after 0s
Build and Push Docker Image / merge (push) Canceled after 0s
Build & Publish (dsh-weknora) / Publish to npm (push) Canceled after 0s
Build & Publish (mcp-server) / Build sdist + wheel (push) Canceled after 0s
Build & Publish (mcp-server) / Publish to PyPI (push) Canceled after 0s
110 lines
2.3 KiB
Go
110 lines
2.3 KiB
Go
package searchutil
|
|
|
|
import "sort"
|
|
|
|
// KeywordScoreCallbacks allows callers to hook into normalization telemetry.
|
|
type KeywordScoreCallbacks struct {
|
|
OnNoVariance func(count int, score float64)
|
|
OnNormalized func(count int, rawMin, rawMax, normalizeMin, normalizeMax float64)
|
|
}
|
|
|
|
// NormalizeKeywordScores normalizes keyword match scores in-place using robust percentile bounds.
|
|
func NormalizeKeywordScores[T any](
|
|
results []T,
|
|
isKeyword func(T) bool,
|
|
getScore func(T) float64,
|
|
setScore func(T, float64),
|
|
callbacks KeywordScoreCallbacks,
|
|
) {
|
|
keywordResults := make([]T, 0, len(results))
|
|
for _, result := range results {
|
|
if isKeyword(result) {
|
|
keywordResults = append(keywordResults, result)
|
|
}
|
|
}
|
|
|
|
if len(keywordResults) == 0 {
|
|
return
|
|
}
|
|
|
|
if len(keywordResults) == 1 {
|
|
setScore(keywordResults[0], 1.0)
|
|
return
|
|
}
|
|
|
|
minS := getScore(keywordResults[0])
|
|
maxS := minS
|
|
for _, r := range keywordResults[1:] {
|
|
score := getScore(r)
|
|
if score < minS {
|
|
minS = score
|
|
}
|
|
if score > maxS {
|
|
maxS = score
|
|
}
|
|
}
|
|
|
|
if maxS <= minS {
|
|
for _, r := range keywordResults {
|
|
setScore(r, 1.0)
|
|
}
|
|
if callbacks.OnNoVariance != nil {
|
|
callbacks.OnNoVariance(len(keywordResults), minS)
|
|
}
|
|
return
|
|
}
|
|
|
|
normalizeMin := minS
|
|
normalizeMax := maxS
|
|
|
|
if len(keywordResults) >= 10 {
|
|
scores := make([]float64, len(keywordResults))
|
|
for i, r := range keywordResults {
|
|
scores[i] = getScore(r)
|
|
}
|
|
sort.Float64s(scores)
|
|
p5Idx := len(scores) * 5 / 100
|
|
p95Idx := len(scores) * 95 / 100
|
|
if p5Idx < len(scores) {
|
|
normalizeMin = scores[p5Idx]
|
|
}
|
|
if p95Idx < len(scores) {
|
|
normalizeMax = scores[p95Idx]
|
|
}
|
|
}
|
|
|
|
rangeSize := normalizeMax - normalizeMin
|
|
if rangeSize > 0 {
|
|
for _, r := range keywordResults {
|
|
clamped := getScore(r)
|
|
if clamped < normalizeMin {
|
|
clamped = normalizeMin
|
|
} else if clamped > normalizeMax {
|
|
clamped = normalizeMax
|
|
}
|
|
ns := (clamped - normalizeMin) / rangeSize
|
|
if ns < 0 {
|
|
ns = 0
|
|
} else if ns > 1 {
|
|
ns = 1
|
|
}
|
|
setScore(r, ns)
|
|
}
|
|
if callbacks.OnNormalized != nil {
|
|
callbacks.OnNormalized(
|
|
len(keywordResults),
|
|
minS,
|
|
maxS,
|
|
normalizeMin,
|
|
normalizeMax,
|
|
)
|
|
}
|
|
return
|
|
}
|
|
|
|
// Fallback when percentile filtering collapses the range.
|
|
for _, r := range keywordResults {
|
|
setScore(r, 1.0)
|
|
}
|
|
}
|