Skip to main content

lc_rag/graph_rag/
mod.rs

1// src/retrieval/graph_rag/mod.rs
2//! GraphRAG (Knowledge Graph RAG) module.
3//!
4//! Builds a knowledge graph from documents via LLM-based entity and relation
5//! extraction, detects communities, and supports Global / Local / Hybrid
6//! query modes.
7//!
8//! # Example
9//! ```ignore
10//! use langchainrust::retrieval::graph_rag::{GraphRAG, GraphRAGConfig, QueryMode};
11//! use langchainrust::OpenAIChat;
12//!
13//! let llm = OpenAIChat::new(config);
14//! let graph_rag = GraphRAG::new(llm).with_config(GraphRAGConfig::default());
15//!
16//! graph_rag.add_documents(&docs).await?;
17//! graph_rag.build_communities().await?;
18//! let result = graph_rag.query("What is Rust?", QueryMode::Local).await?;
19//! println!("{}", result.answer);
20//! ```
21
22pub mod community;
23pub mod extractor;
24pub mod graph_store;
25pub mod matcher;
26pub mod query;
27
28pub use graph_store::{Community, Entity, GraphStore, Relation};
29pub use matcher::{EmbeddingMatcher, EntityMatcher, KeywordMatcher};
30pub use query::{GraphRAGResult, QueryMode};
31
32use lc_core::language_models::BaseChatModel;
33use lc_vector_stores::Document;
34use tokio::sync::RwLock;
35
36/// GraphRAG error type.
37#[derive(Debug, thiserror::Error)]
38#[non_exhaustive]
39pub enum GraphRAGError {
40    /// An error from the underlying LLM call.
41    #[error("LLM error: {0}")]
42    LLMError(String),
43
44    /// An error during entity/relation extraction.
45    #[error("Extraction error: {0}")]
46    ExtractionError(String),
47
48    /// An error during query execution.
49    #[error("Query error: {0}")]
50    QueryError(String),
51
52    /// An error during community detection or summarization.
53    #[error("Community error: {0}")]
54    CommunityError(String),
55}
56
57/// Configuration for GraphRAG.
58pub struct GraphRAGConfig {
59    /// Maximum number of entities to extract per document.
60    pub max_entities_per_doc: usize,
61    /// Maximum number of relations to extract per document.
62    pub max_relations_per_doc: usize,
63    /// 社区大小分桶数(P1-3)。
64    ///
65    /// 注意:这是**大小分桶**(size tiering),不是经典层级社区(父-子包含关系)。
66    /// 社区按大小排名均分为这么多档,`Community::level` 表示"第几档大小"。
67    pub community_size_tiers: usize,
68    /// Maximum number of tokens for context in query prompts.
69    /// When set, community summaries or subgraph context is truncated to fit.
70    pub max_context_tokens: Option<usize>,
71    /// Custom entity matcher for local/hybrid queries.
72    /// When None, uses the default KeywordMatcher.
73    pub entity_matcher: Option<Box<dyn EntityMatcher>>,
74}
75
76impl Default for GraphRAGConfig {
77    fn default() -> Self {
78        Self {
79            max_entities_per_doc: 10,
80            max_relations_per_doc: 10,
81            community_size_tiers: 3,
82            max_context_tokens: None,
83            entity_matcher: None,
84        }
85    }
86}
87
88impl GraphRAGConfig {
89    /// Creates a `GraphRAGConfig` with default values.
90    pub fn new() -> Self {
91        Self::default()
92    }
93
94    /// Sets the maximum number of entities to extract per document.
95    pub fn with_max_entities_per_doc(mut self, n: usize) -> Self {
96        self.max_entities_per_doc = n;
97        self
98    }
99
100    /// Sets the maximum number of relations to extract per document.
101    pub fn with_max_relations_per_doc(mut self, n: usize) -> Self {
102        self.max_relations_per_doc = n;
103        self
104    }
105
106    /// 设置社区大小分桶数(P1-3,见 [`GraphRAGConfig::community_size_tiers`])。
107    pub fn with_community_size_tiers(mut self, n: usize) -> Self {
108        self.community_size_tiers = n;
109        self
110    }
111
112    /// Sets the maximum number of tokens for context in query prompts.
113    ///
114    /// When set, community summaries (Global/Hybrid) or subgraph context
115    /// (Local/Hybrid) are truncated from lowest-priority items to fit
116    /// within this budget.
117    pub fn with_max_context_tokens(mut self, tokens: usize) -> Self {
118        self.max_context_tokens = Some(tokens);
119        self
120    }
121
122    /// Sets a custom entity matcher for local/hybrid queries.
123    ///
124    /// When set, the matcher is used instead of the default keyword-based
125    /// matching to find relevant entities.
126    pub fn with_entity_matcher(mut self, matcher: Box<dyn EntityMatcher>) -> Self {
127        self.entity_matcher = Some(matcher);
128        self
129    }
130}
131
132/// GraphRAG: Knowledge Graph-based Retrieval Augmented Generation.
133///
134/// Wraps an LLM for entity/relation extraction and community summarization,
135/// and an in-memory [`GraphStore`] for graph operations.
136pub struct GraphRAG<M: BaseChatModel> {
137    llm: M,
138    store: RwLock<GraphStore>,
139    config: GraphRAGConfig,
140}
141
142impl<M: BaseChatModel> GraphRAG<M> {
143    /// Creates a new GraphRAG instance with the given LLM.
144    pub fn new(llm: M) -> Self {
145        Self {
146            llm,
147            store: RwLock::new(GraphStore::new()),
148            config: GraphRAGConfig::default(),
149        }
150    }
151
152    /// Sets a custom configuration.
153    pub fn with_config(mut self, config: GraphRAGConfig) -> Self {
154        self.config = config;
155        self
156    }
157
158    /// Adds documents to the knowledge graph by extracting entities and
159    /// relations from each document via the LLM.
160    pub async fn add_documents(&self, docs: &[Document]) -> Result<(), GraphRAGError> {
161        for doc in docs {
162            let extraction = extractor::extract(
163                &self.llm,
164                &doc.content,
165                self.config.max_entities_per_doc,
166                self.config.max_relations_per_doc,
167            )
168            .await?;
169
170            let doc_id = doc.id.clone();
171            let mut store = self.store.write().await;
172
173            // Build a name-to-id map for deduplication.
174            let mut name_to_id: std::collections::HashMap<String, String> = store
175                .all_entities()
176                .values()
177                .map(|e| (e.name.to_lowercase(), e.id.clone()))
178                .collect();
179
180            // Insert extracted entities (deduplicate by name).
181            for ext_ent in &extraction.entities {
182                let key = ext_ent.name.to_lowercase();
183                if let Some(_existing_id) = name_to_id.get(&key) {
184                    // Entity already exists; skip (M57: log instead of silent discard).
185                    log::info!("GraphRAG: skipping duplicate entity '{}'", ext_ent.name);
186                    continue;
187                }
188
189                let id = format!("e_{}", uuid::Uuid::new_v4().as_simple());
190                name_to_id.insert(key, id.clone());
191
192                store.add_entity(Entity {
193                    id,
194                    name: ext_ent.name.clone(),
195                    entity_type: ext_ent.entity_type.clone(),
196                    description: ext_ent.description.clone(),
197                });
198            }
199
200            // Insert extracted relations (resolve names to ids).
201            for ext_rel in &extraction.relations {
202                let source_key = ext_rel.source.to_lowercase();
203                let target_key = ext_rel.target.to_lowercase();
204
205                let source_id = match name_to_id.get(&source_key) {
206                    Some(id) => id.clone(),
207                    None => {
208                        log::info!(
209                            "GraphRAG: skipping relation with unknown source entity '{}'",
210                            ext_rel.source
211                        );
212                        continue;
213                    }
214                };
215                let target_id = match name_to_id.get(&target_key) {
216                    Some(id) => id.clone(),
217                    None => {
218                        log::info!(
219                            "GraphRAG: skipping relation with unknown target entity '{}'",
220                            ext_rel.target
221                        );
222                        continue;
223                    }
224                };
225
226                store.add_relation(Relation {
227                    source: source_id,
228                    target: target_id,
229                    relation_type: ext_rel.relation_type.clone(),
230                    description: ext_rel.description.clone(),
231                    doc_id: doc_id.clone(),
232                });
233            }
234        }
235
236        Ok(())
237    }
238
239    /// Runs community detection and generates community summaries via the LLM.
240    pub async fn build_communities(&self) -> Result<(), GraphRAGError> {
241        let communities = {
242            let store = self.store.read().await;
243            community::detect_communities(&store, self.config.community_size_tiers)
244        };
245
246        // Generate summaries for each community.
247        let mut summaries = Vec::with_capacity(communities.len());
248        for comm in &communities {
249            let store_clone = {
250                let store = self.store.read().await;
251                store.clone()
252            };
253            let summary = community::summarize_community(&self.llm, &store_clone, comm).await?;
254            summaries.push(summary);
255        }
256
257        // Write communities and summaries back.
258        let mut store = self.store.write().await;
259        store.set_communities(communities);
260        store.set_community_summaries(summaries);
261
262        Ok(())
263    }
264
265    /// Queries the knowledge graph using the specified mode.
266    pub async fn query(&self, q: &str, mode: QueryMode) -> Result<GraphRAGResult, GraphRAGError> {
267        let store = {
268            let guard = self.store.read().await;
269            guard.clone()
270        };
271
272        let max_tokens = self.config.max_context_tokens;
273
274        match mode {
275            QueryMode::Global => query::global_query(&self.llm, &store, q, max_tokens).await,
276            QueryMode::Local => {
277                let matcher = self.config.entity_matcher.as_deref();
278                query::local_query(&self.llm, &store, q, max_tokens, matcher).await
279            }
280            QueryMode::Hybrid => {
281                let matcher = self.config.entity_matcher.as_deref();
282                query::hybrid_query(&self.llm, &store, q, max_tokens, matcher).await
283            }
284        }
285    }
286
287    /// Returns the number of entities in the graph.
288    pub async fn entity_count(&self) -> usize {
289        let store = self.store.read().await;
290        store.entity_count()
291    }
292
293    /// Returns the number of relations in the graph.
294    pub async fn relation_count(&self) -> usize {
295        let store = self.store.read().await;
296        store.relation_count()
297    }
298
299    /// Returns the number of communities.
300    pub async fn community_count(&self) -> usize {
301        let store = self.store.read().await;
302        store.communities().len()
303    }
304}
305
306#[cfg(test)]
307mod tests {
308    use super::*;
309
310    #[test]
311    fn test_graph_rag_config_default() {
312        let config = GraphRAGConfig::default();
313        assert_eq!(config.max_entities_per_doc, 10);
314        assert_eq!(config.max_relations_per_doc, 10);
315        assert_eq!(config.community_size_tiers, 3);
316        assert!(config.max_context_tokens.is_none());
317    }
318
319    #[test]
320    fn test_graph_rag_config_builder() {
321        let config = GraphRAGConfig::new()
322            .with_max_entities_per_doc(5)
323            .with_max_relations_per_doc(8)
324            .with_community_size_tiers(2);
325
326        assert_eq!(config.max_entities_per_doc, 5);
327        assert_eq!(config.max_relations_per_doc, 8);
328        assert_eq!(config.community_size_tiers, 2);
329    }
330
331    #[test]
332    fn test_graph_error_display() {
333        let err = GraphRAGError::LLMError("timeout".into());
334        assert!(err.to_string().contains("timeout"));
335
336        let err = GraphRAGError::ExtractionError("bad json".into());
337        assert!(err.to_string().contains("bad json"));
338
339        let err = GraphRAGError::QueryError("no entities".into());
340        assert!(err.to_string().contains("no entities"));
341    }
342}