Repository: CS-SI/eodag
Pull request #372 (2021-12-08 07:48:31)

feat: keywords usage in product_types configuration
- new field `keywords` added in product types configuration file `product_types.yml`. Field is case insensitive and ignores `-` or `_` characters
- `whoosh` product types search index is now rebuilt when `product_types.yml` MD5 checksum changes instead of eodag version.

This enables the following behavior:
```py
>>> from eodag import EODataAccessGateway
>>> dag = EODataAccessGateway()
>>> dag.guess_product_type(keywords="sentinel* SAFE")
['S1_SAR_GRD', 'S1_SAR_OCN', 'S1_SAR_RAW', 'S1_SAR_SLC', 'S2_MSI_L1C', 'S2_MSI_L2A']
>>> dag.guess_product_type(keywords="landsat* Collection2")
['LANDSAT_C2L1', 'LANDSAT_C2L2ALB_BT', 'LANDSAT_C2L2ALB_SR', 'LANDSAT_C2L2ALB_ST', 'LANDSAT_C2L2ALB_TA', 'LANDSAT_C2L2_SR', 'LANDSAT_C2L2_ST']
>>> dag.guess_product_type(keywords="landsat* Collection2 L1")
['LANDSAT_C2L1']
>>> dag.search(keywords="landsat* Collection2 L1")
# will perform search on LANDSAT_C2L1
>>> dag.guess_product_type(keywords="cog")
['S2_MSI_L2A_COG']
>>> dag.search(keywords="cog")
# will perform search on S2_MSI_L2A_COG
```


Patch:
diff --git a/docs/api_reference/core.rst b/docs/api_reference/core.rst
index 4d702bfc..d536b768 100644
--- a/docs/api_reference/core.rst
+++ b/docs/api_reference/core.rst
@@ -24,8 +24,9 @@ Catalog
 
 .. autosummary::
 
-   EODataAccessGateway.list_product_types
    EODataAccessGateway.available_providers
+   EODataAccessGateway.list_product_types
+   EODataAccessGateway.guess_product_type
 
 Search
 ------
@@ -42,6 +43,7 @@ Crunch
 .. autosummary::
 
    EODataAccessGateway.crunch
+   EODataAccessGateway.get_cruncher
 
 Download
 --------
@@ -78,4 +80,4 @@ Misc
 .. autoclass:: eodag.api.core.EODataAccessGateway
    :members: set_preferred_provider, get_preferred_provider, update_providers_config, list_product_types, available_providers,
              search, search_all, search_iter_page, crunch, download, download_all, serialize, deserialize, deserialize_and_register,
-             load_stac_items, group_by_extent
+             load_stac_items, group_by_extent, guess_product_type, get_cruncher
diff --git a/docs/notebooks/api_user_guide/4_search.ipynb b/docs/notebooks/api_user_guide/4_search.ipynb
index 4a69cf8f..5f50a5c5 100644
--- a/docs/notebooks/api_user_guide/4_search.ipynb
+++ b/docs/notebooks/api_user_guide/4_search.ipynb
@@ -1430,25 +1430,71 @@
     "* `platform` (e.g. *SENTINEL2*)\n",
     "* `platformSerialIdentifier` (e.g. *S2A*)\n",
     "* `processingLevel` (e.g. *L1*)\n",
