swiftide_integrations/pgvector/
retrieve.rs1use crate::pgvector::{FieldConfig, PgVector, PgVectorBuilder};
2use anyhow::{Result, anyhow};
3use async_trait::async_trait;
4use pgvector::Vector;
5use sqlx::{Column, Postgres, QueryBuilder, Row, prelude::FromRow, types::Uuid};
6use swiftide_core::{
7 Retrieve,
8 document::Document,
9 indexing::Metadata,
10 querying::{
11 Query,
12 search_strategies::{CustomStrategy, SimilaritySingleEmbedding},
13 states,
14 },
15};
16
17#[allow(dead_code)]
18#[derive(Debug, Clone)]
19struct VectorSearchResult {
20 id: Uuid,
21 chunk: String,
22 metadata: Metadata,
23}
24
25impl From<VectorSearchResult> for Document {
26 fn from(val: VectorSearchResult) -> Self {
27 Document::new(val.chunk, Some(val.metadata))
28 }
29}
30
31impl FromRow<'_, sqlx::postgres::PgRow> for VectorSearchResult {
32 fn from_row(row: &sqlx::postgres::PgRow) -> Result<Self, sqlx::Error> {
33 let mut metadata = Metadata::default();
34
35 for column in row.columns() {
38 if column.name().starts_with("meta_") {
39 row.try_get::<serde_json::Value, _>(column.name())?
40 .as_object()
41 .and_then(|object| {
42 object.keys().collect::<Vec<_>>().first().map(|key| {
43 metadata.insert(
44 key.to_owned(),
45 object.get(key.as_str()).expect("infallible").clone(),
46 );
47 })
48 });
49 }
50 }
51
52 Ok(VectorSearchResult {
53 id: row.try_get("id")?,
54 chunk: row.try_get("chunk")?,
55 metadata,
56 })
57 }
58}
59
60#[allow(clippy::redundant_closure_for_method_calls)]
61#[async_trait]
62impl Retrieve<SimilaritySingleEmbedding<String>> for PgVector {
63 #[tracing::instrument]
64 async fn retrieve(
65 &self,
66 search_strategy: &SimilaritySingleEmbedding<String>,
67 query_state: Query<states::Pending>,
68 ) -> Result<Query<states::Retrieved>> {
69 let embedding = if let Some(embedding) = query_state.embedding.as_ref() {
70 Vector::from(embedding.clone())
71 } else {
72 return Err(anyhow::Error::msg("Missing embedding in query state"));
73 };
74
75 let vector_column_name = self.get_vector_column_name()?;
76
77 let pool = self.pool_get_or_initialize().await?;
78
79 let default_columns: Vec<_> = PgVectorBuilder::default_fields()
80 .iter()
81 .map(|f| f.field_name().to_string())
82 .chain(
83 self.fields
84 .iter()
85 .filter(|f| matches!(f, FieldConfig::Metadata(_)))
86 .map(|f| f.field_name().to_string()),
87 )
88 .collect();
89
90 if !PgVector::is_valid_identifier(&self.table_name)
91 || default_columns
92 .iter()
93 .any(|column| !PgVector::is_valid_identifier(column))
94 || !PgVector::is_valid_identifier(&vector_column_name)
95 {
96 return Err(anyhow!("Invalid table or field name"));
97 }
98
99 let mut sql = QueryBuilder::<Postgres>::new("SELECT ");
100 sql.push(default_columns.join(", "))
101 .push(" FROM ")
102 .push(&self.table_name);
103
104 if let Some(filter) = search_strategy.filter() {
105 let (key, value) = filter
106 .split_once('=')
107 .ok_or_else(|| anyhow!("Invalid filter format"))?;
108 let key = key.trim();
109 let value = value.trim().trim_matches('"');
110 let metadata_column = format!("meta_{}", PgVector::normalize_field_name(key));
111
112 tracing::debug!(
113 "Filter being applied: key = {:#?}, value = {:#?}",
114 key,
115 value
116 );
117
118 sql.push(" WHERE ")
119 .push(metadata_column)
120 .push("->>")
121 .push_bind(key.to_owned())
122 .push(" = ")
123 .push_bind(value.to_owned());
124 }
125
126 let top_k = i32::try_from(search_strategy.top_k())
127 .map_err(|_| anyhow!("Failed to convert top_k to i32"))?;
128
129 sql.push(" ORDER BY ")
130 .push(vector_column_name)
131 .push(" <=> ")
132 .push_bind(embedding)
133 .push(" LIMIT ")
134 .push_bind(top_k);
135
136 tracing::debug!("Running retrieve with SQL: {}", sql.sql().as_str());
137
138 let data: Vec<VectorSearchResult> = sql.build_query_as().fetch_all(pool).await?;
139
140 let docs = data.into_iter().map(Into::into).collect();
141
142 Ok(query_state.retrieved_documents(docs))
143 }
144}
145
146#[async_trait]
147impl Retrieve<SimilaritySingleEmbedding> for PgVector {
148 async fn retrieve(
149 &self,
150 search_strategy: &SimilaritySingleEmbedding,
151 query: Query<states::Pending>,
152 ) -> Result<Query<states::Retrieved>> {
153 Retrieve::<SimilaritySingleEmbedding<String>>::retrieve(
154 self,
155 &search_strategy.into_concrete_filter::<String>(),
156 query,
157 )
158 .await
159 }
160}
161
162#[async_trait]
163impl Retrieve<CustomStrategy<sqlx::QueryBuilder<sqlx::Postgres>>> for PgVector {
164 async fn retrieve(
165 &self,
166 search_strategy: &CustomStrategy<sqlx::QueryBuilder<sqlx::Postgres>>,
167 query: Query<states::Pending>,
168 ) -> Result<Query<states::Retrieved>> {
169 let pool = self.get_pool().await?;
171
172 let mut query_builder = search_strategy.build_query(&query).await?;
174
175 let results = query_builder
177 .build_query_as::<VectorSearchResult>() .fetch_all(pool) .await
180 .map_err(|e| anyhow!("Failed to execute search query: {e}"))?;
181
182 let documents = results.into_iter().map(Into::into).collect();
184
185 Ok(query.retrieved_documents(documents))
187 }
188}
189
190#[cfg(test)]
191mod tests {
192 use crate::pgvector::fixtures::TestContext;
193 use futures_util::TryStreamExt;
194 use std::collections::HashSet;
195 use swiftide_core::{Persist, indexing, indexing::EmbeddedField};
196 use swiftide_core::{
197 Retrieve,
198 querying::{Query, search_strategies::SimilaritySingleEmbedding, states},
199 };
200
201 #[test_log::test(tokio::test)]
202 async fn test_retrieve_multiple_docs_and_filter() {
203 let test_context = TestContext::setup_with_cfg(
204 vec!["filter"].into(),
205 HashSet::from([EmbeddedField::Combined]),
206 )
207 .await
208 .expect("Test setup failed");
209
210 let nodes = vec![
211 indexing::TextNode::new("test_query1").with_metadata(("filter", "true")),
212 indexing::TextNode::new("test_query2").with_metadata(("filter", "true")),
213 indexing::TextNode::new("test_query3").with_metadata(("filter", "false")),
214 ]
215 .into_iter()
216 .map(|node| {
217 node.with_vectors([(EmbeddedField::Combined, vec![1.0; 384])]);
218 node.to_owned()
219 })
220 .collect();
221
222 test_context
223 .pgv_storage
224 .batch_store(nodes)
225 .await
226 .try_collect::<Vec<_>>()
227 .await
228 .unwrap();
229
230 let mut query = Query::<states::Pending>::new("test_query");
231 query.embedding = Some(vec![1.0; 384]);
232
233 let search_strategy = SimilaritySingleEmbedding::<()>::default();
234 let result = test_context
235 .pgv_storage
236 .retrieve(&search_strategy, query.clone())
237 .await
238 .unwrap();
239
240 assert_eq!(result.documents().len(), 3);
241
242 let search_strategy =
243 SimilaritySingleEmbedding::from_filter("filter = \"true\"".to_string());
244
245 let result = test_context
246 .pgv_storage
247 .retrieve(&search_strategy, query.clone())
248 .await
249 .unwrap();
250
251 assert_eq!(result.documents().len(), 2);
252
253 let search_strategy =
254 SimilaritySingleEmbedding::from_filter("filter = \"banana\"".to_string());
255
256 let result = test_context
257 .pgv_storage
258 .retrieve(&search_strategy, query.clone())
259 .await
260 .unwrap();
261 assert_eq!(result.documents().len(), 0);
262 }
263
264 #[test_log::test(tokio::test)]
265 async fn test_retrieve_docs_with_metadata() {
266 let test_context = TestContext::setup_with_cfg(
267 vec!["other", "text"].into(),
268 HashSet::from([EmbeddedField::Combined]),
269 )
270 .await
271 .expect("Test setup failed");
272
273 let nodes = vec![
274 indexing::TextNode::new("test_query1")
275 .with_metadata([
276 ("other", serde_json::Value::from(10)),
277 ("text", serde_json::Value::from("some text")),
278 ])
279 .with_vectors([(EmbeddedField::Combined, vec![1.0; 384])])
280 .to_owned(),
281 ];
282
283 test_context
284 .pgv_storage
285 .batch_store(nodes)
286 .await
287 .try_collect::<Vec<_>>()
288 .await
289 .unwrap();
290
291 let mut query = Query::<states::Pending>::new("test_query");
292 query.embedding = Some(vec![1.0; 384]);
293
294 let search_strategy = SimilaritySingleEmbedding::<()>::default();
295 let result = test_context
296 .pgv_storage
297 .retrieve(&search_strategy, query.clone())
298 .await
299 .unwrap();
300
301 assert_eq!(result.documents().len(), 1);
302
303 let doc = result.documents().first().unwrap();
304 assert_eq!(
305 doc.metadata().get("other"),
306 Some(&serde_json::Value::from(10))
307 );
308 assert_eq!(
309 doc.metadata().get("text"),
310 Some(&serde_json::Value::from("some text"))
311 );
312 }
313}