nx_search_server.nx source
↩ module page · 263 lines · 12455 B
1// nx_search_server.nx -- the LIVE bits-up search site (sovereign HTTP server).
2//
3// module: nishi-core.search.server
4// depends: nx_http_server.nx, nx_rank_fused.nx, nx_diora_data.nx, fx.nx, nx_str.nx
5// capability: APP_RUNNABLE (DAEMON)
6//
7// Serves a real search site over our own HTTP server (no nginx/libc):
8// GET / -> the search page (query box)
9// GET /search?q=.. -> runs nx_rank_fused over the indexed corpus and returns
10// ranked results (facet + source tier + primary star)
11// The corpus is loaded at startup from nx_diora_data (the verified Diora docs --
12// the stand-in for crawled content); in the networked env the crawler
13// (nx_crawl_doc/frontier) fills the same arrays from live scraping. Ranking is
14// LIVE per query: tier prior + BM25 content, fused via RRF -- so a unique
15// low-tier page outranks a thin high-tier one. Deterministic Q16.16.
16
17import "fx.nx"
18import "nx_str.nx"
19import "nx_http_server.nx"
20import "nx_rank_fused.nx"
21import "nx_search_inverted.nx" // nx_bm25_tf + nx_inv_hash_bytes_lower, used on the /search path
22import "nx_diora_data.nx"
23
24const NX_SEARCH_PORT: i64 = 8787
25
26// ---- response-buffer append helpers -------------------------------
27func bputc(buf: *u8, pos: *i64, cap: i64, c: i64) -> i64 {
28 if pos[0] < cap { buf[pos[0]] = c; pos[0] = pos[0] + 1 }
29 return 0
30}
31func bput(buf: *u8, pos: *i64, cap: i64, s: *u8) -> i64 {
32 let n: i64 = nx_str_len(s)
33 var i: i64 = 0
34 while i < n { bputc(buf, pos, cap, s[i] as i64); i = i + 1 }
35 return 0
36}
37func bput_i64(buf: *u8, pos: *i64, cap: i64, n: i64) -> i64 {
38 if n == 0 { bputc(buf, pos, cap, 0x30); return 0 }
39 var v: i64 = n
40 if v < 0 { bputc(buf, pos, cap, 0x2D); v = 0 - v }
41 let tmp: *u8 = sys_mmap(32)
42 var k: i64 = 0
43 while v > 0 { tmp[k] = 0x30 + (v - (v / 10) * 10); v = v / 10; k = k + 1 }
44 while k > 0 { k = k - 1; bputc(buf, pos, cap, tmp[k] as i64) }
45 return 0
46}
47func bput_q16(buf: *u8, pos: *i64, cap: i64, x: i64) -> i64 {
48 bput_i64(buf, pos, cap, x >> 16); bputc(buf, pos, cap, 0x2E)
49 let low: i64 = x - ((x >> 16) << 16)
50 let frac: i64 = (low * 10000) >> 16
51 let d3: i64 = frac / 1000
52 let r3: i64 = frac - d3 * 1000
53 let d2: i64 = r3 / 100
54 let r2: i64 = r3 - d2 * 100
55 let d1: i64 = r2 / 10
56 bputc(buf, pos, cap, 0x30 + d3); bputc(buf, pos, cap, 0x30 + d2)
57 bputc(buf, pos, cap, 0x30 + d1); bputc(buf, pos, cap, 0x30 + (r2 - d1 * 10))
58 return 0
59}
60
61func nx_facet_name(f: i64) -> *u8 {
62 if f == 0 { return "identity" }
63 if f == 1 { return "works" }
64 if f == 2 { return "official-media" }
65 if f == 3 { return "interviews" }
66 if f == 4 { return "press" }
67 if f == 5 { return "self-social" }
68 if f == 6 { return "long-tail" }
69 return "—"
70}
71func nx_tier_name(t: i64) -> *u8 {
72 if t == NX_TIER_PRIMARY { return "primary" }
73 if t == NX_TIER_EDITORIAL { return "editorial" }
74 if t == NX_TIER_PLATFORM { return "platform" }
75 if t == NX_TIER_ARCHIVE { return "archive" }
76 if t == NX_TIER_COMMUNITY { return "community" }
77 return "unknown"
78}
79
80// hex digit value or -1
81func nx_hexval(c: i64) -> i64 {
82 if c >= 0x30 { if c <= 0x39 { return c - 0x30 } }
83 if c >= 0x61 { if c <= 0x66 { return c - 0x61 + 10 } }
84 if c >= 0x41 { if c <= 0x46 { return c - 0x41 + 10 } }
85 return 0 - 1
86}
87
88// extract + url-decode the q= value from a request path region into out.
89// Returns decoded length. Handles '+' -> space and %XX.
90func nx_extract_query(req: *u8, path_off: i64, path_len: i64, out: *u8, out_cap: i64) -> i64 {
91 // find "q=" within [path_off, path_off+path_len)
92 var i: i64 = path_off
93 let end: i64 = path_off + path_len
94 var qstart: i64 = 0 - 1
95 while i < end - 1 {
96 if (req[i] as i64) == 0x71 { // 'q'
97 if (req[i + 1] as i64) == 0x3D { // '='
98 qstart = i + 2
99 i = end
100 }
101 }
102 if qstart < 0 { i = i + 1 }
103 }
104 if qstart < 0 { return 0 }
105 var p: i64 = qstart
106 var olen: i64 = 0
107 var run: i64 = 1
108 while run == 1 {
109 run = 0
110 if p < end {
111 let c: i64 = req[p] as i64
112 if c == 0x26 { run = 0 } // '&' ends the value
113 else {
114 var emit: i64 = c
115 if c == 0x2B { emit = 0x20 } // '+' -> space
116 if c == 0x25 { // '%XX'
117 if p + 2 < end {
118 let hi: i64 = nx_hexval(req[p + 1] as i64)
119 let lo: i64 = nx_hexval(req[p + 2] as i64)
120 if hi >= 0 { if lo >= 0 { emit = hi * 16 + lo; p = p + 2 } }
121 }
122 }
123 if olen < out_cap { out[olen] = emit; olen = olen + 1 }
124 p = p + 1
125 run = 1
126 }
127 }
128 }
129 return olen
130}
131
132// split a decoded query into up to max terms (token slices into qbuf).
133func nx_split_terms(qbuf: *u8, qlen: i64, term_ptrs: **u8, term_lens: *i64, max: i64) -> i64 {
134 var n: i64 = 0
135 var i: i64 = 0
136 while i < qlen {
137 if (qbuf[i] as i64) == 0x20 { i = i + 1 }
138 else {
139 let start: i64 = i
140 var run: i64 = 1
141 while run == 1 {
142 run = 0
143 if i < qlen { if (qbuf[i] as i64) != 0x20 { i = i + 1; run = 1 } }
144 }
145 if n < max {
146 term_ptrs[n] = ((qbuf as i64) + start) as *u8
147 term_lens[n] = i - start
148 n = n + 1
149 }
150 }
151 }
152 return n
153}
154
155func main() -> i64 {
156 // ---- load corpus (urls/titles/facets); titles are the indexed text
157 // field until the live crawl supplies full page content ----
158 let qn: i64 = nx_diora_qn()
159 let curl: **u8 = sys_mmap(qn * 8) as **u8
160 let ctit: **u8 = sys_mmap(qn * 8) as **u8
161 let cfac: *i64 = sys_mmap(qn * 8) as *i64
162 let cgrd: *i64 = sys_mmap(qn * 8) as *i64
163 let cpri: *i64 = sys_mmap(qn * 8) as *i64
164 nx_diora_fill_qrels(curl, ctit, cfac, cgrd, cpri)
165 let ctlen: *i64 = sys_mmap(qn * 8) as *i64
166 var ci: i64 = 0
167 while ci < qn { ctlen[ci] = nx_str_len(ctit[ci]); ci = ci + 1 }
168
169 // ---- listen ----
170 let addr: *u8 = sys_mmap(16)
171 nx_http_server_addr_any(addr, NX_SEARCH_PORT)
172 let vbox: *i64 = sys_mmap(8) as *i64
173 let lfd: i64 = nx_http_server_listen(addr, 16, vbox)
174 if lfd < 0 { return 2 }
175
176 let reqcap: i64 = 8192
177 let respcap: i64 = 131072
178 let v: *NxTierVerdict = sys_mmap(NX_TIER_VERDICT_BYTES) as *NxTierVerdict
179
180 var serving: i64 = 1
181 while serving == 1 {
182 let cfd: i64 = nx_http_server_accept_one(lfd, vbox)
183 if cfd >= 0 {
184 let req: *u8 = sys_mmap(reqcap)
185 let m: *i64 = sys_mmap(8) as *i64
186 let po: *i64 = sys_mmap(8) as *i64
187 let pl: *i64 = sys_mmap(8) as *i64
188 let cl: *i64 = sys_mmap(8) as *i64
189 let bo: *i64 = sys_mmap(8) as *i64
190 let rn: *i64 = sys_mmap(8) as *i64
191 let rc: i64 = nx_http_server_read_request(cfd, req, reqcap, m, po, pl, cl, bo, rn)
192
193 let resp: *u8 = sys_mmap(respcap)
194 let pos: *i64 = sys_mmap(8) as *i64
195 pos[0] = 0
196 bput(resp, pos, respcap, "HTTP/1.1 200 OK\r\nContent-Type: text/html; charset=utf-8\r\nConnection: close\r\n\r\n")
197 bput(resp, pos, respcap, "<!doctype html><html lang='en'><head><meta charset='utf-8'><meta name='viewport' content='width=device-width,initial-scale=1'><title>Nishi Search</title><style>")
198 bput(resp, pos, respcap, "body{font-family:ui-sans-serif,system-ui,Segoe UI,Roboto,sans-serif;background:#0b0e14;color:#dde3ee;margin:0;padding:28px;max-width:880px}")
199 bput(resp, pos, respcap, "h1{font-size:20px;letter-spacing:.5px}form{display:flex;gap:8px;margin:14px 0 6px}")
200 bput(resp, pos, respcap, "input{flex:1;padding:11px 14px;border-radius:10px;border:1px solid #25304a;background:#0f1422;color:#fff;font-size:15px}")
201 bput(resp, pos, respcap, "button{padding:11px 18px;border-radius:10px;border:0;background:#2a6df4;color:#fff;font-weight:600;cursor:pointer}")
202 bput(resp, pos, respcap, ".sub{color:#8ea0c0;font-size:13px}ol{padding-left:0;list-style:none;counter-reset:r}")
203 bput(resp, pos, respcap, "li{counter-increment:r;padding:10px 0;border-bottom:1px solid #161d29}li::before{content:counter(r) '. ';color:#5b6b86}")
204 bput(resp, pos, respcap, ".tag{font-size:11px;padding:1px 7px;border-radius:99px;background:#172033;color:#9db4dc;margin-right:4px}")
205 bput(resp, pos, respcap, ".tag.primary{background:#0f2a1c;color:#6ee7a8}.title{color:#eef2f8}.url{color:#5b6b86;font-size:12px;word-break:break-all}.star{color:#fbbf24}")
206 bput(resp, pos, respcap, "</style></head><body><h1>NISHI SEARCH — bits-up sovereign</h1>")
207 bput(resp, pos, respcap, "<form action='/search' method='get'><input name='q' placeholder='Search…' value='")
208
209 // extract query (if any)
210 let qbuf: *u8 = sys_mmap(1024)
211 var qlen: i64 = 0
212 if rc == NXS_OK { qlen = nx_extract_query(req, po[0], pl[0], qbuf, 1024) }
213 // echo query into the input value (already plain text)
214 var qe: i64 = 0
215 while qe < qlen { bputc(resp, pos, respcap, qbuf[qe] as i64); qe = qe + 1 }
216 bput(resp, pos, respcap, "' autofocus><button>Search</button></form>")
217
218 if qlen > 0 {
219 // rank the corpus for this query
220 let tp: **u8 = sys_mmap(8 * 8) as **u8
221 let tl: *i64 = sys_mmap(8 * 8) as *i64
222 let nterms: i64 = nx_split_terms(qbuf, qlen, tp, tl, 8)
223 let order: *i64 = sys_mmap(qn * 8) as *i64
224 nx_rank_fused(curl, ctit, ctit, ctlen, qn, tp, tl, nterms, order)
225 bput(resp, pos, respcap, "<p class='sub'>ranked by tier prior + BM25 content, fused (RRF) · primary-source-first</p><ol>")
226 var shown: i64 = 0
227 var i: i64 = 0
228 while i < qn {
229 if shown < 10 {
230 let d: i64 = order[i]
231 // only show docs that actually contain a query term
232 var matched: i64 = 0
233 var t: i64 = 0
234 while t < nterms {
235 if nx_bm25_tf(ctit[d], ctlen[d], nx_inv_hash_bytes_lower(tp[t], tl[t])) > 0 { matched = 1 }
236 t = t + 1
237 }
238 if matched == 1 {
239 nx_tier_classify(curl[d], ctit[d], v)
240 bput(resp, pos, respcap, "<li><span class='tag'>")
241 bput(resp, pos, respcap, nx_facet_name(cfac[d]))
242 bput(resp, pos, respcap, "</span>")
243 if v.is_primary == 1 { bput(resp, pos, respcap, "<span class='tag primary'>"); bput(resp, pos, respcap, nx_tier_name(v.tier)); bput(resp, pos, respcap, " <span class='star'>★</span></span>") }
244 else { bput(resp, pos, respcap, "<span class='tag'>"); bput(resp, pos, respcap, nx_tier_name(v.tier)); bput(resp, pos, respcap, "</span>") }
245 bput(resp, pos, respcap, " <span class='title'>"); bput(resp, pos, respcap, ctit[d]); bput(resp, pos, respcap, "</span>")
246 bput(resp, pos, respcap, "<div class='url'>"); bput(resp, pos, respcap, curl[d]); bput(resp, pos, respcap, "</div></li>")
247 shown = shown + 1
248 }
249 }
250 i = i + 1
251 }
252 if shown == 0 { bput(resp, pos, respcap, "<li class='sub'>no matches in the indexed corpus</li>") }
253 bput(resp, pos, respcap, "</ol>")
254 } else {
255 bput(resp, pos, respcap, "<p class='sub'>try: Diora Baird · wedding · interview · ")
256 bput(resp, pos, respcap, "indexed corpus: "); bput_i64(resp, pos, respcap, qn); bput(resp, pos, respcap, " docs (live crawl fills this in the networked env)</p>")
257 }
258 bput(resp, pos, respcap, "</body></html>")
259 nx_http_server_send_response(cfd, resp, pos[0])
260 }
261 }
262 return 0
263}