Repository: deepset-ai/haystack

Issue:
Update one document embeddings failing for Pinecone Document Store
**Describe the bug**
When trying to call `update_embeddings` with `update_existing_embeddings=False` like this: `document_store.update_embeddings(retriever, update_existing_embeddings=False)` I get an error while I should get only one document updated.

**Error message**
```ERROR:    Exception in ASGI application
Traceback (most recent call last):
  File "/home/nikola/portfolio/git/qa_system/venv/lib/python3.8/site-packages/uvicorn/protocols/http/h11_impl.py", line 407, in run_asgi
    result = await app(  # type: ignore[func-returns-value]
  File "/home/nikola/portfolio/git/qa_system/venv/lib/python3.8/site-packages/uvicorn/middleware/proxy_headers.py", line 78, in __call__
    return await self.app(scope, receive, send)
  File "/home/nikola/portfolio/git/qa_system/venv/lib/python3.8/site-packages/fastapi/applications.py", line 270, in __call__
    await super().__call__(scope, receive, send)
  File "/home/nikola/portfolio/git/qa_system/venv/lib/python3.8/site-packages/starlette/applications.py", line 124, in __call__
    await self.middleware_stack(scope, receive, send)
  File "/home/nikola/portfolio/git/qa_system/venv/lib/python3.8/site-packages/starlette/middleware/errors.py", line 184, in __call__
    raise exc
  File "/home/nikola/portfolio/git/qa_system/venv/lib/python3.8/site-packages/starlette/middleware/errors.py", line 162, in __call__
    await self.app(scope, receive, _send)
  File "/home/nikola/portfolio/git/qa_system/venv/lib/python3.8/site-packages/starlette/middleware/exceptions.py", line 79, in __call__
    raise exc
  File "/home/nikola/portfolio/git/qa_system/venv/lib/python3.8/site-packages/starlette/middleware/exceptions.py", line 68, in __call__
    await self.app(scope, receive, sender)
  File "/home/nikola/portfolio/git/qa_system/venv/lib/python3.8/site-packages/fastapi/middleware/asyncexitstack.py", line 21, in __call__
    raise e
  File "/home/nikola/portfolio/git/qa_system/venv/lib/python3.8/site-packages/fastapi/middleware/asyncexitstack.py", line 18, in __call__
    await self.app(scope, receive, send)
  File "/home/nikola/portfolio/git/qa_system/venv/lib/python3.8/site-packages/starlette/routing.py", line 706, in __call__
    await route.handle(scope, receive, send)
  File "/home/nikola/portfolio/git/qa_system/venv/lib/python3.8/site-packages/starlette/routing.py", line 276, in handle
    await self.app(scope, receive, send)
  File "/home/nikola/portfolio/git/qa_system/venv/lib/python3.8/site-packages/starlette/routing.py", line 66, in app
    response = await func(request)
  File "/home/nikola/portfolio/git/qa_system/venv/lib/python3.8/site-packages/fastapi/routing.py", line 237, in app
    raw_response = await run_endpoint_function(
  File "/home/nikola/portfolio/git/qa_system/venv/lib/python3.8/site-packages/fastapi/routing.py", line 165, in run_endpoint_function
    return await run_in_threadpool(dependant.call, **values)
  File "/home/nikola/portfolio/git/qa_system/venv/lib/python3.8/site-packages/starlette/concurrency.py", line 41, in run_in_threadpool
    return await anyio.to_thread.run_sync(func, *args)
  File "/home/nikola/portfolio/git/qa_system/venv/lib/python3.8/site-packages/anyio/to_thread.py", line 31, in run_sync
    return await get_asynclib().run_sync_in_worker_thread(
  File "/home/nikola/portfolio/git/qa_system/venv/lib/python3.8/site-packages/anyio/_backends/_asyncio.py", line 937, in run_sync_in_worker_thread
    return await future
  File "/home/nikola/portfolio/git/qa_system/venv/lib/python3.8/site-packages/anyio/_backends/_asyncio.py", line 867, in run
    result = context.run(func, *args)
  File "/home/nikola/portfolio/git/qa_system/./qa_system.py", line 150, in index_document
    document_store.update_embeddings(retriever, update_existing_embeddings=False, batch_size=1)
  File "/home/nikola/portfolio/git/qa_system/venv/lib/python3.8/site-packages/haystack/document_stores/pinecone.py", line 513, in update_embeddings
    embeddings = retriever.embed_documents(document_batch)
  File "/home/nikola/portfolio/git/qa_system/venv/lib/python3.8/site-packages/haystack/nodes/retriever/dense.py", line 1792, in embed_documents
    return self.embedding_encoder.embed_documents(documents)
  File "/home/nikola/portfolio/git/qa_system/venv/lib/python3.8/site-packages/haystack/nodes/retriever/_openai_encoder.py", line 137, in embed_documents
    return self.embed_batch(self.doc_encoder_model, [d.content for d in docs])
  File "/home/nikola/portfolio/git/qa_system/venv/lib/python3.8/site-packages/haystack/nodes/retriever/_openai_encoder.py", line 131, in embed_batch
    return np.concatenate(all_embeddings)
  File "<__array_function__ internals>", line 180, in concatenate
ValueError: need at least one array to concatenate
```

