From e797832561f1c0d06598425cae4efa18c02d7b64 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Sun, 1 Mar 2026 13:32:57 +0000
Subject: [PATCH 1/4] Initial plan
From 5bf0a4c0b990c3c4915696ae01448bf1ca12f87b Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Sun, 1 Mar 2026 13:46:15 +0000
Subject: [PATCH 2/4] 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>
---
app/api/saved_searches.py | 10 +-
app/api/search.py | 14 +-
app/utils/meilisearch_client.py | 25 +++
frontend/templates/search.html | 300 ++++++++++++++++++++++++++---
tests/test_api_advanced_filters.py | 33 ++++
tests/test_api_search.py | 90 +++++++++
tests/test_views_search.py | 30 +++
7 files changed, 468 insertions(+), 34 deletions(-)
diff --git a/app/api/saved_searches.py b/app/api/saved_searches.py
index 92caba31..20c1f004 100644
--- a/app/api/saved_searches.py
+++ b/app/api/saved_searches.py
@@ -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",
}
)
diff --git a/app/api/search.py b/app/api/search.py
index 2b6a5117..0cec03de 100644
--- a/app/api/search.py
+++ b/app/api/search.py
@@ -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,
diff --git a/app/utils/meilisearch_client.py b/app/utils/meilisearch_client.py
index c99beb87..eccd2993 100644
--- a/app/utils/meilisearch_client.py
+++ b/app/utils/meilisearch_client.py
@@ -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:
diff --git a/frontend/templates/search.html b/frontend/templates/search.html
index e78b28e8..1735deda 100644
--- a/frontend/templates/search.html
+++ b/frontend/templates/search.html
@@ -7,7 +7,7 @@
.search-container { max-width: 800px; margin: 0 auto; }
.search-box {
display: flex; gap: 0.5rem; align-items: center;
- margin-bottom: 1.5rem;
+ margin-bottom: 0.75rem;
}
.search-box input {
flex: 1; padding: 0.75rem 1rem;
@@ -24,11 +24,48 @@
white-space: nowrap;
}
.search-box button:hover { background-color: #2563eb; }
+ .search-filters {
+ display: flex; flex-wrap: wrap; gap: 0.5rem; align-items: flex-end;
+ margin-bottom: 0.75rem; padding: 0.75rem;
+ border: 1px solid #e5e7eb; border-radius: 0.5rem;
+ background: #f9fafb;
+ }
+ .search-filter-item { display: flex; flex-direction: column; gap: 0.2rem; }
+ .search-filter-item label {
+ font-size: 0.75rem; font-weight: 600; color: #4b5563;
+ }
+ .search-filter-item select,
+ .search-filter-item input {
+ padding: 0.4rem 0.5rem; border: 1px solid #d1d5db;
+ border-radius: 0.375rem; font-size: 0.8rem; min-width: 120px;
+ }
+ .search-saved-bar {
+ display: flex; align-items: center; gap: 0.5rem; flex-wrap: wrap;
+ margin-bottom: 1rem; font-size: 0.85rem;
+ }
+ .search-saved-bar .saved-label {
+ font-weight: 600; white-space: nowrap;
+ }
+ .saved-search-tag {
+ display: inline-flex; align-items: center; gap: 0.25rem;
+ background: #e5e7eb; padding: 0.2rem 0.5rem; border-radius: 0.25rem;
+ font-size: 0.8rem;
+ }
+ .saved-search-tag a { text-decoration: none; color: inherit; }
+ .saved-search-tag button {
+ border: none; background: none; cursor: pointer; color: #6b7280;
+ padding: 0; line-height: 1;
+ }
+ .btn-save-search {
+ margin-left: auto; font-size: 0.8rem; padding: 0.25rem 0.5rem;
+ cursor: pointer; border: 1px solid #d1d5db; border-radius: 0.25rem;
+ background: white; color: #374151; white-space: nowrap;
+ }
+ .btn-save-search:hover { background: #f3f4f6; }
.search-summary {
font-size: 0.875rem; color: #6b7280;
margin-bottom: 1rem;
}
- /* Google-style result cards */
.search-result {
margin-bottom: 1.5rem;
}
@@ -101,6 +138,60 @@
+
+
+
+
+
+ Saved Searches:
+
+ Loading...
+
+
+
+
@@ -113,18 +204,58 @@
{% endblock %}
diff --git a/tests/test_api_advanced_filters.py b/tests/test_api_advanced_filters.py
index 514d99cd..03bd7b24 100644
--- a/tests/test_api_advanced_filters.py
+++ b/tests/test_api_advanced_filters.py
@@ -348,3 +348,36 @@ class TestSavedSearchesCRUD:
assert len(data["filters"]) == 8
assert data["filters"]["search"] == "invoice"
assert data["filters"]["tags"] == "invoice,amazon"
+
+ def test_saved_search_with_fulltext_query(self, client: TestClient):
+ """Saved search can include full-text query (q) for the search view."""
+ payload = {
+ "name": "Invoice Search",
+ "filters": {"q": "invoice total amount", "document_type": "Invoice"},
+ }
+ response = client.post("/api/saved-searches", json=payload)
+ assert response.status_code == 201
+ data = response.json()
+ assert data["filters"]["q"] == "invoice total amount"
+ assert data["filters"]["document_type"] == "Invoice"
+
+ def test_saved_search_content_finding_filters(self, client: TestClient):
+ """Saved search accepts content-finding filter keys (language, sender, text_quality)."""
+ payload = {
+ "name": "German Invoices",
+ "filters": {
+ "q": "rechnung",
+ "language": "de",
+ "sender": "ACME GmbH",
+ "text_quality": "high",
+ "tags": "invoice",
+ },
+ }
+ response = client.post("/api/saved-searches", json=payload)
+ assert response.status_code == 201
+ data = response.json()
+ assert data["filters"]["q"] == "rechnung"
+ assert data["filters"]["language"] == "de"
+ assert data["filters"]["sender"] == "ACME GmbH"
+ assert data["filters"]["text_quality"] == "high"
+ assert data["filters"]["tags"] == "invoice"
diff --git a/tests/test_api_search.py b/tests/test_api_search.py
index c44a4db6..43322cb8 100644
--- a/tests/test_api_search.py
+++ b/tests/test_api_search.py
@@ -97,6 +97,7 @@ class TestMeilisearchIndexDocument:
assert doc["file_id"] == 1
assert doc["document_title"] == "Invoice January 2026"
assert "invoice" in doc["tags"]
+ assert doc["ocr_text_length"] == len("This is an invoice for services rendered")
def test_index_document_meilisearch_error(self):
"""index_document returns False on Meilisearch exception."""
@@ -179,6 +180,62 @@ class TestMeilisearchSearchDocuments:
assert 'mime_type = "application/pdf"' in search_params["filter"]
assert 'language = "de"' in search_params["filter"]
+ def test_search_with_tags_filter(self):
+ """search_documents passes tags filter to Meilisearch."""
+ mock_client, mock_index = self._make_mock_client(hits=[], total=0)
+
+ with patch("app.utils.meilisearch_client.get_meilisearch_client", return_value=mock_client):
+ from app.utils.meilisearch_client import search_documents
+
+ search_documents("test", tags="invoice", page=1, per_page=10)
+
+ call_kwargs = mock_index.search.call_args
+ search_params = call_kwargs[0][1]
+ assert "filter" in search_params
+ assert 'tags = "invoice"' in search_params["filter"]
+
+ def test_search_with_sender_filter(self):
+ """search_documents passes sender filter to Meilisearch."""
+ mock_client, mock_index = self._make_mock_client(hits=[], total=0)
+
+ with patch("app.utils.meilisearch_client.get_meilisearch_client", return_value=mock_client):
+ from app.utils.meilisearch_client import search_documents
+
+ search_documents("test", sender="ACME Corp", page=1, per_page=10)
+
+ call_kwargs = mock_index.search.call_args
+ search_params = call_kwargs[0][1]
+ assert "filter" in search_params
+ assert 'sender = "ACME Corp"' in search_params["filter"]
+
+ def test_search_with_text_quality_high(self):
+ """search_documents translates text_quality=high to ocr_text_length filter."""
+ mock_client, mock_index = self._make_mock_client(hits=[], total=0)
+
+ with patch("app.utils.meilisearch_client.get_meilisearch_client", return_value=mock_client):
+ from app.utils.meilisearch_client import search_documents
+
+ search_documents("test", text_quality="high", page=1, per_page=10)
+
+ call_kwargs = mock_index.search.call_args
+ search_params = call_kwargs[0][1]
+ assert "filter" in search_params
+ assert "ocr_text_length >= 2000" in search_params["filter"]
+
+ def test_search_with_text_quality_no_text(self):
+ """search_documents translates text_quality=no_text to ocr_text_length filter."""
+ mock_client, mock_index = self._make_mock_client(hits=[], total=0)
+
+ with patch("app.utils.meilisearch_client.get_meilisearch_client", return_value=mock_client):
+ from app.utils.meilisearch_client import search_documents
+
+ search_documents("test", text_quality="no_text", page=1, per_page=10)
+
+ call_kwargs = mock_index.search.call_args
+ search_params = call_kwargs[0][1]
+ assert "filter" in search_params
+ assert "ocr_text_length = 0" in search_params["filter"]
+
def test_search_pagination(self):
"""search_documents applies correct offset for page 2."""
mock_client, mock_index = self._make_mock_client(hits=[], total=50)
@@ -271,6 +328,9 @@ class TestSearchAPIEndpoint:
mime_type="application/pdf",
document_type=None,
language="en",
+ tags=None,
+ sender=None,
+ text_quality=None,
date_from=None,
date_to=None,
page=2,
@@ -294,6 +354,9 @@ class TestSearchAPIEndpoint:
mime_type=None,
document_type=None,
language=None,
+ tags=None,
+ sender=None,
+ text_quality=None,
date_from=None,
date_to=None,
page=1,
@@ -310,3 +373,30 @@ class TestSearchAPIEndpoint:
call_kwargs = mock_search.call_args
assert call_kwargs[1]["date_from"] == 1704067200
assert call_kwargs[1]["date_to"] == 1735689600
+
+ def test_search_endpoint_tags_filter(self, client):
+ """GET /api/search?q=...&tags=invoice passes tags to search_documents."""
+ mock_result = {"results": [], "total": 0, "page": 1, "pages": 0, "query": "test"}
+ with patch("app.api.search.search_documents", return_value=mock_result) as mock_search:
+ response = client.get("/api/search?q=test&tags=invoice")
+
+ assert response.status_code == 200
+ assert mock_search.call_args[1]["tags"] == "invoice"
+
+ def test_search_endpoint_sender_filter(self, client):
+ """GET /api/search?q=...&sender=ACME passes sender to search_documents."""
+ mock_result = {"results": [], "total": 0, "page": 1, "pages": 0, "query": "test"}
+ with patch("app.api.search.search_documents", return_value=mock_result) as mock_search:
+ response = client.get("/api/search?q=test&sender=ACME")
+
+ assert response.status_code == 200
+ assert mock_search.call_args[1]["sender"] == "ACME"
+
+ def test_search_endpoint_text_quality_filter(self, client):
+ """GET /api/search?q=...&text_quality=high passes text_quality to search_documents."""
+ mock_result = {"results": [], "total": 0, "page": 1, "pages": 0, "query": "test"}
+ with patch("app.api.search.search_documents", return_value=mock_result) as mock_search:
+ response = client.get("/api/search?q=test&text_quality=high")
+
+ assert response.status_code == 200
+ assert mock_search.call_args[1]["text_quality"] == "high"
diff --git a/tests/test_views_search.py b/tests/test_views_search.py
index b3d6bbe3..788ec4c8 100644
--- a/tests/test_views_search.py
+++ b/tests/test_views_search.py
@@ -39,3 +39,33 @@ class TestSearchPage:
long_query = "a" * 600
response = client.get(f"/search?q={long_query}")
assert response.status_code == 422
+
+ def test_search_page_contains_filter_elements(self, client):
+ """GET /search contains content-finding filter UI elements."""
+ response = client.get("/search")
+ assert response.status_code == 200
+ assert 'id="filter-document-type"' in response.text
+ assert 'id="filter-tags"' in response.text
+ assert 'id="filter-sender"' in response.text
+ assert 'id="filter-language"' in response.text
+ assert 'id="filter-text-quality"' in response.text
+ assert 'id="filter-date-from"' in response.text
+ assert 'id="filter-date-to"' in response.text
+
+ def test_search_page_contains_saved_searches(self, client):
+ """GET /search contains saved searches UI elements."""
+ response = client.get("/search")
+ assert response.status_code == 200
+ assert 'id="saved-searches-list"' in response.text
+ assert 'id="save-search-btn"' in response.text
+ assert "/api/saved-searches" in response.text
+
+ def test_search_page_text_quality_options(self, client):
+ """GET /search contains text quality filter with expected options."""
+ response = client.get("/search")
+ assert response.status_code == 200
+ text = response.text
+ assert 'value="high"' in text
+ assert 'value="medium"' in text
+ assert 'value="low"' in text
+ assert 'value="no_text"' in text
From de09dba28532af549b25dfdc35384a71aa1c223c Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Sun, 1 Mar 2026 13:55:55 +0000
Subject: [PATCH 3/4] docs: update API and User Guide with search filters and
saved searches
- Document new /api/search endpoint with all filter parameters
- Update saved searches docs with new allowed filter keys
- Update User Guide with search view filters and saved searches usage
Co-authored-by: christianlouis <361235+christianlouis@users.noreply.github.com>
---
docs/API.md | 66 ++++++++++++++++++++++++++++++++++++++++++++---
docs/UserGuide.md | 25 +++++++++++++++---
2 files changed, 85 insertions(+), 6 deletions(-)
diff --git a/docs/API.md b/docs/API.md
index 3613acfc..8cfc9e2d 100644
--- a/docs/API.md
+++ b/docs/API.md
@@ -332,10 +332,62 @@ GET /api/files?status=completed&mime_type=application/pdf&tags=invoice&date_from
> **Tip**: Filter state is reflected in query parameters, making URLs shareable as bookmarks or direct links.
+### Full-Text Search
+
+**GET** `/api/search`
+
+Search documents by full text across OCR content, titles, filenames, tags, sender, and document type. Powered by Meilisearch.
+
+**Query Parameters**:
+
+| Parameter | Type | Required | Description |
+|-----------|------|----------|-------------|
+| `q` | string | Yes | Full-text search query (1–512 chars) |
+| `mime_type` | string | No | Filter by MIME type (e.g. `application/pdf`) |
+| `document_type` | string | No | Filter by document type (e.g. `Invoice`) |
+| `language` | string | No | Filter by language code (e.g. `de`, `en`) |
+| `tags` | string | No | Filter by tag (exact match on a single tag) |
+| `sender` | string | No | Filter by sender/absender (exact match) |
+| `text_quality` | string | No | Filter by OCR text quality: `no_text`, `low`, `medium`, `high` |
+| `date_from` | int | No | Filter results created after this Unix timestamp |
+| `date_to` | int | No | Filter results created before this Unix timestamp |
+| `page` | int | No | Page number, default: 1 |
+| `per_page` | int | No | Results per page (1–100), default: 20 |
+
+**Example**:
+```
+GET /api/search?q=invoice&document_type=Invoice&tags=amazon&text_quality=high&page=1
+```
+
+**Response**:
+```json
+{
+ "results": [
+ {
+ "file_id": 42,
+ "original_filename": "2026-01-15_Invoice_Amazon.pdf",
+ "document_title": "Amazon Invoice January 2026",
+ "document_type": "Invoice",
+ "tags": ["amazon", "invoice"],
+ "_formatted": {
+ "document_title": "Amazon Invoice January 2026",
+ "ocr_text": "...total amount of the invoice is..."
+ }
+ }
+ ],
+ "total": 42,
+ "page": 1,
+ "pages": 3,
+ "query": "invoice"
+}
+```
+
### Saved Searches
Saved searches allow users to save and reuse filter combinations. Each user can store up to 50 saved searches.
+Saved searches are used on both the **Files** page (for file management filters) and the **Search** page (for content-finding filters including full-text queries).
+
#### List Saved Searches
**GET** `/api/saved-searches`
@@ -349,8 +401,9 @@ Returns all saved searches for the current user.
"id": 1,
"name": "Recent Invoices",
"filters": {
+ "q": "invoice total",
"tags": "invoice",
- "status": "completed",
+ "document_type": "Invoice",
"date_from": "2026-01-01"
},
"created_at": "2026-03-01T10:00:00Z",
@@ -368,14 +421,21 @@ Returns all saved searches for the current user.
{
"name": "Recent Invoices",
"filters": {
+ "q": "invoice total",
"tags": "invoice",
- "status": "completed",
+ "document_type": "Invoice",
"date_from": "2026-01-01"
}
}
```
-**Allowed filter keys**: `search`, `mime_type`, `status`, `date_from`, `date_to`, `storage_provider`, `tags`, `sort_by`, `sort_order`
+**Allowed filter keys**:
+
+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`
**Response** (201 Created): The created saved search object.
diff --git a/docs/UserGuide.md b/docs/UserGuide.md
index 57a4d56d..98ff365c 100644
--- a/docs/UserGuide.md
+++ b/docs/UserGuide.md
@@ -119,16 +119,35 @@ The **Files** page includes a full-text search bar (labelled "Full-Text Search")
### Dedicated Search Page
-For a more focused search experience, use the **Search** page accessible from the main navigation:
+For a more focused content-finding experience, use the **Search** page accessible from the main navigation:
1. Navigate to the **Search** page
2. Type your query into the search box — results appear automatically as you type (or press Enter)
-3. Results are displayed in a Google-style format showing:
+3. Use the **content-finding filters** to narrow results:
+ - **Document Type** — e.g. Invoice, Contract
+ - **Tags** — filter by a specific tag
+ - **Sender** — filter by sender / absender
+ - **Language** — filter by ISO language code (e.g. `de`, `en`)
+ - **Text Quality** — filter by OCR text quality (High, Medium, Low, No text)
+ - **Date From / Date To** — restrict results to a date range
+4. Results are displayed in a Google-style format showing:
- **Document title** (linked to the file detail page)
- **Filename**
- **Document type**, **sender**, and **tag** badges
- **Content preview** with highlighted matching terms
-4. Use pagination to browse through large result sets
+5. Use pagination to browse through large result sets
+
+### Saved Searches
+
+Both the **Files** and **Search** pages support **saved searches** — named filter presets you can create and reuse:
+
+1. Apply your desired filters (and optionally a search query on the Search page)
+2. Click **Save Current** in the saved searches bar
+3. Enter a name for the saved search
+4. Your saved search appears as a clickable tag — click it to instantly re-apply those filters
+5. Click the **×** button next to a saved search to delete it
+
+On the **Search** page, saved searches store the full-text query (`q`) along with all active content-finding filters. On the **Files** page, saved searches store the file management filters (filename search, MIME type, status, etc.).
The search page is also accessible via URL with a pre-filled query: `/search?q=invoice`
From 68857c1785d068e3716ea40c18f54d8ada296c97 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Sun, 1 Mar 2026 13:59:23 +0000
Subject: [PATCH 4/4] style(search): address code review - use const/let, fix
date timezone, deduplicate filter keys
Co-authored-by: christianlouis <361235+christianlouis@users.noreply.github.com>
---
frontend/templates/search.html | 129 ++++++++++++++++-----------------
1 file changed, 64 insertions(+), 65 deletions(-)
diff --git a/frontend/templates/search.html b/frontend/templates/search.html
index 1735deda..abac2a8a 100644
--- a/frontend/templates/search.html
+++ b/frontend/templates/search.html
@@ -204,42 +204,42 @@