Skip to main content

cratestack_sql/filter/
vector.rs

1use crate::order::{OrderClause, OrderTarget};
2use crate::values::{FilterValue, IntoSqlValue};
3use crate::{NullOrder, SortDirection};
4
5use super::expr::FilterExpr;
6use super::op::FilterOp;
7
8/// Distance metric for a `Vector(n)` similarity search (see
9/// `docs/design/extensions.md` §6/§7, cratestack#163). Maps 1:1 onto
10/// pgvector's three distance operators and the `opclass` names used by
11/// `@@index([...], opclass: "...")` (cratestack#156's DDL) — but is
12/// never *inferred* from an index: an index is only ever an optional
13/// access-path speedup, and AC #2 on cratestack#163 requires distance
14/// ordering/filtering to keep working with no vector index present at
15/// all (a plain sequential scan), so callers state the metric
16/// explicitly at the call site. [`VectorMetric::from_opclass`] is a
17/// convenience for callers that already know their index's opclass and
18/// don't want to duplicate the mapping by hand — it is never called
19/// automatically.
20#[derive(Debug, Clone, Copy, PartialEq, Eq)]
21pub enum VectorMetric {
22    /// Euclidean (L2) distance — operator `<->`, opclass `vector_l2_ops`.
23    L2,
24    /// Cosine distance — operator `<=>`, opclass `vector_cosine_ops`.
25    Cosine,
26    /// Negative inner product — operator `<#>`, opclass `vector_ip_ops`.
27    InnerProduct,
28}
29
30impl VectorMetric {
31    /// The Postgres operator pgvector registers for this metric.
32    pub const fn sql_operator(self) -> &'static str {
33        match self {
34            Self::L2 => "<->",
35            Self::Cosine => "<=>",
36            Self::InnerProduct => "<#>",
37        }
38    }
39
40    /// Map a pgvector `opclass` string, as used in `@@index([...],
41    /// opclass: "vector_l2_ops")` (cratestack#156), to the metric it
42    /// indexes. Returns `None` for anything that isn't one of
43    /// pgvector's three recognized vector opclasses (including a
44    /// non-vector opclass on an unrelated index).
45    pub fn from_opclass(opclass: &str) -> Option<Self> {
46        match opclass {
47            "vector_l2_ops" => Some(Self::L2),
48            "vector_cosine_ops" => Some(Self::Cosine),
49            "vector_ip_ops" => Some(Self::InnerProduct),
50            _ => None,
51        }
52    }
53}
54
55/// `<column> <metric op> <query_vector> <cmp> <value>` — a distance-to-
56/// a-query-vector expression compared against a bound threshold. Built
57/// via [`super::field_ref_ext`]'s `FieldRef::distance_to`, then a
58/// comparator method turns it into a [`FilterExpr`]. Mirrors
59/// [`super::CoalesceFilter`]'s shape: a left-hand computed expression
60/// plus a bound right-hand value.
61///
62/// PG-only (pgvector) — the embedded rusqlite backend doesn't ship
63/// pgvector, so its renderer fails loud, mirroring `FilterExpr::Spatial`.
64#[derive(Debug, Clone, PartialEq)]
65pub struct VectorDistanceFilter {
66    pub column: &'static str,
67    pub metric: VectorMetric,
68    pub query_vector: Vec<f32>,
69    pub op: FilterOp,
70    pub value: FilterValue,
71}
72
73/// Builder returned by `FieldRef::distance_to` — chain a comparator
74/// (`.lt`/`.lte`/`.gt`/`.gte`/`.eq`) for a threshold filter, or `.asc`/
75/// `.desc` to use it as an `ORDER BY` target. The common k-NN "closest
76/// first" case is `.asc()`; see also `FieldRef::order_by_distance`,
77/// sugar for exactly that.
78#[derive(Debug, Clone)]
79pub struct VectorDistanceExpr {
80    column: &'static str,
81    metric: VectorMetric,
82    query_vector: Vec<f32>,
83}
84
85impl VectorDistanceExpr {
86    pub(super) fn new(column: &'static str, metric: VectorMetric, query_vector: Vec<f32>) -> Self {
87        Self {
88            column,
89            metric,
90            query_vector,
91        }
92    }
93
94    fn into_filter<V: IntoSqlValue>(self, op: FilterOp, value: V) -> FilterExpr {
95        FilterExpr::VectorDistance(VectorDistanceFilter {
96            column: self.column,
97            metric: self.metric,
98            query_vector: self.query_vector,
99            op,
100            value: FilterValue::Single(value.into_sql_value()),
101        })
102    }
103
104    pub fn lt<V: IntoSqlValue>(self, value: V) -> FilterExpr {
105        self.into_filter(FilterOp::Lt, value)
106    }
107
108    pub fn lte<V: IntoSqlValue>(self, value: V) -> FilterExpr {
109        self.into_filter(FilterOp::Lte, value)
110    }
111
112    pub fn gt<V: IntoSqlValue>(self, value: V) -> FilterExpr {
113        self.into_filter(FilterOp::Gt, value)
114    }
115
116    pub fn gte<V: IntoSqlValue>(self, value: V) -> FilterExpr {
117        self.into_filter(FilterOp::Gte, value)
118    }
119
120    pub fn eq<V: IntoSqlValue>(self, value: V) -> FilterExpr {
121        self.into_filter(FilterOp::Eq, value)
122    }
123
124    /// Order by distance, nearest first (`ASC`) — the standard k-NN
125    /// shape. Equivalent to `FieldRef::order_by_distance`.
126    pub fn asc(self) -> OrderClause {
127        self.order(SortDirection::Asc)
128    }
129
130    /// Order by distance, farthest first (`DESC`) — e.g. diverse /
131    /// furthest-point sampling.
132    pub fn desc(self) -> OrderClause {
133        self.order(SortDirection::Desc)
134    }
135
136    fn order(self, direction: SortDirection) -> OrderClause {
137        OrderClause {
138            target: OrderTarget::VectorDistance {
139                column: self.column,
140                metric: self.metric,
141                query_vector: self.query_vector,
142            },
143            direction,
144            null_order: NullOrder::Last,
145        }
146    }
147}
148
149#[cfg(test)]
150mod tests;