├── CHANGELOG.md ├── LICENSE ├── README.md ├── composer.json └── src ├── Bootstrap.php ├── Searchable.php ├── SearchableBehavior.php ├── SearchableTrait.php ├── TNTSearch.php ├── console └── CommandController.php ├── expression ├── Condition.php ├── ConditionBuilder.php ├── Expression.php ├── OrderBy.php └── OrderByBuilder.php └── queue ├── DeleteSearchable.php ├── Job.php └── MakeSearchable.php /CHANGELOG.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vuongxuongminh/yii2-searchable/7e386016031c893785d60f2a8e1aae4f6b0f080c/CHANGELOG.md -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2019, Vuong Xuong Minh 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | * Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | * Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | * Neither the name of the copyright holder nor the names of its 17 | contributors may be used to endorse or promote products derived from 18 | this software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 22 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 23 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 24 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 25 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 26 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 27 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 28 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Yii2 Searchable 2 | 3 | [![Latest Stable Version](https://poser.pugx.org/vxm/yii2-searchable/v/stable)](https://packagist.org/packages/vxm/yii2-searchable) 4 | [![Total Downloads](https://poser.pugx.org/vxm/yii2-searchable/downloads)](https://packagist.org/packages/vxm/yii2-searchable) 5 | [![Build Status](https://travis-ci.org/vuongxuongminh/yii2-searchable.svg?branch=master)](https://travis-ci.org/vuongxuongminh/yii2-searchable) 6 | [![Code Coverage](https://scrutinizer-ci.com/g/vuongxuongminh/yii2-searchable/badges/coverage.png?b=master)](https://scrutinizer-ci.com/g/vuongxuongminh/yii2-searchable/?branch=master) 7 | [![Scrutinizer Code Quality](https://scrutinizer-ci.com/g/vuongxuongminh/yii2-searchable/badges/quality-score.png?b=master)](https://scrutinizer-ci.com/g/vuongxuongminh/yii2-searchable/?branch=master) 8 | [![Yii2](https://img.shields.io/badge/Powered_by-Yii_Framework-green.svg?style=flat)](http://www.yiiframework.com/) 9 | 10 | ## About it 11 | 12 | An extension provide simple full-text search with ideas get from [laravel/scout](https://github.com/laravel/scout) and base on [teamtnt/tntsearch](https://github.com/teamtnt/tntsearch) wrapper for Yii2 Active Record. 13 | 14 | ## Requirements 15 | 16 | * [PHP >= 7.1](http://php.net) 17 | * [yiisoft/yii2 >= 2.0.14.2](https://github.com/yiisoft/yii2) 18 | 19 | ## Installation 20 | 21 | Require Yii2 Searchable using [Composer](https://getcomposer.org): 22 | 23 | ```bash 24 | composer require vxm/yii2-searchable 25 | ``` 26 | 27 | Finally, add the `\vxm\searchable\SearchableTrait` trait and attach `vxm\searchable\SearchableBehavior` behavior to the Active Record you would like to make searchable. 28 | This will help sync the model with index data. 29 | 30 | ```php 31 | use vxm\searchable\SearchableBehavior; 32 | use vxm\searchable\SearchableTrait; 33 | 34 | class Article extends ActiveRecord 35 | { 36 | 37 | use SearchableTrait; 38 | 39 | /** 40 | * @inheritDoc 41 | */ 42 | public function behaviors() 43 | { 44 | return [ 45 | 'searchable' => SearchableBehavior::class 46 | ]; 47 | } 48 | 49 | 50 | } 51 | ``` 52 | 53 | ### Queueing 54 | 55 | While not strictly required to use this extension, you should strongly consider configuring a [yii2-queue](https://github.com/yiisoft/yii2-queue) 56 | before using an extension. Running a queue worker will allow it to queue all operations that sync your model information to your search indexes, 57 | providing much better response times for your application's web interface. 58 | 59 | Once you have configured a queue component, set the value of the queue option in your application configuration file to component id of it 60 | or an array config of it. 61 | 62 | ```php 63 | 'components' => [ 64 | 'searchable' => [ 65 | 'class' => 'vxm\searchable\Searchable', 66 | 'queue' => 'queueComponentId' 67 | ] 68 | ] 69 | ``` 70 | 71 | ## Configuration 72 | 73 | ### Configuring Component 74 | 75 | By default a component will be add to your application components via bootstrapping with id `searchable`. If you need to config it you can manual config in your application config file: 76 | 77 | ```php 78 | 'components' => [ 79 | 'searchable' => [ 80 | 'class' => 'vxm\searchable\Searchable', 81 | 'storagePath' => '@runtime/vxm/search', 82 | 'queue' => null, // an optional not required 83 | 'defaultSearchMode' => \vxm\searchable\Searchable::FUZZY_SEARCH, 84 | 'asYouType' => false, 85 | 'fuzziness' => false, 86 | 'fuzzyPrefixLength' => 2, 87 | 'fuzzyMaxExpansions' => 50, 88 | 'fuzzyMaxExpansions' => 50, 89 | 'fuzzyDistance' => 50 90 | ] 91 | ] 92 | ``` 93 | 94 | ### Configuring Model Index 95 | 96 | Each Active Record model is synced with a given search `index`, which contains all of the searchable records for that model. 97 | In other words, you can think of each index like a MySQL table. By default, each model will be persisted to an index matching the model's typical `table` name. 98 | Typically, this is the plural form of the model name; however, you are free to customize the `index` name by overriding the `searchableIndex` static method on the Active Record model class: 99 | 100 | ```php 101 | use vxm\searchable\SearchableBehavior; 102 | use vxm\searchable\SearchableTrait; 103 | 104 | class Article extends ActiveRecord 105 | { 106 | 107 | use SearchableTrait; 108 | 109 | /** 110 | * @inheritDoc 111 | */ 112 | public function behaviors() 113 | { 114 | return [ 115 | 'searchable' => SearchableBehavior::class 116 | ]; 117 | } 118 | 119 | /** 120 | * Get the index name for the model class. 121 | * 122 | * @return string 123 | */ 124 | public static function searchableIndex(): string 125 | { 126 | return 'articles_index'; 127 | } 128 | 129 | } 130 | ``` 131 | 132 | ### Configuring Searchable Data 133 | 134 | By default, the entire `toArray` form of a given model will be persisted to its search index. 135 | If you would like to customize the data that is synchronized to the search index, 136 | you may override the `toSearchableArray` method on the model: 137 | 138 | ```php 139 | use vxm\searchable\SearchableBehavior; 140 | use vxm\searchable\SearchableTrait; 141 | 142 | class Article extends ActiveRecord 143 | { 144 | 145 | use SearchableTrait; 146 | 147 | /** 148 | * @inheritDoc 149 | */ 150 | public function behaviors() 151 | { 152 | return [ 153 | 'searchable' => SearchableBehavior::class 154 | ]; 155 | } 156 | 157 | /** 158 | * Get the indexable data array for the model. 159 | * 160 | * @return array 161 | */ 162 | public function toSearchableArray(): array 163 | { 164 | $array = $this->toArray(); 165 | 166 | // Customize array... 167 | 168 | return $array; 169 | } 170 | 171 | } 172 | ``` 173 | 174 | ### Configuring Searchable Key 175 | 176 | By default, the primary key name of the model as the unique ID stored in the search index. 177 | If you need to customize this behavior, you may override the `searchableKey` static method on the model: 178 | 179 | ```php 180 | use vxm\searchable\SearchableBehavior; 181 | use vxm\searchable\SearchableTrait; 182 | 183 | class Article extends ActiveRecord 184 | { 185 | 186 | use SearchableTrait; 187 | 188 | /** 189 | * @inheritDoc 190 | */ 191 | public function behaviors() 192 | { 193 | return [ 194 | 'searchable' => SearchableBehavior::class 195 | ]; 196 | } 197 | 198 | /** 199 | * Get searchable key by default primary key will be use. 200 | * 201 | * @return string key name. 202 | */ 203 | public static function searchableKey(): string 204 | { 205 | return 'id'; 206 | } 207 | 208 | } 209 | ``` 210 | 211 | ## Indexing 212 | 213 | ### Batch Import 214 | 215 | If you are installing an extension into an existing project, you may already have database records you need to import into your search driver. 216 | This extension provides an import action that you may use to import all of your existing records into your search indexes: 217 | 218 | ```php 219 | php yii searchable/import --models="app\models\Post" 220 | ``` 221 | 222 | You can import multi model classes by separator `,`: 223 | 224 | ```php 225 | php yii searchable/import --models="app\models\Post, app\models\Category" 226 | ``` 227 | 228 | ### Adding Records 229 | 230 | Once you have added the `vxm\searchable\SearchableTrait` and attached the `vxm\searchable\SearchableBehavior` behavior to a model, 231 | all you need to do is save a model instance and it will automatically be added to your search index. 232 | If you have configured queue this operation will be performed in the background by your queue worker: 233 | 234 | ```php 235 | $post = new \app\models\Post; 236 | 237 | // ... 238 | 239 | $post->save(); 240 | ``` 241 | 242 | ### Adding Via Active Query Result 243 | 244 | If you would like to add a Active Query results to your search index, you may use `makeSearchable` method onto an Active Query result. 245 | The `makeSearchable` method will chunk the results of the query and add the records to your search index. 246 | Again, if you have configured queue, all of the chunks will be added in the background by your queue workers: 247 | 248 | ```php 249 | // Adding via Active Query result... 250 | $models = \app\models\Post::find()->where(['author_id' => 1])->all(); 251 | 252 | \app\models\Post::makeSearchable($models); 253 | ``` 254 | 255 | The `makeSearchable` method can be considered an `upsert` operation. In other words, if the model record is already in your index, it will be updated. 256 | If it does not exist in the search index, it will be added to the index. 257 | 258 | ### Updating Records 259 | 260 | To update a searchable model, you only need to update the model instance's properties and save the model to your database. 261 | This extension will automatically persist the changes to your search index: 262 | 263 | ```php 264 | $post = \app\models\Post::findOne(1); 265 | 266 | // Update the post... 267 | 268 | $post->save(); 269 | ``` 270 | 271 | You may also use the `makeSearchable` method on an Active Record class to update instance. 272 | If the models do not exist in your search index, they will be created: 273 | 274 | ```php 275 | // Updating via Active Query result... 276 | $models = \app\models\Post::find()->where(['author_id' => 1])->all(); 277 | 278 | \app\models\Post::makeSearchable($models); 279 | ``` 280 | 281 | ### Deleting Records 282 | 283 | To delete a record from your index, delete the model from the database: 284 | 285 | ```php 286 | $post = \app\models\Post::findOne(1); 287 | 288 | $post->delete(); 289 | ``` 290 | 291 | If you would like to delete a Active Query result from your search index, you may use the `deleteSearchable` method on an Active Record class: 292 | 293 | ```php 294 | // Deleting via Active Query result... 295 | $models = \app\models\Post::find()->where(['author_id' => 1])->all(); 296 | 297 | \app\models\Post::deleteSearchable($models); 298 | ``` 299 | 300 | ### Pausing Indexing 301 | 302 | Sometimes you may need to perform a batch of Active Record operations on a model without syncing the model data to your search index. 303 | You may do this using the `withoutSyncingToSearch` method. This method accepts a single callback which will be immediately executed. 304 | Any model operations that occur within the callback will not be synced to the model's index: 305 | 306 | ```php 307 | \app\models\Post::withoutSyncingToSearch(function () { 308 | $post = \app\models\Post::findOne(1); 309 | $post->save(); // will not syncing with index data 310 | }); 311 | ``` 312 | 313 | ### Conditionally Searchable Model Instances 314 | 315 | Sometimes you may need to only make a model searchable under certain conditions. For example, imagine you have `app\models\Article` model that may be in one of two states: `draft` and `published`. 316 | You may only want to allow `published` posts to be searchable. To accomplish this, you may define a `shouldBeSearchable` method on your model: 317 | 318 | ```php 319 | use vxm\searchable\SearchableBehavior; 320 | use vxm\searchable\SearchableTrait; 321 | 322 | class Article extends ActiveRecord 323 | { 324 | 325 | use SearchableTrait; 326 | 327 | /** 328 | * @inheritDoc 329 | */ 330 | public function behaviors() 331 | { 332 | return [ 333 | 'searchable' => SearchableBehavior::class 334 | ]; 335 | } 336 | 337 | /** 338 | * Determine if the model should be searchable. 339 | * 340 | * @return bool 341 | */ 342 | public static function shouldBeSearchable() 343 | { 344 | return $this->is_published; 345 | } 346 | 347 | } 348 | ``` 349 | 350 | The `shouldBeSearchable` method is only applied when manipulating models through the save method. 351 | Directly making models using the `searchable` or `makeSearchable` method will override the result of the `shouldBeSearchable` method: 352 | 353 | ```php 354 | // Will respect "shouldBeSearchable"... 355 | $post = \app\models\Post::findOne(1); 356 | 357 | $post->save(); 358 | 359 | // Will override "shouldBeSearchable"... 360 | $post->searchable(); 361 | 362 | $models = \app\models\Post::find()->where(['author_id' => 1])->all(); 363 | 364 | \app\models\Post::makeSearchable($models); 365 | ``` 366 | 367 | ## Searching 368 | 369 | ### Simple 370 | 371 | You may begin searching a model using the `search` method. The search method accepts a single string that will be used to search your models. 372 | This method return an `ActiveQuery` you can add more condition or relationship like an origin query. 373 | 374 | > Note when add more query condition you must not be use `where` method use `andWhere` or `orWhere` instead because it will override search ids condition result. 375 | 376 | ```php 377 | $posts = \app\models\Post::search('vxm')->all(); 378 | $posts = \app\models\Post::search('vxm')->andWhere(['author_id' => 1])->all(); 379 | 380 | 381 | // not use 382 | $posts = \app\models\Post::search('vxm')->where(['author_id' => 1])->all(); 383 | ``` 384 | 385 | ### Advanced 386 | 387 | You can joining relations on search query with relations support `searchable`: 388 | 389 | ```php 390 | $posts = \app\models\Post::search('vxm')->joinWith('category')->andWhere(Category::search('vxm category')); 391 | ``` 392 | 393 | ### Search mode 394 | 395 | You can choice a `boolean` or `fuzzy` search mode as a second parameter if not set `defaultSearchMode` of `Searchable` component will be use: 396 | 397 | ```php 398 | $posts = \app\models\Post::search('vxm', 'fuzzy', ['fuzziness' => true])->all(); 399 | $posts = \app\models\Post::search('vxm', 'boolean')->all(); 400 | ``` 401 | 402 | For more detail of search mode please refer to [teamtnt/tntsearch](https://github.com/teamtnt/tntsearch) to see full document. 403 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vxm/yii2-searchable", 3 | "description": "Simple full-text search for Yii2 active record", 4 | "type": "yii2-extension", 5 | "keywords": ["yii2","extension","full-text-search","active-record"], 6 | "license": "BSD-3-Clause", 7 | "authors": [ 8 | { 9 | "name": "Vuong Minh", 10 | "email": "vuongxuongminh@gmail.com" 11 | } 12 | ], 13 | "require": { 14 | "php": ">=7.1", 15 | "yiisoft/yii2": ">=2.0.14.2", 16 | "yiisoft/yii2-queue": "^2.2", 17 | "teamtnt/tntsearch": "~2.0" 18 | }, 19 | "autoload": { 20 | "psr-4": { 21 | "vxm\\searchable\\": "src" 22 | } 23 | }, 24 | "repositories": [ 25 | { 26 | "type": "composer", 27 | "url": "https://asset-packagist.org" 28 | } 29 | ], 30 | "extra": { 31 | "branch-alias": { 32 | "dev-master": "1.0.x-dev" 33 | }, 34 | "bootstrap": "vxm\\searchable\\Bootstrap" 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/Bootstrap.php: -------------------------------------------------------------------------------- 1 | 19 | * @since 1.0.0 20 | */ 21 | class Bootstrap implements BootstrapInterface 22 | { 23 | 24 | /** 25 | * @inheritDoc 26 | * @throws \yii\base\InvalidConfigException 27 | */ 28 | public function bootstrap($app) 29 | { 30 | if (!$app->get('searchable', false)) { 31 | $app->set('searchable', ['class' => Searchable::class]); 32 | } 33 | 34 | if ($app instanceof ConsoleApp && !isset($app->controllerMap['searchable'])) { 35 | $app->controllerMap['searchable'] = CommandController::class; 36 | } 37 | } 38 | 39 | } 40 | -------------------------------------------------------------------------------- /src/Searchable.php: -------------------------------------------------------------------------------- 1 | 24 | * @since 1.0.0 25 | */ 26 | class Searchable extends Component 27 | { 28 | /** 29 | * Search data with boolean mode. 30 | */ 31 | const BOOLEAN_SEARCH = 'boolean'; 32 | 33 | /** 34 | * Search data with fuzzy mode. 35 | */ 36 | const FUZZY_SEARCH = 'fuzzy'; 37 | 38 | /** 39 | * @var string default search mode for [[search()]] if `$mode` param not set. 40 | */ 41 | public $defaultSearchMode = self::FUZZY_SEARCH; 42 | 43 | /** 44 | * @var string the TNTSearch class. 45 | */ 46 | public $tntSearchClass = TNTSearch::class; 47 | 48 | /** 49 | * @var bool default as you type search config. 50 | */ 51 | public $asYouType = false; 52 | 53 | /** 54 | * @var bool default fuzziness search config. 55 | */ 56 | public $fuzziness = false; 57 | 58 | /** 59 | * @var int default fuzzy prefix length config. 60 | */ 61 | public $fuzzyPrefixLength = 2; 62 | 63 | /** 64 | * @var int default fuzzy max expansions config. 65 | */ 66 | public $fuzzyMaxExpansions = 50; 67 | 68 | /** 69 | * @var int default fuzzy distance config. 70 | */ 71 | public $fuzzyDistance = 2; 72 | 73 | /** 74 | * @var string default storage path of index data. 75 | */ 76 | public $storagePath = '@runtime/vxm/searchable'; 77 | 78 | /** 79 | * @var Queue|null use for support make or delete index data via worker. 80 | */ 81 | public $queue; 82 | 83 | /** 84 | * @inheritDoc 85 | * @throws \yii\base\InvalidConfigException 86 | */ 87 | public function init() 88 | { 89 | $this->storagePath = Yii::getAlias($this->storagePath); 90 | 91 | if ($this->queue !== null) { 92 | $this->queue = Instance::ensure($this->queue, Queue::class); 93 | } 94 | 95 | parent::init(); 96 | } 97 | 98 | /** 99 | * Search by model class via given query string. 100 | * 101 | * @param string $modelClass need to search. 102 | * @param string $query apply to search. 103 | * @param string $mode boolean or fuzzy search mode. 104 | * @param array $config of [[\vxm\searchable\TNTSearch]]. 105 | * @param int $limit of values search. 106 | * @return array search results. 107 | * @throws \TeamTNT\TNTSearch\Exceptions\IndexNotFoundException 108 | * @throws \yii\base\InvalidConfigException 109 | */ 110 | public function search(string $modelClass, string $query, ?string $mode = null, array $config = [], int $limit = 100): array 111 | { 112 | /** @var \yii\db\ActiveRecord $modelClass */ 113 | $this->initIndex($modelClass, $config); 114 | $tnt = $this->createTNTSearch($modelClass::getDb(), $config); 115 | $tnt->selectIndex("{$modelClass::searchableIndex()}.index"); 116 | $mode = $mode ?? $this->defaultSearchMode; 117 | 118 | if ($mode === self::BOOLEAN_SEARCH) { 119 | 120 | return $tnt->searchBoolean($query, $limit); 121 | } else { 122 | 123 | return $tnt->search($query, $limit); 124 | } 125 | } 126 | 127 | /** 128 | * Delete all instances of the model class from the search index. 129 | * 130 | * @param string $modelClass need to delete all instances. 131 | * @param array $config of [[\vxm\searchable\TNTSearch]]. 132 | * @throws \yii\base\InvalidConfigException 133 | */ 134 | public function deleteAllFromSearch(string $modelClass, array $config = []): void 135 | { 136 | /** @var \yii\db\ActiveRecord $modelClass */ 137 | $tnt = $this->createTNTSearch($modelClass::getDb(), $config); 138 | $pathToIndex = $tnt->config['storage'] . "/{$modelClass::searchableIndex()}.index"; 139 | 140 | if (file_exists($pathToIndex)) { 141 | unlink($pathToIndex); 142 | } 143 | } 144 | 145 | /** 146 | * Dispatch the job to make the given models unsearchable. 147 | * 148 | * @param \yii\db\ActiveRecord|\yii\db\ActiveRecord[]|static|static[] $models dispatch to queue. 149 | * @param array $config of [[\vxm\searchable\TNTSearch]]. 150 | * @throws \TeamTNT\TNTSearch\Exceptions\IndexNotFoundException 151 | * @throws \yii\base\InvalidConfigException 152 | */ 153 | public function queueDeleteFromSearch($models, array $config = []): void 154 | { 155 | $models = is_array($models) ? $models : [$models]; 156 | 157 | if (empty($models)) { 158 | 159 | return; 160 | } 161 | 162 | if ($this->queue === null) { 163 | 164 | $this->delete($models, $config); 165 | } else { 166 | 167 | $job = new DeleteSearchable($models); 168 | $this->queue->push($job); 169 | } 170 | } 171 | 172 | /** 173 | * Dispatch the job to make the given models searchable. 174 | * 175 | * @param \yii\db\ActiveRecord|\yii\db\ActiveRecord[]|static|static[] $models dispatch to queue job. 176 | * @param array $config of [[\vxm\searchable\TNTSearch]]. 177 | * @throws \TeamTNT\TNTSearch\Exceptions\IndexNotFoundException 178 | * @throws \yii\base\InvalidConfigException 179 | */ 180 | public function queueMakeSearchable($models, array $config = []): void 181 | { 182 | $models = is_array($models) ? $models : [$models]; 183 | 184 | if (empty($models)) { 185 | 186 | return; 187 | } 188 | 189 | if ($this->queue === null) { 190 | 191 | $this->upsert($models, $config); 192 | } else { 193 | 194 | $job = new MakeSearchable($models); 195 | $this->queue->push($job); 196 | } 197 | } 198 | 199 | /** 200 | * Update or insert models to search engine. 201 | * 202 | * @param \yii\db\ActiveRecord|\yii\db\ActiveRecord[]|static|static[] $models dispatch to queue. 203 | * @param array $config of [[\vxm\searchable\TNTSearch]]. 204 | * @throws \TeamTNT\TNTSearch\Exceptions\IndexNotFoundException 205 | * @throws \yii\base\InvalidConfigException 206 | */ 207 | public function upsert($models, array $config = []): void 208 | { 209 | $models = is_array($models) ? $models : [$models]; 210 | /** @var \yii\db\ActiveRecord $modelClass */ 211 | $modelClass = get_class(current($models)); 212 | $this->initIndex($modelClass, $config); 213 | $tnt = $this->createTNTSearch($modelClass::getDb(), $config); 214 | $tnt->selectIndex("{$modelClass::searchableIndex()}.index"); 215 | $index = $tnt->getIndex(); 216 | $index->setPrimaryKey($modelClass::searchableKey()); 217 | $index->indexBeginTransaction(); 218 | 219 | foreach ($models as $model) { 220 | 221 | if ($data = $model->toSearchableArray()) { 222 | $index->update($model->getSearchableKey(), $data); 223 | } 224 | } 225 | 226 | $index->indexEndTransaction(); 227 | } 228 | 229 | /** 230 | * Delete models from search engine. 231 | * 232 | * @param \yii\db\ActiveRecord|\yii\db\ActiveRecord[]|static|static[] $models need to delete. 233 | * @param array $config of [[\vxm\searchable\TNTSearch]]. 234 | * @throws \TeamTNT\TNTSearch\Exceptions\IndexNotFoundException 235 | * @throws \yii\base\InvalidConfigException 236 | */ 237 | public function delete($models, array $config = []): void 238 | { 239 | $models = is_array($models) ? $models : [$models]; 240 | /** @var \yii\db\ActiveRecord $modelClass */ 241 | $modelClass = get_class(current($models)); 242 | $this->initIndex($modelClass, $config); 243 | $tnt = $this->createTNTSearch($modelClass::getDb(), $config); 244 | $tnt->selectIndex("{$modelClass::searchableIndex()}.index"); 245 | $index = $tnt->getIndex(); 246 | $index->setPrimaryKey($modelClass::searchableKey()); 247 | 248 | foreach ($models as $model) { 249 | /** @var \yii\db\ActiveRecord $model */ 250 | $index->delete($model->getSearchableKey()); 251 | } 252 | } 253 | 254 | /** 255 | * Init index data of model class. 256 | * 257 | * @param string $modelClass to init index data. 258 | * @param array $config of [[\vxm\searchable\TNTSearch]]. 259 | * @throws \yii\base\InvalidConfigException 260 | */ 261 | public function initIndex(string $modelClass, array $config = []): void 262 | { 263 | /** @var \yii\db\ActiveRecord $modelClass */ 264 | $index = $modelClass::searchableIndex() . '.index'; 265 | $tnt = $this->createTNTSearch($modelClass::getDb(), $config); 266 | 267 | if (!file_exists($tnt->config['storage'] . "/{$index}")) { 268 | $indexer = $tnt->createIndex($index); 269 | $indexer->setPrimaryKey($modelClass::searchableKey()); 270 | } 271 | } 272 | 273 | /** 274 | * Create tnt search object. 275 | * 276 | * @param Connection|null $db use to get database info. 277 | * @param array $config of [[\vxm\searchable\TNTSearch]]. 278 | * @return object|TNTSearch 279 | * @throws \yii\base\InvalidConfigException 280 | */ 281 | public function createTNTSearch(?Connection $db = null, array $config = []): TNTSearch 282 | { 283 | $db = $db ?? Yii::$app->getDb(); 284 | $dbh = $db->getMasterPdo(); 285 | $tnt = Yii::createObject([ 286 | 'class' => $this->tntSearchClass, 287 | 'asYouType' => $config['asYouType'] ?? $this->asYouType, 288 | 'fuzziness' => $config['fuzziness'] ?? $this->fuzziness, 289 | 'fuzzy_distance' => $config['fuzzy_distance'] ?? $config['fuzzyDistance'] ?? $this->fuzzyDistance, 290 | 'fuzzy_prefix_length' => $config['fuzzy_prefix_length'] ?? $config['fuzzyPrefixLength'] ?? $this->fuzzyPrefixLength, 291 | 'fuzzy_max_expansions' => $config['fuzzy_max_expansions'] ?? $config['fuzzyMaxExpansions'] ?? $this->fuzzyMaxExpansions 292 | ]); 293 | $tnt->loadConfig(['storage' => $this->storagePath]); 294 | $tnt->setDatabaseHandle($dbh); 295 | 296 | return $tnt; 297 | } 298 | 299 | 300 | } 301 | -------------------------------------------------------------------------------- /src/SearchableBehavior.php: -------------------------------------------------------------------------------- 1 | 17 | * @since 1.0.0 18 | */ 19 | class SearchableBehavior extends Behavior 20 | { 21 | 22 | /** 23 | * @var \yii\db\ActiveRecord 24 | * @inheritDoc 25 | */ 26 | public $owner; 27 | 28 | /** 29 | * The class names that syncing is disabled for. 30 | * 31 | * @var string[] 32 | */ 33 | protected static $syncingDisabledFor = []; 34 | 35 | /** 36 | * @inheritDoc 37 | */ 38 | public function events() 39 | { 40 | return [ 41 | ActiveRecord::EVENT_AFTER_INSERT => 'afterSave', 42 | ActiveRecord::EVENT_AFTER_UPDATE => 'afterSave', 43 | ActiveRecord::EVENT_AFTER_DELETE => 'afterDelete' 44 | ]; 45 | } 46 | 47 | /** 48 | * Handle the saved event for the model. 49 | */ 50 | public function afterSave() 51 | { 52 | $model = $this->owner; 53 | 54 | if (static::syncingDisabledFor($model)) { 55 | return; 56 | } 57 | 58 | if (!$model->shouldBeSearchable()) { 59 | $model->unsearchable(); 60 | 61 | return; 62 | } 63 | 64 | $model->searchable(); 65 | } 66 | 67 | /** 68 | * Handle the deleted event for the model. 69 | */ 70 | public function afterDelete(): void 71 | { 72 | $model = $this->owner; 73 | 74 | if (static::syncingDisabledFor($model)) { 75 | return; 76 | } 77 | 78 | $model->unsearchable(); 79 | } 80 | 81 | /** 82 | * Enable syncing for the given class. 83 | * 84 | * @param string $class of records need to enable syncing. 85 | */ 86 | public static function enableSyncingFor($class): void 87 | { 88 | unset(static::$syncingDisabledFor[$class]); 89 | } 90 | 91 | /** 92 | * Disable syncing for the given class. 93 | * 94 | * @param string $class of records need to disable syncing. 95 | */ 96 | public static function disableSyncingFor($class): void 97 | { 98 | static::$syncingDisabledFor[$class] = true; 99 | } 100 | 101 | /** 102 | * Determine if syncing is disabled for the given class or model. 103 | * 104 | * @param object|string $class of records need to disable syncing. 105 | * @return bool weather syncing disabled. 106 | */ 107 | public static function syncingDisabledFor($class): bool 108 | { 109 | $class = is_object($class) ? get_class($class) : $class; 110 | 111 | return isset(static::$syncingDisabledFor[$class]); 112 | } 113 | 114 | } 115 | -------------------------------------------------------------------------------- /src/SearchableTrait.php: -------------------------------------------------------------------------------- 1 | 24 | * @since 1.0.0 25 | */ 26 | trait SearchableTrait 27 | { 28 | 29 | /** 30 | * @inheritDoc 31 | * @return \yii\db\Connection 32 | */ 33 | abstract public static function getDb(); 34 | 35 | /** 36 | * @inheritDoc 37 | * @return string 38 | */ 39 | abstract public static function tableName(); 40 | 41 | /** 42 | * @inheritDoc 43 | * @return mixed 44 | */ 45 | abstract public static function primaryKey(); 46 | 47 | /** 48 | * @inheritDoc 49 | * @return \yii\db\ActiveQuery|\yii\db\ActiveQueryInterface 50 | */ 51 | abstract public static function find(); 52 | 53 | /** 54 | * Get searchable support full-text search for this model class. 55 | * 56 | * @return object|Searchable 57 | * @throws \yii\base\InvalidConfigException 58 | */ 59 | public static function getSearchable(): Searchable 60 | { 61 | return Yii::$app->get('searchable'); 62 | } 63 | 64 | /** 65 | * Creating active query had been apply search ids condition by given query string. 66 | * 67 | * @param string $query to search data. 68 | * @param string $mode using for query search, [[\vxm\searchable\Searchable::BOOLEAN_SEARCH]] or [[\vxm\searchable\Searchable::FUZZY_SEARCH]]. 69 | * If not set [[\vxm\searchable\Searchable::$defaultSearchMode]] will be use. 70 | * @param array $config of [[\vxm\searchable\TNTSearch]]. 71 | * @return \yii\db\ActiveQuery|ActiveQueryInterface query instance. 72 | * @throws \TeamTNT\TNTSearch\Exceptions\IndexNotFoundException 73 | * @throws \yii\base\InvalidConfigException 74 | */ 75 | public static function search(string $query, ?string $mode = null, array $config = []): ActiveQueryInterface 76 | { 77 | $ids = static::searchIds($query, $mode, $config); 78 | /** @var \yii\db\ActiveQuery $aq */ 79 | $aq = static::find(); 80 | 81 | if (empty($ids)) { 82 | 83 | $aq->andWhere('1 = 0'); 84 | } else { 85 | /** @var \yii\db\Connection $db */ 86 | $db = static::getDb(); 87 | $db->setQueryBuilder([ 88 | 'expressionBuilders' => [ 89 | Condition::class => ConditionBuilder::class, 90 | OrderBy::class => OrderByBuilder::class 91 | ] 92 | ]); 93 | $expressionConfig = [ 94 | 'query' => $aq, 95 | 'ids' => $ids 96 | ]; 97 | $condition = new Condition($expressionConfig); 98 | $orderBy = new OrderBy($expressionConfig); 99 | $aq->andWhere($condition); 100 | $aq->addOrderBy($orderBy); 101 | } 102 | 103 | return $aq; 104 | } 105 | 106 | /** 107 | * Search ids by given query string. 108 | * 109 | * @param string $query to search data. 110 | * @param string|null $mode using for query search, [[\vxm\searchable\Searchable::BOOLEAN_SEARCH]] or [[\vxm\searchable\Searchable::FUZZY_SEARCH]]. 111 | * If not set [[\vxm\searchable\Searchable::$defaultSearchMode]] will be use. 112 | * @param array $config of [[\vxm\searchable\TNTSearch]]. 113 | * @return array search key values of indexing data search. 114 | * @throws \TeamTNT\TNTSearch\Exceptions\IndexNotFoundException 115 | * @throws \yii\base\InvalidConfigException 116 | */ 117 | public static function searchIds(string $query, ?string $mode = null, array $config = []): array 118 | { 119 | $profileToken = "Searching data via query: `{$query}`"; 120 | Yii::beginProfile($profileToken); 121 | 122 | try { 123 | $result = static::getSearchable()->search(static::class, $query, $mode, $config); 124 | 125 | return $result['ids']; 126 | } finally { 127 | 128 | Yii::endProfile($profileToken); 129 | } 130 | } 131 | 132 | /** 133 | * Delete all instances of the model from the search index. 134 | * 135 | * @throws \yii\base\InvalidConfigException 136 | */ 137 | public static function deleteAllFromSearch(): void 138 | { 139 | static::getSearchable()->deleteAllFromSearch(static::class); 140 | } 141 | 142 | /** 143 | * Enable search syncing for this model class. 144 | */ 145 | public static function enableSearchSyncing(): void 146 | { 147 | SearchableBehavior::enableSyncingFor(static::class); 148 | } 149 | 150 | /** 151 | * Disable search syncing for this model class. 152 | */ 153 | public static function disableSearchSyncing(): void 154 | { 155 | SearchableBehavior::disableSyncingFor(static::class); 156 | } 157 | 158 | /** 159 | * Temporarily disable search syncing for the given callback. 160 | * 161 | * @param callable $callback will be call without syncing mode. 162 | * @return mixed value of $callback. 163 | */ 164 | public static function withoutSyncingToSearch($callback) 165 | { 166 | static::disableSearchSyncing(); 167 | 168 | try { 169 | return $callback(); 170 | } finally { 171 | static::enableSearchSyncing(); 172 | } 173 | } 174 | 175 | /** 176 | * Make given model searchable. 177 | * 178 | * @param \yii\db\ActiveRecord|\yii\db\ActiveRecord[]|static|static[] $models add to searchable index data. 179 | * @throws \TeamTNT\TNTSearch\Exceptions\IndexNotFoundException 180 | * @throws \yii\base\InvalidConfigException 181 | */ 182 | public static function makeSearchable($models): void 183 | { 184 | static::getSearchable()->queueMakeSearchable($models); 185 | } 186 | 187 | /** 188 | * Delete given model searchable. 189 | * 190 | * @param \yii\db\ActiveRecord|\yii\db\ActiveRecord[]|static|static[] $models delete from searchable index data. 191 | * @throws \TeamTNT\TNTSearch\Exceptions\IndexNotFoundException 192 | * @throws \yii\base\InvalidConfigException 193 | */ 194 | public static function deleteSearchable($models): void 195 | { 196 | static::getSearchable()->queueDeleteFromSearch($models); 197 | } 198 | 199 | /** 200 | * Make all instances of the model searchable. 201 | * 202 | * @throws \TeamTNT\TNTSearch\Exceptions\IndexNotFoundException 203 | * @throws \yii\base\InvalidConfigException 204 | */ 205 | public static function makeAllSearchable(): void 206 | { 207 | foreach (static::find()->orderBy(static::searchableKey())->batch() as $models) { 208 | static::makeSearchable($models); 209 | } 210 | } 211 | 212 | /** 213 | * Get the index name for the model. 214 | * 215 | * @return string the name of an index. 216 | */ 217 | public static function searchableIndex(): string 218 | { 219 | return static::getDb()->quoteSql(static::tableName()); 220 | } 221 | 222 | /** 223 | * Get the indexable data array for the model. 224 | * 225 | * @return array ['field' => 'value'] or ['field alias' => 'value']. 226 | */ 227 | public function toSearchableArray(): array 228 | { 229 | return $this->toArray(); 230 | } 231 | 232 | /** 233 | * Get searchable key by default primary key will be use. 234 | * 235 | * @return string key name. 236 | */ 237 | public static function searchableKey(): string 238 | { 239 | return current(static::primaryKey()); 240 | } 241 | 242 | /** 243 | * Determine if the model should be searchable. 244 | * 245 | * @return bool weather instance should be insert to searchable index data. 246 | */ 247 | public function shouldBeSearchable(): bool 248 | { 249 | return true; 250 | } 251 | 252 | /** 253 | * Make the given model instance searchable. 254 | * 255 | * @throws \TeamTNT\TNTSearch\Exceptions\IndexNotFoundException 256 | * @throws \yii\base\InvalidConfigException 257 | */ 258 | public function searchable(): void 259 | { 260 | static::makeSearchable($this); 261 | } 262 | 263 | /** 264 | * Remove the given model instance from the search index. 265 | * 266 | * @throws \TeamTNT\TNTSearch\Exceptions\IndexNotFoundException 267 | * @throws \yii\base\InvalidConfigException 268 | */ 269 | public function unsearchable(): void 270 | { 271 | static::deleteSearchable($this); 272 | } 273 | 274 | /** 275 | * Get searchable key value by default the primary key will be use. 276 | * 277 | * @param bool $asArray weather return an array have a key is a searchable key and value is an value of key or only value. 278 | * @return string|int|string[]|int[] value of an searchable key. 279 | * @throws Exception 280 | */ 281 | public function getSearchableKey(bool $asArray = false) 282 | { 283 | $key = static::searchableKey(); 284 | 285 | if ($asArray) { 286 | return [$key => $this->$key]; 287 | } else { 288 | return $this->$key; 289 | } 290 | } 291 | 292 | } 293 | -------------------------------------------------------------------------------- /src/TNTSearch.php: -------------------------------------------------------------------------------- 1 | 20 | * @since 1.0.0 21 | */ 22 | class TNTSearch extends BaseTNTSearch implements Configurable 23 | { 24 | /** 25 | * @inheritDoc 26 | */ 27 | public function __construct(array $config = []) 28 | { 29 | if (!empty($config)) { 30 | Yii::configure($this, $config); 31 | } 32 | 33 | parent::__construct(); 34 | } 35 | 36 | } 37 | -------------------------------------------------------------------------------- /src/console/CommandController.php: -------------------------------------------------------------------------------- 1 | 17 | * @since 1.0.0 18 | */ 19 | class CommandController extends Controller 20 | { 21 | /** 22 | * @var string|null models class name separate by `,`. 23 | */ 24 | public $models; 25 | 26 | /** 27 | * @inheritDoc 28 | */ 29 | public function options($actionID) 30 | { 31 | return array_merge(parent::options($actionID), ['models']); 32 | } 33 | 34 | /** 35 | * @inheritDoc 36 | */ 37 | public function beforeAction($action) 38 | { 39 | $result = parent::beforeAction($action); 40 | 41 | if ($this->models === null) { 42 | throw new InvalidArgumentException('`models` options must be set!'); 43 | } 44 | 45 | return $result; 46 | } 47 | 48 | /** 49 | * Import the given models into the search index. 50 | */ 51 | public function actionImport() 52 | { 53 | $models = explode(',', $this->models); 54 | $models = array_filter($models); 55 | 56 | foreach ($models as $model) { 57 | $model = trim($model); 58 | $model::makeAllSearchable(); 59 | 60 | $this->stdout('All [' . $model . '] records have been imported.'); 61 | } 62 | } 63 | 64 | /** 65 | * Delete the model class records from the index. 66 | */ 67 | public function actionDelete() 68 | { 69 | $models = explode(',', $this->models); 70 | $models = array_filter($models); 71 | 72 | foreach ($models as $model) { 73 | $model = trim($model); 74 | $model::deleteAllFromSearch(); 75 | 76 | $this->stdout('All [' . $model . '] records have been deleted.'); 77 | } 78 | } 79 | 80 | 81 | } 82 | -------------------------------------------------------------------------------- /src/expression/Condition.php: -------------------------------------------------------------------------------- 1 | 17 | * @since 1.0.0 18 | */ 19 | class Condition extends Expression 20 | { 21 | 22 | /** 23 | * @inheritDoc 24 | */ 25 | public function getExpression(): ExpressionInterface 26 | { 27 | return new InCondition($this->searchableKey(), 'IN', $this->ids); 28 | } 29 | 30 | } 31 | -------------------------------------------------------------------------------- /src/expression/ConditionBuilder.php: -------------------------------------------------------------------------------- 1 | 18 | * @since 1.0.0 19 | */ 20 | class ConditionBuilder implements ExpressionBuilderInterface 21 | { 22 | 23 | use ExpressionBuilderTrait; 24 | 25 | /** 26 | * @param ExpressionInterface|Condition $expression 27 | * @inheritDoc 28 | */ 29 | public function build(ExpressionInterface $expression, array &$params = []) 30 | { 31 | $condition = $expression->getExpression(); 32 | 33 | return $this->queryBuilder->buildCondition($condition, $params); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/expression/Expression.php: -------------------------------------------------------------------------------- 1 | 18 | * @since 1.0.0 19 | */ 20 | abstract class Expression extends BaseObject implements ExpressionInterface 21 | { 22 | /** 23 | * @var \yii\db\ActiveQuery 24 | */ 25 | public $query; 26 | 27 | /** 28 | * @var int[]|string[] 29 | */ 30 | public $ids = []; 31 | 32 | /** 33 | * @inheritDoc 34 | * @throws InvalidConfigException 35 | */ 36 | public function init() 37 | { 38 | if (empty($this->ids)) { 39 | throw new InvalidConfigException('`ids` property must be set to detect id instance!'); 40 | } 41 | 42 | if ($this->query === null) { 43 | throw new InvalidConfigException('`query` property must be set to create condition instance!'); 44 | } 45 | 46 | parent::init(); 47 | } 48 | 49 | /** 50 | * Creating an specific expression to apply to query. 51 | * 52 | * @return ExpressionInterface apply to query. 53 | */ 54 | abstract public function getExpression(): ExpressionInterface; 55 | 56 | /** 57 | * Get pretty searchable key via model with an alias of the table. 58 | * 59 | * @return string the searchable key name 60 | */ 61 | protected function searchableKey(): string 62 | { 63 | /** @var \yii\db\ActiveRecord $modelClass */ 64 | $modelClass = $this->query->modelClass; 65 | list(, $alias) = $this->getTableNameAndAlias(); 66 | 67 | return '{{' . $alias . '}}.[[' . $modelClass::searchableKey() . ']]'; 68 | } 69 | 70 | /** 71 | * Returns the table name and the table alias for [[query::modelClass]]. 72 | * This method extract from \yii\db\ActiveQuery. 73 | * 74 | * @return array the table name and the table alias. 75 | */ 76 | private function getTableNameAndAlias(): array 77 | { 78 | /** @var \yii\db\ActiveRecord $modelClass */ 79 | $query = $this->query; 80 | $modelClass = $query->modelClass; 81 | 82 | if (empty($query->from)) { 83 | $tableName = $modelClass::tableName(); 84 | } else { 85 | $tableName = ''; 86 | // if the first entry in "from" is an alias-tablename-pair return it directly 87 | foreach ($query->from as $alias => $tableName) { 88 | if (is_string($alias)) { 89 | return [$tableName, $alias]; 90 | } 91 | break; 92 | } 93 | } 94 | 95 | if (preg_match('/^(.*?)\s+({{\w+}}|\w+)$/', $tableName, $matches)) { 96 | $alias = $matches[2]; 97 | } else { 98 | $alias = $tableName; 99 | } 100 | 101 | return [$tableName, $alias]; 102 | } 103 | 104 | } 105 | -------------------------------------------------------------------------------- /src/expression/OrderBy.php: -------------------------------------------------------------------------------- 1 | 17 | * @since 1.0.0 18 | */ 19 | class OrderBy extends Expression 20 | { 21 | 22 | /** 23 | * @inheritDoc 24 | * @return ExpressionInterface|OrderBy 25 | */ 26 | public function getExpression(): ExpressionInterface 27 | { 28 | $position = 1; 29 | $cases = ['CASE']; 30 | $params = []; 31 | $searchableKey = $this->searchableKey(); 32 | 33 | foreach ($this->ids as $id) { 34 | $paramName = ":sob{$position}"; 35 | $cases[] = "WHEN {$searchableKey} = {$paramName} THEN {$position}"; 36 | $params[$paramName] = $id; 37 | $position++; 38 | } 39 | 40 | $cases[] = "ELSE {$position}"; 41 | $cases[] = 'END ASC'; 42 | 43 | return new DbExpression(implode(' ', $cases), $params); 44 | } 45 | 46 | 47 | } 48 | -------------------------------------------------------------------------------- /src/expression/OrderByBuilder.php: -------------------------------------------------------------------------------- 1 | 18 | * @since 1.0.0 19 | */ 20 | class OrderByBuilder implements ExpressionBuilderInterface 21 | { 22 | 23 | use ExpressionBuilderTrait; 24 | 25 | /** 26 | * @param ExpressionInterface|OrderBy $expression 27 | * @inheritDoc 28 | */ 29 | public function build(ExpressionInterface $expression, array &$params = []) 30 | { 31 | $orderBy = $expression->query->orderBy; 32 | 33 | if ($orderBy[0] === $expression && count($orderBy) === 1) { 34 | 35 | return $this->queryBuilder->buildExpression($expression->getExpression()); 36 | } else { // user choice 37 | 38 | return '(SELECT NULL)'; 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/queue/DeleteSearchable.php: -------------------------------------------------------------------------------- 1 | 14 | * @since 1.0.0 15 | */ 16 | class DeleteSearchable extends Job 17 | { 18 | 19 | /** 20 | * @inheritDoc 21 | */ 22 | protected function resolve(array $models): void 23 | { 24 | if (!empty($models)) { 25 | 26 | $modelClass = get_class(current($models)); 27 | $modelClass::getSearchable()->delete($models); 28 | } 29 | } 30 | 31 | } 32 | -------------------------------------------------------------------------------- /src/queue/Job.php: -------------------------------------------------------------------------------- 1 | 16 | * @since 1.0.0 17 | */ 18 | abstract class Job implements JobInterface 19 | { 20 | 21 | /** 22 | * @var array primary key for invoke records. 23 | */ 24 | protected $ids = []; 25 | 26 | /** 27 | * QueueJob constructor. 28 | * 29 | * @param \yii\db\ActiveRecord|\yii\db\ActiveRecord[] $models need to making searchable index data. 30 | */ 31 | public function __construct($models) 32 | { 33 | $models = is_array($models) ? $models : [$models]; 34 | 35 | foreach ($models as $model) { 36 | /** @var $model \yii\db\ActiveRecord */ 37 | foreach ($model->getPrimaryKey(true) as $key => $value) { 38 | $this->ids[$key][] = $value; 39 | } 40 | } 41 | } 42 | 43 | /** 44 | * @inheritDoc 45 | */ 46 | public function execute($queue) 47 | { 48 | if (!empty($this->ids)) { 49 | /** @var \yii\db\ActiveRecord $modelClass */ 50 | $models = $modelClass::findAll($this->ids); 51 | $this->resolve($models); 52 | } 53 | } 54 | 55 | /** 56 | * Solve models job. 57 | * 58 | * @param array|\yii\db\ActiveRecord[] $models need to be execute searchable index job. 59 | */ 60 | abstract protected function resolve(array $models): void; 61 | } 62 | -------------------------------------------------------------------------------- /src/queue/MakeSearchable.php: -------------------------------------------------------------------------------- 1 | 14 | * @since 1.0.0 15 | */ 16 | class MakeSearchable extends Job 17 | { 18 | 19 | /** 20 | * @inheritDoc 21 | */ 22 | protected function resolve(array $models): void 23 | { 24 | if (!empty($models)) { 25 | 26 | $modelClass = get_class(current($models)); 27 | $modelClass::getSearchable()->upsert($models); 28 | } 29 | } 30 | 31 | } 32 | --------------------------------------------------------------------------------