Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions .github/workflows/integration.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,8 @@ env:
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
# this speeds up coverage with Python 3.12: https://github.com/nedbat/coveragepy/issues/1665
COVERAGE_CORE: sysmon
CURRENT_CLIENT_LIBS_TEST_STACK_IMAGE_TAG: '8.0.2'
CURRENT_REDIS_VERSION: '8.0.2'
CURRENT_CLIENT_LIBS_TEST_STACK_IMAGE_TAG: '8.2'
CURRENT_REDIS_VERSION: '8.2'

jobs:
dependency-audit:
Expand Down Expand Up @@ -74,7 +74,7 @@ jobs:
max-parallel: 15
fail-fast: false
matrix:
redis-version: ['8.2', '${{ needs.redis_version.outputs.CURRENT }}', '7.4.4', '7.2.9']
redis-version: ['8.2.1-pre', '${{ needs.redis_version.outputs.CURRENT }}', '8.0.2' ,'7.4.4', '7.2.9']
python-version: ['3.9', '3.13']
parser-backend: ['plain']
event-loop: ['asyncio']
Expand Down
4 changes: 2 additions & 2 deletions docker-compose.yml
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
---
# image tag 8.0-RC2-pre is the one matching the 8.0 GA release
x-client-libs-stack-image: &client-libs-stack-image
image: "redislabs/client-libs-test:${CLIENT_LIBS_TEST_STACK_IMAGE_TAG:-8.0.2}"
image: "redislabs/client-libs-test:${CLIENT_LIBS_TEST_STACK_IMAGE_TAG:-8.2}"

x-client-libs-image: &client-libs-image
image: "redislabs/client-libs-test:${CLIENT_LIBS_TEST_IMAGE_TAG:-8.0.2}"
image: "redislabs/client-libs-test:${CLIENT_LIBS_TEST_IMAGE_TAG:-8.2}"

services:

Expand Down
2 changes: 1 addition & 1 deletion redis/commands/vectorset/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,12 +24,12 @@ def __init__(self, client, **kwargs):
# Set the module commands' callbacks
self._MODULE_CALLBACKS = {
VEMB_CMD: parse_vemb_result,
VSIM_CMD: parse_vsim_result,
VGETATTR_CMD: lambda r: r and json.loads(r) or None,
}

self._RESP2_MODULE_CALLBACKS = {
VINFO_CMD: lambda r: r and pairs_to_dict(r) or None,
VSIM_CMD: parse_vsim_result,
VLINKS_CMD: parse_vlinks_result,
}
self._RESP3_MODULE_CALLBACKS = {}
Expand Down
46 changes: 38 additions & 8 deletions redis/commands/vectorset/commands.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import json
from enum import Enum
from typing import Awaitable, Dict, List, Optional, Union
from typing import Any, Awaitable, Dict, List, Optional, Union

from redis.client import NEVER_DECODE
from redis.commands.helpers import get_protocol_version
Expand Down Expand Up @@ -33,6 +33,7 @@ class CallbacksOptions(Enum):

RAW = "RAW"
WITHSCORES = "WITHSCORES"
WITHATTRIBS = "WITHATTRIBS"
ALLOW_DECODING = "ALLOW_DECODING"
RESP3 = "RESP3"

