Repository: deepset-ai/haystack

Issue:
Make version of pipeline config/YAML configurable
**Is your feature request related to a problem? Please describe.**
If we call `pipeline.save_to_yaml()` a YAML is stored having a version attribute that takes the fixed value `0.8`.
If I get the purpose of the version attribute correctly, this should be made configurable as it depicts the version of the pipeline config.

**Describe the solution you'd like**
- add `version` param to `save_to_yaml`
- set default value of `version` to `1.0`


Discussion:
Versioning of the YAML files right now is just a stub. 

We also met with this topic in #2020 and informally with @tholor decided to postpone this issue a bit, until we decide which versioning structure we want for the YAML files. There are still some open questions, for example: the YAML version should match Haystack version, or not? Should the version number be stored into the YAML itself or in the filename, or either? etc...

I suggest adding this issue in the "Refinement" list and gather some more ideas before proceeding with any implementation.

Resolving patch:
diff --git a/.github/utils/generate_json_schema.py b/.github/utils/generate_json_schema.py
--- a/.github/utils/generate_json_schema.py
+++ b/.github/utils/generate_json_schema.py
@@ -4,6 +4,7 @@
 from pathlib import Path
 from typing import Any, Dict, Optional, Set, Tuple
 
+from haystack import __version__
 import haystack.document_stores
 import haystack.nodes
 import pydantic.schema
@@ -15,8 +16,8 @@
 from pydantic.typing import is_callable_type
 from pydantic.utils import lenient_issubclass
 
-schema_version = "0.7"
-filename = f"haystack-pipeline.{schema_version}.schema.json"
+schema_version = __version__
+filename = f"haystack-pipeline-{schema_version}.schema.json"
 destination_path = Path(__file__).parent.parent.parent / "json-schemas" / filename
 
 
diff --git a/haystack/pipelines/base.py b/haystack/pipelines/base.py
--- a/haystack/pipelines/base.py
+++ b/haystack/pipelines/base.py
@@ -29,6 +29,7 @@
     ray = None  # type: ignore
     serve = None  # type: ignore
 
+from haystack import __version__
 from haystack.schema import EvaluationResult, MultiLabel, Document
 from haystack.nodes.base import BaseComponent
 from haystack.document_stores.base import BaseDocumentStore
