├── .gitignore
├── src
├── helpers.php
├── Tokenizers
│ ├── JiebaTokenizer.php
│ ├── ScwsTokenizer.php
│ ├── Tokenizer.php
│ └── PhpAnalysisTokenizer.php
├── Highlighter.php
├── LumenServiceProvider.php
├── TNTSearchScoutServiceProvider.php
├── Console
│ └── ImportCommand.php
└── Engines
│ └── TNTSearchEngine.php
├── phpunit.xml
├── LICENSE.txt
├── config
└── tntsearch.php
├── composer.json
├── tests
└── TNTSearchEngineTest.php
└── README.md
/.gitignore:
--------------------------------------------------------------------------------
1 | /vendor
2 | composer.lock
3 |
--------------------------------------------------------------------------------
/src/helpers.php:
--------------------------------------------------------------------------------
1 | highlight($text, $query, $tag);
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/phpunit.xml:
--------------------------------------------------------------------------------
1 |
2 |
13 |
14 |
15 | ./tests/
16 |
17 |
18 |
--------------------------------------------------------------------------------
/src/Tokenizers/JiebaTokenizer.php:
--------------------------------------------------------------------------------
1 | getConfig('jieba');
13 |
14 | Jieba::init($config);
15 |
16 | if (isset($config['user_dict'])) {
17 | Jieba::loadUserDict($config['user_dict']);
18 | }
19 |
20 | Finalseg::init($config);
21 | }
22 |
23 | public function getTokens($text)
24 | {
25 | return Jieba::cutForSearch($text);
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/src/Tokenizers/ScwsTokenizer.php:
--------------------------------------------------------------------------------
1 | scws = new Scws($this->getConfig('scws'));
14 | }
15 |
16 | public function getTokens($text)
17 | {
18 | $this->scws->sendText($text);
19 |
20 | $tokens = [];
21 |
22 | while ($result = $this->scws->getResult()) {
23 | $tokens = array_merge($tokens, array_column($result, 'word'));
24 | }
25 |
26 | return $tokens;
27 | }
28 |
29 | public function getScws()
30 | {
31 | return $this->scws;
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/src/Tokenizers/Tokenizer.php:
--------------------------------------------------------------------------------
1 | getTokens($text);
25 |
26 | $tokens = array_filter($tokens, 'trim');
27 |
28 | return array_diff($tokens, $stopwords);
29 | }
30 |
31 | abstract protected function getTokens($text);
32 | }
33 |
--------------------------------------------------------------------------------
/src/Highlighter.php:
--------------------------------------------------------------------------------
1 | tokenizer = $tokenizer ?: new Tokenizer;
15 | }
16 |
17 | public function highlight($text, $query, $tag = 'em')
18 | {
19 | $terms = $this->tokenizer->tokenize($query);
20 |
21 | $patterns = array_map(function ($term) {
22 | return "/{$term}/is";
23 | }, array_unique($terms));
24 |
25 | $replacement = "<{$tag}>\$0{$tag}>";
26 |
27 | return preg_replace($patterns, $replacement, $text);
28 | }
29 |
30 | public function getTokenizer()
31 | {
32 | return $this->tokenizer;
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/src/LumenServiceProvider.php:
--------------------------------------------------------------------------------
1 | app->configure('scout');
17 | $this->app->configure('tntsearch');
18 |
19 | $this->mergeConfigFrom(__DIR__.'/../config/tntsearch.php', 'tntsearch');
20 |
21 | $this->app->instance('path.config', $this->app->configPath());
22 | $this->app->singleton(TokenizerInterface::class, $this->getConfig()['tokenizer']);
23 |
24 | if ($this->app->runningInConsole()) {
25 | $this->commands(ImportCommand::class);
26 | }
27 | }
28 |
29 | /**
30 | * Bootstrap any application services.
31 | *
32 | * @return void
33 | */
34 | public function boot()
35 | {
36 | $this->app->make('view');
37 |
38 | parent::boot();
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/src/Tokenizers/PhpAnalysisTokenizer.php:
--------------------------------------------------------------------------------
1 | analysis = new Phpanalysis;
15 |
16 | foreach ($this->getConfig('phpanalysis') as $key => $value) {
17 | $key = Str::camel($key);
18 |
19 | if (property_exists($this->analysis, $key)) {
20 | $this->analysis->$key = $value;
21 | }
22 | }
23 | }
24 |
25 | public function getTokens($text)
26 | {
27 | $this->analysis->SetSource($text);
28 |
29 | $this->analysis->StartAnalysis();
30 |
31 | $result = $this->analysis->GetFinallyResult();
32 |
33 | $result = str_replace(['(', ')'], '', trim($result));
34 |
35 | return explode(' ', $result);
36 | }
37 |
38 | public function getAnalysis()
39 | {
40 | return $this->analysis;
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/LICENSE.txt:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c)
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in
13 | all copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21 | THE SOFTWARE.
22 |
--------------------------------------------------------------------------------
/config/tntsearch.php:
--------------------------------------------------------------------------------
1 | env('TNTSEARCH_TOKENIZER', 'phpanalysis'),
6 |
7 | 'storage' => storage_path('indices'),
8 |
9 | 'stemmer' => TeamTNT\TNTSearch\Stemmer\NoStemmer::class,
10 |
11 | 'tokenizers' => [
12 | 'phpanalysis' => [
13 | 'driver' => Vanry\Scout\Tokenizers\PhpAnalysisTokenizer::class,
14 | 'to_lower' => true,
15 | 'unit_word' => true,
16 | 'differ_max' => true,
17 | 'result_type' => 2,
18 | ],
19 |
20 | 'jieba' => [
21 | 'driver' => Vanry\Scout\Tokenizers\JiebaTokenizer::class,
22 | 'dict' => 'small',
23 | //'user_dict' => resource_path('dicts/mydict.txt'),
24 | ],
25 |
26 | 'scws' => [
27 | 'driver' => Vanry\Scout\Tokenizers\ScwsTokenizer::class,
28 | 'multi' => 1,
29 | 'ignore' => true,
30 | 'duality' => false,
31 | 'charset' => 'utf-8',
32 | 'dict' => '/usr/local/scws/etc/dict.utf8.xdb',
33 | 'rule' => '/usr/local/scws/etc/rules.utf8.ini',
34 | ],
35 | ],
36 |
37 | 'stopwords' => [
38 | //
39 | ],
40 |
41 | ];
42 |
--------------------------------------------------------------------------------
/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "vanry/laravel-scout-tntsearch",
3 | "description": "包含中文分词的 Laravel Scout TNTSearch 驱动,支持 scws, phpanalysis 和 jieba 分词。",
4 | "keywords": ["tntsearch", "search", "scout", "laravel"],
5 | "license": "MIT",
6 | "type": "library",
7 | "support": {
8 | "issues": "https://github.com/vanry/laravel-scout-tntsearch/issues",
9 | "source": "https://github.com/vanry/laravel-scout-tntsearch"
10 | },
11 | "authors": [
12 | {
13 | "name": "vanry",
14 | "email": "ivanry@163.com"
15 | }
16 | ],
17 | "require": {
18 | "php": "^7.0|^8.0",
19 | "illuminate/console": "~5.5|^6.0|^7.0|^8.0",
20 | "illuminate/database": "~5.5|^6.0|^7.0|^8.0",
21 | "illuminate/support": "~5.5|^6.0|^7.0|^8.0",
22 | "illuminate/view": "~5.5|^6.0|^7.0|^8.0",
23 | "laravel/scout": "^7.0|^8.0|^9.0",
24 | "lmz/phpanalysis": "^1.0",
25 | "teamtnt/tntsearch": "^2.0"
26 | },
27 | "require-dev": {
28 | "mockery/mockery": "~0.9",
29 | "phpunit/phpunit": "~5.0"
30 | },
31 | "autoload": {
32 | "psr-4": {
33 | "Vanry\\Scout\\": "src"
34 | },
35 | "files": [
36 | "src/helpers.php"
37 | ]
38 | },
39 | "autoload-dev": {
40 | "psr-4": {
41 | "Tests\\": "tests/"
42 | }
43 | },
44 | "extra": {
45 | "branch-alias": {
46 | "dev-master": "2.0-dev"
47 | },
48 | "laravel": {
49 | "providers": [
50 | "Vanry\\Scout\\TNTSearchScoutServiceProvider"
51 | ]
52 | }
53 | },
54 | "suggest": {
55 | "fukuball/jieba-php": "Required to use the Jieba tokenizer (~0.25).",
56 | "vanry/scws": "Required to use the SCWS tokenizer (^2.0)."
57 | },
58 | "config": {
59 | "sort-packages": true
60 | },
61 | "minimum-stability": "dev"
62 | }
63 |
--------------------------------------------------------------------------------
/tests/TNTSearchEngineTest.php:
--------------------------------------------------------------------------------
1 | shouldReceive('createIndex')
17 | ->with('table.index')
18 | ->andReturn($index = Mockery::mock('TeamTNT\TNTSearch\Indexer\TNTIndexer'));
19 | $index->shouldReceive('setDatabaseHandle');
20 | $index->shouldReceive('setPrimaryKey');
21 | $index->shouldReceive('query');
22 | $index->shouldReceive('run');
23 |
24 | $client->shouldReceive('selectIndex');
25 | $client->shouldReceive('getIndex')
26 | ->andReturn($index);
27 |
28 | $index->shouldReceive('indexBeginTransaction');
29 | $index->shouldReceive('update');
30 | $index->shouldReceive('indexEndTransaction');
31 |
32 | $engine = new TNTSearchEngine($client);
33 | $engine->update(Collection::make([new TNTSearchEngineTestModel()]));
34 | }
35 | }
36 |
37 | class TNTSearchEngineTestModel
38 | {
39 | public $searchable = ['title'];
40 |
41 | public function searchableAs()
42 | {
43 | return 'table';
44 | }
45 |
46 | public function getTable()
47 | {
48 | return 'table';
49 | }
50 |
51 | public function getTablePrefix()
52 | {
53 | return "";
54 | }
55 |
56 | public function getKey()
57 | {
58 | return 1;
59 | }
60 |
61 | public function getKeyName()
62 | {
63 | return 'id';
64 | }
65 |
66 | public function toSearchableArray()
67 | {
68 | return ['id' => 1];
69 | }
70 |
71 | public function getConnection()
72 | {
73 | $connection = Mockery::mock('Illuminate\Database\MySqlConnection');
74 | $connection->shouldReceive('getPdo')->andReturn(Mockery::mock('PDO'));
75 |
76 | return $connection;
77 | }
78 | }
79 |
--------------------------------------------------------------------------------
/src/TNTSearchScoutServiceProvider.php:
--------------------------------------------------------------------------------
1 | app->runningInConsole()) {
24 | $this->commands(ImportCommand::class);
25 |
26 | $this->publishes([
27 | __DIR__.'/../config/tntsearch.php' => config_path('tntsearch.php'),
28 | ]);
29 | }
30 |
31 | $this->mergeConfigFrom(__DIR__.'/../config/tntsearch.php', 'tntsearch');
32 |
33 | $this->app->singleton(TokenizerInterface::class, $this->getConfig()['tokenizer']);
34 | }
35 |
36 | /**
37 | * Bootstrap any application services.
38 | *
39 | * @return void
40 | */
41 | public function boot()
42 | {
43 | $this->app[EngineManager::class]->extend('tntsearch', function ($app) {
44 | $tnt = new TNTSearch;
45 |
46 | $tnt->loadConfig($this->getConfig());
47 | $tnt->setDatabaseHandle(app('db')->connection()->getPdo());
48 |
49 | return new TNTSearchEngine($tnt);
50 | });
51 |
52 | Blade::directive('highlight', function ($expression) {
53 | $segments = array_map('trim', explode(',', $expression));
54 |
55 | list($text, $query) = $segments;
56 |
57 | $tag = $segments[2] ?? "'em'";
58 |
59 | return "highlight($text, $query, $tag); ?>";
60 | });
61 | }
62 |
63 | protected function getConfig()
64 | {
65 | $driver = config('database.default');
66 |
67 | $config = config('tntsearch') + config("database.connections.{$driver}");
68 |
69 | if (! array_key_exists($config['default'], $config['tokenizers'])) {
70 | throw new InvalidArgumentException("Tokenizer [{$config['default']}] is not defined.");
71 | }
72 |
73 | return array_merge($config, ['tokenizer' => $config['tokenizers'][$config['default']]['driver']]);
74 | }
75 | }
76 |
--------------------------------------------------------------------------------
/src/Console/ImportCommand.php:
--------------------------------------------------------------------------------
1 | argument('model');
33 |
34 | $model = new $class;
35 |
36 | $index = $this->initIndex($model);
37 |
38 | $index->query($this->getSql($model));
39 |
40 | $index->run();
41 |
42 | $this->info("All [{$class}] records have been imported.");
43 | }
44 |
45 | protected function initIndex($model)
46 | {
47 | $tnt = new TNTSearch;
48 |
49 | $tnt->loadConfig($this->getConfig($model));
50 |
51 | $tnt->setDatabaseHandle($model->getConnection()->getPdo());
52 |
53 | if (! file_exists($tnt->config['storage'])) {
54 | mkdir($tnt->config['storage'], 0777, true);
55 | }
56 |
57 | $index = $tnt->createIndex("{$model->searchableAs()}.index");
58 |
59 | $index->inMemory = false;
60 |
61 | $index->setPrimaryKey($model->getKeyName());
62 |
63 | return $index;
64 | }
65 |
66 | protected function getConfig($model)
67 | {
68 | $driver = $model->getConnectionName() ?: config('database.default');
69 |
70 | $config = config('tntsearch') + config("database.connections.{$driver}");
71 |
72 | if (! array_key_exists($config['default'], $config['tokenizers'])) {
73 | throw new InvalidArgumentException("Tokenizer [{$config['default']}] is not defined.");
74 | }
75 |
76 | return array_merge($config, ['tokenizer' => $config['tokenizers'][$config['default']]['driver']]);
77 | }
78 |
79 | protected function getSql($model)
80 | {
81 | $query = $model->newQueryWithoutScopes();
82 |
83 | if ($fields = $this->getSearchableFields($model)) {
84 | $query->select($model->getKeyName())->addSelect($fields);
85 | }
86 |
87 | return $query->toSql();
88 | }
89 |
90 | protected function getSearchableFields($model)
91 | {
92 | $availableColumns = $model->getConnection()->getSchemaBuilder()->getColumnListing($model->getTable());
93 |
94 | $desiredColumns = array_keys($model->toSearchableArray());
95 |
96 | return array_intersect($desiredColumns, $availableColumns);
97 | }
98 | }
99 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 | > 说明: `2.x` 版本只支持 `Laravel 5.5` 以上版本,`Laravel 5.5`以下版本请使用 [1.x版本](https://github.com/vanry/laravel-scout-tntsearch/tree/1.x)。
3 |
4 | ## 安装
5 |
6 | > 需安装并开启 `sqlite` 扩展
7 |
8 | ``` bash
9 | composer require vanry/laravel-scout-tntsearch
10 | ```
11 |
12 | ### Laravel
13 |
14 | * 发布 `scout` 配置文件,已安装 `scout` 可省略。
15 |
16 | ```bash
17 | php artisan vendor:publish --provider="Laravel\Scout\ScoutServiceProvider"
18 | ```
19 |
20 | * 如需修改 `tntsearch` 默认配置,发布 `tntsearch` 配置文件。
21 |
22 | ```bash
23 | php artisan vendor:publish --provider="Vanry\Scout\TNTSearchScoutServiceProvider"
24 | ```
25 |
26 | ### Lumen
27 |
28 | `Lumen` 需将服务提供者添加到 `bootstrap/app.php`
29 |
30 | ```php
31 | // bootstrap/app.php
32 |
33 | // 取消注释
34 | $app->withFacades();
35 | $app->withEloquent()
36 |
37 | // 注意先后顺序
38 | $app->register(Vanry\Scout\LumenServiceProvider::class);
39 | $app->register(Laravel\Scout\ScoutServiceProvider::class);
40 | ```
41 |
42 | 在根目录中创建 `config` 文件夹, 将 `laravel scout` 配置文件 `scout.php` 复制到 `config` 中。
43 |
44 | 如需修改 `tntsearch` 默认配置,则将配置文件 `tntsearch.php` 复制 `config` 中进行修改。
45 |
46 | ### 启用
47 |
48 | 在 `.env` 文件中添加
49 |
50 | ```bash
51 | SCOUT_DRIVER=tntsearch
52 | ```
53 |
54 | ## 用法
55 |
56 | 1. 模型添加 `Searchable Trait`
57 |
58 | ```php
59 | namespace App;
60 |
61 | use Illuminate\Database\Eloquent\Model;
62 | use Laravel\Scout\Searchable;
63 |
64 | class Post extends Model
65 | {
66 | use Searchable;
67 |
68 | /**
69 | * Get the indexable data array for the model.
70 | *
71 | * @return array
72 | */
73 | public function toSearchableArray()
74 | {
75 | return [
76 | 'id' => $this->id,
77 | 'title' => $this->title,
78 | 'body' => strip_tags($this->body),
79 | ];
80 | }
81 | }
82 | ```
83 |
84 | 2. 导入模型 创建索引
85 |
86 | ```php
87 | # scout 命令
88 | php artisan scout:import 'App\Post'
89 |
90 | # tntsearch 命令, 性能更好
91 | php artisan tntsearch:import 'App\Post'
92 | ```
93 |
94 | 3. 使用索引进行搜索
95 |
96 | ```php
97 | Post::search('laravel教程')->get();
98 | ```
99 |
100 |
101 | ## 中文分词
102 |
103 | 目前支持 `scws`, `jieba` 和 `phpanalysis` 中文分词,默认使用 `phpanalysis` 分词。
104 |
105 | ### 对比
106 |
107 | * `scws` 是用 `C` 语言编写的 `php` 扩展,性能最好,分词效果好,但不支持 `Windows` 系统。
108 | * `jieba` 为 `python` 版本结巴分词的 `php` 实现,分词效果最好,尤其是新词发现,不足之处是性能较差,占用内存大。
109 | * `phpanalysis` 是 `php` 编写的一款轻量分词器,分词效果不错,性能介于 `scws` 和 `jieba` 两者之间。
110 |
111 | ### 安装
112 |
113 | 使用 `scws` 或者 `jieba`,需安装对应的分词驱动。
114 |
115 | - **scws**
116 |
117 | ```bash
118 | composer require vanry/scws
119 | ```
120 |
121 | - **jieba**
122 |
123 | ```bash
124 | composer require fukuball/jieba-php
125 | ```
126 |
127 | 在 `.env` 文件中配置
128 |
129 | ```bash
130 | # scws
131 | TNTSEARCH_TOKENIZER=scws
132 |
133 | # jieba
134 | TNTSEARCH_TOKENIZER=jieba
135 | ```
136 |
137 | ### 问题
138 |
139 | 使用 `jieba` 分词可能会出现内存分配不足的错误信息:
140 |
141 | > PHP Fatal error: Allowed memory size of 134217728 bytes exhausted (tried to allocate 20480 bytes)
142 |
143 | 在代码中增加内存限制即可
144 |
145 | ```php
146 | ini_set('memory_limit', '1024M');
147 | ```
148 |
149 | ## 高亮
150 |
151 | 默认使用 `em` 作为高亮 `html` 标签,在 `css` 中设置高亮样式即可,也可以自定义高亮标签。
152 |
153 | * **@highlight 指令**
154 |
155 | ```
156 | @highlight($text, $query, $tag);
157 | ```
158 | > * $text: 要高亮的字段
159 | > * $query: 搜索词
160 | > * $tag: 高亮的 html 标签
161 |
162 | ```php
163 | // 高亮 title 字段
164 | @highlight($post->title, $query);
165 |
166 | // 用 strong 作为高亮标签
167 | @highlight($post->title, $query, 'strong');
168 | ```
169 |
170 | * **highlight 帮助函数**
171 |
172 | ```php
173 | highlight($post->title, $query);
174 |
175 | highlight($post->title, $query, 'strong');
176 | ```
177 |
178 | * **Highlighter 对象**
179 |
180 | ```php
181 | use Vanry\Scout\Highlighter;
182 |
183 | // ...
184 |
185 | app(Highlighter::class)->highlight($post->title, $query);
186 | ```
187 |
188 | > `highlight` 帮助函数和 `Highlighter` 对象适合在 `api` 等非 `html` 视图中使用。
189 |
--------------------------------------------------------------------------------
/src/Engines/TNTSearchEngine.php:
--------------------------------------------------------------------------------
1 | tnt = $tnt;
39 | }
40 |
41 | /**
42 | * Update the given model in the index.
43 | *
44 | * @param \Illuminate\Database\Eloquent\Collection $models
45 | * @return void
46 | */
47 | public function update($models)
48 | {
49 | $model = $models->first();
50 |
51 | $index = $this->initIndex($model->searchableAs());
52 |
53 | $index->setPrimaryKey($model->getKeyName());
54 | $index->setStopWords($this->tnt->config['stopwords'] ?? []);
55 |
56 | $index->indexBeginTransaction();
57 |
58 | $models->each(function ($model) use ($index) {
59 | $array = $model->toSearchableArray();
60 |
61 | if (empty($array)) {
62 | return;
63 | }
64 |
65 | if ($model->getKey()) {
66 | $index->update($model->getKey(), $array);
67 | } else {
68 | $index->insert($array);
69 | }
70 | });
71 |
72 | $index->indexEndTransaction();
73 | }
74 |
75 | /**
76 | * Remove the given model from the index.
77 | *
78 | * @param \Illuminate\Database\Eloquent\Collection $models
79 | * @return void
80 | */
81 | public function delete($models)
82 | {
83 | $index = $this->initIndex($models->first()->searchableAs());
84 |
85 | $models->each(function ($model) use ($index) {
86 | $index->delete($model->getKey());
87 | });
88 | }
89 |
90 | /**
91 | * Delete a search index.
92 | *
93 | * @param string $name
94 | * @return mixed
95 | */
96 | public function deleteIndex($name)
97 | {
98 | $indexPath = $this->indexPath($name);
99 |
100 | if (file_exists($indexPath)) {
101 | unlink($indexPath);
102 | }
103 | }
104 |
105 | /**
106 | * Perform the given search on the engine.
107 | *
108 | * @param \Laravel\Scout\Builder $builder
109 | * @return mixed
110 | */
111 | public function search(Builder $builder)
112 | {
113 | try {
114 | return $this->performSearch($builder);
115 | } catch (IndexNotFoundException $e) {
116 | $this->createIndex($builder->model);
117 |
118 | return $this->performSearch($builder);
119 | }
120 | }
121 |
122 | /**
123 | * Perform the given search on the engine.
124 | *
125 | * @param \Laravel\Scout\Builder $builder
126 | * @param int $perPage
127 | * @param int $page
128 | * @return mixed
129 | */
130 | public function paginate(Builder $builder, $perPage, $page)
131 | {
132 | $results = $this->search($builder);
133 |
134 | $results['hits'] = $this->getFilteredTotalCount($builder, $results);
135 |
136 | if ($builder->limit) {
137 | $results['hits'] = min($results['hits'], $builder->limit);
138 | }
139 |
140 | $chunks = array_chunk($results['ids'], $perPage);
141 |
142 | $results['ids'] = $chunks[$page - 1] ?? [];
143 |
144 | return $results;
145 | }
146 |
147 | protected function getFilteredTotalCount(Builder $builder, $results)
148 | {
149 | $this->builder = $builder;
150 |
151 | $model = $builder->model;
152 |
153 | $query = $model->whereIn($model->getQualifiedKeyName(), $results['ids']);
154 |
155 | if ($this->usesSoftDelete($model) && config('scout.soft_delete')) {
156 | $query = $this->handleSoftDelete($query);
157 | }
158 |
159 | return $this->applyWheres($query)->count();
160 | }
161 |
162 | /**
163 | * Perform the given search on the engine.
164 | *
165 | * @param Builder $builder
166 | * @return mixed
167 | */
168 | protected function performSearch(Builder $builder, array $options = [])
169 | {
170 | $index = $builder->index ?: $builder->model->searchableAs();
171 |
172 | $this->tnt->selectIndex("{$index}.index");
173 |
174 | if ($builder->callback) {
175 | return call_user_func($builder->callback, $this->tnt, $builder->query, $options);
176 | }
177 |
178 | return $this->tnt->search($builder->query, $builder->limit ?: static::LIMIT);
179 | }
180 |
181 | /**
182 | * Map the given results to instances of the given model.
183 | *
184 | * @param \Laravel\Scout\Builder $builder
185 | * @param mixed $results
186 | * @param \Illuminate\Database\Eloquent\Model $model
187 | * @return \Illuminate\Database\Eloquent\Collection
188 | */
189 | public function map(Builder $builder, $results, $model)
190 | {
191 | return (empty($results['ids'])) ? $model->newCollection() : $this->mapModels($builder, $results, $model);
192 | }
193 |
194 | /**
195 | * Map the given results to instances of the given model via a lazy collection.
196 | *
197 | * @param \Laravel\Scout\Builder $builder
198 | * @param mixed $results
199 | * @param \Illuminate\Database\Eloquent\Model $model
200 | * @return \Illuminate\Support\LazyCollection
201 | */
202 | public function lazyMap(Builder $builder, $results, $model)
203 | {
204 | return (empty($results['ids']))
205 | ? LazyCollection::make()
206 | : $this->mapModels($builder, $results, $model, function (Query $query) {
207 | return $query->cursor();
208 | });
209 | }
210 |
211 | /**
212 | * Map eloquent models with search results.
213 | *
214 | * @param \Laravel\Scout\Builder $builder
215 | * @param mixed $results
216 | * @param \Illuminate\Database\Eloquent\Model $model
217 | * @param callable $callback
218 | * @return \Illuminate\Database\Eloquent\Collection
219 | */
220 | protected function mapModels(Builder $builder, $results, $model, callable $callback = null)
221 | {
222 | $this->builder = $builder;
223 |
224 | $query = $model->whereIn($model->getQualifiedKeyName(), $results['ids']);
225 |
226 | if ($this->usesSoftDelete($model) && config('scout.soft_delete')) {
227 | $query = $this->handleSoftDelete($query);
228 | }
229 |
230 | $query = $this->applyWheres($query);
231 |
232 | $query = $this->applyOrders($query);
233 |
234 | $query = $this->applySelect($query);
235 |
236 | $models = is_null($callback) ? $query->get() : $callback($query);
237 |
238 | return empty($this->builder->orders) ? $models->sortBy(function ($model) use ($results) {
239 | return array_search($model->getKey(), $results['ids']);
240 | })->values() : $models;
241 | }
242 |
243 | public $field = [];
244 | public function setSelectField(...$params){
245 | $this->field = $params;
246 | }
247 | protected function applySelect($query){
248 | if($this->field){
249 | $query->select($this->field);
250 | }
251 | return $query;
252 | }
253 |
254 |
255 | /**
256 | * Determine if the given model uses soft deletes.
257 | *
258 | * @param \Illuminate\Database\Eloquent\Model $model
259 | * @return bool
260 | */
261 | protected function usesSoftDelete($model)
262 | {
263 | return in_array(SoftDeletes::class, class_uses_recursive($model));
264 | }
265 |
266 | protected function handleSoftDelete(Query $query)
267 | {
268 | if (! array_key_exists('__soft_deleted', $this->builder->wheres)) {
269 | $query->withTrashed();
270 | } elseif ($this->builder->wheres['__soft_deleted'] == 1) {
271 | $query->onlyTrashed();
272 | }
273 |
274 | return $query;
275 | }
276 |
277 | protected function applyWheres(Query $query)
278 | {
279 | unset($this->builder->wheres['__soft_deleted']);
280 |
281 | return $query->where($this->builder->wheres);
282 | }
283 |
284 | protected function applyOrders(Query $query)
285 | {
286 | return array_reduce($this->builder->orders, function ($query, $order) {
287 | return $query->orderBy($order['column'], $order['direction']);
288 | }, $query);
289 | }
290 |
291 | /**
292 | * Pluck and return the primary keys of the given results.
293 | *
294 | * @param mixed $results
295 | * @return \Illuminate\Support\Collection
296 | */
297 | public function mapIds($results)
298 | {
299 | return collect($results['ids'])->values();
300 | }
301 |
302 | /**
303 | * Get the total count from a raw result returned by the engine.
304 | *
305 | * @param mixed $results
306 | * @return int
307 | */
308 | public function getTotalCount($results)
309 | {
310 | return $results['hits'];
311 | }
312 |
313 | protected function initIndex($name)
314 | {
315 | if (! file_exists($this->indexPath($name))) {
316 | $this->createIndex($name);
317 | }
318 |
319 | $this->tnt->selectIndex($this->indexName($name));
320 |
321 | return $this->tnt->getIndex();
322 | }
323 |
324 | protected function indexName($name)
325 | {
326 | return "{$name}.index";
327 | }
328 |
329 | protected function indexPath($name)
330 | {
331 | return $this->tnt->config['storage'].$this->indexName($name);
332 | }
333 |
334 | public function createIndex($name, array $options = [])
335 | {
336 | if (! file_exists($this->tnt->config['storage'])) {
337 | mkdir($this->tnt->config['storage'], 0755, true);
338 |
339 | file_put_contents($this->tnt->config['storage'].'.gitignore', "*\n!.gitignore\n");
340 | }
341 |
342 | $this->tnt->createIndex($this->indexName($name), $options['disableOutput'] ?? false);
343 | }
344 |
345 | /**
346 | * Flush all of the model's records from the engine.
347 | *
348 | * @param \Illuminate\Database\Eloquent\Model $model
349 | * @return void
350 | */
351 | public function flush($model)
352 | {
353 | $this->deleteIndex($model->searchableAs());
354 | }
355 | }
356 |
--------------------------------------------------------------------------------