1use std::{fmt::Display, ops::RangeInclusive};
12
13use rig_core::{
14 Embed,
15 embeddings::{Embedding, EmbeddingModel},
16 vector_store::{
17 InsertDocuments, VectorStoreError, VectorStoreIndex,
18 request::{SearchFilter, SqlCondition, VectorSearchRequest},
19 },
20};
21use serde::{Deserialize, Serialize, de::DeserializeOwned};
22use serde_json::Value;
23use sqlx::{PgPool, Postgres, postgres::PgArguments, query::QueryAs};
24use uuid::Uuid;
25
26pub struct PostgresVectorStore<Model: EmbeddingModel> {
27 model: Model,
28 pg_pool: PgPool,
29 documents_table: String,
30 distance_function: PgVectorDistanceFunction,
31}
32
33pub enum PgVectorDistanceFunction {
42 L2,
43 InnerProduct,
44 Cosine,
45 L1,
46 Hamming,
47 Jaccard,
48}
49
50impl Display for PgVectorDistanceFunction {
51 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
52 match self {
53 PgVectorDistanceFunction::L2 => write!(f, "<->"),
54 PgVectorDistanceFunction::InnerProduct => write!(f, "<#>"),
55 PgVectorDistanceFunction::Cosine => write!(f, "<=>"),
56 PgVectorDistanceFunction::L1 => write!(f, "<+>"),
57 PgVectorDistanceFunction::Hamming => write!(f, "<~>"),
58 PgVectorDistanceFunction::Jaccard => write!(f, "<%>"),
59 }
60 }
61}
62
63const PLACEHOLDER: &str = "$";
68
69#[derive(Clone, Default, Serialize, Deserialize, Debug)]
71pub struct PgSearchFilter(SqlCondition<serde_json::Value>);
72
73impl SearchFilter for PgSearchFilter {
74 type Value = serde_json::Value;
75
76 fn eq(key: impl AsRef<str>, value: Self::Value) -> Self {
77 Self(SqlCondition::binary(key, "=", PLACEHOLDER, value))
78 }
79
80 fn gt(key: impl AsRef<str>, value: Self::Value) -> Self {
81 Self(SqlCondition::binary(key, ">", PLACEHOLDER, value))
82 }
83
84 fn lt(key: impl AsRef<str>, value: Self::Value) -> Self {
85 Self(SqlCondition::binary(key, "<", PLACEHOLDER, value))
86 }
87
88 fn and(self, rhs: Self) -> Self {
89 Self(self.0.and(rhs.0))
90 }
91
92 fn or(self, rhs: Self) -> Self {
93 Self(self.0.or(rhs.0))
94 }
95}
96
97impl PgSearchFilter {
98 fn into_clause(self) -> (String, Vec<serde_json::Value>) {
99 self.0.into_parts()
100 }
101
102 #[allow(clippy::should_implement_trait)]
103 pub fn not(self) -> Self {
104 Self(self.0.not())
105 }
106
107 pub fn gte(key: String, value: <Self as SearchFilter>::Value) -> Self {
108 Self(SqlCondition::binary(key, ">=", PLACEHOLDER, value))
109 }
110
111 pub fn lte(key: String, value: <Self as SearchFilter>::Value) -> Self {
112 Self(SqlCondition::binary(key, "<=", PLACEHOLDER, value))
113 }
114
115 pub fn is_null(key: String) -> Self {
116 Self(SqlCondition::raw(format!("{key} is null")))
117 }
118
119 pub fn is_not_null(key: String) -> Self {
120 Self(SqlCondition::raw(format!("{key} is not null")))
121 }
122
123 pub fn between<T>(key: String, range: RangeInclusive<T>) -> Self
124 where
125 T: std::fmt::Display + Into<serde_json::Number> + Copy,
126 {
127 let lo = range.start();
128 let hi = range.end();
129
130 Self(SqlCondition::raw(format!("{key} between {lo} and {hi}")))
131 }
132
133 pub fn member(key: String, values: Vec<<Self as SearchFilter>::Value>) -> Self {
134 Self(SqlCondition::list(key, "is in", PLACEHOLDER, values))
135 }
136
137 pub fn like(key: String, pattern: &'static str) -> Self {
142 Self(SqlCondition::raw(format!("{key} like {pattern}")))
143 }
144
145 pub fn similar_to(key: String, pattern: &'static str) -> Self {
148 Self(SqlCondition::raw(format!("{key} similar to {pattern}")))
149 }
150}
151
152fn bind_value<S>(
153 builder: QueryAs<'_, Postgres, S, PgArguments>,
154 value: Value,
155) -> QueryAs<'_, Postgres, S, PgArguments> {
156 match value {
157 Value::Null => builder.bind(Option::<String>::None),
158 Value::Bool(b) => builder.bind(b),
159 Value::Number(num) => {
160 if let Some(n) = num.as_f64() {
161 builder.bind(n)
162 } else if let Some(n) = num.as_i64() {
163 builder.bind(n)
164 } else if let Some(n) = num.as_u64() {
165 builder.bind(n as i64)
166 } else {
167 builder.bind(num.to_string())
168 }
169 }
170 Value::String(s) => builder.bind(s),
171 Value::Array(xs) => {
172 if let Some(xs) = xs
173 .iter()
174 .map(|v| v.as_str().map(str::to_string))
175 .collect::<Option<Vec<_>>>()
176 {
177 builder.bind(xs)
178 } else if let Some(xs) = xs.iter().map(Value::as_f64).collect::<Option<Vec<_>>>() {
179 builder.bind(xs)
180 } else if let Some(xs) = xs.iter().map(Value::as_i64).collect::<Option<Vec<_>>>() {
181 builder.bind(xs)
182 } else if let Some(xs) = xs.iter().map(Value::as_bool).collect::<Option<Vec<_>>>() {
183 builder.bind(xs)
184 } else {
185 builder.bind(Value::Array(xs))
186 }
187 }
188 object => builder.bind(object),
190 }
191}
192
193#[derive(Debug, Deserialize, sqlx::FromRow)]
194pub struct SearchResult {
195 id: Uuid,
196 document: Value,
197 distance: f64,
199}
200
201#[derive(Debug, Deserialize, sqlx::FromRow)]
202pub struct SearchResultOnlyId {
203 id: Uuid,
204 distance: f64,
205}
206
207impl SearchResult {
208 pub fn into_result<T: DeserializeOwned>(self) -> Result<(f64, String, T), VectorStoreError> {
209 let document: T =
210 serde_json::from_value(self.document).map_err(VectorStoreError::JsonError)?;
211 Ok((self.distance, self.id.to_string(), document))
212 }
213}
214
215impl<Model> PostgresVectorStore<Model>
216where
217 Model: EmbeddingModel,
218{
219 pub fn new(
220 model: Model,
221 pg_pool: PgPool,
222 documents_table: Option<String>,
223 distance_function: PgVectorDistanceFunction,
224 ) -> Self {
225 Self {
226 model,
227 pg_pool,
228 documents_table: documents_table.unwrap_or(String::from("documents")),
229 distance_function,
230 }
231 }
232
233 pub fn with_defaults(model: Model, pg_pool: PgPool) -> Self {
234 Self::new(model, pg_pool, None, PgVectorDistanceFunction::Cosine)
235 }
236
237 async fn run_search<R>(
240 &self,
241 req: &VectorSearchRequest<PgSearchFilter>,
242 with_document: bool,
243 ) -> Result<Vec<R>, VectorStoreError>
244 where
245 R: for<'r> sqlx::FromRow<'r, sqlx::postgres::PgRow> + Send + Unpin,
246 {
247 if req.samples() > i64::MAX as u64 {
248 return Err(VectorStoreError::DatastoreError(
249 format!(
250 "The maximum amount of samples to return with the `rig` Postgres integration cannot be larger than {}",
251 i64::MAX
252 )
253 .into(),
254 ));
255 }
256
257 let embedded_query: pgvector::Vector = self
258 .model
259 .embed_text(req.query())
260 .await?
261 .vec
262 .iter()
263 .map(|&x| x as f32)
264 .collect::<Vec<f32>>()
265 .into();
266
267 let (search_query, params) = self.search_query(with_document, req);
268 let builder = sqlx::query_as(sqlx::AssertSqlSafe(search_query))
269 .bind(embedded_query)
270 .bind(req.samples() as i64);
271
272 let builder = params.iter().cloned().fold(builder, bind_value);
273
274 builder
275 .fetch_all(&self.pg_pool)
276 .await
277 .map_err(VectorStoreError::datastore)
278 }
279
280 fn search_query(
281 &self,
282 with_document: bool,
283 req: &VectorSearchRequest<PgSearchFilter>,
284 ) -> (String, Vec<serde_json::Value>) {
285 let document = if with_document { ", document" } else { "" };
286
287 let thresh = req
288 .threshold()
289 .map(|t| PgSearchFilter::gt("distance", t.into()));
290 let filter = match (thresh, req.filter()) {
291 (Some(thresh), Some(filt)) => Some(thresh.and(filt.clone())),
292 (Some(thresh), _) => Some(thresh),
293 (_, Some(filt)) => Some(filt.clone()),
294 _ => None,
295 };
296 let (where_clause, params) = match filter {
297 Some(f) => {
298 let (expr, params) = f.into_clause();
299 (String::from("WHERE") + &expr, params)
300 }
301 None => (Default::default(), Default::default()),
302 };
303
304 let mut counter = 3;
305 let mut buf = String::with_capacity(where_clause.len() * 2);
306
307 for c in where_clause.chars() {
308 buf.push(c);
309
310 if c == '$' {
311 buf.push_str(counter.to_string().as_str());
312 counter += 1;
313 }
314 }
315
316 let where_clause = buf;
317
318 let query = format!(
319 "
320 SELECT id{}, distance FROM ( \
321 SELECT DISTINCT ON (id) id{}, embedding {} $1 as distance \
322 FROM {} \
323 {where_clause} \
324 ORDER BY id, distance \
325 ) as d \
326 ORDER BY distance \
327 LIMIT $2",
328 document, document, self.distance_function, self.documents_table
329 );
330
331 (query, params)
332 }
333}
334
335impl<Model> InsertDocuments for PostgresVectorStore<Model>
336where
337 Model: EmbeddingModel + Send + Sync,
338{
339 async fn insert_documents<Doc: Serialize + Embed + Send>(
340 &self,
341 documents: Vec<(Doc, Vec<Embedding>)>,
342 ) -> Result<(), VectorStoreError> {
343 for (document, embeddings) in documents {
344 let id = Uuid::new_v4();
345 let json_document = serde_json::to_value(&document)?;
346
347 for embedding in embeddings {
348 let embedding_text = embedding.document;
349 let embedding: Vec<f64> = embedding.vec;
350
351 sqlx::query(sqlx::AssertSqlSafe(format!(
352 "INSERT INTO {} (id, document, embedded_text, embedding) VALUES ($1, $2, $3, $4)",
353 self.documents_table
354 )))
355 .bind(id)
356 .bind(&json_document)
357 .bind(&embedding_text)
358 .bind(&embedding)
359 .execute(&self.pg_pool)
360 .await
361 .map_err(VectorStoreError::datastore)?;
362 }
363 }
364
365 Ok(())
366 }
367}
368
369impl<Model> VectorStoreIndex for PostgresVectorStore<Model>
370where
371 Model: EmbeddingModel,
372{
373 type Filter = PgSearchFilter;
374
375 async fn top_n<T: for<'a> Deserialize<'a> + Send>(
378 &self,
379 req: VectorSearchRequest<PgSearchFilter>,
380 ) -> Result<Vec<(f64, String, T)>, VectorStoreError> {
381 let rows: Vec<SearchResult> = self.run_search(&req, true).await?;
382
383 let rows: Vec<(f64, String, T)> = rows
384 .into_iter()
385 .flat_map(SearchResult::into_result)
386 .collect();
387
388 Ok(rows)
389 }
390
391 async fn top_n_ids(
393 &self,
394 req: VectorSearchRequest<PgSearchFilter>,
395 ) -> Result<Vec<(f64, String)>, VectorStoreError> {
396 let rows: Vec<SearchResultOnlyId> = self.run_search(&req, false).await?;
397
398 let rows: Vec<(f64, String)> = rows
399 .into_iter()
400 .map(|row| (row.distance, row.id.to_string()))
401 .collect();
402
403 Ok(rows)
404 }
405}
406
407#[cfg(test)]
408mod tests {
409 use super::{PgSearchFilter, SearchFilter};
410 use serde_json::json;
411
412 #[test]
416 fn every_parameterised_operator_uses_dollar_placeholders() {
417 let gte = PgSearchFilter::gte("price".into(), json!(5));
418 let lte = PgSearchFilter::lte("price".into(), json!(10));
419
420 let (cond, values) = gte.and(lte).into_clause();
421 assert_eq!(cond, "(price >= $) AND (price <= $)");
422 assert!(!cond.contains('?'));
423 assert_eq!(cond.matches('$').count(), values.len());
424
425 let member = PgSearchFilter::member("id".into(), vec![json!(1), json!(2)]);
426 let (cond, values) = PgSearchFilter::eq("kind", json!("fruit"))
427 .and(member)
428 .into_clause();
429 assert_eq!(cond, "(kind = $) AND (id is in ($, $))");
430 assert!(!cond.contains('?'));
431 assert_eq!(cond.matches('$').count(), values.len());
432 }
433}