Skip to main content

cognate_rag/
lib.rs

1//! Cognate RAG — Retrieval-Augmented Generation pipeline.
2//!
3//! # Overview
4//!
5//! 1. Choose an [`EmbeddingProvider`] (e.g. `OpenAiProvider` from `cognate-providers`).
6//! 2. Choose a [`VectorStore`] backend (e.g. [`MemoryVectorStore`] for prototyping).
7//! 3. Wrap them in [`RagPipeline`] to get [`ingest`](RagPipeline::ingest) and
8//!    [`retrieve`](RagPipeline::retrieve).
9//!
10//! # Example
11//!
12//! ```rust,no_run
13//! use cognate_rag::{RagPipeline, MemoryVectorStore};
14//! use cognate_core::EmbeddingProvider;
15//!
16//! async fn run(embedder: impl EmbeddingProvider) {
17//!     let store = MemoryVectorStore::new();
18//!     let pipeline = RagPipeline::new(embedder, store);
19//!
20//!     pipeline
21//!         .ingest(
22//!             vec!["Rust is fast".to_string(), "Rust is safe".to_string()],
23//!             vec![serde_json::json!({"source": "doc1"}), serde_json::json!({"source": "doc2"})],
24//!         )
25//!         .await
26//!         .unwrap();
27//!
28//!     let results = pipeline.retrieve("fast systems language", 1).await.unwrap();
29//!     println!("{}", results[0].content);
30//! }
31//! ```
32#![warn(missing_docs)]
33
34pub mod memory;
35
36pub use memory::MemoryVectorStore;
37
38use async_trait::async_trait;
39use cognate_core::{EmbeddingProvider, Error};
40use serde::{Deserialize, Serialize};
41
42// ─── Core types ────────────────────────────────────────────────────────────
43
44/// A dense embedding vector.
45pub type Vector = Vec<f32>;
46
47/// A document stored in a vector store.
48#[derive(Debug, Clone, Serialize, Deserialize)]
49pub struct Document {
50    /// Unique identifier for this document.
51    pub id: String,
52    /// The text content of the document.
53    pub content: String,
54    /// Arbitrary key-value metadata associated with this document.
55    pub metadata: serde_json::Value,
56    /// The embedding vector, if one has been computed.
57    pub embedding: Option<Vector>,
58}
59
60// ─── VectorStore trait ─────────────────────────────────────────────────────
61
62/// A persistent or in-memory store of embedded documents.
63///
64/// Implement this trait to add support for a new vector database backend
65/// (pgvector, Qdrant, Pinecone, etc.).
66#[async_trait]
67pub trait VectorStore: Send + Sync {
68    /// Persist a batch of documents (with their embeddings) to the store.
69    async fn add_documents(
70        &self,
71        docs: Vec<Document>,
72    ) -> Result<(), Box<dyn std::error::Error + Send + Sync>>;
73
74    /// Return the `limit` documents whose embeddings are most similar to
75    /// `query_vector` (cosine similarity, descending).
76    async fn search(
77        &self,
78        query_vector: Vector,
79        limit: usize,
80    ) -> Result<Vec<Document>, Box<dyn std::error::Error + Send + Sync>>;
81}
82
83// ─── RagPipeline ───────────────────────────────────────────────────────────
84
85/// A high-level RAG pipeline combining an embedding provider with a vector store.
86///
87/// # Type parameters
88///
89/// * `E` — any [`EmbeddingProvider`] (e.g. `OpenAiProvider`).
90/// * `V` — any [`VectorStore`] (e.g. [`MemoryVectorStore`]).
91pub struct RagPipeline<E, V> {
92    embedder: E,
93    store: V,
94}
95
96impl<E: EmbeddingProvider, V: VectorStore> RagPipeline<E, V> {
97    /// Create a new pipeline from an embedder and a vector store.
98    pub fn new(embedder: E, store: V) -> Self {
99        Self { embedder, store }
100    }
101
102    /// Embed `texts` and store them alongside `metadata` in the vector store.
103    ///
104    /// `texts` and `metadata` must have the same length.
105    ///
106    /// # Errors
107    ///
108    /// Returns [`Error::VectorStore`] if embedding or storage fails.
109    pub async fn ingest(
110        &self,
111        texts: Vec<String>,
112        metadata: Vec<serde_json::Value>,
113    ) -> cognate_core::Result<()> {
114        if texts.len() != metadata.len() {
115            return Err(Error::InvalidRequest(
116                "texts and metadata must have the same length".to_string(),
117            ));
118        }
119
120        let embeddings = self
121            .embedder
122            .embed(texts.clone())
123            .await
124            .map_err(|e| Error::VectorStore(e.to_string()))?;
125
126        let docs: Vec<Document> = texts
127            .into_iter()
128            .zip(embeddings)
129            .zip(metadata)
130            .enumerate()
131            .map(|(i, ((content, emb), meta))| Document {
132                id: i.to_string(),
133                content,
134                metadata: meta,
135                embedding: Some(emb),
136            })
137            .collect();
138
139        self.store
140            .add_documents(docs)
141            .await
142            .map_err(|e| Error::VectorStore(e.to_string()))
143    }
144
145    /// Embed `query` and return the `limit` most similar documents.
146    ///
147    /// # Errors
148    ///
149    /// Returns [`Error::VectorStore`] if embedding or search fails.
150    pub async fn retrieve(
151        &self,
152        query: &str,
153        limit: usize,
154    ) -> cognate_core::Result<Vec<Document>> {
155        let mut embeddings = self
156            .embedder
157            .embed(vec![query.to_string()])
158            .await
159            .map_err(|e| Error::VectorStore(e.to_string()))?;
160
161        let query_vec = if embeddings.is_empty() {
162            return Err(Error::VectorStore(
163                "embedding provider returned no vectors".to_string(),
164            ));
165        } else {
166            embeddings.remove(0)
167        };
168
169        self.store
170            .search(query_vec, limit)
171            .await
172            .map_err(|e| Error::VectorStore(e.to_string()))
173    }
174}