Repository navigation
Index library.alternate_artist_name in search_doc at weight C (BS#3733) - #3734
Merged
Merged
Conversation
Classic catalog search could not find a release by one word of its alternate artist name. The station librarian filed a CD under Kath Bloom with the alternate artist name `kath bloom & david Shapiro`, and searching `shapiro` did not return it. Stage 1 of Both-mode search reads library.search_doc, which held only artist_name (A) and album_title (B). The artist_search_alias path scores the whole credit (0.32 against its 0.40 floor) and is filled by a daily job. Migration 0208 drops and re-adds search_doc with a third segment, alternate_artist_name at weight C, behind its own sentinel gap. It follows 0178: lock and statement timeouts, IF EXISTS guards, the index rebuild and the trailing ANALYZE. Because the column is generated, a release is findable by any word of its alternate name as soon as it is saved or edited. Weight C keeps an alternate-name-only hit below an artist or title hit on the same word. schema.ts carries the same expression, and drizzle-kit reports no drift. The new integration spec covers the librarian's case, a one-word name inside a credit, the full credit, a name set and a name cleared with PATCH /library/:id, and the ranking against an artist-name hit. The position-gap spec now also covers the title/alternate-name phrase seam and the C band. A unit test pins schema.ts, the migration and the latest snapshot to one expression.
6 tasks
Schema constraint shape reportno new constraints detected in this diff (uniqueIndex, .unique(), SET NOT NULL, CHECK, FK) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #3733
Summary
Classic catalog search could not find a release by one word of its alternate artist name. The station librarian filed a CD under Kath Bloom (BL 239) with the alternate artist name
kath bloom & david Shapiro, and searchingshapiroreturned the other Shapiros but not this release.This PR adds
library.alternate_artist_nameto the generated columnlibrary.search_docat weight C, behind its own position gap. Any word of a release's alternate artist name now finds that release through stage 1 of Both-mode search. Because the column is generated, this works as soon as the release is saved or edited, with no job run. This is what /wxycdb's Lucene index did. Theartist_search_aliaspath is unchanged and still adds the owning artist's card.shared/database/src/migrations/0208_3733-library-search-doc-alternate-artist-name.sql: drop and re-addsearch_doc, following 0178 exactly:SET LOCALlock and statement timeouts,DROP COLUMN IF EXISTS,ADD COLUMN ... GENERATED ALWAYS AS (...) STORED,CREATE INDEX IF NOT EXISTS library_search_doc_idx, and the trailingANALYZE. The expression is the one the issue suggested. The header records the measurements below.shared/database/src/schema.ts: the same expression and an updated comment. After the change,drizzle-kit generatereportsNo schema changes, nothing to migrate.library.service.tsanddocs/catalog-search/README.md/docs/playlist-search/README.mdthat saidsearch_docholds only artist and title.whenis drizzle-kit's own stamp (2026-10-28 16:46 PDT). It is strictly above the tail (0207, 2026-10-23 15:39 PDT), so I did not bump it by hand.meta/0208_snapshot.json(10,130 lines) is drizzle-kit output, which every migration carries (validator Check 7). The production source is the migration (about 20 lines of DDL under its header) and the schema expression.Effect on each
search_docreaderBoth-mode stage 1,
searchLibraryByTsvector(GET /library/withartist_name === album_title: classic catalog search on the DJ site and the DJ apps). This reader gets more hits: every word of an alternate name. The ranking form does not change:match_tier, thenexact_score,album_score, plays. An alternate-name-only hit (C, 0.2 ints_rank) sorts below an artist (A, 1.0) or title (B, 0.4) hit on the same word in the same tier. As with title hits today, an exact alternate-name word (tier 2) still sorts above a prefix-only artist hit (tier 1). Two side effects:dj krushnow lists Cold Krush Cuts (alternateDJ Krush & Coldcut) first among DJ Krush's releases (0.9982 against 0.9910).The documented regression set in
docs/catalog-search/README.md(stereolab transient,the velvet,love) has the same top 3 before and after. On the clone,coldcutnow returns the ten Coldcut releases (A), then the three Various Artists Solid Steel titles that name Coldcut (B), then Cold Krush Cuts (C), which stage 1 could not return before.Alias-only probe after stage 1,
searchLibraryAliasHitsAfterTsvector(BS#3552, Classic search drops alias matches when direct matches fill the page: reserve 10% of the page for them #3578). It excludes every row wheresearch_doc @@ tsquery. A release whose own alternate name matches is now a stage-1 row: untagged, and ranked by weight. Before, it came back as an alias row. The artist's other releases still come back as alias rows. No release shows twice, and the id dedupe is still in place.V/A shortcut in
fuzzySearchLibrary. The code does not change. The same-string case goes through Both-mode, so on the clonevarious artistsnow also matches 78 releases whose alternate name holds both words (5,213 in total). They rank after the 5,135 direct rows. The different-strings case still returns[]for a compilation artist.Request line and
GET /library/search?query=,library.service.ts:searchLibrary. This is the reader near line 8300 that the issue points to. It callssearchLibraryBothModewithout alias rows, so it gets the same stage-1 change. The request-line strategies keep at most 5 results (MAX_SEARCH_RESULTS), then keep only rows whose artist starts with the parsed artist (filterResultsByArtist). So an alternate-name-only hit filed under another artist is dropped there. In one narrow case behaviour changes: when such hits are the only stage-1 rows, the strategy now gets nothing back, where before stage 2 could have answered. Stage-2 rows go through the same artist-prefix filter.GET /library/query(library-search.service.ts). This route does not readsearch_doc. It uses trigram ILIKE over artist, title and label, plus the alias branch. No change.Flowsheet search (
search.service.ts). It readsflowsheet.search_doconly.search.service.gapped-vector-schema-drift.test.tspins its gap sentinel against library's expression, and that test still passes: same sentinel, same width.Other code that touches the column. Catalog export, metadata broadcast and the BMI list already leave
search_docout.captureCatalogDeleteSnapshotskips generated columns, so restore is not affected (see below). Thecascade_library_artist_nametrigger andPATCH /library/:idrecompute the column as before. A PATCH ofalternate_artist_namenow recomputes it too, which is the fix.Stage-1 latency on a prod-shaped clone
Method: I captured the stage-1 statement verbatim from the running backend (
searchLibraryByTsvector,n=50). Then I ran it asEXPLAIN (ANALYZE)(planning plus execution time) against the 64,193-row prod clone (LOAD_CLONE_FIXTURE=true,seed-clone.sql), in a private PG 18.6 cluster. Each round has 200 runs per query after 10 warm-ups (300 runs in round 3). The two expressions were alternated old, new, old, new, old, new, and each state was rebuilt fresh (rewrite, index build,VACUUM ANALYZE) before it was measured.shapirolovetheandRare words do not change. The most common words cost a few milliseconds more, because they match more rows:
the+3%,and+14%. Caveats:andp95 is one example.shapiromatches the same two Sally Shapiro rows in both states, which makes it the rare-word case.shapirop95 14.7 → 4.6 ms. That gain came from the COPY-loaded index (6,320 kB, bloated) and its stats, not from this change, so I discarded it in favour of the fresh-rebuild comparison above.Deploy
migrate()runs every pending migration in one transaction, and theACCESS EXCLUSIVElock thatDROP COLUMNtakes is held until that transaction commits. As of 2026-10-28 17:07 PDT, the last auto-deploy, atmainHEAD31b0dfdc(2026-10-26 16:21 PDT), succeeded, so 0207 is applied. None of the 10 open PRs touchesshared/database/src/migrations/orschema.ts. If another migration merges first, let it deploy before this one merges.DROP COLUMNabout 1 ms;ADD COLUMNrewrite 1,061–1,213 ms (0178's expression: 994–1,103 ms on the same clone);CREATE INDEX115–148 ms;ANALYZE450–509 ms. That is about 1.6–1.9 s of statements under the lock,ANALYZEincluded. Throughdev_env/init-db.mjswithSKIP_SEED=true(the runner the migrate image uses), the whole apply took 2.32 s of wall time, node start-up included. The timeouts (lock_timeout30 s,statement_timeout10 min) bound the wait and the rewrite. DJs are on air around the clock, so merge at a quiet hour.migrate-dryrunlane restored the automated prod RDS snapshot of 2026-10-27 22:04 PDT (engine 14.22) to an ephemeral sandbox and applied the pending chain, which was 0208 alone because 0207 deployed before that snapshot. It reporteddryrun-migrate: ok. The whole step took 11.5 s of wall time, node start-up and a cold restored instance included. That is an upper bound for the prod apply, not a measurement of the lock window.search_docmoves from attnum 31 to 32 and gets itspg_statsrow back from the trailingANALYZE.Safety re-checks on current
mainlibrary.search_doc.pg_depend/pg_rewritereturns 0 rows on the schema at 0207.library_artist_viewandrotation_library_viewreadlibrary, but not this column. The only dependents are the column's own generation expression andlibrary_search_doc_idx.restoreCatalogDeleteSnapshotstill omits generated columns. Capture filters!column.generated(shared/database/src/catalog-delete-snapshot.ts:155), and replay inserts only the captured keys. So the attnum move has no effect on restore, and a restored row gets the new vector on insert.wxycsearchdocgapinartist_name,album_titleoralternate_artist_name.search_docis server-only. It is not inapi.yamlor the catalog export.Tests
tests/integration/library-search-alternate-artist-name.spec.js: one parameterized table over Both-modeGET /library/. Each row files the release, sets the alternate name withPATCH /library/:id, and searches at once with no job run:shapirofinds Kath Bloom /Kath Bloom & David Shapiroand ranks it after Sally Shapiro;coldcutfinds DJ Krush /DJ Krush & Coldcutand ranks it after Coldcut;Before the migration the first two rows failed with
Received array: [9734](Sally Shapiro only), which is the reported bug.tests/integration/library-search-doc-position-gap.spec.js, extended with the realCold Krush Cutsrow:'cuts' <-> 'dj', and the prefix form) matches nothing;'krush' <-> 'coldcut') still matches;The phrase case is here rather than in the HTTP table for one reason. Over HTTP, a stage-1 miss falls through to the trigram tier, which matches the same release on its title, so the response cannot show the seam. 0178's spec tests its own seam at this level for the same reason.
tests/unit/database/schema.search-doc-alternate-artist-name.test.ts:schema.tshas the three segments in order, each with its weight, and one gap before each later segment;schema.ts(whitespace normalized);schema.tsexpression byte for byte;The comparisons find "the latest" through the journal, so if BS#871 redefines the column later, the test keeps working instead of breaking.
Local verification
npm run ci:testmock. Instead, the integration specs ran the waytest.ymlruns them. The database was a private PG 18.6 cluster built bydev_env/init-db.mjswith real migrations: a fresh database, so every migration ran in one transaction. The mock API, auth and backend ran on the host with the integration job's service env.library*suite,requestLine, bothrequest-line-*suites,search-exact-case-insensitive,artist-prefix-fold-searchandspecialty-shows-catalog. On the first run,library-catalog-exportandrequest-line-spotlightfailed only because my local env lackedDIGITAL_ARCHIVE_STREAMING_ENABLEDandFLOWSHEET_SPOTLIGHT_ENABLED. With CI's values they pass.typecheck,typecheck:tests(baseline matches), the jobs typecheck (14 jobs),format:check,lint:migrations,check-bulk-update-analyze.mjs --strict, precondition guards, cross-cache-identity flags, legacy entry-id writes, LML caller classification,check:db-mock-sync,check:better-auth-mock-sync,check-auth-tables-doc,check-sql-claim-docs,check:audit-coverage:ciandbuild.npm run lintlocally, because of its memory cost; CI runs it.migrate-dryrunlane on this PR (see Deploy).After deploy
Classic search for
shapiroon dj.wxyc.org should return the Kath Bloom release. A read-only check on prod is also enough: