feat: add embeddings pipeline and semantic search endpoint

Wire the full embedding flow:
- Rewrite Embedding model to use pgvector Vector(512) with HNSW index
- Add embed_photo, vision_fanout, backfill_vision Celery tasks on
  dedicated `vision` queue
- Hook vision_fanout into generate_thumbnails completion
- Add POST /api/v1/photos/search with hybrid RRF ranking (semantic-only
  for now; FTS leg added in PR5)
- Stub ocr_photo, detect_objects, extract_faces tasks for later PRs

Migration 0003 drops/recreates the embeddings table (was never populated).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-10 09:07:32 +02:00
parent b1c2bdf7f0
commit 649437dc85
8 changed files with 418 additions and 11 deletions

View File

@@ -1,17 +1,19 @@
"""
Embedding model definition (placeholder for AI features)
Embedding model — stores CLIP/SigLIP image embeddings via pgvector.
Composite PK (photo_id, model) allows re-embedding with newer models
without clobbering old vectors.
"""
from sqlalchemy import Column, String, ForeignKey, LargeBinary
import uuid
from sqlalchemy import Column, String, ForeignKey, DateTime, func
from pgvector.sqlalchemy import Vector
from app.database import Base
class Embedding(Base):
"""
Placeholder table for future AI embeddings (CLIP, face recognition, etc.)
"""
__tablename__ = 'embeddings'
photo_id = Column(String, ForeignKey('photos.id', ondelete='CASCADE'), primary_key=True)
model = Column(String) # e.g., 'clip-vit-b32', 'face-recognition', etc.
vector = Column(LargeBinary) # raw float32 bytes for embedding vector
model = Column(String(64), primary_key=True) # e.g. 'openclip_vitb32'
vector = Column(Vector(512)) # OpenCLIP ViT-B/32 → 512-d
created_at = Column(DateTime(timezone=True), server_default=func.now())