feat(gallery): visual 'more like this' search (Phase 3 backend)
GalleryService.similar() ranks images by pgvector cosine distance to a source image's precomputed SigLIP embedding — no query-time ML inference. Composes with the Phase-1/2 scope filters (AND) but replaces the date sort (always nearest-first, bounded top-N, no cursor). Returns None for a missing source (→404), [] for a source with no embedding (video / pending ML); excludes self and NULL-embedding rows. New GET /api/gallery/similar?similar_to=<id>&limit=N. Image-detail payload gains has_embedding so the UI can hide the surface. Alembic 0036 adds an HNSW vector_cosine_ops index on siglip_embedding (1152<2000 dims) so the search is sub-50ms ANN instead of a full scan; one-time ~30-60s build over existing embeddings on deploy. Shared _gallery_images/_image_json helpers de-dup the scroll/similar builders. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,41 @@
|
||||
"""image_record.siglip_embedding: HNSW cosine index for "more like this"
|
||||
|
||||
Revision ID: 0036
|
||||
Revises: 0035
|
||||
Create Date: 2026-06-04
|
||||
|
||||
Gallery Phase 3 (visual similarity search) ranks images by
|
||||
`siglip_embedding.cosine_distance(source_embedding)`. Without an index that's
|
||||
a sequential scan computing a 1152-dim distance for every row — fine at small
|
||||
scale, but it grows linearly with the library. Add an HNSW index with
|
||||
`vector_cosine_ops` so the top-N nearest search is sub-50ms ANN.
|
||||
|
||||
1152 dims is under pgvector's 2000-dim HNSW limit, so HNSW (no training,
|
||||
better recall than IVFFlat) is the right choice. ONE-TIME COST: building the
|
||||
index over the existing embeddings (~57k vectors on the operator's library)
|
||||
locks image_record for ~30-60s during this migration on deploy — acceptable
|
||||
for a single-operator homelab. NULL embeddings (videos / not-yet-embedded
|
||||
rows) are simply not indexed.
|
||||
"""
|
||||
from typing import Sequence, Union
|
||||
|
||||
from alembic import op
|
||||
|
||||
revision: str = "0036"
|
||||
down_revision: Union[str, None] = "0035"
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
# Raw SQL: alembic's create_index doesn't express the `USING hnsw (...
|
||||
# vector_cosine_ops)` access-method + opclass cleanly. Must match the
|
||||
# query's cosine_distance operator class to be usable by the planner.
|
||||
op.execute(
|
||||
"CREATE INDEX ix_image_record_siglip_hnsw "
|
||||
"ON image_record USING hnsw (siglip_embedding vector_cosine_ops)"
|
||||
)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
op.drop_index("ix_image_record_siglip_hnsw", table_name="image_record")
|
||||
Reference in New Issue
Block a user