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

Enterprise Search Laravel Package

elastic/enterprise-search

A Laravel package that integrates Elastic Enterprise Search, offering a clean client, Eloquent-style model syncing, and fluent query building for indexing and searching documents. Configure engines, run queries, and keep your data in sync with minimal setup.

View on GitHub
Deep Wiki
Context7

Getting Started

The package is a thin, generated PHP SDK over the Enterprise Search REST API. It exposes three sub-clients: App Search (site/app search with engines, documents, relevance tuning), Workplace Search (internal content sources, connectors), and an embedded Elasticsearch client for raw queries. It is framework-agnostic — Laravel integration is entirely on you.

1. Install and configure

composer require elastic/enterprise-search

The Enterprise Search host is not your Elasticsearch host. On Elastic Cloud it looks like https://<deployment>.ent-search.<region>.<cloud>.es.io; self-hosted is usually http://localhost:3002.

// config/enterprise-search.php
return [
    'host' => env('ENTERPRISE_SEARCH_HOST', 'http://localhost:3002'),
    'app_search' => [
        'api_key' => env('APP_SEARCH_API_KEY'),
    ],
    'workplace_search' => [
        'api_key' => env('WORKPLACE_SEARCH_API_KEY'),
    ],
    'elasticsearch' => [
        'api_key' => env('ENTERPRISE_SEARCH_ES_API_KEY'),
    ],
];

2. Bind the client as a singleton — building it per request is wasteful and leaks connections in long-running Octane workers.

// app/Providers/EnterpriseSearchServiceProvider.php
public function register(): void
{
    $this->mergeConfigFrom(config_path('enterprise-search.php'), 'enterprise-search');

    $this->app->singleton(\Elastic\EnterpriseSearch\Client::class, fn () => new \Elastic\EnterpriseSearch\Client([
        'host' => config('enterprise-search.host'),
        'app-search' => ['api_key' => config('enterprise-search.app_search.api_key')],
        'workplace-search' => ['api_key' => config('enterprise-search.workplace_search.api_key')],
    ]));
}

3. First use case — query an App Search engine

use Elastic\EnterpriseSearch\AppSearch\Request\Search;
use Elastic\EnterpriseSearch\AppSearch\Schema\SearchRequestParams;

$params = new SearchRequestParams();
$params->query = 'wireless headphones';
$params->page = 1;
$params->pageSize = 20;

$response = app(\Elastic\EnterpriseSearch\Client::class)
    ->appSearch()
    ->search(new Search('products', $params));

return response()->json($response->toArray());

