1#![cfg_attr(test, allow(clippy::expect_used))]
4use aws_sdk_s3vectors::{
15 Client,
16 types::{PutInputVector, VectorData},
17};
18use aws_smithy_types::Document;
19use rig_core::{
20 embeddings::EmbeddingModel,
21 vector_store::{
22 InsertDocuments, VectorStoreError, VectorStoreIndex,
23 request::{DynamicSearchFilter, Filter, FilterError, SearchFilter, VectorSearchRequest},
24 },
25};
26use serde::{Deserialize, Serialize};
27use serde_json::Value;
28use std::collections::HashMap;
29use uuid::Uuid;
30
31#[derive(Debug, Serialize, Deserialize)]
32pub struct CreateRecord {
33 document: serde_json::Value,
34 embedded_text: String,
35}
36
37#[derive(Clone, Debug)]
39pub struct S3SearchFilter(aws_smithy_types::Document);
40
41impl SearchFilter for S3SearchFilter {
42 type Value = aws_smithy_types::Document;
43
44 fn eq(key: impl AsRef<str>, value: Self::Value) -> Self {
45 Self(document_comparison(key, "$eq", value))
46 }
47
48 fn gt(key: impl AsRef<str>, value: Self::Value) -> Self {
49 Self(document_comparison(key, "$gt", value))
50 }
51
52 fn lt(key: impl AsRef<str>, value: Self::Value) -> Self {
53 Self(document_comparison(key, "$lt", value))
54 }
55
56 fn and(self, rhs: Self) -> Self {
57 Self(document_object([(
58 "$and",
59 Document::Array(vec![self.0, rhs.0]),
60 )]))
61 }
62
63 fn or(self, rhs: Self) -> Self {
64 Self(document_object([(
65 "$or",
66 Document::Array(vec![self.0, rhs.0]),
67 )]))
68 }
69}
70
71fn document_object<K>(entries: impl IntoIterator<Item = (K, Document)>) -> Document
73where
74 K: Into<String>,
75{
76 Document::Object(
77 entries
78 .into_iter()
79 .map(|(key, value)| (key.into(), value))
80 .collect(),
81 )
82}
83
84fn document_comparison(key: impl AsRef<str>, op: &str, value: Document) -> Document {
87 document_object([(key.as_ref(), document_object([(op, value)]))])
88}
89
90impl DynamicSearchFilter for S3SearchFilter {
91 fn from_dynamic_filter(filter: Filter<serde_json::Value>) -> Result<Self, FilterError> {
92 Ok(filter.interpret_with(|value| json_value_to_document(&value)))
93 }
94}
95
96impl S3SearchFilter {
97 pub fn inner(&self) -> &aws_smithy_types::Document {
98 &self.0
99 }
100
101 pub fn into_inner(self) -> aws_smithy_types::Document {
102 self.0
103 }
104
105 pub fn gte(key: String, value: <Self as SearchFilter>::Value) -> Self {
106 Self(document_comparison(key, "$gte", value))
107 }
108
109 pub fn lte(key: String, value: <Self as SearchFilter>::Value) -> Self {
110 Self(document_comparison(key, "$lte", value))
111 }
112
113 pub fn exists(key: String) -> Self {
114 Self(document_object([(
115 "$exists",
116 document_object([(key, Document::Bool(true))]),
117 )]))
118 }
119
120 #[allow(clippy::should_implement_trait)]
121 pub fn not(self) -> Self {
122 Self(document_object([("$not", self.0)]))
123 }
124}
125
126pub struct S3VectorsVectorStore<M> {
127 embedding_model: M,
128 client: Client,
129 bucket_name: String,
130 index_name: String,
131}
132
133impl<M> S3VectorsVectorStore<M>
134where
135 M: EmbeddingModel,
136{
137 pub fn new(
138 embedding_model: M,
139 client: aws_sdk_s3vectors::Client,
140 bucket_name: &str,
141 index_name: &str,
142 ) -> Self {
143 Self {
144 embedding_model,
145 client,
146 bucket_name: bucket_name.to_string(),
147 index_name: index_name.to_string(),
148 }
149 }
150
151 pub fn bucket_name(&self) -> &str {
152 &self.bucket_name
153 }
154
155 pub fn set_bucket_name(&mut self, bucket_name: &str) {
156 self.bucket_name = bucket_name.to_string();
157 }
158
159 pub fn index_name(&self) -> &str {
160 &self.index_name
161 }
162
163 pub fn set_index_name(&mut self, index_name: &str) {
164 self.index_name = index_name.to_string();
165 }
166
167 pub fn client(&self) -> &Client {
168 &self.client
169 }
170
171 async fn run_query(
174 &self,
175 req: &VectorSearchRequest<S3SearchFilter>,
176 return_metadata: bool,
177 ) -> Result<Vec<(f64, aws_sdk_s3vectors::types::QueryOutputVector)>, VectorStoreError> {
178 if req.samples() > i32::MAX as u64 {
179 return Err(VectorStoreError::DatastoreError(format!("The number of samples to return with the `rig` AWS S3Vectors integration cannot be higher than {}", i32::MAX).into()));
180 }
181
182 let embedding = self
183 .embedding_model
184 .embed_text(req.query())
185 .await?
186 .vec
187 .into_iter()
188 .map(|x| x as f32)
189 .collect();
190
191 let mut query_builder = self
192 .client
193 .query_vectors()
194 .query_vector(VectorData::Float32(embedding))
195 .top_k(req.samples() as i32)
196 .return_distance(true)
197 .vector_bucket_name(self.bucket_name())
198 .index_name(self.index_name());
199
200 if return_metadata {
201 query_builder = query_builder.return_metadata(true);
202 }
203
204 if let Some(filter) = req.filter() {
205 query_builder = query_builder.filter(filter.inner().clone())
206 }
207
208 let query = query_builder
209 .send()
210 .await
211 .map_err(VectorStoreError::datastore)?;
212
213 Ok(query
214 .vectors
215 .into_iter()
216 .map(|x| {
217 let distance = x.distance.ok_or_else(|| {
218 VectorStoreError::DatastoreError("S3Vectors response missing distance".into())
219 })? as f64;
220
221 Ok((distance, x))
222 })
223 .collect::<Result<Vec<_>, VectorStoreError>>()?
224 .into_iter()
225 .filter(|(distance, _)| {
226 !req.threshold()
227 .is_some_and(|threshold| *distance < threshold)
228 })
229 .collect())
230 }
231}
232
233impl<M> InsertDocuments for S3VectorsVectorStore<M>
234where
235 M: EmbeddingModel,
236{
237 async fn insert_documents<Doc: serde::Serialize + rig_core::Embed + Send>(
238 &self,
239 documents: Vec<(Doc, Vec<rig_core::embeddings::Embedding>)>,
240 ) -> Result<(), rig_core::vector_store::VectorStoreError> {
241 let docs: Vec<PutInputVector> =
242 rig_core::vector_store::flatten_embedded(documents, |json_value, y| {
243 let document = CreateRecord {
244 document: json_value.clone(),
245 embedded_text: y.document,
246 };
247 let document =
248 serde_json::to_value(&document).map_err(VectorStoreError::JsonError)?;
249 let document = json_value_to_document(&document);
250 let vec = y.vec.into_iter().map(|item| item as f32).collect();
251 PutInputVector::builder()
252 .metadata(document.clone())
253 .data(VectorData::Float32(vec))
254 .key(Uuid::new_v4())
255 .build()
256 .map_err(|x| {
257 VectorStoreError::DatastoreError(
258 format!("Couldn't build vector input: {x}").into(),
259 )
260 })
261 })
262 .map_err(|x| {
263 VectorStoreError::DatastoreError(
264 format!("Could not build vector store data: {x}").into(),
265 )
266 })?;
267
268 self.client
269 .put_vectors()
270 .vector_bucket_name(self.bucket_name())
271 .set_vectors(Some(docs))
272 .set_index_name(Some(self.index_name.clone()))
273 .send()
274 .await
275 .map_err(|x| {
276 VectorStoreError::DatastoreError(
277 format!("Error while submitting document insertion request: {x}").into(),
278 )
279 })?;
280
281 Ok(())
282 }
283}
284
285fn json_value_to_document(value: &Value) -> Document {
286 match value {
287 Value::Null => Document::Null,
288 Value::Bool(b) => Document::Bool(*b),
289 Value::Number(n) => {
290 if let Some(i) = n.as_i64() {
291 Document::Number(aws_smithy_types::Number::NegInt(i))
292 } else if let Some(u) = n.as_u64() {
293 Document::Number(aws_smithy_types::Number::PosInt(u))
294 } else if let Some(f) = n.as_f64() {
295 Document::Number(aws_smithy_types::Number::Float(f))
296 } else {
297 Document::Null }
299 }
300 Value::String(s) => Document::String(s.clone()),
301 Value::Array(arr) => Document::Array(arr.iter().map(json_value_to_document).collect()),
302 Value::Object(obj) => Document::Object(
303 obj.iter()
304 .map(|(k, v)| (k.clone(), json_value_to_document(v)))
305 .collect::<HashMap<_, _>>(),
306 ),
307 }
308}
309
310fn document_to_json_value(value: &Document) -> Value {
311 match value {
312 Document::Null => Value::Null,
313 Document::Bool(b) => Value::Bool(*b),
314 Document::Number(n) => match n {
315 aws_smithy_types::Number::Float(f) => serde_json::Number::from_f64(*f)
316 .map(Value::Number)
317 .unwrap_or_else(|| Value::String(f.to_string())),
318 aws_smithy_types::Number::NegInt(i) => {
319 serde_json::Value::Number(serde_json::Number::from(*i))
320 }
321 aws_smithy_types::Number::PosInt(u) => {
322 serde_json::Value::Number(serde_json::Number::from(*u))
323 }
324 },
325 Document::String(s) => Value::String(s.clone()),
326 Document::Array(arr) => Value::Array(arr.iter().map(document_to_json_value).collect()),
327 Document::Object(obj) => {
328 let res = obj
329 .iter()
330 .map(|(k, v)| (k.clone(), document_to_json_value(v)))
331 .collect::<serde_json::Map<String, serde_json::Value>>();
332
333 serde_json::Value::Object(res)
334 }
335 }
336}
337
338impl<M> VectorStoreIndex for S3VectorsVectorStore<M>
339where
340 M: EmbeddingModel,
341{
342 type Filter = S3SearchFilter;
343
344 async fn top_n<T: for<'a> serde::Deserialize<'a> + Send>(
345 &self,
346 req: VectorSearchRequest<S3SearchFilter>,
347 ) -> Result<Vec<(f64, String, T)>, VectorStoreError> {
348 self.run_query(&req, true)
349 .await?
350 .into_iter()
351 .map(|(distance, x)| {
352 let metadata_document = x.metadata.ok_or_else(|| {
353 VectorStoreError::DatastoreError("S3Vectors response missing metadata".into())
354 })?;
355 let val = document_to_json_value(&metadata_document);
356 let metadata: T = serde_json::from_value(val)?;
357
358 Ok((distance, x.key, metadata))
359 })
360 .collect()
361 }
362
363 async fn top_n_ids(
364 &self,
365 req: VectorSearchRequest<S3SearchFilter>,
366 ) -> Result<Vec<(f64, String)>, VectorStoreError> {
367 Ok(self
368 .run_query(&req, false)
369 .await?
370 .into_iter()
371 .map(|(distance, x)| (distance, x.key))
372 .collect())
373 }
374}
375
376#[cfg(test)]
377mod tests {
378 use super::*;
379
380 #[test]
381 fn dynamic_filter_compiles_to_native_aws_documents() {
382 let filter = Filter::eq("status", serde_json::json!("ready"))
383 .and(Filter::eq("tags", serde_json::json!(["rust", "ai"])))
384 .and(Filter::gt("score", serde_json::json!(4.5)));
385
386 let compiled = S3SearchFilter::from_dynamic_filter(filter)
387 .expect("JSON values should compile to AWS documents");
388
389 assert_eq!(
390 document_to_json_value(compiled.inner()),
391 serde_json::json!({
392 "$and": [{
393 "$and": [
394 { "status": { "$eq": "ready" } },
395 { "tags": { "$eq": ["rust", "ai"] } }
396 ]
397 }, {
398 "score": { "$gt": 4.5 }
399 }]
400 })
401 );
402 }
403
404 #[test]
405 fn extension_operators_build_the_documented_filter_shapes() {
406 let number = |n| Document::Number(aws_smithy_types::Number::PosInt(n));
407 let filter = S3SearchFilter::gte("score".into(), number(5))
408 .or(S3SearchFilter::lte("score".into(), number(1)))
409 .or(S3SearchFilter::exists("status".into()))
410 .not();
411
412 assert_eq!(
413 document_to_json_value(filter.inner()),
414 serde_json::json!({
415 "$not": {
416 "$or": [
417 { "$or": [
418 { "score": { "$gte": 5 } },
419 { "score": { "$lte": 1 } }
420 ]},
421 { "$exists": { "status": true } }
422 ]
423 }
424 })
425 );
426 }
427}