feat: add ext: operator for file extension filtering in advanced search
- Add ext: operator support to query parser in backend and frontend - Update search documentation in README and help modal with ext: examples - Parse ext: operator to extract file extension filter (strips leading dot, converts to lowercase) - Filter search candidates by file extension in advanced_search function - Add ext chip display in search UI alongside existing tag/vault/title/path chips - Update API documentation and function
This commit is contained in:
+3
-3
@@ -1636,15 +1636,14 @@ async def api_tree_search(
|
||||
"path": entry["path"],
|
||||
"name": entry["name"],
|
||||
"type": entry["type"],
|
||||
"matched_path": entry["path"],
|
||||
})
|
||||
|
||||
|
||||
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)"),
|
||||
q: str = Query("", description="Advanced search query (supports tag:, vault:, title:, path:, ext: 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"),
|
||||
@@ -1659,6 +1658,7 @@ async def api_advanced_search(
|
||||
- ``vault:<name>`` — filter by vault
|
||||
- ``title:<text>`` — filter by title substring
|
||||
- ``path:<text>`` — filter by path substring
|
||||
- ``ext:<type>`` — filter by file extension
|
||||
- Remaining text is scored using TF-IDF with accent normalization.
|
||||
|
||||
Results include ``<mark>``-highlighted snippets and faceted tag/vault counts.
|
||||
|
||||
+18
-4
@@ -534,19 +534,21 @@ def _parse_advanced_query(raw_query: str) -> Dict[str, Any]:
|
||||
- ``vault:<name>`` — vault filter
|
||||
- ``title:<text>`` — title filter
|
||||
- ``path:<text>`` — path filter
|
||||
- ``ext:<type>`` — file extension filter
|
||||
- Remaining tokens are treated as free-text search terms.
|
||||
|
||||
Args:
|
||||
raw_query: Raw query string from the user.
|
||||
|
||||
Returns:
|
||||
Dict with keys ``tags``, ``vault``, ``title``, ``path``, ``terms``.
|
||||
Dict with keys ``tags``, ``vault``, ``title``, ``path``, ``ext``, ``terms``.
|
||||
"""
|
||||
parsed: Dict[str, Any] = {
|
||||
"tags": [],
|
||||
"vault": None,
|
||||
"title": None,
|
||||
"path": None,
|
||||
"ext": None,
|
||||
"terms": [],
|
||||
}
|
||||
if not raw_query:
|
||||
@@ -568,6 +570,8 @@ def _parse_advanced_query(raw_query: str) -> Dict[str, Any]:
|
||||
parsed["title"] = token[6:].strip()
|
||||
elif lower.startswith("path:"):
|
||||
parsed["path"] = token[5:].strip()
|
||||
elif lower.startswith("ext:"):
|
||||
parsed["ext"] = token[4:].strip().lstrip(".").lower()
|
||||
else:
|
||||
parsed["terms"].append(token)
|
||||
|
||||
@@ -639,7 +643,7 @@ def advanced_search(
|
||||
sorted token list for O(log V + k) instead of O(V) linear scan.
|
||||
|
||||
Parses the query for operators (``tag:``, ``vault:``, ``title:``,
|
||||
``path:``), falls back remaining tokens to TF-IDF scored free-text
|
||||
``path:``, ``ext:``), falls back remaining tokens to TF-IDF scored free-text
|
||||
search using the inverted index. Results include highlighted snippets
|
||||
with ``<mark>`` tags and faceted counts for tags and vaults.
|
||||
|
||||
@@ -673,7 +677,7 @@ def advanced_search(
|
||||
query_terms = [normalize_text(t) for t in parsed["terms"] if t.strip()]
|
||||
has_terms = len(query_terms) > 0
|
||||
|
||||
if not has_terms and not all_tags and not parsed["title"] and not parsed["path"]:
|
||||
if not has_terms and not all_tags and not parsed["title"] and not parsed["path"] and not parsed["ext"]:
|
||||
return {"results": [], "total": 0, "offset": offset, "limit": limit,
|
||||
"facets": {"tags": {}, "vaults": {}}, "query_time_ms": 0}
|
||||
|
||||
@@ -725,6 +729,16 @@ def advanced_search(
|
||||
if norm_path_filter in normalize_text(inv.doc_info[dk].get("path", ""))
|
||||
}
|
||||
|
||||
if parsed["ext"]:
|
||||
ext_filter = parsed["ext"]
|
||||
candidates = {
|
||||
dk for dk in candidates
|
||||
if (
|
||||
inv.doc_info[dk].get("path", "").rsplit("/", 1)[-1].lower() == ext_filter
|
||||
or inv.doc_info[dk].get("path", "").rsplit("/", 1)[-1].lower().endswith(f".{ext_filter}")
|
||||
)
|
||||
}
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Step 3: Score only the candidates (not all N documents)
|
||||
# ------------------------------------------------------------------
|
||||
@@ -774,7 +788,7 @@ def advanced_search(
|
||||
for expanded_term in expansions:
|
||||
score += inv.tf_idf(expanded_term, doc_key) * 0.5
|
||||
else:
|
||||
# Filter-only search (tag/title/path): score = 1
|
||||
# Filter-only search (tag/title/path/ext): score = 1
|
||||
score = 1.0
|
||||
|
||||
if score > 0:
|
||||
|
||||
Reference in New Issue
Block a user