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

Symfony Sphinx Bundle Laravel Package

pluk77/symfony-sphinx-bundle

pluk77/symfony-sphinx-bundle integrates Sphinx full-text search with Symfony, providing bundle configuration and services to connect your application to Sphinx indexes and run search queries. A lightweight helper for adding fast, scalable search to Symfony projects.

View on GitHub
Deep Wiki
Context7

Getting Started

Read the repo before you install anything. This is a Symfony bundle (pluk77/symfony-sphinx-bundle), and you are a Laravel developer. composer require will pull symfony/framework-bundle into an app that already pins its own Symfony components, and the NOASSERTION license is a hard stop for commercial code. Treat the repository as a specification you read, not a dependency you install. Budget an hour: DependencyInjection/Configuration.php (the config surface), the service definitions (how the client is constructed and whether it is multi-server aware), the Command/ directory (indexer/searchd lifecycle), and the query helper class (filters, sorting, pagination).

Stand up the engine, not the bundle. The durable part is the server, not the wrapper. Manticore Search speaks the same MySQL wire protocol on port 9306 as Sphinx, so your first working query is a Laravel database connection:

// config/database.php
'search' => [
    'driver'   => 'mysql',
    'host'     => env('SEARCH_HOST', '127.0.0.1'),
    'port'     => env('SEARCH_PORT', 9306),
    'database' => '',
    'username' => '',
    'password' => '',
    'charset'  => 'utf8mb4',
    'options'  => [PDO::ATTR_TIMEOUT => 2],
],

Then prove the round trip:

DB::connection('search')->select(
    'SELECT id, WEIGHT() AS score FROM products WHERE MATCH(?) ORDER BY score DESC LIMIT 20',
    ['wireless headphones']
);

First real use case. Pick one read path — a site search box or a product listing filter — and implement it against that connection with bound parameters. That single endpoint validates the whole proposition: relevance quality, latency, charset handling, and whether your team can operate searchd. Everything else (indexers, deltas, rotation) is downstream of that proof.

Then port intent. Go back to the bundle's query helper and note what it handles that you have not: how it maps SphinxQL rows to domain objects, how it applies filters and sort, how it paginates, what it logs. That list is your backlog for the in-house adapter. The bundle's job in your world is to answer "what did someone else decide was necessary?" — not to run in production.

Implementation Patterns

Pattern 1 — SphinxQL as a named Laravel connection. The lowest-risk shape. One dedicated search connection in config/database.php gives you connection reuse, query logging, parameter binding, and DB::connection('search')->listen(...) for slow-query instrumentation. Keep query construction in one thin repository so controllers never see SphinxQL:

final class ProductSearchRepository
{
    public function search(string $term, array $filters = [], int $page = 1, int $perPage = 20): array
    {
        $sql = 'SELECT id, WEIGHT() AS score FROM products WHERE MATCH(:term)';
        $bindings = ['term' => $this->escapeMatch($term)];

        if (isset($filters['category_id'])) {
            $sql .= ' AND category_id = :category_id';
            $bindings['category_id'] = (int) $filters['category_id'];
        }

        $sql .= ' ORDER BY score DESC, id ASC LIMIT :offset, :limit';
        $bindings['offset'] = ($page - 1) * $perPage;
        $bindings['limit']  = $perPage;

        return DB::connection('search')->select($sql, $bindings);
    }
}

This mirrors what the bundle's "query helpers for filters, sorting, and pagination" do, but with Laravel's primitives and your own test suite.

Pattern 2 — a Scout engine for model-shaped search. If search results should flow back as Eloquent models (Product::search('...')->paginate()), implement a Manticore-compatible Scout engine instead of hand-rolling repositories per model. The engine class wraps the same repository from Pattern 1 and hydrates IDs back through whereIn. Choose this once search is a product surface rather than one endpoint.

Pattern 3 — mirror the bundle's config shape. The bundle's YAML maps cleanly onto a Laravel config file, and keeping the vocabulary familiar pays off when you compare notes or onboard someone who has read the repo:

// config/search.php — shape borrowed from the bundle's Configuration.php
return [
    'connections' => [
        'default' => [
            'hosts' => [
                ['host' => env('SEARCH_HOST', '127.0.0.1'), 'port' => 9306],
                // the bundle's multi-server support: list all searchd nodes
            ],
            'timeout' => 2,
        ],
    ],
    'indexes' => [
        'products' => ['source' => 'products', 'type' => 'rt'], // or 'plain' for batch
    ],
];

Pattern 4 — port the indexing workflow to Artisan. The bundle ships Symfony commands for indexing and restarting searchd. Reproduce the intent as Artisan commands, invoked by scheduler rather than by hand:

class ReindexProducts extends Command
{
    protected $signature = 'search:reindex {--rotate : Use --rotate for batch indexes}';

    public function handle(): int
    {
        $cmd = sprintf(
            'indexer --config %s %s %s',
            escapeshellarg(config('search.sphinx_conf')),
            $this->option('rotate') ? '--rotate' : '',
            'products'
        );

        $process = Process::timeout(3600)->run($cmd);
        Log::info('search.reindex', ['exit' => $process->getExitCode()]);

        return $process->isSuccessful() ? self::SUCCESS : self::FAILURE;
    }
}

