Add advanced TF-IDF search with autocomplete, query operators, facets, pagination, and accent normalization
This commit is contained in:
+130
-1
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user