**Expected behavior**
Updated document in the pinecone document store.

**Additional context**
I've used OpenAI's `ada` for getting embeddings and pinecone as a document store.

**To Reproduce**
You can try out the code on my repo ([repo](https://github.com/NikolaMr/qa_system/tree/pinecone_error)), but basically, you can just add one document to the document store after initial ingestion of multiple docs to the document store.

**System:**
 - OS: Ubuntu 22.04.1 LTS
 - GPU/CPU: Intel® Core™ i5-6300U CPU @ 2.40GHz × 4
 - Haystack version (commit or version number): 1.13.2
 - DocumentStore: Pinecone
 - Reader: deepset/roberta-base-squad2
 - Retriever: ada

**Possible fix**
I've changed line 496 of `pinecone.py` to `document_count = self.get_document_count(index=index, filters=filters, only_documents_without_embedding=not update_existing_embeddings)` and added to 
```        
    if only_documents_without_embedding:
        count = 0
        for namespace in stats["namespaces"].keys():
            if "no-vectors" in namespace:
                count += stats["namespaces"][namespace]["vector_count"]
        return count
``` 
inside the `get_document_count` in `pinecone.py` and it works as expected then.



Discussion:
Hi @NikolaMr, thank you for reporting this issue! 🙌
Also, we're happy to have contributions and I see you already have a possible fix for this issue. Would you like to take over this one? Let us know if you're interested 🙂 
Hi @bilgeyucel, thanks for taking the time to look into this.

Sounds good. Let me fork, implement the required changes, and push a PR.
Thanks for the fix in the linked PR! Was just running into this and would love to see that land.
It's my pleasure @duffn. I'm looking forward to it as well :)

Resolving patch:
diff --git a/haystack/document_stores/pinecone.py b/haystack/document_stores/pinecone.py
--- a/haystack/document_stores/pinecone.py
+++ b/haystack/document_stores/pinecone.py
@@ -292,12 +292,9 @@ def get_document_count(
         pinecone_syntax_filter = LogicalFilterClause.parse(filters).convert_to_pinecone() if filters else None
 
         stats = self.pinecone_indexes[index].describe_index_stats(filter=pinecone_syntax_filter)
-        # Document count is total number of vectors across all namespaces (no-vectors + vectors)
-        count = 0
-        for namespace in stats["namespaces"].keys():
-            if not (only_documents_without_embedding and "no-vectors" not in namespace):
-                count += stats["namespaces"][namespace]["vector_count"]
-        return count
+        if only_documents_without_embedding:
+            return sum(value["vector_count"] for key, value in stats["namespaces"].items() if "no-vectors" in key)
+        return sum(value["vector_count"] for value in stats["namespaces"].values())
 
     def _validate_index_sync(self, index: Optional[str] = None):
         """
@@ -492,7 +489,9 @@ def update_embeddings(
                 f"Couldn't find a the index '{index}' in Pinecone. Try to init the "
                 f"PineconeDocumentStore() again ..."
             )
-        document_count = self.get_document_count(index=index, filters=filters)
+        document_count = self.get_document_count(
+            index=index, filters=filters, only_documents_without_embedding=not update_existing_embeddings
+        )
         if document_count == 0:
             logger.warning("Calling DocumentStore.update_embeddings() on an empty index")
             return


Test patch:
diff --git a/test/document_stores/test_pinecone.py b/test/document_stores/test_pinecone.py
--- a/test/document_stores/test_pinecone.py
+++ b/test/document_stores/test_pinecone.py
@@ -3,7 +3,7 @@
 import os
 import numpy as np
 from inspect import getmembers, isclass, isfunction
-from unittest.mock import MagicMock, ANY
+from unittest.mock import MagicMock
 
 import pytest
 
@@ -471,6 +471,123 @@ def test_get_embedding_count(self, doc_store_with_docs: PineconeDocumentStore):
         doc_store_with_docs.write_documents([doc])
         assert doc_store_with_docs.get_embedding_count() == 1
 
+    @pytest.mark.integration
+    def test_get_document_count_after_write_doc_with_embedding(self, doc_store_with_docs: PineconeDocumentStore):
+        """
+        Tests that get_document_count() returns the correct number of documents in the document store after a document
+        with an embedding is written to the document store.
+        """
+        # there are 9 docs in doc_store_with_docs (all without embeddings)
+        initial_document_count = 9
+
+        # we expect initial_document_count documents without embeddings in doc_store_with_docs
+        assert doc_store_with_docs.get_document_count(only_documents_without_embedding=True) == initial_document_count
+        # and also initial_document_count documents in total
+        assert doc_store_with_docs.get_document_count() == initial_document_count
+
+        # document with embedding is written to doc_store_with_docs
+        doc = Document(content=f"Doc with embedding", embedding=np.random.rand(768).astype(np.float32))
+        doc_store_with_docs.write_documents([doc])
+
+        # so we expect initial_document_count + 1 documents in total
+        assert doc_store_with_docs.get_document_count() == initial_document_count + 1
+
+        # but we expect initial_document_count documents without embeddings to be unchanged
+        assert doc_store_with_docs.get_document_count(only_documents_without_embedding=True) == initial_document_count
+
+    @pytest.mark.integration
+    def test_get_document_count_after_write_doc_without_embedding(self, doc_store_with_docs: PineconeDocumentStore):
+        """
+        Tests that get_document_count() returns the correct number of documents in the document store after a document
+        without an embedding is written to the document store.
+        """
+        # there are 9 docs in doc_store_with_docs (all without embeddings)
+        initial_document_count = 9
+
+        # we expect initial_document_count documents without embeddings in doc_store_with_docs
+        assert doc_store_with_docs.get_document_count(only_documents_without_embedding=True) == initial_document_count
+        # and we also expect initial_document_count documents in total
+        assert doc_store_with_docs.get_document_count() == initial_document_count
+
+        # document without embedding is written to doc_store_with_docs
+        doc = Document(content=f"Doc without embedding")
+        doc_store_with_docs.write_documents([doc])
+
+        # we now expect initial_document_count + 1 documents in total
+        assert doc_store_with_docs.get_document_count() == initial_document_count + 1
+
+        # And we also expect initial_document_count + 1 documents without embeddings, because the document we just
+        # wrote has no embeddings
+        assert (
+            doc_store_with_docs.get_document_count(only_documents_without_embedding=True) == initial_document_count + 1
+        )
+
+    @pytest.mark.integration
+    def test_get_document_count_after_delete_doc_with_embedding(self, doc_store_with_docs: PineconeDocumentStore):
+        """
+        Tests that get_document_count() returns the correct number of documents in the document store after a document
+        with an embedding is deleted from the document store.
+        """
+        # there are 9 docs in doc_store_with_docs (all without embeddings)
+        initial_document_count = 9
+
+        # we expect initial_document_count documents without embeddings in doc_store_with_docs
+        assert doc_store_with_docs.get_document_count(only_documents_without_embedding=True) == initial_document_count
+        # and also initial_document_count documents in total
+        assert doc_store_with_docs.get_document_count() == initial_document_count
+
+        # two documents with embedding are written to doc_store_with_docs
+        doc_1 = Document(content=f"Doc with embedding 1", embedding=np.random.rand(768).astype(np.float32))
+        doc_2 = Document(content=f"Doc with embedding 2", embedding=np.random.rand(768).astype(np.float32))
+        doc_store_with_docs.write_documents([doc_1, doc_2])
+
+        # total number is initial_document_count + 2
+        assert doc_store_with_docs.get_document_count() == initial_document_count + 2
+
+        # remove one of the documents with embedding
+        all_embedding_docs = doc_store_with_docs.get_all_documents(namespace="vectors")
+        doc_store_with_docs.delete_documents(ids=[all_embedding_docs[0].id])
+
+        # since we deleted one doc, we expect initial_document_count + 1 documents in total
+        assert doc_store_with_docs.get_document_count() == initial_document_count + 1
+
+        # and we expect initial_document_count documents without embeddings
+        assert doc_store_with_docs.get_document_count(only_documents_without_embedding=True) == initial_document_count
+
+    @pytest.mark.integration
+    def test_get_document_count_after_delete_doc_without_embedding(self, doc_store_with_docs: PineconeDocumentStore):
+        """
+        Tests that get_document_count() returns the correct number of documents in the document store after a document
+        without embedding is deleted from the document store.
+        """
+        # there are 9 docs in doc_store_with_docs (all without embeddings)
+        initial_document_count = 9
+
+        # therefore we expect initial_document_count documents without embeddings in doc_store_with_docs
+        assert doc_store_with_docs.get_document_count(only_documents_without_embedding=True) == initial_document_count
+        # and also initial_document_count documents in total
+        assert doc_store_with_docs.get_document_count() == initial_document_count
+
+        # two documents without embedding are written to doc_store_with_docs
+        doc_1 = Document(content=f"Doc with embedding 1", embedding=None)
+        doc_2 = Document(content=f"Doc with embedding 2", embedding=None)
+        doc_store_with_docs.write_documents([doc_1, doc_2])
+
+        # total number is initial_document_count + 2
+        assert doc_store_with_docs.get_document_count() == initial_document_count + 2
+
+        # remove one of the documents without embedding
+        all_non_embedding_docs = doc_store_with_docs.get_all_documents(namespace="no-vectors")
+        doc_store_with_docs.delete_documents(ids=[all_non_embedding_docs[0].id])
+
+        # since we deleted one doc, we expect initial_document_count + 1 documents in total
+        assert doc_store_with_docs.get_document_count() == initial_document_count + 1
+
+        # and we expect initial_document_count +1 documents without embeddings as well
+        assert (
+            doc_store_with_docs.get_document_count(only_documents_without_embedding=True) == initial_document_count + 1
+        )
+
     @pytest.mark.unit
     def test_get_all_labels_legacy_document_id(self, ds, monkeypatch):
         monkeypatch.setattr(