Completed
Push — master ( e296a7...01e065 )
by Nenad
13:49
created

TNTSearchEngine::flush()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 8

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 8
rs 10
c 0
b 0
f 0
cc 2
nc 2
nop 1
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\TNTSearch\Exceptions\IndexNotFoundException;
11
use TeamTNT\TNTSearch\TNTSearch;
12
13
class TNTSearchEngine extends Engine
14
{
15
    /**
16
     * @var TNTSearch
17
     */
18
    protected $tnt;
19
20
    /**
21
     * @var Builder
22
     */
23
    protected $builder;
24
25
    /**
26
     * Create a new engine instance.
27
     *
28
     * @param TNTSearch $tnt
29
     */
30
    public function __construct(TNTSearch $tnt)
31
    {
32
        $this->tnt = $tnt;
33
    }
34
35
    /**
36
     * Update the given model in the index.
37
     *
38
     * @param Collection $models
39
     *
40
     * @return void
41
     */
42
    public function update($models)
43
    {
44
        $this->initIndex($models->first());
45
        $this->tnt->selectIndex("{$models->first()->searchableAs()}.index");
46
        $index = $this->tnt->getIndex();
47
        $index->setPrimaryKey($models->first()->getKeyName());
48
49
        $index->indexBeginTransaction();
50
        $models->each(function ($model) use ($index) {
51
            $array = $model->toSearchableArray();
52
53
            if (empty($array)) {
54
                return;
55
            }
56
57
            if ($model->getKey()) {
58
                $index->update($model->getKey(), $array);
59
            } else {
60
                $index->insert($array);
61
            }
62
        });
63
        $index->indexEndTransaction();
64
    }
65
66
    /**
67
     * Remove the given model from the index.
68
     *
69
     * @param Collection $models
70
     *
71
     * @return void
72
     */
73
    public function delete($models)
74
    {
75
        $this->initIndex($models->first());
76
        $models->each(function ($model) {
77
            $this->tnt->selectIndex("{$model->searchableAs()}.index");
78
            $index = $this->tnt->getIndex();
79
            $index->setPrimaryKey($model->getKeyName());
80
            $index->delete($model->getKey());
81
        });
82
    }
83
84
    /**
85
     * Perform the given search on the engine.
86
     *
87
     * @param Builder $builder
88
     *
89
     * @return mixed
90
     */
91
    public function search(Builder $builder)
92
    {
93
        try {
94
            return $this->performSearch($builder);
95
        } catch (IndexNotFoundException $e) {
96
            $this->initIndex($builder->model);
97
        }
98
    }
99
100
    /**
101
     * Perform the given search on the engine.
102
     *
103
     * @param Builder $builder
104
     * @param int     $perPage
105
     * @param int     $page
106
     *
107
     * @return mixed
108
     */
109
    public function paginate(Builder $builder, $perPage, $page)
110
    {
111
        $results = $this->performSearch($builder);
112
113
        if ($builder->limit) {
114
            $results['hits'] = $builder->limit;
115
        }
116
117
        $filtered = $this->discardIdsFromResultSetByConstraints($builder, $results['ids']);
118
119
        $results['hits'] = $filtered->count();
120
121
        $chunks = array_chunk($filtered->toArray(), $perPage);
122
123
        if (empty($chunks)) {
124
            return $results;
125
        }
126
127
        if (array_key_exists($page - 1, $chunks)) {
128
            $results['ids'] = $chunks[$page - 1];
129
        } else {
130
            $results['ids'] = end($chunks);
131
        }
132
133
        return $results;
134
    }
135
136
    /**
137
     * Perform the given search on the engine.
138
     *
139
     * @param Builder $builder
140
     *
141
     * @return mixed
142
     */
143
    protected function performSearch(Builder $builder, array $options = [])
144
    {
145
        $index = $builder->index ?: $builder->model->searchableAs();
146
        $limit = $builder->limit ?: 10000;
147
        $this->tnt->selectIndex("{$index}.index");
148
149
        $this->builder = $builder;
150
151
        if (isset($builder->model->asYouType)) {
152
            $this->tnt->asYouType = $builder->model->asYouType;
153
        }
154
155
        if ($builder->callback) {
156
            return call_user_func(
157
                $builder->callback,
158
                $this->tnt,
159
                $builder->query,
160
                $options
161
            );
162
        }
163
        if (isset($this->tnt->config['searchBoolean']) ? $this->tnt->config['searchBoolean'] : false) {
164
            return $this->tnt->searchBoolean($builder->query, $limit);
165
        } else {
166
            return $this->tnt->search($builder->query, $limit);
167
        }
168
    }
169
170
    /**
171
     * Map the given results to instances of the given model.
172
     *
173
     * @param mixed                               $results
174
     * @param \Illuminate\Database\Eloquent\Model $model
175
     *
176
     * @return Collection
177
     */
178
    public function map(Builder $builder, $results, $model)
179
    {
180
        if (count($results['ids']) === 0) {
181
            return Collection::make();
182
        }
183
184
        $keys = collect($results['ids'])->values()->all();
185
186
        $builder = $this->getBuilder($model);
187
188
        if ($this->builder->queryCallback) {
189
            call_user_func($this->builder->queryCallback, $builder);
190
        }
191
192
        $models = $builder->whereIn(
193
            $model->getQualifiedKeyName(), $keys
194
        )->get()->keyBy($model->getKeyName());
195
196
        // sort models by user choice
197
        if (!empty($this->builder->orders)) {
198
            return $models->values();
199
        }
200
201
        // sort models by tnt search result set
202
        return collect($results['ids'])->map(function ($hit) use ($models) {
203
            if (isset($models[$hit])) {
204
                return $models[$hit];
205
            }
206
        })->filter()->values();
207
    }
208
209
    /**
210
     * Return query builder either from given constraints, or as
211
     * new query. Add where statements to builder when given.
212
     *
213
     * @param \Illuminate\Database\Eloquent\Model $model
214
     *
215
     * @return Builder
216
     */
217
    public function getBuilder($model)
218
    {
219
        // get query as given constraint or create a new query
220
        $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...
221
222
        $builder = $this->handleSoftDeletes($builder, $model);
223
224
        $builder = $this->applyWheres($builder);
225
226
        $builder = $this->applyOrders($builder);
227
228
        return $builder;
229
    }
230
231
    /**
232
     * Pluck and return the primary keys of the given results.
233
     *
234
     * @param mixed $results
235
     * @return \Illuminate\Support\Collection
236
     */
237
    public function mapIds($results)
238
    {
239
        return collect($results['ids'])->values();
240
    }
241
242
    /**
243
     * Get the total count from a raw result returned by the engine.
244
     *
245
     * @param mixed $results
246
     *
247
     * @return int
248
     */
249
    public function getTotalCount($results)
250
    {
251
        return $results['hits'];
252
    }
253
254
    public function initIndex($model)
255
    {
256
        $indexName = $model->searchableAs();
257
258
        if (!file_exists($this->tnt->config['storage']."/{$indexName}.index")) {
259
            $indexer = $this->tnt->createIndex("$indexName.index");
260
            $indexer->setDatabaseHandle($model->getConnection()->getPdo());
261
            $indexer->setPrimaryKey($model->getKeyName());
262
        }
263
    }
264
265
    /**
266
     * The search index results ($results['ids']) need to be compared against our query
267
     * that contains the constraints.
268
     *
269
     * To get the correct results and counts for the pagination, we remove those ids
270
     * from the search index results that were found by the search but are not part of
271
     * the query ($sub) that is constrained.
272
     *
273
     * This is achieved with self joining the constrained query as subquery and selecting
274
     * the ids which are not matching to anything (i.e., is null).
275
     *
276
     * The constraints usually remove only a small amount of results, which is why the non
277
     * matching results are looked up and removed, instead of returning a collection with
278
     * all the valid results.
279
     */
280
    private function discardIdsFromResultSetByConstraints($builder, $searchResults)
281
    {
282
        $qualifiedKeyName    = $builder->model->getQualifiedKeyName(); // tableName.id
283
        $subQualifiedKeyName = 'sub.'.$builder->model->getKeyName(); // sub.id
284
285
        $sub = $this->getBuilder($builder->model)->whereIn(
286
            $qualifiedKeyName, $searchResults
287
        ); // sub query for left join
288
289
        $discardIds = $builder->model->newQuery()
290
            ->select($qualifiedKeyName)
291
            ->leftJoin(DB::raw('('.$sub->getQuery()->toSql().') as sub'), $subQualifiedKeyName, '=', $qualifiedKeyName)
292
            ->addBinding($sub->getQuery()->getBindings(), 'join')
293
            ->whereIn($qualifiedKeyName, $searchResults)
294
            ->whereNull($subQualifiedKeyName)
295
            ->pluck($builder->model->getKeyName());
296
297
        // returns values of $results['ids'] that are not part of $discardIds
298
        return collect($searchResults)->diff($discardIds);
299
    }
300
301
    /**
302
     * Determine if the given model uses soft deletes.
303
     *
304
     * @param  \Illuminate\Database\Eloquent\Model  $model
305
     * @return bool
306
     */
307
    protected function usesSoftDelete($model)
308
    {
309
        return in_array(SoftDeletes::class, class_uses_recursive($model));
310
    }
311
312
    /**
313
     * Determine if soft delete is active and depending on state return the
314
     * appropriate builder.
315
     *
316
     * @param  Builder  $builder
317
     * @param  \Illuminate\Database\Eloquent\Model  $model
318
     * @return Builder
319
     */
320
    private function handleSoftDeletes($builder, $model)
321
    {
322
        // remove where statement for __soft_deleted when soft delete is not active
323
        // does not show soft deleted items when trait is attached to model and
324
        // config('scout.soft_delete') is false
325
        if (!$this->usesSoftDelete($model) || !config('scout.soft_delete', true)) {
326
            unset($this->builder->wheres['__soft_deleted']);
327
            return $builder;
328
        }
329
330
        /**
331
         * Use standard behaviour of Laravel Scout builder class to support soft deletes.
332
         *
333
         * When no __soft_deleted statement is given return all entries
334
         */
335
        if (!in_array('__soft_deleted', $this->builder->wheres)) {
336
            return $builder->withTrashed();
337
        }
338
339
        /**
340
         * When __soft_deleted is 1 then return only soft deleted entries
341
         */
342
        if ($this->builder->wheres['__soft_deleted']) {
343
            $builder = $builder->onlyTrashed();
344
        }
345
346
        /**
347
         * Returns all undeleted entries, default behaviour
348
         */
349
        unset($this->builder->wheres['__soft_deleted']);
350
        return $builder;
351
    }
352
353
    /**
354
     * Apply where statements as constraints to the query builder.
355
     *
356
     * @param Builder $builder
357
     * @return \Illuminate\Support\Collection
358
     */
359
    private function applyWheres($builder)
360
    {
361
        // iterate over given where clauses
362
        return collect($this->builder->wheres)->map(function ($value, $key) {
363
            // for reduce function combine key and value into array
364
            return [$key, $value];
365
        })->reduce(function ($builder, $where) {
366
            // separate key, value again
367
            list($key, $value) = $where;
368
            return $builder->where($key, $value);
369
        }, $builder);
370
    }
371
372
    /**
373
     * Apply order by statements as constraints to the query builder.
374
     *
375
     * @param Builder $builder
376
     * @return \Illuminate\Support\Collection
377
     */
378
    private function applyOrders($builder)
379
    {
380
        //iterate over given orderBy clauses - should be only one
381
        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...
382
            // for reduce function combine key and value into array
383
            return [$value["column"], $value["direction"]];
384
        })->reduce(function ($builder, $orderBy) {
385
            // separate key, value again
386
            list($column, $direction) = $orderBy;
387
            return $builder->orderBy($column, $direction);
388
        }, $builder);
389
    }
390
391
    /**
392
     * Flush all of the model's records from the engine.
393
     *
394
     * @param  \Illuminate\Database\Eloquent\Model  $model
395
     * @return void
396
     */
397
    public function flush($model)
398
    {
399
        $indexName   = $model->searchableAs();
400
        $pathToIndex = $this->tnt->config['storage']."/{$indexName}.index";
401
        if (file_exists($pathToIndex)) {
402
            unlink($pathToIndex);
403
        }
404
    }
405
}
406