From f4fceca7f30bc2739eb5f3dfc92383aa3610c27f Mon Sep 17 00:00:00 2001 From: yangdx Date: Sat, 15 Mar 2025 10:52:47 +0800 Subject: [PATCH] Refactor graph search to update search engin after node expand or prune --- .../src/components/graph/GraphSearch.tsx | 64 +++++++++++-------- .../src/components/graph/graphSearchTypes.ts | 5 ++ .../src/components/graph/graphSearchUtils.ts | 28 ++++++++ lightrag_webui/src/hooks/useLightragGraph.tsx | 17 ++++- 4 files changed, 87 insertions(+), 27 deletions(-) create mode 100644 lightrag_webui/src/components/graph/graphSearchTypes.ts create mode 100644 lightrag_webui/src/components/graph/graphSearchUtils.ts diff --git a/lightrag_webui/src/components/graph/GraphSearch.tsx b/lightrag_webui/src/components/graph/GraphSearch.tsx index 2ba36bda..0570acf3 100644 --- a/lightrag_webui/src/components/graph/GraphSearch.tsx +++ b/lightrag_webui/src/components/graph/GraphSearch.tsx @@ -10,29 +10,27 @@ import { searchResultLimit } from '@/lib/constants' import { useGraphStore } from '@/stores/graph' import MiniSearch from 'minisearch' import { useTranslation } from 'react-i18next' +import { OptionItem } from './graphSearchTypes' +import { messageId, searchCache } from './graphSearchUtils' -interface OptionItem { - id: string - type: 'nodes' | 'edges' | 'message' - message?: string +const NodeOption = ({ id }: { id: string }) => { + const graph = useGraphStore.use.sigmaGraph() + if (!graph?.hasNode(id)) { + return null + } + return } function OptionComponent(item: OptionItem) { return (
- {item.type === 'nodes' && } + {item.type === 'nodes' && } {item.type === 'edges' && } {item.type === 'message' &&
{item.message}
}
) } -const messageId = '__message_item' -// Reset this cache when graph changes to ensure fresh search results -const lastGraph: any = { - graph: null, - searchEngine: null -} /** * Component thats display the search input. @@ -53,18 +51,18 @@ export const GraphSearchInput = ({ useEffect(() => { if (graph) { // Reset cache to ensure fresh search results with new graph data - lastGraph.graph = null; - lastGraph.searchEngine = null; + searchCache.graph = null; + searchCache.searchEngine = null; } }, [graph]); const searchEngine = useMemo(() => { - if (lastGraph.graph == graph) { - return lastGraph.searchEngine + if (searchCache.graph == graph) { + return searchCache.searchEngine } if (!graph || graph.nodes().length == 0) return - lastGraph.graph = graph + searchCache.graph = graph const searchEngine = new MiniSearch({ idField: 'id', @@ -85,7 +83,7 @@ export const GraphSearchInput = ({ })) searchEngine.addAll(documents) - lastGraph.searchEngine = searchEngine + searchCache.searchEngine = searchEngine return searchEngine }, [graph]) @@ -95,22 +93,38 @@ export const GraphSearchInput = ({ const loadOptions = useCallback( async (query?: string): Promise => { if (onFocus) onFocus(null) - if (!graph || !searchEngine) return [] + + // Safety checks to prevent crashes + if (!graph || !searchEngine) { + // Reset cache to ensure fresh search engine initialization on next render + searchCache.graph = null + searchCache.searchEngine = null + return [] + } - // If no query, return first searchResultLimit nodes + // Verify graph has nodes before proceeding + if (graph.nodes().length === 0) { + return [] + } + + // If no query, return first searchResultLimit nodes that exist if (!query) { - const nodeIds = graph.nodes().slice(0, searchResultLimit) + const nodeIds = graph.nodes() + .filter(id => graph.hasNode(id)) + .slice(0, searchResultLimit) return nodeIds.map(id => ({ id, type: 'nodes' })) } - // If has query, search nodes - const result: OptionItem[] = searchEngine.search(query).map((r: { id: string }) => ({ - id: r.id, - type: 'nodes' - })) + // If has query, search nodes and verify they still exist + const result: OptionItem[] = searchEngine.search(query) + .filter((r: { id: string }) => graph.hasNode(r.id)) + .map((r: { id: string }) => ({ + id: r.id, + type: 'nodes' + })) // prettier-ignore return result.length <= searchResultLimit diff --git a/lightrag_webui/src/components/graph/graphSearchTypes.ts b/lightrag_webui/src/components/graph/graphSearchTypes.ts new file mode 100644 index 00000000..80e392da --- /dev/null +++ b/lightrag_webui/src/components/graph/graphSearchTypes.ts @@ -0,0 +1,5 @@ +export interface OptionItem { + id: string + type: 'nodes' | 'edges' | 'message' + message?: string +} diff --git a/lightrag_webui/src/components/graph/graphSearchUtils.ts b/lightrag_webui/src/components/graph/graphSearchUtils.ts new file mode 100644 index 00000000..6f4cd4c9 --- /dev/null +++ b/lightrag_webui/src/components/graph/graphSearchUtils.ts @@ -0,0 +1,28 @@ +import { DirectedGraph } from 'graphology' +import MiniSearch from 'minisearch' + +export const messageId = '__message_item' + +// Reset this cache when graph changes to ensure fresh search results +export const searchCache: { + graph: DirectedGraph | null; + searchEngine: MiniSearch | null; +} = { + graph: null, + searchEngine: null +} + +export const updateSearchEngine = (nodeId: string, graph: DirectedGraph) => { + if (!searchCache.searchEngine || !graph) return + + const newDocument = { + id: nodeId, + label: graph.getNodeAttribute(nodeId, 'label') + } + searchCache.searchEngine.add(newDocument) +} + +export const removeFromSearchEngine = (nodeId: string) => { + if (!searchCache.searchEngine) return + searchCache.searchEngine.remove({ id: nodeId }) +} diff --git a/lightrag_webui/src/hooks/useLightragGraph.tsx b/lightrag_webui/src/hooks/useLightragGraph.tsx index 98c7fc0a..bf727138 100644 --- a/lightrag_webui/src/hooks/useLightragGraph.tsx +++ b/lightrag_webui/src/hooks/useLightragGraph.tsx @@ -11,6 +11,7 @@ import { useSettingsStore } from '@/stores/settings' import { useTabVisibility } from '@/contexts/useTabVisibility' import seedrandom from 'seedrandom' +import { searchCache, updateSearchEngine, removeFromSearchEngine } from '@/components/graph/graphSearchUtils' const validateGraph = (graph: RawGraph) => { if (!graph) { @@ -544,6 +545,8 @@ const useLightrangeGraph = () => { rawGraph.nodes.push(newNode); // Update nodeIdMap rawGraph.nodeIdMap[nodeId] = rawGraph.nodes.length - 1; + // Update search engine with new node + updateSearchEngine(nodeId, sigmaGraph); } } @@ -572,8 +575,12 @@ const useLightrangeGraph = () => { } } - // Update the dynamic edge map + // Update the dynamic edge map and invalidate search cache rawGraph.buildDynamicMap(); + + // Force search engine rebuild by invalidating cache + searchCache.graph = null; + searchCache.searchEngine = null; // STEP 4: Update the expanded node's size if (sigmaGraph.hasNode(nodeId)) { @@ -710,11 +717,17 @@ const useLightrangeGraph = () => { // Remove from nodeIdMap delete rawGraph.nodeIdMap[nodeToDelete]; + // Remove from search engine + removeFromSearchEngine(nodeToDelete); } } - // Rebuild the dynamic edge map + // Rebuild the dynamic edge map and invalidate search cache rawGraph.buildDynamicMap(); + + // Force search engine rebuild by invalidating cache + searchCache.graph = null; + searchCache.searchEngine = null; // Show notification if we deleted more than just the selected node if (nodesToDelete.size > 1) {