From e769502baa51e87980a1ded9f0b4647725e3a166 Mon Sep 17 00:00:00 2001 From: Hrushikesh Yadav <136978914+HrushiYadav@users.noreply.github.com> Date: Wed, 10 Jun 2026 16:26:48 +0530 Subject: [PATCH] feat: warn when hybrid search silently degrades to semantic-only (#5444) --- mem0/memory/main.py | 19 +++++++++++ tests/test_memory.py | 77 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 96 insertions(+) diff --git a/mem0/memory/main.py b/mem0/memory/main.py index 309583c85..c9303bb92 100644 --- a/mem0/memory/main.py +++ b/mem0/memory/main.py @@ -47,6 +47,7 @@ from mem0.utils.scoring import ( normalize_bm25, score_and_rank, ) +from mem0.vector_stores.base import VectorStoreBase # Suppress SWIG deprecation warnings globally warnings.filterwarnings("ignore", category=DeprecationWarning, message=".*SwigPy.*") @@ -400,6 +401,15 @@ class Memory(MemoryBase): self._telemetry_vector_store = VectorStoreFactory.create( self.config.vector_store.provider, telemetry_config ) + if getattr(type(self.vector_store), "keyword_search", None) is VectorStoreBase.keyword_search: + logger.warning( + "The '%s' vector store does not support keyword search. " + "Hybrid (BM25) scoring will be disabled and search will use " + "semantic similarity only. To enable hybrid search, switch to a " + "store with keyword_search support (e.g. qdrant, elasticsearch, pgvector).", + self.config.vector_store.provider, + ) + capture_event("mem0.init", self, {"sync_type": "sync"}) @property @@ -1872,6 +1882,15 @@ class AsyncMemory(MemoryBase): os.makedirs(telemetry_config.path, exist_ok=True) self._telemetry_vector_store = VectorStoreFactory.create(self.config.vector_store.provider, telemetry_config) + if getattr(type(self.vector_store), "keyword_search", None) is VectorStoreBase.keyword_search: + logger.warning( + "The '%s' vector store does not support keyword search. " + "Hybrid (BM25) scoring will be disabled and search will use " + "semantic similarity only. To enable hybrid search, switch to a " + "store with keyword_search support (e.g. qdrant, elasticsearch, pgvector).", + self.config.vector_store.provider, + ) + capture_event("mem0.init", self, {"sync_type": "async"}) @property diff --git a/tests/test_memory.py b/tests/test_memory.py index 56ac8835d..ae3f3ae89 100644 --- a/tests/test_memory.py +++ b/tests/test_memory.py @@ -970,3 +970,80 @@ async def test_async_create_memory_stores_text_lemmatized(mock_sqlite, mock_llm_ "AsyncMemory._create_memory must store text_lemmatized for BM25 keyword search" ) assert payload[0]["text_lemmatized"] != "", "text_lemmatized must not be empty" + + +class TestHybridSearchWarning: + """Warn at init when vector store does not support keyword_search.""" + + @patch("mem0.memory.telemetry.capture_event") + @patch("mem0.memory.main.SQLiteManager") + @patch("mem0.utils.factory.LlmFactory.create") + @patch("mem0.utils.factory.EmbedderFactory.create") + @patch("mem0.utils.factory.VectorStoreFactory.create") + def test_warning_for_store_without_keyword_search( + self, mock_vs_factory, mock_emb, mock_llm, mock_sqlite, _cap, caplog + ): + from mem0.vector_stores.base import VectorStoreBase + import logging + + class StoreWithoutKeywordSearch(VectorStoreBase): + def create_col(self, *a, **kw): pass + def insert(self, *a, **kw): pass + def search(self, *a, **kw): return [] + def delete(self, *a, **kw): pass + def update(self, *a, **kw): pass + def get(self, *a, **kw): pass + def list_cols(self): return [] + def delete_col(self): pass + def col_info(self): return {} + def list(self, *a, **kw): return [] + def reset(self): pass + + mock_vs_factory.return_value = StoreWithoutKeywordSearch() + mock_emb.return_value = MagicMock() + mock_llm.return_value = MagicMock() + + config = MemoryConfig() + config.vector_store.provider = "chroma" + + with caplog.at_level(logging.WARNING, logger="mem0.memory.main"): + Memory(config) + + assert any("does not support keyword search" in r.message for r in caplog.records) + + @patch("mem0.memory.telemetry.capture_event") + @patch("mem0.memory.main.SQLiteManager") + @patch("mem0.utils.factory.LlmFactory.create") + @patch("mem0.utils.factory.EmbedderFactory.create") + @patch("mem0.utils.factory.VectorStoreFactory.create") + def test_no_warning_for_store_with_keyword_search( + self, mock_vs_factory, mock_emb, mock_llm, mock_sqlite, _cap, caplog + ): + from mem0.vector_stores.base import VectorStoreBase + import logging + + class StoreWithKeywordSearch(VectorStoreBase): + def keyword_search(self, query, top_k=5, filters=None): + return [] + def create_col(self, *a, **kw): pass + def insert(self, *a, **kw): pass + def search(self, *a, **kw): return [] + def delete(self, *a, **kw): pass + def update(self, *a, **kw): pass + def get(self, *a, **kw): pass + def list_cols(self): return [] + def delete_col(self): pass + def col_info(self): return {} + def list(self, *a, **kw): return [] + def reset(self): pass + + mock_vs_factory.return_value = StoreWithKeywordSearch() + mock_emb.return_value = MagicMock() + mock_llm.return_value = MagicMock() + + config = MemoryConfig() + + with caplog.at_level(logging.WARNING, logger="mem0.memory.main"): + Memory(config) + + assert not any("does not support keyword search" in r.message for r in caplog.records)