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
294 lines
8.0 KiB
Go
294 lines
8.0 KiB
Go
package stdbench
|
|
|
|
import (
|
|
"bufio"
|
|
"encoding/json"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"sort"
|
|
"strconv"
|
|
"strings"
|
|
)
|
|
|
|
// maxJSONLLine caps the scanner buffer — benchmark corpus lines can be
|
|
// large code blobs, well past bufio's 64 KiB default.
|
|
const maxJSONLLine = 16 * 1024 * 1024
|
|
|
|
// scanJSONL streams a JSON-lines file, decoding each non-blank line
|
|
// into a map and handing it to fn. Blank lines are skipped; a decode
|
|
// error is reported with its line number.
|
|
func scanJSONL(path string, fn func(rec map[string]any) error) error {
|
|
f, err := os.Open(path)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer f.Close()
|
|
|
|
sc := bufio.NewScanner(f)
|
|
sc.Buffer(make([]byte, 0, 64*1024), maxJSONLLine)
|
|
line := 0
|
|
for sc.Scan() {
|
|
line++
|
|
text := strings.TrimSpace(sc.Text())
|
|
if text == "" {
|
|
continue
|
|
}
|
|
var rec map[string]any
|
|
if err := json.Unmarshal([]byte(text), &rec); err != nil {
|
|
return fmt.Errorf("%s line %d: %w", path, line, err)
|
|
}
|
|
if err := fn(rec); err != nil {
|
|
return fmt.Errorf("%s line %d: %w", path, line, err)
|
|
}
|
|
}
|
|
return sc.Err()
|
|
}
|
|
|
|
// firstString returns the first string-valued key present in m.
|
|
func firstString(m map[string]any, keys ...string) string {
|
|
for _, k := range keys {
|
|
if v, ok := m[k].(string); ok && v != "" {
|
|
return v
|
|
}
|
|
}
|
|
return ""
|
|
}
|
|
|
|
// LoadCoIR loads a CoIR / BEIR-format benchmark from a directory laid
|
|
// out as corpus.jsonl + queries.jsonl + qrels/<split>.tsv. CoIR (Code
|
|
// Information Retrieval, ACL 2025) ships every task in this canonical
|
|
// BEIR triple. Only queries that appear in the qrels file are kept —
|
|
// queries.jsonl typically holds every split at once.
|
|
func LoadCoIR(dir string) (Dataset, error) {
|
|
ds := Dataset{Name: "CoIR"}
|
|
|
|
corpus, err := loadBEIRCorpus(filepath.Join(dir, "corpus.jsonl"))
|
|
if err != nil {
|
|
return Dataset{}, fmt.Errorf("CoIR corpus: %w", err)
|
|
}
|
|
ds.Corpus = corpus
|
|
|
|
queryText, err := loadBEIRQueries(filepath.Join(dir, "queries.jsonl"))
|
|
if err != nil {
|
|
return Dataset{}, fmt.Errorf("CoIR queries: %w", err)
|
|
}
|
|
|
|
qrels, err := loadBEIRQrels(dir)
|
|
if err != nil {
|
|
return Dataset{}, fmt.Errorf("CoIR qrels: %w", err)
|
|
}
|
|
|
|
ids := make([]string, 0, len(qrels))
|
|
for id := range qrels {
|
|
ids = append(ids, id)
|
|
}
|
|
sort.Strings(ids)
|
|
for _, id := range ids {
|
|
ds.Queries = append(ds.Queries, Query{
|
|
ID: id,
|
|
Text: queryText[id],
|
|
Relevant: qrels[id],
|
|
})
|
|
}
|
|
return ds, nil
|
|
}
|
|
|
|
func loadBEIRCorpus(path string) ([]Doc, error) {
|
|
var corpus []Doc
|
|
err := scanJSONL(path, func(rec map[string]any) error {
|
|
id := firstString(rec, "_id", "id", "doc_id")
|
|
if id == "" {
|
|
return nil
|
|
}
|
|
title := firstString(rec, "title")
|
|
text := firstString(rec, "text", "content", "body", "code")
|
|
corpus = append(corpus, Doc{ID: id, Text: strings.TrimSpace(title + " " + text)})
|
|
return nil
|
|
})
|
|
return corpus, err
|
|
}
|
|
|
|
func loadBEIRQueries(path string) (map[string]string, error) {
|
|
out := make(map[string]string)
|
|
err := scanJSONL(path, func(rec map[string]any) error {
|
|
id := firstString(rec, "_id", "id", "query_id")
|
|
if id == "" {
|
|
return nil
|
|
}
|
|
out[id] = firstString(rec, "text", "query", "question")
|
|
return nil
|
|
})
|
|
return out, err
|
|
}
|
|
|
|
// loadBEIRQrels reads the first qrels file it finds — qrels/test.tsv,
|
|
// qrels/dev.tsv, qrels/train.tsv, or a bare qrels.tsv. The TSV is
|
|
// `query-id<TAB>corpus-id<TAB>score`, with an optional header row.
|
|
func loadBEIRQrels(dir string) (map[string]map[string]int, error) {
|
|
candidates := []string{
|
|
filepath.Join(dir, "qrels", "test.tsv"),
|
|
filepath.Join(dir, "qrels", "dev.tsv"),
|
|
filepath.Join(dir, "qrels", "train.tsv"),
|
|
filepath.Join(dir, "qrels.tsv"),
|
|
}
|
|
var path string
|
|
for _, c := range candidates {
|
|
if _, err := os.Stat(c); err == nil {
|
|
path = c
|
|
break
|
|
}
|
|
}
|
|
if path == "" {
|
|
return nil, fmt.Errorf("no qrels file under %s", dir)
|
|
}
|
|
|
|
f, err := os.Open(path)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer f.Close()
|
|
|
|
qrels := make(map[string]map[string]int)
|
|
sc := bufio.NewScanner(f)
|
|
sc.Buffer(make([]byte, 0, 64*1024), maxJSONLLine)
|
|
for sc.Scan() {
|
|
fields := strings.Split(strings.TrimSpace(sc.Text()), "\t")
|
|
if len(fields) < 3 {
|
|
continue
|
|
}
|
|
score, err := strconv.Atoi(strings.TrimSpace(fields[2]))
|
|
if err != nil {
|
|
continue // header row ("query-id corpus-id score") or junk.
|
|
}
|
|
qid, cid := fields[0], fields[1]
|
|
if qrels[qid] == nil {
|
|
qrels[qid] = make(map[string]int)
|
|
}
|
|
qrels[qid][cid] = score
|
|
}
|
|
return qrels, sc.Err()
|
|
}
|
|
|
|
// LoadSWEContextBench loads SWE-ContextBench (arXiv 2602.08316) from a
|
|
// JSON-lines file — one context-retrieval task per line.
|
|
func LoadSWEContextBench(path string) (Dataset, error) {
|
|
return loadJSONLBench("SWE-ContextBench", path)
|
|
}
|
|
|
|
// LoadContextBench loads ContextBench (arXiv 2602.05892) from a
|
|
// JSON-lines file — one context-retrieval task per line.
|
|
func LoadContextBench(path string) (Dataset, error) {
|
|
return loadJSONLBench("ContextBench", path)
|
|
}
|
|
|
|
// loadJSONLBench loads a JSON-lines context-retrieval benchmark. Each
|
|
// line is one task object:
|
|
//
|
|
// {
|
|
// "id": "<task id>", // also task_id / instance_id
|
|
// "query": "<natural-language>", // also question / problem_statement
|
|
// "relevant": ["<doc id>", ...], // also gold / context / expected;
|
|
// // entries may be {"id","score"}
|
|
// "candidates": [{"id","text"}, ...] // optional per-task corpus pool;
|
|
// // also documents / pool
|
|
// }
|
|
//
|
|
// Per-task candidate pools are merged (deduplicated by ID) into one
|
|
// corpus. A task with no candidates still contributes its query — the
|
|
// caller indexes its own corpus in that case.
|
|
func loadJSONLBench(name, path string) (Dataset, error) {
|
|
ds := Dataset{Name: name}
|
|
corpusSeen := make(map[string]bool)
|
|
line := 0
|
|
|
|
err := scanJSONL(path, func(rec map[string]any) error {
|
|
line++
|
|
q := Query{
|
|
ID: firstString(rec, "id", "task_id", "instance_id", "_id"),
|
|
Text: firstString(rec, "query", "question", "problem_statement", "text"),
|
|
Relevant: make(map[string]int),
|
|
}
|
|
if q.ID == "" {
|
|
q.ID = fmt.Sprintf("%s-%d", name, line)
|
|
}
|
|
for _, rs := range relevantIDs(rec, "relevant", "gold", "context", "expected", "gold_files") {
|
|
q.Relevant[rs.id] = rs.score
|
|
}
|
|
for _, doc := range candidateDocs(rec, "candidates", "documents", "pool") {
|
|
if !corpusSeen[doc.ID] {
|
|
corpusSeen[doc.ID] = true
|
|
ds.Corpus = append(ds.Corpus, doc)
|
|
}
|
|
}
|
|
ds.Queries = append(ds.Queries, q)
|
|
return nil
|
|
})
|
|
if err != nil {
|
|
return Dataset{}, err
|
|
}
|
|
return ds, nil
|
|
}
|
|
|
|
// idScore is one relevance judgement extracted from a JSONL task.
|
|
type idScore struct {
|
|
id string
|
|
score int
|
|
}
|
|
|
|
// relevantIDs pulls the relevance judgement list from the first
|
|
// matching key. Each list entry is either a bare ID string or an
|
|
// {"id","score"} object; a bare string defaults to grade 1.
|
|
func relevantIDs(rec map[string]any, keys ...string) []idScore {
|
|
list := firstList(rec, keys...)
|
|
out := make([]idScore, 0, len(list))
|
|
for _, el := range list {
|
|
switch v := el.(type) {
|
|
case string:
|
|
if v != "" {
|
|
out = append(out, idScore{id: v, score: 1})
|
|
}
|
|
case map[string]any:
|
|
id := firstString(v, "id", "_id", "doc_id", "corpus_id")
|
|
if id == "" {
|
|
continue
|
|
}
|
|
score := 1
|
|
if s, ok := v["score"].(float64); ok && int(s) > 0 {
|
|
score = int(s)
|
|
}
|
|
out = append(out, idScore{id: id, score: score})
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
// candidateDocs pulls the per-task candidate pool from the first
|
|
// matching key. Each entry is an {"id","text"} object.
|
|
func candidateDocs(rec map[string]any, keys ...string) []Doc {
|
|
list := firstList(rec, keys...)
|
|
out := make([]Doc, 0, len(list))
|
|
for _, el := range list {
|
|
obj, ok := el.(map[string]any)
|
|
if !ok {
|
|
continue
|
|
}
|
|
id := firstString(obj, "id", "_id", "doc_id")
|
|
if id == "" {
|
|
continue
|
|
}
|
|
out = append(out, Doc{ID: id, Text: firstString(obj, "text", "content", "body", "code")})
|
|
}
|
|
return out
|
|
}
|
|
|
|
// firstList returns the first key whose value is a JSON array.
|
|
func firstList(m map[string]any, keys ...string) []any {
|
|
for _, k := range keys {
|
|
if v, ok := m[k].([]any); ok {
|
|
return v
|
|
}
|
|
}
|
|
return nil
|
|
}
|