Skip to content

Commit 019f651

Browse files
Avital-Finedvora-h
andauthored
Support for Vector Fields for Vector Similarity Search (#2041)
* Support Vector field in FT.CREATE command * linters * fix data error * change to dic * add type hints and docstring to constructor * test not supported algorithm * linters * fix errors * example * delete example Co-authored-by: dvora-h <[email protected]>
1 parent 827dcde commit 019f651

File tree

2 files changed

+111
-6
lines changed

2 files changed

+111
-6
lines changed

redis/commands/search/field.py

Lines changed: 70 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,30 @@
1+
from typing import List
2+
3+
from redis import DataError
4+
5+
16
class Field:
27

38
NUMERIC = "NUMERIC"
49
TEXT = "TEXT"
510
WEIGHT = "WEIGHT"
611
GEO = "GEO"
712
TAG = "TAG"
13+
VECTOR = "VECTOR"
814
SORTABLE = "SORTABLE"
915
NOINDEX = "NOINDEX"
1016
AS = "AS"
1117

12-
def __init__(self, name, args=[], sortable=False, no_index=False, as_name=None):
18+
def __init__(
19+
self,
20+
name: str,
21+
args: List[str] = None,
22+
sortable: bool = False,
23+
no_index: bool = False,
24+
as_name: str = None,
25+
):
26+
if args is None:
27+
args = []
1328
self.name = name
1429
self.args = args
1530
self.args_suffix = list()
@@ -44,7 +59,12 @@ class TextField(Field):
4459
PHONETIC = "PHONETIC"
4560

4661
def __init__(
47-
self, name, weight=1.0, no_stem=False, phonetic_matcher=None, **kwargs
62+
self,
63+
name: str,
64+
weight: float = 1.0,
65+
no_stem: bool = False,
66+
phonetic_matcher: str = None,
67+
**kwargs,
4868
):
4969
Field.__init__(self, name, args=[Field.TEXT, Field.WEIGHT, weight], **kwargs)
5070

@@ -65,7 +85,7 @@ class NumericField(Field):
6585
NumericField is used to define a numeric field in a schema definition
6686
"""
6787

68-
def __init__(self, name, **kwargs):
88+
def __init__(self, name: str, **kwargs):
6989
Field.__init__(self, name, args=[Field.NUMERIC], **kwargs)
7090

7191

@@ -74,7 +94,7 @@ class GeoField(Field):
7494
GeoField is used to define a geo-indexing field in a schema definition
7595
"""
7696

77-
def __init__(self, name, **kwargs):
97+
def __init__(self, name: str, **kwargs):
7898
Field.__init__(self, name, args=[Field.GEO], **kwargs)
7999

80100

@@ -86,7 +106,52 @@ class TagField(Field):
86106

87107
SEPARATOR = "SEPARATOR"
88108

89-
def __init__(self, name, separator=",", **kwargs):
109+
def __init__(self, name: str, separator: str = ",", **kwargs):
90110
Field.__init__(
91111
self, name, args=[Field.TAG, self.SEPARATOR, separator], **kwargs
92112
)
113+
114+
115+
class VectorField(Field):
116+
"""
117+
Allows vector similarity queries against the value in this attribute.
118+
See https://oss.redis.com/redisearch/Vectors/#vector_fields.
119+
"""
120+
121+
def __init__(self, name: str, algorithm: str, attributes: dict, **kwargs):
122+
"""
123+
Create Vector Field. Notice that Vector cannot have sortable or no_index tag,
124+
although it's also a Field.
125+
126+
``name`` is the name of the field.
127+
128+
``algorithm`` can be "FLAT" or "HNSW".
129+
130+
``attributes`` each algorithm can have specific attributes. Some of them
131+
are mandatory and some of them are optional. See
132+
https://oss.redis.com/redisearch/master/Vectors/#specific_creation_attributes_per_algorithm
133+
for more information.
134+
"""
135+
sort = kwargs.get("sortable", False)
136+
noindex = kwargs.get("no_index", False)
137+
138+
if sort or noindex:
139+
raise DataError("Cannot set 'sortable' or 'no_index' in Vector fields.")
140+
141+
if algorithm.upper() not in ["FLAT", "HNSW"]:
142+
raise DataError(
143+
"Realtime vector indexing supporting 2 Indexing Methods:"
144+
"'FLAT' and 'HNSW'."
145+
)
146+
147+
attr_li = []
148+
149+
for key, value in attributes.items():
150+
attr_li.extend([key, value])
151+
152+
Field.__init__(
153+
self,
154+
name,
155+
args=[Field.VECTOR, algorithm, len(attr_li), *attr_li],
156+
**kwargs,
157+
)

tests/test_search.py

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,13 @@
1212
import redis.commands.search.reducers as reducers
1313
from redis.commands.json.path import Path
1414
from redis.commands.search import Search
15-
from redis.commands.search.field import GeoField, NumericField, TagField, TextField
15+
from redis.commands.search.field import (
16+
GeoField,
17+
NumericField,
18+
TagField,
19+
TextField,
20+
VectorField,
21+
)
1622
from redis.commands.search.indexDefinition import IndexDefinition, IndexType
1723
from redis.commands.search.query import GeoFilter, NumericFilter, Query
1824
from redis.commands.search.result import Result
@@ -1522,6 +1528,40 @@ def test_profile_limited(client):
15221528
assert len(res.docs) == 3 # check also the search result
15231529

15241530

1531+
@pytest.mark.redismod
1532+
def test_vector_field(modclient):
1533+
modclient.flushdb()
1534+
modclient.ft().create_index(
1535+
(
1536+
VectorField(
1537+
"v", "HNSW", {"TYPE": "FLOAT32", "DIM": 2, "DISTANCE_METRIC": "L2"}
1538+
),
1539+
)
1540+
)
1541+
modclient.hset("a", "v", "aaaaaaaa")
1542+
modclient.hset("b", "v", "aaaabaaa")
1543+
modclient.hset("c", "v", "aaaaabaa")
1544+
1545+
q = Query("*=>[KNN 2 @v $vec]").return_field("__v_score").sort_by("__v_score", True)
1546+
res = modclient.ft().search(q, query_params={"vec": "aaaaaaaa"})
1547+
1548+
assert "a" == res.docs[0].id
1549+
assert "0" == res.docs[0].__getattribute__("__v_score")
1550+
1551+
1552+
@pytest.mark.redismod
1553+
def test_vector_field_error(modclient):
1554+
modclient.flushdb()
1555+
1556+
# sortable tag
1557+
with pytest.raises(Exception):
1558+
modclient.ft().create_index((VectorField("v", "HNSW", {}, sortable=True),))
1559+
1560+
# not supported algorithm
1561+
with pytest.raises(Exception):
1562+
modclient.ft().create_index((VectorField("v", "SORT", {}),))
1563+
1564+
15251565
@pytest.mark.redismod
15261566
def test_text_params(modclient):
15271567
modclient.flushdb()

0 commit comments

Comments
 (0)