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

Seal Laravel Package

schranz-search/seal

Schranz Search SEAL is a PHP search engine abstraction layer that provides a unified API for Elasticsearch, OpenSearch, Meilisearch, Algolia, and other backends. Swap engines without rewriting search code, with adapters and integrations for Laravel and Symfony.

Deep Wiki
Context7

Getting Started

SEAL is an abstraction layer, not a search engine. You install the core package plus exactly one adapter for the engine you actually run:

composer require schranz-search/seal schranz-search/seal-elasticsearch-adapter
# or: seal-meilisearch-adapter, seal-opensearch-adapter, seal-typesense-adapter,
#     seal-algolia-adapter, seal-solr-adapter, seal-redis-adapter, seal-memory-adapter

The first useful smoke test is the in-memory adapter, which lets you exercise the whole API without infrastructure:

use Schranz\Search\SEAL\Adapter\Memory\MemoryAdapter;
use Schranz\Search\SEAL\Schema\Field;
use Schranz\Search\SEAL\Schema\Schema;
use Schranz\Search\SEAL\Search\Condition\EqualCondition;
use Schranz\Search\SEAL\Search\Engine;
use Schranz\Search\SEAL\Search\Object\Object;
use Schranz\Search\SEAL\Search\Query;
use Schranz\Search\SEAL\Search\Search;

$schema = new Schema('blog', [
    'id'    => new Field\IdentifierField('id'),
    'title' => new Field\TextField('title'),
    'body'  => new Field\TextField('body'),
]);

$engine = new Engine(new MemoryAdapter());
$engine->createIndex($schema);

$engine->index(
    $schema,
    new Object(['id' => '1', 'title' => 'Hello world', 'body' => '...']),
);

$search = new Search($engine, $schema, new Query('hello'));
$search->addFilter(new EqualCondition('id', '1'));

foreach ($search->execute() as $object) {
    echo $object['title'];
}

Look first at Engine (src/Search/Engine.php) and Search (src/Search/Search.php) — they define the entire contract you will use daily. Then read src/Schema/ for field types and the adapter directory for engine-specific behaviour. The MemoryAdapter is also the best executable documentation while learning the API.

Implementation Patterns

Swap adapters behind DI, not in application code. Construct the adapter once in your container and inject a single Engine. Production config points at Elasticsearch/Meilisearch; test config points at MemoryAdapter. Nothing downstream should reference an adapter class.

Treat the schema as code. Define one Schema per logical index (often per locale or per entity), keep it in version control, and manage createIndex/dropIndex from a console command or deploy step. Because mapping changes generally require a rebuild, make reindexing a first-class command: drop, recreate, replay from the database.

Wrap indexing in a small repository/service. Map your domain entities to Object instances in one place, then call index() in bulk batches and delete() on removal. Trigger indexing from domain events or an outbox/queue rather than inside request handlers, so a slow engine never blocks a write.

Build searches with a query builder, not ad-hoc arrays. A typical controller-facing service accepts a search term, filter values, sort key, and page number, then assembles a Search:

$search = new Search($engine, $schema, new Query($term));
$search->addFilter(new EqualCondition('status', 'published'));
$search->addSort(new Sort('published_at', SortOrder::DESC));
$search->setLimit(20)->setOffset(($page - 1) * 20);

Multiple addFilter() calls combine as AND; use AndCondition/OrCondition for nested logic. Add aggregations when rendering facet UIs and read them from the Result.

Hydrate results by ID. Adapters return Object values, not your entities. Collect IDs from the Result, fetch the rows from your database in one query, and optionally use getTotal() for pagination totals. This keeps the search index a projection rather than a source of truth.

Prefer the Symfony bundle when applicable. schranz-search/seal-bundle gives you YAML-configured adapters and schemas plus autowiring for Engine and named search services — far less boilerplate than assembling adapters manually.

Gotchas and Tips

  • Adapters are separate packages. Forgetting to install one, or mixing adapter versions with the core package, is the most common setup failure. Pin them together.
  • This repository is read-only. Issues and pull requests belong in the schranz-search/search monorepo; don't expect fixes here.
  • Every object needs a stable ID. The identifier field must be set explicitly on each Object; most engines expect string-compatible IDs even if your database uses integers.
  • Schema changes are not migrations. Adding or retyping a field usually means recreating the index and replaying data. Plan a rebuild path early.
  • MemoryAdapter is per-process. It's ideal for unit and integration tests but loses everything between requests — never wire it into a long-running app expecting persistence.
  • Engine behaviour is not perfectly uniform. Full-text query syntax, LikeCondition translation, highlighting, and aggregation support vary by adapter. Test the queries you actually rely on against the real engine, not just the memory adapter.
  • Debug through the engine logger. Engine accepts a PSR-3 logger; enable it to see the requests each adapter emits and to diagnose unexpectedly empty results.
  • Deep pagination has limits. Offset/limit works, but large offsets get expensive on Elasticsearch/OpenSearch; consider search_after-style strategies or capping result windows.
  • getTotal() may cost an extra query depending on the adapter. Call it only when you render pagination.
  • Zero-downtime reindexing is not abstracted. If you need alias-swapping or blue/green indices, implement it in adapter- or engine-specific code; SEAL deliberately stays portable.
  • Keep the index eventually consistent. No transaction spans your database and the engine, so design for retries and idempotent reindexing from the start.
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.
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
pnscripts/filament-broadquill-lite
pnscripts/laravel-sentfold-lite
vleap/mx-sdk-laravel-internal
twidpay/utils
htetmyathlaing/scout-elasticsearch-driver
artisan-toolbox/front-awesome
ls/client-assistant
users-au/laravel-client
cgv/localized-resource