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

Solr Bundle Laravel Package

floriansemm/solr-bundle

A Symfony bundle that integrates Apache Solr search, offering configurable clients and services for indexing documents, building queries, and managing Solr connections so applications can add fast, scalable full-text search with minimal setup.

View on GitHub
Deep Wiki
Context7

Getting Started

Install and wire it up

composer require floriansemm/solr-bundle

Register the bundle (config/bundles.php on Symfony 4+; AppKernel.php on Symfony 3):

FlorianSemm\SolrBundle\FlorianSemmSolrBundle::class => ['all' => true],

Add a client pointing at a core (option names have shifted between versions — always confirm with bin/console config:dump-reference floriansemm_solr):

# config/packages/floriansemm_solr.yaml
floriansemm_solr:
    clients:
        default:
            host: '%env(SOLR_HOST)%'
            port: 8983
            path: /solr
            core: posts
            timeout: 5
    document_managers:
        default:
            client: default

The core must already exist in Solr, with a schema defining the fields you plan to index. This bundle does not manage schemas.

First use case: searchable blog posts

Annotate a Doctrine entity with the bundle's annotations (import the annotation namespace, not the classes):

use Doctrine\ORM\Mapping as ORM;
use FlorianSemm\SolrBundle\Annotation as Solr;

/**
 * @ORM\Entity
 * @Solr\Document(repository="App\Solr\PostRepository")
 */
class Post
{
    /** @ORM\Id @ORM\GeneratedValue @ORM\Column(type="integer") @Solr\Id */
    private $id;

    /** @ORM\Column(type="string") @Solr\Field(type="string") */
    private $title;

    /** @ORM\Column(type="text") @Solr\Field(type="text") */
    private $body;

    /** @ORM\Column(type="datetime") @Solr\Field(type="date") */
    private $publishedAt;
}

Then populate and verify:

bin/console solr:index:clear
bin/console solr:index:populate
bin/console debug:container | grep solr   # confirm service IDs and command names

Open the Solr admin UI (http://localhost:8983/solr/#/posts/query) and run *:*. If documents exist, your mapping is working. Query from PHP with the exposed Solarium client:

$client = $this->get('solr.client'); // or inject Solarium\Client

$query = $client->createSelect();
$query->setQuery('title:symfony');
$query->setRows(20);

$result = $client->select($query);
foreach ($result as $doc) {
    // $doc->id, $doc->title, ...
}

If that returns hits, you have the full loop: annotate → populate → query. Everything else is refinement.

Implementation Patterns

Write-through indexing via the Doctrine listener. The bundle ships a listener that indexes on persist/update and deletes on remove. Keep it enabled in dev so edits show up immediately; disable or bypass it during fixtures, imports, and migrations to avoid a Solr commit per row:

floriansemm_solr:
    document_managers:
        default:
            client: default
            auto_index: false   # or remove the listener from the container

Pattern: run fixtures → run a batched indexing command → re-enable. Treat the listener as a convenience for interactive edits, not as the source of truth for bulk data.

Batched population instead of solr:index:populate on big tables. The stock command tends to iterate whole entity sets; on large tables it will exhaust memory. Write your own command that pages by ID and flushes every N documents, and turn off auto_index for the duration:

$qb = $em->createQueryBuilder()->select('p')->from(Post::class, 'p')
    ->orderBy('p.id', 'ASC')->setMaxResults(500);

$lastId = 0;
while ($batch = $qb->andWhere('p.id > :last')->setParameter('last', $lastId)->getQuery()->getResult()) {
    foreach ($batch as $post) { $indexer->index($post); }
    $lastId = end($batch)->getId();
    $em->clear(); // detach, avoid memory growth
}

Prefer raw Solarium for real search features. The bundled repository base class is fine for find($id) / findBy([...]) lookups, but sorting, facets, highlighting, and boolean queries are quicker through the Solarium API:

$query = $client->createSelect();
$query->setQuery('title:symfony OR body:"event dispatcher"');
$query->createFilterQuery('published')->setQuery('publishedAt:[* TO NOW]');
$query->addSort('publishedAt', $query::SORT_DESC);

$facetSet = $query->getFacetSet();
$facetSet->createFacetField('tags')->setField('tags');

$query->getHighlighting()->setFields('body')
    ->setSimplePrefix('<mark>')->setSimplePostfix('</mark>');

$result = $client->select($query);

Hydrate by ID, not by copy. Solr is your search index, not your primary store. Return IDs in ranked order, then fetch entities in one query and re-order in PHP:

$ids = array_map(fn ($doc) => $doc->id, iterator_to_array($result));

$posts = $em->getRepository(Post::class)
    ->createQueryBuilder('p')
    ->where('p.id IN (:ids)')->setParameter('ids', $ids)
    ->getQuery()->getResult();

$byId = [];
foreach ($posts as $p) { $byId[$p->getId()] = $p; }
$ordered = array_values(array_filter(array_map(fn ($id) => $byId[$id] ?? null, $ids)));

For result pages where the DB round-trip is wasteful, hydrate lightweight DTOs straight from the Solr documents instead.

Wrap the client in your own service. Don't scatter $container->get('solr.client') through controllers. Inject Solarium\Client (or the bundle's client service) into App\Search\PostSearch, expose typed methods (search(string $q, array $filters): SearchResult), and keep Solr query syntax out of controllers and Twig. This is the seam you'll use to mock Solr in tests and to swap the bundle out later.

