feat: warn when hybrid search silently degrades to semantic-only (#5444)
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user