Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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
37 changes: 32 additions & 5 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,35 @@ 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 returns, for each element, the JSON attribute associated
with the element or None when no attributes are present.

``with_attribs`` sets if the results should be returned with the
attributes of the elements in the result.

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

Expand Down Expand Up @@ -177,6 +200,10 @@ def vsim(
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
29 changes: 26 additions & 3 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,40 @@ 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))

if withscores ^ 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 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 = {}
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
101 changes: 100 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("7.9.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("7.9.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("7.9.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 @@ -789,9 +863,12 @@ async def test_vrandmember(d_client):
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 Expand Up @@ -820,6 +897,28 @@ async def test_vset_commands_without_decoding_responces(client):
assert isinstance(vsim_with_scores, dict)
assert isinstance(vsim_with_scores[b"elem1"], float)

# 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

# test vlinks - no scores
element_links_all_layers = await client.vset().vlinks("myset", "elem1")
assert len(element_links_all_layers) >= 1
Expand Down
Loading
Loading