Add advanced TF-IDF search with autocomplete, query operators, facets, pagination, and accent normalization

This commit is contained in:
2026-03-23 12:09:46 -04:00
parent ba6271b89b
commit e171a0dc35
6 changed files with 1965 additions and 46 deletions
+130 -1
View File
@@ -25,7 +25,7 @@ from backend.indexer import (
_extract_tags,
SUPPORTED_EXTENSIONS,
)
from backend.search import search, get_all_tags
from backend.search import search, get_all_tags, advanced_search, suggest_titles, suggest_tags
from backend.image_processor import preprocess_images
from backend.attachment_indexer import rescan_vault_attachments, get_attachment_stats
@@ -141,6 +141,57 @@ class TreeSearchResponse(BaseModel):
results: List[TreeSearchResult]
class AdvancedSearchResultItem(BaseModel):
"""A single advanced search result with highlighted snippet."""
vault: str
path: str
title: str
tags: List[str]
score: float
snippet: str
modified: str
class SearchFacets(BaseModel):
"""Faceted counts for search results."""
tags: Dict[str, int] = Field(default_factory=dict)
vaults: Dict[str, int] = Field(default_factory=dict)
class AdvancedSearchResponse(BaseModel):
"""Advanced search response with TF-IDF scoring, facets, and pagination."""
results: List[AdvancedSearchResultItem]
total: int
offset: int
limit: int
facets: SearchFacets
class TitleSuggestion(BaseModel):
"""A file title suggestion for autocomplete."""
vault: str
path: str
title: str
class SuggestResponse(BaseModel):
"""Autocomplete suggestions for file titles."""
query: str
suggestions: List[TitleSuggestion]
class TagSuggestion(BaseModel):
"""A tag suggestion for autocomplete."""
tag: str
count: int
class TagSuggestResponse(BaseModel):
"""Autocomplete suggestions for tags."""
query: str
suggestions: List[TagSuggestion]
class ReloadResponse(BaseModel):
"""Index reload confirmation with per-vault stats."""
status: str
@@ -711,6 +762,84 @@ async def api_tree_search(
return {"query": q, "vault_filter": vault, "results": results}
@app.get("/api/search/advanced", response_model=AdvancedSearchResponse)
async def api_advanced_search(
q: str = Query("", description="Advanced search query (supports tag:, vault:, title:, path: operators)"),
vault: str = Query("all", description="Vault filter"),
tag: Optional[str] = Query(None, description="Comma-separated tag filter"),
limit: int = Query(50, ge=1, le=200, description="Results per page"),
offset: int = Query(0, ge=0, description="Pagination offset"),
sort: str = Query("relevance", description="Sort by 'relevance' or 'modified'"),
):
"""Advanced full-text search with TF-IDF scoring, facets, and pagination.
Supports advanced query operators:
- ``tag:<name>`` or ``#<name>`` — filter by tag
- ``vault:<name>`` — filter by vault
- ``title:<text>`` — filter by title substring
- ``path:<text>`` — filter by path substring
- Remaining text is scored using TF-IDF with accent normalization.
Results include ``<mark>``-highlighted snippets and faceted tag/vault counts.
Args:
q: Query string with optional operators.
vault: Vault name or ``"all"``.
tag: Extra comma-separated tag names to require.
limit: Max results per page (1–200).
offset: Pagination offset.
sort: ``"relevance"`` (TF-IDF) or ``"modified"`` (date).
Returns:
``AdvancedSearchResponse`` with scored results, facets, and pagination info.
"""
return advanced_search(q, vault_filter=vault, tag_filter=tag, limit=limit, offset=offset, sort_by=sort)
@app.get("/api/suggest", response_model=SuggestResponse)
async def api_suggest(
q: str = Query("", description="Prefix to search for in file titles"),
vault: str = Query("all", description="Vault filter"),
limit: int = Query(10, ge=1, le=50, description="Max suggestions"),
):
"""Suggest file titles matching a prefix (accent-insensitive).
Used for autocomplete in the search input.
Args:
q: User-typed prefix (minimum 2 characters).
vault: Vault name or ``"all"``.
limit: Max number of suggestions.
Returns:
``SuggestResponse`` with matching file title suggestions.
"""
suggestions = suggest_titles(q, vault_filter=vault, limit=limit)
return {"query": q, "suggestions": suggestions}
@app.get("/api/tags/suggest", response_model=TagSuggestResponse)
async def api_tags_suggest(
q: str = Query("", description="Prefix to search for in tags"),
vault: str = Query("all", description="Vault filter"),
limit: int = Query(10, ge=1, le=50, description="Max suggestions"),
):
"""Suggest tags matching a prefix (accent-insensitive).
Used for autocomplete when typing ``tag:`` or ``#`` in the search input.
Args:
q: User-typed prefix (with or without ``#``, minimum 2 characters).
vault: Vault name or ``"all"``.
limit: Max number of suggestions.
Returns:
``TagSuggestResponse`` with matching tag suggestions and counts.
"""
suggestions = suggest_tags(q, vault_filter=vault, limit=limit)
return {"query": q, "suggestions": suggestions}
@app.get("/api/index/reload", response_model=ReloadResponse)
async def api_reload():
"""Force a full re-index of all configured vaults.