Skip to main content

swiftide_core/
node.rs

1//! This module defines the `Node` struct and its associated methods.
2//!
3//! `Node` represents a unit of data in the indexing process, containing metadata,
4//! the data chunk itself, and an optional vector representation.
5//!
6//! # Overview
7//!
8//! The `Node` struct is designed to encapsulate all necessary information for a single
9//! unit of data being processed in the indexing pipeline. It includes fields for an identifier,
10//! file path, data chunk, optional vector representation, and metadata.
11//!
12//! The struct provides methods to convert the node into an embeddable string format and to
13//! calculate a hash value for the node based on its path and chunk.
14//!
15//! # Usage
16//!
17//! The `Node` struct is used throughout the indexing pipeline to represent and process
18//! individual units of data. It is particularly useful in scenarios where metadata and data chunks
19//! need to be processed together.
20use std::{
21    collections::HashMap,
22    fmt::Debug,
23    hash::{Hash, Hasher},
24    os::unix::ffi::OsStrExt,
25    path::PathBuf,
26};
27
28use derive_builder::Builder;
29use itertools::Itertools;
30use serde::{Deserialize, Serialize};
31
32use crate::{Embedding, SparseEmbedding, metadata::Metadata};
33
34/// Helper trait for types that can be used as data chunks in a `Node`.
35/// For now always expects an owned value
36///
37/// A chunk must be able to yield its bytes, be cloned (not while streaming), and be sent across
38/// threads.
39pub trait Chunk: Clone + Send + Sync + Debug + AsRef<[u8]> + 'static {}
40impl<T> Chunk for T where T: Clone + Send + Sync + Debug + AsRef<[u8]> + 'static {}
41
42/// Represents a unit of data in the indexing process.
43///
44/// `Node` encapsulates all necessary information for a single unit of data being processed
45/// in the indexing pipeline. It includes fields for an identifier, file path, data chunk, optional
46/// vector representation, and metadata.
47#[derive(Default, Clone, Serialize, Deserialize, PartialEq, Builder)]
48#[builder(setter(into, strip_option), build_fn(error = "anyhow::Error"))]
49pub struct Node<T: Chunk> {
50    /// File path associated with the node.
51    #[builder(default)]
52    pub path: PathBuf,
53    /// Data chunk contained in the node.
54    pub chunk: T,
55    /// Optional vector representation of embedded data.
56    #[builder(default)]
57    pub vectors: Option<HashMap<EmbeddedField, Embedding>>,
58    /// Optional sparse vector representation of embedded data.
59    #[builder(default)]
60    pub sparse_vectors: Option<HashMap<EmbeddedField, SparseEmbedding>>,
61    /// Metadata associated with the node.
62    #[builder(default)]
63    pub metadata: Metadata,
64    /// Mode of embedding data Chunk and Metadata
65    #[builder(default)]
66    pub embed_mode: EmbedMode,
67    /// Size of the input this node was originally derived from in bytes
68    #[builder(default)]
69    pub original_size: usize,
70    /// Offset of the chunk relative to the start of the input this node was originally derived
71    /// from in bytes
72    #[builder(default)]
73    pub offset: usize,
74    /// Id of the first node this node was derived from. Set when a node is created via
75    /// [`Node::build_from_other`] (chunking, conversions) so derived nodes keep pointing at the
76    /// node that entered the pipeline.
77    #[builder(default)]
78    pub parent_id: Option<uuid::Uuid>,
79}
80
81pub type TextNode = Node<String>;
82
83impl<T: Chunk> NodeBuilder<T> {
84    pub fn maybe_sparse_vectors(
85        &mut self,
86        sparse_vectors: Option<HashMap<EmbeddedField, SparseEmbedding>>,
87    ) -> &mut Self {
88        self.sparse_vectors = Some(sparse_vectors);
89        self
90    }
91
92    pub fn maybe_vectors(
93        &mut self,
94        vectors: Option<HashMap<EmbeddedField, Embedding>>,
95    ) -> &mut Self {
96        self.vectors = Some(vectors);
97        self
98    }
99}
100
101impl<T: Chunk> Debug for Node<T> {
102    /// Formats the node for debugging purposes.
103    ///
104    /// This method is used to provide a human-readable representation of the node when debugging.
105    /// The vector field is displayed as the number of elements in the vector if present.
106    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
107        f.debug_struct("Node")
108            .field("id", &self.id())
109            .field("path", &self.path)
110            .field("chunk", &self.chunk)
111            .field("metadata", &self.metadata)
112            .field(
113                "vectors",
114                &self
115                    .vectors
116                    .iter()
117                    .flat_map(HashMap::iter)
118                    .map(|(embed_type, vec)| format!("'{embed_type}': {}", vec.len()))
119                    .join(","),
120            )
121            .field(
122                "sparse_vectors",
123                &self
124                    .sparse_vectors
125                    .iter()
126                    .flat_map(HashMap::iter)
127                    .map(|(embed_type, vec)| {
128                        format!(
129                            "'{embed_type}': indices({}), values({})",
130                            vec.indices.len(),
131                            vec.values.len()
132                        )
133                    })
134                    .join(","),
135            )
136            .field("embed_mode", &self.embed_mode)
137            .finish()
138    }
139}
140
141impl<T: Chunk> Node<T> {
142    /// Builds a new instance of `Node`, returning a `NodeBuilder`. Copies
143    /// over the fields from the provided `Node`.
144    ///
145    /// The new node points at the node that entered the pipeline via [`Node::parent_id`].
146    pub fn build_from_other(node: &Node<T>) -> NodeBuilder<T> {
147        NodeBuilder::default()
148            .path(node.path.clone())
149            .chunk(node.chunk.clone())
150            .metadata(node.metadata.clone())
151            .maybe_vectors(node.vectors.clone())
152            .maybe_sparse_vectors(node.sparse_vectors.clone())
153            .embed_mode(node.embed_mode)
154            .original_size(node.original_size)
155            .offset(node.offset)
156            .parent_id(node.parent_id.unwrap_or_else(|| node.id()))
157            .to_owned()
158    }
159
160    /// Creates a new instance of `NodeBuilder.`
161    pub fn builder<VALUE: Chunk + Clone>() -> NodeBuilder<VALUE> {
162        NodeBuilder::default()
163    }
164
165    /// Creates a new instance of `Node` with the specified data chunk.
166    ///
167    /// The other fields are set to their default values.
168    pub fn new(chunk: impl Into<String>) -> Node<String> {
169        let chunk = chunk.into();
170        let original_size = chunk.len();
171        Node {
172            chunk,
173            original_size,
174            ..Default::default()
175        }
176    }
177
178    pub fn with_metadata(&mut self, metadata: impl Into<Metadata>) -> &mut Self {
179        self.metadata = metadata.into();
180        self
181    }
182
183    pub fn with_vectors(
184        &mut self,
185        vectors: impl Into<HashMap<EmbeddedField, Embedding>>,
186    ) -> &mut Self {
187        self.vectors = Some(vectors.into());
188        self
189    }
190
191    pub fn with_sparse_vectors(
192        &mut self,
193        sparse_vectors: impl Into<HashMap<EmbeddedField, SparseEmbedding>>,
194    ) -> &mut Self {
195        self.sparse_vectors = Some(sparse_vectors.into());
196        self
197    }
198
199    /// Retrieve the identifier of the node.
200    ///
201    /// Calculates the identifier of the node based on its path and chunk as bytes, returning a
202    /// UUID (v3).
203    ///
204    /// WARN: Does not memoize the id. Use sparingly.
205    pub fn id(&self) -> uuid::Uuid {
206        // Calculate the identifier based on the path and chunk as bytes
207        let bytes = [self.path.as_os_str().as_bytes(), self.chunk.as_ref()].concat();
208
209        uuid::Uuid::new_v3(&uuid::Uuid::NAMESPACE_OID, &bytes)
210    }
211
212    /// Returns the id of the first node this node was derived from, if any.
213    pub fn parent_id(&self) -> Option<uuid::Uuid> {
214        self.parent_id
215    }
216}
217
218impl Node<String> {
219    /// Derives nodes from chunks while retaining the first ancestor as their parent.
220    ///
221    /// Resolves the parent before removing the input text, then reuses the remaining
222    /// fields for every output without cloning the full input text.
223    pub fn into_chunks(
224        self,
225        chunks: impl IntoIterator<Item = String>,
226    ) -> impl Iterator<Item = Self> {
227        let parent_id = self.parent_id.unwrap_or_else(|| self.id());
228        let mut template = self;
229        template.chunk.clear();
230        template.parent_id = Some(parent_id);
231
232        chunks.into_iter().map(move |chunk| {
233            let mut node = template.clone();
234            node.chunk = chunk;
235            node
236        })
237    }
238
239    /// Creates embeddable data depending on chosen `EmbedMode`.
240    ///
241    /// # Returns
242    ///
243    /// Embeddable data mapped to their `EmbeddedField`.
244    pub fn as_embeddables(&self) -> Vec<(EmbeddedField, String)> {
245        // TODO: Cow and borrow the inner data + generic
246        let mut embeddables = Vec::new();
247
248        if self.embed_mode == EmbedMode::SingleWithMetadata || self.embed_mode == EmbedMode::Both {
249            embeddables.push((EmbeddedField::Combined, self.combine_chunk_with_metadata()));
250        }
251
252        if self.embed_mode == EmbedMode::PerField || self.embed_mode == EmbedMode::Both {
253            embeddables.push((EmbeddedField::Chunk, self.chunk.clone()));
254            for (name, value) in &self.metadata {
255                let value = value
256                    .as_str()
257                    .map_or_else(|| value.to_string(), ToString::to_string);
258                embeddables.push((EmbeddedField::Metadata(name.clone()), value));
259            }
260        }
261
262        embeddables
263    }
264
265    /// Converts the node into an [`self::EmbeddedField::Combined`] type of embeddable.
266    ///
267    /// This embeddable format consists of the metadata formatted as key-value pairs, each on a new
268    /// line, followed by the data chunk.
269    ///
270    /// # Returns
271    ///
272    /// A string representing the embeddable format of the node.
273    fn combine_chunk_with_metadata(&self) -> String {
274        // Metadata formatted by newlines joined with the chunk
275        let metadata = self
276            .metadata
277            .iter()
278            .map(|(k, v)| {
279                let v = v
280                    .as_str()
281                    .map_or_else(|| v.to_string(), ToString::to_string);
282
283                format!("{k}: {v}")
284            })
285            .collect::<Vec<String>>()
286            .join("\n");
287
288        format!("{}\n{}", metadata, self.chunk)
289    }
290}
291
292impl Hash for Node<String> {
293    /// Hashes the node based on its path and chunk.
294    ///
295    /// This method is used by the `calculate_hash` method to generate a hash value for the node.
296    fn hash<H: Hasher>(&self, state: &mut H) {
297        self.path.hash(state);
298        self.chunk.hash(state);
299    }
300}
301
302impl<T: Into<String>> From<T> for Node<String> {
303    fn from(value: T) -> Self {
304        let value: String = value.into();
305        Node::<String>::new(value)
306    }
307}
308
309/// Embed mode of the pipeline.
310#[derive(Copy, Debug, Default, Clone, Serialize, Deserialize, PartialEq)]
311pub enum EmbedMode {
312    #[default]
313    /// Embedding Chunk of data combined with Metadata.
314    SingleWithMetadata,
315    /// Embedding Chunk of data and every Metadata separately.
316    PerField,
317    /// Embedding Chunk of data and every Metadata separately and Chunk of data combined with
318    /// Metadata.
319    Both,
320}
321
322/// Type of Embeddable stored in model.
323#[derive(
324    Clone, Default, Serialize, Deserialize, PartialEq, Eq, Hash, strum_macros::Display, Debug,
325)]
326pub enum EmbeddedField {
327    #[default]
328    /// Embeddable created from Chunk of data combined with Metadata.
329    Combined,
330    /// Embeddable created from Chunk of data only.
331    Chunk,
332    /// Embeddable created from Metadata.
333    /// String stores Metadata name.
334    #[strum(to_string = "Metadata: {0}")]
335    Metadata(String),
336}
337
338impl EmbeddedField {
339    /// Returns the name of the field when it would be a sparse vector
340    pub fn sparse_field_name(&self) -> String {
341        format!("{self}_sparse")
342    }
343
344    /// Returns the name of the field when it would be a dense vector
345    pub fn field_name(&self) -> String {
346        format!("{self}")
347    }
348}
349
350#[allow(clippy::from_over_into)]
351impl Into<String> for EmbeddedField {
352    fn into(self) -> String {
353        self.to_string()
354    }
355}
356
357#[cfg(test)]
358mod tests {
359    use super::*;
360    use test_case::test_case;
361
362    #[test_case(&EmbeddedField::Combined, ["Combined", "Combined_sparse"])]
363    #[test_case(&EmbeddedField::Chunk, ["Chunk", "Chunk_sparse"])]
364    #[test_case(&EmbeddedField::Metadata("test".into()), ["Metadata: test", "Metadata: test_sparse"])]
365    fn field_name_tests(embedded_field: &EmbeddedField, expected: [&str; 2]) {
366        assert_eq!(embedded_field.field_name(), expected[0]);
367        assert_eq!(embedded_field.sparse_field_name(), expected[1]);
368    }
369
370    #[test]
371    fn test_debugging_node_with_utf8_char_boundary() {
372        let node = Node::from("🦀".repeat(101));
373        // Single char
374        let _ = format!("{node:?}");
375
376        // With invalid char boundary
377        let node = Node::from("Jürgen".repeat(100));
378        let _ = format!("{node:?}");
379    }
380
381    #[test]
382    fn test_build_from_other_without_vectors() {
383        let original_node = Node::from("test_chunk")
384            .with_metadata(Metadata::default())
385            .with_vectors(HashMap::new())
386            .with_sparse_vectors(HashMap::new())
387            .to_owned();
388
389        let builder = Node::build_from_other(&original_node);
390        let new_node = builder.build().unwrap();
391
392        assert_eq!(new_node.parent_id(), Some(original_node.id()));
393        assert_eq!(new_node.chunk, original_node.chunk);
394        assert_eq!(new_node.path, original_node.path);
395        assert_eq!(new_node.metadata, original_node.metadata);
396        assert_eq!(new_node.vectors, original_node.vectors);
397        assert_eq!(new_node.sparse_vectors, original_node.sparse_vectors);
398    }
399
400    #[test]
401    fn test_build_from_other_with_vectors() {
402        let mut vectors = HashMap::new();
403        vectors.insert(EmbeddedField::Chunk, Embedding::default());
404
405        let mut sparse_vectors = HashMap::new();
406        sparse_vectors.insert(
407            EmbeddedField::Chunk,
408            SparseEmbedding {
409                indices: vec![],
410                values: vec![],
411            },
412        );
413
414        let original_node = Node::from("test_chunk")
415            .with_metadata(Metadata::default())
416            .with_vectors(vectors.clone())
417            .with_sparse_vectors(sparse_vectors.clone())
418            .to_owned();
419
420        let builder = Node::build_from_other(&original_node);
421        let new_node = builder.build().unwrap();
422
423        assert_eq!(new_node.parent_id(), Some(original_node.id()));
424        assert_eq!(new_node.chunk, original_node.chunk);
425        assert_eq!(new_node.vectors, original_node.vectors);
426        assert_eq!(new_node.sparse_vectors, original_node.sparse_vectors);
427    }
428
429    #[test]
430    fn into_chunks_preserves_fields_and_first_ancestor() {
431        let source = TextNode::builder()
432            .path("fixtures/日本語.txt")
433            .chunk("αβγ parent".to_string())
434            .metadata(Metadata::from([("language", "日本語")]))
435            .vectors(HashMap::from([(EmbeddedField::Chunk, vec![1.0, 2.0])]))
436            .sparse_vectors(HashMap::from([(
437                EmbeddedField::Chunk,
438                SparseEmbedding {
439                    indices: vec![2],
440                    values: vec![0.5],
441                },
442            )]))
443            .embed_mode(EmbedMode::Both)
444            .original_size(100usize)
445            .offset(7usize)
446            .build()
447            .unwrap();
448        let parent_id = source.id();
449        let chunks = vec!["αβγ".to_string(), "日本語".to_string()];
450        let outputs: Vec<_> = source.clone().into_chunks(chunks.clone()).collect();
451        assert_eq!(outputs.len(), 2);
452        for (output, chunk) in outputs.iter().zip(chunks) {
453            assert_eq!(output.chunk, chunk);
454            assert_eq!(output.path, source.path);
455            assert_eq!(output.metadata, source.metadata);
456            assert_eq!(output.vectors, source.vectors);
457            assert_eq!(output.sparse_vectors, source.sparse_vectors);
458            assert_eq!(output.embed_mode, source.embed_mode);
459            assert_eq!(output.original_size, source.original_size);
460            assert_eq!(output.offset, source.offset);
461            assert_eq!(output.parent_id, Some(parent_id));
462        }
463
464        let descendant: Vec<_> = outputs[0]
465            .clone()
466            .into_chunks(["child".to_string()])
467            .collect();
468        assert_eq!(descendant[0].parent_id, Some(parent_id));
469
470        let no_outputs: Vec<_> = source.into_chunks(Vec::<String>::new()).collect();
471        assert!(no_outputs.is_empty());
472    }
473
474    #[test]
475    fn into_chunks_preserves_explicit_parent_and_live_id() {
476        let mut source = Node::from("original");
477        let original_id = source.id();
478        source.path = "changed.txt".into();
479        source.chunk = "changed".into();
480        assert_ne!(source.id(), original_id);
481
482        let explicit_parent = uuid::Uuid::new_v4();
483        source.parent_id = Some(explicit_parent);
484        let children: Vec<_> = source.into_chunks(["one".into(), "two".into()]).collect();
485        assert_eq!(children.len(), 2);
486        assert!(
487            children
488                .iter()
489                .all(|child| child.parent_id == Some(explicit_parent))
490        );
491    }
492
493    #[test]
494    fn test_build_from_other_keeps_first_ancestor_as_parent() {
495        let original_node = Node::from("test_chunk");
496        let child_node = Node::build_from_other(&original_node).build().unwrap();
497        let grandchild_node = Node::build_from_other(&child_node).build().unwrap();
498
499        assert_eq!(original_node.parent_id(), None);
500        assert_eq!(child_node.parent_id(), Some(original_node.id()));
501        assert_eq!(grandchild_node.parent_id(), Some(original_node.id()));
502    }
503}