Mapping

Elasticsearch bundle requires mapping definitions for it to work with complex operations, like insert and update documents, do a full-text search, etc.

Mapping configuration

Here's an example of configuration containing the definitions of filter and analyzer:

ongr_elasticsearch:
    analysis:
        filter:
            incremental_filter:
                type: edge_ngram
                min_gram: 1
                max_gram: 20
        analyzer:
            incrementalAnalyzer:  #-> analyzer name
                type: custom
                tokenizer: standard
                filter:
                    - lowercase
                    - incremental_filter
    managers:
        default:
            index:
                index_name: your_index_name
                hosts:
                    - 127.0.0.1:9200
            mappings:
                - AppBundle

From 5.0 version mapping was enchased, and now you can change documents directory. See the example below:

#...
    managers:
        custom_dir:
            index:
                index_name: your_index_name
                hosts:
                    - 127.0.0.1:9200
            mappings:
                AppBundle: ~ #Document dir will be Document.
                CustomBundle:
                    document_dir: Entity #For this bundle will search documents in the Entity.
                    
        default:
            index:
                index_name: your_index_name
                hosts:
                    - 127.0.0.1:9200
            mappings:
                - AppBundle

Both mappings are valid. In the above example, you can change the directory for the particular bundles where to find documents. Default dir remains Document.

At the very top, you can see analysis node. It represents Elasticsearch analysis. You can define here analyzers, tokenizers, token filters and character filters. Once you define any analysis, then it can be used in any document mapping.

e.g. let's say you want to use incremental analyzer and custom lowercase filter analyzer in your index. The elasticsearch settings mapping would like this:

//PUT my_index
{
    "settings": {
        "analysis": {
            "filter": {
                "incremental_filter": {
                    "type": "edge_ngram",
                    "min_gram": "1",
                    "max_gram": "100"
                }
            },
            "analyzer": {
                "keywordAnalyzer": {
                "filter": [
                    "lowercase"
                ],
                "type": "custom",
                "tokenizer": "keyword"
            },
            "incrementalAnalyzer": {
                "filter": [
                    "lowercase",
                    "asciifolding",
                    "incremental_filter"
                ],
                "type": "custom",
                "tokenizer": "standard"
                }
            }
        }
    }
}

The representation of this particular example in the elasticsearch configuration:

ongr_elasticsearch:
    analysis:
        analyzer:
            keywordAnalyzer:
                type: custom
                tokenizer: keyword
                filter: [lowercase]
            incrementalAnalyzer:
                type: custom
                tokenizer: standard
                filter:
                    - lowercase
                    - asciifolding
                    - incremental_filter
        filter:
            incremental_filter:
                type: edge_ngram
                min_gram: 1
                max_gram: 100
    managers:
        default:
            index:
                index_name: your_index_name
                hosts:
                    - 127.0.0.1:9200
            mappings:
                - AppBundle

Document class annotations

Lets start with a document class example.

// src/AppBundle/Document/Content.php

namespace AppBundle\Document;

use ONGR\ElasticsearchBundle\Annotation as ES;

/**
 * @ES\Document(type="product")
 */
class Product
{
    /**
     * @ES\Property(type="text", name="title_in_es")
     */
    private $title;

    /**
     * Sets title
     *
     * @param string $title
     */
    public function setTitle($title)
    {
        $this->title = $title;
    }

    /**
     * Returns title
     *
     * @return string
     */
    public function getTitle()
    {
        return $this->title;
    }
}

It is not mandatory to have private properties, and public will work as well. However, we firmly recommend using private according to OOP best practices.

Document annotation configuration

Properties annotations

For defining type properties, there is a @ES\Property annotation. The only required attribute is type - Elasticsearch field type to specify what kind of information will be indexed. By default, the field name is generated from property name by converting it to "snake case" string. You can specify a custom name by setting the name attribute.

Read more about elasticsearch supported types in the official documentation.

To add a custom setting for the property like analyzer include it in the options variable. Analyzers names must be defined in config.yml under the analysis node (read more in the topic above). Here's an example how to add it:

// src/AppBundle/Document/Product.php
namespace AppBundle\Document;

use ONGR\ElasticsearchBundle\Annotation as ES;

/**
 * @ES\Document()
 */
class Product
{
    // ...

    /**
     * @ES\Property(
        type="text",
        options={"analyzer":"incrementalAnalyzer"}
      )
     */
    private $title;
    
    //....

options container accepts any parameters in annotation array format. We leave mapping validation to elasticsearch and elasticsearch-php client. If there will be invalid format annotations reader will throw exception, otherwise elasticsearch-php or elasticsearch database will throw an exception if something is wrong.

Object and Nested types

To define a nested or object type you have to use @ES\Embedded annotation and create a separate class for this annotation. Here's an example, lets assume we have a Product type with Variant object field.

// src/AppBundle/Document/Product.php

namespace AppBundle\Document;

use ONGR\ElasticsearchBundle\Annotation as ES;

/**
 * @ES\Document()
 */
class Product
{
    /**
     * @ES\Property(type="string")
     */
    private $title;

