mirror of
https://github.com/semantica-agi/semantica.git
synced 2026-08-29 04:26:20 +00:00
fix(search-index): bisect ops, thread-safe mutation bridge, drop edge upserts
- Replace list.sort() on every upsert with bisect.insort() — O(log n) per
insert instead of O(n log n); bulk rebuild still sorts once at the end
- Replace list.remove() in remove() with bisect.bisect_left + pop() — O(log n)
find instead of O(n) scan
- Wrap handle_graph_mutation() index mutations in self._lock — mutation bridge
fires from a background thread and was racing concurrent search/rebuild calls
- Drop source/target upserts in add_edge() — edges don't change node text so
the index documents are identical; removes unnecessary cache invalidation
- Sort tag values in _cache_key() — ["a","b"] and ["b","a"] now share a cache
entry since _passes_filters() uses set intersection (order-independent)
- Restore @app.get("/") root handler missing from this branch vs main
Co-authored-by: ZohaibHassan16 <zohaibhassan16@users.noreply.github.com>
Co-authored-by: KaifAhmad1 <mohammadk78600@gmail.com>
This commit is contained in:
co-authored by
ZohaibHassan16
KaifAhmad1
parent
073c48882c
commit
d22a54353a
@@ -10,7 +10,7 @@ from typing import Optional
|
||||
|
||||
from fastapi import FastAPI, HTTPException, Request, WebSocket, WebSocketDisconnect
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from fastapi.responses import FileResponse, JSONResponse
|
||||
from fastapi.responses import FileResponse, HTMLResponse, JSONResponse
|
||||
from fastapi.staticfiles import StaticFiles
|
||||
|
||||
from .. import __version__
|
||||
@@ -144,6 +144,17 @@ def create_app(session: Optional[GraphSession] = None) -> FastAPI:
|
||||
"status": "active",
|
||||
}
|
||||
|
||||
@app.get("/", include_in_schema=False)
|
||||
async def root():
|
||||
index_path = Path(__file__).resolve().parent.parent / "static" / "index.html"
|
||||
if index_path.is_file():
|
||||
return FileResponse(index_path)
|
||||
return HTMLResponse(
|
||||
'<!doctype html><html lang="en"><head><meta charset="UTF-8">'
|
||||
'<title>Semantica Knowledge Explorer</title></head>'
|
||||
'<body><div id="root"></div></body></html>'
|
||||
)
|
||||
|
||||
static_dir = Path(__file__).resolve().parent.parent / "static"
|
||||
if static_dir.is_dir():
|
||||
assets_dir = static_dir / "assets"
|
||||
|
||||
@@ -4,6 +4,7 @@ Explorer-local in-memory node search index.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import bisect
|
||||
import heapq
|
||||
import re
|
||||
from collections import OrderedDict, defaultdict
|
||||
@@ -155,10 +156,9 @@ class GraphSearchIndex:
|
||||
if not prefix_bucket:
|
||||
self._prefix_index.pop(prefix, None)
|
||||
|
||||
try:
|
||||
self._ordered_node_ids.remove(node_id)
|
||||
except ValueError:
|
||||
pass
|
||||
pos = bisect.bisect_left(self._ordered_node_ids, node_id)
|
||||
if pos < len(self._ordered_node_ids) and self._ordered_node_ids[pos] == node_id:
|
||||
self._ordered_node_ids.pop(pos)
|
||||
|
||||
if clear_cache:
|
||||
self.clear_cache()
|
||||
@@ -180,9 +180,8 @@ class GraphSearchIndex:
|
||||
for length in range(self.prefix_min_length, min(len(token), self.prefix_max_length) + 1):
|
||||
self._prefix_index[token[:length]].add(node_id)
|
||||
|
||||
if node_id not in self._ordered_node_ids:
|
||||
self._ordered_node_ids.append(node_id)
|
||||
self._ordered_node_ids.sort()
|
||||
if node_id not in self._documents:
|
||||
bisect.insort(self._ordered_node_ids, node_id)
|
||||
|
||||
if clear_cache:
|
||||
self.clear_cache()
|
||||
@@ -338,7 +337,7 @@ class GraphSearchIndex:
|
||||
for key in sorted(filters.keys()):
|
||||
value = filters[key]
|
||||
if isinstance(value, (list, tuple, set)):
|
||||
serialized_filters.append((key, tuple(str(item) for item in value)))
|
||||
serialized_filters.append((key, tuple(sorted(str(item) for item in value))))
|
||||
else:
|
||||
serialized_filters.append((key, str(value)))
|
||||
return normalized_query, limit, tuple(serialized_filters)
|
||||
|
||||
@@ -411,9 +411,11 @@ class GraphSession:
|
||||
if normalized_event in {"ADD_NODE", "UPDATE_NODE"}:
|
||||
normalized_node = self.normalize_node(payload or {})
|
||||
if normalized_node.get("id"):
|
||||
self._search_index.upsert(normalized_node)
|
||||
with self._lock:
|
||||
self._search_index.upsert(normalized_node)
|
||||
elif normalized_event in {"REMOVE_NODE", "DELETE_NODE"}:
|
||||
self._search_index.remove(str(entity_id))
|
||||
with self._lock:
|
||||
self._search_index.remove(str(entity_id))
|
||||
elif normalized_event in {"RELOAD_GRAPH", "RESET_GRAPH"}:
|
||||
self.rebuild_search_index()
|
||||
|
||||
@@ -638,11 +640,4 @@ class GraphSession:
|
||||
**properties,
|
||||
)
|
||||
has_mutation_callback = callable(getattr(self.graph, "mutation_callback", None))
|
||||
if added and not has_mutation_callback:
|
||||
source = self.get_node(source_id)
|
||||
if source is not None:
|
||||
self._search_index.upsert(source)
|
||||
target = self.get_node(target_id)
|
||||
if target is not None:
|
||||
self._search_index.upsert(target)
|
||||
return added
|
||||
|
||||
Reference in New Issue
Block a user