code wiki / _hdl_build / nx_search_cli.nx

nx_search_cli.nx source

↩ module page · 179 lines · 7913 B

1// nx_search_cli.nx -- ASSIGNMENT E (tutored): the search engine's FRONT DOOR over the DURABLE index. The 2// harvest (nx_library_harvest_v2) proved build->persist; until now nothing user-facing QUERIED that artifact -- 3// the index was provably correct and practically dark. This is the live path: argv terms -> load 4// knowledge/index/library.idx (NO rebuild: build-once-query-forever) -> inverted-index postings shortlist the 5// candidate docids -> docmap names their files -> the Engineer-wired bm_* BM25 ranker (x1e6 fixed-point, the 6// _hdl_build nx_bm25.nx that shadows runtime's on this lane) scores them -> "score path" per hit. Distinct 7// from nx_library_search.nx (the Researcher's IN-MEMORY corpus ranker; no main, no 8// persistence) -- one capability per module. BM25 stats (avgdl/IDF) are computed over the CANDIDATE set; 9// corpus-wide stats stored at harvest = the next rung when the corpus outgrows reading candidates whole. 10// Usage: nx_search_cli.sov.elf <term> [term ...] license_tier: ORIGINAL 11import "nx_search_inverted_persist.nx" 12import "nx_itoa_lib.nx" // shared MSB-first emitter (zero-alloc) 13import "nx_bm25.nx" 14const LS_MAGIC_1000000: i64 = 1000000 15const LS_MAGIC_10000: i64 = 10000 16 17const LS_MAX_DOCS: i64 = 4096 18const LS_MAX_TERMS: i64 = 8 19const LS_ROWID_CAP: i64 = 1024 20const LS_TOP_K: i64 = 10 21 22func sc_puts(s: *u8) -> i64 { var n: i64=0; while s[n]!=(0 as u8){n=n+1} sys_write(1,s,n); return 0 } 23// MIGRATED to the shared emitter (debt 1785563586). The old body mmapped a scratch buffer 24// per call and never freed it. At PAGE granularity that is 4096B leaked PER CALL -- the 25// defect that took 28.5GB of a 36GB host in nx_ts_lumadiff (2MB input, ~3.66M calls). 26// nxi_* is MSB-first, allocates NOTHING, and emits identical bytes including the sign. 27func sc_num(v: i64) -> i64 { nxi_out(v); return 0 } 28func sc_strlen(s: *u8) -> i64 { var n: i64=0; while s[n]!=(0 as u8){n=n+1} return n } 29 30// fold A-Z -> a-z in place. The INDEX hashes lowercased tokens but the bm_* tf/df scorer is a case- 31// sensitive substring count -- so cased occurrences ("PagedAttention", "Tantivy") scored 0 and ranking 32// broke into 0.00 ties (caught by the quality gate's pagedattention row). Folding BOTH the query terms 33// and the candidate doc text at the boundary makes scoring agree with the index's own semantics. 34func sc_fold(s: *u8, n: i64) -> i64 { 35 var i: i64 = 0 36 while i < n { 37 let c: i64 = s[i] as i64 38 if c >= 65 { if c <= 90 { s[i] = (c + 32) as u8 } } 39 i = i + 1 40 } 41 return 0 42} 43 44// print an x1e6 fixed-point score as [-]int.dd 45func sc_score(v: i64) -> i64 { 46 var m: i64 = v 47 if m < 0 { sys_write(1, "-" as *u8, 1); m = 0 - m } 48 sc_num(m / LS_MAGIC_1000000) 49 sys_write(1, "." as *u8, 1) 50 let frac: i64 = (m % LS_MAGIC_1000000) / LS_MAGIC_10000 51 if frac < 10 { sys_write(1, "0" as *u8, 1) } 52 sc_num(frac) 53 return 0 54} 55 56// parse the docmap (one path per line, line i = docid i) IN PLACE: \n -> NUL, record line starts. 57// Returns ndocs. 58func sc_load_docmap(path: *u8, paths_out: *i64, cap: i64) -> i64 { 59 let lenbox: *i64 = sys_mmap(16) as *i64 60 let buf: *u8 = sys_read_file(path, lenbox) 61 if buf == 0 as *u8 { return 0 - 1 } 62 let total: i64 = lenbox[0] 63 var nd: i64 = 0 64 var start: i64 = 0 65 var i: i64 = 0 66 while i < total { 67 if buf[i] == (10 as u8) { 68 buf[i] = 0 as u8 69 if i > start { if nd < cap { paths_out[nd] = (buf as i64) + start; nd = nd + 1 } } 70 start = i + 1 71 } 72 i = i + 1 73 } 74 return nd 75} 76 77func main(argc: i64, argv: *i64) -> i64 { 78 if argc < 2 { 79 sc_puts("usage: nx_search_cli <term> [term ...] (queries knowledge/index/library.idx)\n" as *u8) 80 sys_exit(2); return 2 81 } 82 var nterms: i64 = argc - 1 83 if nterms > LS_MAX_TERMS { nterms = LS_MAX_TERMS } 84 85 // ---- load the DURABLE artifacts (no rebuild: this is the build-once-query-forever path) ---- 86 let idx: *NxInvIndex = nx_inv_load("knowledge/index/library.idx" as *u8) 87 if idx == 0 as *NxInvIndex { sc_puts("SEARCH-FAIL: no index (run nx_library_harvest_v2 first)\n" as *u8); sys_exit(1); return 1 } 88 let paths: *i64 = sys_mmap(8 * LS_MAX_DOCS) as *i64 89 let ndocs: i64 = sc_load_docmap("knowledge/index/library.docmap" as *u8, paths, LS_MAX_DOCS) 90 if ndocs <= 0 { sc_puts("SEARCH-FAIL: no docmap\n" as *u8); sys_exit(1); return 1 } 91 if idx.n_rows != ndocs { sc_puts("SEARCH-FAIL: index rows != docmap lines (stale pair -- re-harvest)\n" as *u8); sys_exit(1); return 1 } 92 93 // ---- candidate docids = union of each term's postings (deduped; index shortlists, BM25 ranks) ---- 94 let seen: *u8 = sys_mmap(ndocs + 8) 95 let res: *NxInvQueryResult = sys_mmap(64) as *NxInvQueryResult 96 let rowids: *i64 = sys_mmap(8 * LS_ROWID_CAP) as *i64 97 let qterms: *i64 = sys_mmap(8 * LS_MAX_TERMS) as *i64 98 let qlens: *i64 = sys_mmap(8 * LS_MAX_TERMS) as *i64 99 var t: i64 = 0 100 while t < nterms { 101 let term: *u8 = argv[1 + t] as *u8 102 qterms[t] = term as i64 103 qlens[t] = sc_strlen(term) 104 sc_fold(term, qlens[t]) 105 nx_inv_query_term(idx, term, qlens[t], rowids, LS_ROWID_CAP, res) 106 sc_puts("term '" as *u8); sc_puts(term); sc_puts("' postings=" as *u8); sc_num(res.postings_count); sc_puts("\n" as *u8) 107 var ri: i64 = 0 108 while ri < res.n_rowids_filled { 109 let rid: i64 = rowids[ri] 110 if rid >= 0 { if rid < ndocs { seen[rid] = 1 as u8 } } 111 ri = ri + 1 112 } 113 t = t + 1 114 } 115 let cand: *i64 = sys_mmap(8 * ndocs) as *i64 116 var ncand: i64 = 0 117 var di: i64 = 0 118 while di < ndocs { 119 if seen[di] == (1 as u8) { cand[ncand] = di; ncand = ncand + 1 } 120 di = di + 1 121 } 122 if ncand == 0 { 123 sc_puts("SEARCH-OK matches=0 (no document contains any query term)\n" as *u8) 124 sys_exit(0); return 0 125 } 126 127 // ---- read candidate docs whole + BM25-rank them (the Engineer-wired bm_* ranker, x1e6 scale) ---- 128 let texts: *i64 = sys_mmap(8 * ncand) as *i64 129 let tlens: *i64 = sys_mmap(8 * ncand) as *i64 130 let dls: *i64 = sys_mmap(8 * ncand) as *i64 131 let lenbox: *i64 = sys_mmap(16) as *i64 132 var total_dl: i64 = 0 133 var ci: i64 = 0 134 while ci < ncand { 135 let buf: *u8 = sys_read_file(paths[cand[ci]] as *u8, lenbox) 136 if buf == 0 as *u8 { texts[ci] = ("" as *u8) as i64; tlens[ci] = 0 } 137 else { texts[ci] = buf as i64; tlens[ci] = lenbox[0]; sc_fold(buf, lenbox[0]) } 138 dls[ci] = bm_token_count(texts[ci] as *u8, tlens[ci]) 139 total_dl = total_dl + dls[ci] 140 ci = ci + 1 141 } 142 var avgdl: i64 = total_dl / ncand 143 if avgdl <= 0 { avgdl = 1 } 144 let scores: *i64 = sys_mmap(8 * ncand) as *i64 145 ci = 0 146 while ci < ncand { 147 scores[ci] = bm_score(texts, tlens, dls, ncand, ci, avgdl, qterms, nterms) 148 ci = ci + 1 149 } 150 151 // ---- selection-sort desc on (scores, cand) in lockstep; print top K ---- 152 var a: i64 = 0 153 while a < ncand { 154 var best: i64 = a 155 var b: i64 = a + 1 156 while b < ncand { 157 if scores[b] > scores[best] { best = b } 158 b = b + 1 159 } 160 if best != a { 161 let ts: i64 = scores[a]; scores[a] = scores[best]; scores[best] = ts 162 let tc: i64 = cand[a]; cand[a] = cand[best]; cand[best] = tc 163 } 164 a = a + 1 165 } 166 var topk: i64 = ncand 167 if topk > LS_TOP_K { topk = LS_TOP_K } 168 sc_puts("SEARCH-RESULTS matches=" as *u8); sc_num(ncand) 169 sc_puts(" of docs=" as *u8); sc_num(ndocs); sc_puts("\n" as *u8) 170 var r: i64 = 0 171 while r < topk { 172 sc_puts(" #" as *u8); sc_num(r + 1) 173 sc_puts(" score=" as *u8); sc_score(scores[r]) 174 sc_puts(" " as *u8); sc_puts(paths[cand[r]] as *u8); sc_puts("\n" as *u8) 175 r = r + 1 176 } 177 sc_puts("SEARCH-OK\n" as *u8) 178 sys_exit(0); return 0 179}