Schedule it in routes/console.php or the kernel, and keep searchd --stopwait / restart as its own command so the two concerns stay separable — that is exactly the split the bundle models.

Pattern 5 — log the way the bundle intends. The bundle advertises structured logging of queries and errors. In Laravel this is a channel plus a wrapper, not a service:

Log::channel('search')->info('sphinx.query', [
    'q'        => $term,
    'filters'  => $filters,
    'took_ms'  => $start,
    'hits'     => count($rows),
    'fallback' => false,
]);

Log the query and the timing, never the raw PDO exception stack in the user-facing path — search failures should degrade, not 500.

Pattern 6 — dual-write, then cut over. Backfill the index offline from MySQL, write to both the database and the index (or rely on RT inserts), run reads behind a feature flag, and only then remove the SQL LIKE path. The bundle gives you no coordination for this — the sequencing is yours.

Pattern 7 — a swappable boundary. Put the engine behind interface SearchRepository and bind it in a service provider. When the POC fails, or when the team moves to Meilisearch via Scout, you replace one binding rather than grepping controllers.

Gotchas and Tips

The stack mismatch is the first gotcha, and it does not go away. Laravel cannot autoload or register a Bundle subclass or a DependencyInjection extension. There is no adapter path. Individual service classes may be instantiable if they have no framework coupling, but that is a code-copy exercise with a license question attached — resolve NOASSERTION with legal before you copy a line.

Assume PHP 7.x constraints. The pluk77 namespace and bundle-era packaging point at Symfony 3/4. On PHP 8.2/8.3 with a current Laravel, composer require will almost certainly fail on constraints before it fails on anything else. Let that failure be the decisive answer rather than spending a day on workarounds.

MATCH() is not a bound parameter in the ordinary sense. SphinxQL's full-text operator parses its own query syntax (@field, |, -, quotes). Wrapping it in ? binds the string but does not sanitize the syntax. Strip or escape operators you do not intend to support, and cap query length. This is the risk you inherit if you copy raw-string query building from the bundle — check whether it parameterizes at all before emulating it.

Allow-list sort and filter columns. You cannot bind an identifier. Accept a key from a fixed map (['price' => 'price', 'newest' => 'created_at']) and 422 on anything else. Never interpolate user input into ORDER BY.

LIMIT offset, count — both must be integers. PDO will bind them as strings and the server will reject or silently misbehave. Cast explicitly, and remember the practical ceiling: deep pagination gets slow, so cap it and steer users to narrower filters.

SHOW META costs an extra round trip. Relevance scores and total counts come from a follow-up SHOW META after the main query on the same connection. If you need total_found for pagination, you must issue both; if you only need scores, WEIGHT() in the select list is enough. The bundle's pagination helper almost certainly does the former — measure whether you need it.

Charset and collation drift is silent. Manticore indexes are built with an explicit charset table; if MySQL serves utf8mb4 and the index was built as utf8, accented searches return zero results with no error. Verify with a known-accented query immediately after backfill, not after launch.

Batch indexes lag writes; that is a product decision. If you use plain indexes plus a delta, results are stale between rotations. RT indexes fix fresher reads at the cost of memory and merge tuning. Decide explicitly, and surface the choice to product — "search misses the item we just added" is a bug report you will otherwise receive.

searchd on the critical path turns a search outage into an app outage. Wrap the repository in a try/catch with a short PDO::ATTR_TIMEOUT (2s, not 30), and fall back to a cached result set or a degraded database query. Log the fallback with the same structured channel so you can graph degradation rate.

Multiple hosts in config is failover, not load balancing. The bundle's multi-server support means "try the next node on connect failure." Your Laravel connection config does not do that for you — either point at a single well-known endpoint (a proxy, or Manticore's own replication) or implement the retry loop in the repository.

Pin the server version. Manticore has diverged from Sphinx 2.x in SHOW META behavior and in deprecating indexer-built plain indexes in favor of RT tables. Patterns copied from a Sphinx-era bundle may run without error and rank differently. Pin the image tag and run the adapter's test suite against upgrades before rolling them.

Zero dependents means zero community safety net. No issue triage, no edge-case reports, no protocol-change tracking. Whatever you port, you own permanently — write the tests the bundle does not have, especially around escaping, pagination boundaries, and empty-result handling.

Extend at the interface, not the SQL. The one extension point worth building early is the SearchRepository binding plus a fake implementation that runs against the database. It lets feature work continue when searchd is down in CI and gives you a place to A/B relevance changes without touching production config.

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.
coleus/frame
thtg88/journalism
filagora/filagora-ai-forums
rlacerda83/query-parser
socialdept/atp-orm
cboxdk/laravel-assistant-client
socialdept/atp-resolver
socialdept/tenant-domains
tautvydasr/blade-class-names
socialdept/color-scales
maherelgamil/controller
coleus/widgets
coleus/table
coleus/users
coleus/music
coleus/health
coleus/calendar
coleus/notes
coleus/support
coleus/settings