code wiki / _hdl_build / nx_site_search.nx
nx_site_search.nx source
↩ module page · 130 lines · 7918 B
1// nx_site_search.nx -- the live /search ENDPOINT handler for a public site (operator: wire the live /search). PURE
2// bytes-in/bytes-out (sr_handle, the nx_status_daemon pattern -- the gate drives it in-process, no socket): on
3// GET /search?q=<terms> it loads the site's PERSISTED onsite index + manifest (the SAME nx_search_inverted_persist
4// the proven nx_onsite_search CLI uses), unions the matching docs, and renders a branded results page whose links
5// are the canonical /practice/<slug> URLs (which the data-driven pages from nx_site_pages now resolve). Composes
6// the existing search engine -- no new ranking math. Wiring it as the GET /search route in the sites daemon is the
7// deploy step. license_tier: ORIGINAL
8import "nx_search_inverted_persist.nx"
9import "nx_syscalls.nx"
10const SR_MAGIC_262144: i64 = 262144
11
12const SR_MAX_TERMS: i64 = 8
13const SR_ROWID_CAP: i64 = 16384
14
15func sr_cat(out: *u8, o: i64, s: *u8) -> i64 { var i: i64 = 0; while s[i] != (0 as u8) { out[o] = s[i]; o = o + 1; i = i + 1 } return o }
16func sr_catn(out: *u8, o: i64, v: i64) -> i64 {
17 if v == 0 { out[o] = 48 as u8; return o + 1 }
18 var m: i64 = v; if m < 0 { out[o] = 45 as u8; o = o + 1; m = 0 - m }
19 let t: *u8 = sys_mmap(24); var k: i64 = 0; while m > 0 { t[k] = (48 + (m % 10)) as u8; m = m / 10; k = k + 1 }
20 var i: i64 = 0; while i < k { out[o] = t[k - 1 - i]; o = o + 1; i = i + 1 } return o
21}
22func sr_hexnib(c: i64) -> i64 { if c >= 48 { if c <= 57 { return c - 48 } } if c >= 97 { if c <= 102 { return c - 87 } } if c >= 65 { if c <= 70 { return c - 55 } } return 0 - 1 }
23
24// extract the q= value from the request line into out (url-decoded, lowercased). returns length.
25func sr_query(req: *u8, n: i64, out: *u8, cap: i64) -> i64 {
26 // bound to the request line (first CR/LF)
27 var le: i64 = 0
28 while le < n { if req[le] == (13 as u8) { break } if req[le] == (10 as u8) { break } le = le + 1 }
29 var i: i64 = 0
30 var qs: i64 = 0 - 1
31 while i + 2 <= le { if req[i] == (113 as u8) { if req[i + 1] == (61 as u8) { qs = i + 2; i = le } } i = i + 1 } // "q="
32 if qs < 0 { out[0] = 0 as u8; return 0 }
33 var o: i64 = 0; var k: i64 = qs
34 while k < le {
35 let c: i64 = req[k] as i64
36 if c == 38 { k = le } else { if c == 32 { k = le } else { // '&' or ' ' (before HTTP/1.1) ends the value
37 if c == 43 { if o < cap - 1 { out[o] = 32 as u8; o = o + 1 } k = k + 1 } // '+' -> space
38 else { if c == 37 { // %XX
39 if k + 3 <= le { let hi: i64 = sr_hexnib(req[k + 1] as i64); let lo: i64 = sr_hexnib(req[k + 2] as i64); if hi >= 0 { if lo >= 0 { if o < cap - 1 { out[o] = ((hi << 4) | lo) as u8; o = o + 1 } } } k = k + 3 } else { k = le } }
40 else { var lc: i64 = c; if lc >= 65 { if lc <= 90 { lc = lc + 32 } } if o < cap - 1 { out[o] = lc as u8; o = o + 1 } k = k + 1 } }
41 } }
42 }
43 out[o] = 0 as u8
44 return o
45}
46
47// build a text/html 200 response with body[0..bo) into out; returns length.
48func sr_resp(out: *u8, body: *u8, bo: i64) -> i64 {
49 var o: i64 = sr_cat(out, 0, "HTTP/1.1 200 OK\r\nContent-Type: text/html; charset=utf-8\r\nX-Content-Type-Options: nosniff\r\nConnection: close\r\nContent-Length: " as *u8)
50 o = sr_catn(out, o, bo)
51 o = sr_cat(out, o, "\r\n\r\n" as *u8)
52 var i: i64 = 0; while i < bo { out[o] = body[i]; o = o + 1; i = i + 1 }
53 return o
54}
55
56// THE /search handler: request bytes in, branded results page out. Loads the site's persisted index + manifest.
57func sr_handle(req: *u8, n: i64, idx_path: *u8, manifest_path: *u8, base_url: *u8, firm: *u8, out: *u8) -> i64 {
58 let q: *u8 = sys_mmap(512)
59 let ql: i64 = sr_query(req, n, q, 512)
60 let body: *u8 = sys_mmap(SR_MAGIC_262144)
61 var bo: i64 = 0
62 bo = sr_cat(body, bo, "<!doctype html><meta charset=utf-8><meta name=viewport content=\"width=device-width,initial-scale=1\"><title>Search — " as *u8)
63 bo = sr_cat(body, bo, firm); bo = sr_cat(body, bo, "</title><style>body{font-family:-apple-system,Segoe UI,sans-serif;max-width:760px;margin:6vh auto;padding:0 20px;color:#1c2530}a{color:#0a5;text-decoration:none}li{margin:.5em 0}</style>" as *u8)
64 bo = sr_cat(body, bo, "<h1>" as *u8); bo = sr_cat(body, bo, firm); bo = sr_cat(body, bo, "</h1><form action=/search><input name=q value=\"" as *u8)
65 bo = sr_cat(body, bo, q); bo = sr_cat(body, bo, "\" placeholder=\"Search: divorce, will, injury, contract...\" style=\"width:70%;padding:9px\"> <button>Search</button></form>" as *u8)
66
67 let idx: *NxInvIndex = nx_inv_load(idx_path)
68 if idx == 0 as *NxInvIndex {
69 bo = sr_cat(body, bo, "<p>Search is being set up. Please call us for now.</p>" as *u8)
70 return sr_resp(out, body, bo)
71 }
72 // load + parse the manifest (url<TAB>title<TAB>text ; docid = line)
73 let mbox: *i64 = sys_mmap(16) as *i64
74 let mbuf: *u8 = sys_read_file(manifest_path, mbox)
75 var mn: i64 = 0; if (mbuf as i64) != 0 { mn = mbox[0] }
76 var nlines: i64 = 0; var z: i64 = 0; while z < mn { if mbuf[z] == (10 as u8) { nlines = nlines + 1 } z = z + 1 }
77 let up: *i64 = sys_mmap(8 * (nlines + 8)) as *i64; let ul: *i64 = sys_mmap(8 * (nlines + 8)) as *i64
78 let tp: *i64 = sys_mmap(8 * (nlines + 8)) as *i64; let tl: *i64 = sys_mmap(8 * (nlines + 8)) as *i64
79 var ndocs: i64 = 0; var ls: i64 = 0; var ii: i64 = 0
80 while ii < mn {
81 if mbuf[ii] == (10 as u8) {
82 let le: i64 = ii
83 var t1: i64 = 0 - 1; var t2: i64 = 0 - 1; var k: i64 = ls
84 while k < le { if mbuf[k] == (9 as u8) { if t1 < 0 { t1 = k } else { if t2 < 0 { t2 = k } } } k = k + 1 }
85 if t1 >= ls { if t2 > t1 {
86 up[ndocs] = (mbuf as i64) + ls; ul[ndocs] = t1 - ls
87 tp[ndocs] = (mbuf as i64) + t1 + 1; tl[ndocs] = t2 - t1 - 1
88 } else { up[ndocs] = 0; ul[ndocs] = 0; tp[ndocs] = 0; tl[ndocs] = 0 } }
89 if t1 < ls { up[ndocs] = 0; ul[ndocs] = 0; tp[ndocs] = 0; tl[ndocs] = 0 }
90 ndocs = ndocs + 1; ls = ii + 1
91 }
92 ii = ii + 1
93 }
94 // union the postings for each (already lowercased) query term
95 let seen: *u8 = sys_mmap(ndocs + 8)
96 let rowids: *i64 = sys_mmap(8 * SR_ROWID_CAP) as *i64
97 let res: *NxInvQueryResult = sys_mmap(64) as *NxInvQueryResult
98 var ts: i64 = 0; var nmatch: i64 = 0
99 while ts < ql {
100 while ts < ql { if q[ts] == (32 as u8) { ts = ts + 1 } else { break } } // skip spaces
101 var te: i64 = ts
102 while te < ql { if q[te] == (32 as u8) { break } te = te + 1 }
103 if te > ts {
104 nx_inv_query_term(idx, (q as i64 + ts) as *u8, te - ts, rowids, SR_ROWID_CAP, res)
105 var ri: i64 = 0
106 while ri < res.n_rowids_filled { let rid: i64 = rowids[ri]; if rid >= 0 { if rid < ndocs { seen[rid] = 1 as u8 } } ri = ri + 1 }
107 }
108 ts = te
109 }
110 var d: i64 = 0; while d < ndocs { if seen[d] == (1 as u8) { nmatch = nmatch + 1 } d = d + 1 }
111
112 bo = sr_cat(body, bo, "<p>" as *u8); bo = sr_catn(body, bo, nmatch); bo = sr_cat(body, bo, " result" as *u8)
113 if nmatch != 1 { bo = sr_cat(body, bo, "s" as *u8) }
114 bo = sr_cat(body, bo, " for “" as *u8); bo = sr_cat(body, bo, q); bo = sr_cat(body, bo, "”</p><ul>" as *u8)
115 d = 0
116 while d < ndocs {
117 if seen[d] == (1 as u8) {
118 bo = sr_cat(body, bo, "<li><a href=\"" as *u8); bo = sr_cat(body, bo, base_url)
119 let uptr: *u8 = up[d] as *u8
120 var a: i64 = 0; while a < ul[d] { body[bo] = uptr[a]; bo = bo + 1; a = a + 1 }
121 bo = sr_cat(body, bo, "\">" as *u8)
122 let tptr: *u8 = tp[d] as *u8
123 var b: i64 = 0; while b < tl[d] { body[bo] = tptr[b]; bo = bo + 1; b = b + 1 }
124 bo = sr_cat(body, bo, "</a></li>" as *u8)
125 }
126 d = d + 1
127 }
128 bo = sr_cat(body, bo, "</ul>" as *u8)
129 return sr_resp(out, body, bo)
130}