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 Datatables Scout Laravel Package

yajra/laravel-datatables-scout

Integrates Laravel Scout's full-text search with yajra/laravel-datatables, letting DataTables queries run through Scout engines like Algolia, Meilisearch, or the database driver. Adds a scout driver so searchable models stay filterable and sortable.

View on GitHub
Deep Wiki
Context7

Getting Started

This plugin is not a new abstraction — it is a single behaviour swap. It takes the DataTables global search box value and sends it to Laravel Scout instead of translating it into WHERE column LIKE '%term%'. Column filters, ordering, pagination, eager loading and Eloquent hydration all remain exactly as they were on your existing Yajra endpoint. Read it that way and you will adopt it in an afternoon; read it as a "search feature" and you will over-engineer it.

Confirm the three prerequisites before you install anything. The plugin cannot compensate for a missing dependency, and most "it doesn't work" reports are actually a prerequisite that was never true:

  1. A working yajra/laravel-datatables server-side endpoint (AJAX, toJson(), sorting and paging behaving).
  2. laravel/scout installed with an engine configured in config/scout.php and an index that has been imported.
  3. The model uses Laravel\Scout\Searchable, and User::search('term')->get() returns sensible results in php artisan tinker. Do this step first, always.

If step 3 fails, stop. Nothing downstream will fix it.

Install and register:

composer require yajra/laravel-datatables-scout

Package discovery should register the provider. If the macro reads as undefined at runtime, the usual cause is a stale bootstrap/cache/packages.php — run php artisan package:discover and php artisan optimize:clear.

First use — opt in on one endpoint you already trust:

use Yajra\DataTables\Facades\DataTables;

Route::get('/users', fn () => DataTables::eloquent(User::query())
    ->withScout()          // verify the exact macro name against your installed tag
    ->toJson());

Verify the macro name against src/ rather than assuming; this package is small enough that reading the provider end to end takes five minutes and removes all ambiguity.

