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

Sphinx Bundle Laravel Package

javer/sphinx-bundle

Integrates the Sphinx full-text search engine with Laravel, letting you define indexes, run fluent search queries, and blend Sphinx results into your Eloquent models. A lightweight bundle for adding fast, scalable keyword search to PHP applications without leaving the familiar Laravel workflow.

View on GitHub
Deep Wiki
Context7

Getting Started

Install and register

composer require javer/sphinx-bundle

Register the bundle in config/bundles.php (Symfony Flex may do this for you):

Javer\SphinxBundle\JaverSphinxBundle::class => ['all' => true],

Configure the connection — SphinxQL speaks the MySQL protocol, so the bundle typically needs a host/port pair pointing at searchd's SphinxQL listener (default 9306, not the legacy binary API port 9312):

# config/packages/javer_sphinx.yaml
javer_sphinx:
    host: '%env(SPHINX_HOST)%'
    port: 9306

Check the bundle's DependencyInjection/Configuration.php first — that file is the authoritative, version-accurate list of options (timeouts, multiple connections, logging) and is more reliable than the README.

First place to look, in order

  1. DependencyInjection/Configuration.php — what you can actually configure.
  2. Resources/config/services.* — the service IDs/aliases you can inject. Find the SphinxQL client service and alias it in your own services.yaml so you can autowire it.
  3. The client/query-builder class — it defines the query API surface you'll live in day to day.

First use case

Point it at an index that already exists in sphinx.conf and run a one-off search from a controller or a bin/console command:

$results = $sphinxClient->query(
    "SELECT id, WEIGHT() AS w FROM my_index WHERE MATCH(:q) LIMIT 0, 20",
    ['q' => $escapedQuery]
);

Get that round-trip working before touching indexing, real-time indexes, or Doctrine integration. It validates the network path, charset, and credentials in one step.

Implementation Patterns

The two-tier hydration pattern (most common in Symfony)

Sphinx is a retrieval engine, not your source of truth. The pattern that scales is: Sphinx returns id + weight + snippet attributes → hydrate full entities from Doctrine by ID → preserve Sphinx ordering in PHP.

$rows     = $sphinx->query($sphinxql);
$ids      = array_column($rows, 'id');
$entities = $repository->findBy(['id' => $ids]);

// re-order to match Sphinx relevance
$byId = [];
foreach ($entities as $e) { $byId[$e->getId()] = $e; }
$ordered = array_values(array_filter(array_map(fn($id) => $byId[$id] ?? null, $ids)));

Build a small SearchService around this so controllers never touch SphinxQL directly. That service is your seam for testing and for swapping the engine later.

Query building and pagination

SHOW META (or the total_found value returned alongside results) is required to paginate — Sphinx will not tell you the total in the main result set. A typical wrapper:

$offset = ($page - 1) * $perPage;
$sql = "SELECT id, WEIGHT() AS w FROM idx WHERE MATCH(:q)
        ORDER BY w DESC LIMIT {$offset}, {$perPage} OPTION max_matches=10000";
$rows = $sphinx->query($sql, ['q' => $q]);
$total = $sphinx->query('SHOW META'); // total_found

Keep max_matches in mind: total_found is a lie the moment your result set exceeds it, and LIMIT offset deep pages get expensive.

Index maintenance from Doctrine events

For real-time indexes, hook postPersist/postUpdate/postRemove via a Doctrine event subscriber and issue REPLACE INTO idx (id, title, content) VALUES (...) / DELETE FROM idx WHERE id = .... Do this after flush commits, not inside it, and consider pushing to a queue — synchronous SphinxQL calls inside a transaction are a latency trap and a consistency hazard if the commit rolls back.

For plain (batch) indexes, cron the indexer --rotate job and accept eventual consistency; the bundle only queries, it does not index.

Grouping, facets, and ranking

Use GROUP BY with WITHIN GROUP ORDER BY ... AS @groupby for "latest per category" style facets, and OPTION ranker=... / field weights to tune relevance. Keep these queries in named methods on your SearchService — SphinxQL is terse and easy to misread six months later.

Integration tips

  • Alias the bundle's client service in your services.yaml and autowire by your own interface, so you can decorate it (logging, timing, retries).
  • Inject the client, not the container. Controllers should get a SearchService.
  • Add a functional test that runs against a real searchd in Docker; mocked SphinxQL strings rot quickly.

Gotchas and Tips

SphinxQL is not MySQL. No JOIN, no correlated subqueries, no HAVING the way you expect, no transactions, no prepared statements. Anything that looks like SQL but is "almost" SQL is where bugs hide.

Escaping is on you. MATCH('...') is not parameterised by the protocol — it's a string literal Sphinx parses with its own extended-syntax rules (@field, |, -, ", *). User input going into MATCH() must be escaped against Sphinx's syntax, not just SQL quoting. Treat this as an injection surface and centralise the escaping in one helper.

Port confusion. 9306 = SphinxQL (what you want). 9312 = legacy binary API (what the bundle does not speak). A connection-refused or protocol-mismatch error is almost always this.

total_found lives in SHOW META. It is a separate query, it belongs to the same connection, and it is capped by max_matches. Also, SHOW META after a multi-statement/multi_query call returns metas for all statements — take the right one.

Real-time vs. plain indexes are configured in sphinx.conf, not in the bundle. The bundle will not create an index for you. REPLACE INTO only works on RT indexes; on a plain index it fails. Budget time for sphinx.conf and indexer, which is most of the operational work.

Charset and encoding. Sphinx indexes declare a charset; if you write UTF-8 into a sbcs-configured index you get silent garbage rather than an error. Verify with a query on a known non-ASCII term.

Long-lived connections and timeouts. searchd will drop idle connections. If you see intermittent "MySQL server has gone away"-style errors under PHP-FPM, add a connect timeout/retry in your decorator rather than patching the bundle.

Maintenance risk is the headline. Last release March 2023, 24 stars, zero dependents, single maintainer. Before adopting, verify the bundle's Symfony constraint against your target version (6.x/7.x support is not guaranteed) and confirm it does not rely on removed Doctrine\DBAL APIs. If it fails to install, the whole surface you need is small — a PDO/mysqli wrapper plus a SearchService — so forking or writing ~150 lines yourself is a legitimate alternative. Keep that fork path in mind and don't spread bundle-specific types beyond your SearchService.

Debugging. Enable searchd's query log (query_log = /var/log/sphinx/query.log) and tail it — it shows the exact SphinxQL the server received, which settles "is it my query or my data?". On the PHP side, log the post-escape query string in your decorator.

Extension points. Override the bundle's service definitions in your app config (services.yaml redefinition wins over the bundle's Resources/config), and prefer decorating the client over subclassing it — the internals are unversioned in practice.

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.
unabomber/timezone
delatbabel/contacts
astritzeqiri/g-recaptcha
kevinongko/laravel-genode
developeruz/laravel-analytics
robbiep/afterthedeadline
frankkessler/incontact-laravel-oauth2-rest
jlxh/laravel-userlog
davidmgilo/timetables
qylinfly/role-permission
reflect-digital/conduit
takashato/laravel-vietqr
aitor24/laralang
kraenzle-ritter/entity-relations
dindigital/din-image
pomirleanu/image-colors
sawmainek/laravel-api-generator
delatbabel/jsonresponses
mouhamedfd/permissions-generator
eduplus/qr