{"id":13617202,"url":"https://github.com/jolicode/elastically","last_synced_at":"2025-05-14T18:04:35.108Z","repository":{"id":37848026,"uuid":"181847651","full_name":"jolicode/elastically","owner":"jolicode","description":"🔍 JoliCode's Elastica wrapper to bootstrap Elasticsearch PHP integrations","archived":false,"fork":false,"pushed_at":"2024-11-21T22:34:21.000Z","size":299,"stargazers_count":253,"open_issues_count":15,"forks_count":41,"subscribers_count":17,"default_branch":"master","last_synced_at":"2025-04-13T12:45:08.963Z","etag":null,"topics":["elastica","elasticsearch","hacktoberfest","php","symfony"],"latest_commit_sha":null,"homepage":"","language":"PHP","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/jolicode.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2019-04-17T08:18:27.000Z","updated_at":"2025-03-29T00:13:48.000Z","dependencies_parsed_at":"2023-11-08T12:24:49.077Z","dependency_job_id":"28918d63-0c48-4a9a-a5d1-ac209a850d3f","html_url":"https://github.com/jolicode/elastically","commit_stats":{"total_commits":134,"total_committers":24,"mean_commits":5.583333333333333,"dds":0.7388059701492538,"last_synced_commit":"08b1ab072448db86e4b1f1f70291472f65f85499"},"previous_names":[],"tags_count":27,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jolicode%2Felastically","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jolicode%2Felastically/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jolicode%2Felastically/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jolicode%2Felastically/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/jolicode","download_url":"https://codeload.github.com/jolicode/elastically/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":254198514,"owners_count":22030965,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["elastica","elasticsearch","hacktoberfest","php","symfony"],"created_at":"2024-08-01T20:01:38.272Z","updated_at":"2025-05-14T18:04:35.096Z","avatar_url":"https://github.com/jolicode.png","language":"PHP","readme":"# Elastically, **Elastica** based framework\n\nOpinionated [Elastica](https://github.com/ruflin/Elastica) based framework to bootstrap PHP and Elasticsearch implementations.\n\nMain features:\n\n- \u003cabbr title=\"Data Transfer Object\"\u003eDTO\u003c/abbr\u003e are **first class citizen**, you send PHP object as documents, and get objects back on search results, **like an ODM**;\n- All indexes are versioned and aliased automatically;\n- Mappings are done via YAML files, PHP or custom via `MappingProviderInterface`;\n- Analysis is separated from mappings to ease reuse;\n- 100% compatibility with [ruflin/elastica](https://github.com/ruflin/Elastica);\n- Mapping migration capabilities with ReIndex;\n- Symfony HttpClient compatible transport (**optional**);\n- Symfony support (**optional**):\n    - See dedicated [chapter](#usage-in-symfony);\n    - Tested with Symfony 5.4 to 7;\n    - Symfony Messenger Handler support (with or without spool);\n\n\u003e [!IMPORTANT]\n\u003e Require PHP 8.0+ and Elasticsearch 8+.\n\nWorks with **Elasticsearch 7** as well, but is not officially supported by Elastica 8. Use with caution.\n\nVersion 2+ does not work with **OpenSearch** anymore due to restrictions added by Elastic on their client.\n\nYou can check the [changelog](CHANGELOG.md) and the [upgrade](UPGRADE.md) documents.\n\n## Installation\n\n```\ncomposer require jolicode/elastically\n```\n\n## Demo\n\n\u003e [!TIP]\n\u003e If you are using Symfony, you can move to the Symfony [chapter](#usage-in-symfony)\n\nQuick example of what the library do on top of Elastica:\n\n```php\n// Your own DTO, or one generated by Jane (see below)\nclass Beer\n{\n    public string $foo;\n    public string $bar;\n}\n\nuse JoliCode\\Elastically\\Factory;\nuse JoliCode\\Elastically\\Model\\Document;\n\n// Factory object with Elastica options + new Elastically options in the same array\n$factory = new Factory([\n    // Where to find the mappings\n    Factory::CONFIG_MAPPINGS_DIRECTORY =\u003e __DIR__.'/mappings',\n    // What objects to find in each index\n    Factory::CONFIG_INDEX_CLASS_MAPPING =\u003e [\n        'beers' =\u003e Beer::class,\n    ],\n]);\n\n// Class to perform request, same as the Elastica Client\n$client = $factory-\u003ebuildClient();\n\n// Class to build Indexes\n$indexBuilder = $factory-\u003ebuildIndexBuilder();\n\n// Create the Index in Elasticsearch\n$index = $indexBuilder-\u003ecreateIndex('beers');\n\n// Set the proper aliases\n$indexBuilder-\u003emarkAsLive($index, 'beers');\n\n// Class to index DTO(s) in an Index\n$indexer = $factory-\u003ebuildIndexer();\n\n$dto = new Beer();\n$dto-\u003ebar = 'American Pale Ale';\n$dto-\u003efoo = 'Hops from Alsace, France';\n\n// Add a document to the queue\n$indexer-\u003escheduleIndex('beers', new Document('123', $dto));\n$indexer-\u003eflush();\n\n// Set parameters on the Bulk\n$indexer-\u003esetBulkRequestParams([\n    'pipeline' =\u003e 'covfefe',\n    'refresh' =\u003e 'wait_for'\n]);\n\n// Force index refresh if needed\n$indexer-\u003erefresh('beers');\n\n// Get the Document (new!)\n$results = $client-\u003egetIndex('beers')-\u003egetDocument('123');\n\n// Get the DTO (new!)\n$results = $client-\u003egetIndex('beers')-\u003egetModel('123');\n\n// Perform a search\n$results = $client-\u003egetIndex('beers')-\u003esearch('alsace');\n\n// Get the Elastic Document\n$results-\u003egetDocuments()[0];\n\n// Get the Elastica compatible Result\n$results-\u003egetResults()[0];\n\n// Get the DTO 🎉 (new!)\n$results-\u003egetResults()[0]-\u003egetModel();\n\n// Create a new version of the Index \"beers\"\n$index = $indexBuilder-\u003ecreateIndex('beers');\n\n// Slow down the Refresh Interval of the new Index to speed up indexation\n$indexBuilder-\u003eslowDownRefresh($index);\n$indexBuilder-\u003espeedUpRefresh($index);\n\n// Set proper aliases\n$indexBuilder-\u003emarkAsLive($index, 'beers');\n\n// Clean the old indices (close the previous one and delete the older)\n$indexBuilder-\u003epurgeOldIndices('beers');\n\n// Mapping change? Just call migrate and enjoy a full reindex (use the Task API internally to avoid timeout)\n$newIndex = $indexBuilder-\u003emigrate($index);\n$indexBuilder-\u003espeedUpRefresh($newIndex);\n$indexBuilder-\u003emarkAsLive($newIndex, 'beers');\n```\n\n\u003e [!NOTE]\n\u003e `scheduleIndex` is here called with `\"beers\"` index because the index was already created before.\n\u003e If you are creating a new index and want to index documents into it, you should pass the `Index` object directly.\n\n*mappings/beers_mapping.yaml*\n\n```yaml\n# Anything you want, no validation\nsettings:\n    number_of_replicas: 1\n    number_of_shards: 1\n    refresh_interval: 60s\nmappings:\n    dynamic: false\n    properties:\n        foo:\n            type: text\n            analyzer: english\n            fields:\n                keyword:\n                    type: keyword\n```\n\n## Configuration\n\nThis library add custom configurations on top of Elastica's:\n\n### `Factory::CONFIG_MAPPINGS_DIRECTORY` (required with default configuration)\n\nThe directory Elastically is going to look for YAML.\n\nWhen creating a `foobar` index, a `foobar_mapping.yaml` file is expected.\n\nIf an `analyzers.yaml` file is present, **all** the indices will get it.\n\n### `Factory::CONFIG_INDEX_CLASS_MAPPING` (required)\n\nAn array of index name to class FQN.\n\n```php\n[\n  'indexName' =\u003e My\\AwesomeDTO::class,\n]\n```\n\n### `Factory::CONFIG_MAPPINGS_PROVIDER`\n\nAn instance of `MappingProviderInterface`.\n\nIf this option is not defined, the factory will fall back to `YamlProvider` and will use\n`Factory::CONFIG_MAPPINGS_DIRECTORY` option.\n\nThere are two providers available in Elastically: `YamlProvider` and `PhpProvider`.\n\n### `Factory::CONFIG_SERIALIZER` (optional)\n\nA `SerializerInterface` compatible object that will be used on indexation.\n\n_Default to Symfony Serializer with Object Normalizer._\n\nA faster alternative is to use Jane to generate plain PHP Normalizer, see below. Also, we recommend [customization to handle things like Date](https://symfony.com/doc/current/components/serializer.html#normalizers).\n\n### `Factory::CONFIG_DENORMALIZER` (optional)\n\nA `DenormalizerInterface` compatible object that will be used on search results to build your objects back.\n\nIf this option is not defined, the factory will fall back to\n`Factory::CONFIG_SERIALIZER` option.\n\n### `Factory::CONFIG_SERIALIZER_CONTEXT_BUILDER` (optional)\n\nAn instance of `ContextBuilderInterface` that build a serializer context from a\nclass name.\n\nIf it is not defined, Elastically, will use a `StaticContextBuilder` with the\nconfiguration from `Factory::CONFIG_SERIALIZER_CONTEXT_PER_CLASS`.\n\n### `Factory::CONFIG_SERIALIZER_CONTEXT_PER_CLASS` (optional)\n\nAllow to specify the Serializer context for normalization and denormalization.\n\n```php\n[\n    Beer::class =\u003e ['attributes' =\u003e ['title']],\n];\n```\n\n_Default to `[]`._\n\n### `Factory::CONFIG_BULK_SIZE` (optional)\n\nWhen running indexation of lots of documents, this setting allow you to fine-tune the number of document threshold.\n\n_Default to 100._\n\n### `Factory::CONFIG_INDEX_PREFIX` (optional)\n\nAdd a prefix to all indexes and aliases created via Elastically.\n\n_Default to `null`._\n\n## Usage in Symfony\n\n### Configuration\n\nYou'll need to add the bundle in `bundles.php`:\n\n```php\n// config/bundles.php\nreturn [\n    // ...\n    JoliCode\\Elastically\\Bridge\\Symfony\\ElasticallyBundle::class =\u003e ['all' =\u003e true],\n];\n```\n\nThen configure the bundle:\n\n```yaml\n# config/packages/elastically.yaml\nelastically:\n    connections:\n        # You can create multiple clients\n        default:\n            # Any Elastica option works here\n            client:\n                hosts:\n                    - '127.0.0.1:9200'\n                # Use HttpClient component\n                transport_config:\n                    http_client: 'Psr\\Http\\Client\\ClientInterface'\n\n            # Path to the mapping directory (in YAML)\n            mapping_directory:       '%kernel.project_dir%/config/elasticsearch'\n\n            # Size of the bulk sent to Elasticsearch (default to 100)\n            bulk_size:               100\n\n            # Mapping between an index name and a FQCN\n            index_class_mapping:\n                my-foobar-index:     App\\Dto\\Foobar\n\n            # Configuration for the serializer\n            serializer:\n                # Fill a static context\n                context_mapping:\n                    foo:                 bar\n\n            # If you want to add a prefix for your index in elasticsearch (you can still call it by its base name everywhere!)\n            # prefix: '%kernel.environment%'\n```\n\nFinally, inject one of those service (autowirable) in you code where you need\nit:\n\n```\nJoliCode\\Elastically\\Client (elastically.default.client)\nJoliCode\\Elastically\\IndexBuilder (elastically.default.index_builder)\nJoliCode\\Elastically\\Indexer (elastically.default.indexer)\n```\n\n#### Advanced Configuration\n\n##### Multiple Connections and Autowiring\n\nIf you define multiple connections, you can define a default one. This will be\nuseful for autowiring:\n\n```yaml\nelastically:\n    default_connection: default\n    connections:\n        default: # ...\n        another: # ...\n```\n\nTo use class for other connection, you can use *Autowirable Types*. To discover\nthem, run:\n\n```\nbin/console debug:autowiring elastically\n```\n\n##### Use a Custom Serializer Context Builder\n\n```yaml\nelastically:\n    default_connection: default\n    connections:\n        default:\n            serializer:\n                context_builder_service: App\\Elastically\\Serializer\\ContextBuilder\n                # Do not define \"context_mapping\" option anymore\n```\n\n##### Use a Custom Mapping provider\n\n```yaml\nelastically:\n    default_connection: default\n    connections:\n        default:\n            mapping_provider_service: App\\Elastically\\MappingProvider\n            # Do not define \"index_class_mapping\" option anymore\n```\n\n##### Using HttpClient as Transport\n\nYou can also use the Symfony HttpClient for all Elastica communications:\n\n```yaml\nJoliCode\\Elastically\\Transport\\HttpClientTransport: ~\n\nJoliCode\\Elastically\\Client:\n    arguments:\n        $config:\n            hosts:\n                - '127.0.0.1:9200'\n            transport_config:\n                http_client: 'Psr\\Http\\Client\\ClientInterface'\n```\n\nSee the [official documentation on how to get a PSR-18 client](https://symfony.com/doc/current/http_client.html#psr-18-and-psr-17).\n\n#### Reference\n\nYou can run the following command to get the default configuration reference:\n\n```\nbin/console config:dump elastically\n```\n\n### Using Messenger for async indexing\n\nElastically ships with a default Message and Handler for Symfony Messenger.\n\nRegister the message in your configuration:\n\n```yaml\nframework:\n    messenger:\n        transports:\n            async: \"%env(MESSENGER_TRANSPORT_DSN)%\"\n\n        routing:\n            # async is whatever name you gave your transport above\n            'JoliCode\\Elastically\\Messenger\\IndexationRequest':  async\n\nservices:\n    JoliCode\\Elastically\\Messenger\\IndexationRequestHandler: ~\n```\n\nThe `IndexationRequestHandler` service depends on an implementation of `JoliCode\\Elastically\\Messenger\\DocumentExchangerInterface`, which isn't provided by this library. You must provide a service that implements this interface, so you can plug your database or any other source of truth.\n\nThen from your code you have to call:\n\n```php\nuse JoliCode\\Elastically\\Messenger\\IndexationRequest;\nuse JoliCode\\Elastically\\Messenger\\IndexationRequestHandler;\n\n$bus-\u003edispatch(new IndexationRequest(Product::class, '1234567890'));\n\n// Third argument is the operation, so for a \"delete\" add this argument:\n// new IndexationRequest(Product::class, 'ref9999', IndexationRequestHandler::OP_DELETE);\n```\n\nAnd then consume the messages:\n\n```sh\nphp bin/console messenger:consume async\n```\n\n### Grouping IndexationRequest in a spool\n\nSending multiple `IndexationRequest` during the same Symfony Request is not always appropriate, it will trigger multiple Bulk operations. Elastically provides a Kernel listener to group all the `IndexationRequest` in a single `MultipleIndexationRequest` message.\n\nTo use this mechanism, we send the `IndexationRequest` in a memory transport to be consumed and grouped in a really async transport:\n\n```yaml\nmessenger:\n    transports:\n        async: \"%env(MESSENGER_TRANSPORT_DSN)%\"\n        queuing: 'in-memory:///'\n\n    routing:\n        'JoliCode\\Elastically\\Messenger\\MultipleIndexationRequest': async\n        'JoliCode\\Elastically\\Messenger\\IndexationRequest': queuing\n```\n\nYou also need to register the subscriber:\n\n```yaml\nservices:\n    JoliCode\\Elastically\\Messenger\\IndexationRequestSpoolSubscriber:\n        arguments:\n            - '@messenger.transport.queuing' # should be the name of the memory transport\n            - '@messenger.default_bus'\n        tags:\n            - { name: kernel.event_subscriber }\n```\n\n## Using Jane to build PHP DTO and fast Normalizers\n\nInstall [JanePHP](https://jane.readthedocs.io/) json-schema tools to build your own DTO and Normalizers. All you have to do is setting the Jane-completed Serializer on the Factory:\n\n```php\n$factory = new Factory([\n    Factory::CONFIG_SERIALIZER =\u003e $serializer,\n]);\n```\n\n\u003e [!CAUTION]\n\u003e Elastically is [not compatible with Jane \u003c 6](https://github.com/jolicode/elastically/issues/12).\n\n## To be done\n\n- some \"todo\" in the code\n- optional Doctrine connector\n- extra commands to monitor, update mapping, reindex... Commonly implemented tasks\n- optional Symfony integration:\n  - web debug toolbar!\n- scripts / commands for common tasks:\n  - auto-reindex when the mapping change, handle the aliases and everything\n  - micro monitoring for cluster / indexes\n  - health-check method\n\n## Sponsors\n\n[![JoliCode](https://jolicode.com/images/logo.svg)](https://jolicode.com)\n\nOpen Source time sponsored by JoliCode.\n","funding_links":[],"categories":["PHP"],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fjolicode%2Felastically","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fjolicode%2Felastically","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fjolicode%2Felastically/lists"}