Implement backend tree search API with frontend integration for real-time sidebar filtering with highlighted matches and vault-scoped results

This commit is contained in:
2026-03-22 22:06:18 -04:00
parent 29e6e1c052
commit 9cd0090117
3 changed files with 273 additions and 21 deletions
+83 -4
View File
@@ -17,6 +17,7 @@ from backend.indexer import (
reload_index,
index,
get_vault_data,
get_vault_names,
find_file_in_index,
parse_markdown_file,
_extract_tags,
@@ -120,10 +121,20 @@ class TagsResponse(BaseModel):
tags: Dict[str, int]
class ReloadResponse(BaseModel):
"""Index reload confirmation with per-vault stats."""
status: str
vaults: Dict[str, Any]
class TreeSearchResult(BaseModel):
"""A single tree search result item."""
vault: str
path: str
name: str
type: str = Field(description="'file' or 'directory'")
matched_path: str
class TreeSearchResponse(BaseModel):
"""Tree search response with matching paths."""
query: str
vault_filter: str
results: List[TreeSearchResult]
class HealthResponse(BaseModel):
@@ -588,6 +599,74 @@ async def api_tags(vault: Optional[str] = Query(None, description="Vault filter"
return {"vault_filter": vault, "tags": tags}
@app.get("/api/tree-search", response_model=TreeSearchResponse)
async def api_tree_search(
q: str = Query("", description="Search query"),
vault: str = Query("all", description="Vault filter"),
):
"""Search for files and directories in the tree structure.
Searches through the file index for matching paths, returning
both files and their parent directories that match the query.
Args:
q: Search string to match against file/directory paths.
vault: Vault name or "all" to search everywhere.
Returns:
``TreeSearchResponse`` with matching paths and their parent directories.
"""
if not q:
return {"query": q, "vault_filter": vault, "results": []}
query_lower = q.lower()
results = []
seen_paths = set() # Avoid duplicates
vaults_to_search = [vault] if vault != "all" else list(index.keys())
for vault_name in vaults_to_search:
vault_data = get_vault_data(vault_name)
if not vault_data:
continue
vault_root = Path(vault_data["path"])
if not vault_root.exists():
continue
for fpath in vault_root.rglob("*"):
if fpath.name.startswith("."):
continue
try:
rel_path = str(fpath.relative_to(vault_root)).replace("\\", "/")
path_lower = rel_path.lower()
name_lower = fpath.name.lower()
if query_lower not in name_lower and query_lower not in path_lower:
continue
entry_type = "directory" if fpath.is_dir() else "file"
entry_key = f"{vault_name}:{entry_type}:{rel_path}"
if entry_key in seen_paths:
continue
seen_paths.add(entry_key)
results.append({
"vault": vault_name,
"path": rel_path,
"name": fpath.name,
"type": entry_type,
"matched_path": rel_path,
})
except PermissionError:
continue
except Exception:
continue
return {"query": q, "vault_filter": vault, "results": results}
@app.get("/api/index/reload", response_model=ReloadResponse)
async def api_reload():
"""Force a full re-index of all configured vaults.