feat(plex): search cast & crew names + clickable person cards
Backend (migration 0046_plex_people_search): a new people_text column (cast +
director names, de-duped) is folded into the generated search_vector at weight B,
so the top search box finds titles by an actor/director name and ranks them above
summary-only mentions. New /api/plex/people endpoint returns the cast/crew matching
the term (name prefix or word-start) with a film count + a headshot (pulled from one
representative film's live metadata; image bytes disk-cached).
Frontend: PlexBrowse shows matching people as virtual cards above the grid; clicking
one adds them to the actor/director filter (multi-value, from the C1 work) and clears
the search box so you land on exactly that person's films. Answers the 'why does
"drew" match Rambo?' confusion — it was matching the word in the synopsis; now names
are searchable. i18n en/hu/de.
⚠️ Prod needs a Plex re-sync after deploy to populate people_text (search_vector
regenerates automatically once the column has data).
This commit is contained in:
parent
6c09420068
commit
280c62dda8
12 changed files with 223 additions and 3 deletions
52
backend/alembic/versions/0046_plex_people_search.py
Normal file
52
backend/alembic/versions/0046_plex_people_search.py
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
"""Plex: make cast/crew names searchable
|
||||
|
||||
Revision ID: 0046_plex_people_search
|
||||
Revises: 0045_plex_filter_meta
|
||||
Create Date: 2026-07-06
|
||||
|
||||
Adds `people_text` (the item's cast + director names, space-joined) and folds it into the generated
|
||||
`search_vector` at weight B — between the title (A) and the summary (C). So the top search box now
|
||||
finds titles by an actor/director name and ranks those above films that merely mention the name in
|
||||
their synopsis. A generated column's expression can't be altered in place, so we drop + recreate it
|
||||
(and its GIN index). `people_text` is populated on the next Plex sync.
|
||||
"""
|
||||
from typing import Sequence, Union
|
||||
|
||||
import sqlalchemy as sa
|
||||
from alembic import op
|
||||
|
||||
revision: str = "0046_plex_people_search"
|
||||
down_revision: Union[str, None] = "0045_plex_filter_meta"
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
_CFG = "public.unaccent_simple"
|
||||
_NEW_VECTOR = (
|
||||
f"setweight(to_tsvector('{_CFG}', coalesce(title, '')), 'A') || "
|
||||
f"setweight(to_tsvector('{_CFG}', coalesce(people_text, '')), 'B') || "
|
||||
f"setweight(to_tsvector('{_CFG}', left(coalesce(summary, ''), 1000)), 'C')"
|
||||
)
|
||||
_OLD_VECTOR = (
|
||||
f"setweight(to_tsvector('{_CFG}', coalesce(title, '')), 'A') || "
|
||||
f"setweight(to_tsvector('{_CFG}', left(coalesce(summary, ''), 1000)), 'C')"
|
||||
)
|
||||
|
||||
|
||||
def _rebuild_vector(expr: str) -> None:
|
||||
op.execute("DROP INDEX IF EXISTS ix_plex_items_search_vector")
|
||||
op.execute("ALTER TABLE plex_items DROP COLUMN search_vector")
|
||||
op.execute(
|
||||
f"ALTER TABLE plex_items ADD COLUMN search_vector tsvector "
|
||||
f"GENERATED ALWAYS AS ({expr}) STORED"
|
||||
)
|
||||
op.execute("CREATE INDEX ix_plex_items_search_vector ON plex_items USING gin (search_vector)")
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
op.add_column("plex_items", sa.Column("people_text", sa.Text(), nullable=True))
|
||||
_rebuild_vector(_NEW_VECTOR)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
_rebuild_vector(_OLD_VECTOR)
|
||||
op.drop_column("plex_items", "people_text")
|
||||
Loading…
Add table
Add a link
Reference in a new issue