Completed
Push — master ( b73a09...a9c27a )
by Nenad
01:26
created

TNTSearchEngine::getTNT()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 4
rs 10
c 0
b 0
f 0
cc 1
nc 1
nop 0
1
<?php
2
3
namespace TeamTNT\Scout\Engines;
4
5
use Illuminate\Database\Eloquent\Collection;
6
use Illuminate\Database\Eloquent\SoftDeletes;
7
use Illuminate\Support\Facades\DB;
8
use Laravel\Scout\Builder;
9
use Laravel\Scout\Engines\Engine;
10
use TeamTNT\Scout\Events\SearchPerformed;
11
use TeamTNT\TNTSearch\Exceptions\IndexNotFoundException;
12
use TeamTNT\TNTSearch\TNTSearch;
13
14
class TNTSearchEngine extends Engine
15
{
16
17
    private $filters;
18
    /**
19
     * @var TNTSearch
20
     */
21
    protected $tnt;
22
23
    /**
24
     * @var Builder
25
     */
26
    protected $builder;
27
28
    /**
29
     * Create a new engine instance.
30
     *
31
     * @param TNTSearch $tnt
32
     */
33
    public function __construct(TNTSearch $tnt)
34
    {
35
        $this->tnt = $tnt;
36
    }
37
38
    public function getTNT()
39
    {
40
        return $this->tnt;
41
    }
42
43
    /**
44
     * Update the given model in the index.
45
     *
46
     * @param Collection $models
47
     *
48
     * @return void
49
     */
50
    public function update($models)
51
    {
52
        $this->initIndex($models->first());
53
        $this->tnt->selectIndex("{$models->first()->searchableAs()}.index");
54
        $index = $this->tnt->getIndex();
55
        $index->setPrimaryKey($models->first()->getKeyName());
56
57
        $index->indexBeginTransaction();
58
        $models->each(function ($model) use ($index) {
59
            $array = $model->toSearchableArray();
60
61
            if (empty($array)) {
62
                return;
63
            }
64
65
            if ($model->getKey()) {
66
                $index->update($model->getKey(), $array);
67
            } else {
68
                $index->insert($array);
69
            }
70
        });
71
        $index->indexEndTransaction();
72
    }
73
74
    /**
75
     * Remove the given model from the index.
76
     *
77
     * @param Collection $models
78
     *
79
     * @return void
80
     */
81
    public function delete($models)
82
    {
83
        $this->initIndex($models->first());
84
        $models->each(function ($model) {
85
            $this->tnt->selectIndex("{$model->searchableAs()}.index");
86
            $index = $this->tnt->getIndex();
87
            $index->setPrimaryKey($model->getKeyName());
88
            $index->delete($model->getKey());
89
        });
90
    }
91
92
    /**
93
     * Perform the given search on the engine.
94
     *
95
     * @param Builder $builder
96
     *
97
     * @return mixed
98
     */
99
    public function search(Builder $builder)
100
    {
101
        try {
102
            return $this->performSearch($builder);
103
        } catch (IndexNotFoundException $e) {
104
            $this->initIndex($builder->model);
105
        }
106
    }
107
108
    /**
109
     * Perform the given search on the engine.
110
     *
111
     * @param Builder $builder
112
     * @param int     $perPage
113
     * @param int     $page
114
     *
115
     * @return mixed
116
     */
117
    public function paginate(Builder $builder, $perPage, $page)
118
    {
119
        $results = $this->performSearch($builder);
120
121
        if ($builder->limit) {
122
            $results['hits'] = $builder->limit;
123
        }
124
125
        $filtered = $this->discardIdsFromResultSetByConstraints($builder, $results['ids']);
126
127
        $results['hits'] = $filtered->count();
128
129
        $chunks = array_chunk($filtered->toArray(), $perPage);
130
131
        if (empty($chunks)) {
132
            return $results;
133
        }
134
135
        if (array_key_exists($page - 1, $chunks)) {
136
            $results['ids'] = $chunks[$page - 1];
137
        } else {
138
            $results['ids'] = [];
139
        }
140
141
        return $results;
142
    }
143
144
    /**
145
     * Perform the given search on the engine.
146
     *
147
     * @param Builder $builder
148
     *
149
     * @return mixed
150
     */
151
    protected function performSearch(Builder $builder, array $options = [])
152
    {
153
        $index = $builder->index ?: $builder->model->searchableAs();
154
        $limit = $builder->limit ?: 10000;
155
        $this->tnt->selectIndex("{$index}.index");
156
157
        $this->builder = $builder;
158
159
        if (isset($builder->model->asYouType)) {
160
            $this->tnt->asYouType = $builder->model->asYouType;
161
        }
162
163
        if ($builder->callback) {
164
            return call_user_func(
165
                $builder->callback,
166
                $this->tnt,
167
                $builder->query,
168
                $options
169
            );
170
        }
171
172
        $builder->query = $this->applyFilters('query_expansion', $builder->query, get_class($builder->model));
173
174
        if (isset($this->tnt->config['searchBoolean']) ? $this->tnt->config['searchBoolean'] : false) {
175
            $res = $this->tnt->searchBoolean($builder->query, $limit);
176
            event(new SearchPerformed($builder, $res, true));
177
            return $res;
178
179
        } else {
180
            $res = $this->tnt->search($builder->query, $limit);
181
            event(new SearchPerformed($builder, $res));
182
            return $res;
183
        }
184
    }
185
186
    /**
187
     * Map the given results to instances of the given model.
188
     *
189
     * @param mixed                               $results
190
     * @param \Illuminate\Database\Eloquent\Model $model
191
     *
192
     * @return Collection
193
     */
194
    public function map(Builder $builder, $results, $model)
195
    {
196
        if (is_null($results['ids']) || count($results['ids']) === 0) {
197
            return Collection::make();
198
        }
199
200
        $keys = collect($results['ids'])->values()->all();
201
202
        $builder = $this->getBuilder($model);
203
204
        if ($this->builder->queryCallback) {
205
            call_user_func($this->builder->queryCallback, $builder);
206
        }
207
208
        $models = $builder->whereIn(
209
            $model->getQualifiedKeyName(), $keys
210
        )->get()->keyBy($model->getKeyName());
211
212
        // sort models by user choice
213
        if (!empty($this->builder->orders)) {
214
            return $models->values();
215
        }
216
217
        // sort models by tnt search result set
218
        return $model->newCollection($results['ids'])->map(function ($hit) use ($models) {
219
            if (isset($models[$hit])) {
220
                return $models[$hit];
221
            }
222
        })->filter()->values();
223
    }
224
225
    /**
226
     * Return query builder either from given constraints, or as
227
     * new query. Add where statements to builder when given.
228
     *
229
     * @param \Illuminate\Database\Eloquent\Model $model
230
     *
231
     * @return Builder
232
     */
233
    public function getBuilder($model)
234
    {
235
        // get query as given constraint or create a new query
236
        $builder = isset($this->builder->constraints) ? $this->builder->constraints : $model->newQuery();
0 ignored issues
show
Bug introduced by
The property constraints does not seem to exist in Laravel\Scout\Builder.

An attempt at access to an undefined property has been detected. This may either be a typographical error or the property has been renamed but there are still references to its old name.

If you really want to allow access to undefined properties, you can define magic methods to allow access. See the php core documentation on Overloading.

Loading history...
237
238
        $builder = $this->handleSoftDeletes($builder, $model);
239
240
        $builder = $this->applyWheres($builder);
241
242
        $builder = $this->applyOrders($builder);
243
244
        return $builder;
245
    }
246
247
    /**
248
     * Pluck and return the primary keys of the given results.
249
     *
250
     * @param mixed $results
251
     * @return \Illuminate\Support\Collection
252
     */
253
    public function mapIds($results)
254
    {
255
        return collect($results['ids'])->values();
256
    }
257
258
    /**
259
     * Get the total count from a raw result returned by the engine.
260
     *
261
     * @param mixed $results
262
     *
263
     * @return int
264
     */
265
    public function getTotalCount($results)
266
    {
267
        return $results['hits'];
268
    }
269
270
    public function initIndex($model)
271
    {
272
        $indexName = $model->searchableAs();
273
274
        if (!file_exists($this->tnt->config['storage']."/{$indexName}.index")) {
275
            $indexer = $this->tnt->createIndex("$indexName.index");
276
            $indexer->setDatabaseHandle($model->getConnection()->getPdo());
277
            $indexer->setPrimaryKey($model->getKeyName());
278
        }
279
    }
280
281
    /**
282
     * The search index results ($results['ids']) need to be compared against our query
283
     * that contains the constraints.
284
     *
285
     * To get the correct results and counts for the pagination, we remove those ids
286
     * from the search index results that were found by the search but are not part of
287
     * the query ($sub) that is constrained.
288
     *
289
     * This is achieved with self joining the constrained query as subquery and selecting
290
     * the ids which are not matching to anything (i.e., is null).
291
     *
292
     * The constraints usually remove only a small amount of results, which is why the non
293
     * matching results are looked up and removed, instead of returning a collection with
294
     * all the valid results.
295
     */
296
    private function discardIdsFromResultSetByConstraints($builder, $searchResults)
297
    {
298
        $qualifiedKeyName    = $builder->model->getQualifiedKeyName(); // tableName.id
299
        $subQualifiedKeyName = 'sub.'.$builder->model->getKeyName(); // sub.id
300
301
        $sub = $this->getBuilder($builder->model)->whereIn(
302
            $qualifiedKeyName, $searchResults
303
        ); // sub query for left join
304
305
        $discardIds = $builder->model->newQuery()
306
            ->select($qualifiedKeyName)
307
            ->leftJoin(DB::raw('('.$sub->getQuery()->toSql().') as '.$builder->model->getConnection()->getTablePrefix().'sub'), $subQualifiedKeyName, '=', $qualifiedKeyName)
308
            ->addBinding($sub->getQuery()->getBindings(), 'join')
309
            ->whereIn($qualifiedKeyName, $searchResults)
310
            ->whereNull($subQualifiedKeyName)
311
            ->pluck($builder->model->getKeyName());
312
313
        // returns values of $results['ids'] that are not part of $discardIds
314
        return collect($searchResults)->diff($discardIds);
315
    }
316
317
    /**
318
     * Determine if the given model uses soft deletes.
319
     *
320
     * @param  \Illuminate\Database\Eloquent\Model  $model
321
     * @return bool
322
     */
323
    protected function usesSoftDelete($model)
324
    {
325
        return in_array(SoftDeletes::class, class_uses_recursive($model));
326
    }
327
328
    /**
329
     * Determine if soft delete is active and depending on state return the
330
     * appropriate builder.
331
     *
332
     * @param  Builder  $builder
333
     * @param  \Illuminate\Database\Eloquent\Model  $model
334
     * @return Builder
335
     */
336
    private function handleSoftDeletes($builder, $model)
337
    {
338
        // remove where statement for __soft_deleted when soft delete is not active
339
        // does not show soft deleted items when trait is attached to model and
340
        // config('scout.soft_delete') is false
341
        if (!$this->usesSoftDelete($model) || !config('scout.soft_delete', true)) {
342
            unset($this->builder->wheres['__soft_deleted']);
343
            return $builder;
344
        }
345
346
        /**
347
         * Use standard behaviour of Laravel Scout builder class to support soft deletes.
348
         *
349
         * When no __soft_deleted statement is given return all entries
350
         */
351
        if (!in_array('__soft_deleted', $this->builder->wheres)) {
352
            return $builder->withTrashed();
353
        }
354
355
        /**
356
         * When __soft_deleted is 1 then return only soft deleted entries
357
         */
358
        if ($this->builder->wheres['__soft_deleted']) {
359
            $builder = $builder->onlyTrashed();
360
        }
361
362
        /**
363
         * Returns all undeleted entries, default behaviour
364
         */
365
        unset($this->builder->wheres['__soft_deleted']);
366
        return $builder;
367
    }
368
369
    /**
370
     * Apply where statements as constraints to the query builder.
371
     *
372
     * @param Builder $builder
373
     * @return \Illuminate\Support\Collection
374
     */
375
    private function applyWheres($builder)
376
    {
377
        // iterate over given where clauses
378
        return collect($this->builder->wheres)->map(function ($value, $key) {
379
            // for reduce function combine key and value into array
380
            return [$key, $value];
381
        })->reduce(function ($builder, $where) {
382
            // separate key, value again
383
            list($key, $value) = $where;
384
            return $builder->where($key, $value);
385
        }, $builder);
386
    }
387
388
    /**
389
     * Apply order by statements as constraints to the query builder.
390
     *
391
     * @param Builder $builder
392
     * @return \Illuminate\Support\Collection
393
     */
394
    private function applyOrders($builder)
395
    {
396
        //iterate over given orderBy clauses - should be only one
397
        return collect($this->builder->orders)->map(function ($value, $key) {
0 ignored issues
show
Unused Code introduced by
The parameter $key is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
398
            // for reduce function combine key and value into array
399
            return [$value["column"], $value["direction"]];
400
        })->reduce(function ($builder, $orderBy) {
401
            // separate key, value again
402
            list($column, $direction) = $orderBy;
403
            return $builder->orderBy($column, $direction);
404
        }, $builder);
405
    }
406
407
    /**
408
     * Flush all of the model's records from the engine.
409
     *
410
     * @param  \Illuminate\Database\Eloquent\Model  $model
411
     * @return void
412
     */
413
    public function flush($model)
414
    {
415
        $indexName   = $model->searchableAs();
416
        $pathToIndex = $this->tnt->config['storage']."/{$indexName}.index";
417
        if (file_exists($pathToIndex)) {
418
            unlink($pathToIndex);
419
        }
420
    }
421
422
    /**
423
     * Adds a filter
424
     *
425
     * @param  string
426
     * @param  callback
427
     * @return void
428
     */
429
    public function addFilter($name, $callback)
430
    {
431
        if (!is_callable($callback, true)) {
432
            throw new InvalidArgumentException(sprintf('Filter is an invalid callback: %s.', print_r($callback, true)));
433
        }
434
        $this->filters[$name][] = $callback;
435
    }
436
437
    /**
438
     * Returns an array of filters
439
     *
440
     * @param  string
441
     * @return array
442
     */
443
    public function getFilters($name)
444
    {
445
        return isset($this->filters[$name]) ? $this->filters[$name] : [];
446
    }
447
448
    /**
449
     * Returns a string on which a filter is applied
450
     *
451
     * @param  string
452
     * @param  string
453
     * @return string
454
     */
455
    public function applyFilters($name, $result, $model)
456
    {
457
        foreach ($this->getFilters($name) as $callback) {
458
            // prevent fatal errors, do your own warning or
459
            // exception here as you need it.
460
            if (!is_callable($callback)) {
461
                continue;
462
            }
463
464
            $result = call_user_func($callback, $result, $model);
465
        }
466
        return $result;
467
    }
468
}
469