Skip to content

Commit 8d38801

Browse files
authored
Increase redisvl test coverage (#109)
Upped the coverage to 81% and fix a minor bug in the utils
1 parent 16c11e2 commit 8d38801

File tree

7 files changed

+324
-14
lines changed

7 files changed

+324
-14
lines changed

conftest.py

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -95,15 +95,6 @@ def sample_data():
9595
},
9696
]
9797

98-
@pytest.fixture(scope="session")
99-
def event_loop():
100-
try:
101-
loop = asyncio.get_running_loop()
102-
except RuntimeError:
103-
loop = asyncio.new_event_loop()
104-
yield loop
105-
loop.close()
106-
10798
@pytest.fixture
10899
def clear_db(redis):
109100
redis.flushall()

redisvl/redis/utils.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,11 +21,11 @@ def convert_bytes(data: Any) -> Any:
2121
except:
2222
return data
2323
if isinstance(data, dict):
24-
return dict(map(convert_bytes, data.items()))
24+
return {convert_bytes(key): convert_bytes(value) for key, value in data.items()}
2525
if isinstance(data, list):
26-
return list(map(convert_bytes, data))
26+
return [convert_bytes(item) for item in data]
2727
if isinstance(data, tuple):
28-
return map(convert_bytes, data)
28+
return tuple(convert_bytes(item) for item in data)
2929
return data
3030

3131

tests/integration/test_llmcache.py

Lines changed: 54 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@
44

55
from redisvl.extensions.llmcache import SemanticCache
66
from redisvl.utils.vectorize import HFTextVectorizer
7-
7+
from redisvl.index.index import SearchIndex
8+
from collections import namedtuple
89

910
@pytest.fixture
1011
def vectorizer():
@@ -18,6 +19,10 @@ def cache(vectorizer):
1819
cache_instance.clear() # Clear cache after each test
1920
cache_instance._index.delete(True) # Clean up index
2021

22+
@pytest.fixture
23+
def cache_no_cleanup(vectorizer):
24+
cache_instance = SemanticCache(vectorizer=vectorizer, distance_threshold=0.2)
25+
yield cache_instance
2126

2227
@pytest.fixture
2328
def cache_with_ttl(vectorizer):
@@ -26,6 +31,12 @@ def cache_with_ttl(vectorizer):
2631
cache_instance.clear() # Clear cache after each test
2732
cache_instance._index.delete(True) # Clean up index
2833

34+
@pytest.fixture
35+
def cache_with_redis_client(vectorizer, client):
36+
cache_instance = SemanticCache(vectorizer=vectorizer, redis_client=client, distance_threshold=0.2)
37+
yield cache_instance
38+
cache_instance.clear() # Clear cache after each test
39+
cache_instance._index.delete(True) # Clean up index
2940

3041
# Test basic store and check functionality
3142
def test_store_and_check(cache, vectorizer):
@@ -83,6 +94,10 @@ def test_check_invalid_input(cache):
8394
with pytest.raises(TypeError):
8495
cache.check(prompt="test", return_fields="bad value")
8596

97+
# Test handling invalid input for check method
98+
def test_bad_ttl(cache):
99+
with pytest.raises(ValueError):
100+
cache.set_ttl(2.5)
86101

87102
# Test storing with metadata
88103
def test_store_with_metadata(cache, vectorizer):
@@ -100,6 +115,16 @@ def test_store_with_metadata(cache, vectorizer):
100115
assert check_result[0]["metadata"] == metadata
101116
assert check_result[0]["prompt"] == prompt
102117

118+
# Test storing with invalid metadata
119+
def test_store_with_invalid_metadata(cache, vectorizer):
120+
prompt = "This is another test prompt."
121+
response = "This is another test response."
122+
metadata = namedtuple('metadata', 'source')(**{'source': 'test'})
123+
124+
vector = vectorizer.embed(prompt)
125+
126+
with pytest.raises(TypeError, match=r"If specified, cached metadata must be a dictionary."):
127+
cache.store(prompt, response, vector=vector, metadata=metadata)
103128

