Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Elasticsearch Query Builder Laravel Package

erichard/elasticsearch-query-builder

A fluent PHP query builder for Elasticsearch that lets you construct complex search queries through an elegant, chainable API instead of hand-writing JSON. A good fit for Laravel and standalone PHP projects that need readable, maintainable Elasticsearch DSL.

Deep Wiki
Context7

Getting Started

Installation

composer require erichard/elasticsearch-query-builder

You also need an Elasticsearch PHP client (elasticsearch/elasticsearch) since this package only builds queries — it does not ship a transport. In a Laravel app the client is usually bound in a service provider or resolved via Elastic\Elasticsearch\ClientBuilder::fromConfig(config('elasticsearch')).

Where to look first

  1. README.md — the method-by-method examples are the fastest way to learn the fluent surface.
  2. src/QueryBuilder.php — the entry point (where, index, size, from, sort, get, first, count, search, toArray).
  3. src/Query/ and src/Aggregation/ — the DSL nodes the builder assembles; useful when you need to build something the fluent API does not expose.
  4. src/Clients/ (or wherever the client wrapper lives) — this is where the actual search() / index() / delete() calls are dispatched.

First use case

Replace a hand-rolled DSL array with a chained builder:

use Erichard\ElasticQueryBuilder\QueryBuilder;

$builder = new QueryBuilder($client);
$builder->setIndex('posts');
$builder->where('status', 'published');
$builder->setSize(20);

$response = $builder->get();        // returns the raw ES response
$hits     = $response['hits']['hits'];

The win is composition: conditionally adding where, addSort, or aggregations without building a nested bool array by hand.

Implementation Patterns

Repository / service wrapper

Do not scatter new QueryBuilder(...) across controllers. Wrap it in a repository class so the index name, client, and default filters live in one place:

class PostSearchRepository
{
    public function __construct(private \Elastic\Elasticsearch\Client $client) {}

    public function search(array $filters, int $page = 1, int $perPage = 20): array
    {
        $qb = new QueryBuilder($this->client);
        $qb->setIndex('posts');
        $qb->setFrom(($page - 1) * $perPage);
        $qb->setSize($perPage);

        if (!empty($filters['status'])) {
            $qb->where('status', $filters['status']);
        }
        if (!empty($filters['author_id'])) {
            $qb->where('author_id', $filters['author_id']);
        }
        if (!empty($filters['q'])) {
            $qb->where('title', $filters['q']); // or a match/multi_match node
        }

        $qb->addSort('published_at', 'desc');

        return $qb->get();
    }
}

Bind it in AppServiceProvider, inject it into controllers or jobs. This keeps the "index name string" out of application code and lets you swap indices per environment.

Composing conditional clauses

The fluent style shines when filters are optional. A small pattern is to build the base builder, then apply a list of closures:

$builder = new QueryBuilder($client);
$builder->setIndex('products');

collect($filters)->each(function ($value, $field) use ($builder) {
    if ($value !== null) {
        $builder->where($field, $value);
    }
});

Prefer explicit if blocks for readability and to make the mapping from request -> ES field obvious.

Pagination as a first-class concern

Treat from/size as your pagination contract. Because ES pagination is offset-based, mirror it in the response shape: return total from $response['hits']['total']['value'] and compute page counts manually. For deep pagination prefer search_after via a raw body rather than large from values.

Aggregations alongside hits

Use the builder's aggregation helpers and attach them to the same query:

$qb->addAggregation('by_status', /* terms aggregation */);
$response = $qb->get();
$buckets  = $response['aggregations']['by_status']['buckets'] ?? [];

Escape hatch for unsupported DSL

For anything the fluent API does not cover (function_score, custom analyzers, script_score, nested search_after), either drop into the underlying aggregation/query node classes in src/Query/ or set the raw body. Keep a documented "raw" path in your repository so you are not blocked by the library's surface area.

Index and document lifecycle

Use the builder/clients for index, bulk, delete, and refresh calls in seeders, artisan commands, and queue jobs. Model observers (saved, deleted) that enqueue a job calling the repository are the idiomatic Laravel integration — never index synchronously in a request cycle.

Gotchas and Tips

  • It is not an Eloquent replacement. There is no model hydration, no ->get() returning typed objects, no relationship loading. You get back raw ES arrays. Write a small toDto() mapper per hit; do not leak raw _source/_id into controllers.
  • You supply the client. Confirm which client version the package expects (elasticsearch/elasticsearch 7.x vs 8.x) and pin it in composer.json. Mismatched client versions are the most common install failure; ES 8's stricter headers and product checks are the usual culprit.
  • get() is a network call, toArray() is not. When debugging, inspect $builder->toArray() (or the equivalent DSL accessor) to see the exact body before it hits the cluster. Log this on failures — raw transport exceptions rarely show the body.
  • No automatic refresh. Immediately after indexing in tests or seeders, your search will not see documents until the index refreshes. Call refresh on the index (or use refresh: 'wait_for' on writes) in test setup.
  • where semantics. In many versions of this package where maps to a term query, not a full-text match. A term against an analyzed text field will not match multi-word input the way you expect. Be explicit: use match/multi_match nodes for search, term/terms for keyword fields. Know whether your field is text or keyword.
  • Missing fields silently return empty. Unlike SQL, ES will not error on a typo — you just get zero hits. Validate queries against a known document during development, and consider ?explain or _validate/query while debugging.
  • first() is convenience, not correctness. It sets size 1 and returns one hit; there is no ordering guarantee unless you sorted. Add an explicit sort for deterministic "latest" behaviour.
  • Total hits shape changed. ES 7+ returns hits.total as an object (value, relation) rather than an integer. Guard for both if you support mixed cluster versions.
  • Config quirks. Index names, default from/size, and timeouts tend to be hard-coded in examples. Centralise them in config/elasticsearch.php and pass them into the repository so staging/production indices differ cleanly.
  • Extension points. The cleanest extension is composition: a repository method that builds a QueryBuilder and adds your custom nodes from src/Query/. Subclassing QueryBuilder is possible but couples you to internal method signatures across upgrades — prefer wrappers.
  • Debugging checklist. No hits? (1) print toArray(), (2) run it via curl/Kibana Dev Tools, (3) check field mapping (GET index/_mapping), (4) check refresh, (5) check index name. That sequence resolves most "the builder is broken" reports.
  • Version pinning. With a small package in the 1.x line and infrequent releases, pin the exact version, run your indexing tests in CI against the same ES image as production,
Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
shitoudev/permissionmanager
johannesclimacus/laravel-tui
hocvt/seotools
vitalybaev/sypexgeo
amphibee/themosis-ignition
gito-me/fast-excel
frankyso/laravel-ipaymu
geolize-studio/laravel-chile
boomerangui/chat
jinas/dhivatar
store-doc/laravel-storage-driver
plus-it/laravel-translation-manager
diephp/perhaps
diephp/telegraph
osmaviation/spreadsheet
microsoft/fluentui-system-icons
mmccook/php-json-canonicalization-scheme
middlewares/firewall
qubiqx/montapacking
nyoncode/laravel-package-console