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