Skip to content

Commit 5153783

Browse files
committed
Improve search config usage.
Make sure that a search config can be set globbaly or per Model. Fallback to the default config by using `get_current_ts_config()`. Use prepared statements rather than interpolating query strings.
1 parent b832041 commit 5153783

File tree

2 files changed

+110
-49
lines changed

2 files changed

+110
-49
lines changed

src/PostgresEngine.php

Lines changed: 38 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -109,21 +109,30 @@ protected function toVector(Model $model)
109109
return $value === null ? '' : $value;
110110
});
111111

112-
$searchConfigString = $this->getSearchConfigString();
112+
$bindings = collect([]);
113113

114-
$select = $fields->keys()
115-
->map(function ($key) use ($model, $searchConfigString) {
116-
$vector = "to_tsvector($searchConfigString ?)";
117-
if ($label = $this->rankFieldWeightLabel($model, $key)) {
118-
$vector = "setweight($vector, '$label')";
119-
}
114+
// The choices of parser, dictionaries and which types of tokens to index are determined
115+
// by the selected text search configuration which can be set globally in config/scout.php
116+
// file or individually for each model in searchableOptions()
117+
// See https://www.postgresql.org/docs/current/static/textsearch-controls.html
118+
$vector = "to_tsvector(COALESCE(?, get_current_ts_config()), ?)";
120119

121-
return $vector;
122-
})->implode(' || ');
120+
$select = $fields->map(function ($value, $key) use ($model, $vector, $bindings) {
121+
$bindings->push($this->searchConfig($model) ?: null)
122+
->push($value);
123+
124+
// Set a field weight if it was specified in Model's searchableOptions()
125+
if ($label = $this->rankFieldWeightLabel($model, $key)) {
126+
$vector = "setweight($vector, ?)";
127+
$bindings->push($label);
128+
}
129+
130+
return $vector;
131+
})->implode(' || ');
123132

124133
return $this->database
125134
->query()
126-
->selectRaw("$select AS tsvector", $fields->values()->all())
135+
->selectRaw("$select AS tsvector", $bindings->all())
127136
->value('tsvector');
128137
}
129138

@@ -209,12 +218,20 @@ protected function performSearch(Builder $builder, $perPage = 0, $page = 1)
209218

210219
$indexColumn = $this->getIndexColumn($builder->model);
211220

212-
$searchConfigString = $this->getSearchConfigString();
221+
$bindings = collect([]);
222+
223+
// The choices of parser, dictionaries and which types of tokens to index are determined
224+
// by the selected text search configuration which can be set globally in config/scout.php
225+
// file or individually for each model in searchableOptions()
226+
// See https://www.postgresql.org/docs/current/static/textsearch-controls.html
227+
$tsQuery = 'plainto_tsquery(COALESCE(?, get_current_ts_config()), ?) AS query';
228+
$bindings->push($this->searchConfig($builder->model) ?: null)
229+
->push($builder->query);
213230

214231
// Build the query
215232
$query = $this->database
216233
->table($builder->index ?: $builder->model->searchableAs())
217-
->crossJoin($this->database->raw("plainto_tsquery($searchConfigString ?) query"))
234+
->crossJoin($this->database->raw($tsQuery))
218235
->select($builder->model->getKeyName())
219236
->selectRaw("{$this->rankingExpression($builder->model, $indexColumn)} AS rank")
220237
->selectRaw('COUNT(*) OVER () AS total_count')
@@ -227,8 +244,7 @@ protected function performSearch(Builder $builder, $perPage = 0, $page = 1)
227244
->limit($perPage);
228245
}
229246

230-
$bindings = collect([$builder->query]);
231-
247+
// Transfer the where clauses that were set on the builder instance if any
232248
foreach ($builder->wheres as $key => $value) {
233249
$query->where($key, $value);
234250
$bindings->push($value);
@@ -461,15 +477,14 @@ protected function preserveModel(Model $model)
461477
$this->model = $model;
462478
}
463479

464-
protected function getSearchConfigString()
480+
/**
481+
* Returns a search config name for a model.
482+
*
483+
* @param \Illuminate\Database\Eloquent\Model $model
484+
* @return string
485+
*/
486+
protected function searchConfig(Model $model)
465487
{
466-
$searchConfig = $this->config('search_configuration');
467-
468-
$searchConfigString = '';
469-
if ($searchConfig !== null) {
470-
$searchConfigString = "'".$searchConfig."',";
471-
}
472-
473-
return $searchConfigString;
488+
return $this->option($model, 'config', $this->config('config', ''));
474489
}
475490
}

tests/PostgresEngineTest.php

Lines changed: 72 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,10 @@ public function test_update_adds_object_to_index()
2626
$db->shouldReceive('query')
2727
->andReturn($query = Mockery::mock('stdClass'));
2828
$query->shouldReceive('selectRaw')
29-
->with("to_tsvector( ?) || setweight(to_tsvector( ?), 'B') AS tsvector", ['Foo', ''])
29+
->with(
30+
"to_tsvector(COALESCE(?, get_current_ts_config()), ?) || setweight(to_tsvector(COALESCE(?, get_current_ts_config()), ?), ?) AS tsvector",
31+
[null, 'Foo', null, '', 'B']
32+
)
3033
->andReturnSelf();
3134
$query->shouldReceive('value')
3235
->with('tsvector')
@@ -88,34 +91,62 @@ public function test_search()
8891
$limit = 5;
8992
$table = $this->setDbExpectations($db, $skip, $limit);
9093

