nx_search_crawl_server.nx source
↩ module page · 224 lines · 10015 B
1// nx_search_crawl_server.nx -- LIVE search site over CRAWLED content.
2//
3// module: nishi-core.search.crawl_server
4// depends: nx_http_server, nx_http_client, nx_html_to_text, nx_simhash,
5// nx_rank_fused, fx, nx_str
6// capability: APP_RUNNABLE (DAEMON)
7//
8// The integrated end-state: at startup the daemon CRAWLS a seed list over our
9// own HTTP client (fetch -> extract text -> content-dedup -> index), then
10// serves a live search site that ranks the SCRAPED corpus per query
11// (nx_rank_fused: tier + BM25 content via RRF). Everything is surfaced by
12// relevance -- no category censorship, no provenance weighting (operator
13// decision 2026-05-29: maximal coverage). Demonstrated on loopback against
14// nx_mock_web; in the networked env the seeds are real URLs over DNS/TLS/HTTP.
15
16import "fx.nx"
17import "nx_str.nx"
18import "nx_http_server.nx"
19import "nx_http_client.nx"
20import "nx_html_to_text.nx"
21import "nx_simhash.nx"
22import "nx_rank_fused.nx"
23
24const NX_SCS_PORT: i64 = 8790
25const NX_SCS_CRAWL_PORT: i64 = 8088 // the mock-web (or any seed host)
26
27func bputc(buf: *u8, pos: *i64, cap: i64, c: i64) -> i64 { if pos[0] < cap { buf[pos[0]] = c; pos[0] = pos[0] + 1 } return 0 }
28func bput(buf: *u8, pos: *i64, cap: i64, s: *u8) -> i64 { let n: i64 = nx_str_len(s); var i: i64 = 0; while i < n { bputc(buf, pos, cap, s[i] as i64); i = i + 1 } return 0 }
29
30func nx_body_off(resp: *u8, n: i64) -> i64 {
31 var i: i64 = 0
32 while i < n - 3 {
33 if (resp[i] as i64) == 0x0D { if (resp[i+1] as i64) == 0x0A { if (resp[i+2] as i64) == 0x0D { if (resp[i+3] as i64) == 0x0A { return i + 4 } } } }
34 i = i + 1
35 }
36 return 0
37}
38
39// extract <title>..</title> text into out; returns len (0 if none).
40func nx_extract_title(html: *u8, n: i64, out: *u8, outcap: i64) -> i64 {
41 let tag: *u8 = "<title>"
42 let tl: i64 = 7
43 var i: i64 = 0
44 var ts: i64 = 0 - 1
45 while i <= n - tl {
46 if ts < 0 {
47 var j: i64 = 0
48 var ok: i64 = 1
49 while j < tl { if (html[i + j] as i64) != (tag[j] as i64) { ok = 0; j = tl } else { j = j + 1 } }
50 if ok == 1 { ts = i + tl }
51 }
52 i = i + 1
53 }
54 if ts < 0 { return 0 }
55 var olen: i64 = 0
56 var p: i64 = ts
57 var run: i64 = 1
58 while run == 1 {
59 run = 0
60 if p < n { if (html[p] as i64) != 0x3C { if olen < outcap { out[olen] = html[p]; olen = olen + 1 } p = p + 1; run = 1 } }
61 }
62 return olen
63}
64
65func nx_extract_query(req: *u8, path_off: i64, path_len: i64, out: *u8, out_cap: i64) -> i64 {
66 var i: i64 = path_off
67 let end: i64 = path_off + path_len
68 var qstart: i64 = 0 - 1
69 while i < end - 1 {
70 if (req[i] as i64) == 0x71 { if (req[i + 1] as i64) == 0x3D { qstart = i + 2; i = end } }
71 if qstart < 0 { i = i + 1 }
72 }
73 if qstart < 0 { return 0 }
74 var p: i64 = qstart
75 var olen: i64 = 0
76 var run: i64 = 1
77 while run == 1 {
78 run = 0
79 if p < end {
80 let c: i64 = req[p] as i64
81 if c == 0x26 { run = 0 } else {
82 var emit: i64 = c
83 if c == 0x2B { emit = 0x20 }
84 if olen < out_cap { out[olen] = emit; olen = olen + 1 }
85 p = p + 1; run = 1
86 }
87 }
88 }
89 return olen
90}
91
92func nx_split_terms(qbuf: *u8, qlen: i64, tp: **u8, tl: *i64, max: i64) -> i64 {
93 var n: i64 = 0
94 var i: i64 = 0
95 while i < qlen {
96 if (qbuf[i] as i64) == 0x20 { i = i + 1 } else {
97 let start: i64 = i
98 var run: i64 = 1
99 while run == 1 { run = 0; if i < qlen { if (qbuf[i] as i64) != 0x20 { i = i + 1; run = 1 } } }
100 if n < max { tp[n] = ((qbuf as i64) + start) as *u8; tl[n] = i - start; n = n + 1 }
101 }
102 i = i + 1
103 }
104 return n
105}
106
107func main() -> i64 {
108 // ---- STARTUP CRAWL: fetch seeds over our HTTP client, extract, dedup, index ----
109 let caddr: *u8 = sys_mmap(16)
110 nx_http_client_sockaddr_ipv4(caddr, 127, 0, 0, 1, NX_SCS_CRAWL_PORT)
111 let host: *u8 = "127.0.0.1"
112 let hlen: i64 = nx_str_len(host)
113
114 let NSEED: i64 = 3
115 let seeds: **u8 = sys_mmap(NSEED * 8) as **u8
116 seeds[0] = "/wiki"; seeds[1] = "/thothub"; seeds[2] = "/fan"
117
118 let curl: **u8 = sys_mmap(64 * 8) as **u8
119 let ctit: **u8 = sys_mmap(64 * 8) as **u8
120 let ctext: **u8 = sys_mmap(64 * 8) as **u8
121 let ctlen: *i64 = sys_mmap(64 * 8) as *i64
122 let keptfp: *i64 = sys_mmap(64 * 8) as *i64
123 var ndoc: i64 = 0
124
125 var s: i64 = 0
126 while s < NSEED {
127 let resp: *u8 = sys_mmap(16384)
128 let vb: *i64 = sys_mmap(8) as *i64
129 let n: i64 = nx_http_client_get(caddr, seeds[s], nx_str_len(seeds[s]), host, hlen, resp, 16384, vb)
130 if n > 0 {
131 let bo: i64 = nx_body_off(resp, n)
132 let text: *u8 = sys_mmap(8192)
133 let tlen: i64 = nx_html_to_text(((resp as i64) + bo) as *u8, n - bo, text, 8192)
134 let fp: i64 = nx_simhash_fingerprint(text, tlen)
135 var dup: i64 = 0
136 var k: i64 = 0
137 while k < ndoc { if nx_simhash_hamming(fp, keptfp[k]) <= 6 { dup = 1 } k = k + 1 }
138 if dup == 0 {
139 let title: *u8 = sys_mmap(256)
140 let tnl: i64 = nx_extract_title(((resp as i64) + bo) as *u8, n - bo, title, 255)
141 title[tnl] = 0
142 // full url for display: host + path
143 let fu: *u8 = sys_mmap(256)
144 let fp2: *i64 = sys_mmap(8) as *i64
145 fp2[0] = 0
146 bput(fu, fp2, 256, "http://127.0.0.1:8088")
147 bput(fu, fp2, 256, seeds[s])
148 fu[fp2[0]] = 0
149 curl[ndoc] = fu; ctit[ndoc] = title; ctext[ndoc] = text; ctlen[ndoc] = tlen; keptfp[ndoc] = fp
150 ndoc = ndoc + 1
151 }
152 }
153 s = s + 1
154 }
155
156 // ---- SERVE ----
157 let addr: *u8 = sys_mmap(16)
158 nx_http_server_addr_any(addr, NX_SCS_PORT)
159 let vbox: *i64 = sys_mmap(8) as *i64
160 let lfd: i64 = nx_http_server_listen(addr, 16, vbox)
161 if lfd < 0 { return 2 }
162
163 var serving: i64 = 1
164 while serving == 1 {
165 let cfd: i64 = nx_http_server_accept_one(lfd, vbox)
166 if cfd >= 0 {
167 let req: *u8 = sys_mmap(8192)
168 let m: *i64 = sys_mmap(8) as *i64
169 let po: *i64 = sys_mmap(8) as *i64
170 let pl: *i64 = sys_mmap(8) as *i64
171 let cl: *i64 = sys_mmap(8) as *i64
172 let bo2: *i64 = sys_mmap(8) as *i64
173 let rn: *i64 = sys_mmap(8) as *i64
174 let rc: i64 = nx_http_server_read_request(cfd, req, 8192, m, po, pl, cl, bo2, rn)
175
176 let resp: *u8 = sys_mmap(131072)
177 let pos: *i64 = sys_mmap(8) as *i64
178 pos[0] = 0
179 bput(resp, pos, 131072, "HTTP/1.1 200 OK\r\nContent-Type: text/html; charset=utf-8\r\nConnection: close\r\n\r\n")
180 bput(resp, pos, 131072, "<!doctype html><html><head><meta charset='utf-8'><title>Nishi Search (crawled)</title><style>body{font-family:system-ui;background:#0b0e14;color:#dde3ee;max-width:820px;margin:0 auto;padding:28px}h1{font-size:19px}form{display:flex;gap:8px;margin:14px 0}input{flex:1;padding:10px 13px;border-radius:9px;border:1px solid #25304a;background:#0f1422;color:#fff}button{padding:10px 16px;border:0;border-radius:9px;background:#2a6df4;color:#fff;font-weight:600}ol{list-style:none;padding:0;counter-reset:r}li{counter-increment:r;padding:9px 0;border-bottom:1px solid #161d29}li::before{content:counter(r) '. ';color:#5b6b86}.t{color:#eef2f8}.u{color:#5b6b86;font-size:12px}.sub{color:#8ea0c0;font-size:13px}</style></head><body>")
181 bput(resp, pos, 131072, "<h1>NISHI SEARCH — over our own crawl</h1><p class='sub'>corpus: ")
182 bput(resp, pos, 131072, "")
183 // crawled-count
184 let cnt: *u8 = sys_mmap(16); let cp: *i64 = sys_mmap(8) as *i64; cp[0] = 0; bputc(cnt, cp, 16, 0x30 + ndoc); cnt[cp[0]] = 0
185 bput(resp, pos, 131072, cnt)
186 bput(resp, pos, 131072, " pages scraped from the loopback web (live crawl in the networked env)</p>")
187 bput(resp, pos, 131072, "<form action='/search'><input name='q' placeholder='Search the crawl…' value='")
188
189 let qbuf: *u8 = sys_mmap(1024)
190 var qlen: i64 = 0
191 if rc == NXS_OK { qlen = nx_extract_query(req, po[0], pl[0], qbuf, 1024) }
192 var qe: i64 = 0
193 while qe < qlen { bputc(resp, pos, 131072, qbuf[qe] as i64); qe = qe + 1 }
194 bput(resp, pos, 131072, "' autofocus><button>Search</button></form>")
195
196 if qlen > 0 {
197 let tp: **u8 = sys_mmap(8 * 8) as **u8
198 let tl: *i64 = sys_mmap(8 * 8) as *i64
199 let nterms: i64 = nx_split_terms(qbuf, qlen, tp, tl, 8)
200 let order: *i64 = sys_mmap(64 * 8) as *i64
201 nx_rank_fused(curl, ctit, ctext, ctlen, ndoc, tp, tl, nterms, order)
202 bput(resp, pos, 131072, "<ol>")
203 var shown: i64 = 0
204 var i: i64 = 0
205 while i < ndoc {
206 let d: i64 = order[i]
207 var matched: i64 = 0
208 var t: i64 = 0
209 while t < nterms { if nx_bm25_tf(ctext[d], ctlen[d], nx_inv_hash_bytes_lower(tp[t], tl[t])) > 0 { matched = 1 } t = t + 1 }
210 if matched == 1 {
211 bput(resp, pos, 131072, "<li><span class='t'>"); bput(resp, pos, 131072, ctit[d]); bput(resp, pos, 131072, "</span><div class='u'>"); bput(resp, pos, 131072, curl[d]); bput(resp, pos, 131072, "</div></li>")
212 shown = shown + 1
213 }
214 i = i + 1
215 }
216 if shown == 0 { bput(resp, pos, 131072, "<li class='sub'>no matches in the crawl</li>") }
217 bput(resp, pos, 131072, "</ol>")
218 }
219 bput(resp, pos, 131072, "</body></html>")
220 nx_http_server_send_response(cfd, resp, pos[0])
221 }
222 }
223 return 0
224}