flexmodel/flexmodel-elasticsearch-bundle
A Laravel/PHP bundle that connects FlexModel to Elasticsearch, enabling model indexing, full-text search, filtering, and query building so you can add fast, scalable search to your application.
Reality check first, updated for 0.1.0. The only thing the new release tells us is that this is now an initial release — 0.1.0, notes: “Initial release.” There is no changelog history, no migration guide, no upgrade path, and no prior users to learn from. Treat everything below as pre-1.0 and therefore unstable in the literal sense: any patch release can rename a service, change a config key, or alter how a model becomes a mapping. Pin the exact version in composer.json ("flexmodel/flexmodel-elasticsearch-bundle": "0.1.0") rather than using a caret range, and read composer.json and Tests/ yourself before committing to anything.
What the package still is, structurally: a thin Symfony DI bundle wrapping the FlexModel concept (plain PHP model classes mapped onto a storage backend) plus an Elasticsearch storage adapter. Two consequences for a Laravel developer:
config/ file. It’s Symfony bundle plumbing — a Bundle class, DependencyInjection/*Extension.php, Resources/config/services.yml. In Laravel you will register the services by hand, not “install and go.”elasticsearch/elasticsearch client of the 2.x/5.x era and PHP constraints that may fight a modern Laravel app. The 0.1.0 release being brand new does not mean the internals are modern — verify the constraints.Where to look first, in this order:
composer.json — the exact elasticsearch/elasticsearch and symfony/* constraints. This decides whether you can use it at all.DependencyInjection/ and Resources/config/ — the bundle exposes almost nothing else. Whatever services exist (client, index manager, storage) are declared here; that is the public API surface at 0.1.0.Tests/ — with a package this small and this young, tests are the only reliable documentation of the mapping and query contract. At 0.1.0 they are also your only signal of what the author actually intended.Minimal wiring (Symfony shape, translate by hand for Laravel):
// Bundles registration, Symfony 3/4 era shape
$bundles = [
new FlexModel\ElasticSearchBundle\FlexModelElasticSearchBundle(),
];
// config.yml
flex_model_elastic_search:
clients:
default:
hosts: ['http://localhost:9200']
# index/type/model mapping usually happens through FlexModel "presets"
First use case worth proving end-to-end: one model, one index, one document indexed, one search returned. Define a model class with properties, register it under a FlexModel preset pointed at the default client, index a document, run a term query through the model’s repository/storage. If that round-trip works against your ES version, everything else is variations. If it fails in the first hour, stop — the failure mode is usually an incompatible ES server version or an assumption the 0.1.0 tests never covered, and neither is fixed by config.
For Laravel, do not try to run the bundle. Harvest the idea and the mapping logic, and bind a client yourself:
// app/Providers/ElasticServiceProvider.php
$this->app->singleton(\Elasticsearch\Client::class, fn () => \Elasticsearch\ClientBuilder::create()
->setHosts(config('elasticsearch.hosts'))
->build());
Model-as-document. The FlexModel premise is that your PHP model is storage-agnostic and you attach a storage (here, Elasticsearch) to it. In practice the model carries field definitions and the adapter derives index mappings from them. Keep models small and flat. Nested objects and arrays are where these adapters are weakest, and mapping drift between PHP field types and the ES mapping is the number one source of silent data loss — you index a document, _source looks right, but the field isn’t searchable because it mapped as text vs keyword, or the document was rejected and you never inspected the response.
// Illustrative shape, not verbatim API — verify against the storage package
class Product extends \FlexModel\Model
{
protected $id;
protected $name;
protected $price;
protected $tags = [];
}
One model per index/type, one preset per environment. Treat the FlexModel “preset” as your environment boundary (dev, staging, production) mapping to a client plus index name. This is what makes the package tolerable: application code asks for a preset by name, and index naming/prefixing lives in config, not code. At 0.1.0 expect this config shape to be the thing most likely to change between patch releases — isolate it in one file.
Push the query DSL through rather than fighting the abstraction. The adapter will cover CRUD and simple criteria; anything beyond degrades into array-shaped ES queries anyway. Own the query body in a thin repository and keep the adapter out of your domain:
class ProductSearchRepository
{
public function __construct(private \Elasticsearch\Client $client) {}
public function search(string $term): array
{
$res = $this->client->search([
'index' => 'products',
'body' => [
'query' => [
'multi_match' => ['query' => $term, 'fields' => ['name^2', 'tags']],
],
],
]);
return array_column($res['hits']['hits'], '_source');
}
}
The integration tip that matters most: let the package own indexing, never let it own querying. Indexing is mechanical and the model → mapping translation is the only genuinely useful thing here. Search is where the abstraction leaks and where you need raw DSL for relevance tuning, aggregations, and pagination.
Bulk indexing. Single-document indexing through an ORM-style adapter will time out on any real backfill. Batch writes and flush explicitly — with this era of client that means building the bulk body yourself and using the model-to-array conversion only as each action’s _source.
Keep it behind an interface. Whatever you do with this package, wrap it:
interface ProductIndexer {
public function index(Product $product): void;
public function delete(int $id): void;
}
interface ProductSearcher {
public function search(ProductQuery $query): ProductResultSet;
}
At 0.1.0 with no adoption history, the only defensible adoption is one where swap-out cost is a single binding. Write the direct-client implementation first, keep the FlexModel-based one as an A/B alternative, and you get the model-mapping idea without the dependency becoming load-bearing.
Index lifecycle is your job, not the bundle’s. There is no alias/rollover management here. Create indices explicitly, use aliases so you can reindex into a new one and flip, and always refresh in tests or you will chase phantom failures.
elasticsearch/elasticsearch. ES 6 removed multiple mapping types; ES 7 removed them entirely and changed the client API significantly. On a 7.x/8.x cluster the adapter’s type-based document paths (/index/type/id) will 400 or 404 immediately. Check the server version against the client constraint before anything else.$response['errors'] and per-item status. A document indexed “successfully” that never appears in search is almost always a mapping rejection or a refresh-timing issue.price as integer, next a decimal → rejected. First doc has tags: [] → mapped as text with no keyword subfield → you can’t aggregate and won’t notice until a dashboard is wrong. Define mappings explicitly and disable dynamic mapping for fields you care about.refresh: true on writes is a test/dev convenience; in production it turns bulk loads into index thrashing. Set refresh_interval: -1 during backfills, then restore and force-merge.debug:container in Symfony, or bind and dd() in Laravel) and find which package was meant to register it.mitmproxy, or an ES slow-log at level: trace) for one reproduction run. Half of “the package is broken” is a query the adapter built differently than you assumed.elasticsearch/elasticsearch directly (or laravel/scout with a first-party ES driver) behind a small mapping layer you own. The FlexModel idea — storage-agnostic models — is appealing, but at 0.1.0 with a single release and no adoption this is a reference implementation, not a runtime dependency. Read it for how to structure model → index mapping; don’t make it load-bearing.How can I help you explore Laravel packages today?