code wiki / (root) / nx_recall_fuse.nx

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}