feat(feed): broaden search to a weighted document (title + keywords + queries + description)
Makes the local search behave more like YouTube's — finding videos by the uploader's own keywords or the query that surfaced them, not only words in the title. A DB-generated, weighted search_vector (migration 0032) replaces the title-only FTS index: - keywords: the creator's snippet.tags (free — already in the snippet we fetch), stored on enrich. - search_terms: distinct live-search queries that surfaced the video (across all users), appended by the search route — folds YouTube's relevance into local search (a video YT returned for a query becomes findable by it even without a title match), the user's own idea. - description (truncated) for broad recall on the existing catalog. Weighted title(A) > keywords+queries(B) > description(C) so ts_rank keeps title hits on top. A plain GIN index on the generated column guarantees index use (no expression/param matching). Verified on localdev: recall 146->213 for one query; 7 'eurovision' hits via the document but not the title; index scan confirmed.
This commit is contained in:
parent
20bcdf5ecb
commit
4395afc210
5 changed files with 115 additions and 11 deletions
|
|
@ -3,6 +3,7 @@ from datetime import date, datetime
|
|||
from sqlalchemy import (
|
||||
BigInteger,
|
||||
Boolean,
|
||||
Computed,
|
||||
Date,
|
||||
DateTime,
|
||||
Float,
|
||||
|
|
@ -14,6 +15,7 @@ from sqlalchemy import (
|
|||
UniqueConstraint,
|
||||
func,
|
||||
)
|
||||
from sqlalchemy.dialects.postgresql import TSVECTOR
|
||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||
|
||||
from app.db import Base
|
||||
|
|
@ -237,6 +239,14 @@ class Video(Base, TimestampMixin):
|
|||
topic_categories: Mapped[list | None] = mapped_column(JSON)
|
||||
default_language: Mapped[str | None] = mapped_column(String(16))
|
||||
detected_language: Mapped[str | None] = mapped_column(String(16))
|
||||
# Creator-supplied keyword tags (snippet.tags joined by spaces) — fetched for free with the
|
||||
# snippet we already request. Indexed into search_vector so the feed search matches the
|
||||
# uploader's own keywords, not just words in the title.
|
||||
keywords: Mapped[str | None] = mapped_column(Text)
|
||||
# Accumulated distinct live-search queries that surfaced this video (across all users) —
|
||||
# folds YouTube's own relevance judgement into the local search: a video YouTube returned
|
||||
# for "ti amo magyarul" becomes findable by that query locally even if its title lacks it.
|
||||
search_terms: Mapped[str | None] = mapped_column(Text)
|
||||
is_short: Mapped[bool] = mapped_column(
|
||||
Boolean, default=False, server_default="false", index=True
|
||||
)
|
||||
|
|
@ -272,6 +282,21 @@ class Video(Base, TimestampMixin):
|
|||
)
|
||||
unavailable_reason: Mapped[str | None] = mapped_column(String(24))
|
||||
|
||||
# Weighted full-text search document (DB-generated, never written by the app): title (A) >
|
||||
# creator keywords + search queries (B) > truncated description (C). ts_rank honours the
|
||||
# weights so title matches outrank description matches. Backed by a GIN index (migration
|
||||
# 0032). The accent-insensitive `unaccent_simple` config comes from migration 0031.
|
||||
search_vector: Mapped[object | None] = mapped_column(
|
||||
TSVECTOR,
|
||||
Computed(
|
||||
"setweight(to_tsvector('public.unaccent_simple', coalesce(title, '')), 'A') || "
|
||||
"setweight(to_tsvector('public.unaccent_simple', coalesce(keywords, '') || ' ' || "
|
||||
"coalesce(search_terms, '')), 'B') || "
|
||||
"setweight(to_tsvector('public.unaccent_simple', left(coalesce(description, ''), 1000)), 'C')",
|
||||
persisted=True,
|
||||
),
|
||||
)
|
||||
|
||||
channel: Mapped["Channel"] = relationship(back_populates="videos")
|
||||
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue