Repository: deepset-ai/haystack

Issue:
Reduce the number of raised PipelineConfigErrors
The top 3 most frequent errors raised are:
```
Failed loading pipeline component 'Retriever'. " "See the stacktrace above for more informations.
Failed loading pipeline component 'DocumentStore'. " "See the stacktrace above for more informations.
Failed loading pipeline component 'Reader'. " "See the stacktrace above for more informations.
```

This error message is triggered here: https://github.com/deepset-ai/haystack/blob/057a8c0b4f22ed858f2ac0a41a4f0dd2efa528e0/haystack/pipelines/base.py#L2005
which means that Haystack tries to load a component from the definition or just return it if the component object is already present in `components` dict. While Haystack tries to do that an Exception is raised that is not a `KeyError` (caught with error message `... Did you spell its name correctly?` 

My first assumption was that users are trying to load a pipeline configuration that contains components for which the user has not installed the dependencies. For example, the pipeline config uses WeaviateDocumentStore but the dependency hasn't been installed. 

I found out that if elasticsearch is installed but no instance is running, the `Failed loading pipeline component 'Retriever'. " "See the stacktrace above for more informations.` PipelineConfigError is raised. We should show a different error message in that case. It's actually a DocumentStoreError and not a PipelineConfigError I would argue.

**Expected behavior**
We would like to reduce the number of those errors. We might want to change the error message to better tell the user how to fix the pipeline configuration. For that purpose we might want to distinguish multiple different errors.


Discussion:

Resolving patch:
diff --git a/haystack/pipelines/base.py b/haystack/pipelines/base.py
--- a/haystack/pipelines/base.py
+++ b/haystack/pipelines/base.py
@@ -46,7 +46,7 @@
 from haystack.pipelines.utils import generate_code, print_eval_report
 from haystack.utils import DeepsetCloud, calculate_context_similarity
 from haystack.schema import Answer, EvaluationResult, MultiLabel, Document, Span
-from haystack.errors import HaystackError, PipelineError, PipelineConfigError
+from haystack.errors import HaystackError, PipelineError, PipelineConfigError, DocumentStoreError
 from haystack.nodes import BaseGenerator, Docs2Answers, BaseReader, BaseSummarizer, BaseTranslator, QuestionGenerator
 from haystack.nodes.base import BaseComponent, RootNode
 from haystack.nodes.retriever.base import BaseRetriever
@@ -2022,9 +2022,13 @@ def _load_or_get_component(cls, name: str, definitions: dict, components: dict):
                 f"Failed loading pipeline component '{name}': "
                 "seems like the component does not exist. Did you spell its name correctly?"
             ) from ke
+        except ConnectionError as ce:
+            raise DocumentStoreError(f"Failed loading pipeline component '{name}': '{ce}'") from ce
+        except DocumentStoreError as de:
+            raise de
         except Exception as e:
             raise PipelineConfigError(
-                f"Failed loading pipeline component '{name}'. " "See the stacktrace above for more informations."
+                f"Failed loading pipeline component '{name}'. See the stacktrace above for more information."
             ) from e
 
     def save_to_yaml(self, path: Path, return_defaults: bool = False):
@@ -2152,7 +2156,7 @@ def _validate_node_names_in_params(self, params: Optional[Dict]):
                 not_a_node = set(params.keys()) - set(self.graph.nodes)
                 # "debug" will be picked up by _dispatch_run, see its code
                 # "add_isolated_node_eval" is set by pipeline.eval / pipeline.eval_batch
-                valid_global_params = set(["debug", "add_isolated_node_eval"])
+                valid_global_params = {"debug", "add_isolated_node_eval"}
                 for node_id in self.graph.nodes:
                     run_signature_args = self._get_run_node_signature(node_id)
                     valid_global_params |= set(run_signature_args)


Test patch:
diff --git a/test/pipelines/test_pipeline_yaml.py b/test/pipelines/test_pipeline_yaml.py
--- a/test/pipelines/test_pipeline_yaml.py
+++ b/test/pipelines/test_pipeline_yaml.py
@@ -14,7 +14,7 @@
 from haystack import Pipeline
 from haystack.nodes import _json_schema
 from haystack.nodes import FileTypeClassifier
-from haystack.errors import HaystackError, PipelineConfigError, PipelineSchemaError
+from haystack.errors import HaystackError, PipelineConfigError, PipelineSchemaError, DocumentStoreError
 from haystack.nodes.base import BaseComponent
 
 from ..conftest import SAMPLES_PATH, MockNode, MockDocumentStore, MockReader, MockRetriever
@@ -141,6 +141,46 @@ def test_load_yaml(tmp_path):
     assert isinstance(pipeline.get_node("reader"), MockReader)
 
 
+def test_load_yaml_elasticsearch_not_responding(tmp_path):
+    # Test if DocumentStoreError is raised if elasticsearch instance is not responding (due to wrong port)
+    with open(tmp_path / "tmp_config.yml", "w") as tmp_file:
+        tmp_file.write(
+            f"""
+            version: ignore
+            components:
+            - name: ESRetriever
+              type: BM25Retriever
+              params:
+                document_store: DocumentStore
+            - name: DocumentStore
+              type: ElasticsearchDocumentStore
+              params:
+                port: 1234
+            - name: PDFConverter
+              type: PDFToTextConverter
+            - name: Preprocessor
+              type: PreProcessor
+            pipelines:
+            - name: query_pipeline
+              nodes:
+              - name: ESRetriever
+                inputs: [Query]
+            - name: indexing_pipeline
+              nodes:
+              - name: PDFConverter
+                inputs: [File]
+              - name: Preprocessor
+                inputs: [PDFConverter]
+              - name: ESRetriever
+                inputs: [Preprocessor]
+              - name: DocumentStore
+                inputs: [ESRetriever]
+        """
+        )
+    with pytest.raises(DocumentStoreError):
+        Pipeline.load_from_yaml(path=tmp_path / "tmp_config.yml", pipeline_name="indexing_pipeline")
+
+
 def test_load_yaml_non_existing_file():
     with pytest.raises(FileNotFoundError):
         Pipeline.load_from_yaml(path=SAMPLES_PATH / "pipeline" / "I_dont_exist.yml")