yangdx commited on
Commit
56a0241
·
1 Parent(s): 169a4c0

Refactor graph search to update search engin after node expand or prune

Browse files
lightrag_webui/src/components/graph/GraphSearch.tsx CHANGED
@@ -10,29 +10,27 @@ import { searchResultLimit } from '@/lib/constants'
10
  import { useGraphStore } from '@/stores/graph'
11
  import MiniSearch from 'minisearch'
12
  import { useTranslation } from 'react-i18next'
 
 
13
 
14
- interface OptionItem {
15
- id: string
16
- type: 'nodes' | 'edges' | 'message'
17
- message?: string
 
 
18
  }
19
 
20
  function OptionComponent(item: OptionItem) {
21
  return (
22
  <div>
23
- {item.type === 'nodes' && <NodeById id={item.id} />}
24
  {item.type === 'edges' && <EdgeById id={item.id} />}
25
  {item.type === 'message' && <div>{item.message}</div>}
26
  </div>
27
  )
28
  }
29
 
30
- const messageId = '__message_item'
31
- // Reset this cache when graph changes to ensure fresh search results
32
- const lastGraph: any = {
33
- graph: null,
34
- searchEngine: null
35
- }
36
 
37
  /**
38
  * Component thats display the search input.
@@ -53,18 +51,18 @@ export const GraphSearchInput = ({
53
  useEffect(() => {
54
  if (graph) {
55
  // Reset cache to ensure fresh search results with new graph data
56
- lastGraph.graph = null;
57
- lastGraph.searchEngine = null;
58
  }
59
  }, [graph]);
60
 
61
  const searchEngine = useMemo(() => {
62
- if (lastGraph.graph == graph) {
63
- return lastGraph.searchEngine
64
  }
65
  if (!graph || graph.nodes().length == 0) return
66
 
67
- lastGraph.graph = graph
68
 
69
  const searchEngine = new MiniSearch({
70
  idField: 'id',
@@ -85,7 +83,7 @@ export const GraphSearchInput = ({
85
  }))
86
  searchEngine.addAll(documents)
87
 
88
- lastGraph.searchEngine = searchEngine
89
  return searchEngine
90
  }, [graph])
91
 
@@ -95,22 +93,38 @@ export const GraphSearchInput = ({
95
  const loadOptions = useCallback(
96
  async (query?: string): Promise<OptionItem[]> => {
97
  if (onFocus) onFocus(null)
98
- if (!graph || !searchEngine) return []
 
 
 
 
 
 
 
99
 
100
- // If no query, return first searchResultLimit nodes
 
 
 
 
 
101
  if (!query) {
102
- const nodeIds = graph.nodes().slice(0, searchResultLimit)
 
 
103
  return nodeIds.map(id => ({
104
  id,
105
  type: 'nodes'
106
  }))
107
  }
108
 
109
- // If has query, search nodes
110
- const result: OptionItem[] = searchEngine.search(query).map((r: { id: string }) => ({
111
- id: r.id,
112
- type: 'nodes'
113
- }))
 
 
114
 
115
  // prettier-ignore
116
  return result.length <= searchResultLimit
 
10
  import { useGraphStore } from '@/stores/graph'
11
  import MiniSearch from 'minisearch'
12
  import { useTranslation } from 'react-i18next'
13
+ import { OptionItem } from './graphSearchTypes'
14
+ import { messageId, searchCache } from './graphSearchUtils'
15
 
16
+ const NodeOption = ({ id }: { id: string }) => {
17
+ const graph = useGraphStore.use.sigmaGraph()
18
+ if (!graph?.hasNode(id)) {
19
+ return null
20
+ }
21
+ return <NodeById id={id} />
22
  }
23
 
24
  function OptionComponent(item: OptionItem) {
25
  return (
26
  <div>
27
+ {item.type === 'nodes' && <NodeOption id={item.id} />}
28
  {item.type === 'edges' && <EdgeById id={item.id} />}
29
  {item.type === 'message' && <div>{item.message}</div>}
30
  </div>
31
  )
32
  }
33
 
 
 
 
 
 
 
34
 
35
  /**
36
  * Component thats display the search input.
 
51
  useEffect(() => {
52
  if (graph) {
53
  // Reset cache to ensure fresh search results with new graph data
54
+ searchCache.graph = null;
55
+ searchCache.searchEngine = null;
56
  }
57
  }, [graph]);
58
 
59
  const searchEngine = useMemo(() => {
60
+ if (searchCache.graph == graph) {
61
+ return searchCache.searchEngine
62
  }
63
  if (!graph || graph.nodes().length == 0) return
64
 
65
+ searchCache.graph = graph
66
 
67
  const searchEngine = new MiniSearch({
68
  idField: 'id',
 
83
  }))
84
  searchEngine.addAll(documents)
85
 
86
+ searchCache.searchEngine = searchEngine
87
  return searchEngine
88
  }, [graph])
89
 
 
93
  const loadOptions = useCallback(
94
  async (query?: string): Promise<OptionItem[]> => {
95
  if (onFocus) onFocus(null)
96
+
97
+ // Safety checks to prevent crashes
98
+ if (!graph || !searchEngine) {
99
+ // Reset cache to ensure fresh search engine initialization on next render
100
+ searchCache.graph = null
101
+ searchCache.searchEngine = null
102
+ return []
103
+ }
104
 
105
+ // Verify graph has nodes before proceeding
106
+ if (graph.nodes().length === 0) {
107
+ return []
108
+ }
109
+
110
+ // If no query, return first searchResultLimit nodes that exist
111
  if (!query) {
112
+ const nodeIds = graph.nodes()
113
+ .filter(id => graph.hasNode(id))
114
+ .slice(0, searchResultLimit)
115
  return nodeIds.map(id => ({
116
  id,
117
  type: 'nodes'
118
  }))
119
  }
120
 
121
+ // If has query, search nodes and verify they still exist
122
+ const result: OptionItem[] = searchEngine.search(query)
123
+ .filter((r: { id: string }) => graph.hasNode(r.id))
124
+ .map((r: { id: string }) => ({
125
+ id: r.id,
126
+ type: 'nodes'
127
+ }))
128
 
129
  // prettier-ignore
130
  return result.length <= searchResultLimit
lightrag_webui/src/components/graph/graphSearchTypes.ts ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ export interface OptionItem {
2
+ id: string
3
+ type: 'nodes' | 'edges' | 'message'
4
+ message?: string
5
+ }
lightrag_webui/src/components/graph/graphSearchUtils.ts ADDED
@@ -0,0 +1,28 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { DirectedGraph } from 'graphology'
2
+ import MiniSearch from 'minisearch'
3
+
4
+ export const messageId = '__message_item'
5
+
6
+ // Reset this cache when graph changes to ensure fresh search results
7
+ export const searchCache: {
8
+ graph: DirectedGraph | null;
9
+ searchEngine: MiniSearch | null;
10
+ } = {
11
+ graph: null,
12
+ searchEngine: null
13
+ }
14
+
15
+ export const updateSearchEngine = (nodeId: string, graph: DirectedGraph) => {
16
+ if (!searchCache.searchEngine || !graph) return
17
+
18
+ const newDocument = {
19
+ id: nodeId,
20
+ label: graph.getNodeAttribute(nodeId, 'label')
21
+ }
22
+ searchCache.searchEngine.add(newDocument)
23
+ }
24
+
25
+ export const removeFromSearchEngine = (nodeId: string) => {
26
+ if (!searchCache.searchEngine) return
27
+ searchCache.searchEngine.remove({ id: nodeId })
28
+ }
lightrag_webui/src/hooks/useLightragGraph.tsx CHANGED
@@ -11,6 +11,7 @@ import { useSettingsStore } from '@/stores/settings'
11
  import { useTabVisibility } from '@/contexts/useTabVisibility'
12
 
13
  import seedrandom from 'seedrandom'
 
14
 
15
  const validateGraph = (graph: RawGraph) => {
16
  if (!graph) {
@@ -544,6 +545,8 @@ const useLightrangeGraph = () => {
544
  rawGraph.nodes.push(newNode);
545
  // Update nodeIdMap
546
  rawGraph.nodeIdMap[nodeId] = rawGraph.nodes.length - 1;
 
 
547
  }
548
  }
549
 
@@ -572,8 +575,12 @@ const useLightrangeGraph = () => {
572
  }
573
  }
574
 
575
- // Update the dynamic edge map
576
  rawGraph.buildDynamicMap();
 
 
 
 
577
 
578
  // STEP 4: Update the expanded node's size
579
  if (sigmaGraph.hasNode(nodeId)) {
@@ -710,11 +717,17 @@ const useLightrangeGraph = () => {
710
 
711
  // Remove from nodeIdMap
712
  delete rawGraph.nodeIdMap[nodeToDelete];
 
 
713
  }
714
  }
715
 
716
- // Rebuild the dynamic edge map
717
  rawGraph.buildDynamicMap();
 
 
 
 
718
 
719
  // Show notification if we deleted more than just the selected node
720
  if (nodesToDelete.size > 1) {
 
11
  import { useTabVisibility } from '@/contexts/useTabVisibility'
12
 
13
  import seedrandom from 'seedrandom'
14
+ import { searchCache, updateSearchEngine, removeFromSearchEngine } from '@/components/graph/graphSearchUtils'
15
 
16
  const validateGraph = (graph: RawGraph) => {
17
  if (!graph) {
 
545
  rawGraph.nodes.push(newNode);
546
  // Update nodeIdMap
547
  rawGraph.nodeIdMap[nodeId] = rawGraph.nodes.length - 1;
548
+ // Update search engine with new node
549
+ updateSearchEngine(nodeId, sigmaGraph);
550
  }
551
  }
552
 
 
575
  }
576
  }
577
 
578
+ // Update the dynamic edge map and invalidate search cache
579
  rawGraph.buildDynamicMap();
580
+
581
+ // Force search engine rebuild by invalidating cache
582
+ searchCache.graph = null;
583
+ searchCache.searchEngine = null;
584
 
585
  // STEP 4: Update the expanded node's size
586
  if (sigmaGraph.hasNode(nodeId)) {
 
717
 
718
  // Remove from nodeIdMap
719
  delete rawGraph.nodeIdMap[nodeToDelete];
720
+ // Remove from search engine
721
+ removeFromSearchEngine(nodeToDelete);
722
  }
723
  }
724
 
725
+ // Rebuild the dynamic edge map and invalidate search cache
726
  rawGraph.buildDynamicMap();
727
+
728
+ // Force search engine rebuild by invalidating cache
729
+ searchCache.graph = null;
730
+ searchCache.searchEngine = null;
731
 
732
  // Show notification if we deleted more than just the selected node
733
  if (nodesToDelete.size > 1) {