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

Laravel Scout Sphinx Laravel Package

constantable/laravel-scout-sphinx

A Laravel Scout driver that integrates the Sphinx search engine into Laravel applications, enabling full-text search through Scout’s familiar model API. Configure your Sphinx index and use search, pagination, and indexing with minimal setup.

View on GitHub
Deep Wiki
Context7

Getting Started

constantable/laravel-scout-sphinx is a Laravel Scout engine that talks to Sphinx (or Manticore) over SphinxQL — the MySQL-compatible port (9306 by default), not the legacy HTTP/searchd API on 9312. It plugs into Scout, so day-to-day you use Model::search() and the standard Scout APIs; the package only replaces the storage/query backend.

  1. Install (v3):

    composer require constantable/laravel-scout-sphinx:^3.0
    

    v3 bumps the laravel/scout constraint, so confirm the resolved Scout version against your Laravel/PHP floor before merging (composer why laravel/scout).

  2. Point Scout at the driver — set SCOUT_DRIVER=sphinx and ensure the package's config (host/port for searchd) is published or present in config/scout.php. Treat the published config file as the source of truth: the key layout is what the engine actually reads, and it's the first thing to check when connections fail.

  3. Set up the index first, the model second. The engine can only query attributes that exist in the Sphinx index definition. For a real-time index, declare every filterable/sortable field as rt_attr_* (or sql_attr_* for plain indexes) and keep id as an unsigned bigint matching the model key.

  4. Model wiring:

    use Laravel\Scout\Searchable;
    
    class Post extends Model
    {
        use Searchable;
    
        public function searchableAs(): string
        {
            return 'posts';
        }
    
        public function toSearchableArray(): array
        {
            return [
                'id'         => $this->id,
                'title'      => $this->title,
                'body'       => $this->body,
                'category_id'=> $this->category_id, // must be an rt_attr_*
                'created_at' => $this->created_at->timestamp,
            ];
        }
    }
    
  5. First use case: create an RT index named posts, run php artisan scout:import "App\Models\Post", then Post::search('laravel')->get(). If that returns rows, the wiring is correct and everything else is tuning.

Implementation Patterns

Real-time indexes are the happy path. RT indexes accept REPLACE/DELETE from the engine, which is what makes Scout's searchable trait, scout:import and scout:flush behave the way you expect. Plain indexes are built by the external indexer binary — scout:import cannot populate them, so pair them with a cron indexer --rotate job and use the package read-only.

Async indexing. Turn on 'queue' => true in config/scout.php so saves don't block on SphinxQL round-trips, then keep a worker running. Watch for the classic ordering bug: a queued REPLACE arriving after a DELETE when a record is created and deleted quickly.

Filtering + full text. Scout's where() maps to a SphinxQL WHERE on attributes only:

Post::search('release notes')
    ->where('category_id', 3)
    ->where('created_at', '>', now()->subMonth()->timestamp)
    ->orderBy('created_at', 'desc')
    ->paginate(20);

Full-text fields (title, body) are not filterable — attempting it produces a driver-level error, not a validation error.

Escaping the abstraction. Use ->raw() when you need Sphinx-specific syntax (snippets, OPTION ranker=, WITHIN, facet-style GROUP BY). After PR #19 removed the option the driver used to inject into every query, this is now the correct place to re-add any option your relevance tuning depends on.

Per-environment index names. searchableAs() returning posts_staging / posts_production is the cheapest way to avoid two apps clobbering one index.

Custom options belong in a custom engine. Subclass SphinxEngine, override the query builder, and rebind the sphinx engine in a service provider. That's more maintainable than post-processing raw queries in every call site, and it survives minor upgrades.

Testing. Run a throwaway Sphinx/Manticore container in CI, import fixtures, and assert on ids and counts, never on relevance scores — those shift between versions and ranker settings.

Manticore is a drop-in. Manticore Search speaks SphinxQL and is actively maintained; if you're greenfield, point the driver at Manticore and keep the same code.

Gotchas and Tips

  • The v3 upgrade is a major for two reasons, and only one of them is loud. The Scout version bump will surface as a Composer conflict (good). The removed hard-coded query option in PR #19 is silent: any query that implicitly relied on that default will still parse but can return different relevance ordering and different result counts. Before/after diff a handful of representative searches — run searchd --console and compare the emitted SphinxQL — rather than assuming the upgrade is a no-op.
  • Scout 10+ changed the Engine contract. If you've copied or subclassed the engine, diff your copy against the vendor class after upgrading; missing interface methods fail at container resolution, which reads as a confusing "driver not supported" error.
  • scout:import vs indexer — the single biggest source of "my search returns nothing." Confirm which index type you're on before debugging the app.
  • Sphinx errors are PDO exceptions. Unknown attribute, bad syntax, searchd down — all arrive as connection/query exceptions, not Laravel-friendly messages. Wrap user-driven searches in try/catch and log the query, otherwise a typo in a filter looks like a 500.
  • Interpolation risk. The engine builds SphinxQL strings, so never pass raw user input into search() or ->raw() unfiltered; strip SphinxQL operators (@, (, ), |, -, ") when accepting public search terms.
  • max_matches defaults to 1000. Deep pagination silently truncates; raise it in the index config if you need page 50.
  • Attribute types matter. Timestamps as strings sort lexicographically-but-wrong; store them as integers (see created_at above). Boolean flags need rt_attr_uint.
  • getTotalCount() can be approximate on large indexes — don't drive pagination UI off it if exact counts matter.
  • Never run scout:flush against a shared or production index — it's a truncate, and the index is global, not per-model.
  • Debug checklist when a search misbehaves: (1) does the index exist and have docs? SELECT COUNT(*) FROM index; (2) is the attribute declared? (3) is the driver using the option you think it is? (4) is the queued worker actually consuming?
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.
uni-method/symfony-lib
tacticmedia/scim-server-bundle
neika-ai/symfony-bundle
mattleads/agent-publishing-bundle
glavweb/framework-bundle
glavweb/extra-bundle
glavweb/api-bundle
glavweb/admin-bundle
adachsoft/api-version-bundle
elsayed85/laravel-api-exceptions
ouhssini/laravel-demo-mode
daworks/ncloud-outbound-mailer-driver
gp247/cms
ignibyte/aic-gates
nexa-merchant/blog
novasa/laravel-language-center
opencage/geocode
adolfoholzer/parameters
livenetworks/ln-starter
cgv/localized-resource