Skip to content

Managing your content with Gally

botisSmile edited this page Nov 7, 2024 · 28 revisions

Managing E-Commerce Content

Gally is tailored for E-Commerce and is able to deal out-of-the-box with the most commonly used concepts in the E-Commerce world : products and categories.

Authentication

First of all, all the write operations will require you to be authenticated.

You should have created an admin user during the setup of your project, that's the account you can use to operate with the API.

To authenticate your API calls, you have to use the authentication_token API :

REST
URL /authentication_token
Method POST
Body
{
  "email": "[email protected]",
  "password": "apassword"
}
Response The auth token
GraphQl
URL /graphql
Method POST
Body
mutation {
  tokenAuthentication(input: {
    email: "[email protected]"
    password: "apassword"
  }) {
    authentication { token }
  }
}
Response
{
  "data": {
    "tokenAuthentication": {
      "authentication": {
        "token": "eyJ0eXAi...o3w"
      }
    }
  }
}

This auth token has then to be injected as a Bearer AUTH_TOKEN in the Authorization HTTP header in all your subsequent API calls.

Define your catalogs

In Gally, a Catalog is the representation of a scope of your E-Commerce website.

If you're a retailer that is running several brands, like www.shiny-shoes-shop.com and www.classic-glasses-shop.com, you'll want to create two different catalogs :

  • Shiny Shoes Shop
  • Classic Glasses Shop

To create your catalog(s), you have to use the Catalog API. The catalog creation is required only once (or if later, you add a new catalog to your webshop).

REST
URL /catalogs
Method POST
Body
{
  "code": "shiny_shoes_com",
  "name": "Shiny Shoes Shop"
}
Response The created catalog, with IRI (field `@id`) for later use
{
  "@context": "/contexts/Catalog",
  "@id": "/catalogs/1",
  "@type": "Catalog",
  "id": 1,
  "code": "shiny_shoes_com",
  "name": "Shiny Shoes Shop",
  "localizedCatalogs": []
}
GraphQl
URL /graphql
Method POST
Body
mutation {
  createCatalog(input: {
    code: "shiny_shoes_com"
    name: "Shiny Shoes Shop"
  }) {
    catalog {
      id
      _id
      code
      name
    }
  }
}
Response Here, the created catalog, with both : - the IRI (field `id`) for later use in REST and GraphQL endpoints - the internal ID (field `_id`)
{
  "data": {
    "createCatalog": {
      "catalog": {
        "id": "/catalogs/1",
        "_id": 1,
        "code": "shiny_shoes_com"
        "name": "Shiny Shoes Shop"
      }
    }
  }
}

Define your localized catalogs

In Gally, a Localized Catalog is the combination of a Catalog and a locale. Under the hood, this will allow to have one Elasticsearch index per catalog and locale, to be able to deal with different languages.

So if your previously seen Shiny Shoes Shop and Classic Glasses Shop are available both in English and French, you'll have two localized catalogs for each of them.

The resulting structure will then be :

  • Shiny Shoes Shop (Catalog) :
    • Shiny Shoes French (Localized Catalog)
    • Shiny Shoes English (Localized Catalog)
  • Classic Glasses Shop (Catalog) :
    • Classic Glasses French (Localized Catalog)
    • Classic Glasses English (Localized Catalog)

To create your localized catalog(s), you have to use the LocalizedCatalog API. The localized catalog(s) creation is required only once (or if later, you add a new locale to your webshop).

REST
URL /localizedcatalogs
Method POST
Body
{
  "name": "Shiny Shoes French",
  "code": "shiny_shoes_com_fr",
  "locale": "fr_FR",
  "currency": "EUR",
  "isDefault": true,
  "catalog": "/catalogs/1"
}
Response The created localized catalog, with IRI (field `@id`) for later use
{
  "@context": "/contexts/LocalizedCatalog",
  "@id": "/localized_catalogs/1",
  "@type": "LocalizedCatalog",
  "id": 1,
  "name": "Shiny Shoes French",
  "code": "shiny_shoes_com_fr",
  "locale": "fr_FR",
  "currency": "EUR",
  "isDefault": true,
  "catalog": "/catalogs/1",
  "localName": "French (France)"
}
GraphQl
URL /graphql
Method POST
Body
mutation {
  createLocalizedCatalog(input: {
    name: "Shiny Shoes French"
    code: "shiny_shoes_com_fr"
    locale: "fr_FR"
    currency: "EUR"
    isDefault: true
    catalog: "/catalogs/1"
  }) {
    localizedCatalog {
      id
      _id
      code
      name
    }
  }
}
Response The created localized catalog, with IRI (field `id`) for later use
{
  "data": {
    "createLocalizedCatalog": {
      "localizedCatalog": {
        "id": "/localized_catalogs/1",
        "_id": 1,
        "code": "shiny_shoes_com_fr",
        "name": "Shiny Shoes French"
      }
    }
  }
}

That's as simple as that.

Define your source fields and their options

