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            // H6: drop this parent's previous chunk set before writing the new one so a
337            // re-ingest that shrank / changed segmentation leaves no stale
338            // `{pid}:chunk:{n}` keys nor ghost membership in `pchunks` / `all_chunks`
339            // (mirrors the in-memory C5 behavior). SMEMBERS on a never-seen parent
340            // returns an empty set, so a first add is a no-op.
341            let pchunks_key = format!("{}:pchunks:{}", config.key_prefix, pid);
342            let old_ids: Vec<String> = redis::cmd("SMEMBERS")
343                .arg(&pchunks_key)
344                .query(&mut conn)
345                .map_err(|e| VectorStoreError::StorageError(e.to_string()))?;
346            let all_chunks_key = format!("{}:all_chunks", config.key_prefix);
347            for old in &old_ids {
348                redis::cmd("DEL")
349                    .arg(format!("{}:chunk:{}", config.key_prefix, old))
350                    .query::<()>(&mut conn)
351                    .map_err(|e| VectorStoreError::StorageError(e.to_string()))?;
352                redis::cmd("SREM")
353                    .arg(&all_chunks_key)
354                    .arg(old)
355                    .query::<()>(&mut conn)
356                    .map_err(|e| VectorStoreError::StorageError(e.to_string()))?;
357            }
358            redis::cmd("DEL")
359                .arg(&pchunks_key)
360                .query::<()>(&mut conn)
361                .map_err(|e| VectorStoreError::StorageError(e.to_string()))?;
362
363            let mut chunk_ids = Vec::new();
364            for (i, text) in chunks.iter().enumerate() {
365                let cid = format!("{}:chunk:{}", pid, i);
366                let chunk = ChunkDocument::new(cid.clone(), pid.clone(), text.clone(), i);
367                let cjson = serde_json::to_string(&chunk)
368                    .map_err(|e| VectorStoreError::StorageError(e.to_string()))?;
369                redis::cmd("SET")
370                    .arg(format!("{}:chunk:{}", config.key_prefix, cid))
371                    .arg(&cjson)
372                    .query::<()>(&mut conn)
373                    .map_err(|e| VectorStoreError::StorageError(e.to_string()))?;
374                redis::cmd("SADD")
375                    .arg(format!("{}:pchunks:{}", config.key_prefix, pid))
376                    .arg(&cid)
377                    .query::<()>(&mut conn)
378                    .map_err(|e| VectorStoreError::StorageError(e.to_string()))?;
379                redis::cmd("SADD")
380                    .arg(format!("{}:all_chunks", config.key_prefix))
381                    .arg(&cid)
382                    .query::<()>(&mut conn)
383                    .map_err(|e| VectorStoreError::StorageError(e.to_string()))?;
384                chunk_ids.push(cid);
385            }
386            redis::cmd("SADD")
387                .arg(format!("{}:parent_ids", config.key_prefix))
388                .arg(&pid)
389                .query::<()>(&mut conn)
390                .map_err(|e| VectorStoreError::StorageError(e.to_string()))?;
391            redis::cmd("SADD")
392                .arg(format!("{}:doc_ids", config.key_prefix))
393                .arg(&pid)
394                .query::<()>(&mut conn)
395                .map_err(|e| VectorStoreError::StorageError(e.to_string()))?;
396
397            Ok((pid, chunk_ids))
398        })
399        .await
400        .map_err(|e| VectorStoreError::StorageError(e.to_string()))?
401    }
402
403    async fn add_parent_documents(
404        &self,
405        documents: Vec<Document>,
406        chunk_size: usize,
407    ) -> Result<Vec<(String, Vec<String>)>, VectorStoreError> {
408        let mut results = Vec::new();
409        for doc in documents {
410            results.push(self.add_parent_document(doc, chunk_size).await?);
411        }
412        Ok(results)
413    }
414
415    async fn get_parent_document(
416        &self,
417        parent_id: &str,
418    ) -> Result<Option<Document>, VectorStoreError> {
419        self.get_document(parent_id).await
420    }
421
422    async fn get_chunk(&self, chunk_id: &str) -> Result<Option<ChunkDocument>, VectorStoreError> {
423        match self.get_str(&self.chunk_key(chunk_id)).await? {
424            Some(json) => match serde_json::from_str(&json) {
425                Ok(chunk) => Ok(Some(chunk)),
426                Err(e) => {
427                    log::error!(
428                        "failed to parse stored payload for chunk `{}` (corrupted or written by an older schema): {}",
429                        chunk_id,
430                        e
431                    );
432                    Ok(None)
433                }
434            },
435            None => Ok(None),
436        }
437    }
438
439    async fn get_chunk_document(
440        &self,
441        chunk_id: &str,
442    ) -> Result<Option<Document>, VectorStoreError> {
443        Ok(self.get_chunk(chunk_id).await?.map(|c| c.to_document()))
444    }
445
446    async fn get_chunks_for_parent(
447        &self,
448        parent_id: &str,
449    ) -> Result<Vec<ChunkDocument>, VectorStoreError> {
450        let ids = self.smembers(&self.parent_chunks_key(parent_id)).await?;
451        let mut chunks = Vec::new();
452        for id in ids {
453            if let Some(c) = self.get_chunk(&id).await? {
454                chunks.push(c);
455            }
456        }
457        chunks.sort_by_key(|c| c.segment);
458        Ok(chunks)
459    }
460
461    async fn get_chunk_documents_for_parent(
462        &self,
463        parent_id: &str,
464    ) -> Result<Vec<Document>, VectorStoreError> {
465        Ok(self
466            .get_chunks_for_parent(parent_id)
467            .await?
468            .into_iter()
469            .map(|c| c.to_document())
470            .collect())
471    }
472
473    async fn delete_parent_document(&self, parent_id: &str) -> Result<(), VectorStoreError> {
474        let chunks = self.get_chunks_for_parent(parent_id).await?;
475        for chunk in &chunks {
476            self.del(&self.chunk_key(&chunk.chunk_id)).await?;
477            // C1: `add_parent_document` adds each chunk id to the global `all_chunks`
478            // set (line 352); deleting the parent must remove them too, or the ids
479            // linger as ghost chunks in `chunk_count`/`get_all_chunks` pointing at
480            // already-deleted keys.
481            self.srem(&self.all_chunks_key(), &chunk.chunk_id).await?;
482        }
483        self.del(&self.doc_key(parent_id)).await?;
484        self.del(&self.parent_chunks_key(parent_id)).await?;
485        self.srem(&self.doc_ids_key(), parent_id).await?;
486        self.srem(&self.parent_ids_key(), parent_id).await
487    }
488
489    async fn parent_count(&self) -> usize {
490        self.scard(&self.parent_ids_key()).await.unwrap_or(0)
491    }
492
493    async fn chunk_count(&self) -> usize {
494        self.scard(&self.all_chunks_key()).await.unwrap_or(0)
495    }
496
497    async fn get_all_chunks(&self) -> Result<Vec<ChunkDocument>, VectorStoreError> {
498        let ids = self.smembers(&self.all_chunks_key()).await?;
499        let mut chunks = Vec::new();
500        for id in ids {
501            if let Some(c) = self.get_chunk(&id).await? {
502                chunks.push(c);
503            }
504        }
505        Ok(chunks)
506    }
507
508    async fn clear(&self) -> Result<(), VectorStoreError> {
509        // M29: use SCAN + DEL with prefix instead of FLUSHDB to avoid wiping other keys
510        self.clear_with_prefix().await
511    }
512
513    fn add_parent_document_blocking(
514        &self,
515        _document: Document,
516        _chunk_size: usize,
517    ) -> Result<(String, Vec<String>), VectorStoreError> {
518        Err(VectorStoreError::StorageError(
519            "blocking not supported, use async API".to_string(),
520        ))
521    }
522
523    fn get_parent_document_blocking(
524        &self,
525        _parent_id: &str,
526    ) -> Result<Option<Document>, VectorStoreError> {
527        Err(VectorStoreError::StorageError(
528            "blocking not supported, use async API".to_string(),
529        ))
530    }
531
532    fn get_chunk_blocking(
533        &self,
534        _chunk_id: &str,
535    ) -> Result<Option<ChunkDocument>, VectorStoreError> {
536        Err(VectorStoreError::StorageError(
537            "blocking not supported, use async API".to_string(),
538        ))
539    }
540
541    fn blocking_get_chunks_for_parent(
542        &self,
543        _parent_id: &str,
544    ) -> Result<Vec<ChunkDocument>, VectorStoreError> {
545        Err(VectorStoreError::StorageError(
546            "blocking not supported, use async API".to_string(),
547        ))
548    }
549}