Repository navigation
feat(storage): answer NIP-50 from an events_by_term index, parity-tested against the fixture - #1113
chappie-daemon wants to merge 15 commits into
Conversation
…ts report was lost
🦋 Changeset detectedLatest commit: 2901660 The changes in this PR will be included in the next version bump. This PR includes changesets to release 0 packagesWhen changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
|
Important Review skippedAuto reviews are disabled on base/target branches other than the default branch. Please check the settings in the CodeRabbit UI or the
|
[Medium impact] The change affects NIP-50 reads and event writes in the LMDB storage backend. Fix the missing field in Findings
|
Dismissed because a newer commit was pushed; Greptile will re-review the current head.
The term-identity stage shifted `lmdb/search.rs`, so two citations in 0012 no longer bracketed what they name: the walk ends at 382, not 368, and `rebuild` now spans 391-456 rather than 378-443. Two more starts were wrong before this change and are fixed with them: the tokeniser citation began on a blank line at 108 (now 109, through the end of `tokens` at 145), and D4's rank citation began at the lone `}` that closes `query_terms` at 171 (now 177, the rank doc). D4's second range now names what proves its claim — `SearchKept` and its `Ord` through `page` — instead of starting inside `remove_event` and ending in another function. Documentation only: every range is checked against the file, and no source file changes here.
Dismissed because a newer commit was pushed; Greptile will re-review the current head.
`177-215` stops at `covered -= 1;`, mid-function; `verdict` ends at 221. Both places that cite the rank now bracket it whole. Documentation only.
| pub(crate) notification_delivery_log: Database<Bytes, Bytes>, | ||
| pub(crate) delivery_log_by_created_at: Database<Bytes, Bytes>, | ||
| pub(crate) delivery_log_by_outbox: Database<Bytes, Bytes>, | ||
| pub(crate) events_by_term: Database<Bytes, Bytes>, |
There was a problem hiding this comment.
Closes nothing by keyword:
baseisrewrite/main, not the repository's default branch. #1109 is closed by hand after the merge.On a limited search, this store ranks by
(span, id)where Postgres keepscreated_at DESC. The set is the capture's; the order is not, and it is the first thing to know about this change. Everything below is the record of why, what it cost, and what it does not do.NIP-50 search was refused on the LMDB path —
StorageError::NotImplemented { module: "storage::lmdb::nip50_search" }— soFilter::searchwas a field a client could send and a store could not answer. This adds a term index (events_by_term, the 30th named database, keyterm ‖ event_id, empty values), a tokeniser shared by the write and read paths, and a rank, so a searchedREQ,COUNTand reconciliation all answer from the same walk.What a reader should check first
The oracle is fixture entry 21, the search row the differential replay had been excluding. It is replayed now:
Entry 21 asks
search: "bitcoin lightning"and the fixture records four rows. The token rule (lowercase, then maximal alphanumeric runs, every term a run ofcontent) selects exactly those four; the capture's in-memory substring rule selects a fifth (f8ef9793…,bitcoininveniice/#LightningNetwork). The capture disagrees with itself here — its SQL path usesto_tsvector/ts_rank, its memory path substrings — and this store reproduces the SQL half's predicate, not its ranker. The fixture's recorded order ists_rank's;(min cover span, event_id)reproduces it on this corpus (spans 2, 2, 2, 4; ids ascending within a span), and a term-frequency rank does not.The store's shape changed, so it has a decision record
docs/rewrite/decisions/0012-nip50-search-index.md(D1–D9) with0011trimmed to a pointer plus the facts it owns. The index is derived: every key rebuilds fromevents,search-index-versioninmetaversions the shape so a change forces a rebuild rather than a migration, and dropping the database costs a rebuild and nothing else. The rebuild runs insideopen, in 1000-row batches, marker written last, behind a per-path lock so two concurrent opens rebuild once (batches == 2, markers == 1; the un-serialised build measures4 / 2).Validation
Static review at this head: 0 findings from 1 provider with status
ran— a verdict, not an absence. Two rounds of lenses:code(gate),architecture,performance,security, all PASS. Round 1's code lens found three defects that are fixed here: the ADR sat in the wrong document,provenwas decided in three places with one test, and the tokeniser ran split-then-lowercase against the specified lowercase-then-split. Static round 3 then found the concurrent-rebuild race, measured and fixed above.Known limits — read these before comparing against Postgres
ts_rankproduces numbers; this store produces a rank of (smallest cover span,event_id). On the fixture corpus the two agree on the four rows' order. A frequency-based rank does not reproduce it (it sorts5454369c, 9ae1fef4, c004, cb41289d), and on a corpus where term frequencies differ the two orders may part with nothing here able to see it.(span, id); Postgres keepscreated_at DESCon a limited search. The unlimited set is the capture's; a limited branch's set can differ.contentterms only. An implementation that also indexed tag keys would answer every criterion correctly (the verdict never reads a tag) and pay index space — so no criterion would notice either way."a"visits 414 candidates for 49 matches on the fixture corpus,"c"254 for 2, and each survivor costs a body decode. Against a million-row store that is roughly 9% of all postings for one REQ. Filed as storage: a one-term search proposes every posting sharing the term's first bytes #1112 with the key-shape fix named.openawaits: 20 000 events ≈ 1.4 s in release, 100 000 ≈ 7 s, 1 000 000 ≈ 70 s on the host measured (240 000 postings in 1.37 s). A store where that is unacceptable on a restart needs the incremental or offline rebuild, not a background build that would answer from a half-built index.open, not by writers, so a write landing inside a rebuild through a second handle can lose postings. ADR D9 records the rule (one handle per store per process; no writes overlapping a rebuild) and storage: a second in-process LMDB handle can lose search postings during a rebuild #1111 carries the failing test that would close it.Fixed in review
A posting whose term merely shared the first term's bytes was counted twice — for an event whose content is
cat cats, a search forcatreached the keeper once per posting, so a count over-reported and a limited page could spend two slots on one row.walknow compares the posting's term slice with the walked term and skips a difference before the other-term probes and before any body decode (307fe09). Read-side only:SEARCH_INDEX_VERSIONstays 1, the key layout is untouched, so no store rebuilds for it. Three tests pin it, two of which were proved red without the comparison.What the lenses measured, so round 3 does not have to ask
create_many(483)&RoTxn), page/count agreeDo not remove the term truncation. It reads like a limitation and is the opposite: without it a client can make a legitimate event unindexable with one very long token.
Out of scope
nostream-cli migrate pg-to-lmdband the benchmark rig — the other two slices of #864.