schranz-search/symfony-bundle
Symfony bundle for schranz-search that adds search engine indexing and querying to Symfony apps via a search abstraction layer spanning Elasticsearch, OpenSearch, Meilisearch, Algolia, and more.
Install the bundle alongside the search library it wires up:
composer require schranz-search/symfony-bundle
If you do not have Symfony Flex, register the bundle in config/bundles.php:
Schranz\Search\Symfony\Bundle\SchranzSearchBundle::class => ['all' => true],
The first place to look is the generated bundle config reference (bin/console config:dump-reference schranz_search) and the upstream php-cmsig/search / SEAL documentation — this bundle is a thin Symfony wrapper, so nearly all concepts (schemas, engines, adapters, fields) come from the library, not the bundle.
A minimal working config defines one engine, one schema, and one index:
# config/packages/schranz_search.yaml
schranz_search:
engines:
default:
adapter: 'meilisearch' # or elasticsearch, opensearch, algolia, redis, typesense, solr...
url: '%env(MEILISEARCH_URL)%'
schemas:
posts:
fields:
id: { type: 'keyword' }
title: { type: 'text' }
body: { type: 'text' }
indexes:
posts:
engine: 'default'
schema: 'posts'
Your first use case is almost always: create the index, push a document, and read it back.
bin/console schranz-search:index:create posts
use Schranz\Search\SEAL\Search\Engine; // or the interface the bundle aliases
final class PostIndexer
{
public function __construct(private Engine $engine) {}
public function index(array $post): void
{
$this->engine->saveDocument('posts', $post);
}
}
Autowire the Engine interface (the bundle exposes an alias for it) — do not try to construct adapters yourself, that is exactly what the bundle exists to automate.
Schema-in-config, documents-in-code. Keep field definitions in YAML under schranz_search.schemas; treat PHP classes as the source of truth only for the document payload. A schema change means a new index + reindex, not a runtime mutation.
Dedicated indexer services. Wrap the Engine in small services per aggregate (PostIndexer, ProductIndexer) that translate Doctrine entities into plain document arrays. This keeps controllers free of mapping logic and gives you one place to change when a field is renamed.
final class PostIndexer
{
public function __construct(private Engine $engine) {}
public function index(Post $post): void
{
$this->engine->saveDocument('posts', [
'id' => (string) $post->getId(),
'title' => $post->getTitle(),
'body' => $post->getBody(),
]);
}
public function remove(Post $post): void
{
$this->engine->deleteDocument('posts', (string) $post->getId());
}
}
Hook these into Doctrine events (postPersist, postUpdate, postRemove) or, better for throughput, dispatch a Symfony Messenger message and index asynchronously.
Bulk indexing for backfills. The single-document API is convenient for real-time updates, but for initial population or reindexing, loop in chunks of 100–500 and use the bulk method exposed by the engine/adapter. This is dramatically faster and avoids per-request overhead.
Search controller as a thin layer. Do not let Engine::search() leak into Twig. Put a SearchService that takes a query string + filters and returns a stable SearchResult DTO. Search request/response shapes differ per adapter, and the bundle passes those through — insulating your app is what makes swapping Meilisearch for Elasticsearch feasible.
final class PostSearch
{
public function __construct(private Engine $engine) {}
public function find(string $q): array
{
$result = $this->engine->search('posts', $q);
return iterator_to_array($result); // yields documents
}
}
Multiple engines. Define more than one entry under engines (e.g. search and analytics) and reference them per index. This is the intended way to separate read-heavy from write-heavy backends without touching service wiring.
Console commands as deployment steps. Treat schranz-search:index:create / index:drop as part of your deploy/release process — either run them in your release script or trigger them from a migration. Never create indexes at request time; you lose atomicity and can race across workers.
Testing with an in-memory or Redis adapter. Configure a lightweight adapter under when@test, point the test suite at it, and reset between tests by dropping/recreating the index. Do not mock the Engine interface — you want to exercise the real schema field mappings.
php-cmsig/search monorepo. Do not file here; check the upstream project first when debugging.text field works everywhere, but keyword, datetime, and especially geo_point and nested fields are not portable. If you care about swapping engines, stick to the lowest common denominator and verify against each adapter you might deploy.posts_v2 with the new schema, backfill, then repoint the alias (or config) and drop posts_v1. Do not edit the field types of a live index in place.Engine only when it can resolve ambiguity. With multiple engines configured, autowiring Engine $engine may fail — inject by name (e.g. the named alias the bundle exposes) or inject a registry. Read the generated service config with debug:container when in doubt.->search() for a document you just saved; tests must either run indexing synchronously or explicitly drain the queue.schranz-search:index:create to onboarding docs and CI setup alongside doctrine:migrations:migrate.cmsig_search root key; newer ones use schranz_search. If your config silently does nothing, run config:dump-reference and confirm which key the installed version expects.bin/console debug:container --tag=schranz_search.engine (adjust tag to what config:dump-reference / bundle docs show) reveals which services are wired; debug:config schranz_search shows the resolved config after merges. When a query returns nothing, first confirm the document exists via the engine's own HTTP API or dashboard before blaming the abstraction.search library, not this bundle. If you need a custom adapter or a transform hook, implement/extend there and register it — do not fork the bundle. Likewise, if you need custom schema logic, the library's field classes are what you extend, not anything in this repository.How can I help you explore Laravel packages today?