Skip to main content

swiftide_integrations/pgvector/
retrieve.rs

1use 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        // Metadata fields are stored each as prefixed meta_ fields. Perhaps we should add a single
36        // metadata field instead of multiple fields.
37        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        // Get the database pool
170        let pool = self.get_pool().await?;
171
172        // Build the custom query using both strategy and query state
173        let mut query_builder = search_strategy.build_query(&query).await?;
174
175        // Execute the query using the builder's built-in methods
176        let results = query_builder
177            .build_query_as::<VectorSearchResult>() // Convert to a typed query
178            .fetch_all(pool) // Execute and get all results
179            .await
180            .map_err(|e| anyhow!("Failed to execute search query: {e}"))?;
181
182        // Transform results into documents
183        let documents = results.into_iter().map(Into::into).collect();
184
185        // Update query state with retrieved documents
186        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}