Expand Down Expand Up @@ -123,6 +124,7 @@ def vsim(
key: KeyT,
input: Union[List[float], bytes, str],
with_scores: Optional[bool] = False,
with_attribs: Optional[bool] = False,
count: Optional[int] = None,
ef: Optional[Number] = None,
filter: Optional[str] = None,
Expand All @@ -131,14 +133,34 @@ def vsim(
no_thread: Optional[bool] = False,
epsilon: Optional[Number] = None,
) -> Union[
Awaitable[Optional[List[Union[List[EncodableT], Dict[EncodableT, Number]]]]],
Optional[List[Union[List[EncodableT], Dict[EncodableT, Number]]]],
Awaitable[
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This typing looks evil 😆

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, absolutely unreadable... I was thinking to extract a separate type alias for this one and I forgot.. :) Now it's fixed.

Optional[
List[
Union[
List[EncodableT],
Dict[EncodableT, Number],
Dict[EncodableT, Dict[str, Any]],
]
]
]
],
Optional[
List[
Union[
List[EncodableT],
Dict[EncodableT, Number],
Dict[EncodableT, Dict[str, Any]],
]
]
],
]:
"""
Compare a vector or element ``input`` with the other vectors in a vector set ``key``.

``with_scores`` sets if the results should be returned with the
similarity scores of the elements in the result.
``with_scores`` sets if similarity scores should be returned for each element in the result.

``with_attribs`` ``with_attribs`` sets if the results should be returned with the
attributes of the elements in the result, or None when no attributes are present.

``count`` sets the number of results to return.

Expand Down Expand Up @@ -173,9 +195,17 @@ def vsim(
else:
pieces.extend(["ELE", input])

if with_scores:
pieces.append("WITHSCORES")
options[CallbacksOptions.WITHSCORES.value] = True
if with_scores or with_attribs:
if get_protocol_version(self.client) in ["3", 3]:
options[CallbacksOptions.RESP3.value] = True

if with_scores:
pieces.append("WITHSCORES")
options[CallbacksOptions.WITHSCORES.value] = True

if with_attribs:
pieces.append("WITHATTRIBS")
options[CallbacksOptions.WITHATTRIBS.value] = True

if count:
pieces.extend(["COUNT", count])
Expand Down
44 changes: 40 additions & 4 deletions redis/commands/vectorset/utils.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import json

from redis._parsers.helpers import pairs_to_dict
from redis.commands.vectorset.commands import CallbacksOptions

Expand Down Expand Up @@ -75,19 +77,53 @@ def parse_vsim_result(response, **options):
structures depending on input options.
Parsing VSIM result into:
- List[List[str]]
- List[Dict[str, Number]]
- List[Dict[str, Number]] - when with_scores is used (without attributes)
- List[Dict[str, Mapping[str, Any]]] - when with_attribs is used (without scores)
- List[Dict[str, Union[Number, Mapping[str, Any]]]] - when with_scores and with_attribs are used

"""
if response is None:
return response

if options.get(CallbacksOptions.WITHSCORES.value):
withscores = bool(options.get(CallbacksOptions.WITHSCORES.value))
withattribs = bool(options.get(CallbacksOptions.WITHATTRIBS.value))

# Exactly one of withscores or withattribs is True
if (withscores and not withattribs) or (not withscores and withattribs):
# Redis will return a list of list of pairs.
# This list have to be transformed to dict
result_dict = {}
for key, value in pairs_to_dict(response).items():
value = float(value)
if options.get(CallbacksOptions.RESP3.value):
resp_dict = response
else:
resp_dict = pairs_to_dict(response)
for key, value in resp_dict.items():
if withscores:
value = float(value)
else:
value = json.loads(value) if value else None

result_dict[key] = value
return result_dict
elif withscores and withattribs:
it = iter(response)
result_dict = {}
if options.get(CallbacksOptions.RESP3.value):
for elem, data in response.items():
if data[1] is not None:
attribs_dict = json.loads(data[1])
else:
attribs_dict = None
result_dict[elem] = {"score": data[0], "attributes": attribs_dict}
else:
for elem, score, attribs in zip(it, it, it):
if attribs is not None:
attribs_dict = json.loads(attribs)
else:
attribs_dict = None

result_dict[elem] = {"score": float(score), "attributes": attribs_dict}
return result_dict
else:
# return the list of elements for each level
# list of lists
Expand Down
114 changes: 113 additions & 1 deletion tests/test_asyncio/test_vsets.py
Original file line number Diff line number Diff line change
Expand Up @@ -262,6 +262,80 @@ async def test_vsim_with_scores(d_client):
assert 0 <= vsim["elem1"] <= 1


@skip_if_server_version_lt("8.2.0")
async def test_vsim_with_attribs_attribs_set(d_client):
elements_count = 5
vector_dim = 10
attrs_dict = {"key1": "value1", "key2": "value2"}
for i in range(elements_count):
float_array = [random.uniform(0, 5) for x in range(vector_dim)]
await d_client.vset().vadd(
"myset",
float_array,
f"elem{i}",
numlinks=64,
attributes=attrs_dict if i % 2 == 0 else None,
)

vsim = await d_client.vset().vsim("myset", input="elem1", with_attribs=True)
assert len(vsim) == 5
assert isinstance(vsim, dict)
assert vsim["elem1"] is None
assert vsim["elem2"] == attrs_dict


