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 |
-
|
15 |
-
|
16 |
-
|
17 |
-
|
|
|
|
|
18 |
}
|
19 |
|
20 |
function OptionComponent(item: OptionItem) {
|
21 |
return (
|
22 |
<div>
|
23 |
-
{item.type === 'nodes' && <
|
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 |
-
|
57 |
-
|
58 |
}
|
59 |
}, [graph]);
|
60 |
|
61 |
const searchEngine = useMemo(() => {
|
62 |
-
if (
|
63 |
-
return
|
64 |
}
|
65 |
if (!graph || graph.nodes().length == 0) return
|
66 |
|
67 |
-
|
68 |
|
69 |
const searchEngine = new MiniSearch({
|
70 |
idField: 'id',
|
@@ -85,7 +83,7 @@ export const GraphSearchInput = ({
|
|
85 |
}))
|
86 |
searchEngine.addAll(documents)
|
87 |
|
88 |
-
|
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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
99 |
|
100 |
-
//
|
|
|
|
|
|
|
|
|
|
|
101 |
if (!query) {
|
102 |
-
const nodeIds = graph.nodes()
|
|
|
|
|
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)
|
111 |
-
id: r.id
|
112 |
-
|
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) {
|