nx_recall_fuse.nx source
↩ module page · 57 lines · 2494 B
1// nx_recall_fuse.nx -- hybrid rank fusion for the recall pipeline: RRF-combine the
2// BM25 ranking and the dense (PPMI-SVD / nx_cosine) ranking into ONE fused order.
3//
4// module: nishi-core.search.recall_fuse
5// depends: fx.nx, nx_rrf.nx, syscalls.nx
6// capability: CORE_COMPUTE
7// wired_status: LIBRARY (daemon wiring into dss_search_off_div = R1b, claims-coordinated)
8// genealogy_id: cormack_clarke_buettcher_2009_rrf (two-signal hybrid fusion)
9//
10// WHY (debt-eating, good form): the live SERP ranks by BM25 (+PageRank) ONLY -- the
11// dense-kNN / PPMI-SVD embeddings and nx_rrf are BUILT but UNWIRED into search. That
12// is our own debt. This organ wires them: given the BM25 rank-order and the dense
13// rank-order over a shortlist, it produces the RRF-fused order. It is built + gated
14// + MEASURED on the nx_ir_eval ruler OFFLINE first (no crown-jewel docportal edit
15// until the serve path is claims-clear), so the lift is a number, not a promise.
16
17import "fx.nx"
18import "nx_rrf.nx"
19import "syscalls.nx"
20
21// Fuse two rank-orders over `ncand` candidate ids into RRF scores (Q16.16).
22// bm25_order[0..nb) and dense_order[0..nd) hold candidate ids in rank order (rank 1
23// = index 0). scores_out[ncand] is zeroed then accumulated. Parameter-free (k=60),
24// monotone, integer-exact -- and SEO/keyword-stuffing resistant: a doc must rank in
25// BOTH signals to top the fused list.
26func rf_fuse(bm25_order: *i64, nb: i64, dense_order: *i64, nd: i64, ncand: i64, scores_out: *i64) -> i64 {
27 var i: i64 = 0
28 while i < ncand { scores_out[i] = 0; i = i + 1 }
29 nx_rrf_add(bm25_order, nb, scores_out)
30 nx_rrf_add(dense_order, nd, scores_out)
31 return 0
32}
33
34// Produce the fused ORDER: candidate ids sorted by fused score DESC (ties -> lower
35// id). out_order[ncand] receives the ranked ids. Selection sort -- the shortlist is
36// small (post BM25 stage-2), so O(n^2) is fine and deterministic.
37func rf_order_by_score(scores: *i64, ncand: i64, out_order: *i64) -> i64 {
38 let used: *u8 = sys_mmap(ncand)
39 var i: i64 = 0
40 while i < ncand { used[i] = 0 as u8; i = i + 1 }
41 var r: i64 = 0
42 while r < ncand {
43 var best: i64 = 0 - 1
44 var j: i64 = 0
45 while j < ncand {
46 if used[j] == (0 as u8) {
47 if best < 0 { best = j }
48 else { if scores[j] > scores[best] { best = j } }
49 }
50 j = j + 1
51 }
52 out_order[r] = best
53 used[best] = 1 as u8
54 r = r + 1
55 }
56 return 0
57}