diff --git a/mem0/memory/main.py b/mem0/memory/main.py index 876f50f9d..e7252f95e 100644 --- a/mem0/memory/main.py +++ b/mem0/memory/main.py @@ -3008,6 +3008,7 @@ class AsyncMemory(MemoryBase): if "created_at" not in new_metadata: new_metadata["created_at"] = datetime.now(timezone.utc).isoformat() new_metadata["updated_at"] = new_metadata["created_at"] + new_metadata["text_lemmatized"] = lemmatize_for_bm25(data) await asyncio.to_thread( self.vector_store.insert, diff --git a/tests/test_memory.py b/tests/test_memory.py index 0bc171ea2..9369992bd 100644 --- a/tests/test_memory.py +++ b/tests/test_memory.py @@ -854,3 +854,84 @@ def test_get_all_rejects_user_id_kwarg(mock_sqlite, mock_llm_factory, mock_vecto with pytest.raises(ValueError, match=r"user_id.*filters"): memory.get_all(user_id="u1") + + +# ─── Regression: AsyncMemory._create_memory must store text_lemmatized ───────── +@patch('mem0.utils.factory.EmbedderFactory.create') +@patch('mem0.utils.factory.VectorStoreFactory.create') +@patch('mem0.utils.factory.LlmFactory.create') +@patch('mem0.memory.storage.SQLiteManager') +def test_sync_create_memory_stores_text_lemmatized(mock_sqlite, mock_llm_factory, mock_vector_factory, mock_embedder_factory): + """Sync Memory._create_memory must include text_lemmatized in payload for BM25 keyword search.""" + embedder = MagicMock() + embedder.embed.return_value = [0.1, 0.2, 0.3] + mock_embedder_factory.return_value = embedder + + mock_vector_store = MagicMock() + mock_vector_store.insert.return_value = None + telemetry_vector_store = MagicMock() + mock_vector_factory.side_effect = [mock_vector_store, telemetry_vector_store] + + mock_llm_factory.return_value = MagicMock() + mock_sqlite.return_value = MagicMock() + + from mem0.memory.main import Memory as MemoryClass + memory = MemoryClass(MemoryConfig()) + + data = "I love hiking in the mountains" + embeddings = {data: [0.1, 0.2, 0.3]} + metadata = {"user_id": "test_user"} + + memory._create_memory(data, embeddings, metadata) + + # Check that text_lemmatized was stored in the payload + insert_call = mock_vector_store.insert.call_args + payload = insert_call.kwargs.get("payloads") or insert_call[1].get("payloads") + assert payload is not None and len(payload) == 1 + assert "text_lemmatized" in payload[0], "Sync _create_memory must store text_lemmatized for BM25" + assert payload[0]["text_lemmatized"] != "", "text_lemmatized must not be empty" + + +@pytest.mark.asyncio +@patch('mem0.utils.factory.EmbedderFactory.create') +@patch('mem0.utils.factory.VectorStoreFactory.create') +@patch('mem0.utils.factory.LlmFactory.create') +@patch('mem0.memory.storage.SQLiteManager') +async def test_async_create_memory_stores_text_lemmatized(mock_sqlite, mock_llm_factory, mock_vector_factory, mock_embedder_factory): + """ + Regression test: AsyncMemory._create_memory must include text_lemmatized + in the vector store payload. + + Without text_lemmatized, memories created via AsyncMemory with infer=False + are invisible to BM25 keyword search, silently degrading search recall for + all async users. + """ + embedder = MagicMock() + embedder.embed.return_value = [0.1, 0.2, 0.3] + mock_embedder_factory.return_value = embedder + + mock_vector_store = MagicMock() + mock_vector_store.insert.return_value = None + mock_vector_factory.return_value = mock_vector_store + + mock_llm_factory.return_value = MagicMock() + mock_sqlite.return_value = MagicMock() + + from mem0.memory.main import AsyncMemory + memory = AsyncMemory(MemoryConfig()) + + data = "I love hiking in the mountains" + embeddings = {data: [0.1, 0.2, 0.3]} + metadata = {"user_id": "test_user"} + + await memory._create_memory(data, embeddings, metadata) + + # Check that text_lemmatized was stored in the payload + insert_call = mock_vector_store.insert.call_args + payload = insert_call.kwargs.get("payloads") or insert_call[1].get("payloads") + assert payload is not None and len(payload) == 1 + assert "text_lemmatized" in payload[0], ( + "AsyncMemory._create_memory must store text_lemmatized for BM25 keyword search" + ) + assert payload[0]["text_lemmatized"] != "", "text_lemmatized must not be empty" +