Skip to content

Commit 5d72a3b

Browse files
committed
A first stab at making querying method configurable
1 parent 99199f8 commit 5d72a3b

File tree

8 files changed

+191
-24
lines changed

8 files changed

+191
-24
lines changed

src/PostgresEngine.php

Lines changed: 44 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,9 @@
77
use Illuminate\Database\Eloquent\Model;
88
use Illuminate\Database\Eloquent\Collection;
99
use Illuminate\Database\ConnectionResolverInterface;
10+
use ScoutEngines\Postgres\TsQuery\PhraseToTsQuery;
11+
use ScoutEngines\Postgres\TsQuery\PlainToTsQuery;
12+
use ScoutEngines\Postgres\TsQuery\ToTsQuery;
1013

1114
class PostgresEngine extends Engine
1215
{
@@ -155,7 +158,7 @@ public function delete($models)
155158

156159
$ids = $models->pluck($key)->all();
157160

158-
return $this->database
161+
$this->database
159162
->table($model->searchableAs())
160163
->whereIn($key, $ids)
161164
->update([$indexColumn => null]);
@@ -213,29 +216,20 @@ protected function performSearch(Builder $builder, $perPage = 0, $page = 1)
213216
{
214217
// We have to preserve the model in order to allow for
215218
// correct behavior of mapIds() method which currently
216-
// does not revceive a model instance
219+
// does not receive a model instance
217220
$this->preserveModel($builder->model);
218221

219222
$bindings = collect([]);
220223

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

231-
// Build the query
226+
// Build the SQL query
232227
$query = $this->database
233228
->table($builder->index ?: $builder->model->searchableAs())
234-
->crossJoin($this->database->raw($tsQuery))
235229
->select($builder->model->getKeyName())
236230
->selectRaw("{$this->rankingExpression($builder->model, $indexColumn)} AS rank")
237231
->selectRaw('COUNT(*) OVER () AS total_count')
238-
->whereRaw("$indexColumn @@ query");
232+
->whereRaw("$indexColumn @@ \"tsquery\"");
239233

240234
// Apply where clauses that were set on the builder instance if any
241235
foreach ($builder->wheres as $key => $value) {
@@ -267,10 +261,45 @@ protected function performSearch(Builder $builder, $perPage = 0, $page = 1)
267261
->limit($perPage);
268262
}
269263

264+
// The choices of parser, dictionaries and which types of tokens to index are determined
265+
// by the selected text search configuration which can be set globally in config/scout.php
266+
// file or individually for each model in searchableOptions()
267+
// See https://www.postgresql.org/docs/current/static/textsearch-controls.html
268+
$tsQuery = $builder->callback
269+
? call_user_func($builder->callback, $builder, $this->searchConfig($builder->model))
270+
: $this->defaultQueryMethod($builder->query, $this->searchConfig($builder->model));
271+
272+
$query->crossJoin($this->database->raw($tsQuery->sql().' AS "tsquery"'));
273+
274+
// Transfer bindings
275+
foreach ($tsQuery->bindings() as $binding) {
276+
$bindings->prepend($binding);
277+
}
278+
270279
return $this->database
271280
->select($query->toSql(), $bindings->all());
272281
}
273282

283+
/**
284+
* Returns the default query method.
285+
*
286+
* @param string $query
287+
* @param string $config
288+
* @return \ScoutEngines\Postgres\TsQuery\TsQueryable
289+
*/
290+
public function defaultQueryMethod($query, $config)
291+
{
292+
switch (strtolower($this->config('search_using', 'plain'))) {
293+
case 'tsquery':
294+
return new ToTsQuery($query, $config);
295+
case 'phrasequery':
296+
return new PhraseToTsQuery($query, $config);
297+
case 'plainquery':
298+
default:
299+
return new PlainToTsQuery($query, $config);
300+
}
301+
}
302+
274303
/**
275304
* Pluck and return the primary keys of the given results.
276305
*
@@ -345,7 +374,7 @@ protected function connect()
345374
*/
346375
protected function rankingExpression(Model $model, $indexColumn)
347376
{
348-
$args = collect([$indexColumn, 'query']);
377+
$args = collect([$indexColumn, '"tsquery"']);
349378

350379
if ($weights = $this->rankWeights($model)) {
351380
$args->prepend("'$weights'");
@@ -504,7 +533,7 @@ protected function preserveModel(Model $model)
504533
*/
505534
protected function searchConfig(Model $model)
506535
{
507-
return $this->option($model, 'config', $this->config('config', ''));
536+
return $this->option($model, 'config', $this->config('config', '')) ?: null;
508537
}
509538

510539
/**

src/PostgresEngineServiceProvider.php

Lines changed: 37 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,50 @@
22

33
namespace ScoutEngines\Postgres;
44

5+
use Laravel\Scout\Builder;
56
use Laravel\Scout\EngineManager;
67
use Illuminate\Support\ServiceProvider;
8+
use ScoutEngines\Postgres\TsQuery\PhraseToTsQuery;
9+
use ScoutEngines\Postgres\TsQuery\PlainToTsQuery;
10+
use ScoutEngines\Postgres\TsQuery\RawQuery;
11+
use ScoutEngines\Postgres\TsQuery\ToTsQuery;
712

813
class PostgresEngineServiceProvider extends ServiceProvider
914
{
1015
public function boot()
1116
{
12-
app(EngineManager::class)->extend('pgsql', function () {
13-
return new PostgresEngine($this->app['db'], config('scout.pgsql', []));
17+
$this->app->make(EngineManager::class)->extend('pgsql', function () {
18+
return new PostgresEngine($this->app['db'], $this->app['config']->get('scout.pgsql', []));
1419
});
20+
21+
if (! Builder::hasMacro('usingPhraseQuery')) {
22+
Builder::macro('usingPhraseQuery', function () {
23+
$this->callback = function ($builder, $config) {
24+
return new PhraseToTsQuery($builder->query, $config);
25+
};
26+
27+
return $this;
28+
});
29+
}
30+
31+
if (! Builder::hasMacro('usingPlainQuery')) {
32+
Builder::macro('usingPlainQuery', function () {
33+
$this->callback = function ($builder, $config) {
34+
return new PlainToTsQuery($builder->query, $config);
35+
};
36+
37+
return $this;
38+
});
39+
}
40+
41+
if (! Builder::hasMacro('usingTsQuery')) {
42+
Builder::macro('usingTsQuery', function () {
43+
$this->callback = function ($builder, $config) {
44+
return new ToTsQuery($builder->query, $config);
45+
};
46+
47+
return $this;
48+
});
49+
}
1550
}
1651
}

src/TsQuery/BaseTsQueryable.php

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
<?php
2+
3+
namespace ScoutEngines\Postgres\TsQuery;
4+
5+
abstract class BaseTsQueryable implements TsQueryable
6+
{
7+
/**
8+
* Text Search Query.
9+
*
10+
* @var string
11+
*/
12+
public $query;
13+
14+
/**
15+
* PostgreSQL Text search configuration.
16+
*
17+
* @var string|null
18+
*/
19+
public $config;
20+
21+
/**
22+
* PostgreSQL Text Search Function.
23+
*
24+
* @var string
25+
*/
26+
protected $tsFunction = '';
27+
28+
/**
29+
* Create a new instance.
30+
*
31+
* @param string $query
32+
* @param string $config
33+
*/
34+
public function __construct($query, $config = null)
35+
{
36+
$this->query = $query;
37+
$this->config = $config;
38+
}
39+
40+
/**
41+
* Render the SQL representation.
42+
*
43+
* @return string
44+
*/
45+
public function sql()
46+
{
47+
return sprintf('%s(COALESCE(?, get_current_ts_config()), ?)', $this->tsFunction);
48+
}
49+
50+
/**
51+
* Return value bindings for the SQL representation.
52+
*
53+
* @return array
54+
*/
55+
public function bindings()
56+
{
57+
return [$this->query, $this->config];
58+
}
59+
}

src/TsQuery/PhraseToTsQuery.php

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
<?php
2+
3+
namespace ScoutEngines\Postgres\TsQuery;
4+
5+
class PhraseToTsQuery extends BaseTsQueryable
6+
{
7+
protected $tsFunction = 'phraseto_tsquery';
8+
}

src/TsQuery/PlainToTsQuery.php

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
<?php
2+
3+
namespace ScoutEngines\Postgres\TsQuery;
4+
5+
class PlainToTsQuery extends BaseTsQueryable
6+
{
7+
protected $tsFunction = 'plainto_tsquery';
8+
}

src/TsQuery/ToTsQuery.php

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
<?php
2+
3+
namespace ScoutEngines\Postgres\TsQuery;
4+
5+
class ToTsQuery extends BaseTsQueryable
6+
{
7+
protected $tsFunction = 'to_tsquery';
8+
}

src/TsQuery/TsQueryable.php

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
<?php
2+
3+
namespace ScoutEngines\Postgres\TsQuery;
4+
5+
interface TsQueryable
6+
{
7+
/**
8+
* Render the SQL representation.
9+
*
10+
* @return string
11+
*/
12+
public function sql();
13+
14+
/**
15+
* Return value bindings for the SQL representation.
16+
*
17+
* @return array
18+
*/
19+
public function bindings();
20+
}

tests/PostgresEngineTest.php

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -200,7 +200,7 @@ public function test_maps_results_to_models()
200200
$model->shouldReceive('get')->once()->andReturn(Collection::make([new TestModel()]));
201201

202202
$results = $engine->map(
203-
json_decode('[{"id": 1, "rank": 0.33, "total_count": 1}]'), $model);
203+
json_decode('[{"id": 1, "tsrank": 0.33, "total_count": 1}]'), $model);
204204

205205
$this->assertCount(1, $results);
206206
}
@@ -219,7 +219,7 @@ public function test_map_filters_out_no_longer_existing_models()
219219
$model->shouldReceive('get')->once()->andReturn(Collection::make([$expectedModel]));
220220

