Files
buzz/crates/buzz-admin
Matt Toohey bb550dbadf fix(search): match the resolved kind-0 head; judge rewrite by expression
Addresses the two open findings from the review of PR #8044. The third,
the stale migration-version pin, was fixed in c2b3426d6.

Profile search applied `search_tsv @@ query` before `DISTINCT ON (pubkey)`,
so it kept the newest *matching* live row, not the current head. With two
live kind-0 rows for one pubkey, an older `Wes` and a newer `Alice`, a
search for `wes` dropped the current row and returned the superseded one,
exposing a name and `about` text the user had removed. Make the head
subquery the row source instead: it resolves each pubkey's newest live
kind-0 row in the replaceable store's `created_at DESC, id ASC` order with
only the identity fences inside it (community, `deleted_at IS NULL`,
`kind = 0`, authors), and the search predicate, channel scope, and
since/until apply to the resolved head, as they would on a store holding
one row per person. The outer wrapper that carried `created_at` and
`search_tsv` through `heads` goes away because the outer query reads them
from the subquery directly; the artifact-head predicate and `kind = ANY`
are skipped for kind 0 since the subquery pins the kind. Non-kind-0
searches emit the same SQL as before; `push_authors` serves both paths.

`ProfileSearchPolicy::rewrite_pending` required `events_populated`, so a
database that was populated when 0056 ran and emptied afterwards reported
`ok` at relay startup and from `buzz-admin profile-search-policy` while
every new profile still indexed its whole JSON, because 0056 never reruns.
Judge the rewrite on the live expression alone and drop the
`events_populated` field and its query. The relay warning no longer says
"populated", and docs/profile-search-deployment.md records that emptying
the table does not help.

Tests. `kind0_search_resolves_the_current_head_before_matching` seeds a
superseded `Wes` head under a current `Alice` head and asserts that `wes`
finds nobody, `alice` finds the current row, and an `until` window that
excludes the current head finds nobody rather than the stale row. With
the search predicate moved back into the head subquery it fails at the
first assertion (the stale row returns) while the existing duplicate-heads
test still passes. `profile_search_policy_probe_tracks_the_maintenance_
rewrite` gains the sequence populated upgrade, `DELETE FROM events`, probe
still pending, a new avatar-only profile matches `needle`, maintenance
rewrite clears the probe and reindexes that row. The
`empty_table_is_never_pending` unit test goes with the term it covered.

Plan check on a scratch copy of the local database (PostgreSQL 17.9)
seeded with 20k chat rows, 3,000 profiles including 300 with ~50 KB avatar
tsvectors, and 500 duplicate heads: the head subquery is bounded to the
community's kind-0 rows (`idx_events_addressable` on mixed partitions),
3,500 rows sorted in ~1 MB; 1.7 ms against 0.4 ms for the old shape on the
post-0056 expression and 6.7 ms against 4.8 ms on the whole-JSON
expression. On that data the old shape returned the renamed `Wes 7` for
`wes`; the new shape does not.

Verified with the lld linker workaround for sqlx proc-macros on this
machine: `scripts/postgres-test-run.sh -p buzz-search --tests` (24
passed), `scripts/postgres-test-run.sh -p buzz-db --lib -E
'test(migration::postgres_tests)'` (13 passed), `cargo test -p buzz-db
--lib search_policy`, `cargo test -p buzz-admin profile_search`, `cargo
fmt --all --check`, and `cargo clippy -p buzz-search -p buzz-db -p
buzz-admin -p buzz-relay --all-targets -- -D warnings`.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Signed-off-by: Matt Toohey <contact@matttoohey.com>
2026-10-02 16:55:27 +10:00
..