Completed
Pull Request — master (#285)
by
unknown
01:29
created

TNTSearchEngine::map()   A

Complexity

Conditions 5
Paths 5

Size

Total Lines 26

Duplication

Lines 0
Ratio 0 %

Importance

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