feat(obsidian): expose semantic search endpoint
This commit is contained in:
@@ -11,6 +11,7 @@ Endpoints:
|
|||||||
POST /reindex?full=true -> trigger full semantic Chroma rebuild
|
POST /reindex?full=true -> trigger full semantic Chroma rebuild
|
||||||
GET /reindex/status -> check last index state
|
GET /reindex/status -> check last index state
|
||||||
GET /semantic-health -> verify state plus semantic search smoke check
|
GET /semantic-health -> verify state plus semantic search smoke check
|
||||||
|
POST /semantic-search -> query the Obsidian Chroma semantic index
|
||||||
GET /healthz -> returns ok
|
GET /healthz -> returns ok
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@@ -90,6 +91,47 @@ def get_status() -> dict:
|
|||||||
return {"error": str(e)}
|
return {"error": str(e)}
|
||||||
|
|
||||||
|
|
||||||
|
def run_semantic_search(query: str, top_k: int = 5) -> dict:
|
||||||
|
"""Query the local Obsidian Chroma index via the rag-search script."""
|
||||||
|
query = (query or "").strip()
|
||||||
|
if not query:
|
||||||
|
return {"ok": False, "error": "query is required", "results": []}
|
||||||
|
top_k = max(1, min(int(top_k or 5), 20))
|
||||||
|
result = subprocess.run(
|
||||||
|
[
|
||||||
|
VENV_PYTHON if Path(VENV_PYTHON).exists() else sys.executable,
|
||||||
|
SEARCH_SCRIPT,
|
||||||
|
"--index",
|
||||||
|
"obsidian",
|
||||||
|
"--top-k",
|
||||||
|
str(top_k),
|
||||||
|
"--raw",
|
||||||
|
query,
|
||||||
|
],
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
timeout=90,
|
||||||
|
)
|
||||||
|
if result.returncode != 0:
|
||||||
|
return {
|
||||||
|
"ok": False,
|
||||||
|
"query": query,
|
||||||
|
"top_k": top_k,
|
||||||
|
"error": result.stderr.strip()[-2000:] or result.stdout.strip()[-2000:],
|
||||||
|
"results": [],
|
||||||
|
}
|
||||||
|
payload = json.loads(result.stdout)
|
||||||
|
results = payload.get("results") or []
|
||||||
|
return {
|
||||||
|
"ok": True,
|
||||||
|
"query": query,
|
||||||
|
"index": payload.get("index", "obsidian"),
|
||||||
|
"top_k": top_k,
|
||||||
|
"result_count": len(results),
|
||||||
|
"results": results,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
def semantic_health() -> dict:
|
def semantic_health() -> dict:
|
||||||
"""Return state plus a tiny semantic-search smoke check."""
|
"""Return state plus a tiny semantic-search smoke check."""
|
||||||
status = get_status()
|
status = get_status()
|
||||||
@@ -109,29 +151,11 @@ def semantic_health() -> dict:
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
try:
|
try:
|
||||||
result = subprocess.run(
|
payload = run_semantic_search("Obsidian reindex", top_k=1)
|
||||||
[
|
health["search_ok"] = bool(payload.get("results"))
|
||||||
VENV_PYTHON if Path(VENV_PYTHON).exists() else sys.executable,
|
health["result_count"] = len(payload.get("results", []))
|
||||||
SEARCH_SCRIPT,
|
if not payload.get("ok"):
|
||||||
"--index",
|
health["search_error"] = payload.get("error")
|
||||||
"obsidian",
|
|
||||||
"--top-k",
|
|
||||||
"1",
|
|
||||||
"--raw",
|
|
||||||
"Obsidian reindex",
|
|
||||||
],
|
|
||||||
capture_output=True,
|
|
||||||
text=True,
|
|
||||||
timeout=90,
|
|
||||||
)
|
|
||||||
if result.returncode == 0:
|
|
||||||
payload = json.loads(result.stdout)
|
|
||||||
health["search_ok"] = bool(payload.get("results"))
|
|
||||||
health["result_count"] = len(payload.get("results", []))
|
|
||||||
else:
|
|
||||||
health["status"] = "degraded"
|
|
||||||
health["search_ok"] = False
|
|
||||||
health["search_error"] = result.stderr.strip()[-1000:] or result.stdout.strip()[-1000:]
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
health["status"] = "degraded"
|
health["status"] = "degraded"
|
||||||
health["search_ok"] = False
|
health["search_ok"] = False
|
||||||
@@ -163,6 +187,19 @@ class ReindexHandler(http.server.BaseHTTPRequestHandler):
|
|||||||
result = run_reindex(full=full)
|
result = run_reindex(full=full)
|
||||||
status = 200 if "error" not in result else 500
|
status = 200 if "error" not in result else 500
|
||||||
self._json_response(result, status=status)
|
self._json_response(result, status=status)
|
||||||
|
elif path == "/semantic-search":
|
||||||
|
try:
|
||||||
|
length = int(self.headers.get("Content-Length") or 0)
|
||||||
|
body = self.rfile.read(length).decode("utf-8") if length else "{}"
|
||||||
|
payload = json.loads(body or "{}")
|
||||||
|
query = payload.get("query") or payload.get("q") or ""
|
||||||
|
top_k = payload.get("top_k") or payload.get("topK") or 5
|
||||||
|
result = run_semantic_search(str(query), int(top_k))
|
||||||
|
self._json_response(result, status=200 if result.get("ok") else 400)
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
self._json_response({"ok": False, "error": "invalid json", "results": []}, status=400)
|
||||||
|
except Exception as exc:
|
||||||
|
self._json_response({"ok": False, "error": str(exc), "results": []}, status=500)
|
||||||
else:
|
else:
|
||||||
self._json_response({"error": "not found"}, status=404)
|
self._json_response({"error": "not found"}, status=404)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user