Now that you have your catalogs and localized catalogs, it's time to teach Gally how your catalog structure looks like.

That's where source fields appears. In Gally, each source field is the representation of a field of your entities.

Eg : If your products do have a name, a price and a brand, you need to tell Gally that you have those 3 source fields for the product entity type.

This step will help Gally to define properly the underlying mapping of the Elasticsearch indices that will be used for indexing and searching in content.

The good news is, you don't have to bother with Elasticsearch complicated field types or to understand how they work, Gally's logic is wrapping this inside a more comprehensible manner to define field types.

Default source field types

Type Details Example
text Text fields Product Name, description, etc...
keyword Non-analyzed text fields String identifiers, etc...
select Fields that contains a closed list of values Brand, color, etc...
int Integer fields Unique identifiers, numerical values etc...
boolean True/false fields "Eco label", "Suitable for children", etc...
float Floating numbers Not "price" (see below), but rather "width", "length", etc...
object Complex objects containing several sub-fields Structured content (tree, persons with name and surname, etc...)
date Date field

Advanced source field types

Type Details Example
price Price field Product prices for different customer groups, including potential discount
stock Stock field Product stock field, including stock status and quantity
category Category/Product relation field Contains the list of categories a product belongs to
reference Unique identifier field SKU
image Image field URLs of the product images

Create source fields

First, you have to create all your source fields. Source fields creation is required only once, or if the data type of your source field has changed in your ECommerce application.

To create source fields, you have to use the SourceFields API :

REST
URL /sourcefields
Method POST
Body
{
  "code": "name",
  "defaultLabel": "Product Name",
  "type": "text",
  "isFilterable": true,   // Not mandatory
  "isSearchable": true,   // Not mandatory
  "isSortable": true,     // Not mandatory
  "isUsedForRules": true, // Not mandatory
  "weight": 1,            // Not mandatory
  "isSpellchecked": true, // Not mandatory
  "metadata": "/metadata/1"
}
Response The created source field, with IRI for later use
GraphQl
URL /graphql
Method POST
Body
mutation {
  createSourceField(input: {
    code: "alternative_name",
    defaultLabel: "Product Alternative Name",
    type: "text",
    isSystem: false,
    isFilterable: true,   # Not mandatory
    isSearchable: true,   # Not mandatory
    isSortable: true,     # Not mandatory
    isUsedForRules: true, # Not mandatory
    weight: 1,            # Not mandatory
    isSpellchecked: true, # Not mandatory
    metadata: "/metadata/1"
  }) {
    sourceField {
      id
      _id
      code
    }
  }
}
Response The created source field , with IRI (field `id`) for later use
{
  "data": {
    "createSourceField": {
      "sourceField": {
        "id": "/source_fields/1",
        "_id": 1,
        "code": "name"
      }
    }
  }
}

Here are the details of all the source fields parameters :

