From a48f34cf774cbbc77ea1644da55d3d5f90aec9e0 Mon Sep 17 00:00:00 2001 From: Yash Singh <123385188+yashs33244@users.noreply.github.com> Date: Mon, 22 Jun 2026 11:29:25 +0530 Subject: [PATCH] fix(vector_stores): deep-copy Redis DEFAULT_FIELDS so instances keep distinct dims (#5633) --- mem0/vector_stores/redis.py | 8 +++-- tests/vector_stores/test_redis.py | 54 ++++++++++++++++++++++++++++++- 2 files changed, 58 insertions(+), 4 deletions(-) diff --git a/mem0/vector_stores/redis.py b/mem0/vector_stores/redis.py index 43bcb04c8..32a8941bd 100644 --- a/mem0/vector_stores/redis.py +++ b/mem0/vector_stores/redis.py @@ -1,3 +1,4 @@ +import copy import json import logging from datetime import datetime, timezone @@ -65,7 +66,7 @@ class RedisDB(VectorStoreBase): "prefix": f"mem0:{collection_name}", } - fields = DEFAULT_FIELDS.copy() + fields = copy.deepcopy(DEFAULT_FIELDS) fields[-1]["attrs"]["dims"] = embedding_model_dims self.schema = {"index": index_schema, "fields": fields} @@ -98,8 +99,9 @@ class RedisDB(VectorStoreBase): "prefix": f"mem0:{collection_name}", } - # Copy the default fields and update the vector field with the specified dimensions - fields = DEFAULT_FIELDS.copy() + # Deep-copy the default fields so mutating the nested vector attrs never + # leaks into the module global or other instances. + fields = copy.deepcopy(DEFAULT_FIELDS) fields[-1]["attrs"]["dims"] = embedding_dims fields[-1]["attrs"]["distance_metric"] = distance_metric diff --git a/tests/vector_stores/test_redis.py b/tests/vector_stores/test_redis.py index 139aaeeb2..13cae0edb 100644 --- a/tests/vector_stores/test_redis.py +++ b/tests/vector_stores/test_redis.py @@ -7,7 +7,7 @@ when vector is None. """ from datetime import datetime -from unittest.mock import MagicMock +from unittest.mock import MagicMock, patch import numpy as np import pytz @@ -115,6 +115,58 @@ def test_list_with_filter_builds_query(): assert query.query_string() == "@user_id:{alice}" +def test_create_col_keeps_distinct_dims_across_instances(): + """Building the schema for two collections with different embedding + dimensions must keep them distinct. Regression: DEFAULT_FIELDS.copy() is a + shallow copy, so the shared "embedding" field dict was mutated in place and + the second create_col() clobbered the first one's dims (and the module + global).""" + import mem0.vector_stores.redis as redis_module + + db, _ = _make_redis_db() + db.client = MagicMock() + + captured = [] + + def capture_schema(schema): + captured.append(schema) + return MagicMock() + + with patch.object(redis_module.SearchIndex, "from_dict", side_effect=capture_schema): + db.create_col(name="col_384", vector_size=384) + db.create_col(name="col_1536", vector_size=1536) + + assert captured[0]["fields"][-1]["attrs"]["dims"] == 384 + assert captured[1]["fields"][-1]["attrs"]["dims"] == 1536 + # the module-level default must never be mutated by building a schema + assert "dims" not in redis_module.DEFAULT_FIELDS[-1]["attrs"] + + +def test_init_keeps_distinct_dims_from_module_global(): + """__init__ must stamp the requested dims into the index schema without + mutating the shared module-level DEFAULT_FIELDS. The create_col test above + covers the other deepcopy site; all other tests build RedisDB via __new__ + and so skip __init__, leaving this call site otherwise uncovered.""" + import mem0.vector_stores.redis as redis_module + from mem0.vector_stores.redis import RedisDB + + captured = [] + + def capture_schema(schema): + captured.append(schema) + return MagicMock() + + with ( + patch("mem0.vector_stores.redis.redis.Redis.from_url", return_value=MagicMock()), + patch.object(redis_module.SearchIndex, "from_dict", side_effect=capture_schema), + ): + RedisDB("redis://localhost:6379", "col_384", 384) + + assert captured[0]["fields"][-1]["attrs"]["dims"] == 384 + # the module-level default must never be mutated by constructing an instance + assert "dims" not in redis_module.DEFAULT_FIELDS[-1]["attrs"] + + def test_get_returns_none_for_missing_id(): """get() must return None for a missing id.