v4.4 Elasticsearch & OpenSearch Support

# Overview

Elasticsearch and OpenSearch are distributed RESTful search and analytics engines capable of solving a growing number of use cases: application search, security analytics, metrics, logging, etc. OpenSearch is an open-source fork of Elasticsearch.

API Platform comes natively with reading support for both Elasticsearch and OpenSearch. It uses internally the official PHP clients: Elasticsearch-PHP or OpenSearch-PHP.

API Platform supports Elasticsearch >= 7.11.0 < 8.0, Elasticsearch >= 8.4 < 9.0, and OpenSearch >= 2.x. Support for Elasticsearch 8 was introduced in API Platform 3.2, and OpenSearch support was introduced in API Platform 4.3.

# Enabling Reading Support

To enable the reading support, require the appropriate PHP client using Composer.

For Elasticsearch 8:

composer require elasticsearch/elasticsearch:^8.4

For Elasticsearch 7:

composer require elasticsearch/elasticsearch:^7.11

For OpenSearch:

composer require opensearch-project/opensearch-php:^2.5

Then, enable it inside the API Platform configuration, using one of the configurations below:

# Enabling Reading Support using Symfony

For Elasticsearch:

# api/config/packages/api_platform.yaml
parameters:
    # ...
    env(ELASTICSEARCH_HOST): "http://localhost:9200"

api_platform:
    # ...

    mapping:
        paths: ["%kernel.project_dir%/src/Model"]

    elasticsearch:
        hosts: ["%env(ELASTICSEARCH_HOST)%"]

    #...

For OpenSearch, set the client option to opensearch:

# api/config/packages/api_platform.yaml
parameters:
    # ...
    env(OPENSEARCH_HOST): "http://localhost:9200"

api_platform:
    # ...

    mapping:
        paths: ["%kernel.project_dir%/src/Model"]

    elasticsearch:
        client: opensearch
        hosts: ["%env(OPENSEARCH_HOST)%"]

    #...

# SSL Configuration

When connecting over HTTPS with self-signed certificates or custom Certificate Authorities, you can configure SSL verification. This works for both Elasticsearch and OpenSearch.

With a custom CA bundle:

# config/packages/api_platform.yaml
api_platform:
    elasticsearch:
        hosts: ["%env(ELASTICSEARCH_HOST)%"]
        ssl_ca_bundle: "/path/to/ca-bundle.crt"

Disable SSL verification (dev/test only):

# config/packages/api_platform.yaml
api_platform:
    elasticsearch:
        hosts: ["%env(ELASTICSEARCH_HOST)%"]
        ssl_verification: false # Never use in production

You cannot use both options together.

# Enabling Reading Support using Laravel

For Elasticsearch:

<?php
// config/api-platform.php
return [
    // ....
    'mapping' => [
        'paths' => [
            base_path('app/Models'),
        ],
    ],
    'elasticsearch' => [
        'hosts' => [
            env('ELASTICSEARCH_HOST', 'http://localhost:9200'),
        ],
    ],
];

For OpenSearch:

<?php
// config/api-platform.php
return [
    // ....
    'mapping' => [
        'paths' => [
            base_path('app/Models'),
        ],
    ],
    'elasticsearch' => [
        'client' => 'opensearch',
        'hosts' => [
            env('OPENSEARCH_HOST', 'http://localhost:9200'),
        ],
    ],
];

# Creating Models

API Platform follows the best practices of Elasticsearch:

This involves having mappings and models which absolutely match each other.

Here is an example of mappings for 2 resources, User and Tweet, and their models:

PUT user

{
    "mappings": {
        "_doc": {
            "properties": {
                "id": {
                    "type": "keyword"
                },
                "gender": {
                    "type": "keyword"
                },
                "age": {
                    "type": "integer"
                },
                "first_name": {
                    "type": "text"
                },
                "last_name": {
                    "type": "text"
                },
                "tweets": {
                    "type": "nested",
                    "properties": {
                        "id": {
                            "type": "keyword"
                        },
                        "date": {
                            "type": "date",
                            "format": "yyyy-MM-dd HH:mm:ss"
                        },
                        "message": {
                            "type": "text"
                        }
                    },
                    "dynamic": "strict"
                }
            },
            "dynamic": "strict"
        }
    }
}

PUT tweet

