Completed
Push — master ( 09b9c9...ea9622 )
by Nenad
17s queued 11s
created

TNTSearchEngine::deleteIndex()   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 1
1
<?php
2
3
namespace TeamTNT\Scout\Engines;
4
5
use Exception;
6
use Illuminate\Database\Eloquent\Collection;
7
use Illuminate\Database\Eloquent\SoftDeletes;
8
use Illuminate\Support\Facades\DB;
9
use Illuminate\Support\LazyCollection;
10
use InvalidArgumentException;
11
use Laravel\Scout\Builder;
12
use Laravel\Scout\Engines\Engine;
13
use TeamTNT\Scout\Events\SearchPerformed;
14
use TeamTNT\TNTSearch\Exceptions\IndexNotFoundException;
15
use TeamTNT\TNTSearch\TNTSearch;
16
17
class TNTSearchEngine extends Engine
18
{
19
20
    private $filters;
21
    /**
22
     * @var TNTSearch
23
     */
24
    protected $tnt;
25
26
    /**
27
     * @var Builder
28
     */
29
    protected $builder;
30
31
    /**
32
     * Create a new engine instance.
33
     *
34
     * @param TNTSearch $tnt
35
     */
36
    public function __construct(TNTSearch $tnt)
37
    {
38
        $this->tnt = $tnt;
39
    }
40
41
    public function getTNT()
42
    {
43
        return $this->tnt;
44
    }
45
46
    /**
47
     * Update the given model in the index.
48
     *
49
     * @param Collection $models
50
     *
51
     * @return void
52
     */
53
    public function update($models)
54
    {
55
        $this->initIndex($models->first());
56
        $this->tnt->selectIndex("{$models->first()->searchableAs()}.index");
57
        $index = $this->tnt->getIndex();
58
        $index->setPrimaryKey($models->first()->getKeyName());
59
60
        $index->indexBeginTransaction();
61
        $models->each(function ($model) use ($index) {
62
            $array = $model->toSearchableArray();
63
64
            if (empty($array)) {
65
                return;
66
            }
67
68
            if ($model->getKey()) {
69
                $index->update($model->getKey(), $array);
70
            } else {
71
                $index->insert($array);
72
            }
73
        });
74
        $index->indexEndTransaction();
75
    }
76
77
    /**
78
     * Remove the given model from the index.
79
     *
80
     * @param Collection $models
81
     *
82
     * @return void
83
     */
84
    public function delete($models)
85
    {
86
        $this->initIndex($models->first());
87
        $models->each(function ($model) {
88
            $this->tnt->selectIndex("{$model->searchableAs()}.index");
89
            $index = $this->tnt->getIndex();
90
            $index->setPrimaryKey($model->getKeyName());
91
            $index->delete($model->getKey());
92
        });
93
    }
94
95
    /**
96
     * Perform the given search on the engine.
97
     *
98
     * @param Builder $builder
99
     *
100
     * @return mixed
101
     */
102
    public function search(Builder $builder)
103
    {
104
        try {
105
            return $this->performSearch($builder);
106
        } catch (IndexNotFoundException $e) {
107
            $this->initIndex($builder->model);
108
        }
109
    }
110
111
    /**
112
     * Perform the given search on the engine.
113
     *
114
     * @param Builder $builder
115
     * @param int     $perPage
116
     * @param int     $page
117
     *
118
     * @return mixed
119
     */
120
    public function paginate(Builder $builder, $perPage, $page)
121
    {
122
        $results = $this->performSearch($builder);
123
124
        if ($builder->limit) {
125
            $results['hits'] = $builder->limit;
126
        }
127
128
        $filtered = $this->discardIdsFromResultSetByConstraints($builder, $results['ids']);
129
130
        $results['hits'] = $filtered->count();
131
132
        $chunks = array_chunk($filtered->toArray(), $perPage);
133
134
        if (empty($chunks)) {
135
            return $results;
136
        }
137
138
        if (array_key_exists($page - 1, $chunks)) {
139
            $results['ids'] = $chunks[$page - 1];
140
        } else {
141
            $results['ids'] = [];
142
        }
143
144
        return $results;
145
    }
146
147
    /**
148
     * Perform the given search on the engine.
149
     *
150
     * @param Builder $builder
151
     *
152
     * @return mixed
153
     */
154
    protected function performSearch(Builder $builder, array $options = [])
155
    {
156
        $index = $builder->index ?: $builder->model->searchableAs();
157
        $limit = $builder->limit ?: 10000;
158
        $this->tnt->selectIndex("{$index}.index");
159
160
        $this->builder = $builder;
161
162
        if (isset($builder->model->asYouType)) {
163
            $this->tnt->asYouType = $builder->model->asYouType;
164
        }
165
166
        if ($builder->callback) {
167
            return call_user_func(
168
                $builder->callback,
169
                $this->tnt,
170
                $builder->query,
171
                $options
172
            );
173
        }
174
175
        $builder->query = $this->applyFilters('query_expansion', $builder->query, get_class($builder->model));
176
177
        if (isset($this->tnt->config['searchBoolean']) ? $this->tnt->config['searchBoolean'] : false) {
178
            $res = $this->tnt->searchBoolean($builder->query, $limit);
179
            event(new SearchPerformed($builder, $res, true));
180
            return $res;
181
182
        } else {
183
            $res = $this->tnt->search($builder->query, $limit);
184
            event(new SearchPerformed($builder, $res));
185
            return $res;
186
        }
187
    }
188
189
    /**
190
     * Map the given results to instances of the given model.
191
     *
192
     * @param mixed                               $results
193
     * @param \Illuminate\Database\Eloquent\Model $model
194
     *
195
     * @return Collection
196
     */
197 View Code Duplication
    public function map(Builder $builder, $results, $model)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
198
    {
199
        if (empty($results['ids'])) {
200
            return Collection::make();
201
        }
202
203
        $keys = collect($results['ids'])->values()->all();
204
205
        $builder = $this->getBuilder($model);
206
207
        if ($this->builder->queryCallback) {
208
            call_user_func($this->builder->queryCallback, $builder);
209
        }
210
211
        $models = $builder->whereIn(
212
            $model->getQualifiedKeyName(), $keys
213
        )->get()->keyBy($model->getKeyName());
214
215
        // sort models by user choice
216
        if (!empty($this->builder->orders)) {
217
            return $models->values();
218
        }
219
220
        // sort models by tnt search result set
221
        return $model->newCollection($results['ids'])->map(function ($hit) use ($models) {
222
            if (isset($models[$hit])) {
223
                return $models[$hit];
224
            }
225
        })->filter()->values();
226
    }
227
228
    /**
229
     * Map the given results to instances of the given model via a lazy collection.
230
     *
231
     * @param mixed                               $results
232
     * @param \Illuminate\Database\Eloquent\Model $model
233
     *
234
     * @return LazyCollection
235
     */
236 View Code Duplication
    public function lazyMap(Builder $builder, $results, $model)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
237
    {
238
        if (empty($results['ids'])) {
239
            return LazyCollection::make();
240
        }
241
242
        $keys = collect($results['ids'])->values()->all();
243
244
        $builder = $this->getBuilder($model);
245
246
        if ($this->builder->queryCallback) {
247
            call_user_func($this->builder->queryCallback, $builder);
248
        }
249
250
        $models = $builder->whereIn(
251
            $model->getQualifiedKeyName(), $keys
252
        )->get()->keyBy($model->getKeyName());
253
254
        // sort models by user choice
255
        if (!empty($this->builder->orders)) {
256
            return $models->values();
257
        }
258
259
        // sort models by tnt search result set
260
        return $model->newCollection($results['ids'])->map(function ($hit) use ($models) {
261
            if (isset($models[$hit])) {
262
                return $models[$hit];
263
            }
264
        })->filter()->values();
265
    }
266
267
    /**
268
     * Return query builder either from given constraints, or as
269
     * new query. Add where statements to builder when given.
270
     *
271
     * @param \Illuminate\Database\Eloquent\Model $model
272
     *
273
     * @return Builder
274
     */
275
    public function getBuilder($model)
276
    {
277
        // get query as given constraint or create a new query
278
        $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...
279
280
        $builder = $this->handleSoftDeletes($builder, $model);
281
282
        $builder = $this->applyWheres($builder);
283
284
        $builder = $this->applyOrders($builder);
285
286
        return $builder;
287
    }
288
289
    /**
290
     * Pluck and return the primary keys of the given results.
291
     *
292
     * @param mixed $results
293
     * @return \Illuminate\Support\Collection
294
     */
295
    public function mapIds($results)
296
    {
297
        if (empty($results['ids'])) {
298
            return collect();
299
        }
300
301
        return collect($results['ids'])->values();
302
    }
303
304
    /**
305
     * Get the total count from a raw result returned by the engine.
306
     *
307
     * @param mixed $results
308
     *
309
     * @return int
310
     */
311
    public function getTotalCount($results)
312
    {
313
        return $results['hits'];
314
    }
315
316
    public function initIndex($model)
317
    {
318
        $indexName = $model->searchableAs();
319
320
        if (!file_exists($this->tnt->config['storage']."/{$indexName}.index")) {
321
            $indexer = $this->tnt->createIndex("$indexName.index");
322
            $indexer->setDatabaseHandle($model->getConnection()->getPdo());
323
            $indexer->setPrimaryKey($model->getKeyName());
324
        }
325
    }
326
327
    /**
328
     * The search index results ($results['ids']) need to be compared against our query
329
     * that contains the constraints.
330
     *
331
     * To get the correct results and counts for the pagination, we remove those ids
332
     * from the search index results that were found by the search but are not part of
333
     * the query ($sub) that is constrained.
334
     *
335
     * This is achieved with self joining the constrained query as subquery and selecting
336
     * the ids which are not matching to anything (i.e., is null).
337
     *
338
     * The constraints usually remove only a small amount of results, which is why the non
339
     * matching results are looked up and removed, instead of returning a collection with
340
     * all the valid results.
341
     */
342
    private function discardIdsFromResultSetByConstraints($builder, $searchResults)
343
    {
344
        $qualifiedKeyName    = $builder->model->getQualifiedKeyName(); // tableName.id
345
        $subQualifiedKeyName = 'sub.'.$builder->model->getKeyName(); // sub.id
346
347
        $sub = $this->getBuilder($builder->model)->whereIn(
348
            $qualifiedKeyName, $searchResults
349
        ); // sub query for left join
350
351
        $discardIds = $builder->model->newQuery()
352
            ->select($qualifiedKeyName)
353
            ->leftJoin(DB::raw('('.$sub->getQuery()->toSql().') as '.$builder->model->getConnection()->getTablePrefix().'sub'), $subQualifiedKeyName, '=', $qualifiedKeyName)
354
            ->addBinding($sub->getQuery()->getBindings(), 'join')
355
            ->whereIn($qualifiedKeyName, $searchResults)
356
            ->whereNull($subQualifiedKeyName)
357
            ->pluck($builder->model->getKeyName());
358
359
        // returns values of $results['ids'] that are not part of $discardIds
360
        return collect($searchResults)->diff($discardIds);
361
    }
362
363
    /**
364
     * Determine if the given model uses soft deletes.
365
     *
366
     * @param  \Illuminate\Database\Eloquent\Model  $model
367
     * @return bool
368
     */
369
    protected function usesSoftDelete($model)
370
    {
371
        return in_array(SoftDeletes::class, class_uses_recursive($model));
372
    }
373
374
    /**
375
     * Determine if soft delete is active and depending on state return the
376
     * appropriate builder.
377
     *
378
     * @param  Builder  $builder
379
     * @param  \Illuminate\Database\Eloquent\Model  $model
380
     * @return Builder
381
     */
382
    private function handleSoftDeletes($builder, $model)
383
    {
384
        // remove where statement for __soft_deleted when soft delete is not active
385
        // does not show soft deleted items when trait is attached to model and
386
        // config('scout.soft_delete') is false
387
        if (!$this->usesSoftDelete($model) || !config('scout.soft_delete', true)) {
388
            unset($this->builder->wheres['__soft_deleted']);
389
            return $builder;
390
        }
391
392
        /**
393
         * Use standard behaviour of Laravel Scout builder class to support soft deletes.
394
         *
395
         * When no __soft_deleted statement is given return all entries
396
         */
397
        if (!array_key_exists('__soft_deleted', $this->builder->wheres)) {
398
            return $builder->withTrashed();
399
        }
400
401
        /**
402
         * When __soft_deleted is 1 then return only soft deleted entries
403
         */
404
        if ($this->builder->wheres['__soft_deleted']) {
405
            $builder = $builder->onlyTrashed();
406
        }
407
408
        /**
409
         * Returns all undeleted entries, default behaviour
410
         */
411
        unset($this->builder->wheres['__soft_deleted']);
412
        return $builder;
413
    }
414
415
    /**
416
     * Apply where statements as constraints to the query builder.
417
     *
418
     * @param Builder $builder
419
     * @return \Illuminate\Support\Collection
420
     */
421
    private function applyWheres($builder)
422
    {
423
        // iterate over given where clauses
424
        return collect($this->builder->wheres)->map(function ($value, $key) {
425
            // for reduce function combine key and value into array
426
            return [$key, $value];
427
        })->reduce(function ($builder, $where) {
428
            // separate key, value again
429
            list($key, $value) = $where;
430
            return $builder->where($key, $value);
431
        }, $builder);
432
    }
433
434
    /**
435
     * Apply order by statements as constraints to the query builder.
436
     *
437
     * @param Builder $builder
438
     * @return \Illuminate\Support\Collection
439
     */
440
    private function applyOrders($builder)
441
    {
442
        //iterate over given orderBy clauses - should be only one
443
        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...
444
            // for reduce function combine key and value into array
445
            return [$value["column"], $value["direction"]];
446
        })->reduce(function ($builder, $orderBy) {
447
            // separate key, value again
448
            list($column, $direction) = $orderBy;
449
            return $builder->orderBy($column, $direction);
450
        }, $builder);