Parameter Name Detail
code* The code of the source field. Unique identifier.
defaultLabel* The label of the source field. Untranslated.
type* The type of the source field. As explained before.
metadata* The entity type this fields relates to ("product", "category", or something else.
isFilterable If this field is used for filtering. Can be changed in the back-office later.
isSearchable If this field is used for searching. Can be changed in the back-office later.
isSortable If this field is used for sorting. Can be changed in the back-office later.
isUsedForRules If this field is used for rules (virtual categories, boosts, etc...). Can be changed in the back-office later.
weight Weight of this field for searching. Can be changed in the back-office later.
isSpellchecked If this field is spellchecked. Can be changed in the back-office later

Create source fields labels (when using several locales)

If you are having several locales (e.g. fr_FR and en_US), you'll have to tell Gally how a source field label should be translated for a given catalog.

Eg : "Material" should be "Matière" when browsing a French localized catalog.

You have to do so with the SourceFieldLabel API.

REST
URL /source_field_labels
Method POST
Body
{
  "sourceField": "/source_fields/4",          // The IRI of the "material" source field
  "localizedCatalog": "/localized_catalogs/1", // The localized catalog to target
  "label": "Matière"                          // The translated label
}
Response The created source field label, with IRI (field `@id`) for later use
{
  "@context": "/contexts/SourceFieldLabel",
  "@id": "/source_field_labels/137",
  "@type": "SourceFieldLabel",
  "id": 137,
  "sourceField": "/source_fields/4",
  "localizedCatalog": "/localized_catalogs/1",
  "label": "Matière"
}
GraphQl
URL /graphql
Method POST
Body
mutation {
  createSourceFieldLabel(input: {
    sourceField: "/source_fields/4",
    localizedCatalog: "/localized_catalogs/1",
    label: "Matière"
  }) {
    sourceFieldLabel {
      id
      _id
      label
    }
  }
}
Response The created source field , with IRI (field `id`) for later use
{
  "data": {
    "createSourceFieldLabel": {
      "sourceFieldLabel": {
        "id": "/source_field_labels/137",
        "_id": 137,
        "label": "Matière"
      }
    }
  }
}

Create source fields options (for "select" source fields)

For your source fields that are of type "select", it means they are option-based. You then need to inform Gally what are the options of each field.

Eg : "Material" could have the following options : "Cotton", "Wool", "Leather"...

You have to do so with the SourceFieldOption API.

REST
URL /source_field_options
Method POST
Body
{
  "code": "material_leather",         // Unique identifier of the option
  "sourceField": "/source_fields/4",  // The IRI of the "material" source field
  "position": 0,                      // The position of this option within all the options
  "defaultLabel": "Leather"           // The default label of this option
}
Response The created option, with IRI (field `@id`) for later use
{
  "@context": "/contexts/SourceFieldOption",
  "@id": "/source_field_options/1231",
  "@type": "SourceFieldOption",
  "id": 1231,
  "code": "material_leather",
  "sourceField": "/source_fields/4",
  "position": 0,
  "defaultLabel": "Leather",
  "labels": []
}
GraphQl
URL /graphql
Method POST
Body
mutation {
  createSourceFieldOption(input: {
    code: "material_leather",         # Unique identifier of the option
    sourceField: "/source_fields/4",  # The IRI of the "material" source field
    position: 0,                      # The position of this option within all the options
    defaultLabel: "Leather"           # The default label of this option
  }) {
    sourceFieldOption {
      id
      code
      position
      defaultLabel
    }
  }
}
Response The created source field , with IRI (field `id`) for later use
{
  "data": {
    "createSourceFieldOption": {
      "sourceFieldOption": {
        "id": "/source_field_options/1231",
        "_id": 1231,
        "code": "material_leather",
        "position": 0,
        "defaultLabel": "Leather"
      }
    }
  }
}

Create source fields options labels (for "select" source fields, when using several locales)

If you are having several locales (e.g. fr_FR and en_US), you'll have to tell Gally how a source field option label should be translated for a given catalog.

Eg : "Leather" value of the "Material" Source Field should be "Cuir" when browsing a French localized catalog.

You have to do so with the SourceFieldOptionLabel API.

REST
URL /source_field_options_labels
Method POST
Body
{
  "sourceFieldOption": "/source_field_options/1231",  // IRI of the option created previously
  "localizedCatalog": "/localized_catalogs/1",        // IRI of the localized catalog the option belongs to
  "label": "Cuir"                                     // The translated label
}
Response The created option label, with IRI (field `@id`) for later use
{
  "@context": "/contexts/SourceFieldOptionLabel",
  "@id": "/source_field_option_labels/1365",
  "@type": "SourceFieldOptionLabel",
  "id": 1365,
  "sourceFieldOption": {
    "@id": "/source_field_options/1231",
    "@type": "SourceFieldOption",
    "code": "material_leather"
  },
  "localizedCatalog": "/localized_catalogs/1",
  "label": "Cuir"
}
GraphQl
URL /graphql
Method POST
Body
mutation {
  createSourceFieldOptionLabel(input: {
    sourceFieldOption: "/source_field_options/1231",
    localizedCatalog: "/localized_catalogs/1",
    label: "Cuir"
  }) {
    sourceFieldOptionLabel {
      _id
      id
      label
      sourceFieldOption {
        id
        code
      }
    }
  }
}
Response The created source field , with IRI (field `id`) for later use
{
  "data": {
    "createSourceFieldOptionLabel": {
      "sourceFieldOptionLabel": {
        "_id": 1371,
        "id": "/source_field_option_labels/1371",
        "label": "Cuir",
        "sourceFieldOption": {
          "id": "/source_field_options/1231",
          "code": "material_leather"
        }
      }
    }
  }
}

Sync your catalog data

Once your catalogs, localized catalogs, and source fields have been defined, you can send your data to Gally to get them indexed into Elasticsearch.

You will have to do this for at least 2 entity types : "product" and "category". That's the minimum requirement to have a functional back-office in Gally.

This means that you have to repeat the following process twice.

Creating your index

To do so, you first have to ask for creating a new index by providing an existing entity type and a localized catalog identifier (either the localized catalog code or its internal ID - not the IRI).

REST
URL /indices
Method POST
Body
{
  "entityType": "product",                  // existing metadata entity type (e.g. "product" or "category")
  "localizedCatalog": "shiny_shoes_com_fr"  // the localized catalog code (e.g. "shiny_shoes_com_fr" or id e.g. "1")
}
Response The created index with IRI (field `@id`) and name (`name`) for later use
{
  "@context": "/contexts/Index",
  "@id": "/indices/gally_shiny_shoes_com_fr_product_20230123_150312",
  "@type": "Index",
  "name": "gally_shiny_shoes_com_fr_product_20230123_150312",
  "aliases": [
    ".catalog_13",
    ".entity_product"
  ]
}
GraphQl
URL /graphql
Method POST
Body
mutation {
  createIndex(input: {
    entityType: "product"                   # existing metadata entity type (e.g. "product" or "category")
    localizedCatalog: "shiny_shoes_com_fr"  # the localized catalog code (e.g. "shiny_shoes_com_fr" or id e.g. "1")
  }) {
    index {
      id
      name
    }
  }
}
Response The created index with IRI (field `id`) and name (`name`) for later use
{
  "data": {
    "createIndex": {
      "index": {
        "id": "/indices/gally_shiny_shoes_com_fr_product_20230123_150605",
        "name": "gally_shiny_shoes_com_fr_product_20230123_150605",
        "aliases": [
          ".catalog_13",
          ".entity_product"
        ]
      }
    }
  }
}

This method will give you the newly created index identifier.

Bulk your data

Then, bulk your data into this newly created index :

REST
URL /index_documents
Method POST
Body
{
  "indexName": "string",
  "documents": [
    "string"
  ]
}

The bulk Api is intended to be used with batches of items. You'll probably be willing to call it in a loop that is parsing your whole catalog and call it as much time as needed.

Bulk format

The Bulk API accepts jsonified version of the data you want to index. Your data has to be accurate with your source field types.

Here is an example table of how your data should be structured per each type :

Source Field Type Data
text
name:"Nike red Shoes"
keyword
my_identifier:"075d8b8"
select
"fashion_size": [
    {
        "value": 33,
        "label": "XL"
    }, {
        "value": 37,
        "label": "XS"
    }, {
        "value": 36,
        "label": "S"
    }, {
        "value": 35,
        "label": "M"
    }, {
        "value": 34,
        "label": "L"
    }
]
int
"width":12
boolean
"has_video": {
        "value": true,
        "label": "Has Video"
    },
float
"length":127.04
object
"actor": {
        "firstname": "Hugh",
        "lastname": "Jackman"
    },
date
"created_at": "2022-09-07 14:57:19"
price
"price": [
    {
        "price": 88,
        "original_price": 88,
        "group_id": 0,
        "is_discounted": false
    }, {
        "price": 75,
        "original_price": 88,
        "group_id": 1,
        "is_discounted": true
    }, {
        "price": 88,
        "original_price": 88,
        "group_id": 2,
        "is_discounted": false
    }
],
stock
"stock": {
    "qty": 0,
    "status": true
},
category
"category": [
    {
        "category_uid": "Mg==",
        "id": "cat_2"
    }, {
        "name": "Tops",
        "category_uid": "OA==",
        "id": "cat_8"
    }, {
        "is_parent": true,
        "name": "Blouses & Shirts",
        "category_uid": "OQ==",
        "id": "cat_9"
    }
]
reference
"sku": "VT04"
image
"image": "\/v\/t\/vt04-mt_main.jpg"
Example of a full document being sent through a bulk
{
	"entity_id": "1104",
	"attribute_set_id": "11",
	"type_id": "configurable",
	"sku": "VT04",
	"has_options": false,
	"required_options": false,
	"created_at": "2022-09-07 14:57:19",
	"updated_at": "2022-09-07 14:57:19",
	"visibility": {
		"value": "4",
		"label": "Catalog, Search"
	},
	"price": [{
		"price": 88,
		"original_price": 88,
		"group_id": 0,
		"is_discounted": false
	}, {
		"price": 88,
		"original_price": 88,
		"group_id": 1,
		"is_discounted": false
	}, {
		"price": 88,
		"original_price": 88,
		"group_id": 2,
		"is_discounted": false
	}, {
		"price": 88,
		"original_price": 88,
		"group_id": 3,
		"is_discounted": false
	}],
	"indexed_attributes": ["price", "name", "url_key", "description", "fashion_material", "fashion_style", "status", "tax_class_id", "fashion_color", "fashion_size", "has_video"],
	"category": [{
		"is_virtual": "false",
		"category_uid": "Mg==",
		"id": "cat_2"
	}, {
		"is_virtual": "false",
		"name": "Tops",
		"category_uid": "OA==",
		"id": "cat_8"
	}, {
		"is_parent": true,
		"is_virtual": "false",
		"name": "Blouses & Shirts",
		"category_uid": "OQ==",
		"id": "cat_9"
	}],
	"name": ["Anna Draped Top"],
	"url_key": ["anna-draped-top"],
	"description": ["<p>The Anna Draped Top hangs beautifully in the all the right places. The soft, sheer material offers graceful lines and seamless movement. Back lace detail and tie at the neckline complete the look.  <\/p><p>Features:<\/p><ul><li>Draped neckline<\/li><li>Capped sleeves<\/li><li>Hits at the hips<\/li><li>Lace detail back &amp; tie neckline<\/li><li>Hand wash, line dry<\/li><\/ul>"],
	"fashion_material": [{
		"value": "47",
		"label": "Cotton"
	}, {
		"value": "56",
		"label": "Viscose"
	}],
	"fashion_style": [{
		"value": "62",
		"label": "Short Sleeve"
	}, {
		"value": "72",
		"label": "Crew"
	}],
	"status": [{
		"value": 1,
		"label": "Enabled"
	}],
	"tax_class_id": [{
		"value": 2,
		"label": "Taxable Goods"
	}],
	"fashion_color": [{
		"value": 5,
		"label": "Gold"
	}, {
		"value": 25,
		"label": "Rain"
	}, {
		"value": 26,
		"label": "Mint"
	}, {
		"value": 24,
		"label": "Lilac"
	}, {
		"value": 28,
		"label": "Latte"
	}],
	"fashion_size": [{
		"value": 33,
		"label": "XL"
	}, {
		"value": 37,
		"label": "XS"
	}, {
		"value": 36,
		"label": "S"
	}, {
		"value": 35,
		"label": "M"
	}, {
		"value": 34,
		"label": "L"
	}],
	"has_video": [{
		"value": true,
		"label": "Has Video"
	}],
	"children_ids": [144, 145, 146, 147, 148, 149, 150, 151, 152, 153, 154, 155, 156, 157, 158, 159],
	"children_attributes": ["indexed_attributes", "url_key", "description", "fashion_material", "fashion_style", "fashion_color", "fashion_size", "has_video"],
	"configurable_attributes": ["fashion_color", "fashion_size"],
	"children.indexed_attributes": ["name", "url_key", "description", "fashion_material", "fashion_style", "status", "tax_class_id", "fashion_color", "fashion_size", "has_video"],
	"children.url_key": ["anna-draped-top"],
	"children.description": ["<p>The Anna Draped Top hangs beautifully in the all the right places. The soft, sheer material offers graceful lines and seamless movement. Back lace detail and tie at the neckline complete the look.  <\/p><p>Features:<\/p><ul><li>Draped neckline<\/li><li>Capped sleeves<\/li><li>Hits at the hips<\/li><li>Lace detail back &amp; tie neckline<\/li><li>Hand wash, line dry<\/li><\/ul>"],
	"children.sku": ["VT04-RN-XS", "VT04-RN-S", "VT04-RN-M", "VT04-RN-L", "VT04-MT-XS", "VT04-MT-S", "VT04-MT-M", "VT04-MT-L", "VT04-LL-XS", "VT04-LL-S", "VT04-LL-M", "VT04-LL-L", "VT04-LA-XS", "VT04-LA-S", "VT04-LA-M", "VT04-LA-L"],
	"id": 1104,
	"stock": {
		"qty": 0,
		"status": true
	},
	"image": "\/v\/t\/vt04-mt_main.jpg"
}

Install the index

When you are done with bulks, you can finish the installation of your index which will make sure that:

  • from now on, this is the index that will be targeted when you use the API search endpoints for the given entity type and localized catalog ;
  • this works because your index receives a special unique index alias of the form [prefix]_[localized_catalog_code]_[entity_type] which characterizes the live index for that localized catalog and entity type.
REST
URL /indices/install/{name} Example:
/indices/install/gally_shiny_shoes_com_fr_product_20230123_150312
Method PUT
Response The installed index with updated aliases and status.
Note the extra alias which is similar to the index name but without the timestamp.
{
  "@context": "/contexts/Index",
  "@id": "/indices/gally_shiny_shoes_com_fr_product_20230123_150605",
  "@type": "Index",
  "name": "gally_shiny_shoes_com_fr_product_20230123_150605",
  "aliases": [
    ".catalog_1",
    ".entity_product"
  ],
  "docsCount": 0,
  "size": "226b",
  "entityType": "product",
  "localizedCatalog": "/localized_catalogs/1",
  "status": "indexing"
}
GraphQl
URL /graphql
Method POST
Body
mutation {
  installIndex(input: {
    name: "gally_shiny_shoes_com_fr_product_20230123_150605"
  }) {
    index {
      name
      aliases
      status
    }
  }
}
Response The installed index with updated aliases and status.
Note the extra alias which is similar to the index name but without the timestamp.
{
  "data": {
    "installIndex": {
      "index": {
        "name": "gally_shiny_shoes_com_fr_product_20230123_150605",
        "aliases": [
          ".catalog_1",
          ".entity_product",
          "gally_shiny_shoes_com_fr_product"
        ],
        "status": "live"
      }
    }
  }
}

And then, refresh it :

REST
URL /indices/refresh/{name}
Example:
/indices/refresh/gally_shiny_shoes_com_fr_product_20230123_150605

Note that since the index is installed, you can also use the unique alias to reference it:

/indices/refresh/gally_shiny_shoes_com_fr_product
Method PUT
GraphQl
URL /graphql
Method POST
Body
mutation {
  refreshIndex(input: {
    name: "gally_shiny_shoes_com_fr_product_20230123_150605"
    # since the index is installed, you can also use the unique alias to reference it:
    # name: "gally_shiny_shoes_com_fr_product"
  }) {
    index {
      name
      aliases
      status
    }
  }
}

Differential indexing

You may not want to re-create a new index and process all your content when something has changed by your side. That's where differential indexing will help you.

If you have some data that has changed recently (Eg : a product has been edited), here's how you want to proceed :

Get the current "live" index

You can get the current live index by calling the indices API :

REST
URL /index_documents
Method GET
Response
  "hydra:member": [
    {
      "@id": "/indices/gally_en_fr_product_20230112_152027",
      "@type": "Index",
      "name": "gally_en_fr_product_20230112_152027",
      "aliases": [
        ".catalog_11",
        ".entity_product",
        "gally_en_fr_product"
      ],
      "docsCount": 0,
      "size": "226b",
      "entityType": "product",
      "localizedCatalog": "/localized_catalogs/11",
      "status": "live"
    },
    {
      "@id": "/indices/gally_fr_fr_product_20230112_152026",
      "@type": "Index",
      "name": "gally_fr_fr_product_20230112_152026",
      "aliases": [
        ".catalog_9",
        ".entity_product",
        "gally_fr_fr_product"
      ],
      "docsCount": 1703,
      "size": "1mb",
      "entityType": "product",
      "localizedCatalog": "/localized_catalogs/9",
      "status": "live"
    },.... 

The index (or indices) that has status:live and entityType:product are the ones that you want to index your modified data into.

Bulk your updated data

Just call the bulk API to those indices with the updated data.

Using the API to search for products

To search on products, you are supposed to use the specialized GraphQl search api 'products()'. You can provide multiple arguments to precise your search :

Query arguments Details
requestType The identifier of the "request type" : in which context you are retrieving products. Could be product_catalog (catalog navigation) or product_search (fulltext search).
localizedCatalog The code or id of the localized catalog you're searching on.
currentPage Current page
pageSize Page size
currentCategoryId Indicates the current Category you are browsing. Mandatory for product_catalog requests.
search The text query to use for fulltext searching. Mandatory for product_search requests.
sort The field that should be used for sorting, with the selected direction (asc or desc)
filter An array of filters to apply. The list of available filters is automatically generated according to the filterable source fields of the product metadata. You can find more details in the next section.

This api can handle complexe filter with nested criteria. You can apply filter on a product field with this different operators:

  • eq: The product field value match exactly the filter value
  • in: The product field value is one of the filter values
  • match:
  • exist: If the filter value is true, the product field value should be defined. If the filter value is false, the product field value should not be defined.
  • gt (gte) : The product field value is greater (or equal) to the filter value
  • lt (lte) : The product field value is lower (or equal) to the filter value

The filter operator depends on the type of the associated source field:

Source Field Type Filter type Available operator Example
text, keyword, reference, image text eq, in, match and exist { description: { match: "bags" } }
{ sku: { eq: "mysku1" }}
int, float, date numeric eq, in, gt, gte, lt, lte and exist { cost: { lte: 120 }}
boolean boolean eq and exist { is_active: { eq: true }}
select select eq, in and exist { brand_value: { exist: true }}
price price eq, in, gt, gte, lt, lte and exist { price__price: { gt: 50 }}
stock stock eq and exist { stock__status: { eq: true }}
category category eq { category__id: { eq: 1254 }}
object Not managed yet

You can use multiple filters at once, the result will contain the products that match all the provided filters:

filter: [
  { description: { match: "bags" } }
  { price__price: { gt: 50 }}
  { category__id: { eq: 1254 }}
]

If you want to create more complexe queries, you can use the boolFilter. This a special filter that can combine multiple filter with boolean logic. Its behavior is based on elasticsearch boolean query. This filter has its own special operator:

  • _must: Only the product matching the filters listed here will be in the result.
  • _should: The product matching this filters here will have a better score.
  • _not: The product matching the filters listed here will be excluded from the result.
There is some examples
  • You want to list the blue products if their price is greater than 50€ but you want to exclude product from the category 10:
    filter: [
      {
        boolFilter: {
          _must: [
            { color__value: { eq: "blue" } }
            { price__price: { gt: 50.0 } }
          ]
          _not: [
            { category__id: { eq: 10 } }
          ]
        } 
      }
    ]
    
  • You want to list product that are in the category "Bags" (with the id 4) or which contains "Bags" in their name:
    filter: [
      {
        boolFilter: {
          _must: [
            { 
              boolFilter: {
                _should: [
                  { category__id: { eq: 4 } }
                  { name: { match: "Bags" } }
                ]
              }
            }
          ]
        } 
      }
    ]
    

There is the list of the data you can ask the products search api :

Query fields Details
collection The list of the products matching your search.
collection.* Source field of product to display in the response (eg: id, name, price.price).
paginationInfo Usefull data to build pagination toolbar
paginationInfo.itemsPerPage Maximum number of products in the current page.
paginationInfo.lastPage Last page with products.
paginationInfo.totalCount Number of products matching your search.
sortInfo Information about the current sort used for the product in result
sortInfo.current.field Current field used for sorting result.
sortInfo.current.direction Current sort direction used in result.
aggregations List of aggregated data from the product in result
aggregations.field Source field code.
aggregations.label Source field label.
aggregations.count Number of products having a value for this source field.
aggregations.type Type of the aggregation, can be checkbox, slider, boolean or category.
aggregations.hasMore You can configure by source field a limit of the number of elements to display in the aggregation. The default value is '10'. The hasMore boolean indicate if the aggregation has more value than the one displayed
aggregations.options.label Label of the source field option.
aggregations.options.value Value of the source field option.
aggregations.options.count Number of products that match this source field option value.
Example GraphQl
URL /graphql
Method POST
Body
query getProducts(
  $requestType: ProductRequestTypeEnum!,
  $localizedCatalog: String!,
  $currentPage: Int,
  $currentCategoryId: String,
  $pageSize: Int,
  $search: String,
  $sort: ProductSortInput
) {
  products(
    requestType: $requestType,
    localizedCatalog: $localizedCatalog,
    currentPage: $currentPage,
    currentCategoryId: $currentCategoryId,
    pageSize: $pageSize,
    search: $search,
    sort: $sort,
    filter: [
      {
        boolFilter: {
          _must: [
            { 
              boolFilter: {
                _should: [
                  { category__id: { eq: 4 } }
                  { name: { match: "Bags" } }
                ]
              }
            }
          ]
        } 
      }
    ]
  ) {
    collection {
      ...on Product {
        id 
        sku 
        name 
        score 
        image 
        stock { status }
        price { price }
      }
    }
    paginationInfo { lastPage itemsPerPage totalCount }
    sortInfo {
      current { field direction }
    }
    aggregations {
      field 
      label 
      type 
      options { count label value }
      hasMore
    }
  }
}
Variables
{
  "localizedCatalog": "1",
  "requestType": "product_search",
  "currentPage": 1,
  "pageSize": 10,
  "search": "scarf"
}

Managing additional content

But Gally is more clever than just products and categories. You can also add any other content type (pages, blog posts, etc...) to the engine easily, and to be able to search on those contents.

Create Metadata

To index additional content (pages, articles, blog posts, recipes, etc...) you need to create "metadata" associated to each of them.

REST
URL /metadata
Method POST
Body
{
  "entity": "blog_post"
}
Response The created metadata, with IRI for later use
GraphQl
URL /graphql
Method POST
Body
mutation {
  createMetadata(input: {
    entity: "blog_post"
  }) {
    metadata {
      id
      _id
      entity
    }
  }
}
Response Here, the created metadata, with both : - the IRI (field `id`) for later use in REST and GraphQL endpoints - the internal ID (field `_id`)
{
  "data": {
    "createMetadata": {
      "metadata": {
        "id": "/metadata/4",
        "_id": 4,
        "entity": "blog_post"
      }
    }
  }
}

Init the source fields of your metadata

As you could do for Products or Categories, you will have to define the source fields of your custom content type.

This is done the same way as for "product" or "category"

⚠️ For additional metadata, you have to create one field that is named id which has the integer type. This field is mandatory because it's used internally as an unique identifier.

Index content

Once you're done with the data structure of your additional content types, you can proceed to index them.

You can refer to the Sync your catalog data : this is exactly the same, except that you have to do this for your additional entity types.

So repeat the steps for them :

  • create index
  • bulk the data
  • install the index

Search for additional content

To search on additional content, you are supposed to use the standardized GraphQl search api 'documents()'. You can provide multiple arguments to precise your search :

Query arguments Details
entityType The identifier of the metadata you're searching on.
localizedCatalog The code or id of the localized catalog you're searching on.
currentPage Current page
pageSize Page size
search The text query to use for searching. Mandatory for fulltext requests.
filter A combination of filters on source fields that are filterables. You can find more details in the next section.
sort The field that should be used for sorting, with the selected direction (asc or desc)

This api can handle complexe filter with nested criteria. This is the different types of filter you can use:

Filter type Available operator Example
existFilter None { existFilter: { field: "description"}}
equalFilter eq and in { equalFilter: { field: "sku", eq: "mysku1" }}
matchFilter match { matchFilter: { field: "description", match: "bags" }}
rangeFilter gt, gte, lt and lte { rangeFilter: { field: "cost", lte: 120 }}
boolFilter _must, _should and _not See next section.

You can use multiple filters at once, the result will contain the documents that match all the provided filters:

filter: [
  { equalFilter: { field: "sku", eq: "mysku1" }}
  { matchFilter: { field: "description", match: "bags" }}
  { rangeFilter: { field: "cost", lte: 120 }}
]

If you want to create more complexe queries, you can use the boolFilter. This a special filter that can combine multiple filter with boolean logic. Its behavior is based on elasticsearch boolean query. This filter has its own special operator:

  • _must: Only the documents matching the filters listed here will be in the result.
  • _should: The documents matching this filters here will have a better score.
  • _not: The documents matching the filters listed here will be excluded from the result.
There is some examples
  • You want to list the documents of color blue if their price is greater than 50€, but you want to exclude documents from the category 10:
    filter: [
      {
        boolFilter: {
          _must: [
            { equalFilter: { field: "color__value", eq: "blue" }}
            { rangeFilter: { field: "price__price", gt: "50.0" }}
          ]
          _not: [
            { equalFilter: { field: "category__id", eq: "10" }}
          ]
        } 
      }
    ]
    
  • You want to list documents that are in the category "Bags" (with the id 4) or which contains "Bags" in their title:
    filter: [
      {
        boolFilter: {
          _must: [
            { 
              boolFilter: {
                _should: [
                  { equalFilter: { field: "category__id", eq: "4" }}
                  { equalFilter: { field: "title", eq: "Bags" }}
                ]
              }
            }
          ]
        } 
      }
    ]
    

There is the list of the data you can ask the documents search api :

Query fields Details
collection The list of the documents matching your search.
collection.id Document id.
collection.data Document data (all source field values indexed in the elasticsearch document).
collection.index Name of the index where the document come from.
collection.score Score of the document for this query.
collection.source Document raw data as they were indexed in elasticsearch.
paginationInfo Usefull data to build pagination toolbar
paginationInfo.itemsPerPage Maximum number of documents in the current page.
paginationInfo.lastPage Last page with documents.
paginationInfo.totalCount Number of documents matching your search.
sortInfo Information about the current sort used for the documents in result
sortInfo.current.field Current field used for sorting result.
sortInfo.current.direction Current sort direction used in result.
aggregations List of aggregated data from the documents in result
aggregations.field Source field code.
aggregations.label Source field label.
aggregations.count Number of documents having a value for this source field.
aggregations.type Type of the aggregation, can be checkbox, slider, boolean or category.
aggregations.hasMore The limit of the number of elements to display in the aggregation is 10 by default. The hasMore boolean indicate if the aggregation has more value than the one displayed.
aggregations.options.label Label of the source field option.
aggregations.options.value Value of the source field option.
aggregations.options.count Number of documents that match this source field option value.
Example GraphQl
URL /graphql
Method POST
Body
query getDocuments(
  $entityType: String!,
  $localizedCatalog: String!,
  $currentPage: Int,
  $pageSize: Int,
  $search: String,
  $sort: SortInput
) {
  documents(
    entityType: $entityType,
    localizedCatalog: $localizedCatalog,
    currentPage: $currentPage,
    pageSize: $pageSize,
    search: $search,
    sort: $sort,
    filter: [
      {
        boolFilter: {
          _must: [
            { equalFilter: { field: "color.value", eq: "blue" }}
            { rangeFilter: { field: "price.price", gt: "50.0" }}
          ]
          _not: [
            { equalFilter: { field: "category.id", eq: "10" }}
          ]
        } 
      }
    ]
  ) {
    collection {
      ...on Document { id data }
    }
    paginationInfo { lastPage itemsPerPage totalCount }
    sortInfo {
      current { field direction }
    }
    aggregations {
      field 
      label 
      type 
      options { count label value }
      hasMore
    }
  }
}
Variables
{
  "entityType": "product",
  "localizedCatalog": "1",
  "currentPage": 1,
  "pageSize": 10,
  "search": "scarf"
}

Using the API to get autocomplete items

The autocomplete feature can be fully managed by the front, to get products the standardized GraphQl search api 'products()' can be called and to get any other entity the standardized GraphQl search api 'documents()' can be called.

Find below an example to get products and categories in the same GraphQl query for the term 'Dress':

query getAutoCompleteDocuments($requestType: ProductRequestTypeEnum!, $localizedCatalog: String!, $currentPage: Int, $currentCategoryId: String, $pageSize: Int, $search: String, $sort: ProductSortInput, $categoryEntityType: String!, $categoryLocalizedCatalog: String!, $categoryCurrentPage: Int, $categoryPageSize: Int, $categorySearch: String, $categorySort: SortInput) {
  products: products(
    requestType: $requestType
    localizedCatalog: $localizedCatalog
    currentPage: $currentPage
    currentCategoryId: $currentCategoryId
    pageSize: $pageSize
    search: $search
    sort: $sort
    filter: null
  ) {
    collection {
      ... on Product {
        id
        sku
        name
        score
        image
        stock {
          status
        }
        price {
          price
        }
      }
    }
    paginationInfo {
      lastPage
      itemsPerPage
      totalCount
    }
    sortInfo {
      current {
        field
        direction
      }
    }
  }
  categories: documents(
    entityType: $categoryEntityType
    localizedCatalog: $categoryLocalizedCatalog
    currentPage: $categoryCurrentPage
    pageSize: $categoryPageSize
    search: $categorySearch
    sort: $categorySort
    filter: null
  ) {
    collection {
      ... on Document {
        id
        score
        source
      }
    }
    paginationInfo {
      lastPage
      itemsPerPage
      totalCount
    }
    sortInfo {
      current {
        field
        direction
      }
    }
  }
}

variables = {
    "localizedCatalog": "com_en",
    "requestType": "product_search",
    "currentPage": 1,
    "pageSize": 5,
    "search": "Dress",
    "categoryEntityType": "category",
    "categoryLocalizedCatalog": "com_en",
    "categoryCurrentPage": 1,
    "categoryPageSize": 5,
    "categorySearch": "Dress"
}