Skip to main content

lc_rag/graph_rag/
graph_store.rs

1// src/retrieval/graph_rag/graph_store.rs
2//! In-memory graph store using adjacency lists.
3//!
4//! Stores entities, relations, and communities without any external graph crate.
5
6use std::collections::HashMap;
7
8/// A named entity extracted from a document.
9#[derive(Debug, Clone)]
10pub struct Entity {
11    /// Unique entity id.
12    pub id: String,
13    /// Entity name.
14    pub name: String,
15    /// Entity type (e.g. Person, Organization, Concept).
16    pub entity_type: String,
17    /// Description of the entity.
18    pub description: String,
19}
20
21/// A directed relation between two entities.
22#[derive(Debug, Clone)]
23pub struct Relation {
24    /// Id of the source entity.
25    pub source: String,
26    /// Id of the target entity.
27    pub target: String,
28    /// Relation type (e.g. works_at, part_of).
29    pub relation_type: String,
30    /// Description of the relation.
31    pub description: String,
32    /// Optional id of the document the relation was extracted from.
33    pub doc_id: Option<String>,
34}
35
36/// A community of entities detected by hierarchical Leiden community
37/// detection.
38#[derive(Debug, Clone)]
39pub struct Community {
40    /// Globally unique community id. Communities are stored level by level,
41    /// so a community's id also indexes its summary in the parallel summary
42    /// vector.
43    pub id: usize,
44    /// Entity ids belonging to this community. Communities above level 0
45    /// contain the union of their children's entities.
46    pub entities: Vec<String>,
47    /// Hierarchy level: 0 is the base Leiden partition; higher levels group
48    /// communities from the level below (parent-child containment).
49    pub level: usize,
50    /// Id of the coarser community at `level + 1` containing this one.
51    /// `None` when this community has no coarser grouping (or it is at the
52    /// top level).
53    pub parent: Option<usize>,
54}
55
56/// In-memory graph store backed by adjacency lists.
57#[derive(Clone)]
58pub struct GraphStore {
59    entities: HashMap<String, Entity>,
60    relations: Vec<Relation>,
61    adjacency: HashMap<String, Vec<usize>>,
62    communities: Vec<Community>,
63    community_summaries: Vec<String>,
64}
65
66impl GraphStore {
67    /// Creates an empty graph store.
68    pub fn new() -> Self {
69        Self {
70            entities: HashMap::new(),
71            relations: Vec::new(),
72            adjacency: HashMap::new(),
73            communities: Vec::new(),
74            community_summaries: Vec::new(),
75        }
76    }
77
78    /// Adds an entity, replacing any existing entity with the same id.
79    pub fn add_entity(&mut self, entity: Entity) {
80        self.entities.insert(entity.id.clone(), entity);
81    }
82
83    /// Adds a directed relation and updates the adjacency list.
84    pub fn add_relation(&mut self, relation: Relation) {
85        let idx = self.relations.len();
86        self.adjacency
87            .entry(relation.source.clone())
88            .or_default()
89            .push(idx);
90        self.adjacency
91            .entry(relation.target.clone())
92            .or_default()
93            .push(idx);
94        self.relations.push(relation);
95    }
96
97    /// Returns a reference to an entity by id.
98    pub fn get_entity(&self, id: &str) -> Option<&Entity> {
99        self.entities.get(id)
100    }
101
102    /// Returns all entity ids.
103    pub fn entity_ids(&self) -> Vec<String> {
104        self.entities.keys().cloned().collect()
105    }
106
107    /// Returns the number of entities.
108    pub fn entity_count(&self) -> usize {
109        self.entities.len()
110    }
111
112    /// Returns the number of relations.
113    pub fn relation_count(&self) -> usize {
114        self.relations.len()
115    }
116
117    /// Returns the neighbor entity ids of a given entity (both directions).
118    pub fn neighbors(&self, entity_id: &str) -> Vec<String> {
119        let mut result = Vec::new();
120        if let Some(indices) = self.adjacency.get(entity_id) {
121            for &idx in indices {
122                let rel = &self.relations[idx];
123                if rel.source == entity_id {
124                    result.push(rel.target.clone());
125                } else {
126                    result.push(rel.source.clone());
127                }
128            }
129        }
130        result.sort();
131        result.dedup();
132        result
133    }
134
135    /// Returns the subgraph around a seed entity: the seed, its direct
136    /// neighbors, and all relations among them.
137    pub fn subgraph(&self, seed: &str, depth: usize) -> (Vec<Entity>, Vec<Relation>) {
138        let mut visited = std::collections::HashSet::new();
139        let mut frontier = vec![seed.to_string()];
140        visited.insert(seed.to_string());
141
142        for _ in 0..depth {
143            let mut next_frontier = Vec::new();
144            for eid in &frontier {
145                for nb in self.neighbors(eid) {
146                    if visited.insert(nb.clone()) {
147                        next_frontier.push(nb);
148                    }
149                }
150            }
151            frontier = next_frontier;
152        }
153
154        let entities: Vec<Entity> = visited
155            .iter()
156            .filter_map(|id| self.entities.get(id))
157            .cloned()
158            .collect();
159
160        let relations: Vec<Relation> = self
161            .relations
162            .iter()
163            .filter(|r| visited.contains(&r.source) && visited.contains(&r.target))
164            .cloned()
165            .collect();
166
167        (entities, relations)
168    }
169
170    /// Returns all relations involving a given entity.
171    pub fn relations_for(&self, entity_id: &str) -> Vec<&Relation> {
172        self.adjacency
173            .get(entity_id)
174            .map(|indices| indices.iter().map(|&i| &self.relations[i]).collect())
175            .unwrap_or_default()
176    }
177
178    /// Returns a reference to all entities.
179    pub fn all_entities(&self) -> &HashMap<String, Entity> {
180        &self.entities
181    }
182
183    /// Returns a reference to all relations.
184    pub fn all_relations(&self) -> &[Relation] {
185        &self.relations
186    }
187
188    // -- Community management --------------------------------------------------
189
190    /// Replaces the stored communities.
191    pub fn set_communities(&mut self, communities: Vec<Community>) {
192        self.communities = communities;
193    }
194
195    /// Returns a reference to the stored communities.
196    pub fn communities(&self) -> &[Community] {
197        &self.communities
198    }
199
200    /// Replaces the stored community summaries.
201    pub fn set_community_summaries(&mut self, summaries: Vec<String>) {
202        self.community_summaries = summaries;
203    }
204
205    /// Returns a reference to the stored community summaries.
206    pub fn community_summaries(&self) -> &[String] {
207        &self.community_summaries
208    }
209
210    /// Returns a mutable reference to the stored community summaries.
211    pub fn community_summaries_mut(&mut self) -> &mut Vec<String> {
212        &mut self.community_summaries
213    }
214}
215
216impl Default for GraphStore {
217    fn default() -> Self {
218        Self::new()
219    }
220}
221
222#[cfg(test)]
223mod tests {
224    use super::*;
225
226    fn make_entity(id: &str, name: &str) -> Entity {
227        Entity {
228            id: id.to_string(),
229            name: name.to_string(),
230            entity_type: "concept".to_string(),
231            description: format!("Entity {}", name),
232        }
233    }
234
235    fn make_relation(source: &str, target: &str, rel_type: &str) -> Relation {
236        Relation {
237            source: source.to_string(),
238            target: target.to_string(),
239            relation_type: rel_type.to_string(),
240            description: format!("{} {}", rel_type, target),
241            doc_id: None,
242        }
243    }
244
245    #[test]
246    fn test_add_and_get_entity() {
247        let mut store = GraphStore::new();
248        store.add_entity(make_entity("e1", "Rust"));
249        assert_eq!(store.entity_count(), 1);
250        assert!(store.get_entity("e1").is_some());
251        assert!(store.get_entity("e2").is_none());
252    }
253
254    #[test]
255    fn test_add_entity_replaces_existing() {
256        let mut store = GraphStore::new();
257        store.add_entity(make_entity("e1", "Rust"));
258        store.add_entity(Entity {
259            id: "e1".to_string(),
260            name: "Rust Lang".to_string(),
261            entity_type: "language".to_string(),
262            description: "Updated".to_string(),
263        });
264        assert_eq!(store.entity_count(), 1);
265        assert_eq!(store.get_entity("e1").unwrap().name, "Rust Lang");
266    }
267
268    #[test]
269    fn test_add_relation_and_count() {
270        let mut store = GraphStore::new();
271        store.add_entity(make_entity("e1", "Alice"));
272        store.add_entity(make_entity("e2", "Bob"));
273        store.add_relation(make_relation("e1", "e2", "mentors"));
274        assert_eq!(store.relation_count(), 1);
275    }
276
277    #[test]
278    fn test_neighbors_bidirectional() {
279        let mut store = GraphStore::new();
280        store.add_entity(make_entity("e1", "Alice"));
281        store.add_entity(make_entity("e2", "Bob"));
282        store.add_relation(make_relation("e1", "e2", "mentors"));
283
284        let alice_neighbors = store.neighbors("e1");
285        assert_eq!(alice_neighbors, vec!["e2"]);
286
287        let bob_neighbors = store.neighbors("e2");
288        assert_eq!(bob_neighbors, vec!["e1"]);
289    }
290
291    #[test]
292    fn test_neighbors_dedup() {
293        let mut store = GraphStore::new();
294        store.add_entity(make_entity("e1", "Alice"));
295        store.add_entity(make_entity("e2", "Bob"));
296        store.add_relation(make_relation("e1", "e2", "mentor"));
297        store.add_relation(make_relation("e1", "e2", "collaborator"));
298
299        let neighbors = store.neighbors("e1");
300        assert_eq!(neighbors, vec!["e2"]); // deduped
301    }
302
303    #[test]
304    fn test_subgraph_depth_1() {
305        let mut store = GraphStore::new();
306        store.add_entity(make_entity("e1", "Alice"));
307        store.add_entity(make_entity("e2", "Bob"));
308        store.add_entity(make_entity("e3", "Charlie"));
309        store.add_relation(make_relation("e1", "e2", "mentor"));
310        store.add_relation(make_relation("e2", "e3", "colleague"));
311
312        let (entities, relations) = store.subgraph("e1", 1);
313        // Depth 1 from Alice: Alice + Bob
314        assert_eq!(entities.len(), 2);
315        assert_eq!(relations.len(), 1); // only Alice->Bob
316    }
317
318    #[test]
319    fn test_community_management() {
320        let mut store = GraphStore::new();
321        store.set_communities(vec![Community {
322            id: 0,
323            entities: vec!["e1".to_string(), "e2".to_string()],
324            level: 0,
325            parent: None,
326        }]);
327        assert_eq!(store.communities().len(), 1);
328
329        store.set_community_summaries(vec!["Community of Alice and Bob".to_string()]);
330        assert_eq!(store.community_summaries().len(), 1);
331        assert_eq!(store.community_summaries()[0], "Community of Alice and Bob");
332    }
333
334    #[test]
335    fn test_entity_ids() {
336        let mut store = GraphStore::new();
337        store.add_entity(make_entity("e1", "A"));
338        store.add_entity(make_entity("e2", "B"));
339        let mut ids = store.entity_ids();
340        ids.sort();
341        assert_eq!(ids, vec!["e1", "e2"]);
342    }
343
344    #[test]
345    fn test_relations_for_entity() {
346        let mut store = GraphStore::new();
347        store.add_entity(make_entity("e1", "Alice"));
348        store.add_entity(make_entity("e2", "Bob"));
349        store.add_entity(make_entity("e3", "Charlie"));
350        store.add_relation(make_relation("e1", "e2", "mentor"));
351        store.add_relation(make_relation("e1", "e3", "advisor"));
352
353        let rels = store.relations_for("e1");
354        assert_eq!(rels.len(), 2);
355    }
356
357    #[test]
358    fn test_default_is_empty() {
359        let store = GraphStore::default();
360        assert_eq!(store.entity_count(), 0);
361        assert_eq!(store.relation_count(), 0);
362        assert!(store.communities().is_empty());
363        assert!(store.community_summaries().is_empty());
364    }
365}