221221
$models = $engine->map(
222-
json_decode('[{"id": 1, "rank": 0.33, "total_count": 2}, {"id": 2, "rank": 0.31, "total_count": 2}]'), $model);
222+
json_decode('[{"id": 1, "tsrank": 0.33, "total_count": 2}, {"id": 2, "tsrank": 0.31, "total_count": 2}]'), $model);
223223

224224
$this->assertCount(1, $models);
225225
$this->assertEquals(2, $models->first()->id);
@@ -230,7 +230,7 @@ public function test_it_returns_total_count()
230230
list($engine) = $this->getEngine();
231231

232232
$count = $engine->getTotalCount(
233-
json_decode('[{"id": 1, "rank": 0.33, "total_count": 100}]')
233+
json_decode('[{"id": 1, "tsrank": 0.33, "total_count": 100}]')
234234
);
235235

236236
$this->assertEquals(100, $count);
@@ -269,17 +269,17 @@ protected function setDbExpectations($db, $withDefaultOrderBy = true)
269269
$db->shouldReceive('table')
270270
->andReturn($table = Mockery::mock('stdClass'));
271271
$db->shouldReceive('raw')
272-
->with('plainto_tsquery(COALESCE(?, get_current_ts_config()), ?) AS query')
273-
->andReturn('plainto_tsquery(COALESCE(?, get_current_ts_config()), ?) AS query');
272+
->with('plainto_tsquery(COALESCE(?, get_current_ts_config()), ?) AS "tsquery"')
273+
->andReturn('plainto_tsquery(COALESCE(?, get_current_ts_config()), ?) AS "tsquery"');
274274

275275
$table->shouldReceive('crossJoin')
276-
->with('plainto_tsquery(COALESCE(?, get_current_ts_config()), ?) AS query')
276+
->with('plainto_tsquery(COALESCE(?, get_current_ts_config()), ?) AS "tsquery"')
277277
->andReturnSelf()
278278
->shouldReceive('select')
279279
->with('id')
280280
->andReturnSelf()
281281
->shouldReceive('selectRaw')
282-
->with('ts_rank(searchable,query) AS rank')
282+
->with('ts_rank(searchable,"tsquery") AS rank')
283283
->andReturnSelf()
284284
->shouldReceive('selectRaw')
285285
->with('COUNT(*) OVER () AS total_count')

0 commit comments

Comments
 (0)