Where to look, in order:

  1. README.md — macro name and supported version matrix.
  2. src/ — the provider plus the class that swaps global search for a Scout query. Read it completely. You want to know precisely which request keys are forwarded to the engine and which stay in SQL.
  3. app/Models/*.php — searchable(), toSearchableArray(), shouldBeSearchable(). This is where the majority of bugs actually live.
  4. config/scout.php — driver, index prefix, queue setting, soft-delete handling.

First use case: pick a read-only internal table (users, customers, an audit log) with a handful of obviously searchable fields, and validate three behaviours by hand before anything else: relevance-ordered results for a typed term, a result count matching Model::search($term)->count(), and a clean return to the full dataset when the box is cleared. Only after that graduate to tables behind tenancy, money or PII.

Implementation Patterns

1. Opt in per endpoint. Never in a base controller. The plugin's whole value proposition depends on the plain Eloquent path staying available as a fallback. Apply it table by table so a regression is confined to one screen.

return DataTables::eloquent(User::query())
    ->withScout()
    ->addColumn('actions', fn (User $u) => view('users.actions', compact('u')))
    ->editColumn('created_at', fn (User $u) => $u->created_at->toDateTimeString())
    ->toJson();

2. Hybrid by default: Scout for the global box, SQL for columns. This is the natural and usually correct split. Global search gets relevance and typo tolerance; per-column filters stay indexed, cheap and deterministic through filterColumn. Make the distinction visible in the UI so users do not expect the typo tolerance of the search box from a column input.

return DataTables::eloquent(Order::with('customer'))
    ->withScout()
    ->filterColumn('customer_name', function ($query, $keyword) {
        $query->whereHas('customer', fn ($q) => $q->where('name', 'like', "%{$keyword}%"));
    })
    ->toJson();

3. Shape toSearchableArray() to the table, not to the model. Whatever you index is what the plugin searches. Index the three or four fields the screen actually displays — smaller payloads mean faster engines, lower Algolia bills and fewer false positives from fields nobody is looking at.

public function toSearchableArray(): array
{
    return [
        'id'      => $this->id,
        'name'    => $this->name,
        'email'   => $this->email,
        'company' => $this->company?->name,
    ];
}

4. Apply tenancy and authorization on the builder, before the plugin sees it. Scout knows nothing about policies. The only guarantee that a tenant admin cannot surface another tenant's rows is the constrained builder you hand to DataTables::eloquent().

$query = User::query()->where('tenant_id', $tenant->id);

return DataTables::eloquent($query)->withScout()->toJson();

Where the engine supports filters (Meilisearch filterableAttributes, Algolia attributesForFaceting) you can add them too, but the Eloquent constraint is the one you must have. Write an explicit test per tenant covering global search, a column filter, and a tampered sort parameter.

5. Wrap it behind one seam. Given the package's size (~12 stars, single maintainer, zero known dependents), the highest-value pattern is a single choke point that makes the plugin replaceable:

// app/Support/DataTables/SearchStrategy.php
public function apply(Builder $query, string $mode): Builder
{
    return $mode === 'scout'
        ? DataTables::eloquent($query)->withScout()
        : DataTables::eloquent($query);
}

Gate $mode behind a config flag. This turns "we adopted a small plugin" into "we can drop it in an afternoon", which is the posture this dependency deserves.

6. Keep eager loading exactly as it was. The plugin changes which rows you get, not how they hydrate. with(['customer', 'assignee']) is still required; without it the N+1 will be blamed on "Scout being slow" when it is a hydration problem.

7. Rank on Scout, sort on DataTables. Let the engine own relevance for the global box, but keep explicit orderColumn mappings for clickable headers so users can switch to a deterministic order. Do not rely on engine ranking surviving Eloquent hydration — see the gotchas below.

8. Test by comparison, not by assertion. For each migrated endpoint, seed a fixture set and assert that the Scout-backed and Eloquent-backed responses return the same ID set for the same global term (ordering may legitimately differ). Then test counts, pagination totals and sorting separately. Use Scout::fake() where the engine is irrelevant, and the database driver in CI so the suite does not need a live engine.

9. Decide the export story explicitly. Yajra exports and ->rawColumns() / ->addColumn() closures compose normally, but an export of a Scout-filtered table exports the filtered set. Confirm that is what the business expects before shipping, because "the CSV doesn't match the table" tickets are expensive to unwind.

Gotchas and Tips

recordsTotal / recordsFiltered mismatches are the classic symptom. A table that looks correct but paginates wrong is almost always a count problem: recordsTotal should reflect the unfiltered-but-scoped set, recordsFiltered the Scout-filtered set. Assert both in tests, and watch the cost of the unfiltered count on large tables — cache it if it is stable.

Relevance order is often lost at hydration. Scout returns an ordered list of IDs; Eloquent hydrates via WHERE id IN (...) and hands you database order. If ranking matters visually, sort in PHP against the Scout ID order or lean on explicit header sorting. Test with a term whose ranking is obviously different from row order.

A stale index is indistinguishable from "no results". After editing toSearchableArray() you must re-import: php artisan scout:import "App\Models\User". With SCOUT_QUEUE=true, newly created records are not searchable for seconds or minutes — an intermittent bug that will cost you half a day if you do not suspect it. Put the re-import in your deploy runbook and monitor index lag.

There is no automatic fallback on engine outage. If Meilisearch or Algolia is down or rate-limited, the endpoint throws and the DataTables UI simply breaks. Decide up front whether to catch the exception, log it and fall back to the plain Eloquent path — for internal admin tables the answer is usually yes.

Scout's query API is deliberately narrow. No joins, no aggregates, no arbitrary whereHas. Filters only work on attributes you declared filterable in the engine. A screen that genuinely needs relation-heavy filtering is a poor candidate — leave it on Eloquent with proper DB indexes rather than fighting the engine.

Per-column search never reaches Scout. filterColumn and column inputs hit SQL. That is usually desirable, but juxtaposing a typo-tolerant global box with exact-match column filters can feel inconsistent. Prefer dropdown column filters where the data allows it.

Four independently versioned moving parts. Laravel, Yajra DataTables, Scout and the engine driver all release on their own cadence, and this adapter is one maintainer at ~12 stars. Pin it exactly ("yajra/laravel-datatables-scout": "x.y.z"), commit the lockfile, and re-read src/ whenever you bump DataTables or Scout. If an upgrade blocks you, MIT means forking into packages/ is a legitimate half-day move, not a last resort.

Debug in this order. (1) Model::search('term')->get() in tinker — engine or plugin? (2) Is the index populated and current? (3) Is the term reaching Scout at all — log $request->input('search.value') and drop a temporary dd() in the plugin's search method. (4) Are the counts right? Roughly nine in ten issues resolve at step 1.

Start local dev on the database Scout driver. It removes the engine from the loop, makes the plugin's behaviour trivial to read, and keeps CI fast. Switch to Meilisearch or Algolia only when you are evaluating relevance quality, not correctness.

Extension points worth knowing before you fork. filterColumn for column semantics, orderColumn for computed sortables, shouldBeSearchable() on the model to keep drafts and soft-deleted rows out of the index, and DataTables builder macros if you need to override search wholesale. Anything deeper justifies reading and then vendoring the small src/ — which, at this package's scale, is a routine decision.

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
cgv/localized-resource
noith/settings
kda/laravel-package
kda/filament-dynamic-navigation
bypersoft/signbox
pairus/product-data
fayne/laravel-filament-cms-core
nodisrupt/laravel-agent
sierratecnologia/media-manager