yangdx
commited on
Commit
·
08f9fdd
1
Parent(s):
77c6181
Added minimum degree filter for graph queries
Browse files- Introduced min_degree parameter in graph query
- Updated UI to include minimum degree setting
- Modified API to handle min_degree parameter
- Updated graph query logic in LightRAG
- lightrag/api/routers/graph_routes.py +9 -4
- lightrag/kg/networkx_impl.py +13 -5
- lightrag/lightrag.py +7 -1
- lightrag_webui/src/api/lightrag.ts +3 -3
- lightrag_webui/src/components/graph/Settings.tsx +17 -2
- lightrag_webui/src/components/ui/Input.tsx +1 -1
- lightrag_webui/src/hooks/useLightragGraph.tsx +10 -6
- lightrag_webui/src/stores/settings.ts +6 -0
lightrag/api/routers/graph_routes.py
CHANGED
@@ -5,7 +5,6 @@ This module contains all graph-related routes for the LightRAG API.
|
|
5 |
from typing import Optional
|
6 |
from fastapi import APIRouter, Depends
|
7 |
|
8 |
-
from ...utils import logger
|
9 |
from ..utils_api import get_api_key_dependency
|
10 |
|
11 |
router = APIRouter(tags=["graph"])
|
@@ -25,7 +24,9 @@ def create_graph_routes(rag, api_key: Optional[str] = None):
|
|
25 |
return await rag.get_graph_labels()
|
26 |
|
27 |
@router.get("/graphs", dependencies=[Depends(optional_api_key)])
|
28 |
-
async def get_knowledge_graph(
|
|
|
|
|
29 |
"""
|
30 |
Retrieve a connected subgraph of nodes where the label includes the specified label.
|
31 |
Maximum number of nodes is constrained by the environment variable `MAX_GRAPH_NODES` (default: 1000).
|
@@ -44,7 +45,11 @@ def create_graph_routes(rag, api_key: Optional[str] = None):
|
|
44 |
Returns:
|
45 |
Dict[str, List[str]]: Knowledge graph for label
|
46 |
"""
|
47 |
-
|
48 |
-
|
|
|
|
|
|
|
|
|
49 |
|
50 |
return router
|
|
|
5 |
from typing import Optional
|
6 |
from fastapi import APIRouter, Depends
|
7 |
|
|
|
8 |
from ..utils_api import get_api_key_dependency
|
9 |
|
10 |
router = APIRouter(tags=["graph"])
|
|
|
24 |
return await rag.get_graph_labels()
|
25 |
|
26 |
@router.get("/graphs", dependencies=[Depends(optional_api_key)])
|
27 |
+
async def get_knowledge_graph(
|
28 |
+
label: str, max_depth: int = 3, min_degree: int = 0, inclusive: bool = False
|
29 |
+
):
|
30 |
"""
|
31 |
Retrieve a connected subgraph of nodes where the label includes the specified label.
|
32 |
Maximum number of nodes is constrained by the environment variable `MAX_GRAPH_NODES` (default: 1000).
|
|
|
45 |
Returns:
|
46 |
Dict[str, List[str]]: Knowledge graph for label
|
47 |
"""
|
48 |
+
return await rag.get_knowledge_graph(
|
49 |
+
node_label=label,
|
50 |
+
max_depth=max_depth,
|
51 |
+
inclusive=inclusive,
|
52 |
+
min_degree=min_degree,
|
53 |
+
)
|
54 |
|
55 |
return router
|
lightrag/kg/networkx_impl.py
CHANGED
@@ -232,7 +232,11 @@ class NetworkXStorage(BaseGraphStorage):
|
|
232 |
return sorted(list(labels))
|
233 |
|
234 |
async def get_knowledge_graph(
|
235 |
-
self,
|
|
|
|
|
|
|
|
|
236 |
) -> KnowledgeGraph:
|
237 |
"""
|
238 |
Retrieve a connected subgraph of nodes where the label includes the specified `node_label`.
|
@@ -268,7 +272,7 @@ class NetworkXStorage(BaseGraphStorage):
|
|
268 |
nodes_to_explore = []
|
269 |
for n, attr in graph.nodes(data=True):
|
270 |
node_str = str(n)
|
271 |
-
if
|
272 |
if node_label == node_str: # Use exact matching
|
273 |
nodes_to_explore.append(n)
|
274 |
else: # inclusive mode
|
@@ -284,12 +288,16 @@ class NetworkXStorage(BaseGraphStorage):
|
|
284 |
for start_node in nodes_to_explore:
|
285 |
node_subgraph = nx.ego_graph(graph, start_node, radius=max_depth)
|
286 |
combined_subgraph = nx.compose(combined_subgraph, node_subgraph)
|
287 |
-
|
288 |
# Filter nodes based on min_degree
|
289 |
if min_degree > 0:
|
290 |
-
nodes_to_keep = [
|
|
|
|
|
|
|
|
|
291 |
combined_subgraph = combined_subgraph.subgraph(nodes_to_keep)
|
292 |
-
|
293 |
subgraph = combined_subgraph
|
294 |
|
295 |
# Check if number of nodes exceeds max_graph_nodes
|
|
|
232 |
return sorted(list(labels))
|
233 |
|
234 |
async def get_knowledge_graph(
|
235 |
+
self,
|
236 |
+
node_label: str,
|
237 |
+
max_depth: int = 5,
|
238 |
+
min_degree: int = 0,
|
239 |
+
inclusive: bool = False,
|
240 |
) -> KnowledgeGraph:
|
241 |
"""
|
242 |
Retrieve a connected subgraph of nodes where the label includes the specified `node_label`.
|
|
|
272 |
nodes_to_explore = []
|
273 |
for n, attr in graph.nodes(data=True):
|
274 |
node_str = str(n)
|
275 |
+
if not inclusive:
|
276 |
if node_label == node_str: # Use exact matching
|
277 |
nodes_to_explore.append(n)
|
278 |
else: # inclusive mode
|
|
|
288 |
for start_node in nodes_to_explore:
|
289 |
node_subgraph = nx.ego_graph(graph, start_node, radius=max_depth)
|
290 |
combined_subgraph = nx.compose(combined_subgraph, node_subgraph)
|
291 |
+
|
292 |
# Filter nodes based on min_degree
|
293 |
if min_degree > 0:
|
294 |
+
nodes_to_keep = [
|
295 |
+
node
|
296 |
+
for node, degree in combined_subgraph.degree()
|
297 |
+
if degree >= min_degree
|
298 |
+
]
|
299 |
combined_subgraph = combined_subgraph.subgraph(nodes_to_keep)
|
300 |
+
|
301 |
subgraph = combined_subgraph
|
302 |
|
303 |
# Check if number of nodes exceeds max_graph_nodes
|
lightrag/lightrag.py
CHANGED
@@ -504,7 +504,11 @@ class LightRAG:
|
|
504 |
return text
|
505 |
|
506 |
async def get_knowledge_graph(
|
507 |
-
self,
|
|
|
|
|
|
|
|
|
508 |
) -> KnowledgeGraph:
|
509 |
"""Get knowledge graph for a given label
|
510 |
|
@@ -520,6 +524,8 @@ class LightRAG:
|
|
520 |
return await self.chunk_entity_relation_graph.get_knowledge_graph(
|
521 |
node_label=node_label,
|
522 |
max_depth=max_depth,
|
|
|
|
|
523 |
)
|
524 |
|
525 |
def _get_storage_class(self, storage_name: str) -> Callable[..., Any]:
|
|
|
504 |
return text
|
505 |
|
506 |
async def get_knowledge_graph(
|
507 |
+
self,
|
508 |
+
node_label: str,
|
509 |
+
max_depth: int,
|
510 |
+
min_degree: int = 0,
|
511 |
+
inclusive: bool = False,
|
512 |
) -> KnowledgeGraph:
|
513 |
"""Get knowledge graph for a given label
|
514 |
|
|
|
524 |
return await self.chunk_entity_relation_graph.get_knowledge_graph(
|
525 |
node_label=node_label,
|
526 |
max_depth=max_depth,
|
527 |
+
min_degree=min_degree,
|
528 |
+
inclusive=inclusive,
|
529 |
)
|
530 |
|
531 |
def _get_storage_class(self, storage_name: str) -> Callable[..., Any]:
|
lightrag_webui/src/api/lightrag.ts
CHANGED
@@ -162,11 +162,11 @@ axiosInstance.interceptors.response.use(
|
|
162 |
|
163 |
// API methods
|
164 |
export const queryGraphs = async (
|
165 |
-
label: string,
|
166 |
maxDepth: number,
|
167 |
-
|
168 |
): Promise<LightragGraphType> => {
|
169 |
-
const response = await axiosInstance.get(`/graphs?label=${encodeURIComponent(label)}&max_depth=${maxDepth}&
|
170 |
return response.data
|
171 |
}
|
172 |
|
|
|
162 |
|
163 |
// API methods
|
164 |
export const queryGraphs = async (
|
165 |
+
label: string,
|
166 |
maxDepth: number,
|
167 |
+
minDegree: number
|
168 |
): Promise<LightragGraphType> => {
|
169 |
+
const response = await axiosInstance.get(`/graphs?label=${encodeURIComponent(label)}&max_depth=${maxDepth}&min_degree=${minDegree}`)
|
170 |
return response.data
|
171 |
}
|
172 |
|
lightrag_webui/src/components/graph/Settings.tsx
CHANGED
@@ -90,9 +90,12 @@ const LabeledNumberInput = ({
|
|
90 |
{label}
|
91 |
</label>
|
92 |
<Input
|
93 |
-
|
|
|
94 |
onChange={onValueChange}
|
95 |
-
className="h-6 w-full min-w-0"
|
|
|
|
|
96 |
onBlur={onBlur}
|
97 |
onKeyDown={(e) => {
|
98 |
if (e.key === 'Enter') {
|
@@ -119,6 +122,7 @@ export default function Settings() {
|
|
119 |
const enableHideUnselectedEdges = useSettingsStore.use.enableHideUnselectedEdges()
|
120 |
const showEdgeLabel = useSettingsStore.use.showEdgeLabel()
|
121 |
const graphQueryMaxDepth = useSettingsStore.use.graphQueryMaxDepth()
|
|
|
122 |
const graphLayoutMaxIterations = useSettingsStore.use.graphLayoutMaxIterations()
|
123 |
|
124 |
const enableHealthCheck = useSettingsStore.use.enableHealthCheck()
|
@@ -177,6 +181,11 @@ export default function Settings() {
|
|
177 |
useSettingsStore.setState({ graphQueryMaxDepth: depth })
|
178 |
}, [])
|
179 |
|
|
|
|
|
|
|
|
|
|
|
180 |
const setGraphLayoutMaxIterations = useCallback((iterations: number) => {
|
181 |
if (iterations < 1) return
|
182 |
useSettingsStore.setState({ graphLayoutMaxIterations: iterations })
|
@@ -266,6 +275,12 @@ export default function Settings() {
|
|
266 |
value={graphQueryMaxDepth}
|
267 |
onEditFinished={setGraphQueryMaxDepth}
|
268 |
/>
|
|
|
|
|
|
|
|
|
|
|
|
|
269 |
<LabeledNumberInput
|
270 |
label="Max Layout Iterations"
|
271 |
min={1}
|
|
|
90 |
{label}
|
91 |
</label>
|
92 |
<Input
|
93 |
+
type="number"
|
94 |
+
value={currentValue === null ? '' : currentValue}
|
95 |
onChange={onValueChange}
|
96 |
+
className="h-6 w-full min-w-0 pr-1"
|
97 |
+
min={min}
|
98 |
+
max={max}
|
99 |
onBlur={onBlur}
|
100 |
onKeyDown={(e) => {
|
101 |
if (e.key === 'Enter') {
|
|
|
122 |
const enableHideUnselectedEdges = useSettingsStore.use.enableHideUnselectedEdges()
|
123 |
const showEdgeLabel = useSettingsStore.use.showEdgeLabel()
|
124 |
const graphQueryMaxDepth = useSettingsStore.use.graphQueryMaxDepth()
|
125 |
+
const graphMinDegree = useSettingsStore.use.graphMinDegree()
|
126 |
const graphLayoutMaxIterations = useSettingsStore.use.graphLayoutMaxIterations()
|
127 |
|
128 |
const enableHealthCheck = useSettingsStore.use.enableHealthCheck()
|
|
|
181 |
useSettingsStore.setState({ graphQueryMaxDepth: depth })
|
182 |
}, [])
|
183 |
|
184 |
+
const setGraphMinDegree = useCallback((degree: number) => {
|
185 |
+
if (degree < 0) return
|
186 |
+
useSettingsStore.setState({ graphMinDegree: degree })
|
187 |
+
}, [])
|
188 |
+
|
189 |
const setGraphLayoutMaxIterations = useCallback((iterations: number) => {
|
190 |
if (iterations < 1) return
|
191 |
useSettingsStore.setState({ graphLayoutMaxIterations: iterations })
|
|
|
275 |
value={graphQueryMaxDepth}
|
276 |
onEditFinished={setGraphQueryMaxDepth}
|
277 |
/>
|
278 |
+
<LabeledNumberInput
|
279 |
+
label="Minimum Degree"
|
280 |
+
min={0}
|
281 |
+
value={graphMinDegree}
|
282 |
+
onEditFinished={setGraphMinDegree}
|
283 |
+
/>
|
284 |
<LabeledNumberInput
|
285 |
label="Max Layout Iterations"
|
286 |
min={1}
|
lightrag_webui/src/components/ui/Input.tsx
CHANGED
@@ -7,7 +7,7 @@ const Input = React.forwardRef<HTMLInputElement, React.ComponentProps<'input'>>(
|
|
7 |
<input
|
8 |
type={type}
|
9 |
className={cn(
|
10 |
-
'border-input file:text-foreground placeholder:text-muted-foreground focus-visible:ring-ring flex h-9 rounded-md border bg-transparent px-3 py-1 text-base shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium focus-visible:ring-1 focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm',
|
11 |
className
|
12 |
)}
|
13 |
ref={ref}
|
|
|
7 |
<input
|
8 |
type={type}
|
9 |
className={cn(
|
10 |
+
'border-input file:text-foreground placeholder:text-muted-foreground focus-visible:ring-ring flex h-9 rounded-md border bg-transparent px-3 py-1 text-base shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium focus-visible:ring-1 focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm [&::-webkit-inner-spin-button]:opacity-100 [&::-webkit-outer-spin-button]:opacity-100',
|
11 |
className
|
12 |
)}
|
13 |
ref={ref}
|
lightrag_webui/src/hooks/useLightragGraph.tsx
CHANGED
@@ -50,11 +50,11 @@ export type NodeType = {
|
|
50 |
}
|
51 |
export type EdgeType = { label: string }
|
52 |
|
53 |
-
const fetchGraph = async (label: string, maxDepth: number) => {
|
54 |
let rawData: any = null
|
55 |
|
56 |
try {
|
57 |
-
rawData = await queryGraphs(label, maxDepth)
|
58 |
} catch (e) {
|
59 |
useBackendState.getState().setErrorMessage(errorMessage(e), 'Query Graphs Error!')
|
60 |
return null
|
@@ -161,13 +161,14 @@ const createSigmaGraph = (rawGraph: RawGraph | null) => {
|
|
161 |
return graph
|
162 |
}
|
163 |
|
164 |
-
const lastQueryLabel = { label: '', maxQueryDepth: 0 }
|
165 |
|
166 |
const useLightrangeGraph = () => {
|
167 |
const queryLabel = useSettingsStore.use.queryLabel()
|
168 |
const rawGraph = useGraphStore.use.rawGraph()
|
169 |
const sigmaGraph = useGraphStore.use.sigmaGraph()
|
170 |
const maxQueryDepth = useSettingsStore.use.graphQueryMaxDepth()
|
|
|
171 |
|
172 |
const getNode = useCallback(
|
173 |
(nodeId: string) => {
|
@@ -185,13 +186,16 @@ const useLightrangeGraph = () => {
|
|
185 |
|
186 |
useEffect(() => {
|
187 |
if (queryLabel) {
|
188 |
-
if (lastQueryLabel.label !== queryLabel ||
|
|
|
|
|
189 |
lastQueryLabel.label = queryLabel
|
190 |
lastQueryLabel.maxQueryDepth = maxQueryDepth
|
|
|
191 |
|
192 |
const state = useGraphStore.getState()
|
193 |
state.reset()
|
194 |
-
fetchGraph(queryLabel, maxQueryDepth).then((data) => {
|
195 |
// console.debug('Query label: ' + queryLabel)
|
196 |
state.setSigmaGraph(createSigmaGraph(data))
|
197 |
data?.buildDynamicMap()
|
@@ -203,7 +207,7 @@ const useLightrangeGraph = () => {
|
|
203 |
state.reset()
|
204 |
state.setSigmaGraph(new DirectedGraph())
|
205 |
}
|
206 |
-
}, [queryLabel, maxQueryDepth])
|
207 |
|
208 |
const lightrageGraph = useCallback(() => {
|
209 |
if (sigmaGraph) {
|
|
|
50 |
}
|
51 |
export type EdgeType = { label: string }
|
52 |
|
53 |
+
const fetchGraph = async (label: string, maxDepth: number, minDegree: number) => {
|
54 |
let rawData: any = null
|
55 |
|
56 |
try {
|
57 |
+
rawData = await queryGraphs(label, maxDepth, minDegree)
|
58 |
} catch (e) {
|
59 |
useBackendState.getState().setErrorMessage(errorMessage(e), 'Query Graphs Error!')
|
60 |
return null
|
|
|
161 |
return graph
|
162 |
}
|
163 |
|
164 |
+
const lastQueryLabel = { label: '', maxQueryDepth: 0, minDegree: 0 }
|
165 |
|
166 |
const useLightrangeGraph = () => {
|
167 |
const queryLabel = useSettingsStore.use.queryLabel()
|
168 |
const rawGraph = useGraphStore.use.rawGraph()
|
169 |
const sigmaGraph = useGraphStore.use.sigmaGraph()
|
170 |
const maxQueryDepth = useSettingsStore.use.graphQueryMaxDepth()
|
171 |
+
const minDegree = useSettingsStore.use.graphMinDegree()
|
172 |
|
173 |
const getNode = useCallback(
|
174 |
(nodeId: string) => {
|
|
|
186 |
|
187 |
useEffect(() => {
|
188 |
if (queryLabel) {
|
189 |
+
if (lastQueryLabel.label !== queryLabel ||
|
190 |
+
lastQueryLabel.maxQueryDepth !== maxQueryDepth ||
|
191 |
+
lastQueryLabel.minDegree !== minDegree) {
|
192 |
lastQueryLabel.label = queryLabel
|
193 |
lastQueryLabel.maxQueryDepth = maxQueryDepth
|
194 |
+
lastQueryLabel.minDegree = minDegree
|
195 |
|
196 |
const state = useGraphStore.getState()
|
197 |
state.reset()
|
198 |
+
fetchGraph(queryLabel, maxQueryDepth, minDegree).then((data) => {
|
199 |
// console.debug('Query label: ' + queryLabel)
|
200 |
state.setSigmaGraph(createSigmaGraph(data))
|
201 |
data?.buildDynamicMap()
|
|
|
207 |
state.reset()
|
208 |
state.setSigmaGraph(new DirectedGraph())
|
209 |
}
|
210 |
+
}, [queryLabel, maxQueryDepth, minDegree])
|
211 |
|
212 |
const lightrageGraph = useCallback(() => {
|
213 |
if (sigmaGraph) {
|
lightrag_webui/src/stores/settings.ts
CHANGED
@@ -22,6 +22,9 @@ interface SettingsState {
|
|
22 |
graphQueryMaxDepth: number
|
23 |
setGraphQueryMaxDepth: (depth: number) => void
|
24 |
|
|
|
|
|
|
|
25 |
graphLayoutMaxIterations: number
|
26 |
setGraphLayoutMaxIterations: (iterations: number) => void
|
27 |
|
@@ -66,6 +69,7 @@ const useSettingsStoreBase = create<SettingsState>()(
|
|
66 |
enableEdgeEvents: false,
|
67 |
|
68 |
graphQueryMaxDepth: 3,
|
|
|
69 |
graphLayoutMaxIterations: 10,
|
70 |
|
71 |
queryLabel: defaultQueryLabel,
|
@@ -107,6 +111,8 @@ const useSettingsStoreBase = create<SettingsState>()(
|
|
107 |
|
108 |
setGraphQueryMaxDepth: (depth: number) => set({ graphQueryMaxDepth: depth }),
|
109 |
|
|
|
|
|
110 |
setEnableHealthCheck: (enable: boolean) => set({ enableHealthCheck: enable }),
|
111 |
|
112 |
setApiKey: (apiKey: string | null) => set({ apiKey }),
|
|
|
22 |
graphQueryMaxDepth: number
|
23 |
setGraphQueryMaxDepth: (depth: number) => void
|
24 |
|
25 |
+
graphMinDegree: number
|
26 |
+
setGraphMinDegree: (degree: number) => void
|
27 |
+
|
28 |
graphLayoutMaxIterations: number
|
29 |
setGraphLayoutMaxIterations: (iterations: number) => void
|
30 |
|
|
|
69 |
enableEdgeEvents: false,
|
70 |
|
71 |
graphQueryMaxDepth: 3,
|
72 |
+
graphMinDegree: 0,
|
73 |
graphLayoutMaxIterations: 10,
|
74 |
|
75 |
queryLabel: defaultQueryLabel,
|
|
|
111 |
|
112 |
setGraphQueryMaxDepth: (depth: number) => set({ graphQueryMaxDepth: depth }),
|
113 |
|
114 |
+
setGraphMinDegree: (degree: number) => set({ graphMinDegree: degree }),
|
115 |
+
|
116 |
setEnableHealthCheck: (enable: boolean) => set({ enableHealthCheck: enable }),
|
117 |
|
118 |
setApiKey: (apiKey: string | null) => set({ apiKey }),
|