Skip to content

Commit f585668

Browse files
authored
community[minor]: add mongodb byte store (#23876)
The `MongoDBStore` can manage only documents. It's not possible to use MongoDB for an `CacheBackedEmbeddings`. With this new implementation, it's possible to use: ```python CacheBackedEmbeddings.from_bytes_store( underlying_embeddings=embeddings, document_embedding_cache=MongoDBByteStore( connection_string=db_uri, db_name=db_name, collection_name=collection_name, ), ) ``` and use MongoDB to cache the embeddings !
1 parent 07715f8 commit f585668

File tree

4 files changed

+146
-6
lines changed

4 files changed

+146
-6
lines changed

libs/community/langchain_community/storage/__init__.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,9 +25,7 @@
2525
from langchain_community.storage.cassandra import (
2626
CassandraByteStore,
2727
)
28-
from langchain_community.storage.mongodb import (
29-
MongoDBStore,
30-
)
28+
from langchain_community.storage.mongodb import MongoDBByteStore, MongoDBStore
3129
from langchain_community.storage.redis import (
3230
RedisStore,
3331
)
@@ -44,6 +42,7 @@
4442
"AstraDBStore",
4543
"CassandraByteStore",
4644
"MongoDBStore",
45+
"MongoDBByteStore",
4746
"RedisStore",
4847
"SQLStore",
4948
"UpstashRedisByteStore",
@@ -55,6 +54,7 @@
5554
"AstraDBStore": "langchain_community.storage.astradb",
5655
"CassandraByteStore": "langchain_community.storage.cassandra",
5756
"MongoDBStore": "langchain_community.storage.mongodb",
57+
"MongoDBByteStore": "langchain_community.storage.mongodb",
5858
"RedisStore": "langchain_community.storage.redis",
5959
"SQLStore": "langchain_community.storage.sql",
6060
"UpstashRedisByteStore": "langchain_community.storage.upstash_redis",

libs/community/langchain_community/storage/mongodb.py

Lines changed: 123 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,126 @@
44
from langchain_core.stores import BaseStore
55

66

7+
class MongoDBByteStore(BaseStore[str, bytes]):
8+
"""BaseStore implementation using MongoDB as the underlying store.
9+
10+
Examples:
11+
Create a MongoDBByteStore instance and perform operations on it:
12+
13+
.. code-block:: python
14+
15+
# Instantiate the MongoDBByteStore with a MongoDB connection
16+
from langchain.storage import MongoDBByteStore
17+
18+
mongo_conn_str = "mongodb://localhost:27017/"
19+
mongodb_store = MongoDBBytesStore(mongo_conn_str, db_name="test-db",
20+
collection_name="test-collection")
21+
22+
# Set values for keys
23+
mongodb_store.mset([("key1", "hello"), ("key2", "workd")])
24+
25+
# Get values for keys
26+
values = mongodb_store.mget(["key1", "key2"])
27+
# [bytes1, bytes1]
28+
29+
# Iterate over keys
30+
for key in mongodb_store.yield_keys():
31+
print(key)
32+
33+
# Delete keys
34+
mongodb_store.mdelete(["key1", "key2"])
35+
"""
36+
37+
def __init__(
38+
self,
39+
connection_string: str,
40+
db_name: str,
41+
collection_name: str,
42+
*,
43+
client_kwargs: Optional[dict] = None,
44+
) -> None:
45+
"""Initialize the MongoDBStore with a MongoDB connection string.
46+
47+
Args:
48+
connection_string (str): MongoDB connection string
49+
db_name (str): name to use
50+
collection_name (str): collection name to use
51+
client_kwargs (dict): Keyword arguments to pass to the Mongo client
52+
"""
53+
try:
54+
from pymongo import MongoClient
55+
except ImportError as e:
56+
raise ImportError(
57+
"The MongoDBStore requires the pymongo library to be "
58+
"installed. "
59+
"pip install pymongo"
60+
) from e
61+
62+
if not connection_string:
63+
raise ValueError("connection_string must be provided.")
64+
if not db_name:
65+
raise ValueError("db_name must be provided.")
66+
if not collection_name:
67+
raise ValueError("collection_name must be provided.")
68+
69+
self.client: MongoClient = MongoClient(
70+
connection_string, **(client_kwargs or {})
71+
)
72+
self.collection = self.client[db_name][collection_name]
73+
74+
def mget(self, keys: Sequence[str]) -> List[Optional[bytes]]:
75+
"""Get the list of documents associated with the given keys.
76+
77+
Args:
78+
keys (list[str]): A list of keys representing Document IDs..
79+
80+
Returns:
81+
list[Document]: A list of Documents corresponding to the provided
82+
keys, where each Document is either retrieved successfully or
83+
represented as None if not found.
84+
"""
85+
result = self.collection.find({"_id": {"$in": keys}})
86+
result_dict = {doc["_id"]: doc["value"] for doc in result}
87+
return [result_dict.get(key) for key in keys]
88+
89+
def mset(self, key_value_pairs: Sequence[Tuple[str, bytes]]) -> None:
90+
"""Set the given key-value pairs.
91+
92+
Args:
93+
key_value_pairs (list[tuple[str, Document]]): A list of id-document
94+
pairs.
95+
"""
96+
from pymongo import UpdateOne
97+
98+
updates = [{"_id": k, "value": v} for k, v in key_value_pairs]
99+
self.collection.bulk_write(
100+
[UpdateOne({"_id": u["_id"]}, {"$set": u}, upsert=True) for u in updates]
101+
)
102+
103+
def mdelete(self, keys: Sequence[str]) -> None:
104+
"""Delete the given ids.
105+
106+
Args:
107+
keys (list[str]): A list of keys representing Document IDs..
108+
"""
109+
self.collection.delete_many({"_id": {"$in": keys}})
110+
111+
def yield_keys(self, prefix: Optional[str] = None) -> Iterator[str]:
112+
"""Yield keys in the store.
113+
114+
Args:
115+
prefix (str): prefix of keys to retrieve.
116+
"""
117+
if prefix is None:
118+
for doc in self.collection.find(projection=["_id"]):
119+
yield doc["_id"]
120+
else:
121+
for doc in self.collection.find(
122+
{"_id": {"$regex": f"^{prefix}"}}, projection=["_id"]
123+
):
124+
yield doc["_id"]
125+
126+
7127
class MongoDBStore(BaseStore[str, Document]):
8128
"""BaseStore implementation using MongoDB as the underlying store.
9129
@@ -68,7 +188,9 @@ def __init__(
68188
if not collection_name:
69189
raise ValueError("collection_name must be provided.")
70190

71-
self.client = MongoClient(connection_string, **(client_kwargs or {}))
191+
self.client: MongoClient = MongoClient(
192+
connection_string, **(client_kwargs or {})
193+
)
72194
self.collection = self.client[db_name][collection_name]
73195

74196
def mget(self, keys: Sequence[str]) -> List[Optional[Document]]:

libs/community/tests/integration_tests/storage/test_mongodb.py

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
1-
from typing import Generator
1+
from typing import Generator, Tuple
22

33
import pytest
44
from langchain_core.documents import Document
5+
from langchain_standard_tests.integration_tests.base_store import BaseStoreSyncTests
56

6-
from langchain_community.storage.mongodb import MongoDBStore
7+
from langchain_community.storage.mongodb import MongoDBByteStore, MongoDBStore
78

89
pytest.importorskip("pymongo")
910

@@ -71,3 +72,19 @@ def test_mdelete(mongo_store: MongoDBStore) -> None:
7172
def test_init_errors() -> None:
7273
with pytest.raises(ValueError):
7374
MongoDBStore("", "", "")
75+
76+
77+
class TestMongoDBStore(BaseStoreSyncTests):
78+
@pytest.fixture
79+
def three_values(self) -> Tuple[bytes, bytes, bytes]: # <-- Provide 3
80+
return b"foo", b"bar", b"buzz"
81+
82+
@pytest.fixture
83+
def kv_store(self) -> MongoDBByteStore:
84+
import mongomock
85+
86+
# mongomock creates a mock MongoDB instance for testing purposes
87+
with mongomock.patch(servers=(("localhost", 27017),)):
88+
return MongoDBByteStore(
89+
"mongodb://localhost:27017/", "test_db", "test_collection"
90+
)

libs/community/tests/unit_tests/storage/test_imports.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
"AstraDBStore",
55
"AstraDBByteStore",
66
"CassandraByteStore",
7+
"MongoDBByteStore",
78
"MongoDBStore",
89
"SQLStore",
910
"RedisStore",

0 commit comments

Comments
 (0)