feat: add OCR text extraction and Postgres full-text search

- Create ocr_text table for storing per-region OCR results
- Add tsvector search_vector column to photos with GIN index and
  auto-update trigger on filename/user_title/user_notes
- Implement ocr_photo Celery task using rapidocr-onnxruntime
- Add FTS leg to hybrid search: queries photos.search_vector and
  ocr_text via UNION, fused with semantic results via RRF (k=60)

Migration 0004 backfills search_vector for existing rows.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-10 09:10:14 +02:00
parent 649437dc85
commit 842a4fc864
5 changed files with 167 additions and 6 deletions

View File

@@ -92,8 +92,44 @@ def vision_fanout(photo_id: str):
@shared_task(name='ocr_photo', queue='vision')
def ocr_photo(photo_id: str):
"""OCR a photo — implemented in PR5."""
return {'status': 'not_implemented'}
"""Run OCR on a photo and store text regions."""
if not settings.vision.enabled or not settings.vision.ocr.enabled:
return {'status': 'skipped', 'reason': 'OCR disabled'}
return asyncio.run(_ocr_photo_async(photo_id))
async def _ocr_photo_async(photo_id: str):
image = _load_thumb(photo_id, "large") # 1280px for better OCR accuracy
if image is None:
return {'status': 'error', 'message': 'thumbnail not found'}
from app.services.vision.registry import registry
ocr_engine = registry.get_ocr()
results = ocr_engine.run(image)
if not results:
logger.info("No OCR text found for photo %s", photo_id)
return {'status': 'success', 'photo_id': photo_id, 'regions': 0}
from app.models.ocr_text import OCRText
async with AsyncSessionLocal() as session:
# Delete existing OCR results for this photo (re-run safe)
await session.execute(
delete(OCRText).where(OCRText.photo_id == photo_id)
)
for r in results:
session.add(OCRText(
photo_id=photo_id,
text=r.text,
language=r.language,
confidence=r.confidence,
bbox=r.bbox,
))
await session.commit()
logger.info("OCR: %d text regions for photo %s", len(results), photo_id)
return {'status': 'success', 'photo_id': photo_id, 'regions': len(results)}
@shared_task(name='detect_objects', queue='vision')