From 15b43ca3d3033fae892ef9c6932aaa9c3403cb02 Mon Sep 17 00:00:00 2001 From: nath Date: Sat, 14 Dec 2019 18:02:57 +0100 Subject: [PATCH] Add GeoSearch Index and search --- README.md | 6 ++- src/Console/ImportCommand.php | 30 ++++++++--- src/Engines/TNTSearchEngine.php | 71 ++++++++++++++++++++++++++- src/TNTSearchScoutServiceProvider.php | 11 ++++- tests/TNTSearchEngineTest.php | 31 +++++++++++- 5 files changed, 138 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index b2083b5..868e230 100644 --- a/README.md +++ b/README.md @@ -71,12 +71,16 @@ In your `config/scout.php` add: ], 'asYouType' => false, 'searchBoolean' => env('TNTSEARCH_BOOLEAN', false), + 'geoIndex' => env('TNTSEARCH_GEOINDEX', false), ], ``` To prevent your search indexes being commited to your project repository, add the following line to your `.gitignore` file. -```/storage/*.index``` +``` +/storage/*.index +/storage/*.geoindex +``` The `asYouType` option can be set per model basis, see the example below. diff --git a/src/Console/ImportCommand.php b/src/Console/ImportCommand.php index 259f4eb..ef37e5f 100644 --- a/src/Console/ImportCommand.php +++ b/src/Console/ImportCommand.php @@ -4,6 +4,7 @@ use Illuminate\Console\Command; use Illuminate\Contracts\Events\Dispatcher; +use TeamTNT\TNTSearch\TNTGeoSearch; use TeamTNT\TNTSearch\TNTSearch; use Illuminate\Support\Facades\Schema; @@ -40,12 +41,6 @@ public function handle(Dispatcher $events) $config = config('scout.tntsearch') + config("database.connections.$driver"); $db = app('db')->connection($driver); - $tnt->loadConfig($config); - $tnt->setDatabaseHandle($db->getPdo()); - - $indexer = $tnt->createIndex($model->searchableAs().'.index'); - $indexer->setPrimaryKey($model->getKeyName()); - $availableColumns = Schema::connection($driver)->getColumnListing($model->getTable()); $desiredColumns = array_keys($model->toSearchableArray()); @@ -58,9 +53,32 @@ public function handle(Dispatcher $events) ->addSelect($fields); } + $tnt->loadConfig($config); + $tnt->setDatabaseHandle($db->getPdo()); + + $indexer = $tnt->createIndex($model->searchableAs().'.index'); + $indexer->setPrimaryKey($model->getKeyName()); + $indexer->query($query->toSql()); $indexer->run(); + + if (!empty($config['geoIndex'])) { + $geotnt = new TNTGeoSearch(); + + $geotnt->loadConfig($config); + $geotnt->setDatabaseHandle($db->getPdo()); + + $geoIndexer = $geotnt->getIndex(); + $geoIndexer->loadConfig($geotnt->config); + $geoIndexer->createIndex($model->searchableAs().'.geoindex'); + $geoIndexer->setPrimaryKey($model->getKeyName()); + + $geoIndexer->query($query->toSql()); + + $geoIndexer->run(); + } + $this->info('All ['.$class.'] records have been imported.'); } } diff --git a/src/Engines/TNTSearchEngine.php b/src/Engines/TNTSearchEngine.php index 1d40377..a06d4c4 100644 --- a/src/Engines/TNTSearchEngine.php +++ b/src/Engines/TNTSearchEngine.php @@ -8,6 +8,7 @@ use Laravel\Scout\Builder; use Laravel\Scout\Engines\Engine; use TeamTNT\TNTSearch\Exceptions\IndexNotFoundException; +use TeamTNT\TNTSearch\TNTGeoSearch; use TeamTNT\TNTSearch\TNTSearch; class TNTSearchEngine extends Engine @@ -17,6 +18,11 @@ class TNTSearchEngine extends Engine */ protected $tnt; + /** + * @var TNTGeoSearch + */ + protected $geotnt; + /** * @var Builder */ @@ -26,10 +32,12 @@ class TNTSearchEngine extends Engine * Create a new engine instance. * * @param TNTSearch $tnt + * @param TNTGeoSearch|null $geotnt */ - public function __construct(TNTSearch $tnt) + public function __construct(TNTSearch $tnt, TNTGeoSearch $geotnt = null) { $this->tnt = $tnt; + $this->geotnt = $geotnt; } /** @@ -46,8 +54,17 @@ public function update($models) $index = $this->tnt->getIndex(); $index->setPrimaryKey($models->first()->getKeyName()); + $geoindex = null; + if ($this->geotnt) { + $this->geotnt->selectIndex("{$models->first()->searchableAs()}.geoindex"); + $geoindex = $this->geotnt->getIndex(); + $geoindex->loadConfig($this->geotnt->config); + $geoindex->setPrimaryKey($models->first()->getKeyName()); + $geoindex->indexBeginTransaction(); + } + $index->indexBeginTransaction(); - $models->each(function ($model) use ($index) { + $models->each(function ($model) use ($index, $geoindex) { $array = $model->toSearchableArray(); if (empty($array)) { @@ -56,11 +73,25 @@ public function update($models) if ($model->getKey()) { $index->update($model->getKey(), $array); + if ($geoindex) { + $geoindex->prepareAndExecuteStatement( + 'DELETE FROM locations WHERE doc_id = :documentId;', + [['key' => ':documentId', 'value' => $model->getKey()]] + ); + } } else { $index->insert($array); } + if ($geoindex && !empty($array['longitude']) && !empty($array['latitude'])) { + $array['longitude'] = (float) $array['longitude']; + $array['latitude'] = (float) $array['latitude']; + $geoindex->insert($array); + } }); $index->indexEndTransaction(); + if ($this->geotnt) { + $geoindex->indexEndTransaction(); + } } /** @@ -78,6 +109,17 @@ public function delete($models) $index = $this->tnt->getIndex(); $index->setPrimaryKey($model->getKeyName()); $index->delete($model->getKey()); + + if ($this->geotnt) { + $this->geotnt->selectIndex("{$model->searchableAs()}.geoindex"); + $index = $this->geotnt->getIndex(); + $index->loadConfig($this->geotnt->config); + $index->setPrimaryKey($model->getKeyName()); + $index->prepareAndExecuteStatement( + 'DELETE FROM locations WHERE doc_id = :documentId;', + [['key' => ':documentId', 'value' => $model->getKey()]] + ); + } }); } @@ -145,6 +187,9 @@ protected function performSearch(Builder $builder, array $options = []) $index = $builder->index ?: $builder->model->searchableAs(); $limit = $builder->limit ?: 10000; $this->tnt->selectIndex("{$index}.index"); + if ($this->geotnt) { + $this->geotnt->selectIndex("{$index}.geoindex"); + } $this->builder = $builder; @@ -160,6 +205,14 @@ protected function performSearch(Builder $builder, array $options = []) $options ); } + + if (is_array($builder->query)) { + $location = $builder->query['location']; + $distance = $builder->query['distance']; + $limit = array_key_exists('limit', $builder->query) ? $builder->query['limit'] : 10; + return $this->geotnt->findNearest($location, $distance, $limit); + } + if (isset($this->tnt->config['searchBoolean']) ? $this->tnt->config['searchBoolean'] : false) { return $this->tnt->searchBoolean($builder->query, $limit); } else { @@ -260,6 +313,13 @@ public function initIndex($model) $indexer->setDatabaseHandle($model->getConnection()->getPdo()); $indexer->setPrimaryKey($model->getKeyName()); } + if ($this->geotnt && !file_exists($this->tnt->config['storage']."/{$indexName}.geoindex")) { + $indexer = $this->geotnt->getIndex(); + $indexer->loadConfig($this->geotnt->config); + $indexer->createIndex("$indexName.geoindex"); + $indexer->setDatabaseHandle($model->getConnection()->getPdo()); + $indexer->setPrimaryKey($model->getKeyName()); + } } /** @@ -401,5 +461,12 @@ public function flush($model) if (file_exists($pathToIndex)) { unlink($pathToIndex); } + + if ($this->geotnt){ + $pathToGeoIndex = $this->geotnt->config['storage']."/{$indexName}.geoindex"; + if (file_exists($pathToGeoIndex)) { + unlink($pathToGeoIndex); + } + } } } diff --git a/src/TNTSearchScoutServiceProvider.php b/src/TNTSearchScoutServiceProvider.php index 3d24d04..5cb451c 100644 --- a/src/TNTSearchScoutServiceProvider.php +++ b/src/TNTSearchScoutServiceProvider.php @@ -1,5 +1,6 @@ setFuzziness($tnt); $this->setAsYouType($tnt); - return new TNTSearchEngine($tnt); + $geotnt = null; + if (!empty($config['geoIndex'])) { + $geotnt = new TNTGeoSearch(); + + $geotnt->loadConfig($config); + $geotnt->setDatabaseHandle(app('db')->connection()->getPdo()); + } + + return new TNTSearchEngine($tnt, $geotnt); }); if ($this->app->runningInConsole()) { diff --git a/tests/TNTSearchEngineTest.php b/tests/TNTSearchEngineTest.php index 22c4285..d3927aa 100644 --- a/tests/TNTSearchEngineTest.php +++ b/tests/TNTSearchEngineTest.php @@ -29,7 +29,36 @@ public function test_update_adds_objects_to_index() $index->shouldReceive('update'); $index->shouldReceive('indexEndTransaction'); - $engine = new TNTSearchEngine($client); + $geoClient = Mockery::mock('TeamTNT\TNTSearch\TNTGeoSearch'); + + $config = [ + 'storage' => '/', + 'fuzziness' => false, + 'fuzzy' => [ + 'prefix_length' => 2, + 'max_expansions' => 50, + 'distance' => 2 + ], + 'asYouType' => false, + 'searchBoolean' => false, + ]; + + $geoClient->shouldReceive('getIndex') + ->andReturn($geoIndex = Mockery::mock('TeamTNT\TNTSearch\Indexer\TNTGeoIndexer')) + ->andSet('config', $config); + $geoIndex->shouldReceive('loadConfig'); + $geoIndex->shouldReceive('createIndex') + ->with('table.geoindex'); + $geoIndex->shouldReceive('setDatabaseHandle'); + $geoIndex->shouldReceive('setPrimaryKey'); + $geoClient->shouldReceive('selectIndex'); + $geoIndex->shouldReceive('indexBeginTransaction'); + $geoIndex->shouldReceive('prepareAndExecuteStatement'); + $geoIndex->shouldReceive('insert'); + $geoIndex->shouldReceive('indexEndTransaction'); + + + $engine = new TNTSearchEngine($client, $geoClient); $engine->update(Collection::make([new TNTSearchEngineTestModel()])); } }