From 1fddc8552ed24bcd799063d172be9ff27ffab579 Mon Sep 17 00:00:00 2001 From: yangdx Date: Wed, 5 Mar 2025 11:37:55 +0800 Subject: [PATCH] Added minimum degree filter for graph queries - 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 | 13 +++++++++---- lightrag/kg/networkx_impl.py | 18 +++++++++++++----- lightrag/lightrag.py | 8 +++++++- lightrag_webui/src/api/lightrag.ts | 6 +++--- .../src/components/graph/Settings.tsx | 19 +++++++++++++++++-- lightrag_webui/src/components/ui/Input.tsx | 2 +- lightrag_webui/src/hooks/useLightragGraph.tsx | 16 ++++++++++------ lightrag_webui/src/stores/settings.ts | 6 ++++++ 8 files changed, 66 insertions(+), 22 deletions(-) diff --git a/lightrag/api/routers/graph_routes.py b/lightrag/api/routers/graph_routes.py index 4d685cc2..d9380094 100644 --- a/lightrag/api/routers/graph_routes.py +++ b/lightrag/api/routers/graph_routes.py @@ -5,7 +5,6 @@ This module contains all graph-related routes for the LightRAG API. from typing import Optional from fastapi import APIRouter, Depends -from ...utils import logger from ..utils_api import get_api_key_dependency router = APIRouter(tags=["graph"]) @@ -25,7 +24,9 @@ def create_graph_routes(rag, api_key: Optional[str] = None): return await rag.get_graph_labels() @router.get("/graphs", dependencies=[Depends(optional_api_key)]) - async def get_knowledge_graph(label: str, max_depth: int = 3, inclusive: bool = False, min_degree: int = 0): + async def get_knowledge_graph( + label: str, max_depth: int = 3, min_degree: int = 0, inclusive: bool = False + ): """ Retrieve a connected subgraph of nodes where the label includes the specified label. 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): Returns: Dict[str, List[str]]: Knowledge graph for label """ - logger.info(f"Inclusive search : {inclusive}, Min degree: {min_degree}, Label: {label}") - return await rag.get_knowledge_graph(node_label=label, max_depth=max_depth, inclusive=inclusive, min_degree=min_degree) + return await rag.get_knowledge_graph( + node_label=label, + max_depth=max_depth, + inclusive=inclusive, + min_degree=min_degree, + ) return router diff --git a/lightrag/kg/networkx_impl.py b/lightrag/kg/networkx_impl.py index ae46e18d..2c76a67d 100644 --- a/lightrag/kg/networkx_impl.py +++ b/lightrag/kg/networkx_impl.py @@ -232,7 +232,11 @@ class NetworkXStorage(BaseGraphStorage): return sorted(list(labels)) async def get_knowledge_graph( - self, node_label: str, max_depth: int = 5, search_mode: str = "exact", min_degree: int = 0 + self, + node_label: str, + max_depth: int = 5, + min_degree: int = 0, + inclusive: bool = False, ) -> KnowledgeGraph: """ Retrieve a connected subgraph of nodes where the label includes the specified `node_label`. @@ -268,7 +272,7 @@ class NetworkXStorage(BaseGraphStorage): nodes_to_explore = [] for n, attr in graph.nodes(data=True): node_str = str(n) - if search_mode == "exact": + if not inclusive: if node_label == node_str: # Use exact matching nodes_to_explore.append(n) else: # inclusive mode @@ -284,12 +288,16 @@ class NetworkXStorage(BaseGraphStorage): for start_node in nodes_to_explore: node_subgraph = nx.ego_graph(graph, start_node, radius=max_depth) combined_subgraph = nx.compose(combined_subgraph, node_subgraph) - + # Filter nodes based on min_degree if min_degree > 0: - nodes_to_keep = [node for node, degree in combined_subgraph.degree() if degree >= min_degree] + nodes_to_keep = [ + node + for node, degree in combined_subgraph.degree() + if degree >= min_degree + ] combined_subgraph = combined_subgraph.subgraph(nodes_to_keep) - + subgraph = combined_subgraph # Check if number of nodes exceeds max_graph_nodes diff --git a/lightrag/lightrag.py b/lightrag/lightrag.py index ce02ad92..298af168 100644 --- a/lightrag/lightrag.py +++ b/lightrag/lightrag.py @@ -504,7 +504,11 @@ class LightRAG: return text async def get_knowledge_graph( - self, node_label: str, max_depth: int, inclusive: bool = False, min_degree: int = 0 + self, + node_label: str, + max_depth: int, + min_degree: int = 0, + inclusive: bool = False, ) -> KnowledgeGraph: """Get knowledge graph for a given label @@ -520,6 +524,8 @@ class LightRAG: return await self.chunk_entity_relation_graph.get_knowledge_graph( node_label=node_label, max_depth=max_depth, + min_degree=min_degree, + inclusive=inclusive, ) def _get_storage_class(self, storage_name: str) -> Callable[..., Any]: diff --git a/lightrag_webui/src/api/lightrag.ts b/lightrag_webui/src/api/lightrag.ts index dbee1954..cba9c964 100644 --- a/lightrag_webui/src/api/lightrag.ts +++ b/lightrag_webui/src/api/lightrag.ts @@ -162,11 +162,11 @@ axiosInstance.interceptors.response.use( // API methods export const queryGraphs = async ( - label: string, + label: string, maxDepth: number, - inclusive: boolean = false + minDegree: number ): Promise => { - const response = await axiosInstance.get(`/graphs?label=${encodeURIComponent(label)}&max_depth=${maxDepth}&inclusive=${inclusive}`) + const response = await axiosInstance.get(`/graphs?label=${encodeURIComponent(label)}&max_depth=${maxDepth}&min_degree=${minDegree}`) return response.data } diff --git a/lightrag_webui/src/components/graph/Settings.tsx b/lightrag_webui/src/components/graph/Settings.tsx index 4d2b998d..67fb1ded 100644 --- a/lightrag_webui/src/components/graph/Settings.tsx +++ b/lightrag_webui/src/components/graph/Settings.tsx @@ -90,9 +90,12 @@ const LabeledNumberInput = ({ {label} { if (e.key === 'Enter') { @@ -119,6 +122,7 @@ export default function Settings() { const enableHideUnselectedEdges = useSettingsStore.use.enableHideUnselectedEdges() const showEdgeLabel = useSettingsStore.use.showEdgeLabel() const graphQueryMaxDepth = useSettingsStore.use.graphQueryMaxDepth() + const graphMinDegree = useSettingsStore.use.graphMinDegree() const graphLayoutMaxIterations = useSettingsStore.use.graphLayoutMaxIterations() const enableHealthCheck = useSettingsStore.use.enableHealthCheck() @@ -177,6 +181,11 @@ export default function Settings() { useSettingsStore.setState({ graphQueryMaxDepth: depth }) }, []) + const setGraphMinDegree = useCallback((degree: number) => { + if (degree < 0) return + useSettingsStore.setState({ graphMinDegree: degree }) + }, []) + const setGraphLayoutMaxIterations = useCallback((iterations: number) => { if (iterations < 1) return useSettingsStore.setState({ graphLayoutMaxIterations: iterations }) @@ -266,6 +275,12 @@ export default function Settings() { value={graphQueryMaxDepth} onEditFinished={setGraphQueryMaxDepth} /> + >( { +const fetchGraph = async (label: string, maxDepth: number, minDegree: number) => { let rawData: any = null try { - rawData = await queryGraphs(label, maxDepth) + rawData = await queryGraphs(label, maxDepth, minDegree) } catch (e) { useBackendState.getState().setErrorMessage(errorMessage(e), 'Query Graphs Error!') return null @@ -161,13 +161,14 @@ const createSigmaGraph = (rawGraph: RawGraph | null) => { return graph } -const lastQueryLabel = { label: '', maxQueryDepth: 0 } +const lastQueryLabel = { label: '', maxQueryDepth: 0, minDegree: 0 } const useLightrangeGraph = () => { const queryLabel = useSettingsStore.use.queryLabel() const rawGraph = useGraphStore.use.rawGraph() const sigmaGraph = useGraphStore.use.sigmaGraph() const maxQueryDepth = useSettingsStore.use.graphQueryMaxDepth() + const minDegree = useSettingsStore.use.graphMinDegree() const getNode = useCallback( (nodeId: string) => { @@ -185,13 +186,16 @@ const useLightrangeGraph = () => { useEffect(() => { if (queryLabel) { - if (lastQueryLabel.label !== queryLabel || lastQueryLabel.maxQueryDepth !== maxQueryDepth) { + if (lastQueryLabel.label !== queryLabel || + lastQueryLabel.maxQueryDepth !== maxQueryDepth || + lastQueryLabel.minDegree !== minDegree) { lastQueryLabel.label = queryLabel lastQueryLabel.maxQueryDepth = maxQueryDepth + lastQueryLabel.minDegree = minDegree const state = useGraphStore.getState() state.reset() - fetchGraph(queryLabel, maxQueryDepth).then((data) => { + fetchGraph(queryLabel, maxQueryDepth, minDegree).then((data) => { // console.debug('Query label: ' + queryLabel) state.setSigmaGraph(createSigmaGraph(data)) data?.buildDynamicMap() @@ -203,7 +207,7 @@ const useLightrangeGraph = () => { state.reset() state.setSigmaGraph(new DirectedGraph()) } - }, [queryLabel, maxQueryDepth]) + }, [queryLabel, maxQueryDepth, minDegree]) const lightrageGraph = useCallback(() => { if (sigmaGraph) { diff --git a/lightrag_webui/src/stores/settings.ts b/lightrag_webui/src/stores/settings.ts index d33969cb..3c99ca40 100644 --- a/lightrag_webui/src/stores/settings.ts +++ b/lightrag_webui/src/stores/settings.ts @@ -22,6 +22,9 @@ interface SettingsState { graphQueryMaxDepth: number setGraphQueryMaxDepth: (depth: number) => void + graphMinDegree: number + setGraphMinDegree: (degree: number) => void + graphLayoutMaxIterations: number setGraphLayoutMaxIterations: (iterations: number) => void @@ -66,6 +69,7 @@ const useSettingsStoreBase = create()( enableEdgeEvents: false, graphQueryMaxDepth: 3, + graphMinDegree: 0, graphLayoutMaxIterations: 10, queryLabel: defaultQueryLabel, @@ -107,6 +111,8 @@ const useSettingsStoreBase = create()( setGraphQueryMaxDepth: (depth: number) => set({ graphQueryMaxDepth: depth }), + setGraphMinDegree: (degree: number) => set({ graphMinDegree: degree }), + setEnableHealthCheck: (enable: boolean) => set({ enableHealthCheck: enable }), setApiKey: (apiKey: string | null) => set({ apiKey }),