    /**
     * @var ContentMetaObject
     *
     * @ES\Embedded(class="AppBundle:CategoryObject")
     */
    private $category;

    //...
}

And the Category object will look like:

// src/AppBundle/Document/CategoryObject.php

namespace AppBundle\Document;

use ONGR\ElasticsearchBundle\Annotation as ES;

/**
 * @ES\Object
 */
class CategoryObject
{
    /**
     * @ES\Property(type="string")
     */
    private $title;

    //...
}

Class name can be anything, we called it CategoryObject to make it more readable. Notice that it is an object, not a document.

For this particular example the mapping in elasticsearch will look like this:

 {
    "product": {
        "properties": {
            "title": {
                "type": "text"
            },
            "category": {
                "type": "object",
                "properties": {
                    "title": {
                        "type": "string"
                    }
                }
            }
        }
    }
}
Saving documents with relations

To insert a document with mapping from example above you have to create 2 objects:

 
  $category = new CategoryObject();
  $category->setTitle('Jeans');
  
  $product = new Product();
  $product->setTitle('Orange Jeans');
  $product->setCategoryObject($category);
  
  //manager to work with elasticsearch index
  $manager->persist($product);
  $manager->commit();
 
Multiple objects

As shown in the example above, by ElasticsearchBundle default, only a single object will be saved in the document. Meanwhile, Elasticsearch database doesn't care if in an object is stored as a single value or as an array. If it is necessary to store multiple objects (array), you have to add multiple=true to the annotation. While initiating a document with multiple items you need to initialize property with the new instance of ArrayCollection().

Here's an example:

// src/AppBundle/Document/Product.php

namespace AppBundle\Document;

use Doctrine\Common\Collections\ArrayCollection;
use ONGR\ElasticsearchBundle\Annotation as ES;

/**
 * @ES\Document()
 */
class Product
{
    /**
     * @ES\Property(type="string")
     */
    private $title;

    /**
     * @var ContentMetaObject
     *
     * @ES\Embedded(class="AppBundle:VariantObject", multiple=true)
     */
    private $variants;
    
    public function __construct()
    {
        $this->variants = new ArrayCollection();
    }
    
    /**
     * Adds variant to the collection.
     *
     * @param VariantObject $variant
     * @return $this
     */
    public function addVariant(VariantObject $variant)
    {
        $this->variants[] = $variant;

        return $this;
    }
    
    //...
}

And the object:

// src/AppBundle/Document/VariantObject.php

namespace AppBundle\Document;

use ONGR\ElasticsearchBundle\Annotation as ES;

/**
 * @ES\Object
 */
class VariantObject
{
    /**
     * @ES\Property(type="string")
     */
    private $color;

    //...
}

Insert action will look like this:

<?php
  
  $product = new Product();
  $product->setTitle('Orange Jeans');
  
  $variant = new VariantObject();
  $variant->setColor('orange');
  $product->addVariant($variant);

  $variant = new VariantObject();
  $variant->setColor('red');
  $product->addVariant($variant);

  $manager->persist($product);
  $manager->commit();

There is no bounds to define other objects within objects.

Nested types can be defined the same way as objects, except @ES\Nested annotation must be used.

The difference between @ES\Embedded and @ES\Nested is in the way that the Elasticsearch indexes them. While the values of the fields in embedded objects are extracted and put into the same array with all the other values of other embedded objects in the same field, during the indexation process, the values of the fields of nested objects stored separately. This introduces differences when querying and filtering the index.

Multi field annotations and usage

Within the properties annotation, you can specify the field attribute. It enables you to map several core types of the same value. This can come very handy, e.g. when you want to map a text type with analyzed and not analyzed values.

Lets take a look at example below:

    /**
     * @var string
     * @ES\Property(
     *  type="text",
     *  name="title",
     *  options={
     *    "analyzer"="keywordAnalyzer",
     *    "fields"={
     *        "raw"={"type"="keyword"},
     *        "standard"={"type"="text", "analyzer"="standard"}
     *    }
     *  }
     * )
     */
    public $title;

The mapping in elasticsearch would look like this:

{
    "product": {
        "properties": {
            "title": {
                "type": "text",
                "analyzer": "keywordAnalyzer",
                "fields": {
                    "raw": {
                        "type": "keyword"
                    },
                    "standard": {
                        "type": "text",
                        "analyzer": "standard"
                    }
                }
            }
        }
    }
}

You will notice that now title value is mapped both with and without the analyzer. Querying these fields will look like this:

        $query = new TermQuery('title', 'Bar');
        $search->addQuery($query);

        $result1 = $repo->execute($search);

        $query = new MatchQuery('title.raw', 'Bar');
        $search->addQuery($query);

        $result2 = $repo->execute($search);

        $query = new MatchQuery('title.standard', 'Bar');
        $search->addQuery($query);

        $result3 = $repo->execute($search);

Meta-Fields Annotations

There are specialized meta fields that introduce different behaviours of elasticsearch. Read the dedicated page about meta-field annotations here.

More information about mapping can be found in the Elasticsearch mapping documentation.