code wiki / _hdl_build / nx_estate_search_lib.nx
nx_estate_search_lib.nx source
↩ module page · 363 lines · 19514 B
1// nx_estate_search_lib.nx -- THE ESTATE QUERY, as a library so a gate drives it in-process on a fixture shard
2// (search rungs E1/E1b/E4, 2026-09-14). es_query ranks with dss_search (the SAME scorer the public search runs: one
3// ruler, two corpora) and renders one line per hit into a caller buffer: rank TAB score TAB url (the url row,
4// else doc:<cid>) TAB snippet (the first ES_SNIPPET bytes of the document with control bytes folded to spaces).
5// It returns the hit count, or ES_UNREADABLE when the shard cannot be opened -- an absent shard and an empty one
6// are different facts and only one of them is a defect.
7// E1b DEFINITION PRECEDENCE: an underscored term is also asked as its definition token (ss_def_token, the token
8// the tree ingest writes once per function head), and the hits are re-ordered by a TIER before the score -- a
9// document carrying a typed term's definition token ranks above one that only mentions the term, whatever the
10// BM25 magnitude (es_def_precedence, the shape of dss_exact_precedence: a lexicographic key needs no coefficient,
11// and a discount constant here would be a magic number nobody could justify). A one-word function name has no
12// definition tier -- a NAMED residual, the same condition ss_ident_collapse already draws. license_tier: ORIGINAL
13import "nx_docportal_search_serve.nx"
14import "nx_uxf_cid.nx" // E8: the search answer's machine envelope is a UXF profile document (canonical bytes + profiled CID)
15
16const ES_DOMAIN: *u8 = "estate"
17const ES_TOP_DEFAULT: i64 = 10
18const ES_TOP_MAX: i64 = 50
19const ES_QBUF: i64 = 1024
20const ES_SNIPPET: i64 = 160
21const ES_PREFIXBUF: i64 = 512
22const ES_KEYBUF: i64 = 64
23const ES_BOX: i64 = 16
24const ES_WORD: i64 = 8
25const ES_NUMBUF: i64 = 28
26const ES_TAB: i64 = 9
27const ES_LF: i64 = 10
28const ES_SPACE: i64 = 32
29const ES_DIGIT0: i64 = 48
30const ES_DIGIT9: i64 = 57
31const ES_MMAP_OPEN: i64 = 1 // ss_open2 mode: mmap the segments, never read a multi-GB shard into anon RAM
32const ES_UNREADABLE: i64 = 0 - 3 // es_query: the shard could not be opened (the program exits 3 on it)
33const ES_LINE_RESERVE: i64 = 256 // per hit: rank, score, tabs, doc:<cid> and the newline beside the url and snippet
34const ES_DEFOFF: i64 = DSS_MAXTERMS // at most one definition token per query term the scorer will read
35const ES_LOOKUP_TERMS: i64 = 1 // a one-term query is a LOOKUP: it also asks for that term's definition token (a rung id, a bare name); prose has more
36
37func es_slen(s: *u8) -> i64 { var n: i64 = 0; while s[n] != (0 as u8) { n = n + 1 } return n }
38func es_cat(d: *u8, o: i64, s: *u8) -> i64 { var i: i64 = 0; while s[i] != (0 as u8) { d[o + i] = s[i]; i = i + 1 } return o + i }
39func es_catn(d: *u8, o: i64, v: i64) -> i64 {
40 var m: i64 = v
41 var oo: i64 = o
42 if m < 0 { d[oo] = 45 as u8; oo = oo + 1; m = 0 - m }
43 if m == 0 { d[oo] = ES_DIGIT0 as u8; return oo + 1 }
44 var pw: i64 = 1
45 while m / pw >= 10 { pw = pw * 10 }
46 while pw > 0 { d[oo] = (ES_DIGIT0 + (m / pw) % 10) as u8; oo = oo + 1; pw = pw / 10 }
47 return oo
48}
49func es_starts(a: *u8, p: *u8) -> i64 { var i: i64 = 0; while p[i] != (0 as u8) { if a[i] != p[i] { return 0 } i = i + 1 } return 1 }
50func es_atoi(s: *u8) -> i64 {
51 var v: i64 = 0
52 var i: i64 = 0
53 while s[i] != (0 as u8) { let c: i64 = s[i] as i64; if c >= ES_DIGIT0 { if c <= ES_DIGIT9 { v = v * 10 + (c - ES_DIGIT0) } } i = i + 1 }
54 return v
55}
56// first occurrence of a NUL-terminated needle in hay[0..n), -1 when absent (the gate's own witness)
57func es_find(hay: *u8, n: i64, needle: *u8) -> i64 {
58 let nl: i64 = es_slen(needle)
59 if nl == 0 { return 0 }
60 var i: i64 = 0
61 while i + nl <= n {
62 var j: i64 = 0
63 var hit: i64 = 1
64 while j < nl { if hay[i + j] != needle[j] { hit = 0; j = nl } else { j = j + 1 } }
65 if hit == 1 { return i }
66 i = i + 1
67 }
68 return 0 - 1
69}
70// E1b: does tok (NUL-terminated) occur in doc[0..dn) as a whole word, case-folded the way the tokenizer folds? The
71// definition anchors sit in the document's anchor tail as space-separated tokens, so the boundary test is the
72// identifier alphabet (ss_is_ident_byte). A linear scan over the mapped document, bounded by ES_TOP_MAX hits.
73func es_has_word(doc: *u8, dn: i64, tok: *u8) -> i64 {
74 let tl: i64 = es_slen(tok)
75 if tl == 0 { return 0 }
76 var i: i64 = 0
77 while i + tl <= dn {
78 var j: i64 = 0
79 var hit: i64 = 1
80 while j < tl { if dss_lower(doc[i + j] as i64) != dss_lower(tok[j] as i64) { hit = 0; j = tl } else { j = j + 1 } }
81 if hit == 1 {
82 var lb: i64 = 1
83 if i > 0 { if ss_is_ident_byte(doc[i - 1] as i64) == 1 { lb = 0 } }
84 var rb: i64 = 1
85 if i + tl < dn { if ss_is_ident_byte(doc[i + tl] as i64) == 1 { rb = 0 } }
86 if lb == 1 { if rb == 1 { return 1 } }
87 }
88 i = i + 1
89 }
90 return 0
91}
92// E1b CONTRACT (es_def_precedence): a hit carrying more of the query's definition tokens ranks first; equal tiers
93// fall through to the score. A TIER, not a coefficient (the shape of dss_exact_precedence). Returns 1 when a wins.
94func es_def_precedence(defa: i64, sca: i64, defb: i64, scb: i64) -> i64 {
95 if defa > defb { return 1 }
96 if defa < defb { return 0 }
97 if sca > scb { return 1 }
98 return 0
99}
100
101// THE QUERY. Renders up to `top` hits into out[0..cap) and stores the byte count in olen[0]; returns the hit count
102// or ES_UNREADABLE. segs_out[0] receives the shard's segment count for the caller's summary line.
103func es_query(domain: *u8, q: *u8, qn: i64, top: i64, out: *u8, cap: i64, olen: *i64, segs_out: *i64) -> i64 {
104 olen[0] = 0
105 segs_out[0] = 0
106 var t: i64 = top
107 if t < 1 { t = 1 }
108 if t > ES_TOP_MAX { t = ES_TOP_MAX }
109 let prefix: *u8 = sys_mmap(ES_PREFIXBUF)
110 dss_prefix(domain, prefix)
111 // ABSENT and EMPTY are different facts: a shard nobody ingested has no manifest and is UNREADABLE (the caller
112 // names the ingest); a shard with a manifest and no segments is empty and answers zero hits honestly.
113 let mfp: *u8 = sys_mmap(ES_PREFIXBUF)
114 var mo: i64 = es_cat(mfp, 0, prefix)
115 mo = es_cat(mfp, mo, "manifest.txt" as *u8)
116 mfp[mo] = 0 as u8
117 let mfd: i64 = sys_openat_rd(mfp)
118 if mfd < 0 { return ES_UNREADABLE }
119 sys_close(mfd)
120 let h: *i64 = ss_open2(prefix, ES_MMAP_OPEN)
121 if (h as i64) == 0 { return ES_UNREADABLE }
122 segs_out[0] = h[0]
123 let cids: *i64 = sys_mmap(ES_TOP_MAX * ES_WORD) as *i64
124 let scores: *i64 = sys_mmap(ES_TOP_MAX * ES_WORD) as *i64
125 // IDENTIFIER EXPANSION (E1) + DEFINITION TOKENS (E1b): a query term carrying an underscore is also asked in its
126 // collapsed form (the anchor every source file carries for every identifier it mentions) and as its definition
127 // token (the anchor a file carries only for the functions it declares). Same derivation both sides:
128 // ss_ident_collapse and ss_def_token. The expansion stops at the scorer's own term cap (DSS_MAXTERMS) so nothing
129 // past it is dropped silently by the scorer; the typed terms always come first.
130 // the expanded query is at most the typed bytes three times over (typed, collapsed, definition) plus a prefix and a
131 // separator per term the scorer will read -- derived from the input, never a guessed ceiling
132 let pfxlen: i64 = es_slen(SS_DEF_PREFIX)
133 let q2cap: i64 = qn + qn + qn + DSS_MAXTERMS * (pfxlen + 1 + 1) + 1
134 let q2: *u8 = sys_mmap(q2cap)
135 var qn2: i64 = 0
136 var qi: i64 = 0
137 var nterms: i64 = 0
138 if qn > 0 { nterms = 1 }
139 while qi < qn {
140 q2[qn2] = q[qi]
141 qn2 = qn2 + 1
142 if (q[qi] as i64) == ES_SPACE { nterms = nterms + 1 }
143 qi = qi + 1
144 }
145 let termcap: i64 = qn + pfxlen + 1 // one typed term, its collapsed twin or its definition token: bounded by the query itself
146 let term: *u8 = sys_mmap(termcap)
147 let coll: *u8 = sys_mmap(termcap)
148 let defcap: i64 = qn + DSS_MAXTERMS * (pfxlen + 1) + 1 // every definition token NUL-separated: derived from the query
149 let defs: *u8 = sys_mmap(defcap)
150 let defoff: *i64 = sys_mmap(ES_DEFOFF * ES_WORD) as *i64
151 var ndef: i64 = 0
152 var dfo: i64 = 0
153 let nterms0: i64 = nterms // the TYPED term count, before any expansion: one term is a lookup
154 var ts: i64 = 0
155 var tp: i64 = 0
156 while tp <= qn {
157 var atend: i64 = 0
158 if tp == qn { atend = 1 } else { if (q[tp] as i64) == ES_SPACE { atend = 1 } }
159 if atend == 1 {
160 let tl: i64 = tp - ts
161 if tl > 0 { if tl < termcap {
162 var k: i64 = 0
163 while k < tl { term[k] = q[ts + k]; k = k + 1 }
164 term[tl] = 0 as u8
165 let cl: i64 = ss_ident_collapse(term, coll, termcap)
166 if cl > 0 { if nterms < DSS_MAXTERMS { if qn2 + 1 + cl < q2cap {
167 q2[qn2] = ES_SPACE as u8; qn2 = qn2 + 1
168 var m: i64 = 0
169 while m < cl { q2[qn2] = coll[m]; qn2 = qn2 + 1; m = m + 1 }
170 nterms = nterms + 1
171 } } }
172 // the definition token is asked for an underscored identifier, and for the one term of a LOOKUP query
173 // (a rung id, a bare name): a multi-term prose query is left exactly as typed
174 var wantdef: i64 = 0
175 if cl > 0 { wantdef = 1 }
176 if nterms0 == ES_LOOKUP_TERMS { wantdef = 1 }
177 if wantdef == 1 {
178 let dfl: i64 = ss_def_token(term, tl, coll, termcap)
179 if nterms < DSS_MAXTERMS { if ndef < ES_DEFOFF { if dfo + dfl + 1 < defcap { if qn2 + 1 + dfl < q2cap {
180 q2[qn2] = ES_SPACE as u8; qn2 = qn2 + 1
181 defoff[ndef] = dfo
182 var m2: i64 = 0
183 while m2 < dfl { q2[qn2] = coll[m2]; qn2 = qn2 + 1; defs[dfo] = coll[m2]; dfo = dfo + 1; m2 = m2 + 1 }
184 defs[dfo] = 0 as u8; dfo = dfo + 1
185 ndef = ndef + 1
186 nterms = nterms + 1
187 } } } }
188 }
189 } }
190 ts = tp + 1
191 }
192 tp = tp + 1
193 }
194 q2[qn2] = 0 as u8
195 // E1b: with definition tokens in play the scorer is asked for its full window (ES_TOP_MAX) so the tier has room
196 // to lift a definer the caller's `top` would have cut; without them the fetch is exactly `top`, byte-identical to E1.
197 var fetch: i64 = t
198 if ndef > 0 { fetch = ES_TOP_MAX }
199 var n: i64 = dss_search(domain, q2, qn2, cids, scores, fetch)
200 let key: *u8 = sys_mmap(ES_KEYBUF)
201 let ukey: *u8 = sys_mmap(ES_KEYBUF)
202 let dp: *i64 = sys_mmap(ES_BOX) as *i64
203 let dl: *i64 = sys_mmap(ES_BOX) as *i64
204 if ndef > 0 {
205 let dtier: *i64 = sys_mmap(ES_TOP_MAX * ES_WORD) as *i64
206 var ti: i64 = 0
207 while ti < n {
208 dtier[ti] = 0
209 dss_mkkey(cids[ti], key)
210 if ss_hget(h, key, dp, dl) == 1 {
211 var di: i64 = 0
212 while di < ndef { if es_has_word(dp[0] as *u8, dl[0], (defs as i64 + defoff[di]) as *u8) == 1 { dtier[ti] = dtier[ti] + 1 } di = di + 1 }
213 }
214 ti = ti + 1
215 }
216 // selection order by the tier then the score: at most ES_TOP_MAX hits, so the square is small
217 var s: i64 = 0
218 while s < n {
219 var best: i64 = s
220 var j: i64 = s + 1
221 while j < n { if es_def_precedence(dtier[j], scores[j], dtier[best], scores[best]) == 1 { best = j } j = j + 1 }
222 if best != s {
223 let tc: i64 = cids[s]; cids[s] = cids[best]; cids[best] = tc
224 let tsc: i64 = scores[s]; scores[s] = scores[best]; scores[best] = tsc
225 let tt: i64 = dtier[s]; dtier[s] = dtier[best]; dtier[best] = tt
226 }
227 s = s + 1
228 }
229 if n > t { n = t }
230 }
231 var o: i64 = 0
232 var i: i64 = 0
233 while i < n {
234 if o + ES_LINE_RESERVE + ES_SNIPPET + ES_QBUF >= cap { i = n } else {
235 o = es_catn(out, o, i + 1); out[o] = ES_TAB as u8; o = o + 1
236 o = es_catn(out, o, scores[i]); out[o] = ES_TAB as u8; o = o + 1
237 dsv_mkurlkey(cids[i], ukey)
238 var haveurl: i64 = 0
239 if ss_hget(h, ukey, dp, dl) == 1 {
240 let up: *u8 = dp[0] as *u8
241 var ul: i64 = dl[0]
242 if ul > ES_QBUF { ul = ES_QBUF }
243 var k: i64 = 0
244 while k < ul { out[o] = up[k]; o = o + 1; k = k + 1 }
245 haveurl = 1
246 }
247 if haveurl == 0 { o = es_cat(out, o, "doc:" as *u8); o = es_catn(out, o, cids[i]) }
248 out[o] = ES_TAB as u8; o = o + 1
249 dss_mkkey(cids[i], key)
250 if ss_hget(h, key, dp, dl) == 1 {
251 let dt: *u8 = dp[0] as *u8
252 var m: i64 = dl[0]
253 if m > ES_SNIPPET { m = ES_SNIPPET }
254 var j: i64 = 0
255 while j < m { var c: i64 = dt[j] as i64; if c < ES_SPACE { c = ES_SPACE } out[o] = c as u8; o = o + 1; j = j + 1 }
256 }
257 out[o] = ES_LF as u8; o = o + 1
258 i = i + 1
259 }
260 }
261 olen[0] = o
262 return n
263}
264// the positional summary line, written LAST by the program: ESTATE-SEARCH q=... hits=... top=... shard=... segments=...
265func es_summary(out: *u8, o0: i64, q: *u8, hits: i64, top: i64, prefix: *u8, segs: i64) -> i64 {
266 var o: i64 = es_cat(out, o0, "ESTATE-SEARCH q=" as *u8)
267 o = es_cat(out, o, q)
268 o = es_cat(out, o, " hits=" as *u8); o = es_catn(out, o, hits)
269 o = es_cat(out, o, " top=" as *u8); o = es_catn(out, o, top)
270 o = es_cat(out, o, " shard=" as *u8); o = es_cat(out, o, prefix)
271 o = es_cat(out, o, " segments=" as *u8); o = es_catn(out, o, segs)
272 out[o] = ES_LF as u8
273 return o + 1
274}
275
276// ---- E8: THE MACHINE ENVELOPE (search rung E8, 2026-09-14) -------------------------------------------------------
277// The answer above is text for a reader. The envelope below is the SAME answer as a UXF profile document: the fields
278// of the summary line plus one field per hit column, canon-encoded (NXR1, key-sorted, length-prefixed, no escaping)
279// and content-addressed under profile UXF_SEARCH. Two queries over the same shard state that render the same text
280// carry the same CID; one changed byte anywhere changes it. JSON and the TAB text are projections of these bytes,
281// never the other way round. Field keys: q hits top shard segments and hit.<i>.rank/score/url/snippet (i from 1).
282const ES_UXF_HEAD_FIELDS: i64 = 5 // q hits top shard segments
283const ES_UXF_HIT_FIELDS: i64 = 4 // rank score url snippet
284const ES_UXF_HIT_COLS: i64 = ES_UXF_HIT_FIELDS - 1 // TAB separators per rendered hit line
285const ES_UXF_KEYLEN: i64 = 24 // "hit." + up to two digits of ES_TOP_MAX + ".snippet" + NUL, the longest key
286
287// one NUL-terminated copy of src[0..n) in fresh memory (canon_encode reads NUL-terminated strings; the rendered text has none)
288func es_uxf_dup(src: *u8, n: i64) -> *u8 {
289 let d: *u8 = sys_mmap(n + 1)
290 var i: i64 = 0
291 while i < n { d[i] = src[i]; i = i + 1 }
292 d[n] = 0 as u8
293 return d
294}
295
296// key "hit.<i>.<col>" into a fresh buffer; returns its pointer
297func es_uxf_hitkey(i: i64, col: *u8) -> *u8 {
298 let k: *u8 = sys_mmap(ES_UXF_KEYLEN)
299 var o: i64 = es_cat(k, 0, "hit." as *u8)
300 o = es_catn(k, o, i)
301 k[o] = 46 as u8; o = o + 1 // '.'
302 o = es_cat(k, o, col)
303 k[o] = 0 as u8
304 return k
305}
306
307// Build the envelope from the rendered text (rendered[0..rn), one hit per line, columns TAB-separated) and the summary
308// values. Writes the canonical bytes to canon_out (which MUST be sized by es_uxf_envelope_size on the same inputs, or
309// pass 0 and read only the size), the profiled CID to cid (UXF_CID_LEN + 1 bytes). Returns the canonical byte length,
310// fields_out[0] = the field count. A rendered line with fewer than ES_UXF_HIT_COLS tabs is malformed and returns -1:
311// the envelope refuses to describe an answer it cannot parse, it never encodes a partial one.
312func es_uxf_envelope(rendered: *u8, rn: i64, q: *u8, hits: i64, top: i64, prefix: *u8, segs: i64, canon_out: *u8, cid: *u8, fields_out: *i64) -> i64 {
313 let maxf: i64 = ES_UXF_HEAD_FIELDS + hits * ES_UXF_HIT_FIELDS
314 let keys: *i64 = sys_mmap(maxf * ES_WORD) as *i64
315 let vals: *i64 = sys_mmap(maxf * ES_WORD) as *i64
316 var nf: i64 = 0
317 let nb: *u8 = sys_mmap(ES_NUMBUF)
318 keys[nf] = "q" as *u8 as i64; vals[nf] = q as i64; nf = nf + 1
319 var l: i64 = es_catn(nb, 0, hits); keys[nf] = "hits" as *u8 as i64; vals[nf] = es_uxf_dup(nb, l) as i64; nf = nf + 1
320 l = es_catn(nb, 0, top); keys[nf] = "top" as *u8 as i64; vals[nf] = es_uxf_dup(nb, l) as i64; nf = nf + 1
321 keys[nf] = "shard" as *u8 as i64; vals[nf] = prefix as i64; nf = nf + 1
322 l = es_catn(nb, 0, segs); keys[nf] = "segments" as *u8 as i64; vals[nf] = es_uxf_dup(nb, l) as i64; nf = nf + 1
323 var p: i64 = 0
324 var hi: i64 = 1
325 while p < rn {
326 if hi > hits { return 0 - 1 } // more lines than hits: the text and the count disagree, refuse
327 // the four columns of this line: rank score url snippet; the snippet runs to the newline and may hold no tab
328 let cs: *i64 = sys_mmap((ES_UXF_HIT_FIELDS + 1) * ES_WORD) as *i64 // column starts, cs[4] = line end
329 cs[0] = p
330 var col: i64 = 1
331 var eol: i64 = 0 // the line's newline was seen (an explicit flag: the cursor p is never written as a sentinel)
332 var go: i64 = 1
333 while go == 1 {
334 if p >= rn { go = 0 }
335 else {
336 let c: u8 = rendered[p]
337 p = p + 1
338 if c == (ES_LF as u8) { cs[ES_UXF_HIT_FIELDS] = p - 1; eol = 1; go = 0 }
339 else { if col < ES_UXF_HIT_FIELDS { if c == (ES_TAB as u8) { cs[col] = p; col = col + 1 } } }
340 }
341 }
342 if eol == 0 { cs[ES_UXF_HIT_FIELDS] = rn } // the last line may end at the buffer instead of a newline
343 if col < ES_UXF_HIT_FIELDS { return 0 - 1 } // fewer than three tabs before the line ended
344 keys[nf] = es_uxf_hitkey(hi, "rank" as *u8) as i64; vals[nf] = es_uxf_dup((rendered as i64 + cs[0]) as *u8, cs[1] - 1 - cs[0]) as i64; nf = nf + 1
345 keys[nf] = es_uxf_hitkey(hi, "score" as *u8) as i64; vals[nf] = es_uxf_dup((rendered as i64 + cs[1]) as *u8, cs[2] - 1 - cs[1]) as i64; nf = nf + 1
346 keys[nf] = es_uxf_hitkey(hi, "url" as *u8) as i64; vals[nf] = es_uxf_dup((rendered as i64 + cs[2]) as *u8, cs[3] - 1 - cs[2]) as i64; nf = nf + 1
347 keys[nf] = es_uxf_hitkey(hi, "snippet" as *u8) as i64; vals[nf] = es_uxf_dup((rendered as i64 + cs[3]) as *u8, cs[ES_UXF_HIT_FIELDS] - cs[3]) as i64; nf = nf + 1
348 hi = hi + 1
349 }
350 if hi - 1 != hits { return 0 - 1 } // fewer lines than hits: same disagreement, same refusal
351 fields_out[0] = nf
352 let need: i64 = canon_encode_size(keys, vals, nf)
353 if (canon_out as i64) == 0 { return need }
354 let got: i64 = canon_encode(keys, vals, nf, canon_out)
355 uxf_cid_profiled(UXF_SEARCH, canon_out, got, cid)
356 return got
357}
358
359// the exact canonical size for these inputs (the caller sizes its buffer from this, never from a constant)
360func es_uxf_envelope_size(rendered: *u8, rn: i64, q: *u8, hits: i64, top: i64, prefix: *u8, segs: i64) -> i64 {
361 let fb: *i64 = sys_mmap(ES_BOX) as *i64
362 return es_uxf_envelope(rendered, rn, q, hits, top, prefix, segs, 0 as *u8, 0 as *u8, fb)
363}