One core per search domain, or one core with a type field. Multi-core means multiple clients/document managers and duplicated schema management. For small apps, a single core with a type_s discriminator and copyField into shared search fields is simpler. Choose multi-core when you need per-domain analyzers or independent reindex windows.

Schema copy fields for sort/facet. type="text" fields are analyzed and cannot be sorted or faceted cleanly. Map the property once and add copyField in the Solr schema to a *_s string field, or map with @Solr\Field(type="string", name="title_sort") when you only need the sortable variant.

Deployment pattern. Run an indexing command as a post-deploy step, or populate a fresh core and swap the core alias. Avoid running a full repopulate on every deploy against a live core — it creates a window of missing/duplicated documents and hammers Solr with commits.

Gotchas and Tips

Maintenance risk is the headline. Last release was November 2018, dependents are effectively zero, and it targets Symfony 3/4-era APIs. Expect to pin solarium/solarium to the version range the bundle allows — Solarium 6+ changed enough of the client/endpoint plumbing that a careless composer update breaks everything. Lock the bundle and Solarium together and test upgrades in isolation.

Annotation-only mapping. There is no PHP 8 attribute support. On PHP 8.1+ you'll still need doctrine/annotations (deprecated) and keep Solr metadata in docblocks. If your project has moved entity mapping to attributes, either keep the Solr annotations separately or write a small attribute-to-metadata shim.

Symfony 5+ command compatibility. The bundled solr:index:* commands were written before Symfony's command refactor. If they fail to instantiate (ContainerAwareCommand removal, missing Command signatures), don't fight it — write a thin console command in your app that calls the indexer service directly. You lose nothing.

Transaction ordering. The listener may push documents to Solr before the database transaction commits. A rollback then leaves phantom documents in the index. If your write paths use transactions, either index after commit (postFlush with an explicit check, or a domain event/outbox) or reconcile with a periodic repopulate. Do not assume Solr and the DB are atomic.

Deletes via DQL are invisible. DELETE FROM post WHERE ... and bulk SQL bypass the ORM listener entirely, leaving stale documents. Every bulk mutation needs a matching delete-by-query against Solr, or a scheduled rebuild.

ID and uniqueKey mismatch. The @Solr\Id property must map to the Solr schema's uniqueKey (conventionally id). If the names differ, updates become inserts and you accumulate duplicates. Check uniqueKey in managed-schema before your first populate.

Dates must be UTC ISO-8601. Solr expects 2018-11-15T10:00:00Z. A DateTime in a non-UTC timezone is serialized as-is and silently shifts your timeline. Normalize to UTC (or store epoch millis and use a pdate/plong field) and add a test that asserts the indexed value.

Visibility after writes is a commit question. Whether fresh documents appear in queries depends on Solr's commit strategy (commit=true on update, commitWithin, auto-commit). If populate succeeds but queries return nothing, check the Solr admin UI first — the document may simply be uncommitted. Long-term, use commitWithin rather than hard commits per document.

Field types fail loudly in Solr, quietly in PHP. A date field fed a string, or a string field fed an array, gets a 400 from Solr that bubbles up as a client exception. Watch Solr's log (solr.log) during populate; the exception message from Solarium is often just "Bad Request". Validate property types against the schema.

Collections cause lazy loads mid-index. Mapping a Doctrine collection to a multi-valued field triggers hydration during the listener — N+1 queries and, worse, lazy loads inside postPersist where associations may be incomplete. Join-fetch what you need in the indexer, or index only scalar/derived fields and handle relations through a separate denormalized property.

solr:index:populate memory. As noted above, page it yourself for anything beyond a few thousand rows. Use $em->clear() between batches, and disable Doctrine's SQL logger in long-running commands.

Testing needs a real Solr. There is no in-memory substitute, and mocking solr.client only proves your controller calls the mock. Run Solr in CI (the official solr:8 Docker image with a baked-in schema) and keep one integration test that round-trips an entity through index → query → delete. Unit-test the search service against a stub for the rest.

Authentication and network exposure. Solr has no built-in auth in its default config, and the bundle doesn't add any. Keep Solr on a private network; if you need credentials, front it with a reverse proxy (nginx basic auth) or build a custom Solarium client with an HTTP adapter that injects headers, then register it under the bundle's client service ID.

Debugging checklist. bin/console debug:container | grep solr to find the real service IDs; config:dump-reference floriansemm_solr for the actual options in your installed version; Solr admin UI for schema and query inspection; solr.log for type errors; doctrine:schema:validate is useless here — validate against the Solr schema instead.

Extension points worth knowing. Subclass the bundled repository for typed finders, decorate the indexer to add logging/metrics, and wrap the client in your own service (see above). Those three seams cover most customization needs without forking the bundle.

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.
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
robertmarney/lara-hierarchial-collections
simonschaufi/rule-doc-generator
emissaoja/emissaoja
blackbricksoftware/gravityforms-civicrm-form-processor
al-rahabi/fancy-crud
jinas/dhivatar-component
mltstephane/laravel-quality
bernskiold/laravel-talentlms
bernskioldmedia/laravel-form-architect