Repository: deepset-ai/haystack

Issue:
feat: implement serialization for `InMemoryDocumentStore` 
**Is your feature request related to a problem? Please describe.**
`InMemoryDocumentStore` is really nice for showcasing demos and it is relatively easy to implement a `to_disk` and `from_disk` method to make this easy. I wrote something custom and easy.

**Describe the solution you'd like**
```python
# Copyright 2024-present, David Berenstein, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

import json
from pathlib import Path
from typing import Any, Dict

from haystack import Document
from haystack.document_stores.in_memory import InMemoryDocumentStore

class Database(InMemoryDocumentStore):
    def to_disk(self, path: str):
        """Write the database and its' data to disk as a JSON file."""
        data: Dict[str, Any] = self.to_dict()
        data["documents"] = [doc.to_dict(flatten=False) for doc in self.storage.values()]
        with open(path, "w") as f:
            json.dump(data, f)

    @classmethod
    def from_disk(cls, path: str) -> "Database":
        """Load the database and its' data from disk as a JSON file."""
        if Path(path).exists():
            try:
                with open(path, "r") as f:
                    data = json.load(f)
                cls_object = cls.from_dict(data)
                cls_object.write_documents([Document(**doc) for doc in data["documents"]])
                return cls_object
            except Exception as e:
                return cls()
        else:
            return cls()

```

**Describe alternatives you've considered**
N.A.

**Additional context**
N.A.


Discussion:
Would love to work on a potential PR too.
@davidberenstein1957 that would be great! 

This methods could be added directly to `InMemoryDocumentStore` really, no need for a wrapper. 😁 

I would be a bit more specific and call them `save_to_disk` and `load_to_disk` maybe. 🤔 
> @davidberenstein1957 that would be great!
> 
> This methods could be added directly to `InMemoryDocumentStore` really, no need for a wrapper. 😁
> 
> I would be a bit more specific and call them `save_to_disk` and `load_to_disk` maybe. 🤔

Yes it was an actual copy-paste from code I was using internally.

Resolving patch:
diff --git a/haystack/document_stores/in_memory/document_store.py b/haystack/document_stores/in_memory/document_store.py
--- a/haystack/document_stores/in_memory/document_store.py
+++ b/haystack/document_stores/in_memory/document_store.py
@@ -2,11 +2,13 @@
 #
 # SPDX-License-Identifier: Apache-2.0
 
+import json
 import math
 import re
 import uuid
 from collections import Counter
 from dataclasses import dataclass
+from pathlib import Path
 from typing import Any, Dict, Iterable, List, Literal, Optional, Tuple
 
 import numpy as np
@@ -339,6 +341,42 @@ def from_dict(cls, data: Dict[str, Any]) -> "InMemoryDocumentStore":
         """
         return default_from_dict(cls, data)
 
+    def save_to_disk(self, path: str) -> None:
+        """
+        Write the database and its' data to disk as a JSON file.
+
+        :param path: The path to the JSON file.
+        """
+        data: Dict[str, Any] = self.to_dict()
+        data["documents"] = [doc.to_dict(flatten=False) for doc in self.storage.values()]
+        with open(path, "w") as f:
+            json.dump(data, f)
+
+    @classmethod
+    def load_from_disk(cls, path: str) -> "InMemoryDocumentStore":
+        """
+        Load the database and its' data from disk as a JSON file.
+
+        :param path: The path to the JSON file.
+        :returns: The loaded InMemoryDocumentStore.
+        """
+        if Path(path).exists():
+            try:
+                with open(path, "r") as f:
+                    data = json.load(f)
+            except Exception as e:
+                raise Exception(f"Error loading InMemoryDocumentStore from disk. error: {e}")
+
+            documents = data.pop("documents")
+            cls_object = default_from_dict(cls, data)
+            cls_object.write_documents(
+                documents=[Document(**doc) for doc in documents], policy=DuplicatePolicy.OVERWRITE
+            )
+            return cls_object
+
+        else:
+            raise FileNotFoundError(f"File {path} not found.")
+
     def count_documents(self) -> int:
         """
         Returns the number of how many documents are present in the DocumentStore.


Test patch:
diff --git a/test/document_stores/test_in_memory.py b/test/document_stores/test_in_memory.py
--- a/test/document_stores/test_in_memory.py
+++ b/test/document_stores/test_in_memory.py
@@ -6,6 +6,7 @@
 
 import pandas as pd
 import pytest
+import tempfile
 
 from haystack import Document
 from haystack.document_stores.errors import DocumentStoreError, DuplicateDocumentError
@@ -18,6 +19,11 @@ class TestMemoryDocumentStore(DocumentStoreBaseTests):  # pylint: disable=R0904
     Test InMemoryDocumentStore's specific features
     """
 
+    @pytest.fixture
+    def tmp_dir(self):
+        with tempfile.TemporaryDirectory() as tmp_dir:
+            yield tmp_dir
+
     @pytest.fixture
     def document_store(self) -> InMemoryDocumentStore:
         return InMemoryDocumentStore(bm25_algorithm="BM25L")
@@ -74,6 +80,18 @@ def test_from_dict(self, mock_regex):
         assert store.bm25_parameters == {"key": "value"}
         assert store.index == "my_cool_index"
 
+    def test_save_to_disk_and_load_from_disk(self, tmp_dir: str):
+        docs = [Document(content="Hello world"), Document(content="Haystack supports multiple languages")]
+        document_store = InMemoryDocumentStore()
+        document_store.write_documents(docs)
+        tmp_dir = tmp_dir + "/document_store.json"
+        document_store.save_to_disk(tmp_dir)
+        document_store_loaded = InMemoryDocumentStore.load_from_disk(tmp_dir)
+
+        assert document_store_loaded.count_documents() == 2
+        assert list(document_store_loaded.storage.values()) == docs
+        assert document_store_loaded.to_dict() == document_store.to_dict()
+
     def test_invalid_bm25_algorithm(self):
         with pytest.raises(ValueError, match="BM25 algorithm 'invalid' is not supported"):
             InMemoryDocumentStore(bm25_algorithm="invalid")