@@ -81,7 +82,7 @@ def load_from_config(
 
             ```python
             |   {
-            |       "version": "0.9",
+            |       "version": "1.0",
             |       "components": [
             |           {  # define all the building-blocks for Pipeline
             |               "name": "MyReader",  # custom-name for the component; helpful for visualization & debugging
@@ -146,7 +147,7 @@ def load_from_yaml(cls, path: Path, pipeline_name: Optional[str] = None, overwri
         Here's a sample configuration:
 
             ```yaml
-            |   version: '0.9'
+            |   version: '1.0'
             |
             |    components:    # define all the building-blocks for Pipeline
             |    - name: MyReader       # custom-name for the component; helpful for visualization & debugging
@@ -173,6 +174,9 @@ def load_from_yaml(cls, path: Path, pipeline_name: Optional[str] = None, overwri
             |        inputs: [MyESRetriever]
             ```
 
+        Note that, in case of a mismatch in version between Haystack and the YAML, a warning will be printed.
+        If the pipeline loads correctly regardless, save again the pipeline using `Pipeline.save_to_yaml()` to remove the warning.
+
         :param path: path of the YAML file.
         :param pipeline_name: if the YAML contains multiple pipelines, the pipeline_name to load must be set.
         :param overwrite_with_env_variables: Overwrite the YAML configuration with environment variables. For example,
@@ -182,6 +186,14 @@ def load_from_yaml(cls, path: Path, pipeline_name: Optional[str] = None, overwri
         """
 
         pipeline_config = cls._read_pipeline_config_from_yaml(path)
+        if pipeline_config["version"] != __version__:
+            logger.warning(
+                f"YAML version ({pipeline_config['version']}) does not match with Haystack version ({__version__}). "
+                "Issues may occur during loading. "
+                "To fix this warning, save again this pipeline with the current Haystack version using Pipeline.save_to_yaml(), "
+                "check out our migration guide at https://haystack.deepset.ai/overview/migration "
+                f"or downgrade to haystack version {__version__}."
+            )
         return cls.load_from_config(
             pipeline_config=pipeline_config,
             pipeline_name=pipeline_name,
@@ -1037,7 +1049,11 @@ def get_config(self, return_defaults: bool = False) -> dict:
             # create the Pipeline definition with how the Component are connected
             pipelines[pipeline_name]["nodes"].append({"name": node, "inputs": list(self.graph.predecessors(node))})
 
-        config = {"components": list(components.values()), "pipelines": list(pipelines.values()), "version": "0.8"}
+        config = {
+            "components": list(components.values()),
+            "pipelines": list(pipelines.values()),
+            "version": __version__,
+        }
         return config
 
     def _format_document_answer(self, document_or_answer: dict):
@@ -1290,6 +1306,10 @@ def load_from_yaml(
             |        inputs: [MyESRetriever]
             ```
 
+
+        Note that, in case of a mismatch in version between Haystack and the YAML, a warning will be printed.
+        If the pipeline loads correctly regardless, save again the pipeline using `RayPipeline.save_to_yaml()` to remove the warning.
+
         :param path: path of the YAML file.
         :param pipeline_name: if the YAML contains multiple pipelines, the pipeline_name to load must be set.
         :param overwrite_with_env_variables: Overwrite the YAML configuration with environment variables. For example,
@@ -1299,6 +1319,14 @@ def load_from_yaml(
         :param address: The IP address for the Ray cluster. If set to None, a local Ray instance is started.
         """
         pipeline_config = cls._read_pipeline_config_from_yaml(path)
+        if pipeline_config["version"] != __version__:
+            logger.warning(
+                f"YAML version ({pipeline_config['version']}) does not match with Haystack version ({__version__}). "
+                "Issues may occur during loading. "
+                "To fix this warning, save again this pipeline with the current Haystack version using Pipeline.save_to_yaml(), "
+                "check out our migration guide at https://haystack.deepset.ai/overview/migration "
+                f"or downgrade to haystack version {__version__}."
+            )
         return RayPipeline.load_from_config(
             pipeline_config=pipeline_config,
             pipeline_name=pipeline_name,


Test patch:
diff --git a/rest_api/test/samples/pipeline/test_pipeline.yaml b/rest_api/test/samples/pipeline/test_pipeline.yaml
--- a/rest_api/test/samples/pipeline/test_pipeline.yaml
+++ b/rest_api/test/samples/pipeline/test_pipeline.yaml
@@ -1,4 +1,4 @@
-version: '0.7'
+version: '1.1.0'
 
 components:
   - name: Reader
diff --git a/rest_api/test/samples/pipeline/test_pipeline_faiss_indexing.yaml b/rest_api/test/samples/pipeline/test_pipeline_faiss_indexing.yaml
--- a/rest_api/test/samples/pipeline/test_pipeline_faiss_indexing.yaml
+++ b/rest_api/test/samples/pipeline/test_pipeline_faiss_indexing.yaml
@@ -1,4 +1,4 @@
-version: '0.7'
+version: '1.1.0'
 
 components:
   - name: DPRRetriever
diff --git a/rest_api/test/samples/pipeline/test_pipeline_faiss_retrieval.yaml b/rest_api/test/samples/pipeline/test_pipeline_faiss_retrieval.yaml
--- a/rest_api/test/samples/pipeline/test_pipeline_faiss_retrieval.yaml
+++ b/rest_api/test/samples/pipeline/test_pipeline_faiss_retrieval.yaml
@@ -1,4 +1,4 @@
-version: '0.7'
+version: '1.1.0'
 
 components:
   - name: DPRRetriever
diff --git a/rest_api/test/samples/pipeline/test_pipeline_tfidfretriever.yaml b/rest_api/test/samples/pipeline/test_pipeline_tfidfretriever.yaml
--- a/rest_api/test/samples/pipeline/test_pipeline_tfidfretriever.yaml
+++ b/rest_api/test/samples/pipeline/test_pipeline_tfidfretriever.yaml
@@ -1,4 +1,4 @@
-version: '0.9'
+version: '1.1.0'
 
 components:
   - name: Reader
diff --git a/test/samples/pipeline/test_pipeline.yaml b/test/samples/pipeline/test_pipeline.yaml
--- a/test/samples/pipeline/test_pipeline.yaml
+++ b/test/samples/pipeline/test_pipeline.yaml
@@ -1,4 +1,4 @@
-version: '0.7'
+version: '1.1.0'
 
 components:
   - name: Reader
diff --git a/test/samples/pipeline/test_pipeline_faiss_indexing.yaml b/test/samples/pipeline/test_pipeline_faiss_indexing.yaml
--- a/test/samples/pipeline/test_pipeline_faiss_indexing.yaml
+++ b/test/samples/pipeline/test_pipeline_faiss_indexing.yaml
@@ -1,4 +1,4 @@
-version: '0.7'
+version: '1.1.0'
 
 components:
   - name: DPRRetriever
diff --git a/test/samples/pipeline/test_pipeline_faiss_retrieval.yaml b/test/samples/pipeline/test_pipeline_faiss_retrieval.yaml
--- a/test/samples/pipeline/test_pipeline_faiss_retrieval.yaml
+++ b/test/samples/pipeline/test_pipeline_faiss_retrieval.yaml
@@ -1,4 +1,4 @@
-version: '0.7'
+version: '1.1.0'
 
 components:
   - name: DPRRetriever
diff --git a/test/samples/pipeline/test_pipeline_tfidfretriever.yaml b/test/samples/pipeline/test_pipeline_tfidfretriever.yaml
--- a/test/samples/pipeline/test_pipeline_tfidfretriever.yaml
+++ b/test/samples/pipeline/test_pipeline_tfidfretriever.yaml
@@ -1,4 +1,4 @@
-version: '0.9'
+version: '1.1.0'
 
 components:
   - name: Reader
diff --git a/test/test_pipeline.py b/test/test_pipeline.py
--- a/test/test_pipeline.py
+++ b/test/test_pipeline.py
@@ -5,8 +5,9 @@
 from unittest.mock import Mock
 import pytest
 import responses
-from haystack.document_stores.deepsetcloud import DeepsetCloudDocumentStore
 
+from haystack import __version__
+from haystack.document_stores.deepsetcloud import DeepsetCloudDocumentStore
 from haystack.document_stores.elasticsearch import ElasticsearchDocumentStore
 from haystack.nodes.retriever.sparse import ElasticsearchRetriever
 from haystack.pipelines import (
@@ -44,7 +45,7 @@ def test_load_and_save_yaml(document_store, tmp_path):
     pipeline.save_to_yaml(tmp_path / "test.yaml")
     with open(tmp_path / "test.yaml", "r", encoding="utf-8") as stream:
         saved_yaml = stream.read()
-    expected_yaml = """
+    expected_yaml = f"""
         components:
         - name: ESRetriever
           params:
@@ -71,7 +72,7 @@ def test_load_and_save_yaml(document_store, tmp_path):
             - ESRetriever
             name: Reader
           type: Pipeline
-        version: '0.8'
+        version: {__version__}
     """
     assert saved_yaml.replace(" ", "").replace("\n", "") == expected_yaml.replace(" ", "").replace("\n", "")
 
@@ -104,7 +105,7 @@ def test_load_and_save_yaml_prebuilt_pipelines(document_store, tmp_path):
     pipeline.save_to_yaml(tmp_path / "test.yaml")
     with open(tmp_path / "test.yaml", "r", encoding="utf-8") as stream:
         saved_yaml = stream.read()
-    expected_yaml = """
+    expected_yaml = f"""
         components:
         - name: ESRetriever
           params:
@@ -131,7 +132,7 @@ def test_load_and_save_yaml_prebuilt_pipelines(document_store, tmp_path):
             - ESRetriever
             name: Reader
           type: Pipeline
-        version: '0.8'
+        version: {__version__}
     """
     assert saved_yaml.replace(" ", "").replace("\n", "") == expected_yaml.replace(" ", "").replace("\n", "")