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 Omega Search Laravel Package

divineomega/laravel-omega-search

Omega Search brings flexible, driver-based full-text search to Laravel. Make Eloquent models searchable, define indexed fields, and run ranked queries through a clean, unified API. Swap search backends without rewriting code—simple to configure, easy to extend, and built for real apps.

View on GitHub
Deep Wiki
Context7

Getting Started

Install and wire it up

composer require <vendor>/omega-search
php artisan vendor:publish --tag=omega-config

Set your connection/index config in config/omega.php, then add the package's searchable trait and a search scope to the models you want searchable. Once the trait is in place, the canonical first query is a relevance-ordered, paginated Eloquent query:

$results = Post::search($request->q)
    ->paginate(20);

Where to look first

  1. The trait/scope registration on your model — that's the seam everything else flows through.
  2. The published config file — index name, connection, and any scoring/order knobs.
  3. The package's query-building class, which as of 4.1.0 is where the ID-matching → WHERE IN → ORDER BY relevance pipeline lives. That's the file to read when ordering or result counts look wrong.

First use case

The lowest-risk entry point is a read-only search page over an already-populated table. Because the scope returns a normal Eloquent builder, you can layer existing constraints (->where('published', true)), eager loads, and pagination on top without touching the package's internals.

Implementation Patterns

Model scope + standard Eloquent chaining

Treat the search scope as a filter, not a terminal operation. Compose it with your existing query pipeline so authorization, soft deletes, and tenant scoping stay where they already live:

$posts = Post::search($term)
    ->where('tenant_id', $tenant->id)
    ->with('author')
    ->orderByDesc('published_at')
    ->paginate(20);

Note the ordering interaction — see Gotchas.

Rely on the empty-result guard

v4.1.0 makes "no matching IDs" return an empty Eloquent query rather than an unconstrained one. This is the behavior you want, and it makes the search scope safe to use in a pipeline without a conditional:

$posts = Post::search($term)->where('published', true)->paginate(20);
// zero matches => zero rows, not "all published posts"

Do not wrap the scope in if ($ids) guards anymore; that defensive pattern exists to work around the old behavior and now just adds noise.

Cross-engine portability

Relevance ordering is now expressed as a parameter-bound CASE instead of MySQL FIELD(). This means the same query runs on MySQL, MariaDB, PostgreSQL, and SQLite. Practically:

  • Use SQLite in-memory in your test suite and trust the relevance order — it's regression-tested by the package now.
  • Keep the search scope in shared code paths rather than database-specific branches.
  • If you maintain a fork or a custom macro that reproduces ordering, delete it in favor of the package's implementation.

Index synchronization

The search path depends on a maintained ID/term index. Choose one pattern deliberately:

  • Model events / observer — simplest, synchronous, fine for low-volume writes, but couples your write latency to index updates.
  • Queued job per save/delete — the default for anything user-facing at scale.
  • Scheduled full reindex — the safety net. Pair with queued sync so drift is bounded.

Backfill explicitly after deploy: any model rows created before the trait/index existed won't be findable until they're indexed.

Testing

Assert against ->pluck('id')->all() with deterministic fixtures ordered by a known relevance, and always cover the zero-match case (it's the case that regressed before 4.1.0). Run the suite on at least two drivers if you support more than one — that's exactly what the package's new compatibility tests do.

Gotchas and Tips

The 4.1.0 empty-result change is a behavior change in disguise

Old behavior: no matching IDs → the constraint was dropped → you got every model back. Anywhere your code silently depended on that (an "everything" browse page, a fallback search, a test asserting count() > 0 for a nonsense term), it will now return nothing. Grep for search callers whose empty state was never designed, and add an explicit fallback:

$posts = Post::search($term)->paginate(20);

if ($posts->isEmpty()) {
    $posts = Post::where('published', true)->latest()->paginate(20); // intentional fallback
}

This is also worth flagging as a quiet data-exposure fix: previously a query on a private model could leak the whole table if the index returned nothing.

CASE ordering is not index-friendly

Portability cost: the relevance order is computed per row. Keep the candidate ID set small by pushing WHERE constraints into the query before ordering, and paginate early. If result sets get large, profile on your target engine — this is the main performance tradeoff of the 4.1.0 change.

Bind-parameter limits

The ID list is bound, not interpolated (good), but large ID sets still hit driver limits — SQLite historically caps around 999 placeholders, MySQL has a similar bind ceiling. Cap the number of index IDs the package fetches, or chunk the search. Symptoms: sudden "too many SQL variables" errors that only appear on broad queries.

Ordering conflicts

If you append your own orderBy after the search scope, you may override relevance ordering entirely. Decide explicitly: relevance-first (no orderBy after), or recency-first (accept losing relevance). Don't assume relevance survives.

Relevance scores aren't portable

Order is preserved across engines, but scores and tie-breaking can differ. Don't assert exact ordering on ties in tests without a deterministic secondary sort.

Version floor constrains contributor code

Laravel 5.1–13 / PHP 7.1 support means no typed properties, no arrow functions, no named arguments, and no Builder methods newer than 5.1 in any code path you share back. If you're patching the package, mirror its style or your PR will fail the lowest-compatibility suite.

Debugging

The scope returns a real builder — dump ->toSql() and ->getBindings() to see the generated CASE and the ID bindings. If results are empty when you expect hits, check the index first (is the row in the index at all?) before suspecting the query.

Extension points

The order/scoring expression and the ID-resolution step are the two seams worth customizing. Prefer a subclass or config override over editing vendor code, and keep custom ordering in one place so the next release doesn't silently diverge from your fork.

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.
mehedimi/laravel-amp-mail
happones/laravel-vue-i18n-generator
jwerd/inertia-table
siberfx/flysystem-wasabi
hollyit/token-replace
ippanelcom/laravel-sdk
adachsoft/changelog-linter
adachsoft/collection
payerurl/telegrambot-crypto-payment-instant-payout
dskripchenko/laravel-hybrid-search
riorizkyrainey/lara-ced
mehedimi/static-vel
zharp/zharp
nonsapiens/bigquery-model-sync
lgu-bayambang-ict-office/lgu-laravel-development-standards
andydefer/laravel-support
se7enxweb/flysystem
lampwick/laravel
jeslage/statamic-emojis
micahdshackelford/rmap-laravel