Skip to main content

lc_vector_stores/
redis_store.rs

1// lc-vector-stores/src/redis_store.rs
2//! Redis document store implementation
3
4use async_trait::async_trait;
5use lc_shared::splitter::{RecursiveCharacterSplitter, TextSplitter};
6
7use crate::document_store::{ChunkDocument, ChunkedDocumentStoreTrait, DocumentStore};
8use crate::{Document, VectorStoreError};
9
10/// Redis document store configuration
11#[derive(Debug, Clone)]
12pub struct RedisStoreConfig {
13    /// Redis connection URL
14    pub url: String,
15    /// Key prefix
16    pub key_prefix: String,
17}
18
19impl Default for RedisStoreConfig {
20    fn default() -> Self {
21        Self {
22            url: "redis://127.0.0.1:6379".to_string(),
23            key_prefix: "langchainrust".to_string(),
24        }
25    }
26}
27
28impl RedisStoreConfig {
29    /// Creates a config from a URL, using the default key prefix.
30    pub fn new(url: impl Into<String>) -> Self {
31        Self {
32            url: url.into(),
33            ..Default::default()
34        }
35    }
36    /// Sets the key prefix.
37    pub fn with_prefix(mut self, prefix: impl Into<String>) -> Self {
38        self.key_prefix = prefix.into();
39        self
40    }
41}
42
43/// Redis document store implementation
44pub struct RedisDocumentStore {
45    config: RedisStoreConfig,
46}
47
48impl RedisDocumentStore {
49    /// Connects to Redis per the config, verifies the connection is usable, and creates a store instance.
50    pub async fn new(config: RedisStoreConfig) -> Result<Self, VectorStoreError> {
51        let client = redis::Client::open(config.url.as_str())
52            .map_err(|e| VectorStoreError::ConnectionError(e.to_string()))?;
53        // verify the connection (the client is not kept; later operations reconnect on demand)
54        let _ = client
55            .get_connection()
56            .map_err(|e| VectorStoreError::ConnectionError(e.to_string()))?;
57        Ok(Self { config })
58    }
59
60    fn doc_key(&self, id: &str) -> String {
61        format!("{}:doc:{}", self.config.key_prefix, id)
62    }
63    fn chunk_key(&self, id: &str) -> String {
64        format!("{}:chunk:{}", self.config.key_prefix, id)
65    }
66    fn parent_chunks_key(&self, pid: &str) -> String {
67        format!("{}:pchunks:{}", self.config.key_prefix, pid)
68    }
69    fn doc_ids_key(&self) -> String {
70        format!("{}:doc_ids", self.config.key_prefix)
71    }
72    fn parent_ids_key(&self) -> String {
73        format!("{}:parent_ids", self.config.key_prefix)
74    }
75    fn all_chunks_key(&self) -> String {
76        format!("{}:all_chunks", self.config.key_prefix)
77    }
78}
79
80#[async_trait]
81impl DocumentStore for RedisDocumentStore {
82    async fn add_document(&self, document: Document) -> Result<String, VectorStoreError> {
83        let id = document
84            .id
85            .clone()
86            .unwrap_or_else(|| uuid::Uuid::new_v4().to_string());
87        let json = serde_json::to_string(&document)
88            .map_err(|e| VectorStoreError::StorageError(e.to_string()))?;
89        let config = self.config.clone();
90        let id2 = id.clone();
91
92        tokio::task::spawn_blocking(move || -> Result<(), VectorStoreError> {
93            let mut conn = redis::Client::open(config.url.as_str())
94                .and_then(|c| c.get_connection())
95                .map_err(|e| VectorStoreError::ConnectionError(e.to_string()))?;
96            redis::cmd("SET")
97                .arg(format!("{}:doc:{}", config.key_prefix, id2))
98                .arg(&json)
99                .query::<()>(&mut conn)
100                .map_err(|e| VectorStoreError::StorageError(e.to_string()))?;
101            redis::cmd("SADD")
102                .arg(format!("{}:doc_ids", config.key_prefix))
103                .arg(&id2)
104                .query::<()>(&mut conn)
105                .map_err(|e| VectorStoreError::StorageError(e.to_string()))?;
106            Ok(())
107        })
108        .await
109        .map_err(|e| VectorStoreError::StorageError(e.to_string()))??;
110
111        Ok(id)
112    }
113
114    async fn add_documents(
115        &self,
116        documents: Vec<Document>,
117    ) -> Result<Vec<String>, VectorStoreError> {
118        let mut ids = Vec::new();
119        for doc in documents {
120            ids.push(self.add_document(doc).await?);
121        }
122        Ok(ids)
123    }
124
125    async fn get_document(&self, id: &str) -> Result<Option<Document>, VectorStoreError> {
126        let result = self.get_str(&self.doc_key(id)).await?;
127        match result {
128            Some(json) => match serde_json::from_str(&json) {
129                Ok(doc) => Ok(Some(doc)),
130                Err(e) => {
131                    // the key exists but the payload is corrupted / written by an older schema, distinct from a true miss
132                    log::error!(
133                        "failed to parse stored payload for document `{}` (corrupted or written by an older schema): {}",
134                        id,
135                        e
136                    );
137                    Ok(None)
138                }
139            },
140            None => Ok(None),
141        }
142    }
143
144    async fn delete_document(&self, id: &str) -> Result<(), VectorStoreError> {
145        self.del(&self.doc_key(id)).await?;
146        self.srem(&self.doc_ids_key(), id).await
147    }
148
149    async fn count(&self) -> usize {
150        self.scard(&self.doc_ids_key()).await.unwrap_or(0)
151    }
152
153    async fn clear(&self) -> Result<(), VectorStoreError> {
154        // M29: use SCAN + DEL with prefix instead of FLUSHDB to avoid wiping other keys
155        self.clear_with_prefix().await
156    }
157}
158
159impl RedisDocumentStore {
160    async fn get_str(&self, key: &str) -> Result<Option<String>, VectorStoreError> {
161        let config = self.config.clone();
162        let key = key.to_string();
163        tokio::task::spawn_blocking(move || -> Result<Option<String>, VectorStoreError> {
164            let mut conn = redis::Client::open(config.url.as_str())
165                .and_then(|c| c.get_connection())
166                .map_err(|e| VectorStoreError::ConnectionError(e.to_string()))?;
167            redis::cmd("GET")
168                .arg(&key)
169                .query(&mut conn)
170                .map_err(|e| VectorStoreError::StorageError(e.to_string()))
171        })
172        .await
173        .map_err(|e| VectorStoreError::StorageError(e.to_string()))?
174    }
175
176    async fn del(&self, key: &str) -> Result<(), VectorStoreError> {
177        let config = self.config.clone();
178        let key = key.to_string();
179        tokio::task::spawn_blocking(move || -> Result<(), VectorStoreError> {
180            let mut conn = redis::Client::open(config.url.as_str())
181                .and_then(|c| c.get_connection())
182                .map_err(|e| VectorStoreError::ConnectionError(e.to_string()))?;
183            redis::cmd("DEL")
184                .arg(&key)
185                .query::<()>(&mut conn)
186                .map_err(|e| VectorStoreError::StorageError(e.to_string()))
187        })
188        .await
189        .map_err(|e| VectorStoreError::StorageError(e.to_string()))?
190    }
191
192    async fn srem(&self, key: &str, member: &str) -> Result<(), VectorStoreError> {
193        let config = self.config.clone();
194        let (k, m) = (key.to_string(), member.to_string());
195        tokio::task::spawn_blocking(move || -> Result<(), VectorStoreError> {
196            let mut conn = redis::Client::open(config.url.as_str())
197                .and_then(|c| c.get_connection())
198                .map_err(|e| VectorStoreError::ConnectionError(e.to_string()))?;
199            redis::cmd("SREM")
200                .arg(&k)
201                .arg(&m)
202                .query::<()>(&mut conn)
203                .map_err(|e| VectorStoreError::StorageError(e.to_string()))
204        })
205        .await
206        .map_err(|e| VectorStoreError::StorageError(e.to_string()))?
207    }
208
209    async fn smembers(&self, key: &str) -> Result<Vec<String>, VectorStoreError> {
210        let config = self.config.clone();
211        let key = key.to_string();
212        tokio::task::spawn_blocking(move || -> Result<Vec<String>, VectorStoreError> {
213            let mut conn = redis::Client::open(config.url.as_str())
214                .and_then(|c| c.get_connection())
215                .map_err(|e| VectorStoreError::ConnectionError(e.to_string()))?;
216            redis::cmd("SMEMBERS")
217                .arg(&key)
218                .query(&mut conn)
219                .map_err(|e| VectorStoreError::StorageError(e.to_string()))
220        })
221        .await
222        .map_err(|e| VectorStoreError::StorageError(e.to_string()))?
223    }
224
225    async fn scard(&self, key: &str) -> Result<usize, VectorStoreError> {
226        let config = self.config.clone();
227        let key = key.to_string();
228        tokio::task::spawn_blocking(move || -> Result<usize, VectorStoreError> {
229            let mut conn = redis::Client::open(config.url.as_str())
230                .and_then(|c| c.get_connection())
231                .map_err(|e| VectorStoreError::ConnectionError(e.to_string()))?;
232            redis::cmd("SCARD")
233                .arg(&key)
234                .query(&mut conn)
235                .map_err(|e| VectorStoreError::StorageError(e.to_string()))
236        })
237        .await
238        .map_err(|e| VectorStoreError::StorageError(e.to_string()))?
239    }
240
241    /// M29: Clear only keys with the configured prefix using SCAN + DEL
242    async fn clear_with_prefix(&self) -> Result<(), VectorStoreError> {
243        let config = self.config.clone();
244        tokio::task::spawn_blocking(move || -> Result<(), VectorStoreError> {
245            let mut conn = redis::Client::open(config.url.as_str())
246                .and_then(|c| c.get_connection())
247                .map_err(|e| VectorStoreError::ConnectionError(e.to_string()))?;
248
249            let pattern = format!("{}:*", config.key_prefix);
250            let mut cursor: u64 = 0;
251
252            loop {
253                let (next_cursor, keys): (u64, Vec<String>) = redis::cmd("SCAN")
254                    .arg(cursor)
255                    .arg("MATCH")
256                    .arg(&pattern)
257                    .arg("COUNT")
258                    .arg(100)
259                    .query(&mut conn)
260                    .map_err(|e| VectorStoreError::StorageError(e.to_string()))?;
261
262                if !keys.is_empty() {
263                    redis::cmd("DEL")
264                        .arg(&keys)
265                        .query::<()>(&mut conn)
266                        .map_err(|e| VectorStoreError::StorageError(e.to_string()))?;
267                }
268
269                cursor = next_cursor;
270                if cursor == 0 {
271                    break;
272                }
273            }
274
275            Ok(())
276        })
277        .await
278        .map_err(|e| VectorStoreError::StorageError(e.to_string()))?
279    }
280
281    /// Persists the Redis dataset to disk (RDB snapshot, the `SAVE` command).
282    ///
283    /// C3: the original `ChunkedDocumentStoreTrait::save` was a fake default method and has been
284    /// removed from the trait; Redis persistence is a whole-database RDB snapshot unrelated to any
285    /// file path, so this is now a parameterless inherent method.
286    pub async fn save_to_disk(&self) -> Result<(), VectorStoreError> {
287        let config = self.config.clone();
288        tokio::task::spawn_blocking(move || -> Result<(), VectorStoreError> {
289            let mut conn = redis::Client::open(config.url.as_str())
290                .and_then(|c| c.get_connection())
291                .map_err(|e| VectorStoreError::ConnectionError(e.to_string()))?;
292            redis::cmd("SAVE")
293                .query::<()>(&mut conn)
294                .map_err(|e| VectorStoreError::StorageError(e.to_string()))
295        })
296        .await
297        .map_err(|e| VectorStoreError::StorageError(e.to_string()))?
298    }
299}
300
301#[async_trait]
302impl ChunkedDocumentStoreTrait for RedisDocumentStore {
303    async fn add_parent_document(
304        &self,
305        document: Document,
306        chunk_size: usize,
307    ) -> Result<(String, Vec<String>), VectorStoreError> {
308        let splitter = RecursiveCharacterSplitter::new(chunk_size, chunk_size / 10);
309        let chunks_text = splitter.split_text(&document.content);
310        let parent_id = document
311            .id
312            .clone()
313            .unwrap_or_else(|| uuid::Uuid::new_v4().to_string());
314        let doc_json = serde_json::to_string(&Document {
315            id: Some(parent_id.clone()),
316            ..document
317        })
318        .map_err(|e| VectorStoreError::StorageError(e.to_string()))?;
319
320        // run all Redis operations via spawn_blocking
321        let config = self.config.clone();
322        let pid = parent_id.clone();
323        let chunks = chunks_text.clone();
324
325        tokio::task::spawn_blocking(move || -> Result<(String, Vec<String>), VectorStoreError> {
326            let mut conn = redis::Client::open(config.url.as_str())
327                .and_then(|c| c.get_connection())
328                .map_err(|e| VectorStoreError::ConnectionError(e.to_string()))?;
329
330            redis::cmd("SET")
331                .arg(format!("{}:doc:{}", config.key_prefix, pid))
332                .arg(&doc_json)
333                .query::<()>(&mut conn)
334                .map_err(|e| VectorStoreError::StorageError(e.to_string()))?;
335
336            let mut chunk_ids = Vec::new();
337            for (i, text) in chunks.iter().enumerate() {
338                let cid = format!("{}:chunk:{}", pid, i);
339                let chunk = ChunkDocument::new(cid.clone(), pid.clone(), text.clone(), i);
340                let cjson = serde_json::to_string(&chunk)
341                    .map_err(|e| VectorStoreError::StorageError(e.to_string()))?;
342                redis::cmd("SET")
343                    .arg(format!("{}:chunk:{}", config.key_prefix, cid))
344                    .arg(&cjson)
345                    .query::<()>(&mut conn)
346                    .map_err(|e| VectorStoreError::StorageError(e.to_string()))?;
347                redis::cmd("SADD")
348                    .arg(format!("{}:pchunks:{}", config.key_prefix, pid))
349                    .arg(&cid)
350                    .query::<()>(&mut conn)
351                    .map_err(|e| VectorStoreError::StorageError(e.to_string()))?;
352                redis::cmd("SADD")
353                    .arg(format!("{}:all_chunks", config.key_prefix))
354                    .arg(&cid)
355                    .query::<()>(&mut conn)
356                    .map_err(|e| VectorStoreError::StorageError(e.to_string()))?;
357                chunk_ids.push(cid);
358            }
359            redis::cmd("SADD")
360                .arg(format!("{}:parent_ids", config.key_prefix))
361                .arg(&pid)
362                .query::<()>(&mut conn)
363                .map_err(|e| VectorStoreError::StorageError(e.to_string()))?;
364            redis::cmd("SADD")
365                .arg(format!("{}:doc_ids", config.key_prefix))
366                .arg(&pid)
367                .query::<()>(&mut conn)
368                .map_err(|e| VectorStoreError::StorageError(e.to_string()))?;
369
370            Ok((pid, chunk_ids))
371        })
372        .await
373        .map_err(|e| VectorStoreError::StorageError(e.to_string()))?
374    }
375
376    async fn add_parent_documents(
377        &self,
378        documents: Vec<Document>,
379        chunk_size: usize,
380    ) -> Result<Vec<(String, Vec<String>)>, VectorStoreError> {
381        let mut results = Vec::new();
382        for doc in documents {
383            results.push(self.add_parent_document(doc, chunk_size).await?);
384        }
385        Ok(results)
386    }
387
388    async fn get_parent_document(
389        &self,
390        parent_id: &str,
391    ) -> Result<Option<Document>, VectorStoreError> {
392        self.get_document(parent_id).await
393    }
394
395    async fn get_chunk(&self, chunk_id: &str) -> Result<Option<ChunkDocument>, VectorStoreError> {
396        match self.get_str(&self.chunk_key(chunk_id)).await? {
397            Some(json) => match serde_json::from_str(&json) {
398                Ok(chunk) => Ok(Some(chunk)),
399                Err(e) => {
400                    log::error!(
401                        "failed to parse stored payload for chunk `{}` (corrupted or written by an older schema): {}",
402                        chunk_id,
403                        e
404                    );
405                    Ok(None)
406                }
407            },
408            None => Ok(None),
409        }
410    }
411
412    async fn get_chunk_document(
413        &self,
414        chunk_id: &str,
415    ) -> Result<Option<Document>, VectorStoreError> {
416        Ok(self.get_chunk(chunk_id).await?.map(|c| c.to_document()))
417    }
418
419    async fn get_chunks_for_parent(
420        &self,
421        parent_id: &str,
422    ) -> Result<Vec<ChunkDocument>, VectorStoreError> {
423        let ids = self.smembers(&self.parent_chunks_key(parent_id)).await?;
424        let mut chunks = Vec::new();
425        for id in ids {
426            if let Some(c) = self.get_chunk(&id).await? {
427                chunks.push(c);
428            }
429        }
430        chunks.sort_by_key(|c| c.segment);
431        Ok(chunks)
432    }
433
434    async fn get_chunk_documents_for_parent(
435        &self,
436        parent_id: &str,
437    ) -> Result<Vec<Document>, VectorStoreError> {
438        Ok(self
439            .get_chunks_for_parent(parent_id)
440            .await?
441            .into_iter()
442            .map(|c| c.to_document())
443            .collect())
444    }
445
446    async fn delete_parent_document(&self, parent_id: &str) -> Result<(), VectorStoreError> {
447        let chunks = self.get_chunks_for_parent(parent_id).await?;
448        for chunk in &chunks {
449            self.del(&self.chunk_key(&chunk.chunk_id)).await?;
450        }
451        self.del(&self.doc_key(parent_id)).await?;
452        self.del(&self.parent_chunks_key(parent_id)).await?;
453        self.srem(&self.doc_ids_key(), parent_id).await?;
454        self.srem(&self.parent_ids_key(), parent_id).await
455    }
456
457    async fn parent_count(&self) -> usize {
458        self.scard(&self.parent_ids_key()).await.unwrap_or(0)
459    }
460
461    async fn chunk_count(&self) -> usize {
462        self.scard(&self.all_chunks_key()).await.unwrap_or(0)
463    }
464
465    async fn get_all_chunks(&self) -> Result<Vec<ChunkDocument>, VectorStoreError> {
466        let ids = self.smembers(&self.all_chunks_key()).await?;
467        let mut chunks = Vec::new();
468        for id in ids {
469            if let Some(c) = self.get_chunk(&id).await? {
470                chunks.push(c);
471            }
472        }
473        Ok(chunks)
474    }
475
476    async fn clear(&self) -> Result<(), VectorStoreError> {
477        // M29: use SCAN + DEL with prefix instead of FLUSHDB to avoid wiping other keys
478        self.clear_with_prefix().await
479    }
480
481    fn add_parent_document_blocking(
482        &self,
483        _document: Document,
484        _chunk_size: usize,
485    ) -> Result<(String, Vec<String>), VectorStoreError> {
486        Err(VectorStoreError::StorageError(
487            "blocking not supported, use async API".to_string(),
488        ))
489    }
490
491    fn get_parent_document_blocking(
492        &self,
493        _parent_id: &str,
494    ) -> Result<Option<Document>, VectorStoreError> {
495        Err(VectorStoreError::StorageError(
496            "blocking not supported, use async API".to_string(),
497        ))
498    }
499
500    fn get_chunk_blocking(
501        &self,
502        _chunk_id: &str,
503    ) -> Result<Option<ChunkDocument>, VectorStoreError> {
504        Err(VectorStoreError::StorageError(
505            "blocking not supported, use async API".to_string(),
506        ))
507    }
508
509    fn blocking_get_chunks_for_parent(
510        &self,
511        _parent_id: &str,
512    ) -> Result<Vec<ChunkDocument>, VectorStoreError> {
513        Err(VectorStoreError::StorageError(
514            "blocking not supported, use async API".to_string(),
515        ))
516    }
517}