nx_facet_filter.nx source
↩ module page · 117 lines · 6065 B
1// nx_facet_filter.nx -- FACETED SEARCH for the onsite-search arc (closes its "facet" worklist gap). The
2// post-ranking faceting layer: given BM25-ranked hit doc-ids + a per-doc Dublin-Core facet schema (subject /
3// type / creator / date -- dc:subject, dc:type, dc:creator, dc:date), (1) FILTER the hits by selected facet
4// values (AND across facets) and (2) return per-facet-value COUNTS (the facet UI: "Subject: law (4), ...").
5// Research-driven: faceted search (the feature) + Dublin Core DCMI Terms (the facet vocabulary), both
6// sovereign-fetched + cited (ledger #24). Integer facet-value ids (strings interned upstream). Reusable: wire
7// the BM25 hits from nx_onsite_search into ff_filter/ff_count. Sovereign (nx_cc->nxasm). license_tier: ORIGINAL
8import "nx_syscalls.nx"
9
10const FF_NDOCS: i64 = 12
11const FF_NFACET: i64 = 4 // DC facets: 0=subject 1=type 2=creator 3=date
12const FF_NOSEL: i64 = 0 - 1 // sentinel: facet not selected (no filter on it)
13
14func ff_puts(s: *u8) -> i64 { var n: i64 = 0; while s[n] != (0 as u8) { n = n + 1 } sys_write(1, s, n); return 0 }
15func ff_putn(v: i64) -> i64 {
16 if v == 0 { sys_write(1, "0" as *u8, 1); return 0 }
17 var m: i64 = v
18 if m < 0 { sys_write(1, "-" as *u8, 1); m = 0 - m }
19 let d: *u8 = sys_mmap(24); var k: i64 = 0
20 while m > 0 { d[k] = (48 + (m % 10)) as u8; m = m / 10; k = k + 1 }
21 var j: i64 = k - 1
22 while j >= 0 { sys_write(1, ((d as i64)+j) as *u8, 1); j = j - 1 }
23 return 0
24}
25
26// vals[f*NDOCS + d] = doc d's value-id for facet f.
27// keep hits where, for EVERY selected facet f (sel[f] != FF_NOSEL), vals[f][doc] == sel[f]. out gets the kept ids.
28func ff_filter(hits: *i64, n: i64, vals: *i64, sel: *i64, out: *i64) -> i64 {
29 var k: i64 = 0
30 var i: i64 = 0
31 while i < n {
32 let d: i64 = hits[i]
33 var pass: i64 = 1
34 var f: i64 = 0
35 while f < FF_NFACET {
36 if sel[f] != FF_NOSEL { if vals[f * FF_NDOCS + d] != sel[f] { pass = 0 } }
37 f = f + 1
38 }
39 if pass == 1 { out[k] = d; k = k + 1 }
40 i = i + 1
41 }
42 return k
43}
44
45// for facet f, tally the hit set per value-id into counts[] (pre-zeroed, size >= max value-id+1)
46func ff_count(hits: *i64, n: i64, vals: *i64, f: i64, counts: *i64) -> i64 {
47 var i: i64 = 0
48 while i < n { let v: i64 = vals[f * FF_NDOCS + hits[i]]; counts[v] = counts[v] + 1; i = i + 1 }
49 return 0
50}
51
52func main() -> i64 {
53 // per-doc DC facet values (synthetic, deterministic): subject=d%3, type=d%2, creator=d%4, date=d%5
54 let vals: *i64 = sys_mmap(FF_NFACET * FF_NDOCS * 8) as *i64
55 var d: i64 = 0
56 while d < FF_NDOCS {
57 vals[0 * FF_NDOCS + d] = d % 3 // subject in {0,1,2}
58 vals[1 * FF_NDOCS + d] = d % 2 // type in {0,1}
59 vals[2 * FF_NDOCS + d] = d % 4 // creator in {0..3}
60 vals[3 * FF_NDOCS + d] = d % 5 // date in {0..4}
61 d = d + 1
62 }
63 // BM25 hit set = all docs (in real use, the ranked results from nx_onsite_search)
64 let hits: *i64 = sys_mmap(FF_NDOCS * 8) as *i64
65 var i: i64 = 0
66 while i < FF_NDOCS { hits[i] = i; i = i + 1 }
67 let nh: i64 = FF_NDOCS
68
69 ff_puts("=== nx_facet_filter: faceted search over Dublin-Core facets (subject/type/creator/date) ===\n")
70
71 // facet COUNTS for subject (the facet UI distribution)
72 let cnt: *i64 = sys_mmap(8 * 8) as *i64
73 var z: i64 = 0
74 while z < 8 { cnt[z] = 0; z = z + 1 }
75 ff_count(hits, nh, vals, 0, cnt)
76 ff_puts(" facet 'subject' counts over "); ff_putn(nh); ff_puts(" hits: v0="); ff_putn(cnt[0])
77 ff_puts(" v1="); ff_putn(cnt[1]); ff_puts(" v2="); ff_putn(cnt[2]); ff_puts("\n")
78
79 // FILTER: subject=1 (no other facet selected)
80 let sel: *i64 = sys_mmap(FF_NFACET * 8) as *i64
81 sel[0] = 1; sel[1] = FF_NOSEL; sel[2] = FF_NOSEL; sel[3] = FF_NOSEL
82 let out: *i64 = sys_mmap(FF_NDOCS * 8) as *i64
83 let nf1: i64 = ff_filter(hits, nh, vals, sel, out)
84 ff_puts(" filter subject=1 -> "); ff_putn(nf1); ff_puts(" docs: ")
85 i = 0
86 while i < nf1 { ff_putn(out[i]); ff_puts(" "); i = i + 1 }
87 ff_puts("\n")
88
89 // FILTER: subject=1 AND type=1 (AND across facets)
90 sel[1] = 1
91 let nf2: i64 = ff_filter(hits, nh, vals, sel, out)
92 ff_puts(" filter subject=1 AND type=1 -> "); ff_putn(nf2); ff_puts(" docs: ")
93 i = 0
94 while i < nf2 { ff_putn(out[i]); ff_puts(" "); i = i + 1 }
95 ff_puts("\n")
96
97 // neg-controls
98 sel[0] = FF_NOSEL; sel[1] = FF_NOSEL
99 let nall: i64 = ff_filter(hits, nh, vals, sel, out) // no filter -> all
100 sel[0] = 99
101 let nimp: i64 = ff_filter(hits, nh, vals, sel, out) // impossible -> 0
102
103 var pass: i64 = 0
104 var fail: i64 = 0
105 // T1: subject=1 yields exactly docs 1,4,7,10 (d%3==1) = 4
106 if nf1 == 4 { ff_puts(" T1 subject=1 -> 4 docs (1,4,7,10): PASS\n"); pass = pass + 1 } else { ff_puts(" T1: FAIL\n"); fail = fail + 1 }
107 // T2: counts consistent -- subject counts sum to nh, and cnt[1] == nf1
108 if cnt[0] + cnt[1] + cnt[2] == nh { if cnt[1] == nf1 { ff_puts(" T2 facet counts consistent (sum=hits, cnt[1]=filtered): PASS\n"); pass = pass + 1 } else { ff_puts(" T2: FAIL\n"); fail = fail + 1 } } else { ff_puts(" T2: FAIL\n"); fail = fail + 1 }
109 // T3: AND-faceting narrows (subject=1 AND type=1 -> docs 1,7 = d%3==1 & d%2==1 = 2)
110 if nf2 == 2 { if nf2 <= nf1 { ff_puts(" T3 AND-facet narrows (subject&type -> 2): PASS\n"); pass = pass + 1 } else { ff_puts(" T3: FAIL\n"); fail = fail + 1 } } else { ff_puts(" T3: FAIL\n"); fail = fail + 1 }
111 // T4: neg-controls -- no filter = all, impossible = 0
112 if nall == nh { if nimp == 0 { ff_puts(" T4 neg-controls (no-filter=all, impossible=0): PASS\n"); pass = pass + 1 } else { ff_puts(" T4: FAIL\n"); fail = fail + 1 } } else { ff_puts(" T4: FAIL\n"); fail = fail + 1 }
113
114 ff_puts("\n PASS="); ff_putn(pass); ff_puts("/4 ")
115 if fail == 0 { ff_puts("VERDICT=GREEN (sovereign faceted search: DC-facet filter + counts -- closes the onsite-search facet gap)\n"); sys_exit(0); return 0 }
116 ff_puts("VERDICT=RED\n"); sys_exit(1); return 1
117}