@claude-flow/cli 3.10.24 → 3.10.26
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/src/memory/bge-embedder.d.ts +19 -0
- package/dist/src/memory/bge-embedder.d.ts.map +1 -0
- package/dist/src/memory/bge-embedder.js +107 -0
- package/dist/src/memory/bge-embedder.js.map +1 -0
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +1 -1
- package/scripts/beir-bootstrap-significance.mjs +162 -0
- package/scripts/run-beir-bge.mjs +294 -0
- package/scripts/run-beir-nfcorpus.mjs +231 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@claude-flow/cli",
|
|
3
|
-
"version": "3.10.
|
|
3
|
+
"version": "3.10.26",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Ruflo CLI - Enterprise AI agent orchestration with 60+ specialized agents, swarm coordination, MCP server, self-learning hooks, and vector memory for Claude Code",
|
|
6
6
|
"main": "dist/src/index.js",
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// beir-bootstrap-significance.mjs — paired bootstrap significance test for
|
|
3
|
+
// BEIR retrieval results (ADR-086).
|
|
4
|
+
//
|
|
5
|
+
// Given a run JSON with per-query nDCG@10 and a baseline mean nDCG@10,
|
|
6
|
+
// estimates whether our result is significantly above the baseline.
|
|
7
|
+
//
|
|
8
|
+
// Method: paired bootstrap — resample N=10000 with replacement from our
|
|
9
|
+
// per-query nDCG scores, compute mean each time, take 2.5%-97.5% percentile
|
|
10
|
+
// for 95% CI. Compare baseline to our CI: if baseline < CI lower bound,
|
|
11
|
+
// the difference is significant at p < 0.05.
|
|
12
|
+
//
|
|
13
|
+
// For a paired test against ANOTHER per-query run (e.g. our pure-BM25 vs
|
|
14
|
+
// our BGE-base), resample paired DIFFERENCES instead. Pass --paired
|
|
15
|
+
// path/to/baseline-run.json.
|
|
16
|
+
//
|
|
17
|
+
// Usage:
|
|
18
|
+
// node scripts/beir-bootstrap-significance.mjs <run-json-path>
|
|
19
|
+
// node scripts/beir-bootstrap-significance.mjs <ours> --paired <baseline>
|
|
20
|
+
//
|
|
21
|
+
// Example:
|
|
22
|
+
// node scripts/beir-bootstrap-significance.mjs \
|
|
23
|
+
// docs/benchmarks/runs/beir-nfcorpus-bge-latest.json
|
|
24
|
+
|
|
25
|
+
import { readFileSync } from 'node:fs';
|
|
26
|
+
import { performance } from 'node:perf_hooks';
|
|
27
|
+
|
|
28
|
+
const args = process.argv.slice(2);
|
|
29
|
+
if (args.length === 0) {
|
|
30
|
+
console.error('Usage: node beir-bootstrap-significance.mjs <run-json> [--paired <baseline-run-json>]');
|
|
31
|
+
process.exit(1);
|
|
32
|
+
}
|
|
33
|
+
const RUN_PATH = args[0];
|
|
34
|
+
const PAIRED_IDX = args.indexOf('--paired');
|
|
35
|
+
const PAIRED_PATH = PAIRED_IDX > 0 ? args[PAIRED_IDX + 1] : null;
|
|
36
|
+
const ITERATIONS = Number(process.env.BOOTSTRAP_ITERATIONS) || 10000;
|
|
37
|
+
|
|
38
|
+
// Deterministic PRNG (Mulberry32) — so re-runs are reproducible.
|
|
39
|
+
const SEED = Number(process.env.BOOTSTRAP_SEED) || 42;
|
|
40
|
+
function mulberry32(a) {
|
|
41
|
+
return function () {
|
|
42
|
+
a |= 0; a = (a + 0x6D2B79F5) | 0;
|
|
43
|
+
let t = a;
|
|
44
|
+
t = Math.imul(t ^ (t >>> 15), t | 1);
|
|
45
|
+
t ^= t + Math.imul(t ^ (t >>> 7), t | 61);
|
|
46
|
+
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function mean(arr) { let s = 0; for (const v of arr) s += v; return s / arr.length; }
|
|
51
|
+
|
|
52
|
+
function percentile(sortedArr, p) {
|
|
53
|
+
const idx = Math.max(0, Math.min(sortedArr.length - 1, Math.floor(p * sortedArr.length)));
|
|
54
|
+
return sortedArr[idx];
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function pointEstimate(scores) {
|
|
58
|
+
return {
|
|
59
|
+
mean: mean(scores),
|
|
60
|
+
n: scores.length,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function bootstrapCI(scores, iterations, rng, percentiles = [0.025, 0.975]) {
|
|
65
|
+
const means = new Float64Array(iterations);
|
|
66
|
+
const n = scores.length;
|
|
67
|
+
for (let i = 0; i < iterations; i++) {
|
|
68
|
+
let s = 0;
|
|
69
|
+
for (let j = 0; j < n; j++) s += scores[Math.floor(rng() * n)];
|
|
70
|
+
means[i] = s / n;
|
|
71
|
+
}
|
|
72
|
+
const sorted = Array.from(means).sort((a, b) => a - b);
|
|
73
|
+
return {
|
|
74
|
+
point: mean(scores),
|
|
75
|
+
lower: percentile(sorted, percentiles[0]),
|
|
76
|
+
upper: percentile(sorted, percentiles[1]),
|
|
77
|
+
p_value_vs_baseline_zero: null,
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Paired bootstrap: compare our per-query scores to a baseline's per-query
|
|
82
|
+
// scores. Resample (idx) and average paired differences. The 95% CI on the
|
|
83
|
+
// mean difference tells us if it's significantly non-zero.
|
|
84
|
+
function pairedBootstrap(oursByQid, baselineByQid, iterations, rng) {
|
|
85
|
+
const sharedIds = [...oursByQid.keys()].filter((id) => baselineByQid.has(id));
|
|
86
|
+
const diffs = sharedIds.map((id) => oursByQid.get(id) - baselineByQid.get(id));
|
|
87
|
+
const n = diffs.length;
|
|
88
|
+
const means = new Float64Array(iterations);
|
|
89
|
+
for (let i = 0; i < iterations; i++) {
|
|
90
|
+
let s = 0;
|
|
91
|
+
for (let j = 0; j < n; j++) s += diffs[Math.floor(rng() * n)];
|
|
92
|
+
means[i] = s / n;
|
|
93
|
+
}
|
|
94
|
+
const sorted = Array.from(means).sort((a, b) => a - b);
|
|
95
|
+
// One-sided p ≈ fraction of resamples where mean diff ≤ 0
|
|
96
|
+
let negCount = 0;
|
|
97
|
+
for (const m of means) if (m <= 0) negCount++;
|
|
98
|
+
return {
|
|
99
|
+
n_paired: n,
|
|
100
|
+
point_diff: mean(diffs),
|
|
101
|
+
lower_95: percentile(sorted, 0.025),
|
|
102
|
+
upper_95: percentile(sorted, 0.975),
|
|
103
|
+
p_one_sided: negCount / iterations,
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function loadRun(path) {
|
|
108
|
+
const data = JSON.parse(readFileSync(path, 'utf-8'));
|
|
109
|
+
if (!data.perQuery || !Array.isArray(data.perQuery)) {
|
|
110
|
+
console.error(`Run JSON at ${path} has no perQuery array — re-run the bench with the updated harness.`);
|
|
111
|
+
process.exit(2);
|
|
112
|
+
}
|
|
113
|
+
return data;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const ours = loadRun(RUN_PATH);
|
|
117
|
+
console.log(`# BEIR significance test (ADR-086)`);
|
|
118
|
+
console.log(`Run: ${RUN_PATH}`);
|
|
119
|
+
console.log(`Dataset: ${ours.dataset ?? '?'} Model: ${ours.model ?? '?'} Queries: ${ours.perQuery.length}`);
|
|
120
|
+
console.log(`Bootstrap: ${ITERATIONS} iterations, seed=${SEED}`);
|
|
121
|
+
|
|
122
|
+
const rng = mulberry32(SEED);
|
|
123
|
+
const ourScores = ours.perQuery.map((q) => q.ndcg10);
|
|
124
|
+
|
|
125
|
+
const t0 = performance.now();
|
|
126
|
+
const oursCI = bootstrapCI(ourScores, ITERATIONS, rng);
|
|
127
|
+
console.log(`\n=== Our nDCG@10 (1-sample bootstrap CI) ===`);
|
|
128
|
+
console.log(` point: ${oursCI.point.toFixed(4)}`);
|
|
129
|
+
console.log(` 95% CI: [${oursCI.lower.toFixed(4)}, ${oursCI.upper.toFixed(4)}]`);
|
|
130
|
+
|
|
131
|
+
if (ours.baselines) {
|
|
132
|
+
console.log(`\n=== vs each published baseline (CI overlap) ===`);
|
|
133
|
+
for (const [name, score] of Object.entries(ours.baselines)) {
|
|
134
|
+
const ciDiff = oursCI.point - score;
|
|
135
|
+
const lowerDiff = oursCI.lower - score;
|
|
136
|
+
const upperDiff = oursCI.upper - score;
|
|
137
|
+
const direction = ciDiff > 0 ? '↑ above' : '↓ below';
|
|
138
|
+
const sig = (lowerDiff > 0 && upperDiff > 0) ? 'p<0.05 (95% CI excludes baseline)'
|
|
139
|
+
: (lowerDiff < 0 && upperDiff < 0) ? 'p<0.05 (95% CI below baseline)'
|
|
140
|
+
: 'n.s. (95% CI overlaps baseline)';
|
|
141
|
+
console.log(` ${score.toFixed(3)} ${name.padEnd(28)} Δ=${ciDiff >= 0 ? '+' : ''}${ciDiff.toFixed(4)} ${direction} [${sig}]`);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (PAIRED_PATH) {
|
|
146
|
+
const baseline = loadRun(PAIRED_PATH);
|
|
147
|
+
console.log(`\n=== Paired bootstrap vs ${PAIRED_PATH} ===`);
|
|
148
|
+
const oursByQid = new Map(ours.perQuery.map((q) => [q.qid, q.ndcg10]));
|
|
149
|
+
const baseByQid = new Map(baseline.perQuery.map((q) => [q.qid, q.ndcg10]));
|
|
150
|
+
const rng2 = mulberry32(SEED + 1);
|
|
151
|
+
const paired = pairedBootstrap(oursByQid, baseByQid, ITERATIONS, rng2);
|
|
152
|
+
console.log(` paired queries: ${paired.n_paired}`);
|
|
153
|
+
console.log(` Δ (ours - baseline): ${paired.point_diff >= 0 ? '+' : ''}${paired.point_diff.toFixed(4)}`);
|
|
154
|
+
console.log(` 95% CI: [${paired.lower_95.toFixed(4)}, ${paired.upper_95.toFixed(4)}]`);
|
|
155
|
+
console.log(` one-sided p (Δ ≤ 0): ${paired.p_one_sided.toFixed(4)}`);
|
|
156
|
+
const sig = paired.lower_95 > 0 ? 'SIGNIFICANT improvement (p<0.025 one-sided)' :
|
|
157
|
+
paired.upper_95 < 0 ? 'SIGNIFICANT regression (p<0.025 one-sided)' :
|
|
158
|
+
'NOT SIGNIFICANT — CI overlaps zero';
|
|
159
|
+
console.log(` verdict: ${sig}`);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
console.log(`\nBootstrap took ${((performance.now() - t0) / 1000).toFixed(1)}s.`);
|
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// run-beir-bge.mjs — NFCorpus retrieval using real BGE-base-en-v1.5
|
|
3
|
+
// embeddings (bypasses neural-tools' broken agentic-flow path that hash-fell-
|
|
4
|
+
// back on darwin-arm64 without sharp/libvips).
|
|
5
|
+
//
|
|
6
|
+
// Stores doc embeddings in a single .f32 binary file (3633 * 768 * 4 = ~11MB)
|
|
7
|
+
// for fast bench iteration without re-embedding.
|
|
8
|
+
//
|
|
9
|
+
// Usage:
|
|
10
|
+
// cd /tmp/beir-nfcorpus
|
|
11
|
+
// node /path/to/scripts/run-beir-bge.mjs # full ingest + bench
|
|
12
|
+
// SKIP_INGEST=1 node /path/to/scripts/run-beir-bge.mjs # reuse cached embeds
|
|
13
|
+
// BGE_MODEL=Xenova/bge-small-en-v1.5 node ... # faster, lower-quality
|
|
14
|
+
// BGE_MODEL=Xenova/bge-large-en-v1.5 node ... # slower, higher-quality
|
|
15
|
+
//
|
|
16
|
+
// ADR-086.
|
|
17
|
+
|
|
18
|
+
import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'node:fs';
|
|
19
|
+
import { fileURLToPath } from 'node:url';
|
|
20
|
+
import { dirname, join, resolve } from 'node:path';
|
|
21
|
+
import { performance } from 'node:perf_hooks';
|
|
22
|
+
|
|
23
|
+
const SCRIPT_DIR = dirname(fileURLToPath(import.meta.url));
|
|
24
|
+
const CLI_ROOT = resolve(SCRIPT_DIR, '..');
|
|
25
|
+
const RUFLO_ROOT = resolve(SCRIPT_DIR, '../../../..');
|
|
26
|
+
const RUNS_DIR = join(RUFLO_ROOT, 'docs', 'benchmarks', 'runs');
|
|
27
|
+
|
|
28
|
+
const DATA_DIR = process.env.BEIR_DATA_DIR || '/tmp/beir-nfcorpus/nfcorpus';
|
|
29
|
+
const CACHE_DIR = join('/tmp/beir-nfcorpus', 'bge-cache');
|
|
30
|
+
const BGE_MODEL = process.env.BGE_MODEL || 'Xenova/bge-base-en-v1.5';
|
|
31
|
+
const SKIP_INGEST = process.env.SKIP_INGEST === '1';
|
|
32
|
+
const MAX_QUERIES = Number(process.env.MAX_QUERIES) || 0;
|
|
33
|
+
|
|
34
|
+
// Published BEIR baselines per dataset (nDCG@10).
|
|
35
|
+
// Sources: Thakur et al. 2021 (BEIR paper), BGE paper (BAAI 2024),
|
|
36
|
+
// SPLADE++ paper, papers-with-code BEIR leaderboards.
|
|
37
|
+
const BASELINES_BY_DATASET = {
|
|
38
|
+
nfcorpus: {
|
|
39
|
+
'BM25 (Lucene)': 0.325,
|
|
40
|
+
'DocT5query': 0.328,
|
|
41
|
+
'TAS-B': 0.319,
|
|
42
|
+
'GenQ': 0.319,
|
|
43
|
+
'ColBERT': 0.305,
|
|
44
|
+
'Contriever': 0.328,
|
|
45
|
+
'GTR-XL': 0.343,
|
|
46
|
+
'SPLADE++': 0.347,
|
|
47
|
+
'BGE-large-v1.5 (pub)': 0.380,
|
|
48
|
+
'SBERT msmarco': 0.272,
|
|
49
|
+
},
|
|
50
|
+
scifact: {
|
|
51
|
+
'BM25 (Lucene)': 0.679,
|
|
52
|
+
'DocT5query': 0.675,
|
|
53
|
+
'TAS-B': 0.643,
|
|
54
|
+
'GenQ': 0.644,
|
|
55
|
+
'ColBERT': 0.671,
|
|
56
|
+
'Contriever': 0.677,
|
|
57
|
+
'GTR-XL': 0.662,
|
|
58
|
+
'SPLADE++': 0.704,
|
|
59
|
+
'BGE-large-v1.5 (pub)': 0.722,
|
|
60
|
+
'SBERT msmarco': 0.555,
|
|
61
|
+
},
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
// Auto-detect dataset from DATA_DIR path; default to nfcorpus baselines.
|
|
65
|
+
function detectDataset(path) {
|
|
66
|
+
const p = path.toLowerCase();
|
|
67
|
+
for (const ds of Object.keys(BASELINES_BY_DATASET)) {
|
|
68
|
+
if (p.includes(ds)) return ds;
|
|
69
|
+
}
|
|
70
|
+
return 'nfcorpus';
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// ---------------------------------------------------------------------------
|
|
74
|
+
// nDCG (graded)
|
|
75
|
+
// ---------------------------------------------------------------------------
|
|
76
|
+
|
|
77
|
+
function dcg(rels, k) {
|
|
78
|
+
let s = 0;
|
|
79
|
+
for (let i = 0; i < Math.min(rels.length, k); i++) {
|
|
80
|
+
s += (Math.pow(2, rels[i]) - 1) / Math.log2(i + 2);
|
|
81
|
+
}
|
|
82
|
+
return s;
|
|
83
|
+
}
|
|
84
|
+
function ndcg(retrieved, qrels, k) {
|
|
85
|
+
const rels = retrieved.slice(0, k).map((id) => qrels.get(id) ?? 0);
|
|
86
|
+
const ideal = [...qrels.values()].sort((a, b) => b - a).slice(0, k);
|
|
87
|
+
const idcg = dcg(ideal, k);
|
|
88
|
+
return idcg > 0 ? dcg(rels, k) / idcg : 0;
|
|
89
|
+
}
|
|
90
|
+
function mrr(retrieved, qrels, k) {
|
|
91
|
+
for (let i = 0; i < Math.min(retrieved.length, k); i++) {
|
|
92
|
+
if ((qrels.get(retrieved[i]) ?? 0) > 0) return 1 / (i + 1);
|
|
93
|
+
}
|
|
94
|
+
return 0;
|
|
95
|
+
}
|
|
96
|
+
function recall(retrieved, qrels, k) {
|
|
97
|
+
const tot = [...qrels.values()].filter((v) => v > 0).length;
|
|
98
|
+
if (tot === 0) return 0;
|
|
99
|
+
let h = 0;
|
|
100
|
+
for (let i = 0; i < Math.min(retrieved.length, k); i++) {
|
|
101
|
+
if ((qrels.get(retrieved[i]) ?? 0) > 0) h++;
|
|
102
|
+
}
|
|
103
|
+
return h / tot;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function cosine(a, b) {
|
|
107
|
+
let s = 0;
|
|
108
|
+
for (let i = 0; i < a.length; i++) s += a[i] * b[i];
|
|
109
|
+
return s; // already L2-normalised in BGE
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// ---------------------------------------------------------------------------
|
|
113
|
+
// Loaders
|
|
114
|
+
// ---------------------------------------------------------------------------
|
|
115
|
+
|
|
116
|
+
function loadJsonl(path) {
|
|
117
|
+
return readFileSync(path, 'utf-8').split('\n').filter(Boolean).map((l) => JSON.parse(l));
|
|
118
|
+
}
|
|
119
|
+
function loadQrels(path) {
|
|
120
|
+
const qrels = new Map();
|
|
121
|
+
const lines = readFileSync(path, 'utf-8').split('\n');
|
|
122
|
+
for (let i = 1; i < lines.length; i++) {
|
|
123
|
+
if (!lines[i].trim()) continue;
|
|
124
|
+
const [qid, did, score] = lines[i].split('\t');
|
|
125
|
+
if (!qrels.has(qid)) qrels.set(qid, new Map());
|
|
126
|
+
qrels.get(qid).set(did, Number(score));
|
|
127
|
+
}
|
|
128
|
+
return qrels;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// ---------------------------------------------------------------------------
|
|
132
|
+
// Embedding cache I/O — single .f32 file + .ids file
|
|
133
|
+
// ---------------------------------------------------------------------------
|
|
134
|
+
|
|
135
|
+
function cachePath(suffix) {
|
|
136
|
+
const safe = BGE_MODEL.replace(/\//g, '_');
|
|
137
|
+
return join(CACHE_DIR, `${safe}.${suffix}`);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function saveEmbeddings(ids, embeddings, dim) {
|
|
141
|
+
mkdirSync(CACHE_DIR, { recursive: true });
|
|
142
|
+
// ids: one per line
|
|
143
|
+
writeFileSync(cachePath('ids'), ids.join('\n') + '\n');
|
|
144
|
+
// embeddings: concatenated Float32Array
|
|
145
|
+
const buf = new Float32Array(ids.length * dim);
|
|
146
|
+
for (let i = 0; i < ids.length; i++) buf.set(embeddings[i], i * dim);
|
|
147
|
+
writeFileSync(cachePath('f32'), Buffer.from(buf.buffer));
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function loadEmbeddings(dim) {
|
|
151
|
+
if (!existsSync(cachePath('ids')) || !existsSync(cachePath('f32'))) return null;
|
|
152
|
+
const ids = readFileSync(cachePath('ids'), 'utf-8').split('\n').filter(Boolean);
|
|
153
|
+
const raw = readFileSync(cachePath('f32'));
|
|
154
|
+
const buf = new Float32Array(raw.buffer, raw.byteOffset, raw.length / 4);
|
|
155
|
+
const embeds = [];
|
|
156
|
+
for (let i = 0; i < ids.length; i++) embeds.push(buf.slice(i * dim, (i + 1) * dim));
|
|
157
|
+
return { ids, embeds };
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// ---------------------------------------------------------------------------
|
|
161
|
+
// Main
|
|
162
|
+
// ---------------------------------------------------------------------------
|
|
163
|
+
|
|
164
|
+
async function main() {
|
|
165
|
+
const dataset = detectDataset(DATA_DIR);
|
|
166
|
+
const BASELINES_NDCG10 = BASELINES_BY_DATASET[dataset];
|
|
167
|
+
console.log(`# BEIR ${dataset} — BGE embedder (ADR-085/086)`);
|
|
168
|
+
console.log(`Model: ${BGE_MODEL}`);
|
|
169
|
+
console.log(`Data: ${DATA_DIR}`);
|
|
170
|
+
|
|
171
|
+
const corpus = loadJsonl(join(DATA_DIR, 'corpus.jsonl'));
|
|
172
|
+
const queries = loadJsonl(join(DATA_DIR, 'queries.jsonl'));
|
|
173
|
+
const qrels = loadQrels(join(DATA_DIR, 'qrels/test.tsv'));
|
|
174
|
+
console.log(`Corpus: ${corpus.length} docs · Queries: ${queries.length} · Test qrels: ${qrels.size}`);
|
|
175
|
+
|
|
176
|
+
const bge = await import(join(CLI_ROOT, 'dist/src/memory/bge-embedder.js'));
|
|
177
|
+
const emb = await bge.getBgeEmbedder(BGE_MODEL);
|
|
178
|
+
if (!emb) {
|
|
179
|
+
console.error('BGE failed to load:', bge.getBgeStatus());
|
|
180
|
+
process.exit(1);
|
|
181
|
+
}
|
|
182
|
+
const dim = emb.dim();
|
|
183
|
+
console.log(`BGE loaded (dim=${dim})`);
|
|
184
|
+
|
|
185
|
+
// Ingest or load cached doc embeddings.
|
|
186
|
+
let docIds, docEmbeds;
|
|
187
|
+
const cached = SKIP_INGEST ? loadEmbeddings(dim) : null;
|
|
188
|
+
if (cached) {
|
|
189
|
+
docIds = cached.ids;
|
|
190
|
+
docEmbeds = cached.embeds;
|
|
191
|
+
console.log(`Loaded ${docIds.length} cached doc embeddings from ${cachePath('f32')}`);
|
|
192
|
+
} else {
|
|
193
|
+
console.log(`\nEmbedding ${corpus.length} docs (this is the slow step, ~20 min on M-series CPU)...`);
|
|
194
|
+
docIds = corpus.map((d) => d._id);
|
|
195
|
+
docEmbeds = new Array(corpus.length);
|
|
196
|
+
const tIngest = performance.now();
|
|
197
|
+
for (let i = 0; i < corpus.length; i++) {
|
|
198
|
+
const d = corpus[i];
|
|
199
|
+
const text = `${d.title || ''}\n${d.text || ''}`.slice(0, 4096);
|
|
200
|
+
docEmbeds[i] = await emb.embed(text);
|
|
201
|
+
if ((i + 1) % 100 === 0) {
|
|
202
|
+
const elapsed = (performance.now() - tIngest) / 1000;
|
|
203
|
+
const rate = (i + 1) / elapsed;
|
|
204
|
+
const eta = (corpus.length - i - 1) / rate;
|
|
205
|
+
console.log(` ${i + 1}/${corpus.length} ${rate.toFixed(1)} docs/s ETA ${eta.toFixed(0)}s`);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
console.log(`Embedded ${corpus.length} docs in ${((performance.now() - tIngest) / 1000).toFixed(0)}s`);
|
|
209
|
+
saveEmbeddings(docIds, docEmbeds, dim);
|
|
210
|
+
console.log(`Cached to ${cachePath('f32')}`);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// Run retrieval per query.
|
|
214
|
+
const queriesById = new Map(queries.map((q) => [q._id, q.text]));
|
|
215
|
+
const evalQueryIds = MAX_QUERIES
|
|
216
|
+
? [...qrels.keys()].slice(0, MAX_QUERIES)
|
|
217
|
+
: [...qrels.keys()];
|
|
218
|
+
|
|
219
|
+
console.log(`\nRunning ${evalQueryIds.length} queries...`);
|
|
220
|
+
let nSum = 0, mSum = 0, r10Sum = 0, r100Sum = 0, n = 0;
|
|
221
|
+
// ADR-086 — save per-query metrics for paired bootstrap significance testing.
|
|
222
|
+
const perQuery = [];
|
|
223
|
+
const tQ = performance.now();
|
|
224
|
+
for (const qid of evalQueryIds) {
|
|
225
|
+
const qtext = queriesById.get(qid);
|
|
226
|
+
if (!qtext) continue;
|
|
227
|
+
// BGE v1.5 non-icl works without the query prefix.
|
|
228
|
+
const qEmb = await emb.embed(qtext);
|
|
229
|
+
const scores = new Array(docEmbeds.length);
|
|
230
|
+
for (let i = 0; i < docEmbeds.length; i++) scores[i] = { id: docIds[i], score: cosine(qEmb, docEmbeds[i]) };
|
|
231
|
+
scores.sort((a, b) => b.score - a.score);
|
|
232
|
+
const top100 = scores.slice(0, 100).map((s) => s.id);
|
|
233
|
+
|
|
234
|
+
const qmap = qrels.get(qid);
|
|
235
|
+
const qNdcg = ndcg(top100, qmap, 10);
|
|
236
|
+
const qMrr = mrr(top100, qmap, 10);
|
|
237
|
+
const qR10 = recall(top100, qmap, 10);
|
|
238
|
+
const qR100 = recall(top100, qmap, 100);
|
|
239
|
+
nSum += qNdcg; mSum += qMrr; r10Sum += qR10; r100Sum += qR100; n++;
|
|
240
|
+
perQuery.push({ qid, ndcg10: qNdcg, mrr10: qMrr, recall10: qR10, recall100: qR100 });
|
|
241
|
+
if (n % 50 === 0) {
|
|
242
|
+
const elapsed = (performance.now() - tQ) / 1000;
|
|
243
|
+
console.log(` ${n}/${evalQueryIds.length} in ${elapsed.toFixed(0)}s running nDCG@10=${(nSum / n).toFixed(4)}`);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
const queryMs = performance.now() - tQ;
|
|
247
|
+
|
|
248
|
+
const ndcg10 = nSum / n;
|
|
249
|
+
const mrr10 = mSum / n;
|
|
250
|
+
const recall10 = r10Sum / n;
|
|
251
|
+
const recall100 = r100Sum / n;
|
|
252
|
+
|
|
253
|
+
console.log(`\n=== Results (N=${n}, BGE-base-en-v1.5) ===`);
|
|
254
|
+
console.log(` nDCG@10: ${ndcg10.toFixed(4)}`);
|
|
255
|
+
console.log(` MRR@10: ${mrr10.toFixed(4)}`);
|
|
256
|
+
console.log(` Recall@10: ${recall10.toFixed(4)}`);
|
|
257
|
+
console.log(` Recall@100: ${recall100.toFixed(4)}`);
|
|
258
|
+
console.log(` Avg query latency: ${(queryMs / n).toFixed(0)}ms`);
|
|
259
|
+
|
|
260
|
+
console.log(`\n=== vs published NFCorpus baselines (nDCG@10) ===`);
|
|
261
|
+
const ourLabel = `ruflo + ${BGE_MODEL.replace('Xenova/', '')}`;
|
|
262
|
+
const ranking = [
|
|
263
|
+
...Object.entries(BASELINES_NDCG10).map(([name, score]) => ({ name, score, ours: false })),
|
|
264
|
+
{ name: ourLabel, score: ndcg10, ours: true },
|
|
265
|
+
].sort((a, b) => b.score - a.score);
|
|
266
|
+
for (const r of ranking) {
|
|
267
|
+
console.log(` ${r.score.toFixed(3)} ${r.name}${r.ours ? ' ← us' : ''}`);
|
|
268
|
+
}
|
|
269
|
+
const ourRank = ranking.findIndex((r) => r.ours) + 1;
|
|
270
|
+
console.log(`\n Our rank: ${ourRank} / ${ranking.length}`);
|
|
271
|
+
|
|
272
|
+
const summary = {
|
|
273
|
+
runAt: new Date().toISOString(),
|
|
274
|
+
benchmark: `beir-${dataset}-bge`,
|
|
275
|
+
dataset,
|
|
276
|
+
model: BGE_MODEL,
|
|
277
|
+
queries: n,
|
|
278
|
+
corpusSize: corpus.length,
|
|
279
|
+
metrics: { ndcg10, mrr10, recall10, recall100, avgQueryLatencyMs: queryMs / n },
|
|
280
|
+
perQuery, // ADR-086: per-query metrics for paired bootstrap significance testing
|
|
281
|
+
baselines: BASELINES_NDCG10,
|
|
282
|
+
ourRank,
|
|
283
|
+
leaderboardLength: ranking.length,
|
|
284
|
+
};
|
|
285
|
+
mkdirSync(RUNS_DIR, { recursive: true });
|
|
286
|
+
const stamp = summary.runAt.replace(/[:.]/g, '-');
|
|
287
|
+
const safe = BGE_MODEL.replace(/\//g, '_');
|
|
288
|
+
writeFileSync(join(RUNS_DIR, `beir-${dataset}-bge-${safe}-${stamp}.json`), JSON.stringify(summary, null, 2));
|
|
289
|
+
writeFileSync(join(RUNS_DIR, `beir-${dataset}-bge-latest.json`), JSON.stringify(summary, null, 2));
|
|
290
|
+
console.log(`\nWrote ${join(RUNS_DIR, `beir-${dataset}-bge-${safe}-${stamp}.json`)}`);
|
|
291
|
+
process.exit(0);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
main().catch((err) => { console.error(err); process.exit(1); });
|