├── .gitignore ├── CHANGELOG.md ├── src ├── Exception │ ├── OpensearchCallException.php │ ├── OpensearchRunException.php │ └── OpensearchException.php ├── Events │ ├── ModelsDeleted.php │ ├── ModelsUpdated.php │ └── DocSyncEvent.php ├── Sdk │ ├── readme.txt │ ├── License │ ├── CloudsearchSuggest.php │ ├── CloudsearchIndex.php │ ├── CloudsearchDoc.php │ ├── CloudsearchClient.php │ └── CloudsearchSearch.php ├── SearchableMethods.php ├── Helper │ └── Whenable.php ├── OpenSearchServiceProvider.php ├── Jobs │ ├── MakeSearchable.php │ ├── RemoveSearchable.php │ └── UpdateSearchable.php ├── Console │ └── FlushCommand.php ├── SearchableScope.php ├── Query │ ├── Grammar.php │ ├── QueryStructureBuilder.php │ └── Builder.php ├── OpenSearchClient.php ├── Searchable.php ├── ExtendedBuilder.php └── OpenSearchEngine.php ├── tests └── SearchTest.php ├── composer.json ├── phpunit.xml ├── LICENSE └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | /vendor 2 | /.phpintel 3 | /.idea 4 | /test.php 5 | composer.lock 6 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Release Notes 2 | 3 | ## v0.0.3 (2017-05-02) 4 | 5 | ### Added 6 | - 添加 `orderByRank` 方法,按排序分排序 7 | -------------------------------------------------------------------------------- /src/Exception/OpensearchCallException.php: -------------------------------------------------------------------------------- 1 | errors = $errors; 14 | } 15 | 16 | public function getErrors() 17 | { 18 | return $this->errors; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/SearchableMethods.php: -------------------------------------------------------------------------------- 1 | queueMakeSearchable(new Collection([$this])); 12 | } 13 | 14 | public function updateSearchable() 15 | { 16 | $this->queueUpdateSearchable(new Collection([$this])); 17 | } 18 | 19 | public function removeSearchable() 20 | { 21 | $this->queueRemoveFromSearch(new Collection([$this])); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /tests/SearchTest.php: -------------------------------------------------------------------------------- 1 | opensearchClient = new OpenSearchClient([ 12 | 'access_key_id' => '', 13 | 'access_key_secret' => '', 14 | 'debug' => true, 15 | ]); 16 | } 17 | 18 | public function test_something_work() 19 | { 20 | $this->assertTrue(true); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/Events/DocSyncEvent.php: -------------------------------------------------------------------------------- 1 | data = $data; 22 | $this->type = $type; 23 | $this->success = $success; 24 | $this->message = $message; 25 | $this->indexName = $indexName; 26 | $this->tableName = $tableName; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/Sdk/License: -------------------------------------------------------------------------------- 1 | Copyright 1999-2015 Alibaba Group Holding Ltd. 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. -------------------------------------------------------------------------------- /src/Helper/Whenable.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 13 | 14 | 15 | ./tests/ 16 | 17 | 18 | 19 | 20 | src/ 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /src/OpenSearchServiceProvider.php: -------------------------------------------------------------------------------- 1 | app->singleton(OpenSearchClient::class, function () { 19 | return new OpenSearchClient($this->app['config']->get('scout.opensearch')); 20 | }); 21 | 22 | $this->app[EngineManager::class]->extend('opensearch', function () { 23 | return new OpenSearchEngine($this->app[OpenSearchClient::class]); 24 | }); 25 | 26 | if ($this->app->runningInConsole()) { 27 | $this->commands([ 28 | FlushCommand::class, 29 | ]); 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/Jobs/MakeSearchable.php: -------------------------------------------------------------------------------- 1 | models = $models; 29 | } 30 | 31 | /** 32 | * Handle the job. 33 | * 34 | * @return void 35 | */ 36 | public function handle() 37 | { 38 | if (count($this->models) === 0) { 39 | return; 40 | } 41 | 42 | $this->models->first()->searchableUsing()->add($this->models); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/Jobs/RemoveSearchable.php: -------------------------------------------------------------------------------- 1 | models = $models; 29 | } 30 | 31 | /** 32 | * Handle the job. 33 | * 34 | * @return void 35 | */ 36 | public function handle() 37 | { 38 | if (count($this->models) === 0) { 39 | return; 40 | } 41 | 42 | $this->models->first()->searchableUsing()->remove($this->models); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/Jobs/UpdateSearchable.php: -------------------------------------------------------------------------------- 1 | models = $models; 29 | } 30 | 31 | /** 32 | * Handle the job. 33 | * 34 | * @return void 35 | */ 36 | public function handle() 37 | { 38 | if (count($this->models) === 0) { 39 | return; 40 | } 41 | 42 | $this->models->first()->searchableUsing()->update($this->models); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 lingxi 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 all 13 | 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 THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/Console/FlushCommand.php: -------------------------------------------------------------------------------- 1 | argument('model'); 28 | 29 | $model = new $class; 30 | 31 | $events->listen(ModelsDeleted::class, function ($event) use ($class) { 32 | $key = $event->models->last()->getKey(); 33 | 34 | $this->line('Deleted ['.$class.'] models up to ID: '.$key); 35 | }); 36 | 37 | $model::removeAllFromSearch(); 38 | 39 | $this->info('All ['.$class.'] records have been flushed.'); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/SearchableScope.php: -------------------------------------------------------------------------------- 1 | macro('searchable', function (EloquentBuilder $builder) { 23 | $builder->chunk(Config::get('scout.count.unsearchable', 100), function ($models) use ($builder) { 24 | $models->searchable(); 25 | 26 | event(new ModelsImported($models)); 27 | }); 28 | }); 29 | 30 | $builder->macro('unsearchable', function (EloquentBuilder $builder) { 31 | $builder->chunk(Config::get('scout.count.unsearchable', 100), function ($models) use ($builder) { 32 | $models->unsearchable(); 33 | 34 | event(new ModelsDeleted($models)); 35 | }); 36 | }); 37 | 38 | $builder->macro('updateSearchable', function (EloquentBuilder $builder) { 39 | $builder->chunk(Config::get('scout.count.updateSearchable', 100), function ($models) use ($builder) { 40 | $models->updateSearchable(); 41 | 42 | event(new ModelsUpdated($models)); 43 | }); 44 | }); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/Query/Grammar.php: -------------------------------------------------------------------------------- 1 | concatenate($this->compileComponents($query))); 17 | 18 | return $sql; 19 | } 20 | 21 | protected function concatenate($segments) 22 | { 23 | return implode(' ', array_filter($segments, function ($value) { 24 | return (string) $value !== ''; 25 | })); 26 | } 27 | 28 | protected function compileComponents(QueryStructureBuilder $query) 29 | { 30 | $sql = []; 31 | 32 | foreach ($this->selectComponents as $component) { 33 | if (! is_null($query->$component)) { 34 | $method = 'compile'.ucfirst($component); 35 | 36 | $sql[$component] = $this->$method($query, $query->$component); 37 | } 38 | } 39 | 40 | return $sql; 41 | } 42 | 43 | public function compileWheres(QueryStructureBuilder $query) 44 | { 45 | $sql = []; 46 | 47 | if (is_null($query->wheres)) { 48 | return ''; 49 | } 50 | 51 | foreach ($query->wheres as $where) { 52 | $method = "where{$where['type']}"; 53 | 54 | $sql[] = strtoupper($where['boolean']).' '.$this->$method($query, $where); 55 | } 56 | 57 | if (count($sql) > 0) { 58 | $sql = implode(' ', $sql); 59 | 60 | return $this->removeLeadingBoolean($sql); 61 | } 62 | 63 | return ''; 64 | } 65 | 66 | protected function whereBasic(QueryStructureBuilder $query, $where) 67 | { 68 | $value = '\''.$where['value'].'\''; 69 | 70 | return $where['column'].$where['operator'].$value; 71 | } 72 | 73 | protected function whereNested(QueryStructureBuilder $query, $where) 74 | { 75 | $nested = $where['query']; 76 | 77 | return '('.$this->compileWheres($nested).')'; 78 | } 79 | 80 | protected function removeLeadingBoolean($value) 81 | { 82 | return preg_replace('/AND |OR /i', '', $value, 1); 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/OpenSearchClient.php: -------------------------------------------------------------------------------- 1 | host; 38 | $debug = isset($configs['debug']) ? $configs['debug'] : false; 39 | 40 | $this->cloudSearchClient = new CloudsearchClient( 41 | $configs['access_key_id'], 42 | $configs['access_key_secret'], 43 | [ 44 | 'host' => $host, 45 | 'debug' => $debug, 46 | ], 47 | 'aliyun' 48 | ); 49 | } 50 | 51 | /** 52 | * 使用自己的 accesskey 和 secret 实例化一个 aliyun opensearch client 53 | * 54 | * @return CloudsearchClient 55 | */ 56 | public function getCloudSearchClient() 57 | { 58 | return $this->cloudSearchClient; 59 | } 60 | 61 | /** 62 | * 实例化一个搜索类 63 | * 64 | * @return CloudsearchSearch 65 | */ 66 | public function getCloudSearchSearch() 67 | { 68 | if ($this->cloudsearchSearch === null) { 69 | $this->cloudsearchSearch = new CloudsearchSearch($this->getCloudSearchClient()); 70 | } 71 | 72 | return $this->cloudsearchSearch; 73 | } 74 | 75 | /** 76 | * 获取 opensearch 文档接口客户端 77 | * 78 | * @param $indexName 79 | * @return CloudsearchDoc 80 | */ 81 | public function getCloudSearchDoc($indexName) 82 | { 83 | return new CloudsearchDoc($indexName, $this->getCloudSearchClient()); 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ali-opensearch-sdk 2 | 3 | 应用层,基于 laravel scout 实现:https://laravel.com/docs/5.4/scout#custom-engines 4 | 5 | scout 默认引擎是 algolia:https://www.algolia.com 6 | 7 | 开源的,大家常用 Elasticsearch:https://laracasts.com/discuss/channels/general-discussion/looking-for-a-search-engine-for-my-laravel-app?page=1 8 | 9 | 阿里云有开放搜索服务:https://help.aliyun.com/document_detail/29104.html?spm=5176.doc35261.6.539.qrzcjR 10 | 11 | 看文档相比自己搭 Elasticsearch 有以下优势: 12 | 13 | - 不用买服务器、搭环境、主从、容灾、维护...这是最重要的原因(实际上看似开放搜索要收费,当时算上人工和服务器成本,自己搭建基础服务贵太多了) 14 | - 开放搜索可以从 RDS 自动同步数据,这样就不用在应用里做数据同步了(实际上,对灵析来说,因为既有 laravel 又有 tp,要做好数据同步非常麻烦) 15 | - 据称,开放搜索比 ElasticSearch 开源系统的QPS高4倍,查询延迟低4倍 16 | 17 | ## 安装 18 | 19 | ```shell 20 | composer require lingxi/ali-opensearch-sdk 21 | ``` 22 | 23 | ## 配置 24 | 25 | 在你的 scout.php 26 | 27 | ```php 28 | 'opensearch', 32 | 33 | 'prefix' => '', // 应用前缀 34 | 35 | 'queue' => true, // 是否开启队列同步数据 36 | 37 | 'opensearch' => [ 38 | 39 | 'access_key_id' => env('OPENSEARCH_ACCESS_KEY'), 40 | 41 | 'access_key_secret' => env('OPENSEARCH_ACCESS_SECRET'), 42 | 43 | 'host' => env('OPENSEARCH_HOST'), 44 | 45 | 'debug' => env('OPENSEARCH_DEBUG'), 46 | 47 | ], 48 | 49 | 'count' => [ 50 | 51 | 'unsearchable' => 20, // 一次性删除文档的 Model 数量 52 | 53 | 'searchable' => 20, // 一次性同步文档的 Model 数量 54 | 55 | 'updateSearchable' => 20, // 一次性更新(先删除,再更新)文档的 Model 数量 56 | 57 | ], 58 | ] 59 | ``` 60 | 61 | ## 注册服务 62 | 63 | ```php 64 | Laravel\Scout\ScoutServiceProvider::class, 65 | Lingxi\AliOpenSearch\OpenSearchServiceProvider::class, 66 | ``` 67 | 68 | --- 69 | 70 | ## 使用 71 | 72 | 请先阅读:https://laravel.com/docs/5.3/scout 73 | 74 | 在 Model 里添加 Searchable Trait: 75 | 76 | ```php 77 | 'lingxi']) 117 | ->select([ 118 | 'id', 119 | 'name', 120 | 'age', 121 | ]) 122 | ->filter(['age', '<', '30']) 123 | ->filter(['age', '>', '18']) 124 | ->orderBy('id', 'desc') 125 | ->paginate(15); 126 | ``` 127 | 128 | 更为复杂的情况就是对搜索添加的构造,仿照 laravel model/builder 的思想写了一个对 Opensearch 的 查询构造器. 129 | 130 | > 根据条件动态的搜索, 基本和 eloquent 提供的数据库查询保持一致. 131 | 132 | ```php 133 | where(function ($query) use ($q) { 141 | return $query->where('name', $q) 142 | ->when(strpos($q, "@") !== false && $q != "@", function ($query) use ($q) { 143 | return $query->orWhere('email', $q); 144 | }) 145 | ->when(is_numeric($q), function ($query) use ($q) { 146 | return $query->orWhere('mobile', $q); 147 | }); 148 | }); 149 | 150 | $users = User::search($query) 151 | ->filter('age', 18) 152 | ->take(5) 153 | ->get(); 154 | ``` 155 | 156 | ### 数据的维护 157 | 158 | 有很多情况可能无法直接使用 opensearch 直接同步 RDS 的数据,那么就需要在应用用去手动维护。 159 | 160 | 这个时候实现 toSearchableDocCallbacks 这个方法,向 opensearch 提供删除,修改的数据。 161 | 162 | 使用可以先阅读源码,有详细的注释,这边还没有想出最佳实践。 163 | 164 | -------------------------------------------------------------------------------- /src/Query/QueryStructureBuilder.php: -------------------------------------------------------------------------------- 1 | [], 21 | ]; 22 | 23 | protected $operators = [':']; 24 | 25 | protected function __construct(Grammar $grammar = null) 26 | { 27 | $this->grammar = $grammar ?: new Grammar; 28 | } 29 | 30 | public static function make() 31 | { 32 | return new static; 33 | } 34 | 35 | public function toSql() 36 | { 37 | return $this->grammar->compileSelect($this); 38 | } 39 | 40 | public function where($column, $operator = null, $value = null, $boolean = 'and') 41 | { 42 | if (is_array($column)) { 43 | return $this->addArrayOfWheres($column, $boolean); 44 | } 45 | 46 | list($value, $operator) = $this->prepareValueAndOperator( 47 | $value, $operator, func_num_args() == 2 48 | ); 49 | 50 | if ($column instanceof Closure) { 51 | return $this->whereNested($column, $boolean); 52 | } 53 | 54 | // 暂时把所有的操作都转化为 =,因为对于 opensearch 来说只有这么一个. 55 | if (! in_array(strtolower($operator), $this->operators, true)) { 56 | list($value, $operator) = [$operator, ':']; 57 | } 58 | 59 | // 处理普通情况 60 | $type = 'Basic'; 61 | 62 | $this->wheres[] = compact('type', 'column', 'operator', 'value', 'boolean'); 63 | 64 | $this->addBinding($value, 'where'); 65 | 66 | return $this; 67 | } 68 | 69 | public function orWhere($column, $operator = null, $value = null) 70 | { 71 | return $this->where($column, $operator, $value, 'or'); 72 | } 73 | 74 | protected function addArrayOfWheres($column, $boolean, $method = 'where') 75 | { 76 | return $this->whereNested(function ($query) use ($column, $method) { 77 | foreach ($column as $key => $value) { 78 | if (is_numeric($key) && is_array($value)) { 79 | call_user_func_array([$query, $method], $value); 80 | } else { 81 | $query->$method($key, ':', $value); 82 | } 83 | } 84 | }, $boolean); 85 | } 86 | 87 | protected function whereNested(Closure $callback, $boolean = 'and') 88 | { 89 | $query = $this->forNestedWhere(); 90 | 91 | call_user_func($callback, $query); 92 | 93 | return $this->addNestedWhereQuery($query, $boolean); 94 | } 95 | 96 | protected function forNestedWhere() 97 | { 98 | return static::make(); 99 | } 100 | 101 | protected function addNestedWhereQuery($query, $boolean = 'and') 102 | { 103 | if (count($query->wheres)) { 104 | $type = 'Nested'; 105 | 106 | $this->wheres[] = compact('type', 'query', 'boolean'); 107 | 108 | $this->addBinding($query->getBindings(), 'where'); 109 | } 110 | 111 | return $this; 112 | } 113 | 114 | protected function addBinding($value, $type = 'where') 115 | { 116 | if (! array_key_exists($type, $this->bindings)) { 117 | throw new InvalidArgumentException("Invalid binding type: {$type}."); 118 | } 119 | 120 | if (is_array($value)) { 121 | $this->bindings[$type] = array_values(array_merge($this->bindings[$type], $value)); 122 | } else { 123 | $this->bindings[$type][] = $value; 124 | } 125 | 126 | return $this; 127 | } 128 | 129 | protected function getBindings() 130 | { 131 | return Arr::flatten($this->bindings); 132 | } 133 | 134 | protected function prepareValueAndOperator($value, $operator, $useDefault = false) 135 | { 136 | if ($useDefault) { 137 | return [$operator, ':']; 138 | } elseif ($this->invalidOperatorAndValue($operator, $value)) { 139 | throw new InvalidArgumentException('Illegal operator and value combination.'); 140 | } 141 | 142 | return [$value, $operator]; 143 | } 144 | 145 | /** 146 | * Opensearch 只支持索引等于这一个操作 147 | * 148 | * @param string $operator 149 | * @param mixed $value 150 | * @return bool 151 | */ 152 | protected function invalidOperatorAndValue($operator, $value) 153 | { 154 | $isOperator = in_array($operator, $this->operators); 155 | 156 | return is_null($value) && $isOperator && ! in_array($operator, ['=']); 157 | } 158 | } 159 | -------------------------------------------------------------------------------- /src/Searchable.php: -------------------------------------------------------------------------------- 1 | registerSearchableMacros(); 28 | } 29 | 30 | public function registerSearchableMacros() 31 | { 32 | $self = $this; 33 | 34 | BaseCollection::macro('searchable', function () use ($self) { 35 | $self->queueMakeSearchable($this); 36 | }); 37 | 38 | BaseCollection::macro('unsearchable', function () use ($self) { 39 | $self->queueRemoveFromSearch($this); 40 | }); 41 | 42 | BaseCollection::macro('updateSearchable', function () use ($self) { 43 | $self->queueUpdateSearchable($this); 44 | }); 45 | } 46 | 47 | public function getSearchableFields() 48 | { 49 | return 'id'; 50 | } 51 | 52 | public function scopeMakeSearchableQuery($query, $self) 53 | { 54 | return $query->orderBy($self->getKeyName()); 55 | } 56 | 57 | /** 58 | * Make all instances of the model searchable. 59 | * 60 | * @return void 61 | */ 62 | public static function makeAllSearchable() 63 | { 64 | $self = new static(); 65 | 66 | $self->newQuery() 67 | ->makeSearchableQuery($self) 68 | ->searchable(); 69 | } 70 | 71 | /** 72 | * Dispatch the job to make the given models searchable. 73 | * 74 | * @param \Illuminate\Database\Eloquent\Collection $models 75 | * @return void 76 | */ 77 | public function queueMakeSearchable($models) 78 | { 79 | if ($models->isEmpty()) { 80 | return; 81 | } 82 | 83 | if (! config('scout.queue')) { 84 | return $models->first()->searchableUsing()->add($models); 85 | } 86 | 87 | dispatch((new MakeSearchable($models)) 88 | ->onQueue($models->first()->syncWithSearchUsingQueue()) 89 | ->onConnection($models->first()->syncWithSearchUsing())); 90 | } 91 | 92 | /** 93 | * Dispatch the job to update the given models searchable. 94 | * 95 | * @param \Illuminate\Database\Eloquent\Collection $models 96 | * @return void 97 | */ 98 | public function queueUpdateSearchable($models) 99 | { 100 | if ($models->isEmpty()) { 101 | return; 102 | } 103 | 104 | if (! config('scout.queue')) { 105 | return $models->first()->searchableUsing()->update($models); 106 | } 107 | 108 | dispatch((new UpdateSearchable($models)) 109 | ->onQueue($models->first()->syncWithSearchUsingQueue()) 110 | ->onConnection($models->first()->syncWithSearchUsing())); 111 | } 112 | 113 | /** 114 | * Dispatch the job to remove the given models searchable. 115 | * 116 | * @param \Illuminate\Database\Eloquent\Collection $models 117 | * @return void 118 | */ 119 | public function queueRemoveFromSearch($models) 120 | { 121 | if ($models->isEmpty()) { 122 | return; 123 | } 124 | 125 | if (! config('scout.queue')) { 126 | return $models->first()->searchableUsing()->remove($models); 127 | } 128 | 129 | dispatch((new RemoveSearchable($models)) 130 | ->onQueue($models->first()->syncWithSearchUsingQueue()) 131 | ->onConnection($models->first()->syncWithSearchUsing())); 132 | } 133 | 134 | /** 135 | * Get the Scout engine for the model. 136 | * @fixme [Scout2.0] \Laravel\Scout\Searchable 里没有传参数,应该是个 bug 137 | * 138 | * @return mixed 139 | */ 140 | public function searchableUsing() 141 | { 142 | return app(EngineManager::class)->engine('opensearch'); 143 | } 144 | 145 | /** 146 | * Perform a search against the model's indexed data. 147 | * 148 | * @param string $query 149 | * @param Closure $callback 150 | * @return Lingxi\AliOpenSearch\ExtendBuilder 151 | */ 152 | public static function search($query, $callback = null) 153 | { 154 | return new ExtendedBuilder(new static, $query, $callback); 155 | } 156 | } 157 | -------------------------------------------------------------------------------- /src/Query/Builder.php: -------------------------------------------------------------------------------- 1 | cloudsearchSearch = $cloudsearchSearch; 18 | } 19 | 20 | public function build($builder) 21 | { 22 | $this->index($builder->index ?: $builder->model->searchableAs()); 23 | $this->query($builder->query, $builder->rawQuerys); 24 | $this->filter($builder->filters, $builder->rawFilters); 25 | $this->hit($builder->limit ?: 20, $builder->page ?: 1); 26 | $this->sort($builder->orders); 27 | $this->addFields($builder->fields); 28 | $this->addDistinct($builder->distincts); 29 | $this->addAggregate($builder->aggregates); 30 | $this->setPair($builder->pair); 31 | 32 | $this->cloudsearchSearch->setFormat('json'); 33 | 34 | return $this->cloudsearchSearch; 35 | } 36 | 37 | /** 38 | * 搜索的应用 39 | * 40 | * @param array|string $index 41 | * @return null 42 | */ 43 | protected function index($index) 44 | { 45 | if (is_array($index)) { 46 | foreach ($index as $key => $value) { 47 | $this->cloudsearchSearch->addIndex($value); 48 | } 49 | } else { 50 | $this->cloudsearchSearch->addIndex($index); 51 | } 52 | } 53 | 54 | /** 55 | * 过滤 filter 子句 56 | * 57 | * @see https://help.aliyun.com/document_detail/29158.html 58 | * @param array $filters 59 | * @param array $rawFilters 60 | * @return null 61 | */ 62 | protected function filter(array $filters, array $rawFilters) 63 | { 64 | foreach ($filters as $filter) { 65 | list($key, $operator, $value) = $filter; 66 | 67 | if (!is_numeric($value) && is_string($value)) { 68 | // literal类型的字段值必须要加双引号,支持所有的关系运算,不支持算术运算 69 | $value = '"' . $value . '"'; 70 | } 71 | 72 | $this->cloudsearchSearch->addFilter($key . $operator . $value, 'AND'); 73 | } 74 | 75 | foreach ($rawFilters as $key => $value) { 76 | $this->cloudsearchSearch->addFilter($value, 'AND'); 77 | } 78 | } 79 | 80 | /** 81 | * 查询 query 子句 82 | * 83 | * @example (name:'rry' AND age:'10') OR (name: 'lirui') 84 | * 85 | * @see https://help.aliyun.com/document_detail/29157.html 86 | * @param mixed $query 87 | * @return null 88 | */ 89 | protected function query($query, $rawQuerys) 90 | { 91 | if ($query instanceof QueryStructureBuilder) { 92 | $query = $query->toSql(); 93 | } elseif (! is_string($query)) { 94 | $query = collect($query) 95 | ->map(function ($value, $key) { 96 | return $key . ':\'' . $value . '\''; 97 | }) 98 | ->implode(' AND '); 99 | } 100 | 101 | $query = $rawQuerys ? $query . ' AND ' . implode($rawQuerys, ' AND ') : $query; 102 | 103 | $this->cloudsearchSearch->setQueryString($query); 104 | } 105 | 106 | /** 107 | * 返回文档的最大数量 108 | * 109 | * @see https://help.aliyun.com/document_detail/29156.html 110 | * @param integer $limit 111 | * @return null 112 | */ 113 | protected function hit($limit, $page) 114 | { 115 | $this->cloudsearchSearch->setHits($limit); 116 | $this->cloudsearchSearch->setStartHit(($page - 1) * $limit); 117 | } 118 | 119 | /** 120 | * 排序sort子句 121 | * 122 | * @see https://help.aliyun.com/document_detail/29159.html 123 | * @param array $orders 124 | * @return null 125 | */ 126 | protected function sort(array $orders) 127 | { 128 | foreach ($orders as $key => $value) { 129 | $this->cloudsearchSearch->addSort($value['column'], $value['direction'] == 'asc' ? CloudsearchSearch::SORT_INCREASE : CloudsearchSearch::SORT_DECREASE); 130 | } 131 | } 132 | 133 | protected function addFields($fields) 134 | { 135 | $this->cloudsearchSearch->addFetchFields($fields); 136 | } 137 | 138 | protected function addDistinct($distincts) 139 | { 140 | foreach ($distincts as $distinct) { 141 | $this->cloudsearchSearch->addDistinct(...$distinct); 142 | } 143 | } 144 | 145 | protected function addAggregate($aggregates) 146 | { 147 | foreach ($aggregates as $aggregate) { 148 | $this->cloudsearchSearch->addAggregate(...$aggregate); 149 | } 150 | } 151 | 152 | protected function setPair($pair) 153 | { 154 | if ($pair) { 155 | $this->cloudsearchSearch->setPair($pair); 156 | } 157 | } 158 | } 159 | -------------------------------------------------------------------------------- /src/Sdk/CloudsearchSuggest.php: -------------------------------------------------------------------------------- 1 | 30 | * $suggest = new CloudsearchSuggest($client); 31 | * $suggest->setIndexName("index_name"); 32 | * $suggest->setSuggestName("suggest_name"); 33 | * $suggest->setHits(10); 34 | * $suggest->setQuery($query); 35 | * 36 | * echo $suggest->search(); 37 | * 38 | * 39 | * 或 40 | * 41 | * 42 | * $suggest = new CloudsearchSuggest($client); 43 | * 44 | * $opts = array( 45 | * "index_name" => "index_name", 46 | * "suggest_name" => "suggest_name", 47 | * "hit" => 10, 48 | * "query" => "query" 49 | * ); 50 | * 51 | * echo $suggest->search($opts); 52 | * 53 | * 54 | */ 55 | class CloudsearchSuggest 56 | { 57 | private $client = null; 58 | 59 | private $indexName = null; 60 | 61 | private $suggestName = null; 62 | 63 | private $hits = 10; 64 | 65 | private $query = null; 66 | 67 | private $path = "/suggest"; 68 | 69 | public function __construct($client) 70 | { 71 | $this->client = $client; 72 | } 73 | 74 | /** 75 | * 设定下拉提示对应的应用名称 76 | * 77 | * @param string $indexName 指定的应用名称 78 | */ 79 | public function setIndexName($indexName) 80 | { 81 | $this->indexName = $indexName; 82 | } 83 | 84 | /** 85 | * 获取下拉提示对应的应用名称 86 | * 87 | * @return string 返回应用名称 88 | */ 89 | public function getIndexName() 90 | { 91 | return $this->indexName; 92 | } 93 | 94 | /** 95 | * 设定下拉提示名称 96 | * 97 | * @param string $suggestName 指定的下拉提示名称。 98 | */ 99 | public function setSuggestName($suggestName) 100 | { 101 | $this->suggestName = $suggestName; 102 | } 103 | 104 | /** 105 | * 获取下拉提示名称 106 | * 107 | * @return string 返回下拉提示名称。 108 | */ 109 | public function getSuggestName() 110 | { 111 | return $this->suggestName; 112 | } 113 | 114 | /** 115 | * 设定返回结果条数 116 | * 117 | * @param int $hits 返回结果的条数。 118 | */ 119 | public function setHits($hits) 120 | { 121 | $hits = (int) $hits; 122 | if ($hits < 0) { 123 | $hits = 0; 124 | } 125 | $this->hits = $hits; 126 | } 127 | 128 | /** 129 | * 获取返回结果条数 130 | * 131 | * @return int 返回条数。 132 | */ 133 | public function getHits() 134 | { 135 | return $this->hits; 136 | } 137 | 138 | /** 139 | * 设定要查询的关键词 140 | * 141 | * @param string $query 要查询的关键词。 142 | */ 143 | public function setQuery($query) 144 | { 145 | $this->query = $query; 146 | } 147 | 148 | /** 149 | * 获取要查询的关键词 150 | * 151 | * @return string 返回要查询的关键词。 152 | */ 153 | public function getQuery() 154 | { 155 | return $this->query; 156 | } 157 | 158 | /** 159 | * 发出查询请求 160 | * 161 | * @param array $opts options参数列表 162 | * @subparam index_name 应用名称 163 | * @subparam suggest_name 下拉提示名称 164 | * @subparam hits 返回结果条数 165 | * @subparam query 查询关键词 166 | * @return string 返回api返回的结果。 167 | */ 168 | public function search($opts = array()) 169 | { 170 | if (!empty($opts)) { 171 | if (isset($opts['index_name']) && $opts['index_name'] !== '') { 172 | $this->setIndexName($opts['index_name']); 173 | } 174 | 175 | if (isset($opts['suggest_name']) && $opts['suggest_name'] !== '') { 176 | $this->setSuggestName($opts['suggest_name']); 177 | } 178 | 179 | if (isset($opts['hits']) && $opts['hits'] !== '') { 180 | $this->setHits($opts['hits']); 181 | } 182 | 183 | if (isset($opts['query']) && $opts['query'] !== '') { 184 | $this->setQuery($opts['query']); 185 | } 186 | } 187 | 188 | $params = array( 189 | "index_name" => $this->getIndexName(), 190 | "suggest_name" => $this->getSuggestName(), 191 | "hit" => $this->getHits(), 192 | "query" => $this->getQuery(), 193 | ); 194 | 195 | return $this->client->call($this->path, $params); 196 | } 197 | } 198 | -------------------------------------------------------------------------------- /src/Sdk/CloudsearchIndex.php: -------------------------------------------------------------------------------- 1 | client = $client; 59 | $this->indexName = $indexName; 60 | $this->_setPath($indexName); 61 | } 62 | 63 | /** 64 | * 通过模板名称创建应用 65 | * 66 | * 用指定的模板名称创建一个新的应用。 67 | * @param string $templateName 模板名称(可以使系统内置模板,也可以是自定义模板) 68 | * @param array $opts 包含应用的备注信息。 69 | * 70 | * @return string 返回api返回的结果。 71 | */ 72 | public function createByTemplateName($templateName, $opts = array()) 73 | { 74 | $params = array( 75 | 'action' => "create", 76 | 'template' => $templateName, 77 | ); 78 | 79 | if (isset($opts['desc']) && !empty($opts['desc'])) { 80 | $params['index_des'] = $opts['desc']; 81 | } 82 | 83 | return $this->client->call($this->path, $params); 84 | } 85 | 86 | /** 87 | * 通过模板创建应用 88 | * 89 | * 用指定的模板创建一个新的应用。模版是一个格式化数组,用于描述应用的结构,可以在控制台中通过创建应用->保存模板->导出模板来获得json结构的模板;也可以自己生成,格式见控制台模板管理。 90 | * @param string $template 使用的模板 91 | * @param array $opts 包含应用的备注信息。 92 | * 93 | * @return string 返回api返回的正确或错误的结果。 94 | */ 95 | public function createByTemplate($template, $opts = array()) 96 | { 97 | $params = array( 98 | 'action' => "create", 99 | 'template' => $template, 100 | ); 101 | 102 | if (isset($opts['desc']) && !empty($opts['desc'])) { 103 | $params['index_des'] = $opts['desc']; 104 | } 105 | 106 | $params['template_type'] = 2; 107 | 108 | return $this->client->call($this->path, $params); 109 | } 110 | 111 | /** 112 | * 修改应用名称和备注 113 | * 114 | * 更新当前应用的应用名称和备注信息。 115 | * @param string $toIndexName 更改后的新名字 116 | * @param array $opts 可选参数,包含: desc 应用备注信息 117 | * @return string API返回的操作结果 118 | */ 119 | public function rename($toIndexName, $opts = array()) 120 | { 121 | $params = array( 122 | 'action' => "update", 123 | 'new_index_name' => $toIndexName, 124 | ); 125 | 126 | if (isset($opts['desc']) && !empty($opts['desc'])) { 127 | $params['description'] = $opts['desc']; 128 | } 129 | 130 | $result = $this->client->call($this->path, $params); 131 | $json = json_decode($result, true); 132 | if (isset($json['status']) && $json['status'] == 'OK') { 133 | $this->indexName = $toIndexName; 134 | $this->_setPath($toIndexName); 135 | } 136 | return $result; 137 | } 138 | 139 | private function _setPath($indexName) 140 | { 141 | $this->path = '/index/' . $indexName; 142 | } 143 | 144 | /** 145 | * 删除应用 146 | * 147 | * @return string API返回的操作结果 148 | */ 149 | public function delete() 150 | { 151 | return $this->client->call($this->path, array('action' => "delete")); 152 | } 153 | 154 | /** 155 | * 查看应用状态 156 | * 157 | * @return string API返回的操作结果 158 | */ 159 | public function status() 160 | { 161 | return $this->client->call($this->path, array('action' => "status")); 162 | } 163 | 164 | /** 165 | * 列出所有应用 166 | * 167 | * @param int $page 页码 168 | * @param int $pageSize 每页的记录条数 169 | */ 170 | public function listIndexes($page = 1, $pageSize = 10) 171 | { 172 | $params = array( 173 | 'page' => $page, 174 | 'page_size' => $pageSize, 175 | ); 176 | return $this->client->call('/index', $params); 177 | } 178 | 179 | /** 180 | * 获取应用名称 181 | * 182 | * 获取当前应用的名称。 183 | * 184 | * @return string 当前应用的名称 185 | */ 186 | public function getIndexName() 187 | { 188 | return $this->indexName; 189 | } 190 | 191 | /** 192 | * 获取应用的最近错误列表 193 | * 194 | * @param int $page 指定获取第几页的错误信息。默认值:1 195 | * @param int $pageSize 指定每页显示的错误条数。默认值:10 196 | * 197 | * @return array 返回指定页数的错误信息列表。 198 | */ 199 | public function getErrorMessage($page = 1, $pageSize = 10) 200 | { 201 | $this->_checkPageClause($page); 202 | $this->_checkPageSizeClause($pageSize); 203 | 204 | $params = array( 205 | 'page' => $page, 206 | 'page_size' => $pageSize, 207 | ); 208 | return $this->client->call('/index/error/' . $this->indexName, $params); 209 | } 210 | 211 | /** 212 | * 检查$page参数是否合法。 213 | * 214 | * @param int $page 指定的页码。 215 | * 216 | * @throws Exception 如果参数不正确,则抛出此异常。 217 | * 218 | * @access private 219 | */ 220 | private function _checkPageClause($page) 221 | { 222 | if (null == $page || !is_int($page)) { 223 | throw new Exception('$page is not an integer.'); 224 | } 225 | if ($page <= 0) { 226 | throw new Exception('$page is not greater than or equal to 0.'); 227 | } 228 | } 229 | 230 | /** 231 | * 检查$pageSize参数是否合法。 232 | * 233 | * @param int $pageSize 每页显示的记录条数。 234 | * 235 | * @throws Exception 参数不合法 236 | * 237 | * @access private 238 | */ 239 | private function _checkPageSizeClause($pageSize) 240 | { 241 | if (null == $pageSize || !is_int($pageSize)) { 242 | throw new Exception('$pageSize is not an integer.'); 243 | } 244 | if ($pageSize <= 0) { 245 | throw new Exception('$pageSize is not greater than 0.'); 246 | } 247 | } 248 | } 249 | -------------------------------------------------------------------------------- /src/ExtendedBuilder.php: -------------------------------------------------------------------------------- 1 | model = $model; 124 | $this->query = $query; 125 | $this->callback = $callback; 126 | 127 | $this->select(); 128 | } 129 | 130 | /** 131 | * Specify a custom index to perform this search on. 132 | * 133 | * @param string $index 134 | * @return $this 135 | */ 136 | public function within($index) 137 | { 138 | $this->index = $index; 139 | 140 | return $this; 141 | } 142 | 143 | /** 144 | * Add a constraint to the search filter. 145 | * 146 | * @param mixed $field 147 | * @param mixed $value 148 | * @return $this 149 | */ 150 | public function filter($field, $value = null) 151 | { 152 | if (is_array($field)) { 153 | $this->filters[] = $field; 154 | } else { 155 | if (! is_array($value)) { 156 | $value = [$field, '=', $value]; 157 | } else { 158 | array_unshift($value, $field); 159 | } 160 | 161 | $this->filters[] = $value; 162 | } 163 | 164 | return $this; 165 | } 166 | 167 | /** 168 | * Set the "limit" for the search query. 169 | * 170 | * @param int $limit 171 | * @return $this 172 | */ 173 | public function take($limit) 174 | { 175 | $this->limit = $limit; 176 | 177 | return $this; 178 | } 179 | 180 | public function forPage($page, $perPage = 20) 181 | { 182 | $this->page = $page; 183 | $this->limit = $perPage; 184 | 185 | return $this; 186 | } 187 | 188 | /** 189 | * Add a constraint to the search query. 190 | * 191 | * @param string $field 192 | * @param array $values 193 | * @return $this 194 | */ 195 | public function filterIn($field, array $values = []) 196 | { 197 | $this->rawFilters[] = '(' . collect($values)->map(function($item) use ($field) { 198 | $item = !is_numeric($item) && is_string($item) ? '"' . $item . '"' : $item; 199 | return $field . '=' . $item; 200 | })->implode(' OR ') . ')'; 201 | 202 | return $this; 203 | } 204 | 205 | /** 206 | * Add an "order" for the search query. 207 | * 208 | * @param string $column 209 | * @param string $direction 210 | * @return $this 211 | */ 212 | public function orderBy($column, $direction = 'asc') 213 | { 214 | $this->orders[] = [ 215 | 'column' => $column, 216 | 'direction' => strtolower($direction) == 'asc' ? 'asc' : 'desc', 217 | ]; 218 | 219 | return $this; 220 | } 221 | 222 | /** 223 | * Add an default rank to order. 224 | * 225 | * @param string $direction 226 | * @return $this 227 | */ 228 | public function orderByRank($direction = 'desc') 229 | { 230 | $this->orders[] = [ 231 | 'column' => 'RANK', 232 | 'direction' => strtolower($direction) == 'desc' ? 'desc' : 'asc', 233 | ]; 234 | 235 | return $this; 236 | } 237 | 238 | public function select($fields = null) 239 | { 240 | if (empty($fields)) { 241 | $fields = $this->model->getSearchableFields(); 242 | 243 | if (! is_array($fields)) { 244 | $fields = explode(',', $fields); 245 | } 246 | } 247 | 248 | $this->fields = $fields; 249 | 250 | return $this; 251 | } 252 | 253 | public function filterRaw($rawFilter) 254 | { 255 | $this->rawFilters[] = $rawFilter; 256 | 257 | return $this; 258 | } 259 | 260 | public function searchRaw($rawQuery) 261 | { 262 | $this->rawQuerys[] = $rawQuery; 263 | 264 | return $this; 265 | } 266 | 267 | public function addDistinct() 268 | { 269 | $this->distincts[] = func_get_args(); 270 | 271 | return $this; 272 | } 273 | 274 | public function addAggregate() 275 | { 276 | $this->aggregates[] = func_get_args(); 277 | 278 | return $this; 279 | } 280 | 281 | public function setPair($pair) 282 | { 283 | $this->pair = $pair; 284 | 285 | return $this; 286 | } 287 | 288 | /** 289 | * Get the keys of search results. 290 | * 291 | * @return \Illuminate\Support\Collection 292 | */ 293 | public function keys() 294 | { 295 | return $this->engine()->keys($this); 296 | } 297 | 298 | /** 299 | * Get the first result from the search. 300 | * 301 | * @return \Illuminate\Database\Eloquent\Model 302 | */ 303 | public function first() 304 | { 305 | return $this->get()->first(); 306 | } 307 | 308 | /** 309 | * Get the results of the search. 310 | * 311 | * @return \Illuminate\Database\Eloquent\Collection 312 | */ 313 | public function get() 314 | { 315 | return $this->engine()->get($this); 316 | } 317 | 318 | /** 319 | * Get the facet from aggregate. 320 | * 321 | * @return \Illuminate\Database\Eloquent\Collection 322 | */ 323 | public function facet($key) 324 | { 325 | return $this->engine()->facet($key, $this); 326 | } 327 | 328 | /** 329 | * Paginate the given query into a simple paginator. 330 | * 331 | * @param int $perPage 332 | * @param string $pageName 333 | * @param int|null $page 334 | * @return \Illuminate\Contracts\Pagination\LengthAwarePaginator 335 | */ 336 | public function paginate($perPage = null, $pageName = 'page', $page = null) 337 | { 338 | $engine = $this->engine(); 339 | 340 | $page = $page ?: Paginator::resolveCurrentPage($pageName); 341 | 342 | $perPage = $perPage ?: $this->model->getPerPage(); 343 | 344 | $this->forPage($page, $perPage); 345 | 346 | $results = Collection::make($engine->map( 347 | $rawResults = $engine->paginate($this, $perPage, $page), $this->model 348 | )); 349 | 350 | $paginator = (new LengthAwarePaginator($results, $engine->getTotalCount($rawResults), $perPage, $page, [ 351 | 'path' => Paginator::resolveCurrentPath(), 352 | 'pageName' => $pageName, 353 | ])); 354 | 355 | return $paginator->appends('query', $this->query); 356 | } 357 | 358 | /** 359 | * Get the engine that should handle the query. 360 | * 361 | * @return mixed 362 | */ 363 | protected function engine() 364 | { 365 | return $this->model->searchableUsing(); 366 | } 367 | } 368 | -------------------------------------------------------------------------------- /src/OpenSearchEngine.php: -------------------------------------------------------------------------------- 1 | opensearch = $opensearch; 46 | 47 | $this->cloudsearchSearch = $opensearch->getCloudSearchSearch(); 48 | } 49 | 50 | /** 51 | * Add the given model in the index. 52 | * 53 | * @param \Illuminate\Database\Eloquent\Collection $models 54 | * @return void 55 | */ 56 | public function add($models) 57 | { 58 | // Get opensearch index client. 59 | $doc = $this->getCloudSearchDoc($models); 60 | 61 | foreach ($this->getSearchableData($models, ['update']) as $name => $value) { 62 | if (! empty($value['update'])) { 63 | try { 64 | $this->waitASecond(); 65 | $doc->add($value['update'], $name); 66 | $this->waitASecond(); 67 | 68 | Event::fire(new DocSyncEvent($models->first()->searchableAs(), $name, $value, 'add', true)); 69 | } catch (OpensearchException $e) { 70 | throw $e; 71 | } 72 | } 73 | } 74 | } 75 | 76 | /** 77 | * Update the given model in the index. 78 | * 79 | * @param \Illuminate\Database\Eloquent\Collection $models 80 | * @return void 81 | */ 82 | public function update($models) 83 | { 84 | // Get opensearch index client. 85 | $doc = $this->getCloudSearchDoc($models); 86 | 87 | foreach ($this->getSearchableData($models) as $name => $value) { 88 | foreach ($value as $method => $items) { 89 | if (! empty($items)) { 90 | try { 91 | $this->waitASecond(); 92 | $doc->$method($items, $name); 93 | $this->waitASecond(); 94 | 95 | Event::fire(new DocSyncEvent($models->first()->searchableAs(), $name, $value, $method, true)); 96 | } catch (OpensearchException $e) { 97 | throw $e; 98 | } 99 | } 100 | } 101 | } 102 | } 103 | 104 | /** 105 | * Remove the given model from the index. 106 | * 107 | * @param \Illuminate\Database\Eloquent\Collection $models 108 | * @return void 109 | */ 110 | public function delete($models) 111 | { 112 | $doc = $this->getCloudSearchDoc($models); 113 | 114 | /* 115 | |---------------------------------------------------------------------- 116 | | 有删除逻辑的走删除逻辑,没有删除逻辑的直接走 id 删除 117 | |---------------------------------------------------------------------- 118 | | 119 | | 同时, opensearch 中的应用多表结构,适用于水平分库,或者提取字段专门建立的福鼠表 120 | | 所以对于数据的删除,只是直接删除 id. 121 | | 122 | */ 123 | foreach ($this->getSearchableData($models, ['delete']) as $name => $value) { 124 | if (array_key_exists('delete', $value)) { 125 | $toBeDeleteData = $value['delete']; 126 | } else { 127 | $toBeDeleteData = $models->map(function ($model) { 128 | return [ 129 | 'cmd' => 'delete', 130 | 'fields' => [ 131 | 'id' => $model->id, 132 | ] 133 | ]; 134 | }); 135 | } 136 | 137 | if (! empty($toBeDeleteData)) { 138 | try { 139 | $this->waitASecond(); 140 | $doc->delete($toBeDeleteData, $name); 141 | $this->waitASecond(); 142 | 143 | Event::fire(new DocSyncEvent($models->first()->searchableAs(), $name, $value, 'delete', true)); 144 | } catch (OpensearchException $e) { 145 | throw $e; 146 | } 147 | } 148 | } 149 | } 150 | 151 | /** 152 | * Sleep 100ms to avoid request frequently. 153 | * 154 | * 经过测试 200ms 比较稳定, 在请求前后分别停止 100ms 155 | * 156 | * @param integer $microSeconds 157 | * @return null 158 | */ 159 | protected function waitASecond($microSeconds = 100000) 160 | { 161 | usleep($microSeconds); 162 | } 163 | 164 | /** 165 | * Equals remove 166 | */ 167 | public function remove($models) 168 | { 169 | return $this->delete($models); 170 | } 171 | 172 | /** 173 | * 获取模型的操作数据 174 | * 175 | * @param \Illuminate\Database\Eloquent\Collection $models 176 | * @param array $actions 177 | * @return array 178 | */ 179 | protected function getSearchableData($models, array $actions = ['delete', 'update']) 180 | { 181 | // 获取应用的全部表名 182 | $tableNames = array_keys($models->first()->toSearchableDocCallbacks()); 183 | /* 184 | |-------------------------------------------------------------------------- 185 | | 构造数据,分两块 186 | |-------------------------------------------------------------------------- 187 | | 188 | | 在 opensearch 中,添加(add)和更新(update)是一样的操作,都是存在即更新,不存在则创建,且作为客户端我们不需要知道这条文档是被添加了或删除了 189 | | 那么最稳健的做法是,如果数据 id 是数据主键(或者其他稳定数据),那么只需要在直接更新就可以了 190 | | 碰到自己构造的 id 情况,就需要手动处理找出数据源 id 之后,手动删除了 191 | | 192 | | 插入数据,一般来说是第一次向 opensearch 添加数据时会使用, 所以,过滤掉 delete 操作 193 | | 194 | */ 195 | $data = []; 196 | foreach ($tableNames as $name) { 197 | $data[$name] = []; 198 | } 199 | 200 | foreach ($models as $model) { 201 | $callbacks = $model->toSearchableDocCallbacks($actions); 202 | 203 | // delete 就是需要在 update 前面 204 | ksort($callbacks); 205 | 206 | foreach ($callbacks as $name => $callback) { 207 | if (! empty($callback)) { 208 | foreach ($actions as $action) { 209 | if (isset($callback[$action])) { 210 | $data[$name][$action] = array_merge( 211 | isset($data[$name][$action]) ? $data[$name][$action] : [], 212 | call_user_func($callback[$action]) 213 | ); 214 | } 215 | } 216 | } 217 | } 218 | } 219 | 220 | return $data; 221 | } 222 | 223 | /** 224 | * Perform the given search on the engine. 225 | * 226 | * @param \Laravel\Scout\Builder $builder 227 | * @return mixed 228 | */ 229 | public function search(ScoutBuilder $builder) 230 | { 231 | $searchKey = serialize($builder); 232 | 233 | if (! isset($this->searchResult[$searchKey])) { 234 | $this->searchResult[$searchKey] = $this->performSearch($this->buildLaravelBuilderIntoOpensearch($builder)); 235 | } 236 | 237 | return $this->searchResult[$searchKey]; 238 | } 239 | 240 | public function paginate(ScoutBuilder $builder, $perPage, $page) 241 | { 242 | $cloudSearchSearch = $this->buildLaravelBuilderIntoOpensearch($builder); 243 | 244 | return $this->performSearch($cloudSearchSearch); 245 | } 246 | 247 | protected function buildLaravelBuilderIntoOpensearch($builder) 248 | { 249 | return (new Builder($this->cloudsearchSearch))->build($builder); 250 | } 251 | 252 | protected function performSearch(CloudsearchSearch $search) 253 | { 254 | return $search->search(); 255 | } 256 | 257 | /** 258 | * Map the given results to instances of the given model. 259 | * 260 | * @param mixed $results 261 | * @param \Illuminate\Database\Eloquent\Model $model 262 | * @return \Illuminate\Support\Collection 263 | * @throws OpensearchException 264 | */ 265 | public function map($results, $model) 266 | { 267 | $fields = $this->opensearch->getCloudSearchSearch()->getFetchFields(); 268 | 269 | if (empty($fields)) { 270 | throw new OpensearchException('搜索字段不能为空'); 271 | } 272 | 273 | if (count($fields) != 1) { 274 | return collect(array_map(function ($item) use ($fields) { 275 | $result = []; 276 | foreach ($fields as $field) { 277 | $result[$field] = $item[$field]; 278 | } 279 | return $result; 280 | }, $results['result']['items'])); 281 | } else { 282 | $fields = $fields[0]; 283 | } 284 | 285 | return $this->mapIds($results, $fields); 286 | } 287 | 288 | /** 289 | * Pluck and return the primary keys of the given results. 290 | * 291 | * @param mixed $results 292 | * @param string $field 293 | * @return \Illuminate\Support\Collection 294 | */ 295 | public function mapIds($results, $field = 'id') 296 | { 297 | return collect($results['result']['items'])->pluck($field)->values(); 298 | } 299 | 300 | /** 301 | * Get the total count from a raw result returned by the engine. 302 | * 303 | * @param mixed $results 304 | * @return int 305 | */ 306 | public function getTotalCount($results) 307 | { 308 | return $results['result']['total']; 309 | } 310 | 311 | /** 312 | * @param $models 313 | * @return Sdk\CloudsearchDoc 314 | */ 315 | private function getCloudSearchDoc($models) 316 | { 317 | return $this->opensearch->getCloudSearchDoc($models->first()->searchableAs()); 318 | } 319 | 320 | /** 321 | * Get the results of the given query mapped onto models. 322 | * 323 | * @param \Lingxi\AliOpenSearch\ScoutBuilder $builder 324 | * @return \Illuminate\Database\Eloquent\Collection 325 | */ 326 | public function get(ScoutBuilder $builder) 327 | { 328 | return Collection::make($this->map( 329 | $this->search($builder), $builder->model 330 | )); 331 | } 332 | 333 | /** 334 | * Get the facet from search results. 335 | * 336 | * @param string $key 337 | * @param ExtendedBuilder $builder 338 | * @return \Illuminate\Database\Eloquent\Collection 339 | */ 340 | public function facet($key, ExtendedBuilder $builder) 341 | { 342 | return Collection::make($this->mapFacet($key, $this->search($builder))); 343 | } 344 | 345 | protected function mapFacet($key, $results) 346 | { 347 | $facets = $results['result']['facet']; 348 | 349 | foreach ($facets as $facet) { 350 | if ($facet['key'] == $key) { 351 | return $facet['items']; 352 | } 353 | } 354 | 355 | return []; 356 | } 357 | } 358 | -------------------------------------------------------------------------------- /src/Sdk/CloudsearchDoc.php: -------------------------------------------------------------------------------- 1 | indexName = $indexName; 120 | $this->client = $client; 121 | $this->path = '/index/doc/' . $this->indexName; 122 | } 123 | 124 | /** 125 | * 查看文档 126 | * 127 | * 根据文档id获取doc的详细信息。 128 | * 129 | * @param string $docId 指定的文档id。 130 | * @return string 该docId对应的doc详细信息 131 | */ 132 | public function detail($docId) 133 | { 134 | $item = array("id" => $docId); 135 | return $this->client->call($this->path, $item, self::METHOD); 136 | } 137 | 138 | /** 139 | * 更新文档 140 | * 141 | * 向指定的表中更新doc。 142 | * @param array $docs 指定要更新的doc。 143 | * @param string $tableName 指定向哪个表中更新doc。 144 | * @return string 返回API返回的结果。 145 | */ 146 | public function update($docs, $tableName) 147 | { 148 | return $this->action($docs, $tableName, self::SIGN_MODE); 149 | } 150 | 151 | /** 152 | * 添加文档 153 | * 154 | * 向指定的表中增加doc。 155 | * @param array $docs 指定要添加的doc。 156 | * @param string $tableName 指定向哪个表中增加doc。 157 | * @return string 返回API返回的结果。 158 | */ 159 | public function add($docs, $tableName) 160 | { 161 | return $this->action($docs, $tableName, self::SIGN_MODE); 162 | } 163 | 164 | /** 165 | * 删除文档 166 | * 167 | * 删除指定表中的doc。 168 | * @param array $docs 指定要删除的doc列表,必须含有主键。 169 | * @param string $tableName 指定要从哪个表删除记录。 170 | * @return string 返回API返回的结果。 171 | */ 172 | public function delete($docs, $tableName) 173 | { 174 | return $this->action($docs, $tableName, self::SIGN_MODE); 175 | } 176 | 177 | /** 178 | * 删除文档 179 | * 180 | * 删除指定表中的doc。 181 | * @param array $docs 指定要删除的doc列表,必须含有主键。 182 | * @param string $tableName 指定要从哪个表删除记录。 183 | * @return string 返回API返回的结果。 184 | */ 185 | public function remove($docs, $tableName) 186 | { 187 | return $this->action($docs, $tableName, self::SIGN_MODE); 188 | } 189 | 190 | /** 191 | * 执行文档相关操作 192 | * 193 | * @param array|string $docs 此docs为用户push的数据,此字段为json_encode的字符串或者数据。 194 | * @param string $tableName 操作的表名。 195 | * @throws Exception 196 | * @return string 请求API并返回相应的结果。 197 | */ 198 | private function action($docs, $tableName, $signMode = self::SIGN_MODE) 199 | { 200 | if (!is_array($docs)) { 201 | $docs = json_decode($docs, true); 202 | } 203 | 204 | if (!is_array($docs) || empty($docs) || !is_array($docs[0])) { 205 | throw new Exception('Operation failed. The docs is not correct.'); 206 | } 207 | 208 | $params = array( 209 | 'action' => "push", 210 | 'items' => json_encode($docs), 211 | 'table_name' => $tableName, 212 | ); 213 | 214 | if ($signMode == self::SIGN_MODE) {; 215 | $params['sign_mode'] = self::SIGN_MODE; 216 | } 217 | 218 | return $this->client->call($this->path, $params, self::METHOD); 219 | } 220 | 221 | /** 222 | * 重新生成doc文档。 223 | * @param array $docs doc文档 224 | * @param string $type 操作类型,有ADD、UPDATE、REMOVE。 225 | * @return array 返回重新生成的doc文档。 226 | */ 227 | private function generate($docs, $type) 228 | { 229 | $result = array(); 230 | foreach ($docs as $doc) { 231 | $item = array('cmd' => $type); 232 | $item['fields'] = $doc; 233 | $result[] = $item; 234 | } 235 | 236 | return $result; 237 | } 238 | 239 | /** 240 | * 通过csv格式文件上传文档数据 241 | * 242 | * NOTE: 此文件必需为csv格式的文件(“,”分割);且第一行为数据结构字段名称,例如: 243 | * 244 | * id, title, name, date,1, "我的测试数据\"1\"测试1", test_name1, "2013-09-21 00:12:22" 245 | * ... 246 | * 247 | * @param string $fileName 本地文件。 248 | * @param string $primaryKey 指定此表的主键。 249 | * @param string $tableName 指定表名。 250 | * @param array $multiValue 指定此表中的多值的字段。默认值为空 251 | * @param int $offset 指定从第offset条记录开始导入。默认值为1 252 | * @param number $maxSize 指定每次push数据的最大值,单位为MB。默认值为4 253 | * @param int $frequence 指定上传数据的频率,默认值为4,单位为次/秒 254 | * 255 | * @return string 返回如果成功上传或上传失败的状态。 256 | */ 257 | public function pushCSVFile($fileName, $primaryKey, $tableName, 258 | $multiValue = array(), $offset = 1, $maxSize = self::PUSH_MAX_SIZE, 259 | $frequence = self::PUSH_FREQUENCE) { 260 | $reader = $this->_connect($fileName); 261 | 262 | $lineNo = 0; 263 | $buffter = array(); 264 | $latestLine = $offset - 1; 265 | $latestPrimaryKey = ''; 266 | $totalSize = 0; 267 | $primaryKeyPos = 0; 268 | 269 | $time = time(); 270 | $timeFreq = 0; 271 | 272 | while ($data = fgetcsv($reader, 1024, self::CSV_SEPARATOR)) { 273 | if ($lineNo == 0) { 274 | $header = $data; 275 | if (count(array_flip($data)) != count($data)) { 276 | throw new Exception('There are some multi fields in your header.'); 277 | } 278 | 279 | $primaryKeyPos = array_search($primaryKey, $header); 280 | if (false === $primaryKey) { 281 | throw new Exception("The primary key '{$primaryKey}' is not exists."); 282 | } 283 | } else { 284 | if ($lineNo < $offset) { 285 | continue; 286 | } 287 | 288 | if (count($data) != count($header)) { 289 | throw new Exception("The number of columns of values is not matched 290 | the number of header of primary key '{$data[$primaryKeyPos]}'. 291 | Latest successful posted primary key number is '{$latestPrimaryKey}'."); 292 | } 293 | 294 | $item = array(); 295 | $item['cmd'] = self::DOC_ADD; 296 | if (!empty($multiValue)) { 297 | foreach ($multiValue as $field => $separator) { 298 | $pos = array_search($field, $header); 299 | if ($pos !== false) { 300 | $data[$pos] = explode($separator, $data[$pos]); 301 | } 302 | } 303 | } 304 | $item['fields'] = array_combine($header, $data); 305 | 306 | $json = json_encode($item); 307 | // 检测是否push数据push成功。 308 | $currentSize = strlen(urlencode($json)); 309 | if ($currentSize + $totalSize >= self::PUSH_MAX_SIZE * 1024 * 1024) { 310 | $txt = $this->add($buffer, $tableName); 311 | $return = json_decode($txt, true); 312 | if ('OK' != $return['status']) { 313 | throw new Exception("Api returns error: " . $txt . 314 | ". Latest successful posted primary key is {$latestPrimaryKey}."); 315 | } else { 316 | // 计算每秒钟的push的频率并如果超过频率则sleep。 317 | $newTime = microtime(true); 318 | $timeFreq++; 319 | 320 | if (floor($newTime) == $time && $timeFreq >= self::PUSH_FREQUENCE) { 321 | usleep((floor($newTime) + 1 - $newTime) * 1000000); 322 | $timeFreq = 0; 323 | } 324 | 325 | $newTime = floor(microtime(true)); 326 | if ($time != $newTime) { 327 | $time = $newTime; 328 | $timeFreq = 0; 329 | } 330 | 331 | if (is_array($buffer) && !empty($buffer)) { 332 | $last = count($buffer) - 1; 333 | $latestPrimaryKey = $buffer[$last][$primaryKeyPos]; 334 | } else { 335 | $latestPrimaryKey = 0; 336 | } 337 | } 338 | $buffer = array(); 339 | $totalSize = 0; 340 | } 341 | $buffer[] = $item; 342 | $totalSize += $currentSize; 343 | } 344 | 345 | $lineNo++; 346 | } 347 | 348 | if (!empty($buffer)) { 349 | $return = json_decode($this->add($buffer, $tableName), true); 350 | if (self::PUSH_RETURN_STATUS_OK != $return['status']) { 351 | throw new Exception($return['errors'][0]['message'] . 352 | ". Latest successful posted line number is {$latestLine}."); 353 | } 354 | } 355 | 356 | return 'The data is posted successfully.'; 357 | } 358 | 359 | /** 360 | * 推送HA3格式文档 361 | * 362 | * 除了上面的方法还可以通过文件将文档导入到指定的表中 363 | * 这里的文档需满足一定的格式,我们称之为HA3文档格式。HA3文件的要求如下: 364 | * 365 | * 文件编码:UTF-8 366 | * 367 | * 支持CMD: add, delete,update。 368 | * 如果给出的字段不是全部,add会在未给出的字段加默认值,覆盖原值;update只会更新给出的字段,未给出的不变。 369 | * 370 | * 文件分隔符: 371 | *
372 |      *
373 |      * 编码-------描述--------------------显示形态
374 |      * "\x1E\n"   每个doc的分隔符.        ^^(接换行符)
375 |      * "\x1F\n"   每个字段key和value分隔  ^_(接换行符)
376 |      * "\x1D"     多值字段的分隔符        ^]
377 |      * 
; 378 | * 379 | * 示例: 380 | * 381 | *
;
382 |      * CMD=add^_
383 |      * url=http://www.opensearch.console.aliyun.com^_
384 |      * title=开放搜索^_
385 |      * body=xxxxx_xxxx^_
386 |      * multi_value_feild=123^]1234^]12345^_
387 |      * ^^
388 |      * CMD=update^_
389 |      * ...
390 |      * 
391 | * 392 | * 注意:文件结尾的分隔符也必需为"^^\n",最后一个换行符不能省略。 393 | * 394 | * @param string $fileName 指定HA3DOC所有在的路径。 395 | * @param string $tableName 指定要导入的表的名称。 396 | * @param int $offset 指定偏移行数,如果非0,则从当前行一下的数据开始导入。默认值为:1 397 | * @param number $maxSize 指定每次导入到api接口的数据量的大小,单位MB,默认值为:4 398 | * @param int $frequence 指定每秒钟导入的频率,单位次/秒,默认值为:4 399 | * @throws Exception 如果在导入的过程中由于字段问题或接口问题则抛出异常。 400 | * @return string 返回导入成功标志。 401 | */ 402 | public function pushHADocFile($fileName, $tableName, $offset = 1, 403 | $maxSize = self::PUSH_MAX_SIZE, $frequence = self::PUSH_FREQUENCE) { 404 | $reader = $this->_connect($fileName); 405 | 406 | // 默认doc初始结构。 407 | $doc = array('cmd' => '', 'fields' => array()); 408 | 409 | // 当前行号,用来记录当前已经解析到了第多少行。 410 | $lineNumber = 1; 411 | 412 | // 最新成功push数据的行号,用于如果在重新上传的时候设定offset偏移行号。 413 | $lastLineNumber = 0; 414 | 415 | // 最后更新的doc中的字段名,如果此行没有字段结束符,则下行的数据会被添加到这行的字段上。 416 | // 有一些富文本,在当前行没有结束此字段,则要记录最后的字段名称。 417 | // 例如: 418 | // rich_text=鲜花 419 | // 礼品专卖店^_ 420 | // other_field=xxx^_ 421 | $lastField = ''; 422 | 423 | // 当前还未上传的文档的大小。单位MB. 424 | $totalSize = 0; 425 | 426 | // 当前秒次已经发了多少次请求,用于限流。 427 | $timeFreq = 0; 428 | 429 | $time = time(); 430 | 431 | $buffer = array(); 432 | 433 | // 开始遍历文件。 434 | try { 435 | while ($line = fgets($reader)) { 436 | 437 | // 如果当前的行号小于设定的offset行号时跳过。 438 | if ($lineNumber < $offset) { 439 | continue; 440 | } 441 | 442 | // 获取结果当前行的最后两个字符。 443 | $separator = substr($line, -2); 444 | 445 | // 如果当前结束符是文档的结束符^^\n,则当前doc解析结束。并计算buffer+当前doc文档的 446 | // 大小,如果大于指定的文档大小,则push buffer到api,并清空buffer,同时把当前doc 447 | // 文档扔到buffer中。 448 | if ($separator == self::HA_DOC_ITEM_SEPARATOR) { 449 | 450 | $lastField = ''; 451 | 452 | // 获取当前文档生成json并urlencode之后的size大小。 453 | $json = json_encode($doc); 454 | $currentSize = strlen(urlencode($json)); 455 | 456 | // 如果计算的大小+buffer的大小大于等于限定的阀值self::PUSH_MAX_SIZE,则push 457 | // buffer数据。 458 | if ($currentSize + $totalSize >= self::PUSH_MAX_SIZE * 1024 * 1024) { 459 | 460 | // push 数据到api。 461 | $return = json_decode($this->add($buffer, $tableName), true); 462 | // 如果push不成功则抛出异常。 463 | if ('OK' != $return['status']) { 464 | throw new Exception("Api returns error. " . $return['errors'][0]['message']); 465 | } else { 466 | // 如果push成功,则计算每秒钟的push的频率并如果超过频率则sleep。 467 | $lastLineNumber = $lineNumber; 468 | $newTime = microtime(true); 469 | $timeFreq++; 470 | 471 | // 如果时间为上次的push时间且push频率超过设定的频率,则unsleep 剩余的毫秒数。 472 | if (floor($newTime) == $time && $timeFreq >= self::PUSH_FREQUENCE) { 473 | usleep((floor($newTime) + 1 - $newTime) * 1000000); 474 | $timeFreq = 0; 475 | } 476 | // 重新设定时间和频率。 477 | $newTime = floor(microtime(true)); 478 | if ($time != $newTime) { 479 | $time = $newTime; 480 | $timeFreq = 0; 481 | } 482 | } 483 | 484 | // 重置buffer为空,并重新设定total size 为0; 485 | $buffer = array(); 486 | $totalSize = 0; 487 | } 488 | // doc 添加到buffer中,并增加total size的大小。 489 | $buffer[] = $doc; 490 | $totalSize += $currentSize; 491 | 492 | // 初始化doc。 493 | $doc = array('cmd' => '', 'fields' => array()); 494 | } else if ($separator == self::HA_DOC_FIELD_SEPARATOR) { 495 | // 表示当前字段结束。 496 | $detail = substr($line, 0, -2); 497 | 498 | if (!empty($lastField)) { 499 | 500 | // 表示当前行非第一行数据,则获取最后生成的字段名称并给其赋值。 501 | $doc['fields'][$lastField] = 502 | $this->_extractFieldValue($doc['fields'][$lastField] . $detail); 503 | } else { 504 | 505 | // 表示当前为第一行数据,则解析key 和value。 506 | list($key, $value) = $this->_parseHADocField($detail); 507 | 508 | if (strtoupper($key) == 'CMD') { 509 | $doc['cmd'] = strtoupper($value); 510 | } else { 511 | $doc['fields'][$key] = $this->_extractFieldValue($value); 512 | } 513 | } 514 | 515 | // 设置字段名称为空。 516 | $lastField = ''; 517 | } else { 518 | // 此else 表示富文本的非最后一行。 519 | 520 | // 表示富文本非第一行。 521 | if (!empty($lastField)) { 522 | $doc['fields'][$lastField] .= $line; 523 | } else { 524 | // 表示字段的第一行数据。 525 | list($key, $value) = $this->_parseHADocField($line); 526 | 527 | $doc['fields'][$key] = $value; 528 | $lastField = $key; 529 | } 530 | } 531 | $lineNumber++; 532 | } 533 | 534 | fclose($reader); 535 | 536 | // 如果buffer 中还有数据则再push一次数据。 537 | if (!empty($buffer)) { 538 | $return = json_decode($this->add($buffer, $tableName), true); 539 | if (self::PUSH_RETURN_STATUS_OK != $return['status']) { 540 | throw new Exception("Api returns error. " . $return['errors'][0]['message']); 541 | } 542 | } 543 | 544 | if (!empty($doc['fields'])) { 545 | throw new Exception('Fail to push doc:' . json_encode($doc)); 546 | } 547 | 548 | return json_encode( 549 | array('status' => 'OK', 'message' => 'The data is posted successfully.') 550 | ); 551 | } catch (Exception $e) { 552 | throw new Exception( 553 | $e->getMessage() . 554 | '. Latest posted successful line no is ' . $lastLineNumber 555 | ); 556 | } 557 | } 558 | 559 | /** 560 | * 创建一个文件指针资源。 561 | * @param string $fileName 562 | * @throws Exception 563 | * @return resource 返回文件指针。 564 | */ 565 | private function _connect($fileName) 566 | { 567 | $reader = fopen($fileName, "r"); 568 | if (!$reader) { 569 | throw new Exception("The file is not exists or not readabled. Please 570 | check your file."); 571 | } 572 | return $reader; 573 | } 574 | 575 | /** 576 | * 解析一段字符串并生成key和value。 577 | * @param string $string 578 | * @return string|boolean 返回一个数组有两个字段,第一个为key,第二个为value。如果解析 579 | * 失败则返回错误。 580 | */ 581 | private function _parseHADocField($string) 582 | { 583 | $separater = '='; 584 | $pos = strpos($string, $separater); 585 | 586 | if ($pos !== false) { 587 | $key = substr($string, 0, $pos); 588 | $value = substr($string, $pos + 1); 589 | return array($key, $value); 590 | } else { 591 | throw new Exception('The are no key and value in the field.'); 592 | } 593 | } 594 | 595 | /** 596 | * 检查字段值的值是否为多值字段,如果是则返回多值的数组,否则返回一个string的结果。 597 | * @param string $value 需要解析的结果。 598 | * @return string|string 如果非多值则返回字符串,否则返回多值数组。 599 | */ 600 | private function _extractFieldValue($value) 601 | { 602 | $split = explode(self::HA_DOC_MULTI_VALUE_SEPARATOR, $value); 603 | return count($split) > 1 ? $split : $split[0]; 604 | } 605 | } 606 | -------------------------------------------------------------------------------- /src/Sdk/CloudsearchClient.php: -------------------------------------------------------------------------------- 1 | key_type = $key_type; 197 | 198 | if ($this->key_type == 'opensearch') { 199 | $this->clientId = $key; 200 | $this->clientSecret = $secret; 201 | } elseif ($this->key_type == 'aliyun') { 202 | $this->accessKeyId = $key; 203 | $this->secret = $secret; 204 | } else { 205 | $this->key_type = 'opensearch'; 206 | $this->clientId = $key; 207 | $this->clientSecret = $secret; 208 | } 209 | if (isset($opts['host']) && !empty($opts['host'])) { 210 | //对于用户通过参数指定的host,需要检查host结尾是否有/,有则去掉 211 | if (substr($opts['host'], -1) == "/") { 212 | $this->host = trim($opts['host'], '/'); 213 | } else { 214 | $this->host = $opts['host']; 215 | } 216 | } 217 | 218 | if (isset($opts['version']) && !empty($opts['version'])) { 219 | $this->version = $opts['version']; 220 | } 221 | 222 | if (isset($opts['timeout']) && !empty($opts['timeout'])) { 223 | $this->timeout = $opts['timeout']; 224 | } 225 | 226 | if (isset($opts['connect_timeout']) && !empty($opts['connect_timeout'])) { 227 | $this->connect_timeout = $opts['connect_timeout']; 228 | } 229 | 230 | if (isset($opts['gzip']) && $opts['gzip'] == true) { 231 | $this->gzip = true; 232 | } 233 | 234 | if (isset($opts['debug']) && $opts['debug'] == true) { 235 | $this->debug = true; 236 | } 237 | 238 | if (isset($opts['signatureMethod']) && !empty($opts['signatureMethod'])) { 239 | $this->signatureMethod = $opts['signatureMethod']; 240 | } 241 | 242 | if (isset($opts['signatureVersion']) && !empty($opts['signatureVersion'])) { 243 | $this->signatureVersion = $opts['signatureVersion']; 244 | } 245 | 246 | $this->baseURI = rtrim($this->host, '/'); 247 | 248 | } 249 | 250 | /** 251 | * 请求服务器 252 | * 253 | * 向服务器发出请求并获得返回结果。 254 | * 255 | * @param string $path 当前请求的path路径。 256 | * @param array $params 当前请求的所有参数数组。 257 | * @param string $method 当前请求的方法。默认值为:GET 258 | * @return string 返回获取的结果。 259 | * @donotgeneratedoc 260 | */ 261 | public function call($path, $params = array(), $method = self::METHOD) 262 | { 263 | $url = $this->baseURI . $path; 264 | if ($this->key_type == 'opensearch') { 265 | $params['client_id'] = $this->clientId; 266 | $params['nonce'] = $this->_nonce(); 267 | $params['sign'] = $this->_sign($params); 268 | } else { 269 | $params['Version'] = $this->version; 270 | $params['AccessKeyId'] = $this->accessKeyId; 271 | $params['SignatureMethod'] = $this->signatureMethod; 272 | $params['SignatureVersion'] = $this->signatureVersion; 273 | $params['SignatureNonce'] = $this->_nonce_aliyun(); 274 | $params['Timestamp'] = gmdate('Y-m-d\TH:i:s\Z'); 275 | $params['Signature'] = $this->_sign_aliyun($params, $method); 276 | } 277 | if ($this->connect == 'curl') { 278 | $result = json_decode($this->_curl($url, $params, $method), true); 279 | } else { 280 | $result = json_decode($this->_socket($url, $params, $method), true); 281 | } 282 | 283 | if ($result['status'] != 'OK') { 284 | $e = new OpensearchRunException($result['errors'][0]['message'], $result['errors'][0]['code']); 285 | $e->setErrors($result['errors']); 286 | 287 | throw $e; 288 | } 289 | 290 | return $result; 291 | } 292 | 293 | /** 294 | * 生成当前的nonce值。 295 | * 296 | * NOTE: $time为10位的unix时间戳。 297 | * 298 | * @return string 返回生成的nonce串。 299 | */ 300 | protected function _nonce() 301 | { 302 | $time = time(); 303 | return md5($this->clientId . $this->clientSecret . $time) . '.' . $time; 304 | } 305 | 306 | /** 307 | * 生产当前的aliyun签名方式对应的nonce值 308 | * 309 | * NOTE:这个值要保证访问唯一性,建议用如下算法,商家也可以自己设置一个唯一值 310 | * 311 | * @return string 返回生产的nonce串 312 | */ 313 | protected function _nonce_aliyun() 314 | { 315 | $microtime = $this->get_microtime(); 316 | return $microtime . mt_rand(1000, 9999); 317 | } 318 | 319 | /** 320 | * 根据参数生成当前的签名。 321 | * 322 | * 如果指定了sign_mode且sign_mode为1,则参数中的items将不会被计算签名。 323 | * 324 | * @param array $params 返回生成的签名。 325 | * @return string 326 | */ 327 | protected function _sign($params = array()) 328 | { 329 | $query = ""; 330 | if (isset($params['sign_mode']) && $params['sign_mode'] == 1) { 331 | unset($params['items']); 332 | } 333 | if (is_array($params) && !empty($params)) { 334 | ksort($params); 335 | $query = $this->_buildQuery($params); 336 | } 337 | return md5($query . $this->clientSecret); 338 | } 339 | 340 | /** 341 | * 根据参数生成当前得签名 342 | * 343 | * 如果指定了sign_mode且sign_mode为1,则参数中的items将不会被计算签名 344 | * 345 | * @param array $params 返回生成签名 346 | * @return string 347 | */ 348 | protected function _sign_aliyun($params = array(), $method = self::METHOD) 349 | { 350 | if (isset($params['sign_mode']) && $params['sign_mode'] == 1) { 351 | unset($params['items']); 352 | } 353 | $params = $this->_params_filter($params); 354 | $query = ''; 355 | $arg = ''; 356 | if (is_array($params) && !empty($params)) { 357 | while (list($key, $val) = each($params)) { 358 | $arg .= $this->_percentEncode($key) . "=" . $this->_percentEncode($val) . "&"; 359 | } 360 | $query = substr($arg, 0, count($arg) - 2); 361 | } 362 | $base_string = strtoupper($method) . '&%2F&' . $this->_percentEncode($query); 363 | return base64_encode(hash_hmac('sha1', $base_string, $this->secret . "&", true)); 364 | } 365 | 366 | /** 367 | * 过滤阿里云签名中不用来签名的参数,并且排序 368 | * 369 | * @param array $params 370 | * @return array 371 | * 372 | */ 373 | protected function _params_filter($parameters = array()) 374 | { 375 | $params = array(); 376 | while (list($key, $val) = each($parameters)) { 377 | if ($key == "Signature" || $val === "" || $val === null) { 378 | continue; 379 | } else { 380 | $params[$key] = $parameters[$key]; 381 | } 382 | } 383 | ksort($params); 384 | reset($params); 385 | return $params; 386 | } 387 | 388 | protected function _percentEncode($str) 389 | { 390 | // 使用urlencode编码后,将"+","*","%7E"做替换即满足 API规定的编码规范 391 | $res = urlencode($str); 392 | $res = preg_replace('/\+/', '%20', $res); 393 | $res = preg_replace('/\*/', '%2A', $res); 394 | $res = preg_replace('/%7E/', '~', $res); 395 | return $res; 396 | } 397 | /** 398 | * 通过curl的方式获取请求结果。 399 | * @param string $url 请求的URI。 400 | * @param array $params 请求的参数数组。 401 | * @param string $method 请求的方法,默认为self::METHOD。 402 | * @return string 返回获取的结果。 403 | */ 404 | private function _curl($url, $params = array(), $method = self::METHOD) 405 | { 406 | $query = $this->_buildQuery($params); 407 | $method = strtoupper($method); 408 | 409 | if ($method == self::METHOD_GET) { 410 | $url .= preg_match('/\?/i', $url) ? '&' . $query : '?' . $query; 411 | } else { 412 | $method = self::METHOD_POST; 413 | } 414 | 415 | $options = array( 416 | CURLOPT_HTTP_VERSION => 'CURL_HTTP_VERSION_1_1', 417 | CURLOPT_CONNECTTIMEOUT => $this->connect_timeout, 418 | CURLOPT_TIMEOUT => $this->timeout, 419 | CURLOPT_CUSTOMREQUEST => $method, 420 | CURLOPT_HEADER => false, 421 | CURLOPT_RETURNTRANSFER => true, 422 | CURLOPT_USERAGENT => "opensearch/php sdk " . $this->sdkVersion, //php sdk 版本信息 423 | CURLOPT_HTTPHEADER => array('Expect:'), 424 | ); 425 | 426 | if ($method == self::METHOD_POST) { 427 | $options[CURLOPT_POSTFIELDS] = $params; 428 | } 429 | 430 | if ($this->gzip) { 431 | $options[CURLOPT_ENCODING] = 'gzip'; 432 | } 433 | 434 | $session = curl_init($url); 435 | curl_setopt_array($session, $options); 436 | $response = curl_exec($session); 437 | $info = curl_getinfo($session); 438 | 439 | if ($this->debug) { 440 | $this->debugInfo = $info; //query基本信息,供调试使用 441 | } 442 | 443 | curl_close($session); 444 | 445 | return $response; 446 | } 447 | 448 | /** 449 | * 通过socket的方式获取请求结果。 450 | * @param string $url 请求的URI。 451 | * @param array $params 请求的参数数组。 452 | * @param string $method 请求方法,默认为self::METHOD。 453 | * @throws Exception 454 | * @return string 455 | */ 456 | private function _socket($url, $params = array(), $method = self::METHOD) 457 | { 458 | $method = strtoupper($method); 459 | 460 | $parse = $this->_parseUrl($url); 461 | $content = $this->_buildRequestContent( 462 | $parse, 463 | $method, 464 | http_build_query($params, '', '&') 465 | ); 466 | if ($this->debug) { 467 | $this->debugInfo = $content; 468 | } 469 | 470 | $socket = fsockopen( 471 | $parse['host'], 472 | $parse["port"], 473 | $errno, 474 | $errstr, 475 | $this->connect_timeout 476 | ); 477 | 478 | stream_set_timeout($socket, $this->timeout); 479 | 480 | if (!$socket) { 481 | throw new OpensearchCallException("Connect " . $parse['host'] . ' fail.'); 482 | } 483 | 484 | $response = ''; 485 | fwrite($socket, $content); 486 | while ($data = fgets($socket)) { 487 | $response .= $data; 488 | } 489 | fclose($socket); 490 | 491 | $ret = $this->_parseResponse($response); 492 | return $ret['result']; 493 | } 494 | 495 | /** 496 | * 调试接口 497 | * 498 | * 获取SDK调用的调试信息,需要指定debug=true才能使用 499 | * 500 | * @return array\null 调试开关(debug)打开时返回调试信息。 501 | */ 502 | public function getRequest() 503 | { 504 | if ($this->debug) { 505 | return $this->debugInfo; 506 | } else { 507 | return null; 508 | } 509 | } 510 | 511 | /** 512 | * 解析http返回的结果,并分析出response 头和body。 513 | * @param string $response_text 514 | * @return array 515 | */ 516 | private function _parseResponse($response) 517 | { 518 | list($headerContent) = explode("\r\n\r\n", $response); 519 | $header = $this->_parseHttpSocketHeader($headerContent); 520 | $response = trim(stristr($response, "\r\n\r\n"), "\r\n"); 521 | 522 | $ret = array(); 523 | $ret["result"] = 524 | (isset($header['Content-Encoding']) && 525 | trim($header['Content-Encoding']) == 'gzip') ? 526 | $this->_gzdecode($response, $header) : $this->_checkChunk($response, $header); 527 | $ret["info"]["http_code"] = 528 | isset($header["http_code"]) ? $header["http_code"] : 0; 529 | $ret["info"]["headers"] = $header; 530 | 531 | return $ret; 532 | } 533 | 534 | /** 535 | * 生成http头信息。 536 | * 537 | * @param array $parse 538 | * @param string $method HTTP方法。 539 | * @param string $data HTTP参数串。 540 | * @return string 541 | */ 542 | private function _buildRequestContent(&$parse, $method, $data) 543 | { 544 | $strLength = ''; 545 | $content = ''; 546 | 547 | if ($method == self::METHOD_GET) { 548 | $data = ltrim($data, '&'); 549 | $query = isset($parse['query']) ? $parse['query'] : ''; 550 | $parse['path'] .= ($query ? '&' : '?') . $data; 551 | } else { 552 | $method = self::METHOD_POST; 553 | $strLength = "Content-length: " . strlen($data) . "\r\n"; 554 | $content = $data; 555 | } 556 | 557 | $write = $method . " " . $parse['path'] . " HTTP/1.0\r\n"; 558 | $write .= "Host: " . $parse['host'] . "\r\n"; 559 | $write .= "Content-type: application/x-www-form-urlencoded\r\n"; 560 | $write .= "User-Agent: opensearch/php sdk " . $this->sdkVersion . "\r\n"; 561 | if ($this->gzip) { 562 | $write .= "Accept-Encoding: gzip\r\n"; 563 | } 564 | $write .= $strLength; 565 | $write .= "Connection: close\r\n\r\n"; 566 | $write .= $content; 567 | 568 | return $write; 569 | } 570 | 571 | /** 572 | * 把数组生成http请求需要的参数。 573 | * @param array $params 574 | * @return string 575 | */ 576 | private function _buildQuery($params) 577 | { 578 | $args = http_build_query($params, '', '&'); 579 | // remove the php special encoding of parameters 580 | // see http://www.php.net/manual/en/function.http-build-query.php#78603 581 | //return preg_replace('/%5B(?:[0-9]|[1-9][0-9]+)%5D=/', '=', $args); 582 | return $args; 583 | } 584 | 585 | /** 586 | * 解析URL并生成host、schema、path、query等信息。 587 | * @param string $url 588 | * @throws Exception 589 | * @return Ambigous 590 | */ 591 | private function _parseUrl($url) 592 | { 593 | $parse = parse_url($url); 594 | if (empty($parse) || !is_array($parse)) { 595 | throw new Exception("Host is empty."); 596 | } 597 | 598 | if (!isset($parse['port']) || !$parse['port']) { 599 | $parse['port'] = '80'; 600 | } 601 | 602 | $parse['host'] = str_replace( 603 | array('http://', 'https://'), 604 | array('', 'ssl://'), 605 | $parse['scheme'] . "://" 606 | ) . $parse['host']; 607 | 608 | $parse["path"] = isset($parse["path"]) ? $parse["path"] : '/'; 609 | $query = isset($parse['query']) ? $parse['query'] : ''; 610 | 611 | $path = str_replace(array('\\', '//'), '/', $parse['path']); 612 | $parse['path'] = $query ? $path . "?" . $query : $path; 613 | 614 | return $parse; 615 | } 616 | 617 | /** 618 | * 解析返回的header头。 619 | * @param string $str 头信息。 620 | * @return array 返回头信息的数组。 621 | */ 622 | private static function _parseHttpSocketHeader($str) 623 | { 624 | $slice = explode("\r\n", $str); 625 | $headers = array(); 626 | 627 | foreach ($slice as $v) { 628 | if (false !== strpos($v, "HTTP")) { 629 | list(, $headers["http_code"]) = explode(" ", $v); 630 | $headers["status"] = $v; 631 | } else { 632 | $item = explode(":", $v); 633 | $headers[$item[0]] = isset($item[1]) ? $item[1] : ''; 634 | } 635 | } 636 | 637 | return $headers; 638 | } 639 | 640 | /** 641 | * 解压缩gzip生成的数据。 642 | * 643 | * @param string $data 压缩的数据。 644 | * @return string 解压缩的数据。 645 | */ 646 | private static function _gzdecode($data, $header, $rn = "\r\n") 647 | { 648 | if (isset($header['Transfer-Encoding'])) { 649 | $lrn = strlen($rn); 650 | $str = ''; 651 | $ofs = 0; 652 | do { 653 | $p = strpos($data, $rn, $ofs); 654 | $len = hexdec(substr($data, $ofs, $p - $ofs)); 655 | $str .= substr($data, $p + $lrn, $len); 656 | $ofs = $p + $lrn * 2 + $len; 657 | } while ($data[$ofs] !== '0'); 658 | $data = $str; 659 | } 660 | if (isset($header['Content-Encoding'])) { 661 | $data = gzinflate(substr($data, 10)); 662 | } 663 | return $data; 664 | } 665 | 666 | /** 667 | * 检查当前是否是返回chunk,如果是的话,从body中获取content长度并截取。 668 | * 669 | * @param string $data body内容。 670 | * @param array $header header头信息的数组。 671 | * @param string $rn chunk的截取字符串。 672 | * 673 | * @return string 如果为chunk则返回正确的body内容,否则全部返回。 674 | */ 675 | private static function _checkChunk($data, $header, $rn = "\r\n") 676 | { 677 | if (isset($header['Transfer-Encoding'])) { 678 | $lrn = strlen($rn); 679 | $p = strpos($data, $rn, 0); 680 | $len = hexdec(substr($data, 0, $p)); 681 | $data = substr($data, $p + 2, $len); 682 | } 683 | return $data; 684 | } 685 | 686 | protected function get_microtime() 687 | { 688 | list($usec, $sec) = explode(" ", microtime()); 689 | return floor(((float) $usec + (float) $sec) * 1000); 690 | } 691 | } 692 | -------------------------------------------------------------------------------- /src/Sdk/CloudsearchSearch.php: -------------------------------------------------------------------------------- 1 | 33 | * $search = new CloudsearchSearch($client); 34 | * $search->search(array('indexes' => 'my_indexname')); 35 | * 36 | * 或 37 | * 38 | * 39 | * $search = new CloudsearchSearch($client); 40 | * $search->addIndex('my_indexname'); 41 | * $search->search(); 42 | * 43 | * 44 | */ 45 | class CloudsearchSearch 46 | { 47 | /** 48 | * 设定搜索结果集升降排序的标志,"+"为升序,"-"为降序。 49 | * 50 | * @var string 51 | */ 52 | const SORT_INCREASE = '+'; 53 | const SORT_DECREASE = '-'; 54 | 55 | const SEARCH_TYPE_SCAN = "scan"; 56 | 57 | /** 58 | * 和API服务进行交互的对象。 59 | * @var CloudsearchClient 60 | */ 61 | private $client; 62 | 63 | /** 64 | * 此次检索指定的应用名称。 65 | * 66 | * 可以指定单个应用名称,也可以指定多个应用名称结合。 67 | * 68 | * @var array 69 | */ 70 | private $indexes = array(); 71 | 72 | /** 73 | * 指定某些字段的一些summary展示规则。 74 | * 75 | * 这些字段必需为可分词的text类型的字段。 76 | * 77 | * 例如: 78 | * 指定title字段为: summary_field=>title 79 | * 指定title长度为50:summary_len=>50 80 | * 指定title飘红标签:summary_element=>em 81 | * 指定title省略符号:summary_ellipsis=>... 82 | * 指定summary缩略段落个数:summary_snipped=>1 83 | * 那么当前的字段值为: 84 | * 85 | * array('title' => array( 86 | * 'summary_field' => 'title', 87 | * 'summary_len' => 50, 88 | * 'summary_element' => 'em', 89 | * 'summary_ellipsis' => '...', 90 | * 'summary_snipped' => 1, 91 | * 'summary_element_prefix' => 'em', 92 | * 'summary_element_postfix' => '/em') 93 | * ); 94 | * 95 | * @var array 96 | */ 97 | private $summary = array(); 98 | 99 | /** 100 | * config 子句。 101 | * 102 | * config子句只能接收三个参数(start, format, hit),其中: 103 | * start为当前结果集的偏移量; 104 | * format为当前返回结果的格式,有json,xml和protobuf三种格式; 105 | * hit为当前获取结果条数。 106 | * 107 | * 例如 "start:0,format:xml,hit:20" 108 | * 109 | * @var string 110 | */ 111 | private $clauseConfig = ''; 112 | 113 | /** 114 | * 返回的数据的格式,有json、xml,protobuf三种类型可选;默认为XML格式。 115 | * @var string 116 | */ 117 | private $format = 'xml'; 118 | 119 | /** 120 | * 设定返回结果集的offset,默认为0。 121 | * @var int 122 | */ 123 | private $start = 0; 124 | 125 | /** 126 | * 设定返回结果集的个数,默认为20。 127 | * @var int 128 | */ 129 | private $hits = 20; 130 | 131 | /** 132 | * 设定排序规则。 133 | * @var array 134 | */ 135 | private $sort = array(); 136 | 137 | /** 138 | * 设定过滤条件。 139 | * @var string 140 | */ 141 | private $filter = ''; 142 | 143 | /** 144 | * aggregate设定规则。 145 | * @var array 146 | */ 147 | private $aggregate = array(); 148 | 149 | /** 150 | * distinct 排序。 151 | * @var array 152 | */ 153 | private $distinct = array(); 154 | 155 | /** 156 | * 返回字段过滤。 157 | * 158 | * 如果设定了此字段,则只返回此字段里边的field。 159 | * @var array 160 | */ 161 | private $fetches = array(); 162 | 163 | /** 164 | * rerankSize表示参与精排算分的文档个数,一般不用使用默认值就能满足,不用设置,会自动使用默认值200 165 | * @var int 166 | */ 167 | private $rerankSize = 200; 168 | 169 | /** 170 | * query 子句。 171 | * 172 | * query子句可以为query='鲜花',也可以指定索引来搜索,例如:query=title:'鲜花'。 173 | * 详情请浏览setQueryString($query)方法。 174 | * 175 | * @var string 176 | */ 177 | private $query; 178 | 179 | /** 180 | * 指定表达式名称,表达式名称和结构在网站中指定。 181 | * 182 | * 183 | * @var string 184 | */ 185 | private $formulaName = ''; 186 | 187 | /** 188 | * 指定粗排表达式名称,表达式名称和结构在网站中指定。 189 | * @var string 190 | */ 191 | private $firstFormulaName = ''; 192 | 193 | /** 194 | * 指定kvpairs子句的内容,内容为k1:v1,k2:v2的方式表示。 195 | * @var string 196 | */ 197 | private $kvpair = ''; 198 | 199 | /** 200 | * 指定qp 名称。 201 | * @var array 202 | */ 203 | private $QPName = array(); 204 | 205 | /** 206 | * 指定关闭的方法名称。 207 | * @var unknown 208 | */ 209 | private $functions = array(); 210 | 211 | /** 212 | * 设定自定义参数。 213 | * 214 | * 如果api有新功能(参数)发布,用户不想更新sdk版本,则可以自己来添加自定义的参数。 215 | * 216 | * @var string 217 | */ 218 | private $customParams = array(); 219 | 220 | private $scrollId = null; 221 | 222 | private $searchType = ''; 223 | 224 | private $scroll = null; 225 | 226 | /** 227 | * 请求API的部分path。 228 | * @var string 229 | */ 230 | private $path = '/search'; 231 | 232 | /** 233 | * 构造函数 234 | * 235 | * @param CloudsearchClient $client 此对象由CloudsearchClient类实例化。 236 | */ 237 | public function __construct($client) 238 | { 239 | $this->client = $client; 240 | } 241 | 242 | /** 243 | * 设置scroll扫描起始id 244 | * 245 | * @param scrollId 扫描起始id 246 | */ 247 | public function setScrollId($scrollId) 248 | { 249 | $this->scrollId = $scrollId; 250 | } 251 | 252 | /** 253 | * 获取scroll扫描起始id 254 | * 255 | * @return string 扫描起始id 256 | */ 257 | public function getScrollId() 258 | { 259 | return $this->scrollId; 260 | } 261 | 262 | /** 263 | * 请求scroll api。 264 | * 265 | * 类似search接口,但是不支持sort, aggregate, distinct, formula_name, summary及qp, 266 | * start 等功能。 267 | * 268 | * scroll实现方式: 269 | * 第一次正常带有指定的子句和参数调用scroll接口,此接口会返回scroll_id信息。 270 | * 第二次请求时只带此scroll_id信息和scroll参数即可。 271 | * 272 | * 类似第一次请求: 273 | * $search = new CloudsearchSearch($client); 274 | * $search->addIndex("juhuasuan"); 275 | * $search->setQueryString("default:'酒店'"); 276 | * $search->setFormat('json'); 277 | * $search->setHits(10); 278 | * $search->setScroll("1m"); 279 | * $result = $search->scroll(); 280 | * 281 | * $array = json_decode($result, true); 282 | * $scrollId = $array['result']['scroll_id']; 283 | * 284 | * 第二次请求: 285 | * $search = new CloudsearchSearch($client); 286 | * $search->setScroll("1m"); 287 | * $search->setScrollId($scrollId); 288 | * $result = $search->scroll(); 289 | * 290 | * @param array $opts 扫描请求所需参数 291 | * @return string 扫描结果 292 | */ 293 | public function scroll($opts = array()) 294 | { 295 | $this->extract($opts, "scroll"); 296 | return $this->call('scroll'); 297 | } 298 | 299 | /** 300 | * 执行搜索 301 | * 302 | * 执行向API提出搜索请求。 303 | * 更多说明请参见 [API 配置config子句]({{!api-reference/query-clause&config-clause!}}) 304 | * @param array $opts 此参数如果被复制,则会把此参数的内容分别赋给相应的变量。此参数的值可能有以下内容: 305 | * @subparam string query 指定的搜索查询串,可以为query=>"索引名:'鲜花'"。 306 | * @subparam array indexes 指定的搜索应用,可以为一个应用,也可以多个应用查询。 307 | * @subparam array fetch_fields 设定返回的字段列表,如果只返回url和title,则为 array('url', 'title')。 308 | * @subparam string format 指定返回的数据格式,有json,xml和protobuf三种格式可选。默认值为:'xml' 309 | * @subparam string formula_name 指定的表达式名称,此名称需在网站中设定。 310 | * @subparam array summary 指定summary字段一些标红、省略、截断等规则。 311 | * @subparam int start 指定搜索结果集的偏移量。默认为0。 312 | * @subparam int hits 指定返回结果集的数量。默认为20。 313 | * @subparam array sort 指定排序规则。默认值为:'self::SORT_DECREASE' (降序) 314 | * @subparam string filter 指定通过某些条件过滤结果集。 315 | * @subparam array aggregate 指定统计类的信息。 316 | * @subparam array distinct 指定distinct排序。 317 | * @subparam string kvpair 指定的kvpair。 318 | * 319 | * @return string 返回搜索结果。 320 | * 321 | */ 322 | public function search($opts = array()) 323 | { 324 | $this->extract($opts); 325 | return $this->call(); 326 | } 327 | 328 | /** 329 | * 增加新的应用来进行检索 330 | * @param string\array $indexName 应用名称或应用名称列表. 331 | */ 332 | public function addIndex($indexName) 333 | { 334 | if (is_array($indexName)) { 335 | $this->indexes = $indexName; 336 | } else { 337 | $this->indexes[] = $indexName; 338 | } 339 | $this->indexes = array_unique($this->indexes); 340 | } 341 | 342 | /** 343 | * 删除待搜索的应用 344 | * 345 | * 在当前检索中删除此应用的检索结果。 346 | * @param string $indexName 待删除的应用名称 347 | */ 348 | public function removeIndex($indexName) 349 | { 350 | $flip = array_flip($this->indexes); 351 | unset($flip[$indexName]); 352 | $this->indexes = array_values(array_flip($flip)); 353 | } 354 | 355 | /** 356 | * 获得请求应用列表 357 | * 358 | * 当前请求中所有的应用名列表。 359 | * 360 | * @return array 返回当前搜索的所有应用列表。 361 | */ 362 | public function getSearchIndexes() 363 | { 364 | return $this->indexes; 365 | } 366 | 367 | /** 368 | * 设置表达式名称 369 | * 此表达式名称和结构需要在网站中已经设定。 370 | * @param string $formulaName 表达式名称。 371 | */ 372 | public function setFormulaName($formulaName) 373 | { 374 | $this->formulaName = $formulaName; 375 | } 376 | 377 | /** 378 | * 获取表达式名称 379 | * 380 | * 获得当前请求中设置的表达式名称。 381 | * 382 | * @return string 返回当前设定的表达式名称。 383 | */ 384 | public function getFormulaName() 385 | { 386 | return $this->formulaName; 387 | } 388 | 389 | /** 390 | * 清空精排表达式名称设置 391 | */ 392 | public function clearFormulaName() 393 | { 394 | $this->formulaName = ''; 395 | } 396 | 397 | /** 398 | * 设置粗排表达式名称 399 | * 400 | * 此表达式名称和结构需要在网站中已经设定。 401 | * 402 | * @param string $FormulaName 表达式名称。 403 | */ 404 | public function setFirstFormulaName($formulaName) 405 | { 406 | $this->firstFormulaName = $formulaName; 407 | } 408 | 409 | /** 410 | * 获取粗排表达式设置 411 | * 412 | * 获取当前设置的粗排表达式名称。 413 | * 414 | * @return string 返回当前设定的表达式名称。 415 | */ 416 | public function getFirstFormulaName() 417 | { 418 | return $this->firstFormulaName; 419 | } 420 | 421 | /** 422 | * 清空粗排表达式名称设置 423 | */ 424 | public function clearFirstFormulaName() 425 | { 426 | $this->firstFormulaName = ''; 427 | } 428 | 429 | /** 430 | * 添加一条summary信息 431 | * @param string $fieldName 指定的生效的字段。此字段必需为可分词的text类型的字段。 432 | * @param string $len 指定结果集返回的词字段的字节长度,一个汉字为2个字节。 433 | * @param string $element 指定命中的query的标红标签,可以为em等。 434 | * @param string $ellipsis 指定用什么符号来标注未展示完的数据,例如“...”。 435 | * @param string $snipped 指定query命中几段summary内容。 436 | * @param string $elementPrefix 如果指定了此参数,则标红的开始标签以此为准。 437 | * @param string $elementPostfix 如果指定了此参数,则标红的结束标签以此为准。 438 | */ 439 | public function addSummary($fieldName, $len = 0, $element = '', 440 | $ellipsis = '', $snipped = 0, $elementPrefix = '', $elementPostfix = '') { 441 | if (empty($fieldName)) { 442 | return false; 443 | } 444 | 445 | $summary = array(); 446 | $summary['summary_field'] = $fieldName; 447 | empty($len) || $summary['summary_len'] = (int) $len; 448 | empty($element) || $summary['summary_element'] = $element; 449 | empty($ellipsis) || $summary['summary_ellipsis'] = $ellipsis; 450 | empty($snipped) || $summary['summary_snipped'] = $snipped; 451 | empty($elementPrefix) || $summary['summary_element_prefix'] = $elementPrefix; 452 | empty($elementPostfix) || $summary['summary_element_postfix'] = $elementPostfix; 453 | 454 | $this->summary[$fieldName] = $summary; 455 | } 456 | 457 | /** 458 | * 获取当前的summary信息 459 | * 可以通过指定字段名称返回指定字段的summary信息 460 | * 461 | * @param string $field 指定的字段,如果此字段为空,则返回整个summary信息,否则返回指定field的summary信息。 462 | * @return array 返回summary信息。 463 | */ 464 | public function getSummary($field = '') 465 | { 466 | return (!empty($field)) ? $this->summary[$field] : $this->summary; 467 | } 468 | 469 | /** 470 | * 获取summary字符串 471 | * 472 | * 把summary信息生成字符串并返回。 473 | * 474 | * @return string 返回字符串的summary信息。 475 | */ 476 | public function getSummaryString() 477 | { 478 | $summary = array(); 479 | if (is_array($s = $this->getSummary()) && !empty($s)) { 480 | foreach ($this->getSummary() as $summaryAttributes) { 481 | $item = array(); 482 | if (is_array($summaryAttributes) && !empty($summaryAttributes)) { 483 | foreach ($summaryAttributes as $k => $v) { 484 | $item[] = $k . ":" . $v; 485 | } 486 | } 487 | $summary[] = implode(",", $item); 488 | } 489 | } 490 | return implode(";", $summary); 491 | } 492 | 493 | /** 494 | * 设置返回的数据格式 495 | * 496 | * @param string $format 数据格式名称,有xml, json和protobuf 三种类型。 497 | */ 498 | public function setFormat($format) 499 | { 500 | $this->format = $format; 501 | } 502 | 503 | /** 504 | * 获取当前的数据格式名称 505 | * 506 | * @return string 返回当前的数据格式名称。 507 | */ 508 | public function getFormat() 509 | { 510 | return $this->format; 511 | } 512 | 513 | /** 514 | * 设置返回结果的offset偏移量 515 | * 516 | * @param int $start 偏移量。 517 | */ 518 | public function setStartHit($start) 519 | { 520 | $this->start = (int) $start; 521 | } 522 | 523 | /** 524 | * 获取返回结果的offset偏移量 525 | * 526 | * @return int 返回当前设定的偏移量。 527 | */ 528 | public function getStartHit() 529 | { 530 | return $this->start; 531 | } 532 | 533 | /** 534 | * 设置结果集大小 535 | * 536 | * 设置当前返回结果集的doc个数。 537 | * 538 | * @param number $hits 指定的doc个数。默认值:20 539 | */ 540 | public function setHits($hits = 20) 541 | { 542 | $this->hits = (int) $hits; 543 | } 544 | 545 | /** 546 | * 获取结果集大小 547 | * 548 | * 获取当前设定的结果集的doc数。 549 | * 550 | * @return number 返回当前指定的doc个数。 551 | */ 552 | public function getHits() 553 | { 554 | return $this->hits; 555 | } 556 | 557 | /** 558 | * 添加排序设置 559 | * 560 | * 增加一个排序字段及排序方式。 561 | * 更多说明请参见[API 排序sort子句]({{!api-reference/query-clause&sort-clause!}}) 562 | * @param string $field 字段名称。 563 | * @param string $sortChar 排序方式,有升序+和降序-两种方式。 564 | */ 565 | public function addSort($field, $sortChar = self::SORT_DECREASE) 566 | { 567 | $this->sort[$field] = $sortChar; 568 | } 569 | 570 | /** 571 | * 删除指定字段的排序 572 | * 573 | * @param string $field 指定的字段名称。 574 | */ 575 | public function removeSort($field) 576 | { 577 | unset($this->sort[$field]); 578 | } 579 | 580 | /** 581 | * 获取排序信息 582 | * 583 | * @param string $sortKey 如果此字段为空,则返回所有排序信息,否则只返回指定字段的排序值。 584 | * @return string\array 返回排序值。 585 | */ 586 | public function getSort($sortKey = '') 587 | { 588 | if (!empty($sortKey)) { 589 | return $this->sort[$sortKey]; 590 | } else { 591 | return $this->sort; 592 | } 593 | } 594 | 595 | /** 596 | * 获取排序字符串 597 | * 598 | * 把排序信息生成字符串并返回。 599 | * 600 | * @return string 返回字符串类型的排序规则。 601 | */ 602 | public function getSortString() 603 | { 604 | $sort = $this->getSort(); 605 | $sortString = array(); 606 | if (is_array($sort) && !empty($sort)) { 607 | foreach ($sort as $k => $v) { 608 | $sortString[] = $v . $k; 609 | } 610 | } 611 | return implode(";", $sortString); 612 | } 613 | 614 | /** 615 | * 添加过滤规则 616 | * 617 | * 针对指定的字段添加过滤规则。 618 | * 更多说明请参见 [API 过滤filter子句]({{!api-reference/query-clause&filter-clause!}}) 619 | * 620 | * @param string $filter 过滤规则,例如fieldName>=1。 621 | * @param string $operator 操作符,可以为 AND OR。默认值为:'AND' 622 | */ 623 | public function addFilter($filter, $operator = 'AND') 624 | { 625 | if (empty($this->filter)) { 626 | $this->filter = $filter; 627 | } else { 628 | $this->filter .= " {$operator} {$filter}"; 629 | } 630 | } 631 | 632 | /** 633 | * 获取过滤规则 634 | * 635 | * @return filter 返回字符串类型的过滤规则。 636 | */ 637 | public function getFilter() 638 | { 639 | return $this->filter; 640 | } 641 | 642 | /** 643 | * 添加统计信息相关参数 644 | * 645 | * 一个关键词通常能命中数以万计的文档,用户不太可能浏览所有文档来获取信息。而用户感兴趣的可 646 | * 能是一些统计类的信息,比如,查询“手机”这个关键词,想知道每个卖家所有商品中的最高价格。 647 | * 则可以按照卖家的user_id分组,统计每个小组中最大的price值: 648 | * groupKey:user_id, aggFun: max(price) 649 | * 更多说明请参见 [APi aggregate子句说明]({{!api-reference/query-clause&aggregate-clause!}}) 650 | * 651 | * @param string $groupKey 指定的group key. 652 | * @param string $aggFun 指定的function。当前支持:count、max、min、sum。 653 | * @param string $range 指定统计范围。 654 | * @param string $maxGroup 最大组个数。 655 | * @param string $aggFilter 表示仅统计满足特定条件的文档。 656 | * @param string $aggSamplerThresHold 抽样统计的阈值。表示该值之前的文档会依次统计,该值之后的文档会进行抽样统计。 657 | * @param string $aggSamplerStep 抽样统计的步长。 658 | */ 659 | public function addAggregate($groupKey, $aggFun, $range = '', $maxGroup = '', 660 | $aggFilter = '', $aggSamplerThresHold = '', $aggSamplerStep = '') { 661 | if (empty($groupKey) || empty($aggFun)) { 662 | return false; 663 | } 664 | 665 | $aggregate = array(); 666 | $aggregate['group_key'] = $groupKey; 667 | $aggregate['agg_fun'] = $aggFun; 668 | 669 | empty($range) || $aggregate['range'] = $range; 670 | empty($maxGroup) || $aggregate['max_group'] = $maxGroup; 671 | empty($aggFilter) || $aggregate['agg_filter'] = $aggFilter; 672 | empty($aggSamplerThresHold) || 673 | $aggregate['agg_sampler_threshold'] = $aggSamplerThresHold; 674 | empty($aggSamplerStep) || $aggregate['agg_sampler_step'] = $aggSamplerStep; 675 | 676 | $this->aggregate[$groupKey][] = $aggregate; 677 | } 678 | 679 | /** 680 | * 删除指定的统计数据 681 | * 682 | * @param string $groupKey 指定的group key。 683 | */ 684 | public function removeAggregate($groupKey) 685 | { 686 | unset($this->aggregate[$groupKey]); 687 | } 688 | 689 | /** 690 | * 获取统计相关信息 691 | * 692 | * @param string $groupKey 指定group key获取其相关信息,如果为空,则返回整个信息。 693 | * @return array 统计相关信息 694 | */ 695 | public function getAggregate($key = '') 696 | { 697 | return (!empty($key)) ? $this->aggregate[$key] : $this->aggregate; 698 | } 699 | 700 | /** 701 | * 获取字符串类型的统计信息 702 | * 703 | * @return string 获取字符串类型的统计信息 704 | */ 705 | public function getAggregateString() 706 | { 707 | $aggregate = array(); 708 | if (is_array($agg = $this->getAggregate()) && !empty($agg)) { 709 | foreach ($agg as $aggDescs) { 710 | $item = array(); 711 | if (is_array($aggDescs) && !empty($aggDescs)) { 712 | foreach ($aggDescs as $aggDesc) { 713 | foreach ($aggDesc as $itemKey => $itemValue) { 714 | $item[] = $itemKey . ":" . $itemValue; 715 | } 716 | $aggregate[] = implode(",", $item); 717 | } 718 | } 719 | 720 | } 721 | } 722 | return implode(";", $aggregate); 723 | } 724 | 725 | /** 726 | * 添加distinct排序信息 727 | * 728 | * 例如:检索关键词“手机”共获得10个结果,分别为:doc1,doc2,doc3,doc4,doc5,doc6, 729 | * doc7,doc8,doc9,doc10。其中前三个属于用户A,doc4-doc6属于用户B,剩余四个属于用户C。 730 | * 如果前端每页仅展示5个商品,则用户C将没有展示的机会。但是如果按照user_id进行抽取,每轮抽 731 | * 取1个,抽取2次,并保留抽取剩余的结果,则可以获得以下文档排列顺序:doc1、doc4、doc7、 732 | * doc2、doc5、doc8、doc3、doc6、doc9、doc10。可以看出,通过distinct排序,各个用户的 733 | * 商品都得到了展示机会,结果排序更趋于合理。 734 | * 更多说明请参见 [API distinct子句]({{!api-reference/query-clause&distinct-clause!}}) 735 | * 736 | * @param string $key 为用户用于做distinct抽取的字段,该字段要求建立Attribute索引。 737 | * @param int $distCount 为一次抽取的document数量,默认值为1。 738 | * @param int $distTimes 为抽取的次数,默认值为1。 739 | * @param string $reserved 为是否保留抽取之后剩余的结果,true为保留,false则丢弃,丢弃时totalHits的个数会减去被distinct而丢弃的个数,但这个结果不一定准确,默认为true。 740 | * @param string $distFilter 为过滤条件,被过滤的doc不参与distinct,只在后面的 排序中,这些被过滤的doc将和被distinct出来的第一组doc一起参与排序。默认是全部参与distinct。 741 | * @param string $updateTotalHit 当reserved为false时,设置update_total_hit为true,则最终total_hit会减去被distinct丢弃的的数目(不一定准确),为false则不减;默认为false。 742 | * @param int $maxItemCount 设置计算distinct时最多保留的doc数目。 743 | * @param number $grade 指定档位划分阈值。 744 | */ 745 | public function addDistinct($key, $distCount = 0, $distTimes = 0, 746 | $reserved = '', $distFilter = '', $updateTotalHit = '', 747 | $maxItemCount = 0, $grade = '') { 748 | 749 | if (empty($key)) { 750 | return false; 751 | } 752 | 753 | $distinct = array(); 754 | $distinct['dist_key'] = $key; 755 | empty($distCount) || ($distinct['dist_count'] = (int) $distCount); 756 | empty($distTimes) || $distinct['dist_times'] = (int) $distTimes; 757 | empty($reserved) || $distinct['reserved'] = $reserved; 758 | empty($distFilter) || $distinct['dist_filter'] = $distFilter; 759 | empty($updateTotalHit) || $distinct['update_total_hit'] = $updateTotalHit; 760 | empty($maxItemCount) || $distinct['max_item_count'] = (int) $maxItemCount; 761 | empty($grade) || $distinct['grade'] = $grade; 762 | 763 | $this->distinct[$key] = $distinct; 764 | } 765 | 766 | /** 767 | * 删除某个字段的所有distinct排序信息 768 | * 769 | * @param string $distinctKey 指定的字段 770 | */ 771 | public function removeDistinct($distinctKey) 772 | { 773 | unset($this->distinct[$distinctKey]); 774 | } 775 | 776 | /** 777 | * 获取某字段的distinct排序信息 778 | * 779 | * @param string $key 指定的distinct字段,如果字段为空则返回所有distinct信息。 780 | * @return array 指定字段的distinct排序信息 781 | */ 782 | public function getDistinct($key = '') 783 | { 784 | return (!empty($key)) ? $this->distinct[$key] : $this->distinct; 785 | } 786 | 787 | /** 788 | * 获取字符串类型的所有的distinct信息 789 | * @return string 字符串类型的所有的distinct信息 790 | */ 791 | public function getDistinctString() 792 | { 793 | $distinct = array(); 794 | if (is_array($s = $this->getDistinct()) && !empty($s)) { 795 | foreach ($s as $distinctAttribute) { 796 | $item = array(); 797 | if ($distinctAttribute['dist_key'] != 'none_dist') { 798 | if (is_array($distinctAttribute) && !empty($distinctAttribute)) { 799 | foreach ($distinctAttribute as $k => $v) { 800 | $item[] = $k . ":" . $v; 801 | } 802 | } 803 | $distinct[] = implode(",", $item); 804 | } else { 805 | $distinct[] = $distinctAttribute['dist_key']; 806 | } 807 | } 808 | } 809 | return implode(";", $distinct); 810 | } 811 | 812 | /** 813 | * 设定指定索引字段范围的搜索关键词 814 | * 815 | * [NOTE]:$query必须指定索引名称,格式类似为 索引名称:'搜索关键词'。 816 | * 817 | * 此query是查询必需的一部分,可以指定不同的索引名,并同时可指定多个查询及之间的关系 818 | * (AND, OR, ANDNOT, RANK)。 819 | * 820 | * 例如查询subject索引字段的query:“手机”,可以设置为 821 | * query=subject:'手机'。 822 | * 823 | * NOTE: text类型索引在建立时做了分词,而string类型的索引则没有分词 824 | * 更多说明请参见 [API query子句]({{!api-reference/query-clause&query-clause!}}) 825 | * 826 | * @param string $query 设定搜索的查询词。 827 | * @param string $fieldName 设定的索引范围。 828 | * 829 | */ 830 | public function setQueryString($query) 831 | { 832 | $this->query = $query; 833 | } 834 | 835 | /** 836 | * 获取当前指定的查询词内容 837 | * 838 | * @return string 当前指定的查询词内容 839 | */ 840 | public function getQuery() 841 | { 842 | return $this->query; 843 | } 844 | 845 | /** 846 | * 添加指定结果集返回的字段 847 | * 848 | * @param array\string $field 结果集返回的字段。 849 | */ 850 | public function addFetchFields($field) 851 | { 852 | if (!is_array($field)) { 853 | if (!in_array($field, $this->fetches)) { 854 | $this->fetches[] = $field; 855 | } 856 | } else { 857 | $this->fetches = $field; 858 | } 859 | } 860 | 861 | /** 862 | * 删除指定结果集的返回字段 863 | * 864 | * @param string $fieldName 指定字段名称。 865 | */ 866 | public function removeFetchField($fieldName) 867 | { 868 | $flip = array_flip($this->fetches); 869 | unset($flip[$fieldName]); 870 | $this->fetches = array_flip($flip); 871 | } 872 | 873 | /** 874 | * 设置kvpair 875 | * 更多说明请参见 [API 自定义kvpair子句]({{!api-reference/query-clause&kvpair-clause!}}) 876 | * 877 | * @param string $pair 指定的pair信息。 878 | */ 879 | public function setPair($pair) 880 | { 881 | $this->kvpair = $pair; 882 | } 883 | 884 | /** 885 | * 获取当前的kvpair 886 | * 887 | * @return string 返回当前设定的kvpair。 888 | */ 889 | public function getPair() 890 | { 891 | return $this->kvpair; 892 | } 893 | 894 | /** 895 | * 增加自定义参数 896 | * 897 | * @param string $paramKey 参数名称。 898 | * @param string $paramValue 参数值。 899 | */ 900 | public function addCustomParam($paramKey, $paramValue) 901 | { 902 | $this->customParams[$paramKey] = $paramValue; 903 | } 904 | 905 | /** 906 | * 指定精排算分的文档个数 907 | * 908 | * 若不指定则使用默认值200 909 | * 910 | * @param int $rerankSize 精排算分文档个数 911 | */ 912 | public function addRerankSize($rerankSize) 913 | { 914 | $this->rerankSize = $rerankSize; 915 | } 916 | 917 | /** 918 | * 添加一条查询分析规则 919 | * 920 | * @param QPName 查询分析规则 921 | */ 922 | public function addQPName($QPName) 923 | { 924 | if (is_array($QPName)) { 925 | $this->QPName = $QPName; 926 | } else { 927 | $this->QPName[] = $QPName; 928 | } 929 | } 930 | 931 | /** 932 | * 获取设置的查询分析规则 933 | * 934 | * @return String 设置的查询分析规则 935 | */ 936 | public function getQPName() 937 | { 938 | return $this->QPName; 939 | } 940 | 941 | /** 942 | * 关闭某些功能模块。 943 | * 944 | * 有如下场景需要考虑: 945 | * 1、如果要关闭整个qp的功能,则参数为空即可。 946 | * 2、要指定某个索引关闭某个功能,则可以指定disableValue="processer:index", 947 | * processer:index为指定关闭某个processer的某个索引功能,其中index为索引名称,多个索引可以用“|”分隔,可以为index1[|index2...] 948 | * 3、如果要关闭多个processor可以传递数组。 949 | * qp processor 有如下模块: 950 | * 1、spell_check: 检查用户查询串中的拼写错误,并给出纠错建议。 951 | * 2、term_weighting: 分析查询中每个词的重要程度,并将其量化成权重,权重较低的词可能不会参与召回。 952 | * 3、stop_word: 根据系统内置的停用词典过滤查询中无意义的词 953 | * 4、synonym: 根据系统提供的通用同义词库和语义模型,对查询串进行同义词扩展,以便扩大召回。 954 | * example: 955 | * "" 表示关闭整个qp。 956 | * "spell_check" 表示关闭qp的拼音纠错功能。 957 | * "stop_word:index1|index2" 表示关闭qp中索引名为index1和index2上的停用词功能。 958 | * 959 | * @param string $functionName 指定的functionName,例如“qp”等 960 | * @param string|array $disableValue 需要关闭的值 961 | */ 962 | public function addDisabledQP($disableValue = "") 963 | { 964 | $this->addDisabledFunction("qp", $disableValue); 965 | } 966 | 967 | /** 968 | * 添加一项禁止的功能模块 969 | * 970 | * @param functionName 功能模块名称 971 | * @param disableValue 禁用的功能细节 972 | */ 973 | public function addDisabledFunction($functionName, $disableValue = "") 974 | { 975 | if (is_array($disableValue)) { 976 | $this->functions[$functionName] = $disableValue; 977 | } else { 978 | $this->functions[$functionName][] = $disableValue; 979 | } 980 | } 981 | 982 | /** 983 | * 获取所有禁止的功能模块 984 | * 985 | * @return array 所哟禁止的功能模块 986 | */ 987 | public function getDisabledFunction() 988 | { 989 | return $this->functions; 990 | } 991 | 992 | /** 993 | * 以字符串的格式返回disable的内容。 994 | * 995 | * @return string 996 | */ 997 | public function getDisabledFunctionString() 998 | { 999 | $functions = $this->getDisabledFunction(); 1000 | $result = array(); 1001 | if (!empty($functions)) { 1002 | foreach ($functions as $functionName => $value) { 1003 | $string = ""; 1004 | if (is_array($value) && !empty($value)) { 1005 | $string = implode(",", $value); 1006 | } 1007 | 1008 | if ($string === "") { 1009 | $result[] = $functionName; 1010 | } else { 1011 | $result[] = $functionName . ":" . $string; 1012 | } 1013 | } 1014 | } 1015 | 1016 | return implode(";", $result); 1017 | } 1018 | 1019 | /** 1020 | * 获取精排算分文档个数 1021 | * 1022 | * @return int 精排算分文档个数 1023 | */ 1024 | public function getRerankSize() 1025 | { 1026 | return $this->rerankSize; 1027 | } 1028 | 1029 | /** 1030 | * 获取自定义参数 1031 | * 1032 | * @return string 自定义参数 1033 | */ 1034 | public function getCustomParam() 1035 | { 1036 | return $this->customParams; 1037 | } 1038 | 1039 | /** 1040 | * 获取指定结果集返回的字段列表 1041 | * 1042 | * @return array 指定结果集返回的字段列表 1043 | */ 1044 | public function getFetchFields() 1045 | { 1046 | return $this->fetches; 1047 | } 1048 | 1049 | /** 1050 | * 设置此次获取的scroll id的期时间。 1051 | * 1052 | * 可以为整形数字,默认为毫秒。也可以用1m表示1min;支持的时间单位包括: 1053 | * w=Week, d=Day, h=Hour, m=minute, s=second 1054 | * 1055 | * @param string|int $scroll 1056 | */ 1057 | public function setScroll($scroll) 1058 | { 1059 | $this->scroll = $scroll; 1060 | } 1061 | 1062 | /** 1063 | * 获取scroll的失效时间。 1064 | * 1065 | * @return string|int 1066 | */ 1067 | public function getScroll() 1068 | { 1069 | return $this->scroll; 1070 | } 1071 | 1072 | /** 1073 | * 设置搜索类型 1074 | * 1075 | * @param searchType 搜索类型 1076 | */ 1077 | private function setSearchType($searchType) 1078 | { 1079 | $this->searchType = $searchType; 1080 | } 1081 | 1082 | /** 1083 | * 获取设置的搜索类型 1084 | * 1085 | * @return String 设置的搜索类型 1086 | */ 1087 | private function getSearchType() 1088 | { 1089 | return $this->searchType; 1090 | } 1091 | 1092 | /** 1093 | * 从$opts数组中抽取所有的需要的参数并复制到属性中。 1094 | * 1095 | * @param array $opts 1096 | */ 1097 | private function extract($opts, $type = 'search') 1098 | { 1099 | if (!empty($opts) && is_array($opts)) { 1100 | isset($opts['query']) && $this->setQueryString($opts['query']); 1101 | isset($opts['indexes']) && $this->addIndex($opts['indexes']); 1102 | isset($opts['fetch_field']) && $this->addFetchFields($opts['fetch_field']); 1103 | isset($opts['format']) && $this->setFormat($opts['format']); 1104 | isset($opts['start']) && $this->setStartHit($opts['start']); 1105 | isset($opts['hits']) && $this->setHits((int) $opts['hits']); 1106 | isset($opts['filter']) && $this->addFilter($opts['filter']); 1107 | isset($opts['kvpair']) && $this->setPair($opts['kvpair']); 1108 | isset($opts['rerankSize']) && $this->addRerankSize($opts['rerankSize']); 1109 | 1110 | if ($type == 'search') { 1111 | isset($opts['sort']) && $this->sort = $opts['sort']; 1112 | isset($opts['aggregate']) && $this->aggregate = $opts['aggregate']; 1113 | isset($opts['distinct']) && $this->distinct = $opts['distinct']; 1114 | isset($opts['formula_name']) && $this->setFormulaName($opts['formula_name']); 1115 | isset($opts['summary']) && $this->summary = $opts['summary']; 1116 | isset($opts['qp']) && $this->addQPName($opts['qp']); 1117 | isset($opts['disable_qp']) && $this->addDisabledQP($opts['disable']); 1118 | } else if ($type == 'scroll') { 1119 | isset($opts['scroll_id']) && $this->setScrollId($opts['scroll_id']); 1120 | isset($opts['scroll']) && $this->setScroll($opts['scroll']); 1121 | $this->setSearchType(self::SEARCH_TYPE_SCAN); 1122 | } 1123 | } 1124 | } 1125 | 1126 | /** 1127 | * 生成HTTP的请求串,并通过CloudsearchClient类向API服务发出请求并返回结果。 1128 | * 1129 | * query参数中的query子句和config子句必需的,其它子句可选。 1130 | * 1131 | * @return string 1132 | */ 1133 | private function call($type = 'search') 1134 | { 1135 | $haquery = array(); 1136 | $haquery[] = "config=" . $this->clauseConfig(); 1137 | $haquery[] = "query=" . ($this->getQuery() ? $this->getQuery() : "''") . ""; 1138 | 1139 | ($f = $this->getFilter()) && ($haquery[] = 'filter=' . $f); 1140 | ($k = $this->getPair()) && ($haquery[] = 'kvpairs=' . $k); 1141 | if ($type == 'search') { 1142 | ($s = $this->getSortString()) && ($haquery[] = "sort=" . $s); 1143 | ($d = $this->getDistinctString()) && ($haquery[] = 'distinct=' . $d); 1144 | ($a = $this->getAggregateString()) && ($haquery[] = 'aggregate=' . $a); 1145 | } 1146 | 1147 | $params = array( 1148 | 'query' => implode("&&", $haquery), 1149 | 'index_name' => implode(";", $this->getSearchIndexes()), 1150 | 'format' => $this->getFormat(), 1151 | ); 1152 | 1153 | if ($result = $this->getCustomParam()) { 1154 | foreach ($result as $k => $v) { 1155 | $params[$k] = $v; 1156 | } 1157 | } 1158 | 1159 | ($f = $this->getFetchFields()) && ($params['fetch_fields'] = implode(";", $f)); 1160 | if ($type == 'search') { 1161 | ($f = $this->getFormulaName()) && ($params['formula_name'] = $f); 1162 | ($f = $this->getFirstFormulaName()) && ($params['first_formula_name'] = $f); 1163 | ($s = $this->getSummaryString()) && ($params['summary'] = $s); 1164 | ($f = $this->getQPName()) && ($params['qp'] = implode(",", $f)); 1165 | ($f = $this->getDisabledFunctionString()) && ($params['disable'] = $f); 1166 | } else if ($type == 'scroll') { 1167 | ($f = $this->getScroll()) && ($params['scroll'] = $f); 1168 | ($f = $this->getScrollId()) && ($params['scroll_id'] = $f); 1169 | $params['search_type'] = self::SEARCH_TYPE_SCAN; 1170 | } 1171 | 1172 | return $this->client->call($this->path, $params, 'GET'); 1173 | } 1174 | 1175 | /** 1176 | * 生成语法的config子句并返回。 1177 | * @return string 1178 | */ 1179 | private function clauseConfig() 1180 | { 1181 | $config = array(); 1182 | $config[] = 'format:' . $this->getFormat(); 1183 | $config[] = 'start:' . $this->getStartHit(); 1184 | $config[] = 'hit:' . $this->getHits(); 1185 | ($r = $this->getRerankSize()) && ($config[] = 'rerank_size:' . $r); 1186 | 1187 | return implode(",", $config); 1188 | } 1189 | } 1190 | --------------------------------------------------------------------------------