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,
|
normalize_bm25,
|
||||||
score_and_rank,
|
score_and_rank,
|
||||||
)
|
)
|
||||||
|
from mem0.vector_stores.base import VectorStoreBase
|
||||||
|
|
||||||
# Suppress SWIG deprecation warnings globally
|
# Suppress SWIG deprecation warnings globally
|
||||||
warnings.filterwarnings("ignore", category=DeprecationWarning, message=".*SwigPy.*")
|
warnings.filterwarnings("ignore", category=DeprecationWarning, message=".*SwigPy.*")
|
||||||
@@ -400,6 +401,15 @@ class Memory(MemoryBase):
|
|||||||
self._telemetry_vector_store = VectorStoreFactory.create(
|
self._telemetry_vector_store = VectorStoreFactory.create(
|
||||||
self.config.vector_store.provider, telemetry_config
|
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"})
|
capture_event("mem0.init", self, {"sync_type": "sync"})
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@@ -1872,6 +1882,15 @@ class AsyncMemory(MemoryBase):
|
|||||||
os.makedirs(telemetry_config.path, exist_ok=True)
|
os.makedirs(telemetry_config.path, exist_ok=True)
|
||||||
self._telemetry_vector_store = VectorStoreFactory.create(self.config.vector_store.provider, telemetry_config)
|
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"})
|
capture_event("mem0.init", self, {"sync_type": "async"})
|
||||||
|
|
||||||
@property
|
@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"
|
"AsyncMemory._create_memory must store text_lemmatized for BM25 keyword search"
|
||||||
)
|
)
|
||||||
assert payload[0]["text_lemmatized"] != "", "text_lemmatized must not be empty"
|
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