Completed
Push — master ( 9df611...a3ae3d )
by Nenad
11s
created

TNTSearchEngine::map()   A

Complexity

Conditions 3
Paths 2

Size

Total Lines 20

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 20
rs 9.6
c 0
b 0
f 0
cc 3
nc 2
nop 2
1
<?php
2
3
namespace TeamTNT\Scout\Engines;
4
5
use Illuminate\Database\Eloquent\Collection;
6
use Laravel\Scout\Builder;
7
use Laravel\Scout\Engines\Engine;
8
use TeamTNT\TNTSearch\TNTSearch;
9
use TeamTNT\TNTSearch\Exceptions\IndexNotFoundException;
10
11
class TNTSearchEngine extends Engine
12
{
13
    /**
14
     * @var TNTSearch
15
     */
16
    protected $tnt;
17
18
    /**
19
     * @var Builder
20
     */
21
    protected $builder;
22
23
    /**
24
     * Create a new engine instance.
25
     *
26
     * @param TNTSearch $tnt
27
     */
28
    public function __construct(TNTSearch $tnt)
29
    {
30
        $this->tnt = $tnt;
31
    }
32
33
    /**
34
     * Update the given model in the index.
35
     *
36
     * @param Collection $models
37
     *
38
     * @return void
39
     */
40
    public function update($models)
41
    {
42
        $this->initIndex($models->first());
43
        $this->tnt->selectIndex("{$models->first()->searchableAs()}.index");
44
        $index = $this->tnt->getIndex();
45
        $index->setPrimaryKey($models->first()->getKeyName());
46
47
        $index->indexBeginTransaction();
48
        $models->each(function ($model) use ($index) {
49
            $array = $model->toSearchableArray();
50
51
            if (empty($array)) {
52
                return;
53
            }
54
55
            if ($model->getKey()) {
56
                $index->update($model->getKey(), $array);
57
            } else {
58
                $index->insert($array);
59
            }
60
        });
61
        $index->indexEndTransaction();
62
    }
63
64
    /**
65
     * Remove the given model from the index.
66
     *
67
     * @param Collection $models
68
     *
69
     * @return void
70
     */
71
    public function delete($models)
72
    {
73
        $this->initIndex($models->first());
74
        $models->each(function ($model) {
75
            $this->tnt->selectIndex("{$model->searchableAs()}.index");
76
            $index = $this->tnt->getIndex();
77
            $index->setPrimaryKey($model->getKeyName());
78
            $index->delete($model->getKey());
79
        });
80
    }
81
82
    /**
83
     * Perform the given search on the engine.
84
     *
85
     * @param Builder $builder
86
     *
87
     * @return mixed
88
     */
89
    public function search(Builder $builder)
90
    {
91
        try {
92
            return $this->performSearch($builder);
93
        } catch (IndexNotFoundException $e) {
94
            $this->initIndex($builder->model);
95
        }
96
    }
97
98
    /**
99
     * Perform the given search on the engine.
100
     *
101
     * @param Builder $builder
102
     * @param int     $perPage
103
     * @param int     $page
104
     *
105
     * @return mixed
106
     */
107
    public function paginate(Builder $builder, $perPage, $page)
108
    {
109
        $results = $this->performSearch($builder);
110
111
        if ($builder->limit) {
112
            $results['hits'] = $builder->limit;
113
        }
114
115
        $searchResults = $results['ids'];
116
117
        $qualifiedKeyName = $builder->model->getQualifiedKeyName();  // tableName.id
118
        $subQualifiedKeyName = 'sub.' . $builder->model->getKeyName(); // sub.id
119
    
120
        $sub = $this->getBuilder($builder->model)->whereIn(
121
            $qualifiedKeyName, $searchResults
122
        ); // sub query for left join
123
124
        /*
125
         * The search index results ($results['ids']) need to be compared against our query
126
         * that contains the constraints.
127
         * 
128
         * To get the correct results and counts for the pagination, we remove those ids
129
         * from the search index results that were found by the search but are not part of
130
         * the query ($sub) that is constrained.
131
         * 
132
         * This is achieved with self joining the constrained query as subquery and selecting
133
         * the ids which are not matching to anything (i.e., is null).
134
         * 
135
         * The constraints usually remove only a small amount of results, which is why the non
136
         * matching results are looked up and removed, instead of returning a collection with
137
         * all the valid results.
138
         */
139
        $discardIds = $builder->model->newQuery()
0 ignored issues
show
Bug introduced by
The method select() does not exist on Illuminate\Database\Eloquent\Builder. Did you maybe mean createSelectWithConstraint()?

This check marks calls to methods that do not seem to exist on an object.

This is most likely the result of a method being renamed without all references to it being renamed likewise.

Loading history...
140
            ->select($qualifiedKeyName)
141
            ->leftJoinSub($sub->getQuery(), 'sub', $subQualifiedKeyName, '=', $qualifiedKeyName)
142
            ->whereIn($qualifiedKeyName, $searchResults)
143
            ->whereNull($subQualifiedKeyName)
144
            ->pluck($builder->model->getKeyName());
145
146
        // returns values of $results['ids'] that are not part of $discardIds
147
        $filtered = collect($results['ids'])->diff($discardIds);
148
149
        $results['hits'] = $filtered->count();
150
151
        $chunks = array_chunk($filtered->toArray(), $perPage);
152
        
153
        if (empty($chunks)) {
154
            return $results;
155
        }
156
157
        if (array_key_exists($page - 1, $chunks)) {
158
            $results['ids'] = $chunks[$page - 1];
159
        } else {
160
            $results['ids'] = end($chunks);
161
        }
162
        
163
        return $results;
164
    }
165
166
    /**
167
     * Perform the given search on the engine.
168
     *
169
     * @param Builder $builder
170
     *
171
     * @return mixed
172
     */
173
    protected function performSearch(Builder $builder, array $options = [])
174
    {
175
        $index = $builder->index ?: $builder->model->searchableAs();
176
        $limit = $builder->limit ?: 10000;
177
        $this->tnt->selectIndex("{$index}.index");
178
179
        $this->builder = $builder;
180
        $this->tnt->asYouType = $builder->model->asYouType ?: false;
181
182
        if ($builder->callback) {
183
            return call_user_func(
184
                $builder->callback,
185
                $this->tnt,
186
                $builder->query,
187
                $options
188
            );
189
        }
190
        if (isset($this->tnt->config['searchBoolean']) ? $this->tnt->config['searchBoolean'] : false) {
191
            return $this->tnt->searchBoolean($builder->query, $limit);
192
        } else {
193
            return $this->tnt->search($builder->query, $limit);
194
        }
195
    }
196
197
    /**
198
     * Get the filter array for the query.
199
     *
200
     * @param Builder $builder
201
     *
202
     * @return array
203
     */
204
    protected function filters(Builder $builder)
205
    {
206
        return collect($builder->wheres)->map(function ($value, $key) {
207
            return $key.'='.$value;
208
        })->values()->all();
209
    }
210
211
    /**
212
     * Map the given results to instances of the given model.
213
     *
214
     * @param mixed                               $results
215
     * @param \Illuminate\Database\Eloquent\Model $model
216
     *
217
     * @return Collection
218
     */
219
    public function map($results, $model)
220
    {
221
        if (count($results['ids']) === 0) {
222
            return Collection::make();
223
        }
224
225
        $keys = collect($results['ids'])->values()->all();
226
227
        $builder = $this->getBuilder($model);
228
229
        $models = $builder->whereIn(
230
            $model->getQualifiedKeyName(), $keys
231
        )->get()->keyBy($model->getKeyName());
232
233
        return collect($results['ids'])->map(function ($hit) use ($models) {
234
            if (isset($models[$hit])) {
235
                return $models[$hit];
236
            }
237
        })->filter()->values();
238
    }
239
240
    /**
241
     * Return query builder either from given constraints, or as
242
     * new query. Add where statements to builder when given.
243
     *
244
     * @param \Illuminate\Database\Eloquent\Model $model
245
     *
246
     * @return Builder
247
     */
248
    public function getBuilder($model)
249
    {
250
        // get query as given constraint or create a new query
251
        $builder = $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...
252
253
        // iterate over given where clauses
254
        return collect($this->builder->wheres)->map(function ($value, $key) {
255
            // for reduce function combine key and value into array
256
            return [$key, $value];
257
        })->reduce(function ($builder, $where) {
258
            // separate key, value again
259
            list($key, $value) = $where;
260
            return $builder->where($key, $value);
261
        }, $builder);
262
    }
263
264
    /**
265
     * Pluck and return the primary keys of the given results.
266
     *
267
     * @param mixed $results
268
     * @return \Illuminate\Support\Collection
269
     */
270
    public function mapIds($results)
271
    {
272
        return collect($results['ids'])->values();
273
    }
274
275
    /**
276
     * Get the total count from a raw result returned by the engine.
277
     *
278
     * @param mixed $results
279
     *
280
     * @return int
281
     */
282
    public function getTotalCount($results)
283
    {
284
        return $results['hits'];
285
    }
286
287
    public function initIndex($model)
288
    {
289
        $indexName = $model->searchableAs();
290
291
        if (!file_exists($this->tnt->config['storage']."/{$indexName}.index")) {
292
            $indexer = $this->tnt->createIndex("$indexName.index");
293
            $indexer->setDatabaseHandle($model->getConnection()->getPdo());
294
            $indexer->setPrimaryKey($model->getKeyName());
295
        }
296
    }
297
}
298