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

152 lines
3.9 KiB
Go

package stdbench
import (
"fmt"
"math"
"sort"
"strings"
)
// DefaultKs are the rank cutoffs Recall@K / Precision@K report.
var DefaultKs = []int{1, 5, 10, 20}
// Retriever ranks corpus Doc IDs for a query, best first, capped at k.
type Retriever func(query string, k int) []string
// Metrics is the aggregate score of a retriever over a Dataset.
type Metrics struct {
Dataset string `json:"dataset"`
Queries int `json:"queries"`
Scored int `json:"scored"` // queries with a relevance judgement
RecallAtK map[int]float64 `json:"recall_at_k"`
PrecAtK map[int]float64 `json:"precision_at_k"`
NDCGAt10 float64 `json:"ndcg_at_10"`
MRR float64 `json:"mrr"`
}
// Evaluate runs retrieve against every query in ds and aggregates the
// standard retrieval metrics. Queries with no relevance judgement are
// counted in Queries but excluded from the metric averages. ks is the
// Recall@K / Precision@K cutoff set; pass nil for DefaultKs.
func Evaluate(ds Dataset, retrieve Retriever, ks []int) Metrics {
if len(ks) == 0 {
ks = DefaultKs
}
maxK := 10 // NDCG@10 always needs at least the top 10.
for _, k := range ks {
if k > maxK {
maxK = k
}
}
m := Metrics{
Dataset: ds.Name,
Queries: len(ds.Queries),
RecallAtK: make(map[int]float64, len(ks)),
PrecAtK: make(map[int]float64, len(ks)),
}
recallSum := make(map[int]float64, len(ks))
precSum := make(map[int]float64, len(ks))
var ndcgSum, rrSum float64
for _, q := range ds.Queries {
if len(q.Relevant) == 0 {
continue
}
m.Scored++
ranked := retrieve(q.Text, maxK)
for _, k := range ks {
hit := 0
for i, id := range ranked {
if i >= k {
break
}
if q.Relevant[id] > 0 {
hit++
}
}
recallSum[k] += float64(hit) / float64(len(q.Relevant))
precSum[k] += float64(hit) / float64(k)
}
ndcgSum += ndcg(ranked, q.Relevant, 10)
rrSum += reciprocalRank(ranked, q.Relevant)
}
if m.Scored > 0 {
for _, k := range ks {
m.RecallAtK[k] = recallSum[k] / float64(m.Scored)
m.PrecAtK[k] = precSum[k] / float64(m.Scored)
}
m.NDCGAt10 = ndcgSum / float64(m.Scored)
m.MRR = rrSum / float64(m.Scored)
}
return m
}
// ndcg computes normalized discounted cumulative gain at cutoff k using
// the graded relevance labels in rel. Returns 0 when no relevant doc
// exists (ideal DCG would be zero).
func ndcg(ranked []string, rel map[string]int, k int) float64 {
dcg := 0.0
for i, id := range ranked {
if i >= k {
break
}
if g := rel[id]; g > 0 {
dcg += float64(g) / math.Log2(float64(i+2))
}
}
grades := make([]int, 0, len(rel))
for _, g := range rel {
if g > 0 {
grades = append(grades, g)
}
}
sort.Sort(sort.Reverse(sort.IntSlice(grades)))
idcg := 0.0
for i, g := range grades {
if i >= k {
break
}
idcg += float64(g) / math.Log2(float64(i+2))
}
if idcg == 0 {
return 0
}
return dcg / idcg
}
// reciprocalRank returns 1/rank of the first relevant hit, or 0 when no
// relevant doc appears in the ranked list.
func reciprocalRank(ranked []string, rel map[string]int) float64 {
for i, id := range ranked {
if rel[id] > 0 {
return 1.0 / float64(i+1)
}
}
return 0
}
// Markdown renders the metrics as a Markdown section.
func (m Metrics) Markdown() string {
var b strings.Builder
fmt.Fprintf(&b, "### %s\n\n", m.Dataset)
fmt.Fprintf(&b, "_%d queries · %d scored against relevance judgements_\n\n", m.Queries, m.Scored)
b.WriteString("| metric | value |\n|--------|-------|\n")
ks := make([]int, 0, len(m.RecallAtK))
for k := range m.RecallAtK {
ks = append(ks, k)
}
sort.Ints(ks)
for _, k := range ks {
fmt.Fprintf(&b, "| Recall@%d | %.3f |\n", k, m.RecallAtK[k])
}
for _, k := range ks {
fmt.Fprintf(&b, "| Precision@%d | %.3f |\n", k, m.PrecAtK[k])
}
fmt.Fprintf(&b, "| NDCG@10 | %.3f |\n", m.NDCGAt10)
fmt.Fprintf(&b, "| MRR | %.3f |\n", m.MRR)
return b.String()
}