-    "* `sensorType` (e.g. *OPTICAL*)"
+    "* `sensorType` (e.g. *OPTICAL*)\n",
+    "* `keywords` (e.g. *SENTINEL2 L1C SAFE*), which is case insensitive and ignores `-` or `_` characters"
    ],
    "cell_type": "markdown",
    "metadata": {}
   },
   {
+   "cell_type": "code",
+   "execution_count": 43,
+   "metadata": {},
+   "outputs": [
+    {
+     "data": {
+      "text/plain": [
+       "['S1_SAR_GRD', 'S1_SAR_OCN', 'S1_SAR_RAW', 'S1_SAR_SLC']"
+      ]
+     },
+     "execution_count": 43,
+     "metadata": {},
+     "output_type": "execute_result"
+    }
+   ],
    "source": [
-    "A search is made without specifying `productType`, however hints are passed with following supported kwargs."
+    "dag.guess_product_type(platform=\"SENTINEL1\")"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 44,
+   "metadata": {},
+   "outputs": [
+    {
+     "data": {
+      "text/plain": [
+       "['LANDSAT_C2L1',\n",
+       " 'LANDSAT_C2L2ALB_BT',\n",
+       " 'LANDSAT_C2L2ALB_SR',\n",
+       " 'LANDSAT_C2L2ALB_ST',\n",
+       " 'LANDSAT_C2L2ALB_TA',\n",
+       " 'LANDSAT_C2L2_SR',\n",
+       " 'LANDSAT_C2L2_ST']"
+      ]
+     },
+     "execution_count": 44,
+     "metadata": {},
+     "output_type": "execute_result"
+    }
    ],
+   "source": [
+    "dag.guess_product_type(keywords=\"LANDSAT* collection2\")"
+   ]
+  },
+  {
    "cell_type": "markdown",
-   "metadata": {}
+   "metadata": {},
+   "source": [
+    "A search is made without specifying `productType`, however hints are passed with following supported kwargs. `guess_product_type()` will internally be used to get the best matching product type."
+   ]
   },
   {
    "cell_type": "code",
-   "execution_count": 180,
+   "execution_count": 45,
    "metadata": {},
    "outputs": [
     {
-     "output_type": "execute_result",
      "data": {
       "text/plain": [
        "{'start': '2021-03-01',\n",
@@ -1460,8 +1506,9 @@
        " 'sensorType': 'OPTICAL'}"
       ]
      },
+     "execution_count": 45,
      "metadata": {},
-     "execution_count": 180
+     "output_type": "execute_result"
     }
    ],
    "source": [
@@ -1478,17 +1525,37 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 181,
+   "execution_count": 46,
+   "metadata": {},
+   "outputs": [
+    {
+     "data": {
+      "text/plain": [
+       "'S2_MSI_L1C'"
+      ]
+     },
+     "execution_count": 46,
+     "metadata": {},
+     "output_type": "execute_result"
+    }
+   ],
+   "source": [
+    "dag.guess_product_type(**guess_search_criteria)[0]"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 47,
    "metadata": {},
    "outputs": [
     {
-     "output_type": "stream",
      "name": "stderr",
+     "output_type": "stream",
      "text": [
-      "2021-04-12 22:22:36,927-15s eodag.core                       [INFO    ] Searching product type 'S2_MSI_L1C' on provider: peps\n",
-      "2021-04-12 22:22:36,929-15s eodag.plugins.search.qssearch    [INFO    ] Sending count request: https://peps.cnes.fr/resto/api/collections/S2ST/search.json?instrument=MSI&platform=S2A&sensorType=OPTICAL&startDate=2021-03-01&completionDate=2021-03-31&geometry=POLYGON ((1.0000 43.0000, 1.0000 44.0000, 2.0000 44.0000, 2.0000 43.0000, 1.0000 43.0000))&productType=S2MSI1C&maxRecords=1&page=1\n",
-      "2021-04-12 22:22:37,629-15s eodag.plugins.search.qssearch    [INFO    ] Sending search request: https://peps.cnes.fr/resto/api/collections/S2ST/search.json?instrument=MSI&platform=S2A&sensorType=OPTICAL&startDate=2021-03-01&completionDate=2021-03-31&geometry=POLYGON ((1.0000 43.0000, 1.0000 44.0000, 2.0000 44.0000, 2.0000 43.0000, 1.0000 43.0000))&productType=S2MSI1C&maxRecords=20&page=1\n",
-      "2021-04-12 22:22:41,644-15s eodag.core                       [INFO    ] Found 24 result(s) on provider 'peps'\n"
+      "2021-12-08 15:39:12,779-15s eodag.core                       [INFO    ] Searching product type 'S2_MSI_L1C' on provider: peps\n",
+      "2021-12-08 15:39:12,782-15s eodag.plugins.search.qssearch    [INFO    ] Sending count request: https://peps.cnes.fr/resto/api/collections/S2ST/search.json?startDate=2021-03-01&completionDate=2021-03-31&geometry=POLYGON ((1.0000 43.0000, 1.0000 44.0000, 2.0000 44.0000, 2.0000 43.0000, 1.0000 43.0000))&productType=S2MSI1C&maxRecords=1&page=1\n",
+      "2021-12-08 15:39:13,409-15s eodag.plugins.search.qssearch    [INFO    ] Sending search request: https://peps.cnes.fr/resto/api/collections/S2ST/search.json?startDate=2021-03-01&completionDate=2021-03-31&geometry=POLYGON ((1.0000 43.0000, 1.0000 44.0000, 2.0000 44.0000, 2.0000 43.0000, 1.0000 43.0000))&productType=S2MSI1C&maxRecords=20&page=1\n",
+      "2021-12-08 15:39:15,426-15s eodag.core                       [INFO    ] Found 48 result(s) on provider 'peps'\n"
      ]
     }
    ],
diff --git a/eodag/api/core.py b/eodag/api/core.py
index 9746a67e..0ce2f7fe 100644
--- a/eodag/api/core.py
+++ b/eodag/api/core.py
@@ -24,7 +24,7 @@ import geojson
 import pkg_resources
 import yaml.parser
 from pkg_resources import resource_filename
-from whoosh import fields
+from whoosh import analysis, fields
 from whoosh.fields import Schema
 from whoosh.index import create_in, exists_in, open_dir
 from whoosh.qparser import QueryParser
@@ -47,6 +47,7 @@ from eodag.utils import (
     _deprecated,
     get_geometry_from_various,
     makedirs,
+    md5sum,
     uri_to_path,
 )
 from eodag.utils.exceptions import (
@@ -78,9 +79,11 @@ class EODataAccessGateway(object):
     """
 
     def __init__(self, user_conf_file_path=None, locations_conf_path=None):
-        self.product_types_config = SimpleYamlProxyConfig(
-            resource_filename("eodag", os.path.join("resources/", "product_types.yml"))
+        product_types_config_path = resource_filename(
+            "eodag", os.path.join("resources/", "product_types.yml")
         )
+        self.product_types_config = SimpleYamlProxyConfig(product_types_config_path)
+        self.product_types_config_md5 = md5sum(product_types_config_path)
         self.providers_config = load_default_config()
 
         self.conf_dir = os.path.join(os.path.expanduser("~"), ".config", "eodag")
@@ -161,9 +164,6 @@ class EODataAccessGateway(object):
         """
         index_dir = os.path.join(self.conf_dir, ".index")
 
-        # use eodag_version to help keeping index up-to-date
-        eodag_version = self.get_version()
-
         try:
             create_index = not exists_in(index_dir)
         except ValueError as ve:
@@ -189,7 +189,7 @@ class EODataAccessGateway(object):
                 logger.debug("Opening product types index in %s", index_dir)
                 self._product_types_index = open_dir(index_dir)
             try:
-                self.guess_product_type(eodagVersion=eodag_version)
+                self.guess_product_type(md5=self.product_types_config_md5)
             except NoMatchingProductType:
                 create_index = True
             finally:
@@ -202,6 +202,14 @@ class EODataAccessGateway(object):
         if create_index:
             logger.debug("Creating product types index in %s", index_dir)
             makedirs(index_dir)
+
+            kw_analyzer = (
+                analysis.CommaSeparatedTokenizer()
+                | analysis.LowercaseFilter()
+                | analysis.SubstitutionFilter("-", "")
+                | analysis.SubstitutionFilter("_", "")
+            )
+
             product_types_schema = Schema(
                 ID=fields.STORED,
                 abstract=fields.TEXT,
@@ -210,18 +218,19 @@ class EODataAccessGateway(object):
                 platformSerialIdentifier=fields.IDLIST,
                 processingLevel=fields.ID,
                 sensorType=fields.ID,
-                eodagVersion=fields.ID,
+                md5=fields.ID,
                 license=fields.ID,
                 title=fields.ID,
                 missionStartDate=fields.ID,
                 missionEndDate=fields.ID,
+                keywords=fields.KEYWORD(analyzer=kw_analyzer),
             )
             non_indexable_fields = []
             self._product_types_index = create_in(index_dir, product_types_schema)
             ix_writer = self._product_types_index.writer()
             for product_type in self.list_product_types():
                 versioned_product_type = dict(
-                    product_type, **{"eodagVersion": eodag_version}
+                    product_type, **{"md5": self.product_types_config_md5}
                 )
                 # add to index
                 ix_writer.add_document(
@@ -438,7 +447,8 @@ class EODataAccessGateway(object):
                 "platformSerialIdentifier",
                 "processingLevel",
                 "sensorType",
-                "eodagVersion",
+                "keywords",
+                "md5",
             )
             if kwargs.get(param, None) is not None
         }
diff --git a/eodag/resources/product_types.yml b/eodag/resources/product_types.yml
index 3622d947..394b835a 100644
--- a/eodag/resources/product_types.yml
+++ b/eodag/resources/product_types.yml
@@ -19,12 +19,13 @@
 # CBERS 4 ---------------------------------------------------------------------
 CBERS4_MUX_L2:
   abstract: |
-    CBERS-4 MUX camera Level-2 product. System corrected images, expect some
+    China-Brazil Earth Resources Satellite, CBERS-4 MUX camera Level-2 product. System corrected images, expect some
     translation error.
   instrument: MUX
-  platform: China-Brazil Earth Resources Satellite
+  platform: CBERS
   platformSerialIdentifier: CBERS-4
   processingLevel: L2
+  keywords: MUX,CBERS,CBERS-4,L2
   sensorType: OPTICAL
   license: proprietary
   missionStartDate: "2014-12-07T00:00:00Z"
@@ -32,12 +33,13 @@ CBERS4_MUX_L2:
 
 CBERS4_AWFI_L2:
   abstract: |
-    CBERS-4 AWFI camera Level-2 product. System corrected images, expect some
+    China-Brazil Earth Resources Satellite, CBERS-4 AWFI camera Level-2 product. System corrected images, expect some
     translation error.
   instrument: AWFI
-  platform: China-Brazil Earth Resources Satellite
+  platform: CBERS
   platformSerialIdentifier: CBERS-4
   processingLevel: L2
+  keywords: AWFI,CBERS,CBERS-4,L2
   sensorType: OPTICAL
   license: proprietary
   missionStartDate: "2014-12-07T00:00:00Z"
@@ -45,12 +47,13 @@ CBERS4_AWFI_L2:
 
 CBERS4_PAN5M_L2:
   abstract: |
-    CBERS-4 PAN5M camera Level-2 product. System corrected images, expect some
+    China-Brazil Earth Resources Satellite, CBERS-4 PAN5M camera Level-2 product. System corrected images, expect some
     translation error.
   instrument: PAN5M
-  platform: China-Brazil Earth Resources Satellite
+  platform: CBERS
   platformSerialIdentifier: CBERS-4
   processingLevel: L2
+  keywords: PAN5M,CBERS,CBERS-4,L2
   sensorType: OPTICAL
   license: proprietary
   missionStartDate: "2014-12-07T00:00:00Z"
@@ -58,12 +61,13 @@ CBERS4_PAN5M_L2:
 
 CBERS4_PAN10M_L2:
   abstract: |
-    CBERS-4 PAN10M camera Level-2 product. System corrected images, expect some
+    China-Brazil Earth Resources Satellite, CBERS-4 PAN10M camera Level-2 product. System corrected images, expect some
     translation error.
   instrument: PAN10M
-  platform: China-Brazil Earth Resources Satellite
+  platform: CBERS
   platformSerialIdentifier: CBERS-4
   processingLevel: L2
+  keywords: PAN10M,CBERS,CBERS-4,L2
   sensorType: OPTICAL
   license: proprietary
   missionStartDate: "2014-12-07T00:00:00Z"
@@ -71,12 +75,13 @@ CBERS4_PAN10M_L2:
 
 CBERS4_MUX_L4:
   abstract: |
-    CBERS-4 MUX camera Level-4 product. Orthorectified with ground control
+    China-Brazil Earth Resources Satellite, CBERS-4 MUX camera Level-4 product. Orthorectified with ground control
     points.
   instrument: MUX
-  platform: China-Brazil Earth Resources Satellite
+  platform: CBERS
   platformSerialIdentifier: CBERS-4
   processingLevel: L4
+  keywords: MUX,CBERS,CBERS-4,L4
   sensorType: OPTICAL
   license: proprietary
   missionStartDate: "2014-12-07T00:00:00Z"
@@ -84,12 +89,13 @@ CBERS4_MUX_L4:
 
 CBERS4_AWFI_L4:
   abstract: |
-    CBERS-4 AWFI camera Level-4 product. Orthorectified with ground control
+    China-Brazil Earth Resources Satellite, CBERS-4 AWFI camera Level-4 product. Orthorectified with ground control
     points.
   instrument: AWFI
-  platform: China-Brazil Earth Resources Satellite
+  platform: CBERS
   platformSerialIdentifier: CBERS-4
   processingLevel: L4
+  keywords: AWFI,CBERS,CBERS-4,L4
   sensorType: OPTICAL
   license: proprietary
   missionStartDate: "2014-12-07T00:00:00Z"
@@ -97,12 +103,13 @@ CBERS4_AWFI_L4:
 
 CBERS4_PAN5M_L4:
   abstract: |
-    CBERS-4 PAN5M camera Level-4 product. Orthorectified with ground control
+    China-Brazil Earth Resources Satellite, CBERS-4 PAN5M camera Level-4 product. Orthorectified with ground control
     points.
   instrument: PAN5M
-  platform: China-Brazil Earth Resources Satellite
+  platform: CBERS
   platformSerialIdentifier: CBERS-4
   processingLevel: L4
+  keywords: PAN5M,CBERS,CBERS-4,L4
   sensorType: OPTICAL
   license: proprietary
   missionStartDate: "2014-12-07T00:00:00Z"
@@ -110,12 +117,13 @@ CBERS4_PAN5M_L4:
 
 CBERS4_PAN10M_L4:
   abstract: |
-    CBERS-4 PAN10M camera Level-4 product. Orthorectified with ground control
+    China-Brazil Earth Resources Satellite, CBERS-4 PAN10M camera Level-4 product. Orthorectified with ground control
     points.
   instrument: PAN10M
-  platform: China-Brazil Earth Resources Satellite
+  platform: CBERS
   platformSerialIdentifier: CBERS-4
   processingLevel: L4
+  keywords: PAN10M,CBERS,CBERS-4,L4
   sensorType: OPTICAL
   license: proprietary
   missionStartDate: "2014-12-07T00:00:00Z"
@@ -131,6 +139,7 @@ L57_REFLECTANCE:
   platform: LANDSAT
   platformSerialIdentifier: L5,L7,L8
   processingLevel: L2A
+  keywords: OLI,TIRS,LANDSAT,L5,L7,L8,L2,L2A,MUSCATE
   sensorType: OPTICAL
   license: proprietary
   missionStartDate: "2014-01-01T00:00:00Z"
@@ -145,6 +154,7 @@ L8_REFLECTANCE:
   platform: LANDSAT8
   platformSerialIdentifier: L8
   processingLevel: L2A
+  keywords: OLI,TIRS,LANDSAT,LANDSAT8,L8,L2,L2A,MUSCATE
   sensorType: OPTICAL
   license: proprietary
   missionStartDate: "2013-02-11T00:00:00Z"
@@ -158,6 +168,7 @@ L8_OLI_TIRS_C1L1:
   platform: LANDSAT8
   platformSerialIdentifier: L8
   processingLevel: L1
+  keywords: OLI,TIRS,LANDSAT,LANDSAT8,L8,L1,C1,COLLECTION1
   sensorType: OPTICAL
   license: proprietary
   missionStartDate: "2013-02-11T00:00:00Z"
@@ -170,6 +181,7 @@ LANDSAT_C2L1:
   platform: LANDSAT
   platformSerialIdentifier: L1,L2,L3,L4,L5,L6,L7,L8
   processingLevel: L1
+  keywords: OLI,TIRS,LANDSAT,L1,L2,L3,L4,L5,L6,L7,L8,C2,COLLECTION2
   sensorType: OPTICAL
   license: proprietary
   title: Landsat Collection 2 Level-1 Product
@@ -183,6 +195,7 @@ LANDSAT_C2L2_SR:
   platform: LANDSAT
   platformSerialIdentifier: L4,L5,L7,L8
   processingLevel: L2
+  keywords: OLI,TIRS,LANDSAT,L4,L5,L7,L8,L2,SR,surface,reflectance,C2,COLLECTION2
   sensorType: OPTICAL
   license: proprietary
   title: Landsat Collection 2 Level-2 UTM Surface Reflectance (SR) Product
@@ -195,6 +208,7 @@ LANDSAT_C2L2_ST:
   platform: LANDSAT
   platformSerialIdentifier: L4,L5,L7,L8
   processingLevel: L2
+  keywords: OLI,TIRS,LANDSAT,L4,L5,L7,L8,L2,ST,surface,temperature,C2,COLLECTION2
   sensorType: OPTICAL
   license: proprietary
   title: Landsat Collection 2 Level-2 UTM Surface Temperature (ST) Product
@@ -208,6 +222,7 @@ LANDSAT_C2L2ALB_BT:
   platform: LANDSAT
   platformSerialIdentifier: L4,L5,L7,L8
   processingLevel: L2
+  keywords: OLI,TIRS,LANDSAT,L4,L5,L7,L8,L2,BT,Brightness,Temperature,C2,COLLECTION2
   sensorType: OPTICAL
   license: proprietary
   title: Landsat Collection 2 Level-2 Albers Top of Atmosphere Brightness Temperature (BT) Product
@@ -221,6 +236,7 @@ LANDSAT_C2L2ALB_SR:
   platform: LANDSAT
   platformSerialIdentifier: L4,L5,L7,L8
   processingLevel: L2
+  keywords: OLI,TIRS,LANDSAT,L4,L5,L7,L8,L2,L2ALB,SR,Surface,Reflectance,C2,COLLECTION2
   sensorType: OPTICAL
   license: proprietary
   title: Landsat Collection 2 Level-2 Albers Surface Reflectance (SR) Product
@@ -233,6 +249,7 @@ LANDSAT_C2L2ALB_ST:
   platform: LANDSAT
   platformSerialIdentifier: L4,L5,L7,L8
   processingLevel: L2
+  keywords: OLI,TIRS,LANDSAT,L4,L5,L7,L8,L2,L2ALB,Surface,Temperature,ST,C2,COLLECTION2
   sensorType: OPTICAL
   license: proprietary
   title: Landsat Collection 2 Level-2 Albers Surface Temperature (ST) Product
@@ -246,6 +263,7 @@ LANDSAT_C2L2ALB_TA:
   platform: LANDSAT
   platformSerialIdentifier: L4,L5,L7,L8
   processingLevel: L2
+  keywords: OLI,TIRS,LANDSAT,L4,L5,L7,L8,L2,L2ALB,TA,Top,Atmosphere,Reflectance,C2,COLLECTION2
   sensorType: OPTICAL
   license: proprietary
   title: Landsat Collection 2 Level-2 Albers Top of Atmosphere (TA) Reflectance Product
@@ -264,6 +282,7 @@ MODIS_MCD43A4:
   platform: Terra+Aqua
   platformSerialIdentifier: EOS AM-1+PM-1
   processingLevel: L3
+  keywords: MODIS,Terra,Aqua,EOS,AM-1+PM-1,L3,MCD43A4
   sensorType: OPTICAL
   license: proprietary
   missionStartDate: "2000-03-05T00:00:00Z"
@@ -279,6 +298,7 @@ OSO:
   platform:
   platformSerialIdentifier:
   processingLevel: L3B
+  keywords: L3B,OSO,land,cover
   sensorType:
   license: proprietary
   missionStartDate: "2016-01-01T00:00:00Z"
@@ -298,6 +318,7 @@ NAIP:
   platform: National Agriculture Imagery Program
   platformSerialIdentifier: NAIP
   processingLevel: N/A
+  keywords: film,digital,cameras,Agriculture,NAIP
   sensorType: OPTICAL
   license: proprietary
   missionStartDate: "2003-01-01T00:00:00Z"
@@ -310,6 +331,7 @@ PLD_PAN:
   platform: PLEIADES
   platformSerialIdentifier: P1A,P1B
   processingLevel: PRIMARY
+  keywords: PHR,PLEIADES,P1A,P1B,PRIMARY,PLD,PAN,Panchromatic
   sensorType: OPTICAL
   license: proprietary
   missionStartDate: "2011-12-17T00:00:00Z"
@@ -321,6 +343,7 @@ PLD_XS:
   platform: PLEIADES
   platformSerialIdentifier: P1A,P1B
   processingLevel: PRIMARY
+  keywords: PHR,PLEIADES,P1A,P1B,PRIMARY,PLD,XS,Multispectral
   sensorType: OPTICAL
   license: proprietary
   missionStartDate: "2011-12-17T00:00:00Z"
@@ -332,6 +355,7 @@ PLD_BUNDLE:
   platform: PLEIADES
   platformSerialIdentifier: P1A,P1B
   processingLevel: PRIMARY
+  keywords: PHR,PLEIADES,P1A,P1B,PRIMARY,PLD,BUNDLE,Pan,Xs
   sensorType: OPTICAL
   license: proprietary
   missionStartDate: "2011-12-17T00:00:00Z"
@@ -343,6 +367,7 @@ PLD_PANSHARPENED:
   platform: PLEIADES
   platformSerialIdentifier: P1A,P1B
   processingLevel: PRIMARY
+  keywords: PHR,PLEIADES,P1A,P1B,PRIMARY,PLD,PANSHARPENED,Pan,Xs
   sensorType: OPTICAL
   license: proprietary
   missionStartDate: "2011-12-17T00:00:00Z"
@@ -366,6 +391,7 @@ S1_SAR_OCN:
   platform: SENTINEL1
   platformSerialIdentifier: S1A,S1B
   processingLevel: L2
+  keywords: SAR,SENTINEL,SENTINEL1,S1,S1A,S1B,L2,OCN,SAFE
   sensorType: RADAR
   license: proprietary
   title: SENTINEL1 Level-2 OCN
@@ -389,6 +415,7 @@ S1_SAR_GRD:
   platform: SENTINEL1
   platformSerialIdentifier: S1A,S1B
   processingLevel: L1
+  keywords: SAR,SENTINEL,SENTINEL1,S1,S1A,S1B,L1,GRD,SAFE
   sensorType: RADAR
   license: proprietary
   title: SENTINEL1 Level-1 Ground Range Detected
@@ -412,6 +439,7 @@ S1_SAR_GRD_JP2:
   platform: SENTINEL1
   platformSerialIdentifier: S1A,S1B
   processingLevel: L1
+  keywords: SAR,SENTINEL,SENTINEL1,S1,S1A,S1B,L1,GRD,JP2
   sensorType: RADAR
   license: proprietary
   title: SENTINEL1 Level-1 Ground Range Detected
@@ -429,6 +457,7 @@ S1_SAR_SLC:
   platform: SENTINEL1
   platformSerialIdentifier: S1A,S1B
   processingLevel: L1
+  keywords: SAR,SENTINEL,SENTINEL1,S1,S1A,S1B,L1,SLC,SAFE
   sensorType: RADAR
   license: proprietary
   title: SENTINEL1 Level-1 Single Look Complex
@@ -445,6 +474,7 @@ S1_SAR_RAW:
   platform: SENTINEL1
   platformSerialIdentifier: S1A,S1B
   processingLevel: L0
+  keywords: SAR,SENTINEL,SENTINEL1,S1,S1A,S1B,L0,RAW,SAFE
   sensorType: RADAR
   license: proprietary
   title: SENTINEL1 SAR Level-0
@@ -465,12 +495,12 @@ S2_MSI_L1C:
   platform: SENTINEL2
   platformSerialIdentifier: S2A,S2B
   processingLevel: L1
+  keywords: MSI,SENTINEL,SENTINEL2,S2,S2A,S2B,L1,L1C,SAFE
   sensorType: OPTICAL
   license: proprietary
   missionStartDate: "2015-06-23T00:00:00Z"
   title: SENTINEL2 Level-1C
 
-
 S2_MSI_L1C_JP2:
   abstract: |
     The Level-1C product is composed of 100x100 km2 tiles (ortho-images in UTM/WGS84 projection). It results from
@@ -485,12 +515,12 @@ S2_MSI_L1C_JP2:
   platform: SENTINEL2
   platformSerialIdentifier: S2A,S2B
   processingLevel: L1
+  keywords: MSI,SENTINEL,SENTINEL2,S2,S2A,S2B,L1C,JP2
   sensorType: OPTICAL
   license: proprietary
   missionStartDate: "2015-06-23T00:00:00Z"
   title: SENTINEL2 Level-1C
 
-
 S2_MSI_L2A:
   abstract: |
     The Level-2A product provides Bottom Of Atmosphere (BOA) reflectance images derived from the associated Level-1C
@@ -500,6 +530,7 @@ S2_MSI_L2A:
   platform: SENTINEL2
   platformSerialIdentifier: S2A,S2B
   processingLevel: L2
+  keywords: MSI,SENTINEL,SENTINEL2,S2,S2A,S2B,L2,L2A,SAFE
   sensorType: OPTICAL
   license: proprietary
   title: SENTINEL2 Level-2A
@@ -514,6 +545,7 @@ S2_MSI_L2A_JP2:
   platform: SENTINEL2
   platformSerialIdentifier: S2A,S2B
   processingLevel: L2
+  keywords: MSI,SENTINEL,SENTINEL2,S2,S2A,S2B,L2,L2A,JP2
   sensorType: OPTICAL
   license: proprietary
   title: SENTINEL2 Level-2A
@@ -528,6 +560,7 @@ S2_MSI_L2A_COG:
   platform: SENTINEL2
   platformSerialIdentifier: S2A,S2B
   processingLevel: L2
+  keywords: MSI,SENTINEL,SENTINEL2,S2,S2A,S2B,L2,L2A,COG
   sensorType: OPTICAL
   license: proprietary
   title: SENTINEL2 Level-2A
@@ -543,6 +576,7 @@ S2_MSI_L2A_MAJA:
   platform: SENTINEL2
   platformSerialIdentifier: S2A,S2B
   processingLevel: L2
+  keywords: MSI,SENTINEL,SENTINEL2,S2,S2A,S2B,L2,L2A,MAJA
   sensorType: OPTICAL
   license: proprietary
   title: SENTINEL2 Level-2A
@@ -558,6 +592,7 @@ S2_MSI_L2B_MAJA_SNOW:
   platform: SENTINEL2
   platformSerialIdentifier: S2A,S2B
   processingLevel: L2
+  keywords: MSI,MAJA,SENTINEL,sentinel2,S2,S2A,S2B,L2,L2B,SNOW
   sensorType: OPTICAL
   license: proprietary
   missionStartDate: "2015-06-23T00:00:00Z"
@@ -571,6 +606,7 @@ S2_MSI_L2B_MAJA_WATER:
   platform: SENTINEL2
   platformSerialIdentifier: S2A,S2B
   processingLevel: L2
+  keywords: MSI,MAJA,SENTINEL,sentinel2,S2,S2A,S2B,L2,L2B,WATER
   sensorType: OPTICAL
   license: proprietary
   missionStartDate: "2015-06-23T00:00:00Z"
@@ -587,6 +623,7 @@ S2_MSI_L3A_WASP:
   platform: SENTINEL2
   platformSerialIdentifier: S2A,S2B
   processingLevel: L3
+  keywords: MSI,SENTINEL,sentinel2,S2,S2A,S2B,L3,L3A,WASP
   sensorType: OPTICAL
   license: proprietary
   missionStartDate: "2015-06-23T00:00:00Z"
@@ -599,6 +636,7 @@ S3_SRA_BS:
   platform: SENTINEL3
   platformSerialIdentifier: S3A,S3B
   processingLevel: L1
+  keywords: SRA,SRAL,SENTINEL,SENTINEL3,S3,S3A,S3B,L1,BS
   sensorType: RADAR
   license: proprietary
   title: SENTINEL3 SRAL Level-1
@@ -610,6 +648,7 @@ S3_SRA_A_BS:
   platform: SENTINEL3
   platformSerialIdentifier: S3A,S3B
   processingLevel: L1
+  keywords: SRA,SRAL,SENTINEL,SENTINEL3,S3,S3A,S3B,L1,BS
   sensorType: RADAR
   license: proprietary
   title: SENTINEL3 SRAL Level-1
@@ -623,6 +662,7 @@ S3_EFR:
   platform: SENTINEL3
   platformSerialIdentifier: S3A,S3B
   processingLevel: L1
+  keywords: OLCI,SENTINEL,SENTINEL3,S3,S3A,S3B,L1,EFR
   sensorType: OPTICAL
   license: proprietary
   title: SENTINEL3 EFR
@@ -634,6 +674,7 @@ S3_ERR:
   platform: SENTINEL3
   platformSerialIdentifier: S3A,S3B
   processingLevel: L1,L2
+  keywords: OLCI,SENTINEL,SENTINEL3,S3,S3A,S3B,L1,L2,ERR
   sensorType: OPTICAL
   license: proprietary
   title: SENTINEL3 ERR
@@ -651,6 +692,7 @@ S3_SLSTR_L1RBT:
   platform: SENTINEL3
   platformSerialIdentifier: S3A,S3B
   processingLevel: L1
+  keywords: SLSTR,SENTINEL,SENTINEL3,S3,S3A,S3B,L1,L1RBT,RBT
   sensorType: OPTICAL
   license: proprietary
   title: SENTINEL3 SLSTR Level-1
@@ -662,6 +704,7 @@ S3_SRA:
   platform: SENTINEL3
   platformSerialIdentifier: S3A,S3B
   processingLevel: L1
+  keywords: SRA,SRAL,SENTINEL,SENTINEL3,S3,S3A,S3B,L1
   sensorType: RADAR
   license: proprietary
   title: SENTINEL3 SRAL Level-1
@@ -673,6 +716,7 @@ S3_WAT:
   platform: SENTINEL3
   platformSerialIdentifier: S3A,S3B
   processingLevel: L2
+  keywords: SRAL,SENTINEL,SENTINEL3,S3,S3A,S3B,L2,WAT
   sensorType: RADAR
   license: proprietary
   title: SENTINEL3 SRAL Level-2 WAT
@@ -684,6 +728,7 @@ S3_LAN:
   platform: SENTINEL3
   platformSerialIdentifier: S3A,S3B
   processingLevel: L2
+  keywords: SRAL,SENTINEL,SENTINEL3,S3,S3A,S3B,L2,LAN
   sensorType: RADAR
   license: proprietary
   title: SENTINEL3 SRAL Level-2 LAN
@@ -698,6 +743,7 @@ S3_SLSTR_L2LST:
   platform: SENTINEL3
   platformSerialIdentifier: S3A,S3B
   processingLevel: L2
+  keywords: SLSTR,SENTINEL,SENTINEL3,S3,S3A,S3B,L2,L2LST,LST
   sensorType: OPTICAL
   license: proprietary
   title: SENTINEL3 SLSTR Level-2 LST
@@ -714,6 +760,7 @@ S3_OLCI_L2LRR:
   platform: SENTINEL3
   platformSerialIdentifier: S3A,S3B
   processingLevel: L2
+  keywords: OLCI,SENTINEL,SENTINEL3,S3,S3A,S3B,L2,L2LRR,LRR
   sensorType: OPTICAL
   license: proprietary
   title: SENTINEL3 OLCI Level-2 Land Reduced Resolution
@@ -730,6 +777,7 @@ S3_OLCI_L2LFR:
   platform: SENTINEL3
   platformSerialIdentifier: S3A,S3B
   processingLevel: L2
+  keywords: OLCI,SENTINEL,SENTINEL3,S3,S3A,S3B,L2,L2LFR,LFR
   sensorType: OPTICAL
   license: proprietary
   title: SENTINEL3 OLCI Level-2 Land Full Resolution
@@ -745,6 +793,7 @@ SPOT_SWH:
   platform: SPOT1-5
   platformSerialIdentifier: SPOT1-5
   processingLevel: L1C
+  keywords: SPOT,SPOT1,SPOT2,SPOT3,SPOT4,SPOT5,L1C
   sensorType: OPTICAL
   license: proprietary
   title: Spot World Heritage
@@ -757,6 +806,7 @@ SPOT_SWH_OLD:
   platform: SPOT1-5
   platformSerialIdentifier: SPOT1-5
   processingLevel: L1C
+  keywords: SPOT,SPOT1,SPOT2,SPOT3,SPOT4,SPOT5,L1C
   sensorType: OPTICAL
   license: proprietary
   title: Spot World Heritage
@@ -769,6 +819,7 @@ SPOT5_SPIRIT:
   platform: SPOT5
   platformSerialIdentifier: SPOT5
   processingLevel: L1A
+  keywords: SPOT,SPOT5,L1A
   sensorType: OPTICAL
   license: proprietary
   title: Spot 5 SPIRIT
@@ -782,6 +833,7 @@ VENUS_L1C:
   platform: VENUS
   platformSerialIdentifier: VENUS
   processingLevel: L1C
+  keywords: VENUS,L1,L1C
   sensorType: OPTICAL
   license: proprietary
   title: Venus Level1-C
@@ -795,6 +847,7 @@ VENUS_L2A_MAJA:
   platform: VENUS
   platformSerialIdentifier: VENUS
   processingLevel: L2A
+  keywords: VENUS,L2,L2A
   sensorType: OPTICAL
   license: proprietary
   title: Venus Level2-A
@@ -806,6 +859,7 @@ VENUS_L3A_MAJA:
   platform: VENUS
   platformSerialIdentifier: VENUS
   processingLevel: L3A
+  keywords: VENUS,L3,L3A
   sensorType: OPTICAL
   license: proprietary
   title: Venus Level3-A
@@ -818,6 +872,7 @@ GENERIC_PRODUCT_TYPE:
   platform:
   platformSerialIdentifier:
   processingLevel:
+  keywords:
   sensorType:
   license:
   title:
diff --git a/eodag/utils/__init__.py b/eodag/utils/__init__.py
index 846e6fff..90ccf43c 100644
--- a/eodag/utils/__init__.py
+++ b/eodag/utils/__init__.py
@@ -24,6 +24,7 @@ import ast
 import copy
 import errno
 import functools
+import hashlib
 import inspect
 import logging as py_logging
 import os
@@ -883,3 +884,21 @@ class MockResponse(object):
     def json(self):
         """Return json data"""
         return self.json_data
+
+
+def md5sum(file_path):
+    """Get file MD5 checksum
+    >>> import os
+    >>> md5sum(os.devnull)
+    'd41d8cd98f00b204e9800998ecf8427e'
+
+    :param file_path: Pairs of key / value
+    :type file_path: str
+    :returns: MD5 checksum
+    :rtype: str
+    """
+    hash_md5 = hashlib.md5()
+    with open(file_path, "rb") as f:
+        for chunk in iter(lambda: f.read(4096), b""):
+            hash_md5.update(chunk)
+    return hash_md5.hexdigest()


Test patch:
diff --git a/tests/units/test_core.py b/tests/units/test_core.py
index 556277df..cae20dee 100644
--- a/tests/units/test_core.py
+++ b/tests/units/test_core.py
@@ -21,6 +21,7 @@ import json
 import os
 import shutil
 import unittest
+import uuid
 from copy import deepcopy
 
 from shapely import wkt
@@ -237,23 +238,18 @@ class TestCore(unittest.TestCase):
         self.assertEqual(dag.locations_config, [])
 
     def test_rebuild_index(self):
-        """Change eodag version and check that whoosh index is rebuilt"""
+        """Change product_types_config_md5 and check that whoosh index is rebuilt"""
         index_dir = os.path.join(self.dag.conf_dir, ".index")
         index_dir_mtime = os.path.getmtime(index_dir)
+        random_md5 = uuid.uuid4().hex
 
-        self.assertNotEqual(self.dag.get_version(), "fake-version")
+        self.assertNotEqual(self.dag.product_types_config_md5, random_md5)
 
-        with mock.patch(
-            "eodag.api.core.EODataAccessGateway.get_version",
-            autospec=True,
-            return_value="fake-version",
-        ):
-            self.assertEqual(self.dag.get_version(), "fake-version")
-
-            self.dag.build_index()
+        self.dag.product_types_config_md5 = random_md5
+        self.dag.build_index()
 
-            # check that index_dir has beeh re-created
-            self.assertNotEqual(os.path.getmtime(index_dir), index_dir_mtime)
+        # check that index_dir has beeh re-created
+        self.assertNotEqual(os.path.getmtime(index_dir), index_dir_mtime)
 
 
 class TestCoreConfWithEnvVar(unittest.TestCase):