integrated/solr-bundle
A Symfony bundle for integrating Apache Solr into PHP applications. It provides configuration and services for indexing content and running fast, scalable full-text search queries, helping teams add Solr-powered search with less boilerplate and tighter framework integration.
Before you run anything, accept the framing reality: this is a Symfony bundle (integrated/solr-bundle), not a Laravel package. There is no ServiceProvider, no facade, no Eloquent bridge, and no Artisan commands in the artifact. The composer require you'd instinctively run will pull in symfony/framework-bundle and likely fail your Laravel dependency graph. So the "getting started" path for a Laravel developer is read first, port second, adopt never.
Where to look first (in this order):
composer.json — the PHP constraint, the symfony/* hard requirements, and the transitive Solr client (almost certainly solarium/solarium). This single file tells you whether a direct port is even viable.DependencyInjection/Configuration.php — the bundle's config tree. This is the crown jewel: it defines index names, field mappings, types, and client settings. This is the part worth porting.DependencyInjection/IntegratedSolrExtension.php — shows how config becomes services, which classes get wired, and what the client factory looks like.Resources/config/services.* — the actual service definitions and their constructor dependencies.Indexer, Query, Client (or Solr), and any Doctrine event subscribers.Minimal first use case — don't port yet. Spin up Solr locally and prove you can talk to it:
docker run -d -p 8983:8983 --name solr solr:9
Then read the bundle's config shape and sketch the equivalent Laravel config file by hand:
// config/solr.php — modeled on the bundle's tree, not copied
return [
'client' => [
'host' => env('SOLR_HOST', '127.0.0.1'),
'port' => env('SOLR_PORT', 8983),
'path' => '/solr',
'core' => env('SOLR_CORE', 'app'),
],
'indexes' => [
'products' => [
'fields' => [
'id' => ['type' => 'string', 'required' => true],
'title' => ['type' => 'text'],
'category' => ['type' => 'string', 'facet' => true],
'price' => ['type' => 'pfloat'],
],
],
],
];
Your first concrete use case should be the smallest possible proof: index a handful of Product records and run one filtered+facetted query. Do this before deciding whether to fork the bundle at all — you may discover Laravel Scout + a Solr driver or raw Solarium covers you in a day.
Every pattern below assumes the bundle is a reference and config source, and the runtime you own is either Solarium directly or a Scout-based adapter. If you genuinely intend to fork, these are the seams to preserve.
1. Own the boundary with interfaces. Never let call sites touch Solr types. Define:
interface SearchIndex {
public function name(): string;
public function index(array $documents): void;
public function delete(array $ids): void;
}
interface SearchQuery {
public function where(string $field, mixed $value): static;
public function facet(string $field): static;
public function highlight(string $field): static;
public function get(): array;
}
The value of the bundle — and its only durable lesson — is that it separates config (index/field schema) from execution (client, indexer, query builder). Mirror that separation. When you later swap Solr for Meilisearch or Typesense, only the implementations change.
2. Config-driven field mappings, evaluated once. The bundle's schema lives in config, not in Solr's managed-schema XML (verify this — see Gotchas). For Laravel, keep the mapping in config/solr.php and generate Solr schema fields from it via a single Artisan command (solr:schema:push). Do not hand-edit managed-schema if config is the source of truth; you'll drift.
3. Index via Eloquent observers → queued jobs, never synchronously. The bundle leans on Doctrine lifecycle events; that doesn't carry over. Replace with:
// ProductObserver — dispatch, don't write
public function saved(Product $product): void
{
IndexProductJob::dispatch($product)->onQueue('search');
}
public function deleted(Product $product): void
{
DeleteFromIndexJob::dispatch($product->getKey())->onQueue('search');
}
Wrap this in an outbox: write an index_outbox row in the same DB transaction as the model save, and have the job consume from it. This is the only reliable way to avoid the dual-write drift the bundle does nothing to prevent.
4. Batch reindex as an Artisan command modeling the bundle's chunking. Look at how the bundle batches — chunk size, commitWithin, and whether it calls softCommit. Port a solr:reindex {model?} {--chunk=500} command that reads chunks, pushes documents, and commits once at the end of each chunk. For large tables, run it as an on-demand queued job, not php artisan in a terminal, so retries and pauses work.
5. Use the pluggable client adapter for tests. The description explicitly mentions "pluggable client adapters for testing and custom transports." Whatever you build, define a FakeSolrClient implementing the same interface so feature tests assert documents without a Solr instance. The bundle's adapter seam is the pattern to steal.
6. Query building mirrors the bundle's fluent shape. The bundle advertises filters, facets, and highlighting. In your adapter, keep those as first-class methods on SearchQuery so relevance and faceting work stays inside one class you can tune — not scattered across call sites.
7. Instrument the index path from day one. This package has no observability. Add a metric for last successful index timestamp per core and alert on lag. Silent indexing failures are the dominant failure mode of every Solr wrapper.
symfony/framework-bundle is a hard requirement; the config tree, service wiring, and console commands won't instantiate outside Symfony's container. Don't attempt a drop-in. Either fork-and-strip or reimplement the config parsing — usually a day or two — and skip the bundle entirely.saveQuietly skips them, queued deletes lose the model). Never assume the bundle's trigger semantics transfer — verify and replace.managed-schema. If it generates, a package/schema change requires a coordinated full reindex with no migration tooling implied. This is the highest-risk unknown; answer it before you write any code.commitWithin and soft-commit handling matters more than any other tuning knob. If the bundle hardcodes commits per document, indexing will be an order of magnitude slower than batching. Expose the knob in your own adapter.composer.lock of a scratch install before committing.products_v1 → build → products_v2 → swap alias) so a full reindex never causes a user-visible search outage. No bundle feature implies this; it's operational discipline.debug=true&debug.explain.structured=true and inspect the parsed query in the Solr admin UI at /solr/#/<core>/query. Log the raw request your adapter sends — wrappers routinely hide the actual params, which is where 90% of relevance bugs live.curl 'http://localhost:8983/solr/app/select?q=*:*&facet=true&facet.field=category&debug=true'
Http), and route retries/timeouts/logging through it so failures surface in your observability stack instead of being swallowed.How can I help you explore Laravel packages today?