survos / search-bundle
First-party Symfony search kernel and faceted UX with field-driven Doctrine, Meilisearch, Algolia, SQLite FTS5, PostgreSQL BM25, and Elasticsearch adapters.
Fund package maintenance!
Requires
- php: ^8.5
- doctrine/dbal: ^4.3
- doctrine/doctrine-bundle: ^2.14|^3.0
- nyholm/psr7: ^1.0
- phpdocumentor/reflection-docblock: ^5.4|^6.0
- survos/field-bundle: ^2.5
- survos/kit-bundle: ^2.5
- symfony/asset: ^8.1
- symfony/cache-contracts: ^3.6
- symfony/config: ^8.1
- symfony/console: ^8.1
- symfony/dependency-injection: ^8.1
- symfony/http-client: ^8.1
- symfony/http-kernel: ^8.1
- symfony/options-resolver: ^8.1
- symfony/serializer: ^8.1
- symfony/service-contracts: ^3.6
- symfony/string: ^8.1
- symfony/translation: ^8.1
- symfony/ux-icons: ^3.4
- symfony/ux-live-component: ^3.4
- symfony/ux-twig-component: ^3.4
- twig/twig: ^3.4|^4.0
Requires (Dev)
- algolia/algoliasearch-client-php: ^4.37
- doctrine/orm: ^3.4
- elasticsearch/elasticsearch: ^9.0
- friendsofphp/php-cs-fixer: ^3.64
- meilisearch/meilisearch-php: ^1.10
- phpstan/phpstan: ^2.1
- phpstan/phpstan-symfony: ^2.0
- phpunit/phpunit: ^13.0
- rector/rector: ^2.3
- symfony/asset-mapper: ^8.1
- symfony/framework-bundle: ^8.1
- symfony/maker-bundle: ^1.62
- symfony/runtime: ^8.1
- symfony/var-exporter: ^8.1
- symfony/web-profiler-bundle: ^8.1
Suggests
- algolia/algoliasearch-client-php: Needed for the Algolia adapter.
- elasticsearch/elasticsearch: Needed for the Elasticsearch adapter. Index creation and population belong in survos/elastic-bundle; this client is only required to query.
- meilisearch/meilisearch-php: Needed for the Meilisearch adapter. Index lifecycle lives in survos/meili-bundle.
Provides
None
Conflicts
None
Replaces
None
This package is auto-updated.
Last update: 2026-08-18 13:36:30 UTC
README
Reusable field-driven search for Symfony applications. SearchBundle owns the complete query/result kernel, faceted Live Component UI, AssetMapper-ready JavaScript and CSS, field-driven configuration, and portable search adapters.
Applications install one package and can choose Doctrine, Meilisearch, Algolia, SQLite FTS5, PostgreSQL BM25, or Elasticsearch without duplicating UI infrastructure.
Search From Fields
Create a search class and point it at an entity or DTO class with #[Field] metadata:
use Survos\FolioBundle\Entity\Row; use Survos\SearchBundle\Search\AbstractFieldSearch; use Survos\SearchBundle\Attribute\AsSearch; #[AsSearch(index: Row::class, adapter: 'folio_fts')] final class FolioRowSearch extends AbstractFieldSearch { protected function getFieldClass(array $options = []): string { return Row::class; } public function build(array $options = []): void { parent::build($options); $this->setAdapterParameters([ 'table' => 'item', 'ftsTable' => 'item_fts', 'idColumn' => 'id', 'labelColumn' => 'label', 'contentColumns' => ['label', 'dto_data', 'extras'], 'where' => 'core_id = :core', 'params' => ['core' => $options['coreId']], ]); } }
Render the first-party component:
<twig:Survos:Search:Layout name="folio_row" :options="{ coreId: core.id }"/>
SQLite FTS5
Configure the SearchBundle adapter:
survos_search: default_adapter: folio_fts adapters: folio_fts: 'sqlite-fts5://folio'
The DSN host is the Doctrine connection name. The adapter uses DBAL and SQLite FTS5:
MATCHfor full-text filteringbm25(fts_table)for score- optional facet counts through normal SQL aggregation
Applications remain responsible for creating and maintaining the FTS5 virtual table. That is deliberate: folios, entities, and denormalized JSON payloads need different indexing strategies.
PostgreSQL BM25
Configure:
survos_search: adapters: pg_bm25: 'postgres-bm25://default'
The first target is pg_textsearch, which provides PostgreSQL BM25 indexes. This bundle keeps the SQL configurable because pg_textsearch and ParadeDB pg_search expose different operators.
Elasticsearch 9
Installations can add Elasticsearch as another UX Search adapter without coupling application code to the official client:
# config/packages/survos_search.yaml survos_search: default_adapter: elastic adapters: elastic: '%env(ELASTICSEARCH_DSN)%'
ELASTICSEARCH_DSN=elasticsearch://127.0.0.1:9200
Supported DSNs are:
elasticsearch://host:portfor HTTPelasticsearch+https://host:portfor HTTPS- basic authentication in the URL
?api_key=...for API-key authentication
The adapter uses elasticsearch/elasticsearch:^9.0, Elastic's official PHP
client. Connections are created lazily by the adapter factory. The indexing
command pings the backend and reports a useful failure before attempting index
work.
An explicit search can control document shape and retrieval:
public function build(array $options = []): void { $this->setAdapterParameters([ 'index' => 'packages', 'idField' => 'id', 'searchFields' => ['name^3', 'description', 'searchText'], 'sourceFields' => ['id', 'name', 'description', 'keywords'], 'facetFields' => ['vendor' => 'vendor', 'keywords' => 'keywords'], 'sortFields' => ['name' => 'name.keyword'], 'mappings' => [ 'id' => ['type' => 'keyword'], 'name' => [ 'type' => 'text', 'fields' => ['keyword' => ['type' => 'keyword']], ], 'description' => ['type' => 'text'], 'keywords' => ['type' => 'keyword'], 'searchText' => ['type' => 'text'], ], 'retrievalMode' => $options['retrievalMode'] ?? 'lexical', 'vectorField' => 'embedding', 'vectorDimensions' => 1536, 'embeddingProvider' => $this->embeddingProvider, 'embeddingText' => static fn (array $document): string => implode("\n", array_filter([ $document['name'] ?? null, $document['description'] ?? null, $document['searchText'] ?? null, ])), ]); }
embeddingProvider may implement
Survos\SearchBundle\Contract\EmbeddingProviderInterface or be a callable
that maps text to a list of floats. Applications choose the embedding model;
SearchBundle only stores and queries the vectors. queryVector can instead be
an explicit vector or callable for experiments and tests.
Create/recreate and bulk-populate registered searches with:
bin/console survos:search:index packages --drop --batch-size=250 bin/console survos:search:index packages --limit=25
The command uses an explicit documentProvider/documentMapper when
configured. Otherwise, automatic entity searches stream Doctrine records and
derive JSON-safe documents from their mapped fields. Dates, enums, nested
arrays, and stringable values are normalized before bulk indexing.
Retrieval modes
lexical: Elasticsearch multi-match/BM25vector: indexeddense_vectorkNN retrievalhybrid: lexical plus vector retrieval fused with reciprocal rank fusion
The adapter first tries Elasticsearch's native RRF retriever. Native RRF is a
paid Elastic feature and returns HTTP 403 on the free self-managed license. In
that specific case the adapter automatically performs the two searches and
applies the same RRF formula client-side. Result-set metadata reports
hybridImplementation as native_rrf or client_rrf.
Every result set exposes backend-neutral diagnostic metadata including engine, mode, elapsed Elasticsearch time, shard status, and hybrid implementation. Each hit preserves its score plus optional highlight, explanation, index, and per-retriever RRF rank details.
Folio Direction
Do not add the search code to folio-bundle. Folio should consume this bundle by:
- Creating/maintaining FTS5 tables next to folio SQLite tables.
- Marking searchable/filterable folio row fields with
#[Field]on DTOs or search-facing row models. - Creating small search classes that bind folio context (
core_id,dto_type) into adapter parameters.
Credits
The UX layer is derived from and inspired by Mezcalito UX Search, released under the MIT license. SearchBundle retains attribution in the imported source and now maintains the integrated implementation under the Survos\SearchBundle namespace.