Completed
Push — master ( e4050c...2f964d )
by Nenad
11s
created

src/Engines/TNTSearchEngine.php (1 issue)

Upgrade to new PHP Analysis Engine

These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more

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

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

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