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 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(label: str, max_depth: int = 3, inclusive: bool = False, min_degree: int = 0):
 
 
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
- logger.info(f"Inclusive search : {inclusive}, Min degree: {min_degree}, Label: {label}")
48
- return await rag.get_knowledge_graph(node_label=label, max_depth=max_depth, inclusive=inclusive, min_degree=min_degree)
 
 
 
 
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, node_label: str, max_depth: int = 5, search_mode: str = "exact", min_degree: int = 0
 
 
 
 
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 search_mode == "exact":
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 = [node for node, degree in combined_subgraph.degree() if degree >= min_degree]
 
 
 
 
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, node_label: str, max_depth: int, inclusive: bool = False, min_degree: int = 0
 
 
 
 
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
- inclusive: boolean = false
168
  ): Promise<LightragGraphType> => {
169
- const response = await axiosInstance.get(`/graphs?label=${encodeURIComponent(label)}&max_depth=${maxDepth}&inclusive=${inclusive}`)
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
- value={currentValue || ''}
 
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 || lastQueryLabel.maxQueryDepth !== maxQueryDepth) {
 
 
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 }),