use crate::actions::helpers;
use crate::actions::vector_index::{
hash_attr, parse_attr_defs, parse_vector_defs, scalar_type_str,
};
use crate::actions::vector_lifecycle::VectorIndexLifecycle;
use crate::errors::{DynoxideError, Result};
use crate::expressions;
use crate::expressions::PathElement;
use crate::expressions::condition::{CompOp, ConditionExpr, Operand};
use crate::storage_backend::{StorageBackend, VectorCandidateRow};
use crate::types::{AttributeValue, Item};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
#[derive(Debug, Default, Deserialize)]
struct SearchVectorsRequestRaw {
#[serde(rename = "IndexName")]
index_name: String,
#[serde(rename = "SearchVector", deserialize_with = "strict_values::vector")]
search_vector: Vec<serde_json::Value>,
#[serde(rename = "TableName")]
table_name: String,
#[serde(rename = "TopK")]
top_k: i32,
#[serde(rename = "SearchConditionExpression", default)]
search_condition_expression: Option<String>,
#[serde(rename = "ProjectionExpression", default)]
projection_expression: Option<String>,
#[serde(rename = "ExpressionAttributeNames", default)]
expression_attribute_names: Option<HashMap<String, String>>,
#[serde(
rename = "ExpressionAttributeValues",
default,
deserialize_with = "strict_values::value_map"
)]
expression_attribute_values: Option<Vec<(String, serde_json::Value)>>,
#[serde(rename = "ReturnConsumedCapacity", default)]
return_consumed_capacity: Option<String>,
}
mod strict_values {
use serde::Deserialize;
use serde::de::{self, Deserializer, MapAccess, SeqAccess, Unexpected, Visitor};
use serde_json::Value;
use std::collections::HashMap;
use std::fmt;
const VARIANTS: &[&str] = &[
"B",
"BOOL",
"BS",
"DECIMAL",
"DECIMALSET",
"DOUBLE",
"DOUBLESET",
"INT",
"INTSET",
"L",
"M",
"N",
"NS",
"NULL",
"S",
"SS",
];
pub(super) const INTERNAL_TYPES: &[&str] = &[
"DECIMAL",
"DECIMALSET",
"DOUBLE",
"DOUBLESET",
"INT",
"INTSET",
];
fn shapeless<E: de::Error>() -> E {
E::custom(crate::serde_errors::SERIALIZATION_MARKER)
}
struct Shape;
struct Element(Value);
impl<'de> Deserialize<'de> for Element {
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
deserializer.deserialize_any(Shape).map(Element)
}
}
struct Base64Text(String);
impl<'de> Deserialize<'de> for Base64Text {
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
struct Text;
impl Visitor<'_> for Text {
type Value = String;
fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result {
f.write_str("a Base64-encoded string")
}
fn visit_str<E: de::Error>(self, v: &str) -> Result<String, E> {
Ok(v.to_string())
}
}
deserializer.deserialize_string(Text).map(Base64Text)
}
}
impl<'de> Visitor<'de> for Shape {
type Value = Value;
fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result {
f.write_str("an attribute value")
}
fn visit_map<A: MapAccess<'de>>(self, mut map: A) -> Result<Value, A::Error> {
let Some(member) = map.next_key::<String>()? else {
return Err(shapeless());
};
let value = match member.as_str() {
"S" | "N" | "INT" | "DECIMAL" | "DOUBLE" => Value::String(map.next_value()?),
"B" => Value::String(map.next_value::<Base64Text>()?.0),
"BOOL" | "NULL" => Value::Bool(map.next_value()?),
"SS" | "NS" | "INTSET" | "DECIMALSET" | "DOUBLESET" => {
Value::from(map.next_value::<Vec<String>>()?)
}
"BS" => Value::from(
map.next_value::<Vec<Base64Text>>()?
.into_iter()
.map(|text| text.0)
.collect::<Vec<_>>(),
),
"L" => Value::Array(
map.next_value::<Vec<Element>>()?
.into_iter()
.map(|element| element.0)
.collect(),
),
"M" => Value::Object(
map.next_value::<HashMap<String, Element>>()?
.into_iter()
.map(|(name, element)| (name, element.0))
.collect(),
),
_ => return Err(de::Error::unknown_variant(&member, VARIANTS)),
};
if map.next_key::<de::IgnoredAny>()?.is_some() {
return Err(shapeless());
}
let mut object = serde_json::Map::new();
object.insert(member, value);
Ok(Value::Object(object))
}
fn visit_str<E: de::Error>(self, v: &str) -> Result<Value, E> {
if VARIANTS.contains(&v) {
Err(E::invalid_type(Unexpected::UnitVariant, &"newtype variant"))
} else {
Err(E::unknown_variant(v, VARIANTS))
}
}
fn visit_unit<E: de::Error>(self) -> Result<Value, E> {
Err(shapeless())
}
fn visit_bool<E: de::Error>(self, _: bool) -> Result<Value, E> {
Err(shapeless())
}
fn visit_i64<E: de::Error>(self, _: i64) -> Result<Value, E> {
Err(shapeless())
}
fn visit_u64<E: de::Error>(self, _: u64) -> Result<Value, E> {
Err(shapeless())
}
fn visit_f64<E: de::Error>(self, _: f64) -> Result<Value, E> {
Err(shapeless())
}
fn visit_seq<A: SeqAccess<'de>>(self, _: A) -> Result<Value, A::Error> {
Err(shapeless())
}
}
pub(super) fn vector<'de, D: Deserializer<'de>>(
deserializer: D,
) -> Result<Vec<Value>, D::Error> {
let values = Vec::<Element>::deserialize(deserializer)?;
Ok(values.into_iter().map(|value| value.0).collect())
}
pub(super) fn value_map<'de, D: Deserializer<'de>>(
deserializer: D,
) -> Result<Option<Vec<(String, Value)>>, D::Error> {
struct Values;
impl<'de> Visitor<'de> for Values {
type Value = Vec<(String, Value)>;
fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result {
f.write_str("a map")
}
fn visit_map<A: MapAccess<'de>>(self, mut map: A) -> Result<Self::Value, A::Error> {
let mut values = Vec::new();
while let Some((name, value)) = map.next_entry::<String, Element>()? {
values.push((name, value.0));
}
Ok(values)
}
}
struct Present;
impl<'de> Visitor<'de> for Present {
type Value = Option<Vec<(String, Value)>>;
fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result {
f.write_str("a map")
}
fn visit_none<E: de::Error>(self) -> Result<Self::Value, E> {
Ok(None)
}
fn visit_unit<E: de::Error>(self) -> Result<Self::Value, E> {
Ok(None)
}
fn visit_some<D: Deserializer<'de>>(self, d: D) -> Result<Self::Value, D::Error> {
d.deserialize_map(Values).map(Some)
}
}
deserializer.deserialize_option(Present)
}
}
fn read_expression_value(raw: serde_json::Value) -> std::result::Result<AttributeValue, String> {
if let Some(object) = raw.as_object() {
if object
.keys()
.any(|member| strict_values::INTERNAL_TYPES.contains(&member.as_str()))
{
return Err(format!(
"{}This type mapping is not supported",
crate::serde_errors::VALIDATION_MARKER
));
}
if let Some(refusal) = object
.iter()
.find_map(|(member, value)| crate::types::empty_set_refusal(member, value))
{
return Err(format!(
"{}{refusal}",
crate::serde_errors::VALIDATION_MARKER
));
}
}
AttributeValue::deserialize(raw).map_err(|e| e.to_string())
}
fn read_vector_element(raw: serde_json::Value) -> Option<AttributeValue> {
match raw {
serde_json::Value::Object(mut object) if object.len() == 1 => match object.remove("N") {
Some(serde_json::Value::String(n)) => Some(AttributeValue::N(n)),
_ => None,
},
_ => None,
}
}
#[derive(Debug, Default)]
pub struct SearchVectorsRequest {
pub table_name: String,
pub index_name: String,
pub search_vector: Vec<AttributeValue>,
pub top_k: i64,
pub search_condition_expression: Option<String>,
pub projection_expression: Option<String>,
pub expression_attribute_names: Option<HashMap<String, String>>,
pub expression_attribute_values: Option<HashMap<String, AttributeValue>>,
pub return_consumed_capacity: Option<String>,
}
fn member_constraint_errors(index_name: &str, search_vector_len: usize, top_k: i64) -> Vec<String> {
let mut errors = Vec::new();
if index_name.len() < 3 {
errors.push(
"Value at 'IndexName' failed to satisfy constraint: \
Member must have length greater than or equal to 3"
.to_string(),
);
} else if index_name.len() > 255 {
errors.push(
"Value at 'IndexName' failed to satisfy constraint: \
Member must have length less than or equal to 255"
.to_string(),
);
}
if search_vector_len == 0 {
errors.push(
"Value at 'SearchVector' failed to satisfy constraint: \
Member must have length greater than or equal to 1"
.to_string(),
);
}
if top_k < 1 {
errors.push(
"Value at 'TopK' failed to satisfy constraint: \
Member must have value greater than or equal to 1"
.to_string(),
);
}
errors
}
impl<'de> serde::Deserialize<'de> for SearchVectorsRequest {
fn deserialize<D: serde::Deserializer<'de>>(
deserializer: D,
) -> std::result::Result<Self, D::Error> {
let raw = SearchVectorsRequestRaw::deserialize(deserializer).map_err(|e| {
let msg = e.to_string();
let marker = if msg.starts_with("invalid type: ") {
crate::serde_errors::SERIALIZATION_AS_READ_MARKER
} else if msg.starts_with("invalid value: ") {
crate::serde_errors::VALIDATION_AS_READ_MARKER
} else {
return e;
};
serde::de::Error::custom(format!("{marker}{msg}"))
})?;
use crate::validation::format_validation_errors;
if raw.table_name.is_empty() {
return Err(serde::de::Error::custom(format!(
"VALIDATION:{}",
crate::validation::envelope_message(crate::validation::EMPTY_TABLE_NAME_REJECTION)
)));
}
let errors = member_constraint_errors(
&raw.index_name,
raw.search_vector.len(),
i64::from(raw.top_k),
);
if let Some(msg) = format_validation_errors(&errors) {
return Err(serde::de::Error::custom(format!("VALIDATION:{msg}")));
}
if let Some(msg) = crate::validation::first_table_name_error(&raw.table_name) {
return Err(serde::de::Error::custom(format!("VALIDATION:{msg}")));
}
if let Some(e) = return_consumed_capacity_error(raw.return_consumed_capacity.as_deref()) {
return Err(serde::de::Error::custom(format!("VALIDATION:{e}")));
}
if let Some(e) = top_k_ceiling_error(i64::from(raw.top_k)) {
return Err(serde::de::Error::custom(format!("VALIDATION:{e}")));
}
let search_vector = raw
.search_vector
.into_iter()
.map(read_vector_element)
.collect::<Option<Vec<_>>>()
.ok_or_else(|| {
serde::de::Error::custom(format!("VALIDATION:{}", invalid_search_vector()))
})?;
search_vector_floats(&search_vector)
.map_err(|e| serde::de::Error::custom(format!("VALIDATION:{e}")))?;
let expression_attribute_values = raw
.expression_attribute_values
.map(|values| {
values
.into_iter()
.map(|(name, value)| Ok((name, read_expression_value(value)?)))
.collect::<std::result::Result<HashMap<_, _>, String>>()
})
.transpose()
.map_err(serde::de::Error::custom)?;
Ok(SearchVectorsRequest {
table_name: raw.table_name,
index_name: raw.index_name,
search_vector,
top_k: i64::from(raw.top_k),
search_condition_expression: raw.search_condition_expression,
projection_expression: raw.projection_expression,
expression_attribute_names: raw.expression_attribute_names,
expression_attribute_values,
return_consumed_capacity: raw.return_consumed_capacity,
})
}
}
#[derive(Debug, Default, Serialize)]
pub struct SearchVectorsResponse {
#[serde(rename = "SearchResults")]
pub search_results: Vec<SearchResult>,
#[serde(rename = "ConsumedCapacity", skip_serializing_if = "Option::is_none")]
pub consumed_capacity: Option<crate::types::VectorSearchCapacity>,
}
fn search_scanned_bytes(candidates: &[VectorCandidateRow]) -> f64 {
let scanned: i64 = candidates.iter().map(|row| row.entry_bytes).sum();
crate::types::vector_request_bytes(scanned.max(0) as usize)
}
#[derive(Debug, Serialize)]
pub struct SearchResult {
#[serde(rename = "Item")]
pub item: Item,
#[serde(rename = "Score")]
pub score: f64,
}
fn invalid_search_vector() -> DynoxideError {
DynoxideError::ValidationException(
"Search vector contains invalid values. All values in the search vector must be a \
32-bit floating-point number attribute"
.to_string(),
)
}
fn invalid_comparator() -> DynoxideError {
DynoxideError::ValidationException(
"Invalid SearchConditionExpression: Invalid comparator used in SearchConditionExpression"
.to_string(),
)
}
struct EqualityPair {
attr: String,
nested: bool,
value: AttributeValue,
}
fn collect_equalities(
expr: &ConditionExpr,
tracker: &expressions::TrackedExpressionAttributes,
pairs: &mut Vec<EqualityPair>,
attrs: &mut Vec<String>,
) -> Result<()> {
match expr {
ConditionExpr::And(left, right) => {
collect_equalities(left, tracker, pairs, attrs)?;
collect_equalities(right, tracker, pairs, attrs)?;
Ok(())
}
ConditionExpr::Comparison { left, op, right } => {
if *op != CompOp::Eq {
return Err(invalid_comparator());
}
let (path, value_ref) = match (left, right) {
(Operand::Path(p), Operand::ValueRef(v))
| (Operand::ValueRef(v), Operand::Path(p)) => (p, v),
_ => return Err(invalid_comparator()),
};
let top = match path.first() {
Some(PathElement::Attribute(name)) => tracker.resolve_name(name).map_err(|e| {
DynoxideError::ValidationException(format!(
"Invalid SearchConditionExpression: {e}"
))
})?,
_ => return Err(invalid_comparator()),
};
let value = tracker
.resolve_value(value_ref)
.map_err(|e| {
DynoxideError::ValidationException(format!(
"Invalid SearchConditionExpression: {e}"
))
})?
.clone();
attrs.push(top.clone());
pairs.push(EqualityPair {
attr: top,
nested: path.len() > 1,
value,
});
Ok(())
}
_ => Err(invalid_comparator()),
}
}
fn score_candidate(distance_function: &str, query: &[f32], stored: &[f32]) -> f64 {
let score = match distance_function {
"EUCLIDEAN" => {
let mut sum = 0.0f64;
for (q, s) in query.iter().zip(stored) {
let d = f64::from(*q) - f64::from(*s);
sum += d * d;
}
sum.sqrt()
}
"DOT_PRODUCT" => {
let mut dot = 0.0f64;
for (q, s) in query.iter().zip(stored) {
dot += f64::from(*q) * f64::from(*s);
}
dot
}
_ => {
let mut dot = 0.0f64;
let mut qq = 0.0f64;
let mut ss = 0.0f64;
for (q, s) in query.iter().zip(stored) {
dot += f64::from(*q) * f64::from(*s);
qq += f64::from(*q) * f64::from(*q);
ss += f64::from(*s) * f64::from(*s);
}
if qq == 0.0 || ss == 0.0 {
return 1.0;
}
if query == stored {
return 0.0;
}
let c = 1.0 - dot / (qq.sqrt() * ss.sqrt());
c.clamp(0.0, 2.0)
}
};
score.clamp(-f64::from(f32::MAX), f64::from(f32::MAX))
}
fn higher_is_better(distance_function: &str) -> bool {
distance_function == "DOT_PRODUCT"
}
fn search_vector_floats(search_vector: &[AttributeValue]) -> Result<Vec<f32>> {
search_vector
.iter()
.map(|elem| match elem {
AttributeValue::N(n) => match n.parse::<f32>() {
Ok(v) if v.is_finite() => Ok(v),
_ => Err(invalid_search_vector()),
},
_ => Err(invalid_search_vector()),
})
.collect()
}
fn return_consumed_capacity_error(mode: Option<&str>) -> Option<DynoxideError> {
crate::validation::return_consumed_capacity_rejection(mode)
.map(DynoxideError::ValidationException)
}
fn top_k_ceiling_error(top_k: i64) -> Option<DynoxideError> {
(top_k > 100).then(|| {
DynoxideError::ValidationException(format!(
"Provided TopK value '{top_k}' is out of valid range. The value must be between 1 \
and 100 inclusive"
))
})
}
fn check_expression_parameters(request: &SearchVectorsRequest) -> Result<()> {
let refuse = |message: String| Err(DynoxideError::ValidationException(message));
let names = request.expression_attribute_names.as_ref();
let values = request.expression_attribute_values.as_ref();
if names.is_some_and(HashMap::is_empty) {
return refuse("ExpressionAttributeNames must not be empty".to_string());
}
if values.is_some_and(HashMap::is_empty) {
return refuse("ExpressionAttributeValues must not be empty".to_string());
}
for (map, keys, prefix) in [
(
"ExpressionAttributeNames",
names.map(|n| n.keys().collect::<Vec<_>>()),
'#',
),
(
"ExpressionAttributeValues",
values.map(|v| v.keys().collect::<Vec<_>>()),
':',
),
] {
let mut keys = keys.unwrap_or_default();
keys.sort();
if let Some(key) = keys
.into_iter()
.find(|key| !key.starts_with(prefix) || !key.is_ascii())
{
return refuse(format!(
"{map} contains invalid key: Syntax error; key: \"{key}\""
));
}
}
let condition = request.search_condition_expression.is_some();
if names.is_some() && !condition && request.projection_expression.is_none() {
return refuse(
"ExpressionAttributeNames can only be specified when using expressions".to_string(),
);
}
if values.is_some() && !condition {
return refuse(
"ExpressionAttributeValues can only be specified when using expressions".to_string(),
);
}
Ok(())
}
pub async fn execute<S: StorageBackend>(
storage: &S,
request: SearchVectorsRequest,
lifecycle: &VectorIndexLifecycle,
) -> Result<SearchVectorsResponse> {
if request.table_name.is_empty() {
return Err(DynoxideError::ValidationException(
crate::validation::envelope_message(crate::validation::EMPTY_TABLE_NAME_REJECTION),
));
}
if let Some(msg) = crate::validation::format_validation_errors(&member_constraint_errors(
&request.index_name,
request.search_vector.len(),
request.top_k,
)) {
return Err(DynoxideError::ValidationException(msg));
}
if let Some(msg) = crate::validation::first_table_name_error(&request.table_name) {
return Err(DynoxideError::ValidationException(msg));
}
if let Some(e) = return_consumed_capacity_error(request.return_consumed_capacity.as_deref()) {
return Err(e);
}
if let Some(e) = top_k_ceiling_error(request.top_k) {
return Err(e);
}
let query_vec = search_vector_floats(&request.search_vector)?;
for value in request
.expression_attribute_values
.iter()
.flat_map(HashMap::values)
{
let raw = serde_json::to_value(value)
.map_err(|e| DynoxideError::InternalServerError(e.to_string()))?;
read_expression_value(raw).map_err(|msg| {
crate::serde_errors::classify_marked_serde_error(&msg)
.unwrap_or(DynoxideError::ValidationException(msg))
})?;
}
check_expression_parameters(&request)?;
let tracker = expressions::TrackedExpressionAttributes::new(
&request.expression_attribute_names,
&request.expression_attribute_values,
);
let mut pairs: Vec<EqualityPair> = Vec::new();
let mut referenced_attrs: Vec<String> = Vec::new();
let condition = match request.search_condition_expression.as_deref() {
Some("") => {
return Err(DynoxideError::ValidationException(
"Invalid SearchConditionExpression: The expression can not be empty;".to_string(),
));
}
Some(expr_str) => {
let parsed = expressions::condition::parse(expr_str).map_err(|e| {
DynoxideError::ValidationException(format!(
"Invalid SearchConditionExpression: {e}"
))
})?;
expressions::condition::validate_name_refs(
&parsed,
&request.expression_attribute_names,
)
.map_err(|e| {
DynoxideError::ValidationException(format!(
"Invalid SearchConditionExpression: {e}"
))
})?;
collect_equalities(&parsed, &tracker, &mut pairs, &mut referenced_attrs)?;
Some(parsed)
}
None => None,
};
let projection = match request.projection_expression.as_deref() {
Some("") => {
return Err(DynoxideError::ValidationException(
"Invalid ProjectionExpression: The expression can not be empty;".to_string(),
));
}
Some(proj_expr) => Some(
expressions::projection::parse(proj_expr)
.map_err(DynoxideError::ValidationException)?,
),
None => None,
};
if pairs.iter().any(|p| p.nested) {
return Err(DynoxideError::ValidationException(
"Invalid SearchConditionExpression: SearchConditionExpression cannot have \
conditions on nested attributes"
.to_string(),
));
}
{
let mut seen: Vec<&str> = Vec::with_capacity(pairs.len());
for pair in &pairs {
if seen.contains(&pair.attr.as_str()) {
return Err(DynoxideError::ValidationException(
"Invalid SearchConditionExpression: SearchConditionExpression must only \
contain one condition per attribute"
.to_string(),
));
}
seen.push(&pair.attr);
}
}
if let Some(ref cond) = condition {
tracker.track_condition_expr(cond);
}
if let Some(ref proj) = projection {
tracker.track_projection_expr(proj);
expressions::projection::validate(proj, &tracker)
.map_err(DynoxideError::ValidationException)?;
}
tracker.check_unused()?;
let meta = helpers::require_table_for_item_op(storage, &request.table_name).await?;
let vixs = parse_vector_defs(&meta)?;
let Some(vix) = vixs.iter().find(|v| v.index_name == request.index_name) else {
return Err(DynoxideError::ValidationException(format!(
"The table does not have the specified index: {}",
request.index_name
)));
};
if !lifecycle
.phase_of_index(storage, &request.table_name, &request.index_name)
.is_searchable()
{
return Err(DynoxideError::ValidationException(format!(
"Cannot search backfilling vector index: {}",
request.index_name
)));
}
if query_vec.len() != vix.dimensions as usize {
return Err(DynoxideError::ValidationException(format!(
"Input search vector dimension {} does not match vector index dimension {}",
query_vec.len(),
vix.dimensions
)));
}
for attr in &referenced_attrs {
let in_schema = vix
.search_schema
.as_ref()
.is_some_and(|schema| schema.iter().any(|e| e.attribute_name == *attr));
if !in_schema {
return Err(DynoxideError::ValidationException(format!(
"SearchConditionExpression must not contain any attributes that is not in \
SearchSchema. Invalid attribute: {attr}"
)));
}
}
let index_hash_attr = hash_attr(vix);
if let Some(hash) = index_hash_attr {
let provided = pairs.iter().any(|p| p.attr == hash && !p.nested);
if !provided {
return Err(DynoxideError::ValidationException(
"SearchConditionExpression must be provided when SearchSchema has a HASH key"
.to_string(),
));
}
}
let mut hash_scope: Option<String> = None;
let mut unscopable_hash = false;
if let Some(hash) = index_hash_attr {
let attr_defs = parse_attr_defs(&meta)?;
for pair in pairs.iter().filter(|p| p.attr == hash) {
if let Some(def) = attr_defs.iter().find(|d| d.attribute_name == hash) {
let declared = scalar_type_str(&def.attribute_type);
let actual = pair.value.type_name();
if actual != declared {
return Err(DynoxideError::ValidationException(format!(
"Type of '{hash}' attribute in SearchConditionExpression ({actual}) \
does not match type in search schema ({declared})"
)));
}
}
if matches!(&pair.value, AttributeValue::S(s) if s.is_empty()) {
return Err(DynoxideError::ValidationException(format!(
"One or more parameter values are not valid. The AttributeValue for a key \
attribute cannot contain an empty string value. Key: {hash}"
)));
}
match pair.value.to_key_string() {
Some(key) => hash_scope = Some(key),
None => unscopable_hash = true,
}
}
}
let candidates: Vec<VectorCandidateRow> = if unscopable_hash {
Vec::new()
} else {
storage
.query_vector_candidates(
&request.table_name,
&request.index_name,
hash_scope.as_deref(),
)
.await?
};
let filter_pairs: Vec<&EqualityPair> = pairs
.iter()
.filter(|p| Some(p.attr.as_str()) != index_hash_attr)
.collect();
let mut scored: Vec<(f64, &VectorCandidateRow)> = Vec::with_capacity(candidates.len());
for row in &candidates {
let filter_map: HashMap<String, AttributeValue> = if filter_pairs.is_empty() {
HashMap::new()
} else {
serde_json::from_str(&row.filter_json).map_err(|e| {
DynoxideError::InternalServerError(format!("Bad filter JSON in storage: {e}"))
})?
};
let passes = filter_pairs.iter().all(|pair| {
filter_map
.get(&pair.attr)
.is_some_and(|stored| expressions::condition::values_equal(stored, &pair.value))
});
if !passes {
continue;
}
let stored_vec: Vec<f32> = serde_json::from_str(&row.vector_json).map_err(|e| {
DynoxideError::InternalServerError(format!("Bad vector JSON in storage: {e}"))
})?;
scored.push((
score_candidate(&vix.distance_function, &query_vec, &stored_vec),
row,
));
}
if higher_is_better(&vix.distance_function) {
scored.sort_by(|a, b| b.0.total_cmp(&a.0));
} else {
scored.sort_by(|a, b| a.0.total_cmp(&b.0));
}
scored.truncate(request.top_k as usize);
let loop_tracker = expressions::TrackedExpressionAttributes::without_tracking(
&request.expression_attribute_names,
&request.expression_attribute_values,
);
let no_keys: &[String] = &[];
let survivor_keys: Vec<(String, String)> = scored
.iter()
.map(|(_, row)| (row.table_pk.clone(), row.table_sk.clone()))
.collect();
let items: HashMap<(String, String), String> = storage
.vector_items_for_keys(&request.table_name, &request.index_name, &survivor_keys)
.await?
.into_iter()
.map(|(pk, sk, item_json)| ((pk, sk), item_json))
.collect();
let mut search_results = Vec::with_capacity(scored.len());
for (score, row) in scored {
let key = (row.table_pk.clone(), row.table_sk.clone());
let Some(item_json) = items.get(&key) else {
continue;
};
let item: Item = serde_json::from_str(item_json).map_err(|e| {
DynoxideError::InternalServerError(format!("Bad item JSON in storage: {e}"))
})?;
let result_item = if let Some(ref proj) = projection {
expressions::projection::apply(&item, proj, &loop_tracker, no_keys)
.map_err(DynoxideError::ValidationException)?
} else {
let mut item = item;
item.remove(&vix.vector_attribute.attribute_name);
item
};
search_results.push(SearchResult {
item: result_item,
score,
});
}
let consumed_capacity =
crate::types::capacity_wanted(request.return_consumed_capacity.as_deref()).then(|| {
crate::types::VectorSearchCapacity {
vector_search_request_bytes: search_scanned_bytes(&candidates),
}
});
Ok(SearchVectorsResponse {
search_results,
consumed_capacity,
})
}