Skip to content

Commit be80eb4

Browse files
committed
feat(types): add L1 distance operator and corresponding tests for Vector type
1 parent f687570 commit be80eb4

2 files changed

Lines changed: 48 additions & 5 deletions

File tree

advanced_alchemy/types/vector.py

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
from sqlalchemy.engine import Dialect
2222
from sqlalchemy.ext.compiler import compiles
2323
from sqlalchemy.sql.expression import ColumnElement
24+
from sqlalchemy.sql.visitors import InternalTraversal
2425
from sqlalchemy.types import JSON, TypeDecorator, TypeEngine
2526

2627
if TYPE_CHECKING:
@@ -30,8 +31,8 @@
3031

3132
__all__ = ("Vector",)
3233

33-
_PGVECTOR_OPERATORS = {"cosine": "<=>", "l2": "<->", "inner_product": "<#>"}
34-
_ORACLE_METRICS = {"cosine": "COSINE", "l2": "EUCLIDEAN", "inner_product": "DOT"}
34+
_PGVECTOR_OPERATORS = {"cosine": "<=>", "l2": "<->", "l1": "<+>", "inner_product": "<#>"}
35+
_ORACLE_METRICS = {"cosine": "COSINE", "l2": "EUCLIDEAN", "l1": "MANHATTAN", "inner_product": "DOT"}
3536

3637

3738
class Vector(TypeDecorator[list[float]]):
@@ -94,6 +95,9 @@ def cosine_distance(self, other: "Sequence[float]") -> "_VectorDistance":
9495
def l2_distance(self, other: "Sequence[float]") -> "_VectorDistance":
9596
return self._distance(other, "l2")
9697

98+
def l1_distance(self, other: "Sequence[float]") -> "_VectorDistance":
99+
return self._distance(other, "l1")
100+
97101
def max_inner_product(self, other: "Sequence[float]") -> "_VectorDistance":
98102
return self._distance(other, "inner_product")
99103

@@ -106,12 +110,23 @@ class _VectorDistance(ColumnElement[float]):
106110
The SQL is chosen at compile time so the same expression compiles to native
107111
operators on each backend:
108112
109-
- PostgreSQL / CockroachDB → ``<=>`` / ``<->`` / ``<#>`` (pgvector)
110-
- Oracle 23ai → ``VECTOR_DISTANCE(col, :vec, COSINE | EUCLIDEAN | DOT)``
113+
- PostgreSQL / CockroachDB → ``<=>`` / ``<->`` / ``<+>`` / ``<#>`` (pgvector)
114+
- Oracle 23ai → ``VECTOR_DISTANCE(col, :vec, COSINE | EUCLIDEAN | MANHATTAN | DOT)``
111115
- other dialects → :exc:`NotImplementedError` (no native vector backend)
116+
117+
``max_inner_product`` (pgvector ``<#>``, Oracle ``DOT``) returns the *negated*
118+
inner product on both backends, so ascending order yields the nearest match.
119+
120+
``_traverse_internals`` makes the expression cache-safe: statements containing
121+
a vector distance participate in SQLAlchemy's compiled-statement cache keyed on
122+
the operands and metric.
112123
"""
113124

114-
inherit_cache = False
125+
_traverse_internals = [
126+
("left", InternalTraversal.dp_clauseelement),
127+
("right", InternalTraversal.dp_clauseelement),
128+
("metric", InternalTraversal.dp_string),
129+
]
115130

116131
def __init__(self, left: "ColumnElement[Any]", right: "ColumnElement[Any]", metric: str) -> None:
117132
self.left = left

tests/unit/test_types/test_vector.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,7 @@ def test_vector_distance_methods_exist_on_column() -> None:
164164
column: Column[list[float]] = Column("embedding", Vector(3))
165165
assert hasattr(column, "cosine_distance")
166166
assert hasattr(column, "l2_distance")
167+
assert hasattr(column, "l1_distance")
167168
assert hasattr(column, "max_inner_product")
168169

169170

@@ -183,6 +184,14 @@ def test_vector_l2_distance_postgresql_emits_operator() -> None:
183184
assert "<->" in sql
184185

185186

187+
def test_vector_l1_distance_postgresql_emits_operator() -> None:
188+
"""L1 distance compiles to the pgvector ``<+>`` operator on PostgreSQL."""
189+
column: Column[list[float]] = Column("embedding", Vector(3))
190+
statement = column.l1_distance([1.0, 2.0, 3.0])
191+
sql = str(statement.compile(dialect=postgresql_dialect_mod.dialect())) # type: ignore[no-untyped-call,unused-ignore]
192+
assert "<+>" in sql
193+
194+
186195
def test_vector_max_inner_product_postgresql_emits_operator() -> None:
187196
"""Negative inner product compiles to the pgvector ``<#>`` operator on PostgreSQL."""
188197
column: Column[list[float]] = Column("embedding", Vector(3))
@@ -209,6 +218,15 @@ def test_vector_l2_distance_oracle_uses_euclidean_metric() -> None:
209218
assert "EUCLIDEAN" in sql
210219

211220

221+
def test_vector_l1_distance_oracle_uses_manhattan_metric() -> None:
222+
"""L1 distance maps to the Oracle ``MANHATTAN`` metric."""
223+
column: Column[list[float]] = Column("embedding", Vector(3))
224+
statement = column.l1_distance([1.0, 2.0, 3.0])
225+
sql = str(statement.compile(dialect=oracle_dialect_mod.dialect())) # type: ignore[no-untyped-call,unused-ignore]
226+
assert "VECTOR_DISTANCE" in sql
227+
assert "MANHATTAN" in sql
228+
229+
212230
def test_vector_max_inner_product_oracle_uses_dot_metric() -> None:
213231
"""Negative inner product maps to the Oracle ``DOT`` metric."""
214232
column: Column[list[float]] = Column("embedding", Vector(3))
@@ -218,6 +236,16 @@ def test_vector_max_inner_product_oracle_uses_dot_metric() -> None:
218236
assert "DOT" in sql
219237

220238

239+
def test_vector_distance_is_cacheable() -> None:
240+
"""Distance expressions generate a SQLAlchemy cache key keyed on operands and metric."""
241+
column: Column[list[float]] = Column("embedding", Vector(3))
242+
cosine = column.cosine_distance([1.0, 2.0, 3.0])
243+
l2 = column.l2_distance([1.0, 2.0, 3.0])
244+
assert cosine._generate_cache_key() is not None # pyright: ignore[reportPrivateUsage]
245+
assert cosine._generate_cache_key() == column.cosine_distance([1.0, 2.0, 3.0])._generate_cache_key() # pyright: ignore[reportPrivateUsage]
246+
assert cosine._generate_cache_key() != l2._generate_cache_key() # pyright: ignore[reportPrivateUsage]
247+
248+
221249
def test_vector_distance_unsupported_dialect_raises() -> None:
222250
"""Distance operations require a native vector backend; JSON fallback raises clearly."""
223251
column: Column[list[float]] = Column("embedding", Vector(3))

0 commit comments

Comments
 (0)