diff --git a/semantica/explorer/app.py b/semantica/explorer/app.py
index 4ab5a020..ea776f19 100644
--- a/semantica/explorer/app.py
+++ b/semantica/explorer/app.py
@@ -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(
+ '
'
+ 'Semantica Knowledge Explorer'
+ ''
+ )
+
static_dir = Path(__file__).resolve().parent.parent / "static"
if static_dir.is_dir():
assets_dir = static_dir / "assets"
diff --git a/semantica/explorer/search_index.py b/semantica/explorer/search_index.py
index 64c51a3b..1e4df821 100644
--- a/semantica/explorer/search_index.py
+++ b/semantica/explorer/search_index.py
@@ -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)
diff --git a/semantica/explorer/session.py b/semantica/explorer/session.py
index 857a4a3c..059fdea7 100644
--- a/semantica/explorer/session.py
+++ b/semantica/explorer/session.py
@@ -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