104129
# Test setting and getting the distance threshold
105130
def test_distance_threshold(cache):
@@ -110,6 +135,11 @@ def test_distance_threshold(cache):
110135
assert cache.distance_threshold == new_threshold
111136
assert cache.distance_threshold != initial_threshold
112137

138+
# Test out of range distance threshold
139+
def test_distance_threshold_out_of_range(cache):
140+
out_of_range_threshold = -1
141+
with pytest.raises(ValueError):
142+
cache.set_threshold(out_of_range_threshold)
113143

114144
# Test storing and retrieving multiple items
115145
def test_multiple_items(cache, vectorizer):
@@ -130,3 +160,26 @@ def test_multiple_items(cache, vectorizer):
130160
print(check_result, flush=True)
131161
assert check_result[0]["response"] == expected_response
132162
assert "metadata" not in check_result[0]
163+
164+
# Test retrieving underlying SearchIndex for the cache.
165+
def test_get_index(cache):
166+
assert isinstance(cache.index, SearchIndex)
167+
168+
# Test basic functionality with cache created with user-provided Redis client
169+
def test_store_and_check_with_provided_client(cache_with_redis_client, vectorizer):
170+
prompt = "This is a test prompt."
171+
response = "This is a test response."
172+
vector = vectorizer.embed(prompt)
173+
174+
cache_with_redis_client.store(prompt, response, vector=vector)
175+
check_result = cache_with_redis_client.check(vector=vector)
176+
177+
assert len(check_result) == 1
178+
print(check_result, flush=True)
179+
assert response == check_result[0]["response"]
180+
assert "metadata" not in check_result[0]
181+
182+
# Test deleting the cache
183+
def test_delete(cache_no_cleanup, vectorizer):
184+
cache_no_cleanup.delete()
185+
assert not cache_no_cleanup.index.exists()
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
import pytest
2+
3+
from redisvl.index import SearchIndex
4+
from redisvl.query import FilterQuery
5+
from redisvl.query.filter import Tag
6+
7+
@pytest.fixture
8+
def filter_query():
9+
return FilterQuery(
10+
return_fields=None,
11+
filter_expression=Tag("credit_score") == "high",
12+
)
13+
14+
@pytest.fixture
15+
def index(sample_data):
16+
fields_spec = [
17+
{"name": "credit_score", "type": "tag"},
18+
{"name": "user", "type": "tag"},
19+
{"name": "job", "type": "text"},
20+
{"name": "age", "type": "numeric"},
21+
{
22+
"name": "user_embedding",
23+
"type": "vector",
24+
"attrs": {
25+
"dims": 3,
26+
"distance_metric": "cosine",
27+
"algorithm": "flat",
28+
"datatype": "float32",
29+
},
30+
},
31+
]
32+
33+
json_schema = {
34+
"index": {
35+
"name": "user_index_json",
36+
"prefix": "users_json",
37+
"storage_type": "json",
38+
},
39+
"fields": fields_spec,
40+
}
41+
42+
# construct a search index from the schema
43+
index = SearchIndex.from_dict(json_schema)
44+
45+
# connect to local redis instance
46+
index.connect("redis://localhost:6379")
47+
48+
# create the index (no data yet)
49+
index.create(overwrite=True)
50+
51+
# Prepare and load the data
52+
index.load(sample_data)
53+
54+
# run the test
55+
yield index
56+
57+
# clean up
58+
index.delete(drop=True)
59+
60+
def test_process_results_unpacks_json_properly(index, filter_query):
61+
results = index.query(filter_query)
62+
assert len(results) == 4

tests/unit/test_async_search_index.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
from redisvl.index import AsyncSearchIndex
44
from redisvl.redis.utils import convert_bytes
55
from redisvl.schema import IndexSchema, StorageType
6+
from redisvl.query import VectorQuery
67

78
fields = [{"name": "test", "type": "tag"}]
89

