lc_rag/
parent_document.rs1use async_trait::async_trait;
5use lc_vector_stores::{ChunkedDocumentStore, ChunkedDocumentStoreTrait, Document, SearchResult};
6use std::sync::Arc;
7use tokio::sync::RwLock;
8
9use crate::bm25::{AutoMergingConfig, ChunkedBM25Retriever};
10use crate::retriever::{RetrieverError, RetrieverTrait};
11
12pub struct ParentDocumentRetriever<S: ChunkedDocumentStoreTrait = ChunkedDocumentStore> {
29 inner: RwLock<ChunkedBM25Retriever<S>>,
30}
31
32impl<S: ChunkedDocumentStoreTrait> ParentDocumentRetriever<S> {
33 pub fn new(store: Arc<S>) -> Self {
35 Self {
36 inner: RwLock::new(ChunkedBM25Retriever::new(store)),
37 }
38 }
39
40 pub fn with_config(store: Arc<S>, config: AutoMergingConfig) -> Self {
46 Self {
47 inner: RwLock::new(ChunkedBM25Retriever::with_config(store, config)),
48 }
49 }
50
51 pub fn inner(&self) -> &RwLock<ChunkedBM25Retriever<S>> {
55 &self.inner
56 }
57}
58
59#[async_trait]
60impl<S: ChunkedDocumentStoreTrait> RetrieverTrait for ParentDocumentRetriever<S> {
61 async fn retrieve(&self, query: &str, k: usize) -> Result<Vec<Document>, RetrieverError> {
62 let inner = self.inner.read().await;
63 let matched = inner.search_matched_parents(query, k);
64 let docs: Vec<Document> = matched
65 .into_iter()
66 .filter_map(|(parent_id, _)| inner.get_parent_document(&parent_id))
67 .collect();
68 Ok(docs)
69 }
70
71 async fn retrieve_with_scores(
72 &self,
73 query: &str,
74 k: usize,
75 ) -> Result<Vec<SearchResult>, RetrieverError> {
76 let inner = self.inner.read().await;
77 let matched = inner.search_matched_parents(query, k);
78 let results: Vec<SearchResult> = matched
79 .into_iter()
80 .filter_map(|(parent_id, score)| {
81 inner
82 .get_parent_document(&parent_id)
83 .map(|document| SearchResult { document, score })
84 })
85 .collect();
86 Ok(results)
87 }
88
89 async fn add_documents(&self, documents: Vec<Document>) -> Result<(), RetrieverError> {
90 let mut inner = self.inner.write().await;
91 inner
92 .add_documents_async(documents)
93 .await
94 .map_err(RetrieverError::StoreError)
95 }
96}
97
98#[cfg(test)]
99mod tests {
100 use super::*;
101 use crate::RetrieverRunnable;
102 use lc_core::language_models::{BaseChatModel, BaseLanguageModel, LLMResult};
103 use lc_core::runnables::{Runnable, RunnableConfig, RunnableExt, RunnableLambda};
104 use lc_prompts::ChatPromptTemplate;
105 use lc_schema::Message;
106 use std::collections::HashMap;
107 use std::pin::Pin;
108
109 fn multi_leaf_parent_content() -> String {
111 let filler =
112 "lorem ipsum dolor sit amet consectetur adipiscing elit sed do eiusmod tempor \
113 incididunt ut labore et dolore magna aliqua. ";
114 format!("{filler}{filler}{filler}{filler} zebra")
115 }
116
117 fn test_retriever() -> ParentDocumentRetriever {
118 ParentDocumentRetriever::new(Arc::new(ChunkedDocumentStore::new()))
119 }
120
121 #[tokio::test]
122 async fn parent_document_returns_full_parent_on_chunk_hit() {
123 let retriever = test_retriever();
124 let content = multi_leaf_parent_content();
125 assert!(
126 content.len() > 400,
127 "content must span multiple leaves, got {} chars",
128 content.len()
129 );
130
131 retriever
132 .add_documents(vec![Document::new(content.clone())])
133 .await
134 .unwrap();
135
136 let docs = retriever.retrieve("zebra", 1).await.unwrap();
138 assert_eq!(docs.len(), 1);
139 assert_eq!(
140 docs[0].content, content,
141 "a leaf hit must return the FULL parent document, not the leaf chunk"
142 );
143 assert!(docs[0].content.len() > 400);
144 }
145
146 #[tokio::test]
147 async fn parent_document_retrieve_with_scores_reports_score() {
148 let retriever = test_retriever();
149 let content = multi_leaf_parent_content();
150 retriever
151 .add_documents(vec![Document::new(content.clone())])
152 .await
153 .unwrap();
154
155 let results = retriever.retrieve_with_scores("zebra", 1).await.unwrap();
156 assert_eq!(results.len(), 1);
157 assert!(results[0].score > 0.0, "BM25 score should be positive");
158 assert_eq!(results[0].document.content, content);
159 }
160
161 #[tokio::test]
164 async fn parent_document_chains_into_prompt_and_llm() {
165 let retriever = test_retriever();
166 let content = multi_leaf_parent_content();
167 retriever
168 .add_documents(vec![Document::new(content.clone())])
169 .await
170 .unwrap();
171
172 let step = RetrieverRunnable::new(Arc::new(retriever), 1);
173 let to_context = RunnableLambda::new_sync(|docs: Vec<Document>| {
175 HashMap::from([(
176 "context".to_string(),
177 docs.iter()
178 .map(|d| d.content.clone())
179 .collect::<Vec<_>>()
180 .join("\n\n"),
181 )])
182 });
183 let prompt = ChatPromptTemplate::from_messages([
184 Message::system("你是一个检索问答助手。"),
185 Message::human("请根据以下资料回答问题:\n\n{context}"),
186 ]);
187 let chain = step.pipe(to_context).pipe(prompt).pipe(MockChat);
188
189 let result: LLMResult = chain.invoke("zebra".to_string(), None).await.unwrap();
190 assert!(
191 result.content.contains("context has zebra: true"),
192 "the full parent doc (containing `zebra`) must reach the model, got: {}",
193 result.content
194 );
195 }
196
197 #[derive(Debug)]
199 struct MockChat;
200
201 #[derive(Debug)]
202 struct MockChatError(String);
203
204 impl std::fmt::Display for MockChatError {
205 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
206 write!(f, "MockChatError: {}", self.0)
207 }
208 }
209
210 impl std::error::Error for MockChatError {}
211
212 impl From<MockChatError> for lc_core::runnables::LcelError {
213 fn from(e: MockChatError) -> Self {
214 lc_core::runnables::LcelError::Other(e.0)
215 }
216 }
217
218 #[async_trait]
219 impl Runnable<Vec<Message>, LLMResult> for MockChat {
220 type Error = MockChatError;
221
222 async fn invoke(
223 &self,
224 input: Vec<Message>,
225 config: Option<RunnableConfig>,
226 ) -> Result<LLMResult, Self::Error> {
227 self.chat(input, config).await
228 }
229 }
230
231 #[async_trait]
232 impl BaseLanguageModel<Vec<Message>, LLMResult> for MockChat {
233 fn model_name(&self) -> &str {
234 "mock-chat"
235 }
236
237 fn get_num_tokens(&self, text: &str) -> usize {
238 text.len() / 4
239 }
240
241 fn with_temperature(self, _temp: f32) -> Self
242 where
243 Self: Sized,
244 {
245 self
246 }
247
248 fn with_max_tokens(self, _max: usize) -> Self
249 where
250 Self: Sized,
251 {
252 self
253 }
254 }
255
256 #[async_trait]
257 impl BaseChatModel for MockChat {
258 async fn chat(
259 &self,
260 messages: Vec<Message>,
261 _config: Option<RunnableConfig>,
262 ) -> Result<LLMResult, Self::Error> {
263 let last = messages
264 .last()
265 .map(|m| m.content.clone())
266 .unwrap_or_default();
267 Ok(LLMResult {
268 content: format!(
269 "context has zebra: {}; received {} chars of context",
270 last.contains("zebra"),
271 last.len()
272 ),
273 model: "mock-chat".to_string(),
274 token_usage: None,
275 tool_calls: None,
276 thinking_content: None,
277 })
278 }
279
280 async fn stream_chat(
281 &self,
282 _messages: Vec<Message>,
283 _config: Option<RunnableConfig>,
284 ) -> Result<
285 Pin<Box<dyn futures_util::Stream<Item = Result<String, Self::Error>> + Send>>,
286 Self::Error,
287 > {
288 unreachable!("stream_chat not exercised in parent_document tests")
289 }
290 }
291}