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:
+83
-4
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user