Where to look first: the src/AppSearch/Request/* and src/AppSearch/Schema/* directories are your real API — every endpoint and every request/response shape is a class in there, and your IDE will autocomplete the property names. The Enterprise Search dashboard in Kibana is where you provision engines, mint API keys, and tune relevance; the SDK only drives it.

Implementation Patterns

Repository wrapper around the client. Don't inject Client into controllers and call ->appSearch()->search() inline — the chained dynamic dispatch is hard to test and hard to swap. Wrap it once:

final class ProductSearch
{
    public function __construct(private Client $client) {}

    public function query(string $q, int $page = 1, ?string $category = null): array
    {
        $params = new SearchRequestParams();
        $params->query = $q;
        $params->page = $page;
        $params->pageSize = 20;
        if ($category) {
            $params->filters = ['category' => [$category]];
        }

        return $this->client
            ->appSearch()
            ->search(new Search('products', $params))
            ->toArray();
    }
}

Indexing driven by Eloquent observers + queue jobs. Indexing on the request thread will hurt latency and occasionally fail during traffic spikes. Push it onto a queue:

class ProductSaved implements ShouldQueue
{
    public function __construct(public int $productId) {}

    public function handle(Client $client): void
    {
        $product = Product::find($this->productId);
        if (!$product) {
            // deletion path: use DeleteDocuments with the model id
            return;
        }

        $docs = new Documents();
        $docs[] = [
            'id' => (string) $product->id,
            'title' => $product->name,
            'price' => (float) $product->price,
            'category' => [$product->category?->name],
        ];

        $client->appSearch()->indexDocuments(new IndexDocuments('products', $docs));
    }
}

Register it in Product::observe() for saved and deleted. Chunk bulk syncs from a console command — App Search caps document/payload size, so a chunk(100) over the query builder is the safe default for a full reindex.

Search endpoint with facets and pagination. App Search returns meta.page + results in one payload; map it directly rather than re-paginating through Eloquent:

Route::get('/search', function (ProductSearch $search, Request $request) {
    $result = $search->query(
        (string) $request->string('q'),
        (int) $request->integer('page', 1),
        $request->string('category')->toString() ?: null,
    );

    return response()->json([
        'data' => $result['results'],
        'meta' => $result['meta'],
        'facets' => $result['facets'] ?? [],
    ]);
});

Query suggestions for typeahead. App Search has a dedicated QuerySuggestion endpoint backed by analytics — cheaper and more relevant than rolling your own LIKE query, and it improves as users search.

Workplace Search for internal/employee search. Different API surface (WorkplaceSearch\Request\*), different auth. Useful when you're indexing docs, tickets, or wiki pages rather than products. Same client, ->workplaceSearch().

Lower-level Elasticsearch calls. The embedded ->elasticsearch() sub-client is what you reach for when you need raw aggregations, custom mappings, or ILM — things the App Search API doesn't expose. It talks to the same cluster but through the /api/enterprise_search/elasticsearch proxy on the Enterprise Search host, so use the Enterprise Search URL, not the Elasticsearch endpoint.

Testing. Don't mock the chained client calls — the fluent proxy makes ->appSearch()->search() fragile in mocks. Mock your own ProductSearch / repository in feature tests, and reserve a thin integration test suite (skipped when ENTERPRISE_SEARCH_HOST isn't set) for the real client.

Gotchas and Tips

The product is winding down. Elastic announced the discontinuation of App Search and Workplace Search; Enterprise Search was deprecated in favor of the Elasticsearch-native tooling. The SDK's last release is April 2025 and continues to receive patch releases, but if you're greenfielding a Laravel app today, prefer elasticsearch/elasticsearch + laravel/scout with the Elasticsearch driver. This package is worth using when you already run Enterprise Search, not when you're choosing a stack.

Host vs host vs host. Three different URLs get confused constantly: the Elastic Cloud console URL, the Enterprise Search URL (port 3002 self-hosted, *.ent-search.* cloud), and the Elasticsearch URL (port 9200). The host config expects the Enterprise Search URL; the embedded Elasticsearch sub-client also routes through Enterprise Search's proxy in cloud. A wrong host produces unhelpful 404s from the proxy layer, not "wrong URL."

API keys are per-product. An App Search key is not a Workplace Search key is not an Elasticsearch key. A 401 on one sub-client while the other works is almost always this — check the dashboard's credential page for each.

Engine schema is immutable-ish. Adding a facet or changing a field type in App Search requires creating or reindexing the engine. Plan your searchable fields up front; changing a text field to a date later means a full reindex. Run the schema update in a migration-style command, not ad-hoc, or environments drift.

SSL / self-signed certs. Pass http_client_options with verify settings to the Client constructor for local dev. Never commit verify: false — use a CA bundle path or the ELASTIC_CLIENT_APIVERSIONING env pattern.

Long-running workers. In Octane / queue workers, the singleton client is fine (PSR-18 client underneath handles connections), but timeouts and retries come from elastic-transport, not Laravel's HTTP client. Tune client options (timeout, retries) in the constructor — a 30-second hang on a 3-second page will otherwise block your queue.

Error handling. Failures throw Elastic\Transport\Exception\NoNodeAvailableException, Missing404Exception, CouldNotConnectToHost, etc. For search UX, wrap the call and fall back to a database LIKE query or a cached last-good response rather than surfacing a 500 — the search box going blank on a cluster blip loses trust fast.

Response objects are ArrayAccess but not plain arrays. toArray() gives you a clean array; $response->results works via dynamic property access but won't survive JSON serialization in every context. In controllers, always ->toArray() before response()->json().

Document IDs matter. App Search will auto-generate IDs if you omit id, which makes updates and deletes impossible without storing the mapping. Always pass id as a string (App Search treats it as a string; passing 42 vs "42" intermittently causes duplicate documents).

Engine-per-environment. Prefix engine names (products_staging, products_prod) and drive them from env, not code. It's cheap, and it prevents the classic "staging indexed 400k rows and blew up the relevance model" incident.

Caching. Search responses are highly cacheable for popular queries. Cache::remember("search:{$hash}", 60, fn () => $search->query(...)) takes enormous load off both Laravel and the cluster, and App Search's analytics still record the original query — you lose freshness on results, not on relevance signals.

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.
uni-method/symfony-lib
tacticmedia/scim-server-bundle
neika-ai/symfony-bundle
mattleads/agent-publishing-bundle
glavweb/framework-bundle
glavweb/extra-bundle
glavweb/api-bundle
glavweb/admin-bundle
adachsoft/api-version-bundle
elsayed85/laravel-api-exceptions
ouhssini/laravel-demo-mode
daworks/ncloud-outbound-mailer-driver
gp247/cms
ignibyte/aic-gates
nexa-merchant/blog
novasa/laravel-language-center
opencage/geocode
adolfoholzer/parameters
livenetworks/ln-starter
cgv/localized-resource