@@ -137,3 +138,36 @@ async def test_no_id_field(async_client, async_index):
137138
# catch missing / invalid id_field
138139
with pytest.raises(ValueError):
139140
await async_index.load(bad_data, id_field="key")
141+
142+
143+
@pytest.mark.asyncio
144+
async def test_check_index_exists_before_delete(async_client, async_index):
145+
async_index.set_client(async_client)
146+
await async_index.create(overwrite=True, drop=True)
147+
await async_index.delete(drop=True)
148+
with pytest.raises(ValueError):
149+
await async_index.delete()
150+
151+
@pytest.mark.asyncio
152+
async def test_check_index_exists_before_search(async_client, async_index):
153+
async_index.set_client(async_client)
154+
await async_index.create(overwrite=True, drop=True)
155+
await async_index.delete(drop=True)
156+
157+
query = VectorQuery(
158+
[0.1, 0.1, 0.5],
159+
"user_embedding",
160+
return_fields=["user", "credit_score", "age", "job", "location"],
161+
num_results=7,
162+
)
163+
with pytest.raises(ValueError):
164+
await async_index.search(query.query, query_params=query.params)
165+
166+
@pytest.mark.asyncio
167+
async def test_check_index_exists_before_info(async_client, async_index):
168+
async_index.set_client(async_client)
169+
await async_index.create(overwrite=True, drop=True)
170+
await async_index.delete(drop=True)
171+
172+
with pytest.raises(ValueError):
173+
await async_index.info()

tests/unit/test_search_index.py

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
from redisvl.index import SearchIndex
44
from redisvl.redis.utils import convert_bytes
55
from redisvl.schema import IndexSchema, StorageType
6+
from redisvl.query import VectorQuery
67

78
fields = [{"name": "test", "type": "tag"}]
89

@@ -11,11 +12,13 @@
1112
def index_schema():
1213
return IndexSchema.from_dict({"index": {"name": "my_index"}, "fields": fields})
1314

14-
1515
@pytest.fixture
1616
def index(index_schema):
1717
return SearchIndex(schema=index_schema)
1818

19+
@pytest.fixture
20+
def index_from_yaml():
21+
return SearchIndex.from_yaml("schemas/test_json_schema.yaml")
1922

2023
def test_search_index_properties(index_schema, index):
2124
assert index.schema == index_schema
@@ -28,6 +31,13 @@ def test_search_index_properties(index_schema, index):
2831
assert index.storage_type == index_schema.index.storage_type == StorageType.HASH
2932
assert index.key("foo").startswith(index.prefix)
3033

34+
def test_search_index_from_yaml(index_from_yaml):
35+
assert index_from_yaml.name == "json-test"
36+
assert index_from_yaml.client == None
37+
assert index_from_yaml.prefix == "json"
38+
assert index_from_yaml.key_separator == ":"
39+
assert index_from_yaml.storage_type == StorageType.JSON
40+
assert index_from_yaml.key("foo").startswith(index_from_yaml.prefix)
3141

3242
def test_search_index_no_prefix(index_schema):
3343
# specify an explicitly empty prefix...
@@ -118,3 +128,36 @@ def test_no_id_field(client, index):
118128
# catch missing / invalid id_field
119129
with pytest.raises(ValueError):
120130
index.load(bad_data, id_field="key")
131+
132+
def test_check_index_exists_before_delete(client, index):
133+
index.set_client(client)
134+
index.create(overwrite=True, drop=True)
135+
index.delete(drop=True)
136+
with pytest.raises(ValueError):
137+
index.delete()
138+
139+
def test_check_index_exists_before_search(client, index):
140+
index.set_client(client)
141+
index.create(overwrite=True, drop=True)
142+
index.delete(drop=True)
143+
144+
query = VectorQuery(
145+
[0.1, 0.1, 0.5],
146+
"user_embedding",
147+
return_fields=["user", "credit_score", "age", "job", "location"],
148+
num_results=7,
149+
)
150+
with pytest.raises(ValueError):
151+
index.search(query.query, query_params=query.params)
152+
153+
def test_check_index_exists_before_info(client, index):
154+
index.set_client(client)
155+
index.create(overwrite=True, drop=True)
156+
index.delete(drop=True)
157+
158+
with pytest.raises(ValueError):
159+
index.info()
160+
161+
def test_index_needs_valid_schema():
162+
with pytest.raises(ValueError, match=r"Must provide a valid IndexSchema object"):
163+
index = SearchIndex(schema="Not A Valid Schema")

0 commit comments

Comments
 (0)