╭──────────╮ │← previous│ ╰──────────╯ ╭ changed ─────╮ │src/search.zig│ ╰──────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │183 // intersect by seeking: a term that lands past the candidate makes │ │184 // its key the new candidate and the others seek up to it. │ │185 var i: usize = 0; │ │220 - while (i < self.terms.len) { │ │221 - const key = (try self.terms[i].seek(self.pos[0..self.pos_len])) orelse return null; │ │186 + while (i < self.terms.items.len) { │ │187 + const key = (try self.terms.items[i].seek(self.pos[0..self.pos_len])) orelse return null;│ │188 if (std.mem.order(u8, key, self.pos[0..self.pos_len]) == .gt) { │ │189 @memcpy(self.pos[0..key.len], key); │ │190 self.pos_len = key.len; │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭ changed ────────────╮ │src/search_commit.zig│ ╰─────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │ 60 │ │ 61 fn docKeyLen(comptime hash_kind: hash.HashKind) usize { │ │ 62 return @sizeOf(u64) + hash.byteLen(hash_kind); │ │ 63 -} │ │ 64 - │ │ 65 -// the action a commit index refresh records when it runs on its own │ │ 66 -pub const undo_action = "haxy/commit-index"; │ │ 67 - │ │ 68 -// reconcile the index versions with the repo's ref tips, in a transaction of │ │ 69 -// its own │ │ 70 -pub fn refresh( │ │ 71 - comptime repo_opts: rp.RepoOpts(.xit), │ │ 72 - io: std.Io, │ │ 73 - allocator: std.mem.Allocator, │ │ 74 - repo: *rp.Repo(.xit, repo_opts), │ │ 75 -) !void { │ │ 76 - const Repo = rp.Repo(.xit, repo_opts); │ │ 77 - const DB = Repo.DB; │ │ 78 - const Save = struct { │ │ 79 - core: *Repo.Core, │ │ 80 - io: std.Io, │ │ 81 - allocator: std.mem.Allocator, │ │ 82 - │ │ 83 - pub fn run(ctx: @This(), cursor: *DB.Cursor(.read_write)) !void { │ │ 84 - var moment = try DB.HashMap(.read_write).init(cursor.*); │ │ 85 - const state = Repo.State(.read_write){ .core = ctx.core, .extra = .{ .moment = &moment } }; │ │ 86 - if (!try refreshInTransaction(repo_opts, state, &moment, ctx.io, ctx.allocator, &.{}, null)) return error.CancelTransaction; │ │ 87 - │ │ 88 - // record the user action for undo │ │ 89 - try xit.undo.write(repo_opts, state, std.Io.Timestamp.now(ctx.io, .real).toSeconds(), .{ .custom = .{ .action_kind = undo_action } }); │ │ 90 - } │ │ 91 - }; │ │ 92 - │ │ 93 - // held across the reads and the writes, so a concurrent refresh can't │ │ 94 - // change the entries these decisions were made from. │ │ 95 - try repo.core.db_file.lock(io, .exclusive); │ │ 96 - defer repo.core.db_file.unlock(io); │ │ 97 - │ │ 98 - const history = try DB.ArrayList(.read_write).init(repo.core.db.rootCursor()); │ │ 99 - history.appendContext(.{ .slot = try history.getSlot(-1) }, Save{ .core = &repo.core, .io = io, .allocator = allocator }) catch |err| switch (err) {│ │100 - error.CancelTransaction => {}, │ │101 - else => return err, │ │102 - }; │ │ 63 } │ │ 64 │ │ 65 // reconcile them inside the caller's transaction, so the index lands with the │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │323 var object = try obj.Object(.xit, repo_opts).init(state, io, allocator, entry.key_ptr); │ │324 defer object.deinit(); │ │325 const key = try readPosting(repo_opts, allocator, &message, &object); │ │366 - if (flags == Ancestry.one) { │ │367 - try srch.remove(DB, version, allocator, &key, message.items); │ │368 - } else { │ │369 - try srch.add(DB, version, allocator, &key, message.items); │ │370 - } │ │326 + const removed = flags == Ancestry.one; │ │327 + try srch.replace(DB, version, allocator, &key, if (removed) message.items else "", if (removed) "" else message.items);│ │328 } │ │329 progress.report(repo_opts, io, progress_ctx_maybe, .{ .end = .writing_patch }); │ │330 } │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────╮ │331 │ │332 // index every commit `tip` reaches into an empty `version`, as the walk │ │376 -// reaches it │ │333 +// reaches it. │ │334 +// TODO: the walk holds every visited oid in memory. if that proves too costly│ │335 +// for large repositories, consider indexing first-parent history only, which │ │336 +// needs no visited set and matches the commits list. │ │337 fn indexHistory( │ │338 comptime repo_opts: rp.RepoOpts(.xit), │ │339 state: rp.Repo(.xit, repo_opts).State(.read_only), │ ╰───────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────╮ │359 defer object.deinit(); │ │360 if (progress.cancelled(repo_opts, progress_ctx_maybe)) return error.ClientGone; │ │361 const key = try readPosting(repo_opts, allocator, &message, object); │ │402 - try srch.add(DB, version, allocator, &key, message.items); │ │362 + try srch.replace(DB, version, allocator, &key, "", message.items); │ │363 progress.report(repo_opts, io, progress_ctx_maybe, .{ .complete_one = .writing_patch });│ │364 } │ │365 progress.report(repo_opts, io, progress_ctx_maybe, .{ .end = .writing_patch }); │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭ changed ────────────╮ │src/search_thread.zig│ ╰─────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────╮ │ 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 -}; │ │11 │ │12 pub fn indexKey(comptime kind: evt.EventKind) []const u8 { │ │13 return switch (kind) { │ ╰───────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────╮ │15 .patch => "word->patch-id-set", │ │16 .discuss => "word->discussion-id-set", │ │17 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, │ │18 }; │ │19 } │ │20 │ ╰─────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────╮ │41 -// move `old`'s postings to `new`'s. an unchanged doc costs nothing: a status │ │42 -// change or an edit that leaves the indexed fields alone returns here. both │ │43 -// sides share a doc key, since a thread keeps the one it was created with. │ │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, │ │50 - old: ?Doc, │ │51 - new: ?Doc, │ │30 + key: []const u8, │ │31 + old_maybe: anytype, │ │32 + new: anytype, │ │33 ) !void { │ │53 - if (old == null and new == null) return; │ │54 - if (old) |before| if (new) |after| { │ │55 - if (std.mem.eql(u8, before.key, after.key) and │ │56 - std.mem.eql(u8, before.title, after.title) and │ │57 - std.mem.eql(u8, before.tags, after.tags) and │ │58 - std.mem.eql(u8, before.description, after.description)) return;│ │59 - }; │ │60 - │ │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))));│ │65 - if (old) |before| { │ │66 - if (new) |after| { │ │67 - try srch.replace(DB, index, allocator, after.key, try text(aa, before), try text(aa, after)); │ │68 - } else { │ │69 - try srch.remove(DB, index, allocator, before.key, try text(aa, before)); │ │70 - } │ │71 - } else if (new) |after| { │ │72 - try srch.add(DB, index, allocator, after.key, try text(aa, after)); │ │73 - } │ │41 + try srch.replace(DB, index, allocator, key, old_text, new_text); │ │42 } │ │43 │ │44 // the doc keys matching `query_text`, in key order, narrowed to `filter` when │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────╮ │next →│ ╰──────╯