Changes committed
This commit is contained in:
217
docengine/app/services/matching_service.py
Normal file
217
docengine/app/services/matching_service.py
Normal file
@@ -0,0 +1,217 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import uuid
|
||||
from typing import Any
|
||||
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.core.logging_config import get_logger
|
||||
from app.models.document import Document, TemplateMatch
|
||||
from app.models.template import DocumentFormat
|
||||
from app.repositories.document_repository import DocumentRepository, TemplateMatchRepository
|
||||
from app.repositories.template_repository import TemplateFingerprintRepository, TemplateRepository
|
||||
from app.services.fingerprint_service import FingerprintService
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
class MatchingService:
|
||||
"""Match documents against existing templates using fingerprint comparison."""
|
||||
|
||||
def __init__(self, db: Session) -> None:
|
||||
self.db = db
|
||||
self.doc_repo = DocumentRepository(db)
|
||||
self.template_repo = TemplateRepository(db)
|
||||
self.match_repo = TemplateMatchRepository(db)
|
||||
self.fingerprint_repo = TemplateFingerprintRepository(db)
|
||||
self.fingerprint_service = FingerprintService(db)
|
||||
|
||||
def match_document(
|
||||
self,
|
||||
document_id: uuid.UUID,
|
||||
min_confidence: float = 0.5,
|
||||
max_results: int = 5,
|
||||
) -> list[TemplateMatch]:
|
||||
"""Match a document against all existing templates."""
|
||||
document = self.doc_repo.get_with_pages(document_id)
|
||||
if not document:
|
||||
raise ValueError(f"Document '{document_id}' not found")
|
||||
|
||||
if not document.pages:
|
||||
raise ValueError(f"Document '{document_id}' has no processed pages")
|
||||
|
||||
# Generate document fingerprint data
|
||||
doc_fingerprint_data = self._build_document_fingerprint(document)
|
||||
|
||||
# Get all templates with fingerprints
|
||||
templates = self.template_repo.get_all_with_fingerprints()
|
||||
fingerprints = self.fingerprint_repo.get_all_fingerprints()
|
||||
|
||||
# Map format_id -> fingerprint
|
||||
fp_map = {fp.format_id: fp for fp in fingerprints}
|
||||
|
||||
matches: list[tuple[DocumentFormat, float, dict[str, Any]]] = []
|
||||
|
||||
for template in templates:
|
||||
fp = fp_map.get(template.id)
|
||||
if not fp:
|
||||
continue
|
||||
|
||||
score = self.fingerprint_service.compute_similarity(fp, doc_fingerprint_data)
|
||||
if score >= min_confidence:
|
||||
match_details = {
|
||||
"page_dimensions_score": self.fingerprint_service._compare_dimensions(
|
||||
fp.page_dimensions, doc_fingerprint_data.get("page_dimensions")
|
||||
),
|
||||
"logo_score": self.fingerprint_service._compare_coordinates(
|
||||
fp.logo_coordinates, doc_fingerprint_data.get("logo_coordinates")
|
||||
),
|
||||
"header_score": self.fingerprint_service._compare_coordinates(
|
||||
fp.header_coordinates, doc_fingerprint_data.get("header_coordinates")
|
||||
),
|
||||
"footer_score": self.fingerprint_service._compare_coordinates(
|
||||
fp.footer_coordinates, doc_fingerprint_data.get("footer_coordinates")
|
||||
),
|
||||
"table_score": self.fingerprint_service._compare_coordinates(
|
||||
fp.table_coordinates, doc_fingerprint_data.get("table_coordinates")
|
||||
),
|
||||
"cell_score": self.fingerprint_service._compare_coordinates(
|
||||
fp.cell_coordinates, doc_fingerprint_data.get("cell_coordinates")
|
||||
),
|
||||
}
|
||||
matches.append((template, score, match_details))
|
||||
|
||||
# Sort by score descending
|
||||
matches.sort(key=lambda x: x[1], reverse=True)
|
||||
matches = matches[:max_results]
|
||||
|
||||
# Store match results
|
||||
result_matches: list[TemplateMatch] = []
|
||||
for idx, (template, score, details) in enumerate(matches):
|
||||
template_match = self.match_repo.create_match(
|
||||
document_id=document_id,
|
||||
format_id=template.id,
|
||||
confidence_score=score,
|
||||
match_details=details,
|
||||
selected=(idx == 0), # Auto-select best match
|
||||
)
|
||||
result_matches.append(template_match)
|
||||
|
||||
logger.info(
|
||||
"document_matched",
|
||||
document_id=str(document_id),
|
||||
matches_found=len(result_matches),
|
||||
best_score=result_matches[0].confidence_score if result_matches else 0.0,
|
||||
)
|
||||
|
||||
return result_matches
|
||||
|
||||
def _build_document_fingerprint(self, document: Document) -> dict[str, Any]:
|
||||
"""Build fingerprint data from a document for comparison."""
|
||||
first_page = document.pages[0] if document.pages else None
|
||||
|
||||
page_dimensions = None
|
||||
if first_page:
|
||||
page_dimensions = {
|
||||
"width": first_page.width,
|
||||
"height": first_page.height,
|
||||
"page_count": document.page_count or len(document.pages),
|
||||
}
|
||||
|
||||
# Extract logo coordinates from images
|
||||
logo_coordinates = None
|
||||
logos = []
|
||||
for page in document.pages:
|
||||
for img in page.images:
|
||||
if img.image_type == "logo":
|
||||
logos.append({
|
||||
"page": page.page_number,
|
||||
"x": img.x,
|
||||
"y": img.y,
|
||||
"width": img.width,
|
||||
"height": img.height,
|
||||
})
|
||||
if logos:
|
||||
logo_coordinates = {"items": logos}
|
||||
|
||||
# Extract header coordinates
|
||||
header_coordinates = None
|
||||
headers = []
|
||||
for page in document.pages:
|
||||
header_blocks = [b for b in page.text_blocks if b.block_type == "header"]
|
||||
if header_blocks:
|
||||
min_x = min(b.x for b in header_blocks)
|
||||
min_y = min(b.y for b in header_blocks)
|
||||
max_x = max(b.x + b.width for b in header_blocks)
|
||||
max_y = max(b.y + b.height for b in header_blocks)
|
||||
headers.append({
|
||||
"page": page.page_number,
|
||||
"x": min_x,
|
||||
"y": min_y,
|
||||
"width": max_x - min_x,
|
||||
"height": max_y - min_y,
|
||||
})
|
||||
if headers:
|
||||
header_coordinates = {"items": headers}
|
||||
|
||||
# Extract footer coordinates
|
||||
footer_coordinates = None
|
||||
footers = []
|
||||
for page in document.pages:
|
||||
footer_blocks = [b for b in page.text_blocks if b.block_type == "footer"]
|
||||
if footer_blocks:
|
||||
min_x = min(b.x for b in footer_blocks)
|
||||
min_y = min(b.y for b in footer_blocks)
|
||||
max_x = max(b.x + b.width for b in footer_blocks)
|
||||
max_y = max(b.y + b.height for b in footer_blocks)
|
||||
footers.append({
|
||||
"page": page.page_number,
|
||||
"x": min_x,
|
||||
"y": min_y,
|
||||
"width": max_x - min_x,
|
||||
"height": max_y - min_y,
|
||||
})
|
||||
if footers:
|
||||
footer_coordinates = {"items": footers}
|
||||
|
||||
# Extract table coordinates
|
||||
table_coordinates = None
|
||||
tables = []
|
||||
for page in document.pages:
|
||||
for table in page.tables:
|
||||
tables.append({
|
||||
"page": page.page_number,
|
||||
"x": table.x,
|
||||
"y": table.y,
|
||||
"width": table.width,
|
||||
"height": table.height,
|
||||
"rows": table.rows,
|
||||
"columns": table.columns,
|
||||
})
|
||||
if tables:
|
||||
table_coordinates = {"items": tables}
|
||||
|
||||
# Extract cell coordinates from text blocks
|
||||
cell_coordinates = None
|
||||
cells = []
|
||||
for page in document.pages:
|
||||
for block in page.text_blocks:
|
||||
if block.block_type == "text":
|
||||
cells.append({
|
||||
"page": page.page_number,
|
||||
"x": block.x,
|
||||
"y": block.y,
|
||||
"width": block.width,
|
||||
"height": block.height,
|
||||
})
|
||||
if cells:
|
||||
cell_coordinates = {"items": cells}
|
||||
|
||||
return {
|
||||
"page_dimensions": page_dimensions,
|
||||
"logo_coordinates": logo_coordinates,
|
||||
"header_coordinates": header_coordinates,
|
||||
"footer_coordinates": footer_coordinates,
|
||||
"table_coordinates": table_coordinates,
|
||||
"cell_coordinates": cell_coordinates,
|
||||
}
|
||||
Reference in New Issue
Block a user