jolicode/elastically
jolicode/elastically is an elegant Elasticsearch client for PHP. It simplifies index management, document indexing, and query building with a fluent API, making search integration cleaner and more maintainable in Symfony and other PHP applications.
composer require jolicode/elastically. Add symfony/serializer only if you want automatic model hydration, and doctrine/orm only if you plan to use Doctrine populators. The Symfony Flex recipe creates config/packages/elastically.yaml.elastically.yaml and one index config file. That is where the whole integration lives.# config/packages/elastically.yaml
elastically:
clients:
default:
hosts: ['%env(ELASTICSEARCH_DSN)%']
indices:
articles:
client: default
config: '%kernel.project_dir%/config/elastically/articles.yaml'
# config/elastically/articles.yaml
settings:
number_of_shards: 1
mappings:
properties:
title: { type: text }
publishedAt: { type: date }
aliases:
- articles
php bin/console elastically:index:create articles
php bin/console elastically:index:list
services.yaml instead of injecting the client everywhere:services:
_defaults:
bind:
Elastica\Index $articlesIndex: '@elastica.index.articles'
final class ArticleSearch
{
public function __construct(private Elastica\Index $articlesIndex) {}
public function search(string $term): array
{
$query = new Elastica\Query\MatchQuery('title', $term);
return array_map(
fn ($result) => $result->getData(),
$this->articlesIndex->search($query)->getResults()
);
}
}
Elastica\Index $articlesIndex: '@elastica.index.articles'). Keep JoliCode\Elastically\Client only in code that manages indices or populates them.IndexBuilder for lifecycle operations rather than calling Elastica directly. It applies your YAML config and handles aliases:$builder = $client->getIndexBuilder();
$builder->createIndex('articles'); // raw creation
$builder->buildIndex('articles'); // create from config, alias-aware
$builder->reindex('articles'); // build a fresh index and swap the alias
articles (alias), writers build articles_<timestamp> (physical index). This is what makes zero-downtime reindexing possible.IndexBuilder for imports, flush in chunks, and check the bulk response for per-item errors — a bulk call can return HTTP 200 while individual documents failed.elastically:index:populate articles --populator=.... Make populators idempotent and chunked so they can be re-run during reindexes.Result::getModel(); keep getData() / getDocument() for cases where you only need raw source. This separates search plumbing from domain objects.ArticleSearch/ArticleRepository also gives you a clean seam for tests.elastically:index:create articles in fresh environments; for running environments use the “live”/reindex flow (elastically:index:live articles or IndexBuilder::reindex()), which builds the new index, populates it, and swaps aliases atomically.elastically:index:update. Elasticsearch only permits additive mapping changes; changing a field type requires a new index plus alias swap. Plan reindexes, don't patch.articles_v1 physically and articles as the alias.Elastica\Index and Elastica\Finder need explicit named bindings, otherwise Symfony cannot decide which index to inject.refresh=wait_for on writes when immediacy matters; otherwise batch and refresh explicitly.bin/console cache:clear). Stale config in commands is a common source of “my mappings didn't change”.Elastica\Type-based code and index/type-style service IDs; use index-level searches.index_prefix changes what is created in the cluster; commands and service IDs still use the logical name. When confused, run elastically:index:list and compare with _cat/indices?v / _cat/aliases?v.elasticsearch Monolog channel for the PHP client, _cat APIs for cluster state, and elastically:index:list for the package's own view. For zero-downtime reindexes, verify search results on the new alias before deleting the old physical index.How can I help you explore Laravel packages today?