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