451
    }
452
453
    /**
454
     * Flush all of the model's records from the engine.
455
     *
456
     * @param  \Illuminate\Database\Eloquent\Model  $model
457
     * @return void
458
     */
459
    public function flush($model)
460
    {
461
        $indexName   = $model->searchableAs();
462
        $pathToIndex = $this->tnt->config['storage']."/{$indexName}.index";
463
        if (file_exists($pathToIndex)) {
464
            unlink($pathToIndex);
465
        }
466
    }
467
468
469
    /**
470
     * Create a search index.
471
     *
472
     * @param  string  $name
473
     * @param  array  $options
474
     * @return mixed
475
     *
476
     * @throws \Exception
477
     */
478
    public function createIndex($name, array $options = [])
479
    {
480
        throw new Exception('TNT indexes are created automatically upon adding objects.');
481
    }
482
483
    /**
484
     * Delete a search index.
485
     *
486
     * @param  string  $name
487
     * @return mixed
488
     */
489
    public function deleteIndex($name)
490
    {
491
        throw new Exception(sprintf('TNT indexes cannot reliably be removed. Please manually remove the file in %s/%s.index', config('scout.tntsearch.storage'), $name));
492
    }
493
494
    /**
495
     * Adds a filter
496
     *
497
     * @param  string
498
     * @param  callback
499
     * @return void
500
     */
501
    public function addFilter($name, $callback)
502
    {
503
        if (!is_callable($callback, true)) {
504
            throw new InvalidArgumentException(sprintf('Filter is an invalid callback: %s.', print_r($callback, true)));
505
        }
506
        $this->filters[$name][] = $callback;
507
    }
508
509
    /**
510
     * Returns an array of filters
511
     *
512
     * @param  string
513
     * @return array
514
     */
515
    public function getFilters($name)
516
    {
517
        return isset($this->filters[$name]) ? $this->filters[$name] : [];
518
    }
519
520
    /**
521
     * Returns a string on which a filter is applied
522
     *
523
     * @param  string
524
     * @param  string
525
     * @return string
526
     */
527
    public function applyFilters($name, $result, $model)
528
    {
529
        foreach ($this->getFilters($name) as $callback) {
530
            // prevent fatal errors, do your own warning or
531
            // exception here as you need it.
532
            if (!is_callable($callback)) {
533
                continue;
534
            }
535
536
            $result = call_user_func($callback, $result, $model);
537
        }
538
        return $result;
539
    }
540
}
541