@skip_if_server_version_lt("8.2.0")
async def test_vsim_with_scores_and_attribs_attribs_set(d_client):
elements_count = 5
vector_dim = 10
attrs_dict = {"key1": "value1", "key2": "value2"}
for i in range(elements_count):
float_array = [random.uniform(0, 5) for x in range(vector_dim)]
await d_client.vset().vadd(
"myset",
float_array,
f"elem{i}",
numlinks=64,
attributes=attrs_dict if i % 2 == 0 else None,
)

vsim = await d_client.vset().vsim(
"myset", input="elem1", with_scores=True, with_attribs=True
)
assert len(vsim) == 5
assert isinstance(vsim, dict)
assert isinstance(vsim["elem1"], dict)
assert "score" in vsim["elem1"]
assert "attributes" in vsim["elem1"]
assert isinstance(vsim["elem1"]["score"], float)
assert vsim["elem1"]["attributes"] is None

assert isinstance(vsim["elem2"], dict)
assert "score" in vsim["elem2"]
assert "attributes" in vsim["elem2"]
assert isinstance(vsim["elem2"]["score"], float)
assert vsim["elem2"]["attributes"] == attrs_dict


@skip_if_server_version_lt("8.2.0")
async def test_vsim_with_attribs_attribs_not_set(d_client):
elements_count = 20
vector_dim = 50
for i in range(elements_count):
float_array = [random.uniform(0, 10) for x in range(vector_dim)]
await d_client.vset().vadd(
"myset",
float_array,
f"elem{i}",
numlinks=64,
)

vsim = await d_client.vset().vsim("myset", input="elem1", with_attribs=True)
assert len(vsim) == 10
assert isinstance(vsim, dict)
assert vsim["elem1"] is None


@skip_if_server_version_lt("7.9.0")
async def test_vsim_with_different_vector_input_types(d_client):
elements_count = 10
Expand Down Expand Up @@ -785,13 +859,51 @@ async def test_vrandmember(d_client):
assert members_list == []


@skip_if_server_version_lt("8.2.0")
async def test_8_2_new_vset_features_without_decoding_responces(client):
# test vadd
elements = ["elem1", "elem2", "elem3"]
attrs_dict = {"key1": "value1", "key2": "value2"}
for elem in elements:
float_array = [random.uniform(0.5, 10) for x in range(0, 8)]
resp = await client.vset().vadd(
"myset", float_array, element=elem, attributes=attrs_dict
)
assert resp == 1

# test vsim with attributes
vsim_with_attribs = await client.vset().vsim(
"myset", input="elem1", with_attribs=True
)
assert len(vsim_with_attribs) == 3
assert isinstance(vsim_with_attribs, dict)
assert isinstance(vsim_with_attribs[b"elem1"], dict)
assert vsim_with_attribs[b"elem1"] == attrs_dict

# test vsim with score and attributes
vsim_with_scores_and_attribs = await client.vset().vsim(
"myset", input="elem1", with_scores=True, with_attribs=True
)
assert len(vsim_with_scores_and_attribs) == 3
assert isinstance(vsim_with_scores_and_attribs, dict)
assert isinstance(vsim_with_scores_and_attribs[b"elem1"], dict)
assert "score" in vsim_with_scores_and_attribs[b"elem1"]
assert "attributes" in vsim_with_scores_and_attribs[b"elem1"]
assert isinstance(vsim_with_scores_and_attribs[b"elem1"]["score"], float)
assert isinstance(vsim_with_scores_and_attribs[b"elem1"]["attributes"], dict)
assert vsim_with_scores_and_attribs[b"elem1"]["attributes"] == attrs_dict


@skip_if_server_version_lt("7.9.0")
async def test_vset_commands_without_decoding_responces(client):
# test vadd
elements = ["elem1", "elem2", "elem3"]
attrs_dict = {"key1": "value1", "key2": "value2"}
for elem in elements:
float_array = [random.uniform(0.5, 10) for x in range(0, 8)]
resp = await client.vset().vadd("myset", float_array, element=elem)
resp = await client.vset().vadd(
"myset", float_array, element=elem, attributes=attrs_dict
)
assert resp == 1

# test vemb
Expand Down
Loading
Loading