94+
$table->shouldReceive('skip')
95+
->with($skip)
96+
->andReturnSelf()
97+
->shouldReceive('limit')
98+
->with($limit)
99+
->andReturnSelf()
100+
->shouldReceive('where')
101+
->with('bar', 1);
102+
103+
$db->shouldReceive('select')
104+
->with(null, [null, 'foo', 1]);
105+
106+
$builder = new Builder(new TestModel(), 'foo');
107+
$builder->where('bar', 1)->take(5);
108+
109+
$engine->search($builder);
110+
}
111+
112+
public function test_search_with_global_config()
113+
{
114+
list($engine, $db) = $this->getEngine(['config' => 'simple']);
115+
116+
$skip = 0;
117+
$limit = 5;
118+
$table = $this->setDbExpectations($db);
119+
91120
$table->shouldReceive('skip')->with($skip)->andReturnSelf()
92121
->shouldReceive('limit')->with($limit)->andReturnSelf()
93122
->shouldReceive('where')->with('bar', 1);
94123

95-
$db->shouldReceive('select')->with(null, ['foo', 1]);
124+
$db->shouldReceive('select')->with(null, ['simple', 'foo', 1]);
96125

97126
$builder = new Builder(new TestModel(), 'foo');
98127
$builder->where('bar', 1)->take(5);
99128

100129
$engine->search($builder);
101130
}
102131

103-
public function test_search_configuration()
132+
public function test_search_with_model_config()
104133
{
105-
$searchConfig = 'simple';
106-
list($engine, $db) = $this->getEngine(['search_configuration' => $searchConfig]);
134+
list($engine, $db) = $this->getEngine(['config' => 'simple']);
107135

108136
$skip = 0;
109137
$limit = 5;
110-
$table = $this->setDbExpectations($db, $skip, $limit, "'".$searchConfig."',");
138+
$table = $this->setDbExpectations($db);
111139

112140
$table->shouldReceive('skip')->with($skip)->andReturnSelf()
113141
->shouldReceive('limit')->with($limit)->andReturnSelf()
114142
->shouldReceive('where')->with('bar', 1);
115143

116-
$db->shouldReceive('select')->with(null, ['foo', 1]);
144+
$db->shouldReceive('select')->with(null, ['english', 'foo', 1]);
117145

118-
$builder = new Builder(new TestModel(), 'foo');
146+
$model = new TestModel();
147+
$model->searchableOptions['config'] = 'english';
148+
149+
$builder = new Builder($model, 'foo');
119150
$builder->where('bar', 1)->take(5);
120151

121152
$engine->search($builder);
@@ -175,21 +206,34 @@ protected function getEngine($config = [])
175206
return [new PostgresEngine($resolver, $config), $db];
176207
}
177208

178-
protected function setDbExpectations($db, $skip = 0, $limit = 5, $searchConfiguration = '')
209+
protected function setDbExpectations($db)
179210
{
180211
$db->shouldReceive('table')
181212
->andReturn($table = Mockery::mock('stdClass'));
182213
$db->shouldReceive('raw')
183-
->with("plainto_tsquery($searchConfiguration ?) query")
184-
->andReturn("plainto_tsquery($searchConfiguration ?) query");
185-
186-
$table->shouldReceive('crossJoin')->with("plainto_tsquery($searchConfiguration ?) query")->andReturnSelf()
187-
->shouldReceive('select')->with('id')->andReturnSelf()
188-
->shouldReceive('selectRaw')->with('ts_rank(searchable,query) AS rank')->andReturnSelf()
189-
->shouldReceive('selectRaw')->with('COUNT(*) OVER () AS total_count')->andReturnSelf()
190-
->shouldReceive('whereRaw')->andReturnSelf()
191-
->shouldReceive('orderBy')->with('rank', 'desc')->andReturnSelf()
192-
->shouldReceive('orderBy')->with('id')->andReturnSelf()
214+
->with("plainto_tsquery(COALESCE(?, get_current_ts_config()), ?) AS query")
215+
->andReturn("plainto_tsquery(COALESCE(?, get_current_ts_config()), ?) AS query");
216+
217+
$table->shouldReceive('crossJoin')
218+
->with("plainto_tsquery(COALESCE(?, get_current_ts_config()), ?) AS query")
219+
->andReturnSelf()
220+
->shouldReceive('select')
221+
->with('id')
222+
->andReturnSelf()
223+
->shouldReceive('selectRaw')
224+
->with('ts_rank(searchable,query) AS rank')
225+
->andReturnSelf()
226+
->shouldReceive('selectRaw')
227+
->with('COUNT(*) OVER () AS total_count')
228+
->andReturnSelf()
229+
->shouldReceive('whereRaw')
230+
->andReturnSelf()
231+
->shouldReceive('orderBy')
232+
->with('rank', 'desc')
233+
->andReturnSelf()
234+
->shouldReceive('orderBy')
235+
->with('id')
236+
->andReturnSelf()
193237
->shouldReceive('toSql');
194238

195239
return $table;
@@ -200,17 +244,22 @@ class TestModel extends Model
200244
{
201245
public $id = 1;
202246

203-
public $text = 'Foo';
247+
// public $text = 'Foo';
204248

205-
protected $searchableOptions = [
249+
public $searchableOptions = [
206250
'rank' => [
207251
'fields' => [
208252
'nullable' => 'B',
209253
],
210254
],
211255
];
212256

213-
protected $searchableAdditionalArray = [];
257+
public $searchableArray = [
258+
'text' => 'Foo',
259+
'nullable' => null,
260+
];
261+
262+
public $searchableAdditionalArray = [];
214263

215264
public function searchableAs()
216265
{
@@ -234,10 +283,7 @@ public function getTable()
234283

235284
public function toSearchableArray()
236285
{
237-
return [
238-
'text' => $this->text,
239-
'nullable' => null,
240-
];
286+
return $this->searchableArray;
241287
}
242288

243289
public function searchableOptions()

0 commit comments

Comments
 (0)