File: /var/www/ivs.kaunokolegija.lt/laravel/vendor/yajra/laravel-datatables-oracle/src/QueryDataTable.php
<?php
namespace Yajra\DataTables;
use Illuminate\Contracts\Database\Eloquent\Builder as EloquentBuilder;
use Illuminate\Contracts\Database\Query\Builder as QueryBuilder;
use Illuminate\Database\Connection;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Query\Expression;
use Illuminate\Http\JsonResponse;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Str;
use Yajra\DataTables\Utilities\Helper;
class QueryDataTable extends DataTableAbstract
{
/**
* Builder object.
*
* @var QueryBuilder
*/
protected QueryBuilder $query;
/**
* Flag for ordering NULLS LAST option.
*
* @var bool
*/
protected bool $nullsLast = false;
/**
* Flag to check if query preparation was already done.
*
* @var bool
*/
protected bool $prepared = false;
/**
* Query callback for custom pagination using limit without offset.
*
* @var callable|null
*/
protected $limitCallback = null;
/**
* Flag to keep the select bindings.
*
* @var bool
*/
protected bool $keepSelectBindings = false;
/**
* Flag to ignore the selects in count query.
*
* @var bool
*/
protected bool $ignoreSelectInCountQuery = false;
/**
* Enable scout search and use this model for searching.
*
* @var Model|null
*/
protected ?Model $scoutModel = null;
/**
* Maximum number of hits to return from scout.
*
* @var int
*/
protected int $scoutMaxHits = 1000;
/**
* Add dynamic filters to scout search.
*
* @var callable|null
*/
protected $scoutFilterCallback = null;
/**
* Flag if scout search was performed.
*
* @var bool
*/
protected bool $scoutSearched = false;
/**
* Scout index name.
*
* @var string
*/
protected string $scoutIndex;
/**
* Scout key name.
*
* @var string
*/
protected string $scoutKey;
/**
* Flag to disable user ordering if a fixed ordering was performed (e.g. scout search).
* Only works with corresponding javascript listener.
*
* @var bool
*/
protected $disableUserOrdering = false;
/**
* @param QueryBuilder $builder
*/
public function __construct(QueryBuilder $builder)
{
$this->query = $builder;
$this->request = app('datatables.request');
$this->config = app('datatables.config');
$this->columns = $builder->columns;
if ($this->config->isDebugging()) {
$this->getConnection()->enableQueryLog();
}
}
/**
* @return \Illuminate\Database\Connection
*/
public function getConnection(): Connection
{
/** @var Connection $connection */
$connection = $this->query->getConnection();
return $connection;
}
/**
* Can the DataTable engine be created with these parameters.
*
* @param mixed $source
* @return bool
*/
public static function canCreate($source): bool
{
return $source instanceof QueryBuilder && ! ($source instanceof EloquentBuilder);
}
/**
* Organizes works.
*
* @param bool $mDataSupport
* @return \Illuminate\Http\JsonResponse
*
* @throws \Exception
*/
public function make($mDataSupport = true): JsonResponse
{
try {
$results = $this->prepareQuery()->results();
$processed = $this->processResults($results, $mDataSupport);
$data = $this->transform($results, $processed);
return $this->render($data);
} catch (\Exception $exception) {
return $this->errorResponse($exception);
}
}
/**
* Get paginated results.
*
* @return \Illuminate\Support\Collection<int, array>
*/
public function results(): Collection
{
return $this->query->get();
}
/**
* Prepare query by executing count, filter, order and paginate.
*
* @return $this
*/
public function prepareQuery(): static
{
if (! $this->prepared) {
$this->totalRecords = $this->totalCount();
$this->filterRecords();
$this->ordering();
$this->paginate();
}
$this->prepared = true;
return $this;
}
/**
* Counts current query.
*
* @return int
*/
public function count(): int
{
return $this->prepareCountQuery()->count();
}
/**
* Prepare count query builder.
*
* @return QueryBuilder
*/
public function prepareCountQuery(): QueryBuilder
{
$builder = clone $this->query;
if ($this->isComplexQuery($builder)) {
$builder->select(DB::raw('1 as dt_row_count'));
if ($this->ignoreSelectInCountQuery || ! $this->isComplexQuery($builder)) {
return $this->getConnection()
->query()
->fromRaw('('.$builder->toSql().') count_row_table')
->setBindings($builder->getBindings());
}
$builder = clone $this->query;
return $this->getConnection()
->query()
->fromRaw('('.$builder->toSql().') count_row_table')
->setBindings($builder->getBindings());
}
$row_count = $this->wrap('row_count');
$builder->select($this->getConnection()->raw("'1' as {$row_count}"));
if (! $this->keepSelectBindings) {
$builder->setBindings([], 'select');
}
return $builder;
}
/**
* Check if builder query uses complex sql.
*
* @param QueryBuilder|EloquentBuilder $query
* @return bool
*/
protected function isComplexQuery($query): bool
{
return Str::contains(Str::lower($query->toSql()), ['union', 'having', 'distinct', 'order by', 'group by']);
}
/**
* Wrap column with DB grammar.
*
* @param string $column
* @return string
*/
protected function wrap(string $column): string
{
return $this->getConnection()->getQueryGrammar()->wrap($column);
}
/**
* Keep the select bindings.
*
* @return $this
*/
public function keepSelectBindings(): static
{
$this->keepSelectBindings = true;
return $this;
}
/**
* Perform column search.
*
* @return void
*/
protected function filterRecords(): void
{
$initialQuery = clone $this->query;
if ($this->autoFilter && $this->request->isSearchable()) {
$this->filtering();
}
if (is_callable($this->filterCallback)) {
call_user_func_array($this->filterCallback, $this->resolveCallbackParameter());
}
$this->columnSearch();
$this->searchPanesSearch();
// If no modification between the original query and the filtered one has been made
// the filteredRecords equals the totalRecords
if ($this->query == $initialQuery) {
$this->filteredRecords ??= $this->totalRecords;
} else {
$this->filteredCount();
}
}
/**
* Perform column search.
*
* @return void
*/
public function columnSearch(): void
{
$columns = $this->request->columns();
foreach ($columns as $index => $column) {
$column = $this->getColumnName($index);
if (is_null($column)) {
continue;
}
if (! $this->request->isColumnSearchable($index) || $this->isBlacklisted($column) && ! $this->hasFilterColumn($column)) {
continue;
}
if ($this->hasFilterColumn($column)) {
$keyword = $this->getColumnSearchKeyword($index, true);
$this->applyFilterColumn($this->getBaseQueryBuilder(), $column, $keyword);
} else {
$column = $this->resolveRelationColumn($column);
$keyword = $this->getColumnSearchKeyword($index);
$this->compileColumnSearch($index, $column, $keyword);
}
}
}
/**
* Check if column has custom filter handler.
*
* @param string $columnName
* @return bool
*/
public function hasFilterColumn(string $columnName): bool
{
return isset($this->columnDef['filter'][$columnName]);
}
/**
* Get column keyword to use for search.
*
* @param int $i
* @param bool $raw
* @return string
*/
protected function getColumnSearchKeyword(int $i, bool $raw = false): string
{
$keyword = $this->request->columnKeyword($i);
if ($raw || $this->request->isRegex($i)) {
return $keyword;
}
return $this->setupKeyword($keyword);
}
protected function getColumnNameByIndex(int $index): string
{
$name = (isset($this->columns[$index]) && $this->columns[$index] != '*')
? $this->columns[$index]
: $this->getPrimaryKeyName();
if ($name instanceof Expression) {
$name = $name->getValue($this->query->getGrammar());
}
return in_array($name, $this->extraColumns, true) ? $this->getPrimaryKeyName() : $name;
}
/**
* Apply filterColumn api search.
*
* @param QueryBuilder $query
* @param string $columnName
* @param string $keyword
* @param string $boolean
* @return void
*/
protected function applyFilterColumn($query, string $columnName, string $keyword, string $boolean = 'and'): void
{
$query = $this->getBaseQueryBuilder($query);
$callback = $this->columnDef['filter'][$columnName]['method'];
if ($this->query instanceof EloquentBuilder) {
$builder = $this->query->newModelInstance()->newQuery();
} else {
$builder = $this->query->newQuery();
}
$callback($builder, $keyword);
/** @var \Illuminate\Database\Query\Builder $baseQueryBuilder */
$baseQueryBuilder = $this->getBaseQueryBuilder($builder);
$query->addNestedWhereQuery($baseQueryBuilder, $boolean);
}
/**
* Get the base query builder instance.
*
* @param QueryBuilder|EloquentBuilder|null $instance
* @return QueryBuilder
*/
protected function getBaseQueryBuilder($instance = null)
{
if (! $instance) {
$instance = $this->query;
}
if ($instance instanceof EloquentBuilder) {
return $instance->getQuery();
}
return $instance;
}
/**
* Get query builder instance.
*
* @return QueryBuilder
*/
public function getQuery(): QueryBuilder
{
return $this->query;
}
/**
* Resolve the proper column name be used.
*
* @param string $column
* @return string
*/
protected function resolveRelationColumn(string $column): string
{
return $column;
}
/**
* Compile queries for column search.
*
* @param int $i
* @param string $column
* @param string $keyword
* @return void
*/
protected function compileColumnSearch(int $i, string $column, string $keyword): void
{
if ($this->request->isRegex($i)) {
$this->regexColumnSearch($column, $keyword);
} else {
$this->compileQuerySearch($this->query, $column, $keyword, '');
}
}
/**
* Compile regex query column search.
*
* @param string $column
* @param string $keyword
* @return void
*/
protected function regexColumnSearch(string $column, string $keyword): void
{
$column = $this->wrap($column);
switch ($this->getConnection()->getDriverName()) {
case 'oracle':
$sql = ! $this->config->isCaseInsensitive()
? 'REGEXP_LIKE( '.$column.' , ? )'
: 'REGEXP_LIKE( LOWER('.$column.') , ?, \'i\' )';
break;
case 'pgsql':
$column = $this->castColumn($column);
$sql = ! $this->config->isCaseInsensitive() ? $column.' ~ ?' : $column.' ~* ? ';
break;
default:
$sql = ! $this->config->isCaseInsensitive()
? $column.' REGEXP ?'
: 'LOWER('.$column.') REGEXP ?';
$keyword = Str::lower($keyword);
}
$this->query->whereRaw($sql, [$keyword]);
}
/**
* Wrap a column and cast based on database driver.
*
* @param string $column
* @return string
*/
protected function castColumn(string $column): string
{
switch ($this->getConnection()->getDriverName()) {
case 'pgsql':
return 'CAST('.$column.' as TEXT)';
case 'firebird':
return 'CAST('.$column.' as VARCHAR(255))';
default:
return $column;
}
}
/**
* Compile query builder where clause depending on configurations.
*
* @param QueryBuilder|EloquentBuilder $query
* @param string $column
* @param string $keyword
* @param string $boolean
* @return void
*/
protected function compileQuerySearch($query, string $column, string $keyword, string $boolean = 'or'): void
{
$column = $this->addTablePrefix($query, $column);
$column = $this->castColumn($column);
$sql = $column.' LIKE ?';
if ($this->config->isCaseInsensitive()) {
$sql = 'LOWER('.$column.') LIKE ?';
}
$query->{$boolean.'WhereRaw'}($sql, [$this->prepareKeyword($keyword)]);
}
/**
* Patch for fix about ambiguous field.
* Ambiguous field error will appear when query use join table and search with keyword.
*
* @param QueryBuilder|EloquentBuilder $query
* @param string $column
* @return string
*/
protected function addTablePrefix($query, string $column): string
{
if (! str_contains($column, '.')) {
$q = $this->getBaseQueryBuilder($query);
$from = $q->from;
/** @phpstan-ignore-next-line */
if (! $from instanceof Expression) {
if (str_contains($from, ' as ')) {
$from = explode(' as ', $from)[1];
}
$column = $from.'.'.$column;
}
}
return $this->wrap($column);
}
/**
* Prepare search keyword based on configurations.
*
* @param string $keyword
* @return string
*/
protected function prepareKeyword(string $keyword): string
{
if ($this->config->isCaseInsensitive()) {
$keyword = Str::lower($keyword);
}
if ($this->config->isStartsWithSearch()) {
return "$keyword%";
}
if ($this->config->isWildcard()) {
$keyword = Helper::wildcardLikeString($keyword);
}
if ($this->config->isSmartSearch()) {
$keyword = "%$keyword%";
}
return $keyword;
}
/**
* Add custom filter handler for the give column.
*
* @param string $column
* @param callable $callback
* @return $this
*/
public function filterColumn($column, callable $callback): static
{
$this->columnDef['filter'][$column] = ['method' => $callback];
return $this;
}
/**
* Order each given columns versus the given custom sql.
*
* @param array $columns
* @param string $sql
* @param array $bindings
* @return $this
*/
public function orderColumns(array $columns, $sql, $bindings = []): static
{
foreach ($columns as $column) {
$this->orderColumn($column, str_replace(':column', $column, $sql), $bindings);
}
return $this;
}
/**
* Override default column ordering.
*
* @param string $column
* @param string|\Closure $sql
* @param array $bindings
* @return $this
*
* @internal string $1 Special variable that returns the requested order direction of the column.
*/
public function orderColumn($column, $sql, $bindings = []): static
{
$this->columnDef['order'][$column] = compact('sql', 'bindings');
return $this;
}
/**
* Set datatables to do ordering with NULLS LAST option.
*
* @return $this
*/
public function orderByNullsLast(): static
{
$this->nullsLast = true;
return $this;
}
/**
* Perform pagination.
*
* @return void
*/
public function paging(): void
{
$start = $this->request->start();
$length = $this->request->length();
$limit = $length > 0 ? $length : 10;
if (is_callable($this->limitCallback)) {
$this->query->limit($limit);
call_user_func_array($this->limitCallback, [$this->query]);
} else {
$this->query->skip($start)->take($limit);
}
}
/**
* Paginate dataTable using limit without offset
* with additional where clause via callback.
*
* @param callable $callback
* @return $this
*/
public function limit(callable $callback): static
{
$this->limitCallback = $callback;
return $this;
}
/**
* Add column in collection.
*
* @param string $name
* @param string|callable $content
* @param bool|int $order
* @return $this
*/
public function addColumn($name, $content, $order = false): static
{
$this->pushToBlacklist($name);
return parent::addColumn($name, $content, $order);
}
/**
* Perform search using search pane values.
*
* @return void
*
* @throws \Psr\Container\ContainerExceptionInterface
* @throws \Psr\Container\NotFoundExceptionInterface
*/
protected function searchPanesSearch(): void
{
/** @var string[] $columns */
$columns = $this->request->get('searchPanes', []);
foreach ($columns as $column => $values) {
if ($this->isBlacklisted($column)) {
continue;
}
if ($this->searchPanes[$column] && $callback = $this->searchPanes[$column]['builder']) {
$callback($this->query, $values);
} else {
$this->query->whereIn($column, $values);
}
}
}
/**
* Resolve callback parameter instance.
*
* @return array<int|string, mixed>
*/
protected function resolveCallbackParameter(): array
{
return [$this->query, $this->scoutSearched];
}
/**
* Perform default query orderBy clause.
*
* @return void
*
* @throws \Psr\Container\ContainerExceptionInterface
* @throws \Psr\Container\NotFoundExceptionInterface
*/
protected function defaultOrdering(): void
{
collect($this->request->orderableColumns())
->map(function ($orderable) {
$orderable['name'] = $this->getColumnName($orderable['column'], true);
return $orderable;
})
->reject(function ($orderable) {
return $this->isBlacklisted($orderable['name']) && ! $this->hasOrderColumn($orderable['name']);
})
->each(function ($orderable) {
$column = $this->resolveRelationColumn($orderable['name']);
if ($this->hasOrderColumn($orderable['name'])) {
$this->applyOrderColumn($orderable['name'], $orderable);
} elseif ($this->hasOrderColumn($column)) {
$this->applyOrderColumn($column, $orderable);
} else {
$nullsLastSql = $this->getNullsLastSql($column, $orderable['direction']);
$normalSql = $this->wrap($column).' '.$orderable['direction'];
$sql = $this->nullsLast ? $nullsLastSql : $normalSql;
$this->query->orderByRaw($sql);
}
});
}
/**
* Check if column has custom sort handler.
*
* @param string $column
* @return bool
*/
protected function hasOrderColumn(string $column): bool
{
return isset($this->columnDef['order'][$column]);
}
/**
* Apply orderColumn custom query.
*
* @param string $column
* @param array $orderable
*/
protected function applyOrderColumn(string $column, array $orderable): void
{
$sql = $this->columnDef['order'][$column]['sql'];
if ($sql === false) {
return;
}
if (is_callable($sql)) {
call_user_func($sql, $this->query, $orderable['direction']);
} else {
$sql = str_replace('$1', $orderable['direction'], $sql);
$bindings = $this->columnDef['order'][$column]['bindings'];
$this->query->orderByRaw($sql, $bindings);
}
}
/**
* Get NULLS LAST SQL.
*
* @param string $column
* @param string $direction
* @return string
*
* @throws \Psr\Container\ContainerExceptionInterface
* @throws \Psr\Container\NotFoundExceptionInterface
*/
protected function getNullsLastSql($column, $direction): string
{
/** @var string $sql */
$sql = $this->config->get('datatables.nulls_last_sql', '%s %s NULLS LAST');
return str_replace(
[':column', ':direction'],
[$column, $direction],
sprintf($sql, $column, $direction)
);
}
/**
* Perform global search for the given keyword.
*
* @param string $keyword
* @return void
*/
protected function globalSearch(string $keyword): void
{
// Try scout search first & fall back to default search if disabled/failed
if ($this->applyScoutSearch($keyword)) {
return;
}
$this->query->where(function ($query) use ($keyword) {
collect($this->request->searchableColumnIndex())
->map(function ($index) {
return $this->getColumnName($index);
})
->filter()
->reject(function ($column) {
return $this->isBlacklisted($column) && ! $this->hasFilterColumn($column);
})
->each(function ($column) use ($keyword, $query) {
if ($this->hasFilterColumn($column)) {
$this->applyFilterColumn($query, $column, $keyword, 'or');
} else {
$this->compileQuerySearch($query, $column, $keyword);
}
});
});
}
/**
* Perform multi-term search by splitting keyword into
* individual words and searches for each of them.
*
* @param string $keyword
* @return void
*/
protected function smartGlobalSearch($keyword): void
{
// Try scout search first & fall back to default search if disabled/failed
if ($this->applyScoutSearch($keyword)) {
return;
}
parent::smartGlobalSearch($keyword);
}
/**
* Append debug parameters on output.
*
* @param array $output
* @return array
*/
protected function showDebugger(array $output): array
{
$query_log = $this->getConnection()->getQueryLog();
array_walk_recursive($query_log, function (&$item) {
if (is_string($item) && extension_loaded('iconv')) {
$item = iconv('iso-8859-1', 'utf-8', $item);
}
});
$output['queries'] = $query_log;
$output['input'] = $this->request->all();
return $output;
}
/**
* Attach custom with meta on response.
*
* @param array $data
* @return array
*/
protected function attachAppends(array $data): array
{
$appends = [];
foreach ($this->appends as $key => $value) {
if (is_callable($value)) {
$appends[$key] = value($value($this->getFilteredQuery()));
} else {
$appends[$key] = $value;
}
}
// Set flag to disable ordering
$appends['disableOrdering'] = $this->disableUserOrdering;
return array_merge($data, $appends);
}
/**
* Get filtered, ordered and paginated query.
*
* @return QueryBuilder
*/
public function getFilteredQuery(): QueryBuilder
{
$this->prepareQuery();
return $this->getQuery();
}
/**
* Ignore the selects in count query.
*
* @return $this
*/
public function ignoreSelectsInCountQuery(): static
{
$this->ignoreSelectInCountQuery = true;
return $this;
}
/**
* Perform sorting of columns.
*
* @return void
*/
public function ordering(): void
{
// Skip if user ordering is disabled (e.g. scout search)
if ($this->disableUserOrdering) {
return;
}
parent::ordering();
}
/**
* Enable scout search and use provided model for searching.
* $max_hits is the maximum number of hits to return from scout.
*
* @param string $model
* @param int $max_hits
* @return $this
*
* @throws \Exception
*/
public function enableScoutSearch(string $model, int $max_hits = 1000): static
{
$scout_model = new $model;
if (! class_exists($model) || ! ($scout_model instanceof Model)) {
throw new \Exception("$model must be an Eloquent Model.");
}
if (! method_exists($scout_model, 'searchableAs') || ! method_exists($scout_model, 'getScoutKeyName')) {
throw new \Exception("$model must use the Searchable trait.");
}
$this->scoutModel = $scout_model;
$this->scoutMaxHits = $max_hits;
$this->scoutIndex = $this->scoutModel->searchableAs();
$this->scoutKey = $this->scoutModel->getScoutKeyName();
return $this;
}
/**
* Add dynamic filters to scout search.
*
* @param callable $callback
* @return $this
*/
public function scoutFilter(callable $callback): static
{
$this->scoutFilterCallback = $callback;
return $this;
}
/**
* Apply scout search to query if enabled.
*
* @param string $search_keyword
* @return bool
*/
protected function applyScoutSearch(string $search_keyword): bool
{
if ($this->scoutModel == null) {
return false;
}
try {
// Perform scout search
$search_filters = '';
if (is_callable($this->scoutFilterCallback)) {
$search_filters = ($this->scoutFilterCallback)($search_keyword);
}
$search_results = $this->performScoutSearch($search_keyword, $search_filters);
// Apply scout search results to query
$this->query->where(function ($query) use ($search_results) {
$this->query->whereIn($this->scoutKey, $search_results);
});
// Order by scout search results & disable user ordering (if db driver is supported)
if (count($search_results) > 0 && $this->applyFixedOrderingToQuery($this->scoutKey, $search_results)) {
// Disable user ordering because we already ordered by search relevancy
$this->disableUserOrdering = true;
}
$this->scoutSearched = true;
return true;
} catch (\Exception) {
// Scout search failed, fallback to default search
return false;
}
}
/**
* Apply fixed ordering to query by a fixed set of values depending on database driver (used for scout search).
*
* Currently supported drivers: MySQL
*
* @param string $keyName
* @param array $orderedKeys
* @return bool
*/
protected function applyFixedOrderingToQuery(string $keyName, array $orderedKeys)
{
$connection = $this->getConnection();
$driverName = $connection->getDriverName();
// Escape keyName and orderedKeys
$keyName = $connection->getQueryGrammar()->wrap($keyName);
$orderedKeys = collect($orderedKeys)
->map(function ($value) use ($connection) {
return $connection->escape($value);
});
switch ($driverName) {
case 'mysql':
$this->query->orderByRaw("FIELD($keyName, ".$orderedKeys->implode(',').')');
return true;
case 'pgsql':
case 'oracle':
$this->query->orderByRaw(
'CASE '
.
$orderedKeys
->map(fn ($value, $index) => "WHEN $keyName=$value THEN $index")
->implode(' ')
.
' END'
);
return true;
case 'sqlite':
case 'sqlsrv':
$this->query->orderByRaw(
"CASE $keyName "
.
$orderedKeys
->map(fn ($value, $index) => "WHEN $value THEN $index")
->implode(' ')
.
' END'
);
return true;
default:
return false;
}
}
/**
* Perform a scout search with the configured engine and given parameters. Return matching model IDs.
*
* @param string $searchKeyword
* @param mixed $searchFilters
* @return array
*
* @throws \Exception
*/
protected function performScoutSearch(string $searchKeyword, mixed $searchFilters = []): array
{
if (! class_exists('\Laravel\Scout\EngineManager')) {
throw new \Exception('Laravel Scout is not installed.');
}
$engine = app(\Laravel\Scout\EngineManager::class)->engine();
if ($engine instanceof \Laravel\Scout\Engines\MeilisearchEngine) {
/** @var \Meilisearch\Client $engine */
$search_results = $engine
->index($this->scoutIndex)
->rawSearch($searchKeyword, [
'limit' => $this->scoutMaxHits,
'attributesToRetrieve' => [$this->scoutKey],
'filter' => $searchFilters,
]);
/** @var array<int, array<string, mixed>> $hits */
$hits = $search_results['hits'] ?? [];
return collect($hits)
->pluck($this->scoutKey)
->all();
} elseif ($engine instanceof \Laravel\Scout\Engines\AlgoliaEngine) {
/** @var \Algolia\AlgoliaSearch\SearchClient $engine */
$algolia = $engine->initIndex($this->scoutIndex);
$search_results = $algolia->search($searchKeyword, [
'offset' => 0,
'length' => $this->scoutMaxHits,
'attributesToRetrieve' => [$this->scoutKey],
'attributesToHighlight' => [],
'filters' => $searchFilters,
]);
/** @var array<int, array<string, mixed>> $hits */
$hits = $search_results['hits'] ?? [];
return collect($hits)
->pluck($this->scoutKey)
->all();
} else {
throw new \Exception('Unsupported Scout Engine. Currently supported: Meilisearch, Algolia');
}
}
}