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 &mdash; " 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 &ldquo;" as *u8); bo = sr_cat(body, bo, q); bo = sr_cat(body, bo, "&rdquo;</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}