code wiki / _hdl_build / nx_beir_eval.nx

nx_beir_eval.nx source

↩ module page · 505 lines · 22111 B

1// nx_beir_eval.nx -- SOVEREIGN BEIR harness: real external ground truth (BEIR/nfcorpus) scored by our own 2// integer BM25 -> mean nDCG@10 (the standard BEIR metric). The HONEST external row the maturity ladder is 3// gated on (F236): real 3633-doc corpus + 323 test queries + 12335 graded qrels, vs the published BEIR BM25 4// baseline (nfcorpus nDCG@10 ~0.32, Anserini). BM25 k1=0.9 b=0.4 (BEIR defaults). IDF=log2 (ranking-invariant 5// vs ln -> identical nDCG). Integer/fixed-point throughout (no float) = bit-reproducible. Forward index 6// (per-doc sorted term,tf). Tokenizer = lowercase [a-z0-9] runs, NO stem / NO stopwords (v1 -- the gap to the 7// stemmed 0.32 baseline IS the stemming rung, honestly, not a defect). 8// expect_exit: 0 license_tier: ORIGINAL 9import "nx_qabench_engine.nx" 10import "nx_ppmi_lib.nx" 11// seq1494 RERANK ARM: BM25 retrieves a shortlist, the PPMI late-interaction model re-orders it, and BOTH 12// nDCG@10 values are printed FROM THE SAME RUN so the delta cannot be confounded by corpus/qrel/tokeniser 13// drift. The BM25 arm is untouched: top[0..9] keeps its exact BM25 order, we merely widen the candidate 14// selection past 10 so the reranker has something to re-order. 15// derived: 50 candidates is the standard first-stage depth for late-interaction reranking -- deep enough 16// that recall@50 >> recall@10 (so the reranker CAN win) and shallow enough to stay O(50) maxsim per query. 17const BE_KCAND: i64 = 50 18// sized: query/doc PPMI id buffers; nfcorpus docs are abstracts, far under this 19const BE_IDCAP: i64 = 4096 20const K_MAGIC_1024: i64 = 1024 21const K_MAGIC_8192: i64 = 8192 22const K_MAGIC_4096: i64 = 4096 23const K_MAGIC_1900: i64 = 1900 24 25const MAXDOC: i64 = 4096 26const CAPFILE: i64 = 67108864 27const NTCAP: i64 = 2000000 28const DTCAP: i64 = 2000000 29const NJCAP: i64 = 20000 30 31func be_read(path: *u8, slot: *i64) -> i64 { 32 let fd: i64 = sys_openat_rd(path) 33 if fd < 0 { return 0 - 1 } 34 let buf: *u8 = sys_mmap(CAPFILE) 35 var total: i64 = 0 36 var r: i64 = 1 37 while r > 0 { 38 if total >= CAPFILE { r = 0 } else { 39 r = sys_read(fd, (buf as i64 + total) as *u8, CAPFILE - total) 40 if r > 0 { total = total + r } 41 } 42 } 43 sys_close(fd) 44 slot[0] = buf as i64 45 return total 46} 47 48// read pathA, else fall back to pathB (NAS stable path first, local /tmp second) 49func be_read_fb(pa: *u8, pb: *u8, slot: *i64) -> i64 { 50 let n: i64 = be_read(pa, slot) 51 if n > 0 { return n } 52 return be_read(pb, slot) 53} 54 55// index of byte ch in buf[from,limit), else limit 56func be_find(buf: *u8, from: i64, limit: i64, ch: i64) -> i64 { 57 var i: i64 = from 58 var res: i64 = limit 59 var go: i64 = 1 60 while go == 1 { 61 if i >= limit { go = 0 } else { 62 if buf[i] == (ch as u8) { res = i; go = 0 } else { i = i + 1 } 63 } 64 } 65 return res 66} 67 68// lowercase+hash [a-z0-9] tokens in buf[start,end); append hashes to out at noff[0]; return token count 69func be_tok(buf: *u8, start: i64, end: i64, out: *i64, noff: *i64, scr: *u8) -> i64 { 70 var i: i64 = start 71 var cnt: i64 = 0 72 var sl: i64 = 0 73 while i <= end { 74 var c: i64 = 0 75 if i < end { c = buf[i] as i64 } 76 var isc: i64 = 0 77 var lc: i64 = c 78 if c >= 65 { if c <= 90 { lc = c + 32; isc = 1 } } 79 if c >= 97 { if c <= 122 { isc = 1 } } 80 if c >= 48 { if c <= 57 { isc = 1 } } 81 if isc == 1 { 82 if sl < 500 { scr[sl] = lc as u8; sl = sl + 1 } 83 } else { 84 if sl > 0 { 85 out[noff[0]] = db_semhash(scr, 0, sl) 86 noff[0] = noff[0] + 1 87 cnt = cnt + 1 88 sl = 0 89 } 90 } 91 i = i + 1 92 } 93 return cnt 94} 95 96func be_qs1(a: *i64, lo: i64, hi: i64) -> i64 { 97 if lo >= hi { return 0 } 98 var i: i64 = lo 99 var j: i64 = hi 100 let p: i64 = a[(lo + hi) / 2] 101 while i <= j { 102 while a[i] < p { i = i + 1 } 103 while a[j] > p { j = j - 1 } 104 if i <= j { 105 let t: i64 = a[i]; a[i] = a[j]; a[j] = t 106 i = i + 1; j = j - 1 107 } 108 } 109 be_qs1(a, lo, j) 110 be_qs1(a, i, hi) 111 return 0 112} 113 114func be_qs2(key: *i64, pay: *i64, lo: i64, hi: i64) -> i64 { 115 if lo >= hi { return 0 } 116 var i: i64 = lo 117 var j: i64 = hi 118 let p: i64 = key[(lo + hi) / 2] 119 while i <= j { 120 while key[i] < p { i = i + 1 } 121 while key[j] > p { j = j - 1 } 122 if i <= j { 123 let t: i64 = key[i]; key[i] = key[j]; key[j] = t 124 let u: i64 = pay[i]; pay[i] = pay[j]; pay[j] = u 125 i = i + 1; j = j - 1 126 } 127 } 128 be_qs2(key, pay, lo, j) 129 be_qs2(key, pay, i, hi) 130 return 0 131} 132 133// log2(q) in 1/1024ths for q>=1 134func be_log2_1024(q: i64) -> i64 { 135 if q <= 1 { return 0 } 136 var bl: i64 = 0 137 var t: i64 = q 138 while t > 1 { t = t / 2; bl = bl + 1 } 139 var frac: i64 = 0 140 if bl >= 6 { frac = (q / (1 << (bl - 6))) - 64 } 141 if bl < 6 { frac = (q << (6 - bl)) - 64 } 142 if frac < 0 { frac = 0 } 143 return bl * K_MAGIC_1024 + frac * 16 144} 145 146func be_disc(r: i64) -> i64 { 147 if r == 1 { return 1000 } 148 if r == 2 { return 631 } 149 if r == 3 { return 500 } 150 if r == 4 { return 431 } 151 if r == 5 { return 387 } 152 if r == 6 { return 356 } 153 if r == 7 { return 333 } 154 if r == 8 { return 315 } 155 if r == 9 { return 301 } 156 if r == 10 { return 289 } 157 return 0 158} 159 160func main() -> i64 { 161 let G: *i64 = sys_mmap(64) as *i64 162 let scr: *u8 = sys_mmap(K_MAGIC_1024) 163 164 // ---------- load corpus ---------- 165 let cn: i64 = be_read_fb("knowledge/beir/nfcorpus/corpus.tsv" as *u8, "/tmp/beir/nfcorpus/corpus.tsv" as *u8, G) 166 if cn <= 0 { db_w("{\"error\":\"corpus.tsv missing\"}\n" as *u8); return 1 } 167 let cbuf: *u8 = G[0] as *u8 168 169 let thash: *i64 = sys_mmap(NTCAP * 8) as *i64 170 let noff: *i64 = sys_mmap(8) as *i64 171 noff[0] = 0 172 let doc_start: *i64 = sys_mmap((MAXDOC + 1) * 8) as *i64 173 let doc_len: *i64 = sys_mmap(MAXDOC * 8) as *i64 174 let doc_idh: *i64 = sys_mmap(MAXDOC * 8) as *i64 175 // seq1494 RERANK ARM: keep each doc's RAW TEXT SPAN in cbuf. BM25 works on this harness's own hashed 176 // vocab, but the PPMI late-interaction model has its OWN word ids, so the reranker needs the original 177 // characters. Two i64 arrays is the whole cost of making rung 2 measurable instead of asserted. 178 let doc_ts: *i64 = sys_mmap(MAXDOC * 8) as *i64 179 let doc_te: *i64 = sys_mmap(MAXDOC * 8) as *i64 180 var nd: i64 = 0 181 182 var p: i64 = 0 183 while p < cn { 184 let eol: i64 = be_find(cbuf, p, cn, 10) 185 let tab: i64 = be_find(cbuf, p, eol, 9) 186 if tab < eol { if nd < MAXDOC { 187 doc_idh[nd] = db_semhash(cbuf, p, tab - p) 188 doc_start[nd] = noff[0] 189 doc_ts[nd] = tab + 1 190 doc_te[nd] = eol 191 doc_len[nd] = be_tok(cbuf, tab + 1, eol, thash, noff, scr) 192 nd = nd + 1 193 } } 194 p = eol + 1 195 } 196 doc_start[nd] = noff[0] 197 let ntok: i64 = noff[0] 198 199 // ---------- vocab = sort+unique ---------- 200 let vocab: *i64 = sys_mmap(ntok * 8) as *i64 201 var vi: i64 = 0 202 while vi < ntok { vocab[vi] = thash[vi]; vi = vi + 1 } 203 be_qs1(vocab, 0, ntok - 1) 204 var nv: i64 = 0 205 var k: i64 = 0 206 while k < ntok { 207 if nv == 0 { vocab[nv] = vocab[k]; nv = nv + 1 } 208 else { if vocab[k] != vocab[nv - 1] { vocab[nv] = vocab[k]; nv = nv + 1 } } 209 k = k + 1 210 } 211 212 // ---------- forward index + df ---------- 213 let dterm: *i64 = sys_mmap(DTCAP * 8) as *i64 214 let dtf: *i64 = sys_mmap(DTCAP * 8) as *i64 215 let doc_off: *i64 = sys_mmap((MAXDOC + 1) * 8) as *i64 216 let df: *i64 = sys_mmap(nv * 8) as *i64 217 var z: i64 = 0 218 while z < nv { df[z] = 0; z = z + 1 } 219 let tmp: *i64 = sys_mmap(K_MAGIC_8192 * 8) as *i64 220 var dpos: i64 = 0 221 var d: i64 = 0 222 while d < nd { 223 let s0: i64 = doc_start[d] 224 let s1: i64 = doc_start[d + 1] 225 var m: i64 = 0 226 var q: i64 = s0 227 while q < s1 { 228 if m < K_MAGIC_8192 { tmp[m] = db_bsearch_i64(vocab, nv, thash[q]); m = m + 1 } 229 q = q + 1 230 } 231 be_qs1(tmp, 0, m - 1) 232 doc_off[d] = dpos 233 var a: i64 = 0 234 while a < m { 235 var b: i64 = a + 1 236 var go3: i64 = 1 237 while go3 == 1 { if b < m { if tmp[b] == tmp[a] { b = b + 1 } else { go3 = 0 } } else { go3 = 0 } } 238 if dpos < DTCAP { dterm[dpos] = tmp[a]; dtf[dpos] = b - a; dpos = dpos + 1 } 239 df[tmp[a]] = df[tmp[a]] + 1 240 a = b 241 } 242 d = d + 1 243 } 244 doc_off[nd] = dpos 245 246 var totlen: i64 = 0 247 z = 0 248 while z < nd { totlen = totlen + doc_len[z]; z = z + 1 } 249 var avgdl: i64 = 1 250 if nd > 0 { avgdl = totlen / nd } 251 if avgdl < 1 { avgdl = 1 } 252 253 // ---------- doc-id -> docidx (sorted idhash) ---------- 254 let sidh: *i64 = sys_mmap(nd * 8) as *i64 255 let sidx: *i64 = sys_mmap(nd * 8) as *i64 256 z = 0 257 while z < nd { sidh[z] = doc_idh[z]; sidx[z] = z; z = z + 1 } 258 be_qs2(sidh, sidx, 0, nd - 1) 259 260 // ---------- qrels ---------- 261 let qn: i64 = be_read_fb("knowledge/beir/nfcorpus/qrels/test.tsv" as *u8, "/tmp/beir/nfcorpus/qrels/test.tsv" as *u8, (G as i64 + 8) as *i64) 262 if qn <= 0 { db_w("{\"error\":\"qrels test.tsv missing\"}\n" as *u8); return 1 } 263 let qbuf: *u8 = G[1] as *u8 264 let j_qid: *i64 = sys_mmap(NJCAP * 8) as *i64 265 let j_doc: *i64 = sys_mmap(NJCAP * 8) as *i64 266 let j_scr: *i64 = sys_mmap(NJCAP * 8) as *i64 267 var nj: i64 = 0 268 var qp: i64 = 0 269 var hdr: i64 = 1 270 while qp < qn { 271 let e2: i64 = be_find(qbuf, qp, qn, 10) 272 if hdr == 1 { hdr = 0 } else { 273 let t1: i64 = be_find(qbuf, qp, e2, 9) 274 if t1 < e2 { 275 let t2: i64 = be_find(qbuf, t1 + 1, e2, 9) 276 if t2 < e2 { 277 var sc: i64 = 0 278 var sp: i64 = t2 + 1 279 while sp < e2 { let dch: i64 = qbuf[sp] as i64; if dch >= 48 { if dch <= 57 { sc = sc * 10 + (dch - 48) } } sp = sp + 1 } 280 let dh: i64 = db_semhash(qbuf, t1 + 1, t2 - t1 - 1) 281 let dpos2: i64 = db_bsearch_i64(sidh, nd, dh) 282 if dpos2 >= 0 { if nj < NJCAP { 283 j_qid[nj] = db_semhash(qbuf, qp, t1 - qp) 284 j_doc[nj] = sidx[dpos2] 285 j_scr[nj] = sc 286 nj = nj + 1 287 } } 288 } 289 } 290 } 291 qp = e2 + 1 292 } 293 // sort judgments by qid, carrying an index permutation (so j_doc/j_scr stay addressable via jidx) 294 let jidx: *i64 = sys_mmap(nj * 8) as *i64 295 z = 0 296 while z < nj { jidx[z] = z; z = z + 1 } 297 be_qs2(j_qid, jidx, 0, nj - 1) 298 299 // ---------- per query ---------- 300 let dqn: i64 = be_read_fb("knowledge/beir/nfcorpus/queries.tsv" as *u8, "/tmp/beir/nfcorpus/queries.tsv" as *u8, (G as i64 + 16) as *i64) 301 if dqn <= 0 { db_w("{\"error\":\"queries.tsv missing\"}\n" as *u8); return 1 } 302 let dbuf: *u8 = G[2] as *u8 303 let score: *i64 = sys_mmap(MAXDOC * 8) as *i64 304 let qtid: *i64 = sys_mmap(K_MAGIC_1024 * 8) as *i64 305 let top: *i64 = sys_mmap(16 * 8) as *i64 306 let goldsc: *i64 = sys_mmap(K_MAGIC_4096 * 8) as *i64 307 let used: *u8 = sys_mmap(MAXDOC) 308 let qhbuf: *i64 = sys_mmap(K_MAGIC_1024 * 8) as *i64 309 let qnoff: *i64 = sys_mmap(8) as *i64 310 311 // seq1494: load the PPMI model ONCE. HONEST-ABSENT: if it is missing the rerank arm reports 312 // UNAVAILABLE rather than silently scoring 0 -- an absent model must never read as "reranking is bad". 313 let PG: *i64 = sys_mmap(64) as *i64 314 let rr_ok: i64 = ppl_load(PG, "knowledge/index/semppmi_v1.bin" as *u8) 315 let qids: *i64 = sys_mmap(BE_IDCAP * 8) as *i64 316 let dids: *i64 = sys_mmap(BE_IDCAP * 8) as *i64 317 let cscore: *i64 = sys_mmap(BE_KCAND * 8) as *i64 318 let cused: *u8 = sys_mmap(BE_KCAND) 319 var sum_ndcg_rr: i64 = 0 320 var q_improved: i64 = 0 321 var q_worsened: i64 = 0 322 var q_same: i64 = 0 323 var sum_ndcg: i64 = 0 324 var nq: i64 = 0 325 var n_noqrel: i64 = 0 326 var n_noterm: i64 = 0 327 var n_noidcg: i64 = 0 328 var qpp: i64 = 0 329 while qpp < dqn { 330 let e3: i64 = be_find(dbuf, qpp, dqn, 10) 331 let tb: i64 = be_find(dbuf, qpp, e3, 9) 332 if tb < e3 { 333 let qh2: i64 = db_semhash(dbuf, qpp, tb - qpp) 334 qnoff[0] = 0 335 be_tok(dbuf, tb + 1, e3, qhbuf, qnoff, scr) 336 var nqt: i64 = 0 337 var qi: i64 = 0 338 while qi < qnoff[0] { 339 let tid2: i64 = db_bsearch_i64(vocab, nv, qhbuf[qi]) 340 if tid2 >= 0 { if nqt < K_MAGIC_1024 { qtid[nqt] = tid2; nqt = nqt + 1 } } 341 qi = qi + 1 342 } 343 let lo0: i64 = db_bsearch_i64(j_qid, nj, qh2) 344 if lo0 < 0 { n_noqrel = n_noqrel + 1 } else { 345 var jlo: i64 = lo0 346 var g1: i64 = 1 347 while g1 == 1 { if jlo > 0 { if j_qid[jlo - 1] == qh2 { jlo = jlo - 1 } else { g1 = 0 } } else { g1 = 0 } } 348 var jhi: i64 = lo0 349 var g2: i64 = 1 350 while g2 == 1 { if jhi < nj - 1 { if j_qid[jhi + 1] == qh2 { jhi = jhi + 1 } else { g2 = 0 } } else { g2 = 0 } } 351 352 // gold + IDCG@10 FIRST (methodology: every query with relevant judgments is counted) 353 var ng: i64 = 0 354 var jjg: i64 = jlo 355 while jjg <= jhi { if ng < K_MAGIC_4096 { goldsc[ng] = j_scr[jidx[jjg]]; ng = ng + 1 } jjg = jjg + 1 } 356 be_qs1(goldsc, 0, ng - 1) 357 var idcg: i64 = 0 358 var ri: i64 = 0 359 while ri < 10 { if ri < ng { idcg = idcg + goldsc[ng - 1 - ri] * be_disc(ri + 1) } ri = ri + 1 } 360 361 if idcg <= 0 { n_noidcg = n_noidcg + 1 } else { 362 var dcg: i64 = 0 363 if nqt <= 0 { n_noterm = n_noterm + 1 } else { 364 z = 0 365 while z < nd { score[z] = 0; z = z + 1 } 366 var qt: i64 = 0 367 while qt < nqt { 368 let tid3: i64 = qtid[qt] 369 let dfi: i64 = df[tid3] 370 if dfi > 0 { 371 let idf: i64 = be_log2_1024((2 * nd + 2) / (2 * dfi + 1)) 372 var dd: i64 = 0 373 while dd < nd { 374 let o0: i64 = doc_off[dd] 375 let o1: i64 = doc_off[dd + 1] 376 let pp: i64 = db_bsearch_i64((dterm as i64 + o0 * 8) as *i64, o1 - o0, tid3) 377 if pp >= 0 { 378 let tf: i64 = dtf[o0 + pp] 379 let norm1000: i64 = 600 + (400 * doc_len[dd]) / avgdl 380 let denom1000: i64 = tf * 1000 + (900 * norm1000) / 1000 381 if denom1000 > 0 { score[dd] = score[dd] + (idf * tf * K_MAGIC_1900) / denom1000 } 382 } 383 dd = dd + 1 384 } 385 } 386 qt = qt + 1 387 } 388 z = 0 389 while z < nd { used[z] = 0 as u8; z = z + 1 } 390 var rr: i64 = 0 391 while rr < BE_KCAND { 392 var best: i64 = 0 - 1 393 var dd2: i64 = 0 394 while dd2 < nd { 395 if used[dd2] == (0 as u8) { 396 if best < 0 { best = dd2 } else { if score[dd2] > score[best] { best = dd2 } } 397 } 398 dd2 = dd2 + 1 399 } 400 top[rr] = best 401 if best >= 0 { used[best] = 1 as u8 } 402 rr = rr + 1 403 } 404 rr = 0 405 while rr < 10 { 406 let dpk: i64 = top[rr] 407 var gv: i64 = 0 408 if dpk >= 0 { 409 var jj2: i64 = jlo 410 while jj2 <= jhi { 411 if j_doc[jidx[jj2]] == dpk { gv = j_scr[jidx[jj2]]; jj2 = jhi + 1 } else { jj2 = jj2 + 1 } 412 } 413 } 414 dcg = dcg + gv * be_disc(rr + 1) 415 rr = rr + 1 416 } 417 } 418 // ---- seq1494 RERANK ARM: re-order the SAME BM25 shortlist by PPMI late-interaction ---- 419 // Identical qrels, identical idcg, identical candidate pool -> the ONLY difference is 420 // the ordering function, which is what makes this a controlled comparison. 421 var dcg_rr: i64 = dcg 422 if rr_ok == 1 { if nqt > 0 { 423 // same (buf,len) fix on the query side: the query text is dbuf[tb+1 .. e3) 424 let nqi: i64 = ppl_tokenize_ids(PG, ((dbuf as i64) + tb + 1) as *u8, e3 - tb - 1, qids, BE_IDCAP) 425 var ci: i64 = 0 426 while ci < BE_KCAND { 427 cscore[ci] = 0 - 1 428 cused[ci] = 0 as u8 429 let dpc: i64 = top[ci] 430 if dpc >= 0 { 431 let dsx: i64 = doc_ts[dpc] 432 let dex: i64 = doc_te[dpc] 433 // BUGFIX (caught by an EMPTY result file, not by a passing test): this took 434 // (buf, len) but was handed (cbuf, dex) -- the corpus BASE and an ABSOLUTE 435 // END offset -- so every candidate re-tokenised the WHOLE corpus from byte 0. 436 // 323 queries x 50 candidates x 3633 docs never finished. Pass the doc's own 437 // span. ★a silent non-finish is a LOUDER bug than a wrong number. 438 let ndi: i64 = ppl_tokenize_ids(PG, ((cbuf as i64) + dsx) as *u8, dex - dsx, dids, BE_IDCAP) 439 cscore[ci] = ppl_maxsim_idf(PG, qids, nqi, dids, ndi) 440 } 441 ci = ci + 1 442 } 443 var dcg2: i64 = 0 444 var rk: i64 = 0 445 while rk < 10 { 446 var bi: i64 = 0 - 1 447 var cj: i64 = 0 448 while cj < BE_KCAND { 449 if cused[cj] == (0 as u8) { if top[cj] >= 0 { 450 if bi < 0 { bi = cj } else { if cscore[cj] > cscore[bi] { bi = cj } } 451 } } 452 cj = cj + 1 453 } 454 if bi >= 0 { 455 cused[bi] = 1 as u8 456 let dpk2: i64 = top[bi] 457 var gv2: i64 = 0 458 var jj3: i64 = jlo 459 while jj3 <= jhi { 460 if j_doc[jidx[jj3]] == dpk2 { gv2 = j_scr[jidx[jj3]]; jj3 = jhi + 1 } else { jj3 = jj3 + 1 } 461 } 462 dcg2 = dcg2 + gv2 * be_disc(rk + 1) 463 } 464 rk = rk + 1 465 } 466 dcg_rr = dcg2 467 } } 468 if dcg_rr > dcg { q_improved = q_improved + 1 } else { if dcg_rr < dcg { q_worsened = q_worsened + 1 } else { q_same = q_same + 1 } } 469 sum_ndcg_rr = sum_ndcg_rr + (dcg_rr * 1000) / idcg 470 sum_ndcg = sum_ndcg + (dcg * 1000) / idcg 471 nq = nq + 1 472 } 473 } 474 } 475 qpp = e3 + 1 476 } 477 478 var mean: i64 = 0 479 if nq > 0 { mean = sum_ndcg / nq } 480 db_w("{\"tool\":\"nx_beir_eval\",\"dataset\":\"BEIR/nfcorpus\",\"docs\":" as *u8); db_n(nd) 481 db_w(",\"vocab\":" as *u8); db_n(nv) 482 db_w(",\"test_queries_scored\":" as *u8); db_n(nq) 483 db_w(",\"dropped_no_qrels\":" as *u8); db_n(n_noqrel) 484 db_w(",\"dropped_no_relevant\":" as *u8); db_n(n_noidcg) 485 db_w(",\"counted_zero_no_query_term_match\":" as *u8); db_n(n_noterm) 486 db_w(",\"avgdl\":" as *u8); db_n(avgdl) 487 db_w(",\"bm25\":{\"k1x1000\":900,\"bx1000\":400,\"idf\":\"log2\",\"stemming\":false,\"stopwords\":false}" as *u8) 488 db_w(",\"ndcg_at_10_permil\":" as *u8); db_n(mean) 489 db_w(",\"published_bm25_baseline_permil\":320" as *u8) 490 // seq1494: the rerank arm, printed from the SAME run. UNAVAILABLE (not 0) when the model is absent. 491 var mean_rr: i64 = 0 492 if nq > 0 { mean_rr = sum_ndcg_rr / nq } 493 db_w(",\"rerank\":{\"model\":" as *u8) 494 if rr_ok == 1 { db_w("\"semppmi_v1 late-interaction maxsim(idf)\"" as *u8) } else { db_w("\"UNAVAILABLE -- semppmi_v1.bin did not load; rerank arm NOT measured (this is not a score of 0)\"" as *u8) } 495 db_w(",\"candidates\":" as *u8); db_n(BE_KCAND) 496 db_w(",\"ndcg_at_10_permil\":" as *u8); db_n(mean_rr) 497 db_w(",\"delta_permil\":" as *u8); db_n(mean_rr - mean) 498 db_w(",\"queries_improved\":" as *u8); db_n(q_improved) 499 db_w(",\"queries_worsened\":" as *u8); db_n(q_worsened) 500 db_w(",\"queries_unchanged\":" as *u8); db_n(q_same) 501 db_w(",\"accept_rule\":\"declared BEFORE the run: rerank must EXCEED the BM25 arm on these same queries or it does NOT get wired into search (seq1494)\"}" as *u8) 502 db_w(",\"note\":\"real external ground truth; integer/bit-reproducible; no-stem/no-stopword v1 (gap to the 0.32 stemmed baseline = the stemming rung)\"}" as *u8) 503 db_w("\n" as *u8) 504 return 0 505}