From 3ab1f67dbf50c3fbffd9e3979fb6fe1a53913c0c Mon Sep 17 00:00:00 2001 From: Furkan Akkoc Date: Wed, 11 Oct 2023 21:03:26 +0200 Subject: [PATCH 01/12] Implemented scout searching (currently only meilisearch) --- src/QueryDataTable.php | 173 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 173 insertions(+) diff --git a/src/QueryDataTable.php b/src/QueryDataTable.php index 53a1d23d..9a8c6bef 100644 --- a/src/QueryDataTable.php +++ b/src/QueryDataTable.php @@ -5,6 +5,7 @@ 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; @@ -56,6 +57,35 @@ class QueryDataTable extends DataTableAbstract */ protected bool $ignoreSelectInCountQuery = false; + /** + * Enable scout search and use this model for searching. + * + * @var string|null + */ + protected ?string $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 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 */ @@ -778,6 +808,9 @@ protected function getNullsLastSql($column, $direction): string */ 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) { @@ -835,6 +868,9 @@ protected function attachAppends(array $data): array } } + // Set flag to disable ordering + $appends['disableOrdering'] = $this->disableUserOrdering; + return array_merge($data, $appends); } @@ -861,4 +897,141 @@ public function ignoreSelectsInCountQuery(): static 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 + */ + public function enableScoutSearch(string $model, int $max_hits = 1000): static + { + if ( + class_exists($model) + && + is_subclass_of($model, Model::class) + && + in_array("Laravel\Scout\Searchable", class_uses_recursive($model)) + ) + { + $this->scoutModel = $model; + $this->scoutMaxHits = $max_hits; + } + return $this; + } + + /** + * Add dynamic filters to scout search. + * + * @param callable $callback + * @return $this + */ + public function scoutFilter(callable $callback): static + { + $this->scoutFilterCallback = $callback; + + return $this; + } + + protected function applyScoutSearch(string $search_keyword): bool + { + if ($this->scoutModel == null) return false; + + try + { + // Perform scout search + $scout_index = (new $this->scoutModel)->searchableAs(); + $scout_key = (new $this->scoutModel)->getScoutKeyName(); + $search_filters = []; + if (is_callable($this->scoutFilterCallback)) + { + $search_filters = ($this->scoutFilterCallback)($search_keyword); + } + + $search_results = $this->performScoutSearch($scout_index, $scout_key, $search_keyword, $search_filters); + + // Apply scout search results to query + $this->query->where(function ($query) use ($scout_key, $search_results) { + $this->query->whereIn($scout_key, $search_results); + }); + + // Order by scout search results & disable user ordering + if (count($search_results) > 0) + { + $escaped_ids = collect($search_results) + ->map(function ($id) { + return \DB::connection()->getPdo()->quote($id); + }) + ->implode(','); + + $this->query->orderByRaw("FIELD($scout_key, $escaped_ids)"); + } + + // Disable user ordering because we already order by search relevancy + $this->disableUserOrdering = true; + + return true; + } + catch (\Exception) + { + // Scout search failed, fallback to default search + return false; + } + } + + /** + * Perform a scout search with the configured engine and given parameters. Return matching model IDs. + * + * @param string $scoutIndex + * @param string $scoutKey + * @param string $searchKeyword + * @param array $searchFilters + * @return array + */ + protected function performScoutSearch(string $scoutIndex, string $scoutKey, string $searchKeyword, array $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) + { + // Meilisearch Engine + $search_results = $engine + ->index($scoutIndex) + ->rawSearch($searchKeyword, [ + 'limit' => $this->scoutMaxHits, + 'attributesToRetrieve' => [ $scoutKey ], + 'filter' => $searchFilters, + ]); + + return collect($search_results['hits'] ?? []) + ->pluck($scoutKey) + ->all(); + } + else + { + throw new \Exception('Unsupported Scout Engine. Currently supported: Meilisearch'); + } + } } From 7cf6425d0376432fb581af88cf315b243c17eb16 Mon Sep 17 00:00:00 2001 From: Furkan Akkoc Date: Wed, 11 Oct 2023 22:05:50 +0200 Subject: [PATCH 02/12] Added $scout_searched to filter callback, Fixed bug (searchfilters can be string) --- src/CollectionDataTable.php | 6 +++--- src/DataTableAbstract.php | 4 ++-- src/QueryDataTable.php | 22 +++++++++++++++------- 3 files changed, 20 insertions(+), 12 deletions(-) diff --git a/src/CollectionDataTable.php b/src/CollectionDataTable.php index 4c195f83..a22c0f7b 100644 --- a/src/CollectionDataTable.php +++ b/src/CollectionDataTable.php @@ -334,10 +334,10 @@ protected function getSorter(array $criteria): Closure /** * Resolve callback parameter instance. * - * @return static + * @return array */ - protected function resolveCallbackParameter(): self + protected function resolveCallbackParameter(): array { - return $this; + return [$this, false]; } } diff --git a/src/DataTableAbstract.php b/src/DataTableAbstract.php index 7814c7f1..6f6041f3 100644 --- a/src/DataTableAbstract.php +++ b/src/DataTableAbstract.php @@ -681,7 +681,7 @@ protected function isBlacklisted($column): bool public function ordering(): void { if ($this->orderCallback) { - call_user_func($this->orderCallback, $this->resolveCallbackParameter()); + call_user_func_array($this->orderCallback, $this->resolveCallbackParameter()); } else { $this->defaultOrdering(); } @@ -776,7 +776,7 @@ protected function filterRecords(): void } if (is_callable($this->filterCallback)) { - call_user_func($this->filterCallback, $this->resolveCallbackParameter()); + call_user_func_array($this->filterCallback, $this->resolveCallbackParameter()); } $this->columnSearch(); diff --git a/src/QueryDataTable.php b/src/QueryDataTable.php index 9a8c6bef..b5c21423 100644 --- a/src/QueryDataTable.php +++ b/src/QueryDataTable.php @@ -78,6 +78,13 @@ class QueryDataTable extends DataTableAbstract */ protected $scoutFilterCallback = null; + /** + * Flag if scout search was performed. + * + * @var bool + */ + protected bool $scoutSearched = false; + /** * Flag to disable user ordering if a fixed ordering was performed (e.g. scout search). * Only works with corresponding javascript listener. @@ -267,7 +274,7 @@ protected function filterRecords(): void } if (is_callable($this->filterCallback)) { - call_user_func($this->filterCallback, $this->resolveCallbackParameter()); + call_user_func_array($this->filterCallback, $this->resolveCallbackParameter()); } $this->columnSearch(); @@ -703,11 +710,11 @@ protected function searchPanesSearch(): void /** * Resolve callback parameter instance. * - * @return QueryBuilder + * @return array */ - protected function resolveCallbackParameter() + protected function resolveCallbackParameter(): array { - return $this->query; + return [$this->query, $this->scoutSearched]; } /** @@ -960,7 +967,7 @@ protected function applyScoutSearch(string $search_keyword): bool // Perform scout search $scout_index = (new $this->scoutModel)->searchableAs(); $scout_key = (new $this->scoutModel)->getScoutKeyName(); - $search_filters = []; + $search_filters = ''; if (is_callable($this->scoutFilterCallback)) { $search_filters = ($this->scoutFilterCallback)($search_keyword); @@ -988,6 +995,7 @@ protected function applyScoutSearch(string $search_keyword): bool // Disable user ordering because we already order by search relevancy $this->disableUserOrdering = true; + $this->scoutSearched = true; return true; } catch (\Exception) @@ -1003,10 +1011,10 @@ protected function applyScoutSearch(string $search_keyword): bool * @param string $scoutIndex * @param string $scoutKey * @param string $searchKeyword - * @param array $searchFilters + * @param mixed $searchFilters * @return array */ - protected function performScoutSearch(string $scoutIndex, string $scoutKey, string $searchKeyword, array $searchFilters = []): array + protected function performScoutSearch(string $scoutIndex, string $scoutKey, string $searchKeyword, mixed $searchFilters = []): array { if (!class_exists('\Laravel\Scout\EngineManager')) { From c72cbdfdbf45996e6a53ceb30bc9e411ac0fb5f1 Mon Sep 17 00:00:00 2001 From: Furkan Akkoc Date: Thu, 12 Oct 2023 04:34:17 +0200 Subject: [PATCH 03/12] Fixed some StyleCI / PHPStan errors --- src/CollectionDataTable.php | 2 +- src/DataTableAbstract.php | 2 +- src/QueryDataTable.php | 42 ++++++++++++++++--------------------- 3 files changed, 20 insertions(+), 26 deletions(-) diff --git a/src/CollectionDataTable.php b/src/CollectionDataTable.php index a22c0f7b..99e7da1a 100644 --- a/src/CollectionDataTable.php +++ b/src/CollectionDataTable.php @@ -334,7 +334,7 @@ protected function getSorter(array $criteria): Closure /** * Resolve callback parameter instance. * - * @return array + * @return array */ protected function resolveCallbackParameter(): array { diff --git a/src/DataTableAbstract.php b/src/DataTableAbstract.php index 6f6041f3..90ff9709 100644 --- a/src/DataTableAbstract.php +++ b/src/DataTableAbstract.php @@ -690,7 +690,7 @@ public function ordering(): void /** * Resolve callback parameter instance. * - * @return mixed + * @return array */ abstract protected function resolveCallbackParameter(); diff --git a/src/QueryDataTable.php b/src/QueryDataTable.php index b5c21423..9d34f1d0 100644 --- a/src/QueryDataTable.php +++ b/src/QueryDataTable.php @@ -710,7 +710,7 @@ protected function searchPanesSearch(): void /** * Resolve callback parameter instance. * - * @return array + * @return array */ protected function resolveCallbackParameter(): array { @@ -816,7 +816,9 @@ protected function getNullsLastSql($column, $direction): string protected function globalSearch(string $keyword): void { // Try scout search first & fall back to default search if disabled/failed - if ($this->applyScoutSearch($keyword)) return; + if ($this->applyScoutSearch($keyword)) { + return; + } $this->query->where(function ($query) use ($keyword) { collect($this->request->searchableColumnIndex()) @@ -913,8 +915,7 @@ public function ignoreSelectsInCountQuery(): static public function ordering(): void { // Skip if user ordering is disabled (e.g. scout search) - if ($this->disableUserOrdering) - { + if ($this->disableUserOrdering) { return; } @@ -937,11 +938,11 @@ class_exists($model) is_subclass_of($model, Model::class) && in_array("Laravel\Scout\Searchable", class_uses_recursive($model)) - ) - { + ) { $this->scoutModel = $model; $this->scoutMaxHits = $max_hits; } + return $this; } @@ -960,16 +961,16 @@ public function scoutFilter(callable $callback): static protected function applyScoutSearch(string $search_keyword): bool { - if ($this->scoutModel == null) return false; + if ($this->scoutModel == null) { + return false; + } - try - { + try { // Perform scout search $scout_index = (new $this->scoutModel)->searchableAs(); $scout_key = (new $this->scoutModel)->getScoutKeyName(); $search_filters = ''; - if (is_callable($this->scoutFilterCallback)) - { + if (is_callable($this->scoutFilterCallback)) { $search_filters = ($this->scoutFilterCallback)($search_keyword); } @@ -981,8 +982,7 @@ protected function applyScoutSearch(string $search_keyword): bool }); // Order by scout search results & disable user ordering - if (count($search_results) > 0) - { + if (count($search_results) > 0) { $escaped_ids = collect($search_results) ->map(function ($id) { return \DB::connection()->getPdo()->quote($id); @@ -997,9 +997,7 @@ protected function applyScoutSearch(string $search_keyword): bool $this->scoutSearched = true; return true; - } - catch (\Exception) - { + } catch (\Exception) { // Scout search failed, fallback to default search return false; } @@ -1016,29 +1014,25 @@ protected function applyScoutSearch(string $search_keyword): bool */ protected function performScoutSearch(string $scoutIndex, string $scoutKey, string $searchKeyword, mixed $searchFilters = []): array { - if (!class_exists('\Laravel\Scout\EngineManager')) - { + 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) - { + if ($engine instanceof \Laravel\Scout\Engines\MeilisearchEngine) { // Meilisearch Engine $search_results = $engine ->index($scoutIndex) ->rawSearch($searchKeyword, [ 'limit' => $this->scoutMaxHits, - 'attributesToRetrieve' => [ $scoutKey ], + 'attributesToRetrieve' => [$scoutKey], 'filter' => $searchFilters, ]); return collect($search_results['hits'] ?? []) ->pluck($scoutKey) ->all(); - } - else - { + } else { throw new \Exception('Unsupported Scout Engine. Currently supported: Meilisearch'); } } From 831abbf36d8dbd765c4391b1ceca7be249295235 Mon Sep 17 00:00:00 2001 From: Furkan Akkoc Date: Thu, 12 Oct 2023 04:36:10 +0200 Subject: [PATCH 04/12] Fixed more StyleCI errors --- src/QueryDataTable.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/QueryDataTable.php b/src/QueryDataTable.php index 9d34f1d0..8a1611b4 100644 --- a/src/QueryDataTable.php +++ b/src/QueryDataTable.php @@ -996,6 +996,7 @@ protected function applyScoutSearch(string $search_keyword): bool $this->disableUserOrdering = true; $this->scoutSearched = true; + return true; } catch (\Exception) { // Scout search failed, fallback to default search @@ -1014,7 +1015,7 @@ protected function applyScoutSearch(string $search_keyword): bool */ protected function performScoutSearch(string $scoutIndex, string $scoutKey, string $searchKeyword, mixed $searchFilters = []): array { - if (!class_exists('\Laravel\Scout\EngineManager')) { + if (! class_exists('\Laravel\Scout\EngineManager')) { throw new \Exception('Laravel Scout is not installed.'); } $engine = app(\Laravel\Scout\EngineManager::class)->engine(); From 9b2d241722f28d2e74b3de8db35e7638e1a82e1c Mon Sep 17 00:00:00 2001 From: Furkan Akkoc Date: Thu, 12 Oct 2023 16:31:38 +0200 Subject: [PATCH 05/12] Added Algolia support --- src/QueryDataTable.php | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/src/QueryDataTable.php b/src/QueryDataTable.php index 8a1611b4..c4368c07 100644 --- a/src/QueryDataTable.php +++ b/src/QueryDataTable.php @@ -1030,11 +1030,26 @@ protected function performScoutSearch(string $scoutIndex, string $scoutKey, stri 'filter' => $searchFilters, ]); + return collect($search_results['hits'] ?? []) + ->pluck($scoutKey) + ->all(); + } elseif ($engine instanceof \Laravel\Scout\Engines\AlgoliaEngine) { + // Algolia Engine + $algolia = $engine->initIndex($scoutIndex); + + $search_results = $algolia->search($searchKeyword, [ + 'offset' => 0, + 'length' => $this->scoutMaxHits, + 'attributesToRetrieve' => [ $scoutKey ], + 'attributesToHighlight' => [], + 'filters' => $searchFilters, + ]); + return collect($search_results['hits'] ?? []) ->pluck($scoutKey) ->all(); } else { - throw new \Exception('Unsupported Scout Engine. Currently supported: Meilisearch'); + throw new \Exception('Unsupported Scout Engine. Currently supported: Meilisearch, Algolia'); } } } From 08deb99c88a881cce0eb193c80d25d7ec4218f98 Mon Sep 17 00:00:00 2001 From: Furkan Akkoc Date: Thu, 12 Oct 2023 16:32:12 +0200 Subject: [PATCH 06/12] Fixed StyleCI error --- src/QueryDataTable.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/QueryDataTable.php b/src/QueryDataTable.php index c4368c07..86289110 100644 --- a/src/QueryDataTable.php +++ b/src/QueryDataTable.php @@ -1040,7 +1040,7 @@ protected function performScoutSearch(string $scoutIndex, string $scoutKey, stri $search_results = $algolia->search($searchKeyword, [ 'offset' => 0, 'length' => $this->scoutMaxHits, - 'attributesToRetrieve' => [ $scoutKey ], + 'attributesToRetrieve' => [$scoutKey], 'attributesToHighlight' => [], 'filters' => $searchFilters, ]); From add87c03a6b39a6996965c04fb760b298ea42cba Mon Sep 17 00:00:00 2001 From: Furkan Akkoc Date: Thu, 12 Oct 2023 19:58:47 +0200 Subject: [PATCH 07/12] Fixed phpstan errors (hopefully) --- composer.json | 8 ++++- src/QueryDataTable.php | 78 ++++++++++++++++++++++++++---------------- 2 files changed, 55 insertions(+), 31 deletions(-) diff --git a/composer.json b/composer.json index ecf473b7..c2aa0f81 100644 --- a/composer.json +++ b/composer.json @@ -22,6 +22,9 @@ "illuminate/view": "^9|^10" }, "require-dev": { + "algolia/algoliasearch-client-php": "^3.4", + "laravel/scout": "^10.5", + "meilisearch/meilisearch-php": "^1.4", "nunomaduro/larastan": "^2.4", "orchestra/testbench": "^8", "yajra/laravel-datatables-html": "^9.3.4|^10" @@ -60,7 +63,10 @@ } }, "config": { - "sort-packages": true + "sort-packages": true, + "allow-plugins": { + "php-http/discovery": true + } }, "scripts": { "test": "vendor/bin/phpunit" diff --git a/src/QueryDataTable.php b/src/QueryDataTable.php index 86289110..a470d43b 100644 --- a/src/QueryDataTable.php +++ b/src/QueryDataTable.php @@ -60,9 +60,9 @@ class QueryDataTable extends DataTableAbstract /** * Enable scout search and use this model for searching. * - * @var string|null + * @var Model|null */ - protected ?string $scoutModel = null; + protected ?Model $scoutModel = null; /** * Maximum number of hits to return from scout. @@ -85,6 +85,20 @@ class QueryDataTable extends DataTableAbstract */ 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. @@ -932,17 +946,19 @@ public function ordering(): void */ public function enableScoutSearch(string $model, int $max_hits = 1000): static { - if ( - class_exists($model) - && - is_subclass_of($model, Model::class) - && - in_array("Laravel\Scout\Searchable", class_uses_recursive($model)) - ) { - $this->scoutModel = $model; - $this->scoutMaxHits = $max_hits; + $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; } @@ -967,18 +983,16 @@ protected function applyScoutSearch(string $search_keyword): bool try { // Perform scout search - $scout_index = (new $this->scoutModel)->searchableAs(); - $scout_key = (new $this->scoutModel)->getScoutKeyName(); $search_filters = ''; if (is_callable($this->scoutFilterCallback)) { $search_filters = ($this->scoutFilterCallback)($search_keyword); } - $search_results = $this->performScoutSearch($scout_index, $scout_key, $search_keyword, $search_filters); + $search_results = $this->performScoutSearch($search_keyword, $search_filters); // Apply scout search results to query - $this->query->where(function ($query) use ($scout_key, $search_results) { - $this->query->whereIn($scout_key, $search_results); + $this->query->where(function ($query) use ($search_results) { + $this->query->whereIn($this->scoutKey, $search_results); }); // Order by scout search results & disable user ordering @@ -989,7 +1003,7 @@ protected function applyScoutSearch(string $search_keyword): bool }) ->implode(','); - $this->query->orderByRaw("FIELD($scout_key, $escaped_ids)"); + $this->query->orderByRaw("FIELD($this->scoutKey, $escaped_ids)"); } // Disable user ordering because we already order by search relevancy @@ -1007,13 +1021,11 @@ protected function applyScoutSearch(string $search_keyword): bool /** * Perform a scout search with the configured engine and given parameters. Return matching model IDs. * - * @param string $scoutIndex - * @param string $scoutKey * @param string $searchKeyword * @param mixed $searchFilters * @return array */ - protected function performScoutSearch(string $scoutIndex, string $scoutKey, string $searchKeyword, mixed $searchFilters = []): array + protected function performScoutSearch(string $searchKeyword, mixed $searchFilters = []): array { if (! class_exists('\Laravel\Scout\EngineManager')) { throw new \Exception('Laravel Scout is not installed.'); @@ -1021,32 +1033,38 @@ protected function performScoutSearch(string $scoutIndex, string $scoutKey, stri $engine = app(\Laravel\Scout\EngineManager::class)->engine(); if ($engine instanceof \Laravel\Scout\Engines\MeilisearchEngine) { - // Meilisearch Engine + /** @var \Meilisearch\Client $engine */ $search_results = $engine - ->index($scoutIndex) + ->index($this->scoutIndex) ->rawSearch($searchKeyword, [ 'limit' => $this->scoutMaxHits, - 'attributesToRetrieve' => [$scoutKey], + 'attributesToRetrieve' => [$this->scoutKey], 'filter' => $searchFilters, ]); - return collect($search_results['hits'] ?? []) - ->pluck($scoutKey) + /** @var array> $hits */ + $hits = $search_results['hits'] ?? []; + + return collect($hits) + ->pluck($this->scoutKey) ->all(); } elseif ($engine instanceof \Laravel\Scout\Engines\AlgoliaEngine) { - // Algolia Engine - $algolia = $engine->initIndex($scoutIndex); + /** @var \Algolia\AlgoliaSearch\SearchClient $engine */ + $algolia = $engine->initIndex($this->scoutIndex); $search_results = $algolia->search($searchKeyword, [ 'offset' => 0, 'length' => $this->scoutMaxHits, - 'attributesToRetrieve' => [$scoutKey], + 'attributesToRetrieve' => [$this->scoutKey], 'attributesToHighlight' => [], 'filters' => $searchFilters, ]); - return collect($search_results['hits'] ?? []) - ->pluck($scoutKey) + /** @var array> $hits */ + $hits = $search_results['hits'] ?? []; + + return collect($hits) + ->pluck($this->scoutKey) ->all(); } else { throw new \Exception('Unsupported Scout Engine. Currently supported: Meilisearch, Algolia'); From ff66bcb885ecc3a1780e9eebfa7f2994526d3f8a Mon Sep 17 00:00:00 2001 From: Furkan Akkoc Date: Thu, 12 Oct 2023 20:00:37 +0200 Subject: [PATCH 08/12] Fixed styleci errors --- src/QueryDataTable.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/QueryDataTable.php b/src/QueryDataTable.php index a470d43b..18a37e34 100644 --- a/src/QueryDataTable.php +++ b/src/QueryDataTable.php @@ -86,14 +86,14 @@ class QueryDataTable extends DataTableAbstract protected bool $scoutSearched = false; /** - * Scout index name + * Scout index name. * * @var string */ protected string $scoutIndex; /** - * Scout key name + * Scout key name. * * @var string */ @@ -947,10 +947,10 @@ public function ordering(): void public function enableScoutSearch(string $model, int $max_hits = 1000): static { $scout_model = new $model; - if (!class_exists($model) || !($scout_model instanceof 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')) { + if (! method_exists($scout_model, 'searchableAs') || ! method_exists($scout_model, 'getScoutKeyName')) { throw new \Exception("$model must use the Searchable trait."); } From dcc351d6ec7506bb3bd80b69149f019d9fc637cb Mon Sep 17 00:00:00 2001 From: Furkan Akkoc Date: Sat, 28 Oct 2023 00:31:05 +0200 Subject: [PATCH 09/12] Swapped out mysql fixed ordering with more generic fixed ordering for scout search (currently only MySQL supported) --- src/QueryDataTable.php | 76 +++++++++++++++++++++++++++++++++++++----- 1 file changed, 67 insertions(+), 9 deletions(-) diff --git a/src/QueryDataTable.php b/src/QueryDataTable.php index 18a37e34..fd515013 100644 --- a/src/QueryDataTable.php +++ b/src/QueryDataTable.php @@ -975,6 +975,12 @@ public function scoutFilter(callable $callback): static 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) { @@ -997,18 +1003,12 @@ protected function applyScoutSearch(string $search_keyword): bool // Order by scout search results & disable user ordering if (count($search_results) > 0) { - $escaped_ids = collect($search_results) - ->map(function ($id) { - return \DB::connection()->getPdo()->quote($id); - }) - ->implode(','); + $this->applyFixedOrderingToQuery($this->scoutKey, $search_results); - $this->query->orderByRaw("FIELD($this->scoutKey, $escaped_ids)"); + // Disable user ordering because we already order by search relevancy + $this->disableUserOrdering = true; } - // Disable user ordering because we already order by search relevancy - $this->disableUserOrdering = true; - $this->scoutSearched = true; return true; @@ -1018,6 +1018,64 @@ protected function applyScoutSearch(string $search_keyword): bool } } + /** + * 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 void + * @throws \Exception If the database driver is unsupported. + */ + protected function applyFixedOrderingToQuery(string $keyName, array $orderedKeys) + { + $connection = $this->query->getConnection(); + $driver_name = $connection->getDriverName(); + + // Escape keyName and orderedKeys + $keyName = $connection->escape($keyName); + $orderedKeys = collect($orderedKeys) + ->map(function ($value) use ($connection) { + return $connection->escape($value); + }); + + switch ($driver_name) + { + case 'mysql': + // MySQL + $this->query->orderByRaw("FIELD($keyName, " . $orderedKeys->implode(',') . ")"); + break; + + /* + TODO: test implementations, fix if necessary and uncomment + case 'pgsql': + // PostgreSQL + $this->query->orderByRaw("array_position(ARRAY[" . $orderedKeys->implode(',') . "], $keyName)"); + break; + + case 'sqlite': + case 'sqlsrv': + // SQLite & Microsoft SQL Server + + // should be generally compatible with all drivers using SQL syntax (but ugly solution) + $this->query->orderByRaw( + "CASE $keyName " + . + $orderedKeys + ->map(fn($value, $index) => "WHEN $keyName = $value THEN $index") + ->implode(' ') + . + " END" + ); + break; + */ + + default: + throw new \Exception("Unsupported database driver: $driver_name"); + } + } + /** * Perform a scout search with the configured engine and given parameters. Return matching model IDs. * From 98230170288fb92156b5d4bfa97b4bd9f9d6dfe8 Mon Sep 17 00:00:00 2001 From: Furkan Akkoc Date: Sat, 28 Oct 2023 00:36:57 +0200 Subject: [PATCH 10/12] Fixed StyleCI/PHPStan errors --- src/QueryDataTable.php | 54 +++++++++++++++++++++--------------------- 1 file changed, 27 insertions(+), 27 deletions(-) diff --git a/src/QueryDataTable.php b/src/QueryDataTable.php index fd515013..c6bbd299 100644 --- a/src/QueryDataTable.php +++ b/src/QueryDataTable.php @@ -1026,11 +1026,12 @@ protected function applyScoutSearch(string $search_keyword): bool * @param string $keyName * @param array $orderedKeys * @return void + * * @throws \Exception If the database driver is unsupported. */ protected function applyFixedOrderingToQuery(string $keyName, array $orderedKeys) { - $connection = $this->query->getConnection(); + $connection = $this->getConnection(); $driver_name = $connection->getDriverName(); // Escape keyName and orderedKeys @@ -1040,36 +1041,35 @@ protected function applyFixedOrderingToQuery(string $keyName, array $orderedKeys return $connection->escape($value); }); - switch ($driver_name) - { + switch ($driver_name) { case 'mysql': // MySQL - $this->query->orderByRaw("FIELD($keyName, " . $orderedKeys->implode(',') . ")"); - break; - - /* - TODO: test implementations, fix if necessary and uncomment - case 'pgsql': - // PostgreSQL - $this->query->orderByRaw("array_position(ARRAY[" . $orderedKeys->implode(',') . "], $keyName)"); + $this->query->orderByRaw("FIELD($keyName, ".$orderedKeys->implode(',').')'); break; - case 'sqlite': - case 'sqlsrv': - // SQLite & Microsoft SQL Server - - // should be generally compatible with all drivers using SQL syntax (but ugly solution) - $this->query->orderByRaw( - "CASE $keyName " - . - $orderedKeys - ->map(fn($value, $index) => "WHEN $keyName = $value THEN $index") - ->implode(' ') - . - " END" - ); - break; - */ + /* + TODO: test implementations, fix if necessary and uncomment + case 'pgsql': + // PostgreSQL + $this->query->orderByRaw("array_position(ARRAY[" . $orderedKeys->implode(',') . "], $keyName)"); + break; + + case 'sqlite': + case 'sqlsrv': + // SQLite & Microsoft SQL Server + + // should be generally compatible with all drivers using SQL syntax (but ugly solution) + $this->query->orderByRaw( + "CASE $keyName " + . + $orderedKeys + ->map(fn($value, $index) => "WHEN $keyName = $value THEN $index") + ->implode(' ') + . + " END" + ); + break; + */ default: throw new \Exception("Unsupported database driver: $driver_name"); From 7eb12aba958a8e7b5a3b0993d7a52c687cee1f21 Mon Sep 17 00:00:00 2001 From: Furkan Akkoc Date: Sat, 28 Oct 2023 00:39:02 +0200 Subject: [PATCH 11/12] Fixed styleci error --- src/QueryDataTable.php | 46 +++++++++++++++++++++--------------------- 1 file changed, 23 insertions(+), 23 deletions(-) diff --git a/src/QueryDataTable.php b/src/QueryDataTable.php index c6bbd299..b01f53c3 100644 --- a/src/QueryDataTable.php +++ b/src/QueryDataTable.php @@ -1047,29 +1047,29 @@ protected function applyFixedOrderingToQuery(string $keyName, array $orderedKeys $this->query->orderByRaw("FIELD($keyName, ".$orderedKeys->implode(',').')'); break; - /* - TODO: test implementations, fix if necessary and uncomment - case 'pgsql': - // PostgreSQL - $this->query->orderByRaw("array_position(ARRAY[" . $orderedKeys->implode(',') . "], $keyName)"); - break; - - case 'sqlite': - case 'sqlsrv': - // SQLite & Microsoft SQL Server - - // should be generally compatible with all drivers using SQL syntax (but ugly solution) - $this->query->orderByRaw( - "CASE $keyName " - . - $orderedKeys - ->map(fn($value, $index) => "WHEN $keyName = $value THEN $index") - ->implode(' ') - . - " END" - ); - break; - */ + /* + TODO: test implementations, fix if necessary and uncomment + case 'pgsql': + // PostgreSQL + $this->query->orderByRaw("array_position(ARRAY[" . $orderedKeys->implode(',') . "], $keyName)"); + break; + + case 'sqlite': + case 'sqlsrv': + // SQLite & Microsoft SQL Server + + // should be generally compatible with all drivers using SQL syntax (but ugly solution) + $this->query->orderByRaw( + "CASE $keyName " + . + $orderedKeys + ->map(fn($value, $index) => "WHEN $keyName = $value THEN $index") + ->implode(' ') + . + " END" + ); + break; + */ default: throw new \Exception("Unsupported database driver: $driver_name"); From 5b0563fc47f2bc90d5b717f4febab0b359aa5738 Mon Sep 17 00:00:00 2001 From: Furkan Akkoc Date: Fri, 3 Nov 2023 14:24:04 +0100 Subject: [PATCH 12/12] Made scout search fixed ordering optional & enabled sqlite/mssql integration --- src/QueryDataTable.php | 54 ++++++++++++++++++++---------------------- 1 file changed, 26 insertions(+), 28 deletions(-) diff --git a/src/QueryDataTable.php b/src/QueryDataTable.php index b01f53c3..b7336176 100644 --- a/src/QueryDataTable.php +++ b/src/QueryDataTable.php @@ -1001,11 +1001,9 @@ protected function applyScoutSearch(string $search_keyword): bool $this->query->whereIn($this->scoutKey, $search_results); }); - // Order by scout search results & disable user ordering - if (count($search_results) > 0) { - $this->applyFixedOrderingToQuery($this->scoutKey, $search_results); - - // Disable user ordering because we already order by search relevancy + // 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; } @@ -1025,9 +1023,7 @@ protected function applyScoutSearch(string $search_keyword): bool * * @param string $keyName * @param array $orderedKeys - * @return void - * - * @throws \Exception If the database driver is unsupported. + * @return bool */ protected function applyFixedOrderingToQuery(string $keyName, array $orderedKeys) { @@ -1035,6 +1031,7 @@ protected function applyFixedOrderingToQuery(string $keyName, array $orderedKeys $driver_name = $connection->getDriverName(); // Escape keyName and orderedKeys + $rawKeyName = $keyName; $keyName = $connection->escape($keyName); $orderedKeys = collect($orderedKeys) ->map(function ($value) use ($connection) { @@ -1043,36 +1040,37 @@ protected function applyFixedOrderingToQuery(string $keyName, array $orderedKeys switch ($driver_name) { case 'mysql': - // MySQL + // MySQL / MariaDB $this->query->orderByRaw("FIELD($keyName, ".$orderedKeys->implode(',').')'); - break; + return true; /* TODO: test implementations, fix if necessary and uncomment case 'pgsql': // PostgreSQL $this->query->orderByRaw("array_position(ARRAY[" . $orderedKeys->implode(',') . "], $keyName)"); - break; - - case 'sqlite': - case 'sqlsrv': - // SQLite & Microsoft SQL Server - - // should be generally compatible with all drivers using SQL syntax (but ugly solution) - $this->query->orderByRaw( - "CASE $keyName " - . - $orderedKeys - ->map(fn($value, $index) => "WHEN $keyName = $value THEN $index") - ->implode(' ') - . - " END" - ); - break; + return true; + */ + case 'sqlite': + case 'sqlsrv': + // SQLite & Microsoft SQL Server + // Compatible with all SQL drivers (but ugly solution) + + $this->query->orderByRaw( + "CASE `$rawKeyName` " + . + $orderedKeys + ->map(fn($value, $index) => "WHEN $value THEN $index") + ->implode(' ') + . + " END" + ); + return true; + default: - throw new \Exception("Unsupported database driver: $driver_name"); + return false; } }