obsidian_parser/vault/vault_petgraph/
mod.rs

1//! Graph analysis for Obsidian vaults using [`petgraph`](https://docs.rs/petgraph/latest/petgraph)
2//!
3//! This module provides functionality to convert an Obsidian vault into:
4//! - **Directed graphs** ([`DiGraph`]) where edges represent one-way links
5//! - **Undirected graphs** ([`UnGraph`]) where connections are bidirectional
6//!
7//! # Key Features
8//! - Efficient graph construction using parallel processing (with `rayon` feature)
9//! - Smart link parsing that handles Obsidian's link formats
10//! - Memory-friendly design (prefer [`NoteOnDisk`](crate::prelude::NoteOnDisk) for large vaults)
11//!
12//! # Why [`NoteOnDisk`](crate::prelude::NoteOnDisk) > [`NoteInMemory`](crate::prelude::NoteInMemory)?
13//! [`NoteOnDisk`](crate::prelude::NoteOnDisk) is recommended for large vaults because:
14//! 1. **Lower memory usage**: Only reads file content on demand
15//! 2. **Better scalability**: Avoids loading entire vault into RAM
16//! 3. **Faster initialization**: Defers parsing until needed
17//!
18//! Use [`NoteInMemory`](crate::prelude::NoteInMemory) only for small vaults or when you
19//! need repeated access to content.
20//!
21//! # Requirements
22//! Enable [`petgraph`](https://docs.rs/petgraph/latest/petgraph) feature in Cargo.toml:
23//! ```toml
24//! [dependencies]
25//! obsidian-parser = { version = "0.", features = ["petgraph"] }
26//! ```
27
28mod graph_builder;
29mod index;
30
31use super::Vault;
32use crate::note::Note;
33use graph_builder::GraphBuilder;
34use petgraph::{
35    EdgeType, Graph,
36    graph::{DiGraph, UnGraph},
37};
38use std::marker::{Send, Sync};
39
40impl<F> Vault<F>
41where
42    F: Note,
43{
44    #[cfg_attr(docsrs, doc(cfg(feature = "petgraph")))]
45    #[cfg_attr(feature = "tracing", tracing::instrument(skip(self), fields(path = %self.path.display(), count_notes = %self.notes.len())))]
46    fn get_graph<Ty>(&self) -> Result<Graph<&F, (), Ty>, F::Error>
47    where
48        Ty: EdgeType,
49    {
50        #[cfg(feature = "tracing")]
51        tracing::debug!("Building graph");
52
53        let graph_builder = GraphBuilder::new(self);
54        graph_builder.build()
55    }
56
57    #[cfg_attr(docsrs, doc(cfg(feature = "petgraph")))]
58    #[cfg_attr(docsrs, doc(cfg(feature = "rayon")))]
59    #[cfg(feature = "rayon")]
60    fn par_get_graph<Ty>(&self) -> Result<Graph<&F, (), Ty>, F::Error>
61    where
62        F: Send + Sync,
63        F::Error: Send,
64        Ty: EdgeType + Send,
65    {
66        #[cfg(feature = "tracing")]
67        tracing::debug!("Building graph with parallel");
68
69        let graph_builder = GraphBuilder::new(self);
70        graph_builder.par_build()
71    }
72
73    /// Builds directed graph representing note relationships
74    ///
75    /// Edges point from source note to linked note (A → B means A links to B)
76    ///
77    /// # Performance Notes
78    /// - For vaults with 1000+ notes, enable `rayon` feature
79    /// - Uses [`NoteOnDisk`](crate::prelude::NoteOnDisk) for minimal memory footprint
80    ///
81    /// # Other
82    /// See [`get_ungraph`](Vault::get_ungraph)
83    #[cfg_attr(docsrs, doc(cfg(feature = "petgraph")))]
84    #[cfg_attr(feature = "tracing", tracing::instrument(skip(self), fields(path = %self.path.display(), count_notes = %self.notes.len())))]
85    pub fn get_digraph(&self) -> Result<DiGraph<&F, ()>, F::Error> {
86        #[cfg(feature = "tracing")]
87        tracing::debug!("Building directed graph");
88
89        self.get_graph()
90    }
91
92    /// Parallel builds directed graph representing note relationships
93    ///
94    /// Edges point from source note to linked note (A → B means A links to B)
95    ///
96    /// # Performance Notes
97    /// - For vaults with 1000+ notes, enable `rayon` feature
98    /// - Uses [`NoteOnDisk`](crate::prelude::NoteOnDisk) for minimal memory footprint
99    ///
100    /// # Other
101    /// See [`par_get_ungraph`](Vault::par_get_ungraph)
102    #[cfg_attr(docsrs, doc(cfg(feature = "petgraph")))]
103    #[cfg_attr(docsrs, doc(cfg(feature = "rayon")))]
104    #[cfg(feature = "rayon")]
105    #[cfg_attr(feature = "tracing", tracing::instrument(skip(self), fields(path = %self.path.display(), count_notes = %self.notes.len())))]
106    pub fn par_get_digraph(&self) -> Result<DiGraph<&F, ()>, F::Error>
107    where
108        F: Send + Sync,
109        F::Error: Send,
110    {
111        #[cfg(feature = "tracing")]
112        tracing::debug!("Building directed graph");
113
114        self.par_get_graph()
115    }
116
117    /// Builds undirected graph showing note connections
118    ///
119    /// Useful for connectivity analysis where direction doesn't matter
120    #[cfg_attr(docsrs, doc(cfg(feature = "petgraph")))]
121    #[cfg_attr(feature = "tracing", tracing::instrument(skip(self), fields(path = %self.path.display(), count_notes = %self.notes.len())))]
122    pub fn get_ungraph(&self) -> Result<UnGraph<&F, ()>, F::Error> {
123        #[cfg(feature = "tracing")]
124        tracing::debug!("Building undirected graph");
125
126        self.get_graph()
127    }
128
129    /// Parallel builds undirected graph showing note connections
130    ///
131    /// Useful for connectivity analysis where direction doesn't matter
132    #[cfg_attr(docsrs, doc(cfg(feature = "petgraph")))]
133    #[cfg_attr(docsrs, doc(cfg(feature = "rayon")))]
134    #[cfg(feature = "rayon")]
135    #[cfg_attr(feature = "tracing", tracing::instrument(skip(self), fields(path = %self.path.display(), count_notes = %self.notes.len())))]
136    pub fn par_get_ungraph(&self) -> Result<UnGraph<&F, ()>, F::Error>
137    where
138        F: Send + Sync,
139        F::Error: Send,
140    {
141        #[cfg(feature = "tracing")]
142        tracing::debug!("Building undirected graph");
143
144        self.par_get_graph()
145    }
146}
147
148#[cfg(test)]
149mod tests {
150    use crate::vault::vault_test::create_test_vault;
151
152    #[cfg_attr(feature = "tracing", tracing_test::traced_test)]
153    #[test]
154    #[cfg(feature = "petgraph")]
155    fn get_digraph() {
156        let (vault, _temp_dir, files) = create_test_vault().unwrap();
157
158        let graph = vault.get_digraph().unwrap();
159
160        assert_eq!(graph.edge_count(), 3);
161        assert_eq!(graph.node_count(), files.len());
162    }
163
164    #[cfg_attr(feature = "tracing", tracing_test::traced_test)]
165    #[test]
166    #[cfg(feature = "petgraph")]
167    #[cfg(feature = "rayon")]
168    fn par_get_digraph() {
169        let (vault, _temp_dir, files) = create_test_vault().unwrap();
170
171        let graph = vault.par_get_digraph().unwrap();
172
173        assert_eq!(graph.edge_count(), 3);
174        assert_eq!(graph.node_count(), files.len());
175    }
176
177    #[cfg_attr(feature = "tracing", tracing_test::traced_test)]
178    #[test]
179    #[cfg(feature = "petgraph")]
180    #[cfg(feature = "rayon")]
181    fn par_get_ungraph() {
182        let (vault, _temp_dir, files) = create_test_vault().unwrap();
183
184        let graph = vault.par_get_ungraph().unwrap();
185        assert_eq!(graph.edge_count(), 3);
186        assert_eq!(graph.node_count(), files.len());
187    }
188}