{
    "mappings": {
        "_doc": {
            "properties": {
                "id": {
                    "type": "keyword"
                },
                "author": {
                    "properties": {
                        "id": {
                            "type": "keyword"
                        },
                        "gender": {
                            "type": "keyword"
                        },
                        "age": {
                            "type": "integer"
                        },
                        "first_name": {
                            "type": "text"
                        },
                        "last_name": {
                            "type": "text"
                        }
                    },
                    "dynamic": "strict"
                },
                "date": {
                    "type": "date",
                    "format": "yyyy-MM-dd HH:mm:ss"
                },
                "message": {
                    "type": "text"
                }
            },
            "dynamic": "strict"
        }
    }
}
<?php
// api/src/Model/User.php with Symfony or app/Model/User.php with Laravel
namespace App\Model;

use ApiPlatform\Elasticsearch\State\CollectionProvider;
use ApiPlatform\Elasticsearch\State\ItemProvider;
use ApiPlatform\Elasticsearch\State\Options;
use ApiPlatform\Metadata\ApiProperty;
use ApiPlatform\Metadata\ApiResource;
use ApiPlatform\Metadata\Get;
use ApiPlatform\Metadata\GetCollection;

#[ApiResource(
    operations: [
        new GetCollection(provider: CollectionProvider::class, stateOptions: new Options(index: 'user')),
        new Get(provider: ItemProvider::class, stateOptions: new Options(index: 'user')),
    ],
)]
class User
{
    #[ApiProperty(identifier: true)]
    public string $id = '';

    public string $gender;

    public int $age;

    public string $firstName;

    public string $lastName;

    /**
     * @var Tweet[]
     */
    public iterable $tweets = [];
}
<?php
// api/src/Model/Tweet.php with Symfony or app/Model/Tweet.php with Laravel
namespace App\Model;

use ApiPlatform\Elasticsearch\State\CollectionProvider;
use ApiPlatform\Elasticsearch\State\ItemProvider;
use ApiPlatform\Elasticsearch\State\Options;
use ApiPlatform\Metadata\ApiProperty;
use ApiPlatform\Metadata\ApiResource;
use ApiPlatform\Metadata\Get;
use ApiPlatform\Metadata\GetCollection;

#[ApiResource(
    operations: [
        new GetCollection(provider: CollectionProvider::class, stateOptions: new Options(index: 'tweet')),
        new Get(provider: ItemProvider::class, stateOptions: new Options(index: 'tweet')),
    ],
)]
class Tweet
{
    #[ApiProperty(identifier: true)]
    public string $id = '';

    public User $author;

    public \DateTimeInterface $date;

    public string $message;
}

API Platform will automatically disable write operations.

Keep in mind that it is your responsibility to populate your Elasticsearch index. To do so, you can use Logstash, a custom state processors or any other mechanism that suits your project (such as an ETL).

You’re done! The API is now ready to use.

# Mapping Field Names

By default, API Platform does not convert Elasticsearch document field names: a _source field is mapped to the object property of the same name (unless #[SerializedName] is used). Use camelCase field names in your mappings, as described above.

If your documents use snake_case field names, configure a name converter so that they are mapped to camelCase properties. There are two options.

To convert names for every format, set the global name_converter in the API Platform configuration. This also changes the serialization of all other formats:

# api/config/packages/api_platform.yaml
api_platform:
    name_converter: "serializer.name_converter.camel_case_to_snake_case"

To convert names for Elasticsearch only, override the api_platform.elasticsearch.name_converter.inner_fields service and pass it the converter:

# api/config/services.yaml
services:
    api_platform.elasticsearch.name_converter.inner_fields:
        class: ApiPlatform\Elasticsearch\Serializer\NameConverter\InnerFieldsNameConverter
        arguments:
            - "@serializer.name_converter.camel_case_to_snake_case"

The same name converter builds the field names used by the Elasticsearch filters and by

sorting. Once a converter is set, a filter or sort on the firstName property targets the first_name field of the index.

# Filtering

See how to use Elasticsearch filters and how to create Elasticsearch custom filters in the Elasticsearch filters documentation.

# Creating Custom Extensions

See how to create Elasticsearch custom extensions in the Extensions chapter.

You can also help us improve the documentation of this page.

Using an AI coding agent? See the documentation index for LLMs at /docs/llms.txt.

Made with love by

Les-Tilleuls.coop can help you design and develop your APIs and web projects, and train your teams in API Platform, Symfony, Next.js, Kubernetes and a wide range of other technologies.

Learn more

Copyright © 2023 Kévin Dunglas

Sponsored by Les-Tilleuls.coop