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