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;