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:
+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