9748103782
Co-authored-by: christianlouis <361235+christianlouis@users.noreply.github.com>
98 lines
2.9 KiB
Python
98 lines
2.9 KiB
Python
"""Document similarity API endpoints.
|
||
|
||
Provides an endpoint to find documents similar to a given file based on
|
||
text embeddings and cosine similarity scoring.
|
||
"""
|
||
|
||
import logging
|
||
from typing import Annotated
|
||
|
||
from fastapi import APIRouter, Depends, HTTPException, Query, Request, status
|
||
from sqlalchemy.orm import Session
|
||
|
||
from app.auth import require_login
|
||
from app.database import get_db
|
||
from app.models import FileRecord
|
||
|
||
logger = logging.getLogger(__name__)
|
||
|
||
router = APIRouter()
|
||
|
||
DbSession = Annotated[Session, Depends(get_db)]
|
||
|
||
|
||
@router.get("/files/{file_id}/similar")
|
||
@require_login
|
||
def get_similar_documents(
|
||
request: Request,
|
||
file_id: int,
|
||
db: DbSession,
|
||
limit: int = Query(5, ge=1, le=20, description="Maximum number of similar documents to return"),
|
||
threshold: float = Query(0.3, ge=0.0, le=1.0, description="Minimum similarity score (0–1)"),
|
||
):
|
||
"""Find documents similar to the specified file.
|
||
|
||
Uses text embeddings generated from OCR-extracted text and cosine
|
||
similarity to rank documents by relevance. Similarity scores range
|
||
from 0 (completely different) to 1 (identical content).
|
||
|
||
Embeddings are generated on first access and cached for subsequent
|
||
requests. Documents without OCR text are excluded.
|
||
|
||
Query Parameters:
|
||
- limit: Maximum results to return (default: 5, max: 20)
|
||
- threshold: Minimum similarity score to include (default: 0.3)
|
||
|
||
Example:
|
||
```
|
||
GET /api/files/42/similar?limit=5&threshold=0.5
|
||
```
|
||
|
||
Response:
|
||
```json
|
||
{
|
||
"file_id": 42,
|
||
"similar_documents": [
|
||
{
|
||
"file_id": 15,
|
||
"original_filename": "Invoice_2026-01.pdf",
|
||
"document_title": "January Invoice",
|
||
"similarity_score": 0.8934,
|
||
"mime_type": "application/pdf",
|
||
"created_at": "2026-01-15T10:30:00+00:00"
|
||
}
|
||
],
|
||
"count": 1
|
||
}
|
||
```
|
||
"""
|
||
# Verify the file exists
|
||
file_record = db.query(FileRecord).filter(FileRecord.id == file_id).first()
|
||
if not file_record:
|
||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="File not found")
|
||
|
||
if not file_record.ocr_text or not file_record.ocr_text.strip():
|
||
return {
|
||
"file_id": file_id,
|
||
"similar_documents": [],
|
||
"count": 0,
|
||
"message": "No OCR text available for similarity comparison",
|
||
}
|
||
|
||
try:
|
||
from app.utils.similarity import find_similar_documents
|
||
|
||
similar = find_similar_documents(db, file_id, limit=limit, threshold=threshold)
|
||
|
||
return {
|
||
"file_id": file_id,
|
||
"similar_documents": similar,
|
||
"count": len(similar),
|
||
}
|
||
except Exception as e:
|
||
logger.error(f"Error finding similar documents for file {file_id}: {e}")
|
||
raise HTTPException(
|
||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||
detail="Failed to compute document similarity",
|
||
)
|