From 2dcdf8ef4c83468fdd3eab947382f118f05394ef Mon Sep 17 00:00:00 2001 From: David Froger Date: Mon, 12 Jan 2026 11:09:18 +0100 Subject: [PATCH 01/12] doc: add note for stable pagination --- redisvl/index/index.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/redisvl/index/index.py b/redisvl/index/index.py index 4bf928ed..e0172496 100644 --- a/redisvl/index/index.py +++ b/redisvl/index/index.py @@ -1160,6 +1160,9 @@ def paginate(self, query: BaseQuery, page_size: int = 30) -> Generator: batch contains. Adjust this value based on performance considerations and the expected volume of search results. + Note: + For stable pagination, the query must have a `sort_by` clause. + """ if not isinstance(page_size, int): raise TypeError("page_size must be an integer") @@ -2039,6 +2042,9 @@ async def paginate(self, query: BaseQuery, page_size: int = 30) -> AsyncGenerato batch contains. Adjust this value based on performance considerations and the expected volume of search results. + Note: + For stable pagination, the query must have a `sort_by` clause. + """ if not isinstance(page_size, int): raise TypeError("page_size must be of type int") From d9d66dd06c01100f218009764dd8654c6730d0a6 Mon Sep 17 00:00:00 2001 From: David Froger Date: Mon, 12 Jan 2026 15:02:57 +0100 Subject: [PATCH 02/12] added support to cluster to .info() --- redisvl/index/index.py | 18 +++++++-- .../integration/test_redis_cluster_support.py | 40 +++++++++++++++++++ 2 files changed, 55 insertions(+), 3 deletions(-) diff --git a/redisvl/index/index.py b/redisvl/index/index.py index e0172496..7c349a8f 100644 --- a/redisvl/index/index.py +++ b/redisvl/index/index.py @@ -39,7 +39,7 @@ convert_bytes, make_dict, ) -from redisvl.types import AsyncRedisClient, SyncRedisClient +from redisvl.types import AsyncRedisClient, SyncRedisClient, SyncRedisCluster from redisvl.utils.utils import deprecated_argument, deprecated_function, sync_wrapper if TYPE_CHECKING: @@ -1200,7 +1200,13 @@ def exists(self) -> bool: def _info(name: str, redis_client: SyncRedisClient) -> Dict[str, Any]: """Run FT.INFO to fetch information about the index.""" try: - return convert_bytes(redis_client.ft(name).info()) # type: ignore + if isinstance(redis_client, SyncRedisCluster): + node = redis_client.get_random_node() + values = redis_client.execute_command("FT.INFO", name, target_nodes=node) + info = make_dict(values) + else: + info = redis_client.ft(name).info() + return convert_bytes(info) except Exception as e: raise RedisSearchError( f"Error while fetching {name} index info: {str(e)}" @@ -1428,7 +1434,13 @@ async def _validate_client( @staticmethod async def _info(name: str, redis_client: AsyncRedisClient) -> Dict[str, Any]: try: - return convert_bytes(await redis_client.ft(name).info()) + if isinstance(redis_client, AsyncRedisCluster): + node = redis_client.get_random_node() + values = await redis_client.execute_command("FT.INFO", name, target_nodes=node) + info = make_dict(values) + else: + info = await redis_client.ft(name).info() + return convert_bytes(info) except Exception as e: raise RedisSearchError( f"Error while fetching {name} index info: {str(e)}" diff --git a/tests/integration/test_redis_cluster_support.py b/tests/integration/test_redis_cluster_support.py index c7134eca..80b82420 100644 --- a/tests/integration/test_redis_cluster_support.py +++ b/tests/integration/test_redis_cluster_support.py @@ -70,6 +70,46 @@ def test_search_index_cluster_client(redis_cluster_url): index.delete(drop=True) +@pytest.mark.requires_cluster +def test_search_index_cluster_info(redis_cluster_url): + """Test .info() method on SearchIndex with RedisCluster client.""" + schema = IndexSchema.from_dict( + { + "index": {"name": "test_cluster_info", "prefix": "test_info"}, + "fields": [{"name": "name", "type": "text"}], + } + ) + client = RedisCluster.from_url(redis_cluster_url) + index = SearchIndex(schema=schema, redis_client=client) + try: + index.create(overwrite=True) + info = index.info() + assert isinstance(info, dict) + assert info.get("index_name", None) == "test_cluster_info" + finally: + index.delete(drop=True) + +@pytest.mark.requires_cluster +@pytest.mark.asyncio +async def test_async_search_index_cluster_info(redis_cluster_url): + """Test .info() method on AsyncSearchIndex with AsyncRedisCluster client.""" + schema = IndexSchema.from_dict( + { + "index": {"name": "async_cluster_info", "prefix": "async_info"}, + "fields": [{"name": "name", "type": "text"}], + } + ) + client = AsyncRedisCluster.from_url(redis_cluster_url) + index = AsyncSearchIndex(schema=schema, redis_client=client) + try: + await index.create(overwrite=True) + info = await index.info() + assert isinstance(info, dict) + assert info.get("index_name", None) == "async_cluster_info" + finally: + await index.delete(drop=True) + await client.aclose() + @pytest.mark.requires_cluster @pytest.mark.asyncio async def test_async_search_index_client(redis_cluster_url): From df5e09a8a0a31a6bc1c84d0279582b48b88905ea Mon Sep 17 00:00:00 2001 From: David Froger Date: Mon, 12 Jan 2026 16:02:02 +0100 Subject: [PATCH 03/12] extract ._delete_batch() from .clear() --- redisvl/index/index.py | 86 +++++++++++++++++++++++++++--------------- 1 file changed, 56 insertions(+), 30 deletions(-) diff --git a/redisvl/index/index.py b/redisvl/index/index.py index 7c349a8f..b26fa17d 100644 --- a/redisvl/index/index.py +++ b/redisvl/index/index.py @@ -706,6 +706,34 @@ def delete(self, drop: bool = True): except Exception as e: raise RedisSearchError(f"Error while deleting index: {str(e)}") from e + def _delete_batch(self, batch_keys: List[str]) -> int: + """Delete a batch of keys from Redis. + + For Redis Cluster, keys are deleted individually due to potential + cross-slot limitations. For standalone Redis, keys are deleted in + a single operation for better performance. + + Args: + batch_keys (List[str]): List of Redis keys to delete. + + Returns: + int: Count of records deleted from Redis. + """ + client = cast(SyncRedisClient, self._redis_client) + is_cluster = isinstance(client, RedisCluster) + if is_cluster: + records_deleted_in_batch = 0 + for key_to_delete in batch_keys: + try: + records_deleted_in_batch += cast( + int, client.delete(key_to_delete) + ) + except redis.exceptions.RedisError as e: + logger.warning(f"Failed to delete key {key_to_delete}: {e}") + else: + records_deleted_in_batch = cast(int, client.delete(*batch_keys)) + return records_deleted_in_batch + def clear(self) -> int: """Clear all keys in Redis associated with the index, leaving the index available and in-place for future insertions or updates. @@ -717,7 +745,6 @@ def clear(self) -> int: Returns: int: Count of records deleted from Redis. """ - client = cast(SyncRedisClient, self._redis_client) total_records_deleted: int = 0 for batch in self.paginate( @@ -725,20 +752,7 @@ def clear(self) -> int: ): batch_keys = [record["id"] for record in batch] if batch_keys: - is_cluster = isinstance(client, RedisCluster) - if is_cluster: - records_deleted_in_batch = 0 - for key_to_delete in batch_keys: - try: - records_deleted_in_batch += cast( - int, client.delete(key_to_delete) - ) - except redis.exceptions.RedisError as e: - logger.warning(f"Failed to delete key {key_to_delete}: {e}") - total_records_deleted += records_deleted_in_batch - else: - record_deleted = cast(int, client.delete(*batch_keys)) - total_records_deleted += record_deleted + total_records_deleted += self._delete_batch(batch_keys) return total_records_deleted @@ -1564,6 +1578,32 @@ async def delete(self, drop: bool = True): except Exception as e: raise RedisSearchError(f"Error while deleting index: {str(e)}") from e + async def _delete_batch(self, batch_keys: List[str]) -> int: + """Delete a batch of keys from Redis. + + For Redis Cluster, keys are deleted individually due to potential + cross-slot limitations. For standalone Redis, keys are deleted in + a single operation for better performance. + + Args: + batch_keys (List[str]): List of Redis keys to delete. + + Returns: + int: Count of records deleted from Redis. + """ + client = await self._get_client() + is_cluster = isinstance(client, AsyncRedisCluster) + if is_cluster: + records_deleted_in_batch = 0 + for key_to_delete in batch_keys: + try: + records_deleted_in_batch += cast(int, await client.delete(key_to_delete)) + except redis.exceptions.RedisError as e: + logger.warning(f"Failed to delete key {key_to_delete}: {e}") + else: + records_deleted_in_batch = await client.delete(*batch_keys) + return records_deleted_in_batch + async def clear(self) -> int: """Clear all keys in Redis associated with the index, leaving the index available and in-place for future insertions or updates. @@ -1575,7 +1615,6 @@ async def clear(self) -> int: Returns: int: Count of records deleted from Redis. """ - client = await self._get_client() total_records_deleted: int = 0 async for batch in self.paginate( @@ -1583,20 +1622,7 @@ async def clear(self) -> int: ): batch_keys = [record["id"] for record in batch] if batch_keys: - is_cluster = isinstance(client, AsyncRedisCluster) - if is_cluster: - records_deleted_in_batch = 0 - for key_to_delete in batch_keys: - try: - records_deleted_in_batch += cast( - int, await client.delete(key_to_delete) - ) - except redis.exceptions.RedisError as e: - logger.warning(f"Failed to delete key {key_to_delete}: {e}") - total_records_deleted += records_deleted_in_batch - else: - records_deleted = await client.delete(*batch_keys) - total_records_deleted += records_deleted + total_records_deleted += await self._delete_batch(batch_keys) return total_records_deleted From 49454d4fb741483a43a61da1063e1f89b77f4b8e Mon Sep 17 00:00:00 2001 From: David Froger Date: Mon, 12 Jan 2026 16:43:30 +0100 Subject: [PATCH 04/12] fix index.clear() MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Before: the `.clear()` algorithm is wrong: 1. Suppose they are 2200 documents. The first 500 are deleted: 0 1000 2000 | | | xxxxx----------------- | | | index + limit index 2. There are now 1700 documents, and the next pagination query wrongly skips the first 500 documents, and remove from 500 to 1000: 0 1000 | | -----xxxxx------- | | | index + limit index 3. There are now 1200 documents, and the next pagination query wrongly skips the first 1000 documents, and remove from 1000 to 1200: 0 1000 | | ----------xx => Only 1200 of the 2200 documents are remove, and 1000 documents remain. After: 500 documents are deleted in loop until there is nothing to delete (with a security condition on the initial documents number, in case of concurrent insertions). --- redisvl/index/index.py | 37 ++++++++++++++++++++++++++----------- 1 file changed, 26 insertions(+), 11 deletions(-) diff --git a/redisvl/index/index.py b/redisvl/index/index.py index b26fa17d..8f7457bc 100644 --- a/redisvl/index/index.py +++ b/redisvl/index/index.py @@ -1,6 +1,7 @@ import asyncio import json import threading +from math import ceil import time import warnings import weakref @@ -717,7 +718,7 @@ def _delete_batch(self, batch_keys: List[str]) -> int: batch_keys (List[str]): List of Redis keys to delete. Returns: - int: Count of records deleted from Redis. + int: Count of recordrecords deleted from Redis. """ client = cast(SyncRedisClient, self._redis_client) is_cluster = isinstance(client, RedisCluster) @@ -745,14 +746,21 @@ def clear(self) -> int: Returns: int: Count of records deleted from Redis. """ + batch_size = 500 + max_ratio = 1.01 + + info = self.info() + max_records_deleted = ceil(info["num_records"]*max_ratio) # Allow to remove some additional concurrent inserts total_records_deleted: int = 0 + query = FilterQuery(FilterExpression("*"), return_fields=["id"]).paging(0, batch_size) - for batch in self.paginate( - FilterQuery(FilterExpression("*"), return_fields=["id"]), page_size=500 - ): - batch_keys = [record["id"] for record in batch] - if batch_keys: + while True: + batch = self._query(query) + if batch and total_records_deleted <= max_records_deleted: + batch_keys = [record["id"] for record in batch] total_records_deleted += self._delete_batch(batch_keys) + else: + break return total_records_deleted @@ -1615,14 +1623,21 @@ async def clear(self) -> int: Returns: int: Count of records deleted from Redis. """ + batch_size = 500 + max_ratio = 1.01 + + info = await self.info() + max_records_deleted = ceil(info["num_records"]*max_ratio) # Allow to remove some additional concurrent inserts total_records_deleted: int = 0 + query = FilterQuery(FilterExpression("*"), return_fields=["id"]).paging(0, batch_size) - async for batch in self.paginate( - FilterQuery(FilterExpression("*"), return_fields=["id"]), page_size=500 - ): - batch_keys = [record["id"] for record in batch] - if batch_keys: + while True: + batch = await self._query(query) + if batch and total_records_deleted <= max_records_deleted: + batch_keys = [record["id"] for record in batch] total_records_deleted += await self._delete_batch(batch_keys) + else: + break return total_records_deleted From dc323f135bf09bec24dbbfac47ac2575d9e839bf Mon Sep 17 00:00:00 2001 From: David Froger Date: Mon, 19 Jan 2026 16:55:38 +0100 Subject: [PATCH 05/12] fix isort --- redisvl/index/index.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/redisvl/index/index.py b/redisvl/index/index.py index 8f7457bc..fbcb9bd1 100644 --- a/redisvl/index/index.py +++ b/redisvl/index/index.py @@ -1,10 +1,10 @@ import asyncio import json import threading -from math import ceil import time import warnings import weakref +from math import ceil from typing import ( TYPE_CHECKING, Any, From c83b0cda7669c406847b3f0160ec2d1631d0463f Mon Sep 17 00:00:00 2001 From: David Froger Date: Mon, 19 Jan 2026 16:56:09 +0100 Subject: [PATCH 06/12] fix docstring typo --- redisvl/index/index.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/redisvl/index/index.py b/redisvl/index/index.py index fbcb9bd1..8afc974e 100644 --- a/redisvl/index/index.py +++ b/redisvl/index/index.py @@ -718,7 +718,7 @@ def _delete_batch(self, batch_keys: List[str]) -> int: batch_keys (List[str]): List of Redis keys to delete. Returns: - int: Count of recordrecords deleted from Redis. + int: Count of records deleted from Redis. """ client = cast(SyncRedisClient, self._redis_client) is_cluster = isinstance(client, RedisCluster) From eb7b45566850b8971037712f91915181c6916696 Mon Sep 17 00:00:00 2001 From: David Froger Date: Tue, 20 Jan 2026 09:08:03 +0100 Subject: [PATCH 07/12] improve index.clear() tests --- tests/integration/test_async_search_index.py | 13 ++++++++++--- tests/integration/test_search_index.py | 12 +++++++++--- 2 files changed, 19 insertions(+), 6 deletions(-) diff --git a/tests/integration/test_async_search_index.py b/tests/integration/test_async_search_index.py index 2ce139e7..8f3ca6f4 100644 --- a/tests/integration/test_async_search_index.py +++ b/tests/integration/test_async_search_index.py @@ -1,4 +1,5 @@ import warnings +from random import choice from unittest import mock import pytest @@ -245,14 +246,20 @@ async def test_search_index_delete(async_index): @pytest.mark.asyncio -async def test_search_index_clear(async_index): +@pytest.mark.parametrize("num_docs", [0, 1, 5, 10, 2042]) +async def test_search_index_clear(async_index, num_docs): await async_index.create(overwrite=True, drop=True) - data = [{"id": "1", "test": "foo"}] + tags = ["foo", "bar", "baz"] + data = [{"id": str(i), "test": choice(tags)} for i in range(num_docs)] await async_index.load(data, id_field="id") + info = await async_index.info() + assert info["num_records"] == num_docs count = await async_index.clear() - assert count == len(data) + assert count == num_docs assert await async_index.exists() + info = await async_index.info() + assert info["num_records"] == 0 @pytest.mark.asyncio diff --git a/tests/integration/test_search_index.py b/tests/integration/test_search_index.py index 0cbdf8ab..43249b37 100644 --- a/tests/integration/test_search_index.py +++ b/tests/integration/test_search_index.py @@ -1,4 +1,5 @@ import warnings +from random import choice from unittest import mock import pytest @@ -303,15 +304,20 @@ def test_search_index_delete(index): assert not index.exists() assert index.name not in convert_bytes(index.client.execute_command("FT._LIST")) - -def test_search_index_clear(index): +@pytest.mark.parametrize("num_docs", [0, 1, 5, 10, 2042]) +def test_search_index_clear(index, num_docs): index.create(overwrite=True, drop=True) - data = [{"id": "1", "test": "foo"}] + tags = ["foo", "bar", "baz"] + data = [{"id": str(i), "test": choice(tags)} for i in range(num_docs)] index.load(data, id_field="id") + info = index.info() + assert info["num_records"] == num_docs count = index.clear() assert count == len(data) assert index.exists() + info = index.info() + assert info["num_records"] == 0 def test_search_index_drop_key(index): From 461e610fe0b8ddc37c5c1bf6fe6f33073b053d2b Mon Sep 17 00:00:00 2001 From: David Froger Date: Tue, 20 Jan 2026 09:11:31 +0100 Subject: [PATCH 08/12] fix black --- redisvl/index/index.py | 32 ++++++++++++++++++++++---------- 1 file changed, 22 insertions(+), 10 deletions(-) diff --git a/redisvl/index/index.py b/redisvl/index/index.py index 8afc974e..1a0e92f8 100644 --- a/redisvl/index/index.py +++ b/redisvl/index/index.py @@ -726,9 +726,7 @@ def _delete_batch(self, batch_keys: List[str]) -> int: records_deleted_in_batch = 0 for key_to_delete in batch_keys: try: - records_deleted_in_batch += cast( - int, client.delete(key_to_delete) - ) + records_deleted_in_batch += cast(int, client.delete(key_to_delete)) except redis.exceptions.RedisError as e: logger.warning(f"Failed to delete key {key_to_delete}: {e}") else: @@ -750,9 +748,13 @@ def clear(self) -> int: max_ratio = 1.01 info = self.info() - max_records_deleted = ceil(info["num_records"]*max_ratio) # Allow to remove some additional concurrent inserts + max_records_deleted = ceil( + info["num_records"] * max_ratio + ) # Allow to remove some additional concurrent inserts total_records_deleted: int = 0 - query = FilterQuery(FilterExpression("*"), return_fields=["id"]).paging(0, batch_size) + query = FilterQuery(FilterExpression("*"), return_fields=["id"]).paging( + 0, batch_size + ) while True: batch = self._query(query) @@ -1224,7 +1226,9 @@ def _info(name: str, redis_client: SyncRedisClient) -> Dict[str, Any]: try: if isinstance(redis_client, SyncRedisCluster): node = redis_client.get_random_node() - values = redis_client.execute_command("FT.INFO", name, target_nodes=node) + values = redis_client.execute_command( + "FT.INFO", name, target_nodes=node + ) info = make_dict(values) else: info = redis_client.ft(name).info() @@ -1458,7 +1462,9 @@ async def _info(name: str, redis_client: AsyncRedisClient) -> Dict[str, Any]: try: if isinstance(redis_client, AsyncRedisCluster): node = redis_client.get_random_node() - values = await redis_client.execute_command("FT.INFO", name, target_nodes=node) + values = await redis_client.execute_command( + "FT.INFO", name, target_nodes=node + ) info = make_dict(values) else: info = await redis_client.ft(name).info() @@ -1605,7 +1611,9 @@ async def _delete_batch(self, batch_keys: List[str]) -> int: records_deleted_in_batch = 0 for key_to_delete in batch_keys: try: - records_deleted_in_batch += cast(int, await client.delete(key_to_delete)) + records_deleted_in_batch += cast( + int, await client.delete(key_to_delete) + ) except redis.exceptions.RedisError as e: logger.warning(f"Failed to delete key {key_to_delete}: {e}") else: @@ -1627,9 +1635,13 @@ async def clear(self) -> int: max_ratio = 1.01 info = await self.info() - max_records_deleted = ceil(info["num_records"]*max_ratio) # Allow to remove some additional concurrent inserts + max_records_deleted = ceil( + info["num_records"] * max_ratio + ) # Allow to remove some additional concurrent inserts total_records_deleted: int = 0 - query = FilterQuery(FilterExpression("*"), return_fields=["id"]).paging(0, batch_size) + query = FilterQuery(FilterExpression("*"), return_fields=["id"]).paging( + 0, batch_size + ) while True: batch = await self._query(query) From 9c70e8c69597c5158af700662c6a01c7bb0becf7 Mon Sep 17 00:00:00 2001 From: David Froger Date: Tue, 20 Jan 2026 09:25:45 +0100 Subject: [PATCH 09/12] fix mypy (workaround) correct fix (in redis repo) would be for Query.paging() to return Self, not "Query" --- redisvl/index/index.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/redisvl/index/index.py b/redisvl/index/index.py index 1a0e92f8..5d10d963 100644 --- a/redisvl/index/index.py +++ b/redisvl/index/index.py @@ -752,9 +752,8 @@ def clear(self) -> int: info["num_records"] * max_ratio ) # Allow to remove some additional concurrent inserts total_records_deleted: int = 0 - query = FilterQuery(FilterExpression("*"), return_fields=["id"]).paging( - 0, batch_size - ) + query = FilterQuery(FilterExpression("*"), return_fields=["id"]) + query.paging(0, batch_size) while True: batch = self._query(query) @@ -1639,9 +1638,8 @@ async def clear(self) -> int: info["num_records"] * max_ratio ) # Allow to remove some additional concurrent inserts total_records_deleted: int = 0 - query = FilterQuery(FilterExpression("*"), return_fields=["id"]).paging( - 0, batch_size - ) + query = FilterQuery(FilterExpression("*"), return_fields=["id"]) + query.paging(0, batch_size) while True: batch = await self._query(query) From 29eb28834e86666f67caed67d6933eafb5cbcaf7 Mon Sep 17 00:00:00 2001 From: David Froger Date: Wed, 21 Jan 2026 17:05:59 +0100 Subject: [PATCH 10/12] test: fix num_docs vs num_records --- tests/integration/test_async_search_index.py | 4 ++-- tests/integration/test_search_index.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/integration/test_async_search_index.py b/tests/integration/test_async_search_index.py index 8f3ca6f4..c3ca3774 100644 --- a/tests/integration/test_async_search_index.py +++ b/tests/integration/test_async_search_index.py @@ -253,13 +253,13 @@ async def test_search_index_clear(async_index, num_docs): data = [{"id": str(i), "test": choice(tags)} for i in range(num_docs)] await async_index.load(data, id_field="id") info = await async_index.info() - assert info["num_records"] == num_docs + assert info["num_docs"] == num_docs count = await async_index.clear() assert count == num_docs assert await async_index.exists() info = await async_index.info() - assert info["num_records"] == 0 + assert info["num_docs"] == 0 @pytest.mark.asyncio diff --git a/tests/integration/test_search_index.py b/tests/integration/test_search_index.py index 43249b37..ae64a229 100644 --- a/tests/integration/test_search_index.py +++ b/tests/integration/test_search_index.py @@ -311,13 +311,13 @@ def test_search_index_clear(index, num_docs): data = [{"id": str(i), "test": choice(tags)} for i in range(num_docs)] index.load(data, id_field="id") info = index.info() - assert info["num_records"] == num_docs + assert info["num_docs"] == num_docs count = index.clear() assert count == len(data) assert index.exists() info = index.info() - assert info["num_records"] == 0 + assert info["num_docs"] == 0 def test_search_index_drop_key(index): From 5ce978a4ad1605547b5e3bcef1bb605d505daab1 Mon Sep 17 00:00:00 2001 From: David Froger Date: Thu, 22 Jan 2026 09:28:17 +0100 Subject: [PATCH 11/12] fix num_docs vs num_records in .clear() method --- redisvl/index/index.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/redisvl/index/index.py b/redisvl/index/index.py index 5d10d963..4bc66f67 100644 --- a/redisvl/index/index.py +++ b/redisvl/index/index.py @@ -749,7 +749,7 @@ def clear(self) -> int: info = self.info() max_records_deleted = ceil( - info["num_records"] * max_ratio + info["num_docs"] * max_ratio ) # Allow to remove some additional concurrent inserts total_records_deleted: int = 0 query = FilterQuery(FilterExpression("*"), return_fields=["id"]) @@ -1635,7 +1635,7 @@ async def clear(self) -> int: info = await self.info() max_records_deleted = ceil( - info["num_records"] * max_ratio + info["num_docs"] * max_ratio ) # Allow to remove some additional concurrent inserts total_records_deleted: int = 0 query = FilterQuery(FilterExpression("*"), return_fields=["id"]) From 8e2fcfab56e3c8638478fd4a16eb3eb9d6cfdba9 Mon Sep 17 00:00:00 2001 From: David Froger Date: Thu, 22 Jan 2026 10:12:36 +0100 Subject: [PATCH 12/12] fix tests on .clear() error handling mock .info() and ._query() instead of .paginate() --- tests/unit/test_error_handling.py | 31 +++++++++++++++++++++++-------- 1 file changed, 23 insertions(+), 8 deletions(-) diff --git a/tests/unit/test_error_handling.py b/tests/unit/test_error_handling.py index 478743f9..173ff226 100644 --- a/tests/unit/test_error_handling.py +++ b/tests/unit/test_error_handling.py @@ -454,10 +454,15 @@ def test_clear_individual_key_deletion_errors(self, mock_validate): 1, # Third succeeds ] - # Mock the paginate method to return test data - with patch.object(SearchIndex, "paginate") as mock_paginate: - mock_paginate.return_value = [ - [{"id": "test:key1"}, {"id": "test:key2"}, {"id": "test:key3"}] + # Mock the .info() and ._query() methods to return test data + with ( + patch.object(SearchIndex, "info") as mock_info, + patch.object(SearchIndex, "_query") as mock_query, + ): + mock_info.return_value = {"num_docs": 3} + mock_query.side_effect = [ + [{"id": "test:key1"}, {"id": "test:key2"}, {"id": "test:key3"}], + [], ] # Create index with mocked client @@ -502,11 +507,21 @@ async def test_async_clear_individual_key_deletion_errors(self, mock_validate): ] ) - # Mock the paginate method to return test data - async def mock_paginate_generator(*args, **kwargs): - yield [{"id": "test:key1"}, {"id": "test:key2"}, {"id": "test:key3"}] + # Mock the .info() and ._query() methods to return test data + async def mock_info(*args, **kwargs): + return {"num_docs": 3} - with patch.object(AsyncSearchIndex, "paginate", mock_paginate_generator): + mock_query = AsyncMock( + side_effect=[ + [{"id": "test:key1"}, {"id": "test:key2"}, {"id": "test:key3"}], + [], + ] + ) + + with ( + patch.object(AsyncSearchIndex, "info", mock_info), + patch.object(AsyncSearchIndex, "_query", mock_query), + ): # Create index with mocked client index = AsyncSearchIndex(schema) index._redis_client = mock_cluster_client