╭─────────────╮ │scroll to top│ ╰─────────────╯ ╭ search_thread.zig ────────────────────────────────────────────╮ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ ╰───────────────────────────────────────────────────────────────╯
╭ find file ─ ─ ─ ─ ─ ─ ─ ─ ─ ─╮ ╭────╮ ╭ clone ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─╮ │ │ │http│ ssh │ │ ╰ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─╯ ╰────╯ ╰ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─╯
15 .patch => "word->patch-id-set", 16 .discuss => "word->discussion-id-set", 17 else => @compileError("no thread index for " ++ @tagName(kind)), 18 }; 19 } 20 21 // reindex the thread `key` names after `old_maybe` became `new`. an unchanged 22 // doc costs nothing: a status change or an edit that leaves the indexed fields 23 // alone returns here. 24 pub fn update( 25 comptime DB: type, 26 comptime hash_kind: hash.HashKind, 27 comptime kind: evt.EventKind, 28 haxy_moment: DB.HashMap(.read_write), 29 allocator: std.mem.Allocator, 30 key: []const u8, 31 old_maybe: anytype, 32 new: anytype, 33 ) !void { 34 var arena = std.heap.ArenaAllocator.init(allocator); 35 defer arena.deinit(); 36 const aa = arena.allocator(); 37 const old_text = if (old_maybe) |old| try text(aa, old) else ""; 38 const new_text = try text(aa, new); 39 if (std.mem.eql(u8, old_text, new_text)) return; 40 const index = try DB.SortedMap(.read_write).init(try haxy_moment.putCursor(hash.hashInt(hash_kind, indexKey(kind)))); 41 try srch.replace(DB, index, allocator, key, old_text, new_text); 42 } 43 44 // the doc keys matching `query_text`, in key order 45 pub fn query( 46 comptime DB: type, 47 comptime hash_kind: hash.HashKind, 48 comptime kind: evt.EventKind, 49 haxy_moment: DB.HashMap(.read_only), 50 aa: std.mem.Allocator, 51 query_text: []const u8, 52 ) !srch.Query(DB) { 53 const cursor = try haxy_moment.getCursor(hash.hashInt(hash_kind, indexKey(kind))) orelse { 54 // no index means no words match, which one empty term expresses 55 var results: srch.Query(DB) = .{ .terms = .empty }; 56 try results.terms.append(aa, .{ .postings = &.{} }); 57 return results; 58 }; 59 return srch.Query(DB).init(try DB.SortedMap(.read_only).init(cursor), aa, query_text); 60 } 61 62 pub const label_prefix = "label:"; 63 pub const assignee_prefix = "assignee:"; 64 65 // a thread search split into its filter terms and the words left for the index 66 pub const Filters = struct { 67 labels: []const []const u8, 68 assignees: []const []const u8, 69 // the remaining terms joined by single spaces, "" when none 70 words: []const u8, 71 }; 72 73 // split `search` on whitespace into label and assignee terms and plain words. 74 // a prefix with no value is dropped. 75 pub fn parseFilters(aa: std.mem.Allocator, search: []const u8) !Filters { 76 var labels: std.ArrayList([]const u8) = .empty; 77 var assignees: std.ArrayList([]const u8) = .empty; 78 var words: std.ArrayList([]const u8) = .empty; 79 var iter = std.mem.tokenizeAny(u8, search, &std.ascii.whitespace); 80 while (iter.next()) |term| { 81 if (std.mem.startsWith(u8, term, label_prefix)) { 82 if (term.len > label_prefix.len) try labels.append(aa, term[label_prefix.len..]); 83 } else if (std.mem.startsWith(u8, term, assignee_prefix)) { 84 if (term.len > assignee_prefix.len) try assignees.append(aa, term[assignee_prefix.len..]); 85 } else try words.append(aa, term); 86 } 87 return .{ .labels = labels.items, .assignees = assignees.items, .words = try std.mem.join(aa, " ", words.items) }; 88 } 89 90 // `search` with its `prefix` terms replaced by one for `value`, put first 91 pub fn withFilter(aa: std.mem.Allocator, search: []const u8, prefix: []const u8, value: []const u8) ![]const u8 { 92 var terms: std.ArrayList([]const u8) = .empty; 93 try terms.append(aa, try std.mem.concat(aa, u8, &.{ prefix, value })); 94 var iter = std.mem.tokenizeAny(u8, search, &std.ascii.whitespace); 95 while (iter.next()) |term| { 96 if (!std.mem.startsWith(u8, term, prefix)) try terms.append(aa, term); 97 } 98 return std.mem.join(aa, " ", terms.items); 99 } 100 101 // the text a thread is indexed under, none once it is removed: the title and 102 // labels first, so the tokenizer's caps can only drop the tail of a long 103 // description. 104 fn text(aa: std.mem.Allocator, record: anytype) ![]const u8 { 105 if (record.removed) return ""; 106 return std.mem.concat(aa, u8, &.{ record.event.title, " ", record.event.labels, " ", record.event.description }); 107 } 108