╭ search_thread.zig ────────────────────────────────────────────╮ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ ╰───────────────────────────────────────────────────────────────╯
╭────╮ ╭ clone ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ╮ │http│ ssh │ │ ╰────╯ ╰ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ╯
1 const std = @import("std"); 2 const evt = @import("event.zig"); 3 const srch = @import("search.zig"); 4 const xit = @import("xit"); 5 const hash = xit.hash; 6 7 // the thread search indexes: one per kind, inside the haxy moment, so events 8 // carry them and every host that consumes events builds them. a doc key is the 9 // thread's creation order key, which never changes. issues and patches list in 10 // that order too, so their list sets filter a query by intersection. 11 12 // one indexed thread: its doc key and the fields the index covers 13 pub const Doc = struct { 14 key: []const u8, 15 title: []const u8, 16 tags: []const u8, 17 description: []const u8, 18 }; 19 20 pub fn indexKey(comptime kind: evt.EventKind) []const u8 { 21 return switch (kind) { 22 .issue => "word->issue-id-set", 23 .patch => "word->patch-id-set", 24 .discuss => "word->discussion-id-set", 25 else => @compileError("no thread index for " ++ @tagName(kind)), 26 }; 27 } 28 29 // `record`'s indexed side under `key`, or null when it is removed and so has 30 // no postings 31 pub fn doc(key: []const u8, record: anytype) ?Doc { 32 if (record.removed) return null; 33 return .{ 34 .key = key, 35 .title = record.event.title, 36 .tags = record.event.tags, 37 .description = record.event.description, 38 }; 39 } 40 41 // replace `old`'s postings with `new`'s. an unchanged doc costs nothing: a 42 // status change or an edit that leaves the indexed fields alone returns here. 43 pub fn update( 44 comptime DB: type, 45 comptime hash_kind: hash.HashKind, 46 comptime kind: evt.EventKind, 47 haxy_moment: DB.HashMap(.read_write), 48 allocator: std.mem.Allocator, 49 old: ?Doc, 50 new: ?Doc, 51 ) !void { 52 if (old == null and new == null) return; 53 if (old) |before| if (new) |after| { 54 if (std.mem.eql(u8, before.key, after.key) and 55 std.mem.eql(u8, before.title, after.title) and 56 std.mem.eql(u8, before.tags, after.tags) and 57 std.mem.eql(u8, before.description, after.description)) return; 58 }; 59 60 var arena = std.heap.ArenaAllocator.init(allocator); 61 defer arena.deinit(); 62 const index = try DB.SortedMap(.read_write).init(try haxy_moment.putCursor(hash.hashInt(hash_kind, indexKey(kind)))); 63 if (old) |before| try srch.remove(DB, index, allocator, before.key, try text(arena.allocator(), before)); 64 if (new) |after| try srch.add(DB, index, allocator, after.key, try text(arena.allocator(), after)); 65 } 66 67 // the doc keys matching `query_text`, in key order, narrowed to `filter` when 68 // it is keyed the same way 69 pub fn query( 70 comptime DB: type, 71 comptime hash_kind: hash.HashKind, 72 comptime kind: evt.EventKind, 73 haxy_moment: DB.HashMap(.read_only), 74 aa: std.mem.Allocator, 75 query_text: []const u8, 76 filter: ?DB.SortedSet(.read_only), 77 ) !srch.Query(DB) { 78 const cursor = try haxy_moment.getCursor(hash.hashInt(hash_kind, indexKey(kind))) orelse return .{ .terms = &.{} }; 79 const index = try DB.SortedMap(.read_only).init(cursor); 80 var results = try srch.Query(DB).init(index, aa, query_text); 81 if (filter) |set| try results.require(aa, set); 82 return results; 83 } 84 85 // the text a thread is indexed under: the title and tags first, so the 86 // tokenizer's caps can only drop the tail of a long description. 87 fn text(aa: std.mem.Allocator, value: Doc) ![]const u8 { 88 return std.mem.concat(aa, u8, &.{ value.title, " ", value.tags, " ", value.description }); 89 } 90