feat(search): add content-finding filters, saved searches, and text quality to Search view

- Add tags, sender, text_quality filters to search API and Meilisearch client
- Add sender and ocr_text_length to Meilisearch filterable attributes
- Expand saved search allowed filter keys to include q, document_type, language, sender, text_quality
- Add filters panel and saved searches UI to the Search view template
- Add tests for new search filters, saved search keys, and search view elements

Co-authored-by: christianlouis <361235+christianlouis@users.noreply.github.com>
This commit is contained in:
copilot-swe-agent[bot]
2026-03-01 13:46:15 +00:00
parent e797832561
commit 5bf0a4c0b9
7 changed files with 468 additions and 34 deletions
+9 -1
View File
@@ -23,10 +23,14 @@ router = APIRouter(prefix="/saved-searches", tags=["saved-searches"])
DbSession = Annotated[Session, Depends(get_db)]
# Allowed filter keys that can be saved
# Allowed filter keys that can be saved.
# Files-view keys: search, mime_type, status, storage_provider, sort_by, sort_order
# Search-view keys: q, document_type, language, sender, text_quality
# Shared keys: tags, date_from, date_to
ALLOWED_FILTER_KEYS = frozenset(
{
"search",
"q",
"mime_type",
"status",
"date_from",
@@ -35,6 +39,10 @@ ALLOWED_FILTER_KEYS = frozenset(
"tags",
"sort_by",
"sort_order",
"document_type",
"language",
"sender",
"text_quality",
}
)
+13 -1
View File
@@ -29,6 +29,12 @@ def search_api(
mime_type: Optional[str] = Query(None, description="Filter by MIME type (e.g. application/pdf)"),
document_type: Optional[str] = Query(None, description="Filter by document type (e.g. Invoice)"),
language: Optional[str] = Query(None, description="Filter by language code (e.g. de, en)"),
tags: Optional[str] = Query(None, description="Filter by tag (exact match)"),
sender: Optional[str] = Query(None, description="Filter by sender/absender (exact match)"),
text_quality: Optional[str] = Query(
None,
description="Filter by OCR text quality: no_text, low, medium, high",
),
date_from: Optional[int] = Query(None, description="Filter results created after this Unix timestamp"),
date_to: Optional[int] = Query(None, description="Filter results created before this Unix timestamp"),
page: int = Query(1, ge=1, description="Page number (1-based)"),
@@ -50,6 +56,9 @@ def search_api(
- mime_type: Filter by MIME type
- document_type: Filter by document type
- language: Filter by language code
- tags: Filter by tag (exact match on a single tag)
- sender: Filter by sender/absender (exact match)
- text_quality: Filter by OCR text quality (no_text, low, medium, high)
- date_from: Unix timestamp lower bound
- date_to: Unix timestamp upper bound
- page: Page number (default: 1)
@@ -57,7 +66,7 @@ def search_api(
Example:
```
GET /api/search?q=invoice&document_type=Invoice&date_from=1704067200&page=1&per_page=20
GET /api/search?q=invoice&document_type=Invoice&tags=amazon&date_from=1704067200&page=1&per_page=20
```
Response:
@@ -90,6 +99,9 @@ def search_api(
mime_type=mime_type,
document_type=document_type,
language=language,
tags=tags,
sender=sender,
text_quality=text_quality,
date_from=date_from,
date_to=date_to,
page=page,
+25
View File
@@ -33,8 +33,10 @@ _INDEX_SETTINGS = {
"document_type",
"language",
"tags",
"sender",
"created_at_ts",
"file_id",
"ocr_text_length",
],
"sortableAttributes": [
"created_at_ts",
@@ -55,6 +57,7 @@ _INDEX_SETTINGS = {
"file_size",
"created_at_ts",
"ocr_text",
"ocr_text_length",
],
"rankingRules": [
"words",
@@ -142,6 +145,7 @@ def _build_document(file_record: "FileRecord", text: str, metadata: dict) -> dic
"file_size": file_record.file_size or 0,
"created_at_ts": created_at_ts,
"ocr_text": text or "",
"ocr_text_length": len(text) if text else 0,
}
@@ -202,6 +206,9 @@ def search_documents(
mime_type: Optional[str] = None,
document_type: Optional[str] = None,
language: Optional[str] = None,
tags: Optional[str] = None,
sender: Optional[str] = None,
text_quality: Optional[str] = None,
date_from: Optional[int] = None,
date_to: Optional[int] = None,
page: int = 1,
@@ -214,6 +221,9 @@ def search_documents(
mime_type: Optional MIME-type filter.
document_type: Optional document type filter.
language: Optional language filter (ISO 639-1, e.g. "de").
tags: Optional tag filter (exact match on a single tag).
sender: Optional sender/absender filter (exact match).
text_quality: Optional text quality filter: no_text, low, medium, high.
date_from: Optional lower bound Unix timestamp for created_at.
date_to: Optional upper bound Unix timestamp for created_at.
page: 1-based page number.
@@ -240,6 +250,21 @@ def search_documents(
filters.append(f'document_type = "{document_type}"')
if language:
filters.append(f'language = "{language}"')
if tags:
filters.append(f'tags = "{tags}"')
if sender:
filters.append(f'sender = "{sender}"')
if text_quality:
# Translate text_quality labels into ocr_text_length ranges
_tq_filters = {
"no_text": "ocr_text_length = 0",
"low": "ocr_text_length > 0 AND ocr_text_length < 500",
"medium": "ocr_text_length >= 500 AND ocr_text_length < 2000",
"high": "ocr_text_length >= 2000",
}
tq_expr = _tq_filters.get(text_quality)
if tq_expr:
filters.append(tq_expr)
if date_from is not None:
filters.append(f"created_at_ts >= {date_from}")
if date_to is not None: