Skip to content

Commit 17ddaae

Browse files
authored
Merge pull request #1 from suenert/extend-map-function
PR: adds constrain macro to use eloquent for retrieving records. Fixes pagination when simple where() statements are used.
2 parents 9df6115 + 2a65dc4 commit 17ddaae

File tree

3 files changed

+139
-16
lines changed

3 files changed

+139
-16
lines changed

README.md

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,61 @@ After that you can search your models with:
129129

130130
`Post::search('Bugs Bunny')->get();`
131131

132+
## Constrains
133+
134+
Additionally to `where()` statements as conditions, you're able to use Eloquent queries to constrain your search. This allows you to take relationships into account.
135+
136+
If you make use of this, the search command has to be called after all queries have been defined in your controller.
137+
138+
The `where()` statements you already know can be applied everywhere.
139+
140+
```php
141+
namespace App\Http\Controllers;
142+
143+
use App\Post;
144+
145+
class PostController extends Controller
146+
{
147+
/**
148+
* Display a listing of the resource.
149+
*
150+
* @return \Illuminate\Http\Response
151+
*/
152+
public function index(Request $request)
153+
{
154+
$post = new Post;
155+
156+
// filter out posts to which the given topic is assigned
157+
if($request->topic) {
158+
$post = $post->whereNotIn('id', function($query){
159+
$query->select('assigned_to')->from('comments')->where('topic','=', request()->input('topic'));
160+
});
161+
}
162+
163+
// only posts from people that are no moderators
164+
$post = $post->byRole('moderator','!=');
165+
166+
// when user is not admin filter out internal posts
167+
if(!auth()->user()->hasRole('admin'))
168+
{
169+
$post= $post->where('internal_post', false);
170+
}
171+
172+
if ($request->searchTerm) {
173+
$constraints = $post; // not necessary but for better readability
174+
$post = Department::search($request->searchTerm)->constrain($constraints);
175+
}
176+
177+
$post->where('deleted', false);
178+
179+
$paginator = $department->paginate(10);
180+
$posts = $paginator->getCollection();
181+
182+
// return posts
183+
}
184+
}
185+
```
186+
132187
## Sponsors
133188

134189
Become a sponsor and get your logo on our README on Github with a link to your site. [[Become a sponsor](https://opencollective.com/tntsearch#sponsor)]

src/Engines/TNTSearchEngine.php

Lines changed: 78 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -112,16 +112,54 @@ public function paginate(Builder $builder, $perPage, $page)
112112
$results['hits'] = $builder->limit;
113113
}
114114

115-
$chunks = array_chunk($results['ids'], $perPage);
115+
$searchResults = $results['ids'];
116116

117-
if (!empty($chunks)) {
118-
if (array_key_exists($page - 1, $chunks)) {
119-
$results['ids'] = $chunks[$page - 1];
120-
} else {
121-
$results['ids'] = end($chunks);
122-
}
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()
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;
123155
}
124156

157+
if (array_key_exists($page - 1, $chunks)) {
158+
$results['ids'] = $chunks[$page - 1];
159+
} else {
160+
$results['ids'] = end($chunks);
161+
}
162+
125163
return $results;
126164
}
127165

@@ -184,19 +222,43 @@ public function map($results, $model)
184222
return Collection::make();
185223
}
186224

187-
$keys = collect($results['ids'])->values()->all();
188-
$fieldsWheres = array_keys($this->builder->wheres);
189-
$models = $model->whereIn(
225+
$keys = collect($results['ids'])->values()->all();
226+
227+
$builder = $this->getBuilder($model);
228+
229+
$models = $builder->whereIn(
190230
$model->getQualifiedKeyName(), $keys
191231
)->get()->keyBy($model->getKeyName());
192232

193233
return collect($results['ids'])->map(function ($hit) use ($models) {
194-
return $models->has($hit) ? $models[$hit] : null;
195-
})->filter(function ($model) use ($fieldsWheres) {
196-
return !is_null($model) && array_reduce($fieldsWheres, function ($carry, $item) use($model) {
197-
return $carry && $model[$item] == $this->builder->wheres[$item];
198-
}, true);
199-
});
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();
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);
200262
}
201263

202264
/**

src/TNTSearchScoutServiceProvider.php

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
use TeamTNT\TNTSearch\TNTSearch;
44
use Laravel\Scout\EngineManager;
5+
use Laravel\Scout\Builder;
56
use Illuminate\Support\ServiceProvider;
67
use TeamTNT\Scout\Console\ImportCommand;
78
use TeamTNT\Scout\Engines\TNTSearchEngine;
@@ -35,6 +36,11 @@ public function boot()
3536
ImportCommand::class,
3637
]);
3738
}
39+
40+
Builder::macro('constrain', function($constraints) {
41+
$this->constraints = $constraints;
42+
return $this;
43+
});
3844
}
3945

4046
protected function setFuzziness($tnt)

0 commit comments

Comments
 (0)