├── CHANGELOG.md
├── LICENSE.md
├── README.md
├── composer.json
└── src
├── ActiveDataProvider.php
├── ActiveFixture.php
├── ActiveQuery.php
├── ActiveRecord.php
├── BatchQueryResult.php
├── BulkCommand.php
├── Command.php
├── Connection.php
├── DebugAction.php
├── DebugPanel.php
├── ElasticsearchTarget.php
├── Exception.php
├── Query.php
└── QueryBuilder.php
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | Yii Framework 2 Elasticsearch extension Change Log
2 | ==================================================
3 |
4 | 2.1.6 under development
5 | -----------------------
6 |
7 | - no changes in this release.
8 |
9 |
10 | 2.1.5 February 13, 2025
11 | -----------------------
12 |
13 | - Bug #344: Disabled JSON pretty print for ElasticSearch bulk API (rhertogh)
14 | - Bug #350: Remove deprecated code, set $pagination->totalCount (lav45)
15 | - Bug #352: Add support for query parameter `search_after` to paginate over large datasets (lubosdz)
16 |
17 |
18 | 2.1.4 May 22, 2023
19 | ------------------
20 |
21 | - Bug #330: Fix `curlOptions` merging (yuniorsk)
22 | - Bug #332: Added `[\ReturnTypeWillChange]` attribute for BatchQueryResult methods to be compatible with Iterator interface (warton)
23 | - Enh #329: Added `curlOptions` attribute for advanced configuration of curl session (yuniorsk)
24 |
25 |
26 | 2.1.3 August 07, 2022
27 | -----------------------
28 |
29 | - Enh #311: Added support for runtime mappings in Elasticsearch 7.11+ (mabentley85)
30 | - Enh #323: Updated API calls for compatibility with Elastcisearch 8+ (tehmaestro)
31 | - Enh #323: Improved github testing workflow (terabytesoftw)
32 | - Enh #318: Added "match" and "match_phrase" queries to query builder (Barton0403)
33 |
34 |
35 | 2.1.2 August 09, 2021
36 | ---------------------
37 |
38 | - Enh #18817: Use `random_int()` when choosing connection (samdark)
39 |
40 |
41 | 2.1.1 May 06, 2021
42 | ------------------
43 |
44 | - Bug #297: Fix `Query::count()` when index contains more than 10,000 documents (rhertogh)
45 | - Bug #298: Fix ElasticSearch performance when passing `false` to `Query::source()` (rhertogh)
46 |
47 |
48 | 2.1.0 July 21, 2020
49 | -------------------
50 |
51 | - Bug #161: Changed yii\base\Object to yii\base\BaseObject (sashsvamir)
52 | - Bug #171: Allow to have both `query()` and `where()` in a query (beowulfenator)
53 | - Bug #176: Allow very long scroll id by passing scroll id in request body (russianlagman)
54 | - Bug #180: Fixed `count()` compatibility with PHP 7.2 to not call it on scalar values (cebe)
55 | - Bug #191: Fixed error when calling `column('_id')` in `ActiveQuery` (pvassiliou)
56 | - Bug #216: Updated `suggest()` command to support Elasticsearch 6.5+ (beowulfenator)
57 | - Bug #239: Make sure that `ElasticsearchTarget` consistently logs message as text (beowufenator)
58 | - Bug: (CVE-2018-8074): Fixed possibility of manipulated condition when unfiltered input is passed to `ActiveRecord::findOne()` or `findAll()` (cebe)
59 | - Enh #112: Added support for Elasticsearch 5.0. Minimum requirement is also now Elasticsearch 5.0 (holycheater, beowulfenator, i-lie)
60 | - Enh #136: Added docs on error handling in queries (beowulfenator)
61 | - Enh #156: Added suggester support to `ActiveDataProvider` (Julian-B90)
62 | - Enh #222: Added collapse support (walkskyer)
63 | - Enh #272: Add Elasticsearch 7 compatibility (beowulfenator)
64 | - Chg #269: Replace InvalidParamException with InvalidArgumentException (Julian-B90)
65 | - Chg: Removed `Command::getIndexStatus()` and added `getIndexStats()` and `getIndexRecoveryStats()` to reflect changes in Elasticsearch 5.0 (cebe)
66 | - Chg: Search queries that result in a 404 error due to missing indices are now no longer silently interpreted as empty result, but will throw an exception (cebe)
67 |
68 |
69 | 2.0.7 June 01, 2020
70 | -------------------
71 |
72 | - Bug #199: Fixed `ActiveRecord::insert()` check if insert was indeed successful (rhertogh)
73 | - Bug #248: Fix 'run query' in debugger tool (tunecino)
74 | - Bug #257: ActiveRecord::get() for non-existent ID now works in PHP 7.4 (trifonovivan)
75 | - Enh #56: Added docs regarding updates to attributes with "object" mapping (beowulfenator)
76 |
77 |
78 | 2.0.6 May 27, 2020
79 | ------------------
80 |
81 | - Bug #180: Fixed `count()` compatibility with PHP 7.2 to not call it on scalar values (cebe)
82 | - Bug #227: Fixed `Bad Request (#400): Unable to verify your data submission.` in debug details panel 'run query' (rhertogh)
83 | - Enh #117: Add support for `QueryInterface::emulateExecution()` (cebe)
84 |
85 |
86 | 2.0.5 March 20, 2018
87 | --------------------
88 |
89 | - Bug #120: Fix debug panel markup to be compatible with Yii 2.0.10 (drdim)
90 | - Bug #125: Fixed `ActiveDataProvider::refresh()` to also reset `$queryResults` data (sizeg)
91 | - Bug #134: Fix infinite query loop "ActiveDataProvider" when the index does not exist (eolitich)
92 | - Bug #149: Changed `yii\base\Object` to `yii\base\BaseObject` (dmirogin)
93 | - Bug: (CVE-2018-8074): Fixed possibility of manipulated condition when unfiltered input is passed to `ActiveRecord::findOne()` or `findAll()` (cebe)
94 | - Bug: Updated debug panel classes to be consistent with yii 2.0.7 (beowulfenator)
95 | - Bug: Added accessor method for the default Elasticsearch primary key (kyle-mccarthy)
96 | - Enh #15: Special data provider `yii\elasticsearch\ActiveDataProvider` created (klimov-paul)
97 | - Enh #43: Elasticsearch log target (trntv, beowulfenator)
98 | - Enh #47: Added support for post_filter option in search queries (mxkh)
99 | - Enh #60: Minor updates to guide (devypt, beowulfenator)
100 | - Enh #82: Support HTTPS protocol (dor-denis, beowulfenator)
101 | - Enh #83: Support for "gt", ">", "gte", ">=", "lt", "<", "lte", "<=" operators in query (i-lie, beowulfenator)
102 | - Enh #119: Added support for explanation on query (kyle-mccarthy)
103 | - Enh #150: Explicitily send `Content-Type` header in HTTP requests to Elasticsearch (lubobill1990)
104 | - Enh: Bulk API implemented and used in AR (tibee, beowulfenator)
105 | - Enh: Deserialization of raw response when text/plain is supported (Tezd)
106 | - Enh: Added ability to work with aliases through Command class (Tezd)
107 |
108 |
109 | 2.0.4 March 17, 2016
110 | --------------------
111 |
112 | - Bug #8: Fixed issue with running out of sockets when running a large number of requests by reusing curl handles (cebe)
113 | - Bug #13: Fixed wrong API call for getting all types or searching all types, `_all` works only for indexes (cebe)
114 | - Bug #19: `DeleteAll` now deletes all entries, not first 10 (beowulfenator)
115 | - Bug #48: `UpdateAll` now updates all entries, not first 10 (beowulfenator)
116 | - Bug #65: Fixed warning `array to string conversion` when parsing error response (rhertogh, silverfire)
117 | - Bug #73: Fixed debug panel exception when no data was recorded for Elasticsearch panel (jafaripur)
118 | - Enh #2: Added `min_score` option to query (knut)
119 | - Enh #28: AWS Elasticsearch service compatibility (andrey-bahrachev)
120 | - Enh #33: Implemented `Command::updateSettings()` and `Command::updateAnalyzers()` (githubjeka)
121 | - Enh #50: Implemented HTTP auth (silverfire)
122 | - Enh #62: Added support for scroll API in `batch()` and `each()` (beowulfenator, 13leaf)
123 | - Enh #70: `Query` and `ActiveQuery` now have `$options` attribute that is passed to commands generated by queries (beowulfenator)
124 | - Enh: Unified model creation from result set in `Query` and `ActiveQuery` with `populate()` (beowulfenator)
125 |
126 |
127 | 2.0.3 March 01, 2015
128 | --------------------
129 |
130 | - no changes in this release.
131 |
132 |
133 | 2.0.2 January 11, 2015
134 | ----------------------
135 |
136 | - Enh: Added `ActiveFixture` class for testing fixture support for Elasticsearch (cebe, viilveer)
137 |
138 |
139 | 2.0.1 December 07, 2014
140 | -----------------------
141 |
142 | - Bug #5662: Elasticsearch AR updateCounters() now uses explicitly `groovy` script for updating making it compatible with ES >1.3.0 (cebe)
143 | - Bug #6065: `ActiveRecord::unlink()` was failing in some situations when working with relations via array valued attributes (cebe)
144 | - Enh #5758: Allow passing custom options to `ActiveRecord::update()` and `::delete()` including support for routing needed for updating records with parent relation (cebe)
145 | - Enh: Add support for optimistic locking (cebe)
146 |
147 |
148 | 2.0.0 October 12, 2014
149 | ----------------------
150 |
151 | - Enh #3381: Added ActiveRecord::arrayAttributes() to define attributes that should be treated as array when retrieved via `fields` (cebe)
152 |
153 |
154 | 2.0.0-rc September 27, 2014
155 | ---------------------------
156 |
157 | - Bug #3587: Fixed an issue with storing empty records (cebe)
158 | - Bug #4187: Elasticsearch dynamic scripting is disabled in 1.2.0, so do not use it in query builder (cebe)
159 | - Enh #3527: Added `highlight` property to Query and ActiveRecord. (Borales)
160 | - Enh #4048: Added `init` event to `ActiveQuery` classes (qiangxue)
161 | - Enh #4086: changedAttributes of afterSave Event now contain old values (dizews)
162 | - Enh: Make error messages more readable in HTML output (cebe)
163 | - Enh: Added support for query stats (cebe)
164 | - Enh: Added support for query suggesters (cebe, tvdavid)
165 | - Enh: Added support for delete by query (cebe, tvdavid)
166 | - Chg #4451: Removed support for facets and replaced them with aggregations (cebe, tadaszelvys)
167 | - Chg: asArray in ActiveQuery is now equal to using the normal Query. This means, that the output structure has changed and `with` is supported anymore. (cebe)
168 | - Chg: Deletion of a record is now also considered successful if the record did not exist. (cebe)
169 | - Chg: Requirement changes: Yii now requires Elasticsearch version 1.0 or higher (cebe)
170 |
171 |
172 | 2.0.0-beta April 13, 2014
173 | -------------------------
174 |
175 | - Bug #1993: afterFind event in AR is now called after relations have been populated (cebe, creocoder)
176 | - Bug #2324: Fixed QueryBuilder bug when building a query with "query" option (mintao)
177 | - Enh #1313: made index and type available in `ActiveRecord::instantiate()` to allow creating records based on Elasticsearch type when doing cross index/type search (cebe)
178 | - Enh #1382: Added a debug toolbar panel for Elasticsearch (cebe)
179 | - Enh #1765: Added support for primary key path mapping, pk can now be part of the attributes when mapping is defined (cebe)
180 | - Enh #2002: Added filterWhere() method to yii\elasticsearch\Query to allow easy addition of search filter conditions by ignoring empty search fields (samdark, cebe)
181 | - Enh #2892: ActiveRecord dirty attributes are now reset after call to `afterSave()` so information about changed attributes is available in `afterSave`-event (cebe)
182 | - Chg #1765: Changed handling of ActiveRecord primary keys, removed getId(), use getPrimaryKey() instead (cebe)
183 | - Chg #2281: Renamed `ActiveRecord::create()` to `populateRecord()` and changed signature. This method will not call instantiate() anymore (cebe)
184 | - Chg #2146: Removed `ActiveRelation` class and moved the functionality to `ActiveQuery`.
185 | All relational queries are now directly served by `ActiveQuery` allowing to use
186 | custom scopes in relations (cebe)
187 |
188 |
189 | 2.0.0-alpha, December 1, 2013
190 | -----------------------------
191 |
192 | - Initial release.
193 |
194 |
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | Copyright © 2008 by Yii Software LLC (http://www.yiisoft.com)
2 | All rights reserved.
3 |
4 | Redistribution and use in source and binary forms, with or without
5 | modification, are permitted provided that the following conditions
6 | are met:
7 |
8 | * Redistributions of source code must retain the above copyright
9 | notice, this list of conditions and the following disclaimer.
10 | * Redistributions in binary form must reproduce the above copyright
11 | notice, this list of conditions and the following disclaimer in
12 | the documentation and/or other materials provided with the
13 | distribution.
14 | * Neither the name of Yii Software LLC nor the names of its
15 | contributors may be used to endorse or promote products derived
16 | from this software without specific prior written permission.
17 |
18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
19 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
20 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
21 | FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
22 | COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
23 | INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
24 | BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
25 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
26 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
27 | LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
28 | ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
29 | POSSIBILITY OF SUCH DAMAGE.
30 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
Elasticsearch Query and ActiveRecord for Yii 2
6 |
7 |
8 |
9 | This extension provides the [Elasticsearch](https://www.elastic.co/products/elasticsearch) integration for the [Yii framework 2.0](https://www.yiiframework.com).
10 | It includes basic querying/search support and also implements the `ActiveRecord` pattern that allows you to store active
11 | records in Elasticsearch.
12 |
13 | For license information check the [LICENSE](LICENSE.md)-file.
14 |
15 | Documentation is at [docs/guide/README.md](docs/guide/README.md).
16 |
17 | [](https://packagist.org/packages/yiisoft/yii2-elasticsearch)
18 | [](https://packagist.org/packages/yiisoft/yii2-elasticsearch)
19 | [](https://travis-ci.com/yiisoft/yii2-elasticsearch)
20 | [](https://codecov.io/gh/yiisoft/yii2-elasticsearch)
21 |
22 | Requirements
23 | ------------
24 |
25 | - PHP 7.3 or higher.
26 |
27 | Depending on the version of Elasticsearch you are using you need a different version of this extension.
28 |
29 | - For Elasticsearch 1.6.0 to 1.7.6 use extension version 2.0.x
30 | - For Elasticsearch 5.x or above use extension version 2.1.x
31 |
32 | Installation
33 | ------------
34 |
35 | The preferred way to install this extension is through [composer](https://getcomposer.org/download/):
36 |
37 |
38 | ```
39 | composer require --prefer-dist yiisoft/yii2-elasticsearch:"~2.1.0"
40 | ```
41 |
42 | Configuration
43 | -------------
44 |
45 | To use this extension, you have to configure the Connection class in your application configuration:
46 |
47 | ```php
48 | return [
49 | //....
50 | 'components' => [
51 | 'elasticsearch' => [
52 | 'class' => 'yii\elasticsearch\Connection',
53 | 'nodes' => [
54 | ['http_address' => '127.0.0.1:9200'],
55 | // configure more hosts if you have a cluster
56 | ],
57 | 'dslVersion' => 7, // default is 5
58 | ],
59 | ]
60 | ];
61 | ```
62 |
--------------------------------------------------------------------------------
/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "yiisoft/yii2-elasticsearch",
3 | "description": "Elasticsearch integration and ActiveRecord for the Yii framework",
4 | "keywords": ["yii2", "elasticsearch", "active-record", "search", "fulltext"],
5 | "type": "yii2-extension",
6 | "license": "BSD-3-Clause",
7 | "support": {
8 | "issues": "https://github.com/yiisoft/yii2-elasticsearch/issues",
9 | "forum": "https://www.yiiframework.com/forum/",
10 | "wiki": "https://www.yiiframework.com/wiki/",
11 | "irc": "ircs://irc.libera.chat:6697/yii",
12 | "source": "https://github.com/yiisoft/yii2-elasticsearch"
13 | },
14 | "authors": [
15 | {
16 | "name": "Carsten Brandt",
17 | "email": "mail@cebe.cc"
18 | }
19 | ],
20 | "require": {
21 | "php": ">=7.3",
22 | "ext-curl": "*",
23 | "ext-json": "*",
24 | "ext-mbstring": "*",
25 | "yiisoft/yii2": "~2.0.14"
26 | },
27 | "require-dev": {
28 | "phpunit/phpunit": "^9.6"
29 | },
30 | "autoload": {
31 | "psr-4": { "yii\\elasticsearch\\": "src" }
32 | },
33 | "config": {
34 | "allow-plugins": {
35 | "yiisoft/yii2-composer": true
36 | }
37 | },
38 | "repositories": [
39 | {
40 | "type": "composer",
41 | "url": "https://asset-packagist.org"
42 | }
43 | ],
44 | "extra": {
45 | "branch-alias": {
46 | "dev-master": "2.1.x-dev"
47 | }
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/src/ActiveDataProvider.php:
--------------------------------------------------------------------------------
1 |
27 | * @since 2.0.5
28 | */
29 | class ActiveDataProvider extends \yii\data\ActiveDataProvider
30 | {
31 | /**
32 | * @var array the full query results.
33 | */
34 | private $_queryResults;
35 |
36 |
37 | /**
38 | * @param array $results full query results
39 | */
40 | public function setQueryResults($results)
41 | {
42 | $this->_queryResults = $results;
43 | }
44 |
45 | /**
46 | * @return array full query results
47 | */
48 | public function getQueryResults()
49 | {
50 | if (!is_array($this->_queryResults)) {
51 | $this->prepare();
52 | }
53 | return $this->_queryResults;
54 | }
55 |
56 | /**
57 | * @return array all aggregations results
58 | */
59 | public function getAggregations()
60 | {
61 | $results = $this->getQueryResults();
62 | return isset($results['aggregations']) ? $results['aggregations'] : [];
63 | }
64 |
65 | /**
66 | * Returns results of the specified aggregation.
67 | * @param string $name aggregation name.
68 | * @return array aggregation results.
69 | * @throws InvalidCallException if query results do not contain the requested aggregation.
70 | */
71 | public function getAggregation($name)
72 | {
73 | $aggregations = $this->getAggregations();
74 | if (!isset($aggregations[$name])) {
75 | throw new InvalidCallException("Aggregation '{$name}' not found.");
76 | }
77 | return $aggregations[$name];
78 | }
79 |
80 | /**
81 | * @return array all suggestions results
82 | */
83 | public function getSuggestions()
84 | {
85 | $results = $this->getQueryResults();
86 | return isset($results['suggest']) ? $results['suggest'] : [];
87 | }
88 |
89 | /**
90 | * Returns results of the specified suggestion.
91 | * @param string $name suggestion name.
92 | * @return array suggestion results.
93 | * @throws InvalidCallException if query results do not contain the requested suggestion.
94 | */
95 | public function getSuggestion($name)
96 | {
97 | $suggestions = $this->getSuggestions();
98 | if (!isset($suggestions[$name])) {
99 | throw new InvalidCallException("Suggestion '{$name}' not found.");
100 | }
101 | return $suggestions[$name];
102 | }
103 |
104 | /**
105 | * {@inheritdoc}
106 | */
107 | protected function prepareModels()
108 | {
109 | if (!$this->query instanceof Query) {
110 | throw new InvalidConfigException('The "query" property must be an instance "' . Query::className() . '" or its subclasses.');
111 | }
112 |
113 | $query = clone $this->query;
114 | if (($pagination = $this->getPagination()) !== false) {
115 | // pagination fails to validate page number, because total count is unknown at this stage
116 | $pagination->validatePage = false;
117 | $query->limit($pagination->getLimit())->offset($pagination->getOffset());
118 | }
119 | if (($sort = $this->getSort()) !== false) {
120 | $query->addOrderBy($sort->getOrders());
121 | }
122 |
123 | if (is_array(($results = $query->search($this->db)))) {
124 | $this->setQueryResults($results);
125 | return $results['hits']['hits'];
126 | }
127 | $this->setQueryResults([]);
128 | return [];
129 | }
130 |
131 | /**
132 | * {@inheritdoc}
133 | */
134 | protected function prepareTotalCount()
135 | {
136 | if (!$this->query instanceof Query) {
137 | throw new InvalidConfigException('The "query" property must be an instance "' . Query::className() . '" or its subclasses.');
138 | }
139 |
140 | $results = $this->getQueryResults();
141 | if (isset($results['hits']['total'])) {
142 | return is_array($results['hits']['total']) ? (int) $results['hits']['total']['value'] : (int) $results['hits']['total'];
143 | }
144 | return 0;
145 | }
146 |
147 | /**
148 | * {@inheritdoc}
149 | */
150 | protected function prepareKeys($models)
151 | {
152 | $keys = [];
153 | if ($this->key !== null) {
154 | foreach ($models as $model) {
155 | if (is_string($this->key)) {
156 | $keys[] = $model[$this->key];
157 | } else {
158 | $keys[] = call_user_func($this->key, $model);
159 | }
160 | }
161 |
162 | return $keys;
163 | } elseif ($this->query instanceof ActiveQueryInterface) {
164 | /* @var $class \yii\elasticsearch\ActiveRecord */
165 | $class = $this->query->modelClass;
166 | foreach ($models as $model) {
167 | $keys[] = $model->primaryKey;
168 | }
169 | return $keys;
170 | } else {
171 | return array_keys($models);
172 | }
173 | }
174 |
175 | /**
176 | * {@inheritdoc}
177 | */
178 | public function refresh()
179 | {
180 | parent::refresh();
181 | $this->_queryResults = null;
182 | }
183 | }
184 |
--------------------------------------------------------------------------------
/src/ActiveFixture.php:
--------------------------------------------------------------------------------
1 |
27 | * @author Qiang Xue
28 | * @since 2.0.2
29 | */
30 | class ActiveFixture extends BaseActiveFixture
31 | {
32 | /**
33 | * @var Connection|string the DB connection object or the application component ID of the DB connection.
34 | * After the DbFixture object is created, if you want to change this property, you should only assign it
35 | * with a DB connection object.
36 | */
37 | public $db = 'elasticsearch';
38 | /**
39 | * @var string the name of the index that this fixture is about. If this property is not set,
40 | * the name will be determined via [[modelClass]].
41 | * @see modelClass
42 | */
43 | public $index;
44 | /**
45 | * @var string the name of the type that this fixture is about. If this property is not set,
46 | * the name will be determined via [[modelClass]].
47 | * @see modelClass
48 | */
49 | public $type;
50 | /**
51 | * @var string|boolean the file path or path alias of the data file that contains the fixture data
52 | * to be returned by [[getData()]]. If this is not set, it will default to `FixturePath/data/Index/Type.php`,
53 | * where `FixturePath` stands for the directory containing this fixture class, `Index` stands for the elasticsearch [[index]] name
54 | * and `Type` stands for the [[type]] associated with this fixture.
55 | * You can set this property to be false to prevent loading any data.
56 | */
57 | public $dataFile;
58 |
59 |
60 | /**
61 | * @inheritdoc
62 | */
63 | public function init()
64 | {
65 | parent::init();
66 | if (!isset($this->modelClass) && (!isset($this->index) || !isset($this->type))) {
67 | throw new InvalidConfigException('Either "modelClass" or "index" and "type" must be set.');
68 | }
69 | /* @var $modelClass ActiveRecord */
70 | $modelClass = $this->modelClass;
71 | if ($this->index === null) {
72 | $this->index = $modelClass::index();
73 | }
74 | if ($this->type === null) {
75 | $this->type = $modelClass::type();
76 | }
77 | }
78 |
79 | /**
80 | * Loads the fixture.
81 | *
82 | * The default implementation will first clean up the index by calling [[resetIndex()]].
83 | * It will then populate the index with the data returned by [[getData()]].
84 | *
85 | * If you override this method, you should consider calling the parent implementation
86 | * so that the data returned by [[getData()]] can be populated into the index.
87 | */
88 | public function load()
89 | {
90 | $this->resetIndex();
91 | $this->data = [];
92 |
93 | $idField = '_id';
94 |
95 | foreach ($this->getData() as $alias => $row) {
96 | $options = [];
97 | $id = isset($row[$idField]) ? $row[$idField] : null;
98 | unset($row[$idField]);
99 | if (isset($row['_parent'])) {
100 | $options['parent'] = $row['_parent'];
101 | unset($row['_parent']);
102 | }
103 |
104 | try {
105 | $response = $this->db->createCommand()->insert($this->index, $this->type, $row, $id, $options);
106 | } catch(\yii\db\Exception $e) {
107 | throw new \yii\base\Exception("Failed to insert fixture data \"$alias\": " . $e->getMessage() . "\n" . print_r($e->errorInfo, true), $e->getCode(), $e);
108 | }
109 | if ($id === null) {
110 | $row[$idField] = $response['_id'];
111 | }
112 | $this->data[$alias] = $row;
113 | }
114 | // ensure all data is flushed and immediately available in the test
115 | $this->db->createCommand()->refreshIndex($this->index);
116 | }
117 |
118 | /**
119 | * Returns the fixture data.
120 | *
121 | * The default implementation will try to return the fixture data by including the external file specified by [[dataFile]].
122 | * The file should return an array of data rows (column name => column value), each corresponding to a row in the index.
123 | *
124 | * If the data file does not exist, an empty array will be returned.
125 | *
126 | * @return array the data rows to be inserted into the database index.
127 | */
128 | protected function getData()
129 | {
130 | if ($this->dataFile === null) {
131 | $class = new \ReflectionClass($this);
132 | $dataFile = dirname($class->getFileName()) . "/data/{$this->index}/{$this->type}.php";
133 | return is_file($dataFile) ? require($dataFile) : [];
134 | } else {
135 | return parent::getData();
136 | }
137 | }
138 |
139 | /**
140 | * Removes all existing data from the specified index and type.
141 | * This method is called before populating fixture data into the index associated with this fixture.
142 | */
143 | protected function resetIndex()
144 | {
145 | $this->db->createCommand([
146 | 'index' => $this->index,
147 | 'type' => $this->type,
148 | 'queryParts' => ['query' => ['match_all' => new \stdClass()]],
149 | ])->deleteByQuery();
150 | }
151 | }
152 |
--------------------------------------------------------------------------------
/src/ActiveQuery.php:
--------------------------------------------------------------------------------
1 | with('orders')->asArray()->all();
47 | * ```
48 | * > NOTE: Elasticsearch limits the number of records returned to 10 records by default.
49 | * > If you expect to get more records you should specify limit explicitly.
50 | *
51 | * Relational query
52 | * ----------------
53 | *
54 | * In relational context ActiveQuery represents a relation between two Active Record classes.
55 | *
56 | * Relational ActiveQuery instances are usually created by calling [[ActiveRecord::hasOne()]] and
57 | * [[ActiveRecord::hasMany()]]. An Active Record class declares a relation by defining
58 | * a getter method which calls one of the above methods and returns the created ActiveQuery object.
59 | *
60 | * A relation is specified by [[link]] which represents the association between columns
61 | * of different tables; and the multiplicity of the relation is indicated by [[multiple]].
62 | *
63 | * If a relation involves a junction table, it may be specified by [[via()]].
64 | * This methods may only be called in a relational context. Same is true for [[inverseOf()]], which
65 | * marks a relation as inverse of another relation.
66 | *
67 | * > Note: Elasticsearch limits the number of records returned by any query to 10 records by default.
68 | * > If you expect to get more records you should specify limit explicitly in relation definition.
69 | * > This is also important for relations that use [[via()]] so that if via records are limited to 10
70 | * > the relations records can also not be more than 10.
71 | *
72 | * > Note: Currently [[with]] is not supported in combination with [[asArray]].
73 | *
74 | * @author Carsten Brandt
75 | * @since 2.0
76 | */
77 | class ActiveQuery extends Query implements ActiveQueryInterface
78 | {
79 | use ActiveQueryTrait;
80 | use ActiveRelationTrait;
81 |
82 | /**
83 | * @event Event an event that is triggered when the query is initialized via [[init()]].
84 | */
85 | const EVENT_INIT = 'init';
86 |
87 |
88 | /**
89 | * Constructor.
90 | * @param string $modelClass the model class associated with this query
91 | * @param array $config configurations to be applied to the newly created query object
92 | */
93 | public function __construct($modelClass, $config = [])
94 | {
95 | $this->modelClass = $modelClass;
96 | parent::__construct($config);
97 | }
98 |
99 | /**
100 | * Initializes the object.
101 | * This method is called at the end of the constructor. The default implementation will trigger
102 | * an [[EVENT_INIT]] event. If you override this method, make sure you call the parent implementation at the end
103 | * to ensure triggering of the event.
104 | */
105 | public function init()
106 | {
107 | parent::init();
108 | $this->trigger(self::EVENT_INIT);
109 | }
110 |
111 | /**
112 | * Creates a DB command that can be used to execute this query.
113 | * @param Connection $db the DB connection used to create the DB command.
114 | * If null, the DB connection returned by [[modelClass]] will be used.
115 | * @return Command the created DB command instance.
116 | */
117 | public function createCommand($db = null)
118 | {
119 | if ($this->primaryModel !== null) {
120 | // lazy loading
121 | if (is_array($this->via)) {
122 | // via relation
123 | /* @var $viaQuery ActiveQuery */
124 | list($viaName, $viaQuery) = $this->via;
125 | if ($viaQuery->multiple) {
126 | $viaModels = $viaQuery->all();
127 | $this->primaryModel->populateRelation($viaName, $viaModels);
128 | } else {
129 | $model = $viaQuery->one();
130 | $this->primaryModel->populateRelation($viaName, $model);
131 | $viaModels = $model === null ? [] : [$model];
132 | }
133 | $this->filterByModels($viaModels);
134 | } else {
135 | $this->filterByModels([$this->primaryModel]);
136 | }
137 | }
138 |
139 | /* @var $modelClass ActiveRecord */
140 | $modelClass = $this->modelClass;
141 | if ($db === null) {
142 | $db = $modelClass::getDb();
143 | }
144 |
145 | if ($this->type === null) {
146 | $this->type = $modelClass::type();
147 | }
148 | if ($this->index === null) {
149 | $this->index = $modelClass::index();
150 | $this->type = $modelClass::type();
151 | }
152 | $commandConfig = $db->getQueryBuilder()->build($this);
153 |
154 | return $db->createCommand($commandConfig);
155 | }
156 |
157 | /**
158 | * Executes query and returns all results as an array.
159 | * @param Connection $db the DB connection used to create the DB command.
160 | * If null, the DB connection returned by [[modelClass]] will be used.
161 | * @return array the query results. If the query results in nothing, an empty array will be returned.
162 | */
163 | public function all($db = null)
164 | {
165 | return parent::all($db);
166 | }
167 |
168 | /**
169 | * Converts found rows into model instances
170 | * @param array $rows
171 | * @return array|ActiveRecord[]
172 | * @since 2.0.4
173 | */
174 | private function createModels($rows)
175 | {
176 | $models = [];
177 | if ($this->asArray) {
178 | if ($this->indexBy === null) {
179 | return $rows;
180 | }
181 | foreach ($rows as $row) {
182 | if (is_string($this->indexBy)) {
183 | $key = isset($row['fields'][$this->indexBy]) ? reset($row['fields'][$this->indexBy]) : $row['_source'][$this->indexBy];
184 | } else {
185 | $key = call_user_func($this->indexBy, $row);
186 | }
187 | $models[$key] = $row;
188 | }
189 | } else {
190 | /* @var $class ActiveRecord */
191 | $class = $this->modelClass;
192 | if ($this->indexBy === null) {
193 | foreach ($rows as $row) {
194 | $model = $class::instantiate($row);
195 | $modelClass = get_class($model);
196 | $modelClass::populateRecord($model, $row);
197 | $models[] = $model;
198 | }
199 | } else {
200 | foreach ($rows as $row) {
201 | $model = $class::instantiate($row);
202 | $modelClass = get_class($model);
203 | $modelClass::populateRecord($model, $row);
204 | if (is_string($this->indexBy)) {
205 | $key = $model->{$this->indexBy};
206 | } else {
207 | $key = call_user_func($this->indexBy, $model);
208 | }
209 | $models[$key] = $model;
210 | }
211 | }
212 | }
213 |
214 | return $models;
215 | }
216 |
217 | /**
218 | * @inheritdoc
219 | * @since 2.0.4
220 | */
221 | public function populate($rows)
222 | {
223 | if (empty($rows)) {
224 | return [];
225 | }
226 |
227 | $models = $this->createModels($rows);
228 | if (!empty($this->with)) {
229 | $this->findWith($this->with, $models);
230 | }
231 | if (!$this->asArray) {
232 | foreach ($models as $model) {
233 | $model->afterFind();
234 | }
235 | }
236 |
237 | return $models;
238 | }
239 |
240 | /**
241 | * Executes query and returns a single row of result.
242 | * @param Connection $db the DB connection used to create the DB command.
243 | * If null, the DB connection returned by [[modelClass]] will be used.
244 | * @return ActiveRecord|array|null a single row of query result. Depending on the setting of [[asArray]],
245 | * the query result may be either an array or an ActiveRecord object. Null will be returned
246 | * if the query results in nothing.
247 | */
248 | public function one($db = null)
249 | {
250 | if (($result = parent::one($db)) === false) {
251 | return null;
252 | }
253 | if ($this->asArray) {
254 | // TODO implement with()
255 | // /* @var $modelClass ActiveRecord */
256 | // $modelClass = $this->modelClass;
257 | // $model = $result['_source'];
258 | // $pk = $modelClass::primaryKey()[0];
259 | // if ($pk === '_id') {
260 | // $model['_id'] = $result['_id'];
261 | // }
262 | // $model['_score'] = $result['_score'];
263 | // if (!empty($this->with)) {
264 | // $models = [$model];
265 | // $this->findWith($this->with, $models);
266 | // $model = $models[0];
267 | // }
268 | return $result;
269 | } else {
270 | /* @var $class ActiveRecord */
271 | $class = $this->modelClass;
272 | $model = $class::instantiate($result);
273 | $class = get_class($model);
274 | $class::populateRecord($model, $result);
275 | if (!empty($this->with)) {
276 | $models = [$model];
277 | $this->findWith($this->with, $models);
278 | $model = $models[0];
279 | }
280 | $model->afterFind();
281 | return $model;
282 | }
283 | }
284 |
285 | /**
286 | * @inheritdoc
287 | */
288 | public function search($db = null, $options = [])
289 | {
290 | if ($this->emulateExecution) {
291 | return [
292 | 'hits' => [
293 | 'total' => 0,
294 | 'hits' => [],
295 | ],
296 | ];
297 | }
298 |
299 | $command = $this->createCommand($db);
300 | $result = $command->search($options);
301 | if ($result === false) {
302 | throw new Exception('Elasticsearch search query failed.', [
303 | 'index' => $command->index,
304 | 'type' => $command->type,
305 | 'query' => $command->queryParts,
306 | 'options' => $command->options,
307 | ]);
308 | }
309 |
310 | // TODO implement with() for asArray
311 | if (!empty($result['hits']['hits']) && !$this->asArray) {
312 | $models = $this->createModels($result['hits']['hits']);
313 | if (!empty($this->with)) {
314 | $this->findWith($this->with, $models);
315 | }
316 | foreach ($models as $model) {
317 | $model->afterFind();
318 | }
319 | $result['hits']['hits'] = $models;
320 | }
321 |
322 | return $result;
323 | }
324 |
325 | /**
326 | * @inheritdoc
327 | */
328 | public function column($field, $db = null)
329 | {
330 | if ($field === '_id') {
331 | $command = $this->createCommand($db);
332 | $command->queryParts['_source'] = false;
333 | $result = $command->search();
334 | if ($result === false) {
335 | throw new Exception('Elasticsearch search query failed.');
336 | }
337 | if (empty($result['hits']['hits'])) {
338 | return [];
339 | }
340 | $column = [];
341 | foreach ($result['hits']['hits'] as $row) {
342 | $column[] = $row['_id'];
343 | }
344 |
345 | return $column;
346 | }
347 |
348 | return parent::column($field, $db);
349 | }
350 | }
351 |
--------------------------------------------------------------------------------
/src/ActiveRecord.php:
--------------------------------------------------------------------------------
1 |
59 | * @since 2.0
60 | */
61 | class ActiveRecord extends BaseActiveRecord
62 | {
63 | private $_id;
64 | private $_score;
65 | private $_version;
66 | private $_highlight;
67 | private $_explanation;
68 |
69 |
70 | /**
71 | * Returns the database connection used by this AR class.
72 | * By default, the "elasticsearch" application component is used as the database connection.
73 | * You may override this method if you want to use a different database connection.
74 | * @return Connection the database connection used by this AR class.
75 | */
76 | public static function getDb()
77 | {
78 | return \Yii::$app->get('elasticsearch');
79 | }
80 |
81 | /**
82 | * @inheritdoc
83 | * @return ActiveQuery the newly created [[ActiveQuery]] instance.
84 | */
85 | public static function find()
86 | {
87 | return Yii::createObject(ActiveQuery::className(), [get_called_class()]);
88 | }
89 |
90 | /**
91 | * @inheritdoc
92 | */
93 | public static function findOne($condition)
94 | {
95 | if (!is_array($condition)) {
96 | return static::get($condition);
97 | }
98 | if (!ArrayHelper::isAssociative($condition)) {
99 | $records = static::mget(array_values($condition));
100 | return empty($records) ? null : reset($records);
101 | }
102 |
103 | $condition = static::filterCondition($condition);
104 | return static::find()->andWhere($condition)->one();
105 | }
106 |
107 | /**
108 | * @inheritdoc
109 | */
110 | public static function findAll($condition)
111 | {
112 | if (!ArrayHelper::isAssociative($condition)) {
113 | return static::mget(is_array($condition) ? array_values($condition) : [$condition]);
114 | }
115 |
116 | $condition = static::filterCondition($condition);
117 | return static::find()->andWhere($condition)->all();
118 | }
119 |
120 | /**
121 | * Filter out condition parts that are array valued, to prevent building arbitrary conditions.
122 | * @param array $condition
123 | */
124 | private static function filterCondition($condition)
125 | {
126 | foreach ($condition as $k => $v) {
127 | if (is_array($v)) {
128 | $condition[$k] = array_values($v);
129 | foreach ($v as $vv) {
130 | if (is_array($vv)) {
131 | throw new InvalidArgumentException('Nested arrays are not allowed in condition for findAll() and findOne().');
132 | }
133 | }
134 | }
135 | }
136 | return $condition;
137 | }
138 |
139 | /**
140 | * Gets a record by its primary key.
141 | *
142 | * @param mixed $primaryKey the primaryKey value
143 | * @param array $options options given in this parameter are passed to Elasticsearch
144 | * as request URI parameters.
145 | * Please refer to the [Elasticsearch documentation](https://www.elastic.co/guide/en/elasticsearch/reference/current/docs-get.html)
146 | * for more details on these options.
147 | * @return static|null The record instance or null if it was not found.
148 | */
149 | public static function get($primaryKey, $options = [])
150 | {
151 | if ($primaryKey === null) {
152 | return null;
153 | }
154 | $command = static::getDb()->createCommand();
155 | $result = $command->get(static::index(), static::type(), $primaryKey, $options);
156 | if ($result && $result['found']) {
157 | $model = static::instantiate($result);
158 | static::populateRecord($model, $result);
159 | $model->afterFind();
160 |
161 | return $model;
162 | }
163 |
164 | return null;
165 | }
166 |
167 | /**
168 | * Gets a list of records by its primary keys.
169 | *
170 | * @param array $primaryKeys an array of primaryKey values
171 | * @param array $options options given in this parameter are passed to Elasticsearch
172 | * as request URI parameters.
173 | *
174 | * Please refer to the [Elasticsearch documentation](https://www.elastic.co/guide/en/elasticsearch/reference/current/docs-get.html)
175 | * for more details on these options.
176 | * @return array The record instances, or empty array if nothing was found
177 | */
178 | public static function mget(array $primaryKeys, $options = [])
179 | {
180 | if (empty($primaryKeys)) {
181 | return [];
182 | }
183 | if (count($primaryKeys) === 1) {
184 | $model = static::get(reset($primaryKeys));
185 | return $model === null ? [] : [$model];
186 | }
187 |
188 | $command = static::getDb()->createCommand();
189 | $result = $command->mget(static::index(), static::type(), $primaryKeys, $options);
190 | $models = [];
191 | foreach ($result['docs'] as $doc) {
192 | if ($doc['found']) {
193 | $model = static::instantiate($doc);
194 | static::populateRecord($model, $doc);
195 | $model->afterFind();
196 | $models[] = $model;
197 | }
198 | }
199 |
200 | return $models;
201 | }
202 |
203 | // TODO add more like this feature https://www.elastic.co/guide/en/elasticsearch/reference/current/search-more-like-this.html
204 |
205 | // TODO add percolate functionality https://www.elastic.co/guide/en/elasticsearch/reference/current/search-percolate.html
206 |
207 | // TODO implement copy and move as pk change is not possible
208 |
209 | /**
210 | * @return float returns the score of this record when it was retrieved via a [[find()]] query.
211 | */
212 | public function getScore()
213 | {
214 | return $this->_score;
215 | }
216 |
217 | /**
218 | * @return array|null A list of arrays with highlighted excerpts indexed by field names.
219 | */
220 | public function getHighlight()
221 | {
222 | return $this->_highlight;
223 | }
224 |
225 | /**
226 | * @return array|null An explanation for each hit on how its score was computed.
227 | * @since 2.0.5
228 | */
229 | public function getExplanation()
230 | {
231 | return $this->_explanation;
232 | }
233 |
234 | /**
235 | * Alias to [[get_id()]]. Returns the primary key value.
236 | * @param bool $asArray
237 | * @return mixed
238 | * @deprecated since 2.1.0
239 | */
240 | public function getPrimaryKey($asArray = false)
241 | {
242 | $pk = static::primaryKey()[0];
243 | if ($asArray) {
244 | return [$pk => $this->$pk];
245 | } else {
246 | return $this->$pk;
247 | }
248 | }
249 |
250 | /**
251 | * Alias to [[set_id()]]. Sets the primary key value.
252 | * @param mixed $value
253 | * @throws \yii\base\InvalidCallException when record is not new
254 | * @deprecated since 2.1.0
255 | */
256 | public function setPrimaryKey($value)
257 | {
258 | $pk = static::primaryKey()[0];
259 | $this->$pk = $value;
260 | }
261 |
262 | /**
263 | * Sets the `_id` attribute that holds the primary key (for compatibility with relations)
264 | * @param mixed $value
265 | * @throws \yii\base\InvalidCallException when record is not new
266 | */
267 | public function set_id($value)
268 | {
269 | $pk = static::primaryKey()[0];
270 | if ($this->getIsNewRecord()) {
271 | $this->$pk = $value;
272 | } else {
273 | throw new InvalidCallException('Changing the primaryKey of an already saved record is not allowed.');
274 | }
275 | }
276 |
277 | /**
278 | * Returns the `_id` attribute that holds the primary key (for compatibility with relations)
279 | * @return mixed
280 | */
281 | public function get_id()
282 | {
283 | $pk = static::primaryKey()[0];
284 | return $this->$pk;
285 | }
286 |
287 | /**
288 | * @inheritdoc
289 | */
290 | public function getOldPrimaryKey($asArray = false)
291 | {
292 | $pk = static::primaryKey()[0];
293 | if ($this->getIsNewRecord()) {
294 | $id = null;
295 | } else {
296 | $id = $this->_id;
297 | }
298 | if ($asArray) {
299 | return [$pk => $id];
300 | } else {
301 | return $id;
302 | }
303 | }
304 |
305 | /**
306 | * This method defines the attribute that uniquely identifies a record.
307 | * The name of the primary key attribute is `_id`, and can not be changed.
308 | *
309 | * Elasticsearch does not support composite primary keys in the traditional sense. However to match the signature
310 | * of the [[\yii\db\ActiveRecordInterface|ActiveRecordInterface]] this methods returns an array instead of a
311 | * single string.
312 | *
313 | * @return string[] array of primary key attributes. Only the first element of the array will be used.
314 | */
315 | final public static function primaryKey()
316 | {
317 | return ['_id'];
318 | }
319 |
320 | /**
321 | * Returns the list of all attribute names of the model.
322 | *
323 | * This method must be overridden by child classes to define available attributes.
324 | * IMPORTANT: The primary key (the `_id` attribute) MUST NOT be included in [[attributes()]].
325 | *
326 | * Attributes are names of fields of the corresponding Elasticsearch document.
327 | *
328 | * @return string[] list of attribute names.
329 | * @throws \yii\base\InvalidConfigException if not overridden in a child class.
330 | */
331 | public function attributes()
332 | {
333 | throw new InvalidConfigException('The attributes() method of Elasticsearch ActiveRecord has to be implemented by child classes.');
334 | }
335 |
336 | /**
337 | * A list of attributes that should be treated as array valued when retrieved through [[ActiveQuery::fields]].
338 | *
339 | * If not listed by this method, attributes retrieved through [[ActiveQuery::fields]] will converted to a scalar value
340 | * when the result array contains only one value.
341 | *
342 | * @return string[] list of attribute names. Must be a subset of [[attributes()]].
343 | */
344 | public function arrayAttributes()
345 | {
346 | return [];
347 | }
348 |
349 | /**
350 | * @return string the name of the index this record is stored in.
351 | */
352 | public static function index()
353 | {
354 | return Inflector::pluralize(Inflector::camel2id(StringHelper::basename(get_called_class()), '-'));
355 | }
356 |
357 | /**
358 | * Returns the name of the type of this record.
359 | * IMPORTANT: For Elasticsearch 7 and later, [[type()]] is ignored.
360 | * @return string the name of the type of this record.
361 | */
362 | public static function type()
363 | {
364 | return Inflector::camel2id(StringHelper::basename(get_called_class()), '-');
365 | }
366 |
367 | /**
368 | * @inheritdoc
369 | *
370 | * @param ActiveRecord $record the record to be populated. In most cases this will be an instance
371 | * created by [[instantiate()]] beforehand.
372 | * @param array $row attribute values (name => value)
373 | */
374 | public static function populateRecord($record, $row)
375 | {
376 | $attributes = [];
377 | if (isset($row['_source'])) {
378 | $attributes = $row['_source'];
379 | }
380 | if (isset($row['fields'])) {
381 | // reset fields in case it is scalar value
382 | $arrayAttributes = $record->arrayAttributes();
383 | foreach ($row['fields'] as $key => $value) {
384 | if (!isset($arrayAttributes[$key]) && count($value) === 1) {
385 | $row['fields'][$key] = reset($value);
386 | }
387 | }
388 | $attributes = array_merge($attributes, $row['fields']);
389 | }
390 |
391 | parent::populateRecord($record, $attributes);
392 |
393 | $pk = static::primaryKey()[0];
394 | $record->_id = $row[$pk];
395 | $record->_highlight = isset($row['highlight']) ? $row['highlight'] : null;
396 | $record->_score = isset($row['_score']) ? $row['_score'] : null;
397 | $record->_version = isset($row['_version']) ? $row['_version'] : null; // TODO version should always be available...
398 | $record->_explanation = isset($row['_explanation']) ? $row['_explanation'] : null;
399 | }
400 |
401 | /**
402 | * Creates an active record instance.
403 | *
404 | * This method is called together with [[populateRecord()]] by [[ActiveQuery]].
405 | * It is not meant to be used for creating new records directly.
406 | *
407 | * You may override this method if the instance being created
408 | * depends on the row data to be populated into the record.
409 | * For example, by creating a record based on the value of a column,
410 | * you may implement the so-called single-table inheritance mapping.
411 | * @param array $row row data to be populated into the record.
412 | * This array consists of the following keys:
413 | * - `_source`: refers to the attributes of the record.
414 | * - `_type`: the type this record is stored in.
415 | * - `_index`: the index this record is stored in.
416 | * @return static the newly created active record
417 | */
418 | public static function instantiate($row)
419 | {
420 | return new static;
421 | }
422 |
423 | /**
424 | * Inserts a document into the associated index using the attribute values of this record.
425 | *
426 | * This method performs the following steps in order:
427 | *
428 | * 1. call [[beforeValidate()]] when `$runValidation` is true. If validation
429 | * fails, it will skip the rest of the steps;
430 | * 2. call [[afterValidate()]] when `$runValidation` is true.
431 | * 3. call [[beforeSave()]]. If the method returns false, it will skip the
432 | * rest of the steps;
433 | * 4. insert the record into database. If this fails, it will skip the rest of the steps;
434 | * 5. call [[afterSave()]];
435 | *
436 | * In the above step 1, 2, 3 and 5, events [[EVENT_BEFORE_VALIDATE]],
437 | * [[EVENT_BEFORE_INSERT]], [[EVENT_AFTER_INSERT]] and [[EVENT_AFTER_VALIDATE]]
438 | * will be raised by the corresponding methods.
439 | *
440 | * Only the [[dirtyAttributes|changed attribute values]] will be inserted into database.
441 | *
442 | * If the [[primaryKey|primary key]] is not set (null) during insertion,
443 | * it will be populated with a randomly generated value after insertion.
444 | *
445 | * For example, to insert a customer record:
446 | *
447 | * ~~~
448 | * $customer = new Customer;
449 | * $customer->name = $name;
450 | * $customer->email = $email;
451 | * $customer->insert();
452 | * ~~~
453 | *
454 | * @param bool $runValidation whether to perform validation before saving the record.
455 | * If the validation fails, the record will not be inserted into the database.
456 | * @param array $attributes list of attributes that need to be saved. Defaults to null,
457 | * meaning all attributes will be saved.
458 | * @param array $options options given in this parameter are passed to Elasticsearch
459 | * as request URI parameters. These are among others:
460 | *
461 | * - `routing` define shard placement of this record.
462 | * - `parent` by giving the primaryKey of another record this defines a parent-child relation
463 | *
464 | * Please refer to the [Elasticsearch documentation](https://www.elastic.co/guide/en/elasticsearch/reference/current/docs-index_.html)
465 | * for more details on these options.
466 | *
467 | * By default the `op_type` is set to `create` if model primary key is present.
468 | * @return bool whether the attributes are valid and the record is inserted successfully.
469 | */
470 | public function insert($runValidation = true, $attributes = null, $options = [ ])
471 | {
472 | if ($runValidation && !$this->validate($attributes)) {
473 | return false;
474 | }
475 | if (!$this->beforeSave(true)) {
476 | return false;
477 | }
478 | $values = $this->getDirtyAttributes($attributes);
479 |
480 | if ($this->getPrimaryKey() !== null) {
481 | $options['op_type'] = isset($options['op_type']) ? $options['op_type'] : 'create';
482 | }
483 |
484 | $response = static::getDb()->createCommand()->insert(
485 | static::index(),
486 | static::type(),
487 | $values,
488 | $this->getPrimaryKey(),
489 | $options
490 | );
491 |
492 | if ($response === false) {
493 | return false;
494 | }
495 |
496 | $pk = static::primaryKey()[0];
497 | $this->$pk = $response['_id'];
498 | if ($pk != '_id') {
499 | $values[$pk] = $response['_id'];
500 | }
501 | $this->_version = $response['_version'];
502 | $this->_score = null;
503 |
504 | $changedAttributes = array_fill_keys(array_keys($values), null);
505 | $this->setOldAttributes($values);
506 | $this->afterSave(true, $changedAttributes);
507 |
508 | return true;
509 | }
510 |
511 | /**
512 | * @inheritdoc
513 | *
514 | * @param bool $runValidation whether to perform validation before saving the record.
515 | * If the validation fails, the record will not be inserted into the database.
516 | * @param array $attributeNames list of attribute names that need to be saved. Defaults to null,
517 | * meaning all attributes that are loaded from DB will be saved.
518 | * @param array $options options given in this parameter are passed to Elasticsearch
519 | * as request URI parameters. These are among others:
520 | *
521 | * - `routing` define shard placement of this record.
522 | * - `parent` by giving the primaryKey of another record this defines a parent-child relation
523 | * - `timeout` timeout waiting for a shard to become available.
524 | * - `replication` the replication type for the delete/index operation (sync or async).
525 | * - `consistency` the write consistency of the index/delete operation.
526 | * - `refresh` refresh the relevant primary and replica shards (not the whole index) immediately after the operation occurs, so that the updated document appears in search results immediately.
527 | * - `detect_noop` this parameter will become part of the request body and will prevent the index from getting updated when nothing has changed.
528 | *
529 | * Please refer to the [Elasticsearch documentation](https://www.elastic.co/guide/en/elasticsearch/reference/current/docs-update.html#docs-update-api-query-params)
530 | * for more details on these options.
531 | *
532 | * The following parameters are Yii specific:
533 | *
534 | * - `optimistic_locking` set this to `true` to enable optimistic locking, avoid updating when the record has changed since it
535 | * has been loaded from the database. Yii will set the `version` parameter to the value stored in [[version]].
536 | * See the [Elasticsearch documentation](https://www.elastic.co/guide/en/elasticsearch/guide/current/optimistic-concurrency-control.html) for details.
537 | *
538 | * Make sure the record has been fetched with a [[version]] before. This is only the case
539 | * for records fetched via [[get()]] and [[mget()]] by default. For normal queries, the `_version` field has to be fetched explicitly.
540 | *
541 | * @return int|bool the number of rows affected, or false if validation fails
542 | * or [[beforeSave()]] stops the updating process.
543 | * @throws StaleObjectException if optimistic locking is enabled and the data being updated is outdated.
544 | * @throws InvalidParamException if no [[version]] is available and optimistic locking is enabled.
545 | * @throws Exception in case update failed.
546 | */
547 | public function update($runValidation = true, $attributeNames = null, $options = [])
548 | {
549 | if ($runValidation && !$this->validate($attributeNames)) {
550 | return false;
551 | }
552 | return $this->updateInternal($attributeNames, $options);
553 | }
554 |
555 | /**
556 | * @param array $attributes attributes to update
557 | * @param array $options options given in this parameter are passed to Elasticsearch
558 | * as request URI parameters. See [[update()]] for details.
559 | * @return int|false the number of rows affected, or false if [[beforeSave()]] stops the updating process.
560 | * @throws StaleObjectException if optimistic locking is enabled and the data being updated is outdated.
561 | * @throws InvalidParamException if no [[version]] is available and optimistic locking is enabled.
562 | * @throws Exception in case update failed.
563 | * @see update()
564 | */
565 | protected function updateInternal($attributes = null, $options = [])
566 | {
567 | if (!$this->beforeSave(false)) {
568 | return false;
569 | }
570 | $values = $this->getDirtyAttributes($attributes);
571 | if (empty($values)) {
572 | $this->afterSave(false, $values);
573 | return 0;
574 | }
575 |
576 | if (isset($options['optimistic_locking']) && $options['optimistic_locking']) {
577 | if ($this->_version === null) {
578 | throw new InvalidArgumentException('Unable to use optimistic locking on a record that has no version set. Refer to the docs of ActiveRecord::update() for details.');
579 | }
580 | $options['version'] = $this->_version;
581 | unset($options['optimistic_locking']);
582 | }
583 |
584 | try {
585 | $result = static::getDb()->createCommand()->update(
586 | static::index(),
587 | static::type(),
588 | $this->getOldPrimaryKey(false),
589 | $values,
590 | $options
591 | );
592 | } catch (Exception $e) {
593 | // HTTP 409 is the response in case of failed optimistic locking
594 | // https://www.elastic.co/guide/en/elasticsearch/guide/current/optimistic-concurrency-control.html
595 | if (isset($e->errorInfo['responseCode']) && $e->errorInfo['responseCode'] == 409) {
596 | throw new StaleObjectException('The object being updated is outdated.', $e->errorInfo, $e->getCode(), $e);
597 | }
598 | throw $e;
599 | }
600 |
601 | if (is_array($result) && isset($result['_version'])) {
602 | $this->_version = $result['_version'];
603 | }
604 |
605 | $changedAttributes = [];
606 | foreach ($values as $name => $value) {
607 | $changedAttributes[$name] = $this->getOldAttribute($name);
608 | $this->setOldAttribute($name, $value);
609 | }
610 | $this->afterSave(false, $changedAttributes);
611 |
612 | if ($result === false) {
613 | return 0;
614 | } else {
615 | return 1;
616 | }
617 | }
618 |
619 | /**
620 | * Performs a quick and highly efficient scroll/scan query to get the list of primary keys that
621 | * satisfy the given condition. If condition is a list of primary keys
622 | * (e.g.: `['_id' => ['1', '2', '3']]`), the query is not performed for performance considerations.
623 | * @param array $condition please refer to [[ActiveQuery::where()]] on how to specify this parameter
624 | * @return array primary keys that correspond to given conditions
625 | * @see updateAll()
626 | * @see updateAllCounters()
627 | * @see deleteAll()
628 | * @since 2.0.4
629 | */
630 | protected static function primaryKeysByCondition($condition)
631 | {
632 | $pkName = static::primaryKey()[0];
633 | if (count($condition) == 1 && isset($condition[$pkName])) {
634 | $primaryKeys = (array)$condition[$pkName];
635 | } else {
636 | //fetch only document metadata (no fields), 1000 documents per shard
637 | $query = static::find()->where($condition)->asArray()->source(false)->limit(1000);
638 | $primaryKeys = [];
639 | foreach ($query->each('1m') as $document) {
640 | $primaryKeys[] = $document['_id'];
641 | }
642 | }
643 | return $primaryKeys;
644 | }
645 |
646 | /**
647 | * Updates all records that match a certain condition.
648 | * For example, to change the status to be 1 for all customers whose status is 2:
649 | *
650 | * ~~~
651 | * Customer::updateAll(['status' => 1], ['status' => 2]);
652 | * ~~~
653 | *
654 | * @param array $attributes attribute values (name-value pairs) to be saved into the table
655 | * @param array $condition the conditions that will be passed to the `where()` method when building the query.
656 | * Please refer to [[ActiveQuery::where()]] on how to specify this parameter.
657 | * @return int the number of rows updated
658 | * @throws Exception on error.
659 | * @see [[ActiveRecord::primaryKeysByCondition()]]
660 | */
661 | public static function updateAll($attributes, $condition = [])
662 | {
663 | $primaryKeys = static::primaryKeysByCondition($condition);
664 | if (empty($primaryKeys)) {
665 | return 0;
666 | }
667 |
668 | $bulkCommand = static::getDb()->createBulkCommand([
669 | "index" => static::index(),
670 | "type" => static::type(),
671 | ]);
672 | foreach ($primaryKeys as $pk) {
673 | $bulkCommand->addAction(["update" => ["_id" => $pk]], ["doc" => $attributes]);
674 | }
675 | $response = $bulkCommand->execute();
676 |
677 | $n = 0;
678 | $errors = [];
679 | foreach ($response['items'] as $item) {
680 | if (isset($item['update']['status']) && $item['update']['status'] == 200) {
681 | $n++;
682 | } else {
683 | $errors[] = $item['update'];
684 | }
685 | }
686 | if (!empty($errors) || isset($response['errors']) && $response['errors']) {
687 | throw new Exception(__METHOD__ . ' failed updating records.', $errors);
688 | }
689 |
690 | return $n;
691 | }
692 |
693 | /**
694 | * Updates all matching records using the provided counter changes and conditions.
695 | * For example, to add 1 to age of all customers whose status is 2,
696 | *
697 | * ~~~
698 | * Customer::updateAllCounters(['age' => 1], ['status' => 2]);
699 | * ~~~
700 | *
701 | * @param array $counters the counters to be updated (attribute name => increment value).
702 | * Use negative values if you want to decrement the counters.
703 | * @param array $condition the conditions that will be passed to the `where()` method when building the query.
704 | * Please refer to [[ActiveQuery::where()]] on how to specify this parameter.
705 | * @return int the number of rows updated
706 | * @throws Exception on error.
707 | * @see [[ActiveRecord::primaryKeysByCondition()]]
708 | */
709 | public static function updateAllCounters($counters, $condition = [])
710 | {
711 | $primaryKeys = static::primaryKeysByCondition($condition);
712 | if (empty($primaryKeys) || empty($counters)) {
713 | return 0;
714 | }
715 |
716 | $bulkCommand = static::getDb()->createBulkCommand([
717 | "index" => static::index(),
718 | "type" => static::type(),
719 | ]);
720 | foreach ($primaryKeys as $pk) {
721 | $script = '';
722 | foreach ($counters as $counter => $value) {
723 | $script .= "ctx._source.{$counter} += params.{$counter};\n";
724 | }
725 | $bulkCommand->addAction(["update" => ["_id" => $pk]], [
726 | 'script' => [
727 | 'inline' => $script,
728 | 'params' => $counters,
729 | 'lang' => 'painless',
730 | ],
731 | ]);
732 | }
733 | $response = $bulkCommand->execute();
734 |
735 | $n = 0;
736 | $errors = [];
737 | foreach ($response['items'] as $item) {
738 | if (isset($item['update']['status']) && $item['update']['status'] == 200) {
739 | $n++;
740 | } else {
741 | $errors[] = $item['update'];
742 | }
743 | }
744 | if (!empty($errors) || isset($response['errors']) && $response['errors']) {
745 | throw new Exception(__METHOD__ . ' failed updating records counters.', $errors);
746 | }
747 |
748 | return $n;
749 | }
750 |
751 | /**
752 | * @inheritdoc
753 | *
754 | * @param array $options options given in this parameter are passed to Elasticsearch
755 | * as request URI parameters. These are among others:
756 | *
757 | * - `routing` define shard placement of this record.
758 | * - `parent` by giving the primaryKey of another record this defines a parent-child relation
759 | * - `timeout` timeout waiting for a shard to become available.
760 | * - `replication` the replication type for the delete/index operation (sync or async).
761 | * - `consistency` the write consistency of the index/delete operation.
762 | * - `refresh` refresh the relevant primary and replica shards (not the whole index) immediately after the operation occurs, so that the updated document appears in search results immediately.
763 | *
764 | * Please refer to the [Elasticsearch documentation](https://www.elastic.co/guide/en/elasticsearch/reference/current/docs-delete.html)
765 | * for more details on these options.
766 | *
767 | * The following parameters are Yii specific:
768 | *
769 | * - `optimistic_locking` set this to `true` to enable optimistic locking, avoid updating when the record has changed since it
770 | * has been loaded from the database. Yii will set the `version` parameter to the value stored in [[version]].
771 | * See the [Elasticsearch documentation](https://www.elastic.co/guide/en/elasticsearch/reference/current/docs-delete.html#delete-versioning) for details.
772 | *
773 | * Make sure the record has been fetched with a [[version]] before. This is only the case
774 | * for records fetched via [[get()]] and [[mget()]] by default. For normal queries, the `_version` field has to be fetched explicitly.
775 | *
776 | * @return int|bool the number of rows deleted, or false if the deletion is unsuccessful for some reason.
777 | * Note that it is possible the number of rows deleted is 0, even though the deletion execution is successful.
778 | * @throws StaleObjectException if optimistic locking is enabled and the data being deleted is outdated.
779 | * @throws Exception in case delete failed.
780 | */
781 | public function delete($options = [])
782 | {
783 | if (!$this->beforeDelete()) {
784 | return false;
785 | }
786 | if (isset($options['optimistic_locking']) && $options['optimistic_locking']) {
787 | if ($this->_version === null) {
788 | throw new InvalidArgumentException('Unable to use optimistic locking on a record that has no version set. Refer to the docs of ActiveRecord::delete() for details.');
789 | }
790 | $options['version'] = $this->_version;
791 | unset($options['optimistic_locking']);
792 | }
793 |
794 | try {
795 | $result = static::getDb()->createCommand()->delete(
796 | static::index(),
797 | static::type(),
798 | $this->getOldPrimaryKey(false),
799 | $options
800 | );
801 | } catch (Exception $e) {
802 | // HTTP 409 is the response in case of failed optimistic locking
803 | // https://www.elastic.co/guide/en/elasticsearch/guide/current/optimistic-concurrency-control.html
804 | if (isset($e->errorInfo['responseCode']) && $e->errorInfo['responseCode'] == 409) {
805 | throw new StaleObjectException('The object being deleted is outdated.', $e->errorInfo, $e->getCode(), $e);
806 | }
807 | throw $e;
808 | }
809 |
810 | $this->setOldAttributes(null);
811 |
812 | $this->afterDelete();
813 |
814 | if ($result === false) {
815 | return 0;
816 | } else {
817 | return 1;
818 | }
819 | }
820 |
821 | /**
822 | * Deletes rows in the table using the provided conditions.
823 | * WARNING: If you do not specify any condition, this method will delete ALL rows in the table.
824 | *
825 | * For example, to delete all customers whose status is 3:
826 | *
827 | * ~~~
828 | * Customer::deleteAll(['status' => 3]);
829 | * ~~~
830 | *
831 | * @param array $condition the conditions that will be passed to the `where()` method when building the query.
832 | * Please refer to [[ActiveQuery::where()]] on how to specify this parameter.
833 | * @return int the number of rows deleted
834 | * @throws Exception on error.
835 | * @see [[ActiveRecord::primaryKeysByCondition()]]
836 | */
837 | public static function deleteAll($condition = [])
838 | {
839 | $primaryKeys = static::primaryKeysByCondition($condition);
840 | if (empty($primaryKeys)) {
841 | return 0;
842 | }
843 |
844 | $bulkCommand = static::getDb()->createBulkCommand([
845 | "index" => static::index(),
846 | "type" => static::type(),
847 | ]);
848 | foreach ($primaryKeys as $pk) {
849 | $bulkCommand->addDeleteAction($pk);
850 | }
851 | $response = $bulkCommand->execute();
852 |
853 | $n = 0;
854 | $errors = [];
855 | foreach ($response['items'] as $item) {
856 | if (isset($item['delete']['status']) && $item['delete']['status'] == 200) {
857 | if (isset($item['delete']['found']) && $item['delete']['found']) {
858 | # ES5 uses "found"
859 | $n++;
860 | } elseif (isset($item['delete']['result']) && $item['delete']['result'] == "deleted") {
861 | # ES6 uses "result"
862 | $n++;
863 | }
864 | } else {
865 | $errors[] = $item['delete'];
866 | }
867 | }
868 | if (!empty($errors) || isset($response['errors']) && $response['errors']) {
869 | throw new Exception(__METHOD__ . ' failed deleting records.', $errors);
870 | }
871 |
872 | return $n;
873 | }
874 |
875 | /**
876 | * This method has no effect in Elasticsearch ActiveRecord.
877 | *
878 | * Elasticsearch ActiveRecord uses [native Optimistic locking](https://www.elastic.co/guide/en/elasticsearch/guide/current/optimistic-concurrency-control.html).
879 | * See [[update()]] for more details.
880 | */
881 | public function optimisticLock()
882 | {
883 | return null;
884 | }
885 |
886 | /**
887 | * Destroys the relationship in current model.
888 | *
889 | * This method is not supported by Elasticsearch.
890 | */
891 | public function unlinkAll($name, $delete = false)
892 | {
893 | throw new NotSupportedException('unlinkAll() is not supported by Elasticsearch, use unlink() instead.');
894 | }
895 |
896 | public function link($name, $model, $extraColumns = [])
897 | {
898 | $relation = $this->getRelation($name);
899 |
900 | if ($relation->via === null) {
901 | $this->validateViaRelationLink($model, $relation);
902 | }
903 |
904 | parent::link($name, $model, $extraColumns);
905 | }
906 |
907 | /**
908 | * Validates model so that it does not contain array as its keys while linking.
909 | *
910 | * @param ActiveRecordInterface $model the model to be linked with the current one.
911 | * @param ActiveQueryInterface|ActiveQuery the relational query object.
912 | */
913 | protected function validateViaRelationLink($model, $relation)
914 | {
915 | $p1 = $model->isPrimaryKey(array_keys($relation->link));
916 | $p2 = static::isPrimaryKey(array_values($relation->link));
917 |
918 | $atLeastOneExists = !$this->getIsNewRecord() || !$model->getIsNewRecord();
919 |
920 | $foreign = null;
921 | $link = null;
922 |
923 | if ($p1 && $p2 && $atLeastOneExists) {
924 |
925 | if ($this->getIsNewRecord()) {
926 | $foreign = $this;
927 | $link = array_flip($relation->link);
928 | } else {
929 | $foreign = $model;
930 | $link = $relation->link;
931 | }
932 | } elseif ($p1) {
933 | $foreign = $this;
934 | $link = array_flip($relation->link);
935 | } elseif ($p2) {
936 | $foreign = $model;
937 | $link = $relation->link;
938 | }
939 |
940 | if ($foreign && $link) {
941 | foreach ($link as $fk => $pk) {
942 | if (is_array($foreign->{$fk})) {
943 | throw new InvalidCallException('Unable to link models: foreign model cannot be linked if its property is an array.');
944 | }
945 | }
946 | }
947 | }
948 | }
949 |
--------------------------------------------------------------------------------
/src/BatchQueryResult.php:
--------------------------------------------------------------------------------
1 | from('user');
31 | * foreach ($query->batch() as $i => $users) {
32 | * // $users represents the rows in the $i-th batch
33 | * }
34 | * foreach ($query->each() as $user) {
35 | * }
36 | * ```
37 | *
38 | * @author Konstantin Sirotkin
39 | * @since 2.0.4
40 | */
41 | class BatchQueryResult extends BaseObject implements \Iterator
42 | {
43 | /**
44 | * @var Connection the DB connection to be used when performing batch query.
45 | * If null, the `elasticsearch` application component will be used.
46 | */
47 | public $db;
48 | /**
49 | * @var Query the query object associated with this batch query.
50 | * Do not modify this property directly unless after [[reset()]] is called explicitly.
51 | */
52 | public $query;
53 | /**
54 | * @var boolean whether to return a single row during each iteration.
55 | * If false, a whole batch of rows will be returned in each iteration.
56 | */
57 | public $each = false;
58 | /**
59 | * @var DataReader the data reader associated with this batch query.
60 | */
61 | private $_dataReader;
62 | /**
63 | * @var array the data retrieved in the current batch
64 | */
65 | private $_batch;
66 | /**
67 | * @var mixed the value for the current iteration
68 | */
69 | private $_value;
70 | /**
71 | * @var string|integer the key for the current iteration
72 | */
73 | private $_key;
74 | /**
75 | * @var string the amount of time to keep the scroll window open
76 | * (in Elasticsearch [time units](https://www.elastic.co/guide/en/elasticsearch/reference/current/common-options.html#time-units).
77 | */
78 | public $scrollWindow = '1m';
79 |
80 | /*
81 | * @var string internal Elasticsearch scroll id
82 | */
83 | private $_lastScrollId = null;
84 |
85 |
86 | /**
87 | * Destructor.
88 | */
89 | public function __destruct()
90 | {
91 | // make sure cursor is closed
92 | $this->reset();
93 | }
94 |
95 | /**
96 | * Resets the batch query.
97 | * This method will clean up the existing batch query so that a new batch query can be performed.
98 | */
99 | public function reset()
100 | {
101 | if(isset($this->_lastScrollId)) {
102 | $this->query->createCommand($this->db)->clearScroll(['scroll_id' => $this->_lastScrollId]);
103 | }
104 |
105 | $this->_batch = null;
106 | $this->_value = null;
107 | $this->_key = null;
108 | $this->_lastScrollId = null;
109 | }
110 |
111 | /**
112 | * Resets the iterator to the initial state.
113 | * This method is required by the interface [[\Iterator]].
114 | */
115 | #[ReturnTypeWillChange]
116 | public function rewind()
117 | {
118 | $this->reset();
119 | $this->next();
120 | }
121 |
122 | /**
123 | * Moves the internal pointer to the next dataset.
124 | * This method is required by the interface [[\Iterator]].
125 | */
126 | #[ReturnTypeWillChange]
127 | public function next()
128 | {
129 | if ($this->_batch === null || !$this->each || $this->each && next($this->_batch) === false) {
130 | $this->_batch = $this->fetchData();
131 | reset($this->_batch);
132 | }
133 |
134 | if ($this->each) {
135 | $this->_value = current($this->_batch);
136 | if ($this->query->indexBy !== null) {
137 | $this->_key = key($this->_batch);
138 | } elseif (key($this->_batch) !== null) {
139 | $this->_key++;
140 | } else {
141 | $this->_key = null;
142 | }
143 | } else {
144 | $this->_value = $this->_batch;
145 | $this->_key = $this->_key === null ? 0 : $this->_key + 1;
146 | }
147 | }
148 |
149 | /**
150 | * Fetches the next batch of data.
151 | * @return array the data fetched
152 | */
153 | protected function fetchData()
154 | {
155 | if (null === $this->_lastScrollId) {
156 | //first query - do search
157 | $options = ['scroll' => $this->scrollWindow];
158 | if(!$this->query->orderBy) {
159 | $query = clone $this->query;
160 | $query->orderBy('_doc');
161 | $cmd = $this->query->createCommand($this->db);
162 | } else {
163 | $cmd = $this->query->createCommand($this->db);
164 | }
165 | $result = $cmd->search($options);
166 | if ($result === false) {
167 | throw new Exception('Elasticsearch search query failed.');
168 | }
169 | } else {
170 | //subsequent queries - do scroll
171 | $result = $this->query->createCommand($this->db)->scroll([
172 | 'scroll_id' => $this->_lastScrollId,
173 | 'scroll' => $this->scrollWindow,
174 | ]);
175 | }
176 |
177 | //get last scroll id
178 | $this->_lastScrollId = $result['_scroll_id'];
179 |
180 | //get data
181 | return $this->query->populate($result['hits']['hits']);
182 | }
183 |
184 | /**
185 | * Returns the index of the current dataset.
186 | * This method is required by the interface [[\Iterator]].
187 | * @return int the index of the current row.
188 | */
189 | #[ReturnTypeWillChange]
190 | public function key()
191 | {
192 | return $this->_key;
193 | }
194 |
195 | /**
196 | * Returns the current dataset.
197 | * This method is required by the interface [[\Iterator]].
198 | * @return mixed the current dataset.
199 | */
200 | #[ReturnTypeWillChange]
201 | public function current()
202 | {
203 | return $this->_value;
204 | }
205 |
206 | /**
207 | * Returns whether there is a valid dataset at the current position.
208 | * This method is required by the interface [[\Iterator]].
209 | * @return bool whether there is a valid dataset at the current position.
210 | */
211 | #[ReturnTypeWillChange]
212 | public function valid()
213 | {
214 | return !empty($this->_batch);
215 | }
216 | }
217 |
--------------------------------------------------------------------------------
/src/BulkCommand.php:
--------------------------------------------------------------------------------
1 |
21 | * @since 2.0.5
22 | */
23 | class BulkCommand extends Component
24 | {
25 | /**
26 | * @var Connection
27 | */
28 | public $db;
29 | /**
30 | * @var string Default index to execute the queries on. Defaults to null meaning that index needs to be specified in every action.
31 | */
32 | public $index;
33 | /**
34 | * @var string Default type to execute the queries on. Defaults to null meaning that type needs to be specified in every action.
35 | */
36 | public $type;
37 | /**
38 | * @var array|string Actions to be executed in this bulk command, given as either an array of arrays or as one newline-delimited string.
39 | * All actions except delete span two lines.
40 | */
41 | public $actions;
42 | /**
43 | * @var array Options to be appended to the query URL.
44 | */
45 | public $options = [];
46 |
47 |
48 | /**
49 | * Executes the bulk command.
50 | * @return mixed
51 | * @throws \yii\base\InvalidCallException
52 | */
53 | public function execute()
54 | {
55 | //valid endpoints are /_bulk, /{index}/_bulk, and {index}/{type}/_bulk
56 | //for ES7+ type is omitted
57 | if ($this->index === null && $this->type === null) {
58 | $endpoint = ['_bulk'];
59 | } elseif ($this->index !== null && $this->type === null) {
60 | $endpoint = [$this->index, '_bulk'];
61 | } elseif ($this->index !== null && $this->type !== null) {
62 | if ($this->db->dslVersion >= 7) {
63 | $endpoint = [$this->index, '_bulk'];
64 | } else {
65 | $endpoint = [$this->index, $this->type, '_bulk'];
66 | }
67 | } else {
68 | throw new InvalidCallException('Invalid endpoint: if type is defined, index must be defined too.');
69 | }
70 |
71 | if (empty($this->actions)) {
72 | $body = '{}';
73 | } elseif (is_array($this->actions)) {
74 | $body = '';
75 | $prettyPrintSupported = property_exists('yii\\helpers\\Json', 'prettyPrint');
76 | if ($prettyPrintSupported) {
77 | $originalPrettyPrint = Json::$prettyPrint;
78 | Json::$prettyPrint = false; // ElasticSearch bulk API uses new lines as delimiters.
79 | }
80 | foreach ($this->actions as $action) {
81 | $body .= Json::encode($action) . "\n";
82 | }
83 | if ($prettyPrintSupported) {
84 | Json::$prettyPrint = $originalPrettyPrint;
85 | }
86 | } else {
87 | $body = $this->actions;
88 | }
89 |
90 | return $this->db->post($endpoint, $this->options, $body);
91 | }
92 |
93 | /**
94 | * Adds an action to the command. Will overwrite existing actions if they are specified as a string.
95 | * @param array $line1 First action expressed as an array (will be encoded to JSON automatically).
96 | * @param array|null $line2 Second action expressed as an array (will be encoded to JSON automatically).
97 | * @see https://www.elastic.co/guide/en/elasticsearch/reference/7.x/docs-bulk.html
98 | */
99 | public function addAction($line1, $line2 = null)
100 | {
101 | if (!is_array($this->actions)) {
102 | $this->actions = [];
103 | }
104 |
105 | $this->actions[] = $line1;
106 |
107 | if ($line2 !== null) {
108 | $this->actions[] = $line2;
109 | }
110 | }
111 |
112 | /**
113 | * Adds a delete action to the command.
114 | * @param string $id Document ID
115 | * @param string|null $index Index that the document belongs to. Can be set to null if the command has
116 | * a default index ([[BulkCommand::$index]]) assigned.
117 | * @param string|null $type Type that the document belongs to. Can be set to null if the command has
118 | * a default type ([[BulkCommand::$type]]) assigned.
119 | */
120 | public function addDeleteAction($id, $index = null, $type = null)
121 | {
122 | $actionData = ['_id' => $id];
123 |
124 | if (!empty($index)) {
125 | $actionData['_index'] = $index;
126 | }
127 |
128 | if (!empty($type)) {
129 | $actionData['_type'] = $type;
130 | }
131 |
132 | $this->addAction(['delete' => $actionData]);
133 | }
134 | }
135 |
--------------------------------------------------------------------------------
/src/Command.php:
--------------------------------------------------------------------------------
1 |
22 | * @since 2.0
23 | */
24 | class Command extends Component
25 | {
26 | /**
27 | * @var Connection
28 | */
29 | public $db;
30 | /**
31 | * @var string|array the indexes to execute the query on. Defaults to null meaning all indexes
32 | * @see https://www.elastic.co/guide/en/elasticsearch/reference/current/search-search.html#search-multi-index-type
33 | */
34 | public $index;
35 | /**
36 | * @var string|array|null the types to execute the query on. Defaults to null meaning all types
37 | */
38 | public $type;
39 | /**
40 | * @var array list of arrays or json strings that become parts of a query
41 | */
42 | public $queryParts;
43 | /**
44 | * @var array options to be appended to the query URL, such as "search_type" for search or "timeout" for delete
45 | */
46 | public $options = [];
47 |
48 |
49 | /**
50 | * Sends a request to the _search API and returns the result
51 | * @param array $options URL options
52 | * @return mixed
53 | * @throws Exception
54 | * @throws \yii\base\InvalidConfigException
55 | */
56 | public function search($options = [])
57 | {
58 | $query = $this->queryParts;
59 | if (empty($query)) {
60 | $query = '{}';
61 | }
62 | if (is_array($query)) {
63 | $query = Json::encode($query);
64 | }
65 | $url = [$this->index !== null ? $this->index : '_all'];
66 |
67 | if ($this->db->dslVersion < 7 && $this->type !== null) {
68 | $url[] = $this->type;
69 | }
70 |
71 | $url[] = '_search';
72 |
73 | return $this->db->get($url, array_merge($this->options, $options), $query);
74 | }
75 |
76 | /**
77 | * Sends a request to the delete by query
78 | * @param array $options URL options
79 | * @return mixed
80 | * @throws Exception
81 | * @throws \yii\base\InvalidConfigException
82 | */
83 | public function deleteByQuery($options = [])
84 | {
85 | if (!isset($this->queryParts['query'])) {
86 | throw new InvalidCallException('Can not call deleteByQuery when no query is given.');
87 | }
88 | $query = [
89 | 'query' => $this->queryParts['query'],
90 | ];
91 | if (isset($this->queryParts['filter'])) {
92 | $query['filter'] = $this->queryParts['filter'];
93 | }
94 | $query = Json::encode($query);
95 | $url = [$this->index !== null ? $this->index : '_all'];
96 | if ($this->type !== null) {
97 | $url[] = $this->type;
98 | }
99 | $url[] = '_delete_by_query';
100 |
101 | return $this->db->post($url, array_merge($this->options, $options), $query);
102 | }
103 |
104 | /**
105 | * Sends a suggest request to the _search API and returns the result
106 | * @param string|array $suggester the suggester body
107 | * @param array $options URL options
108 | * @return mixed
109 | * @throws Exception
110 | * @throws \yii\base\InvalidConfigException
111 | * @see https://www.elastic.co/guide/en/elasticsearch/reference/current/search-suggesters.html
112 | */
113 | public function suggest($suggester, $options = [])
114 | {
115 | if (empty($suggester)) {
116 | $suggester = '{}';
117 | }
118 | if (is_array($suggester)) {
119 | $suggester = Json::encode($suggester);
120 | }
121 | $body = '{"suggest":'.$suggester.',"size":0}';
122 | $url = [
123 | $this->index !== null ? $this->index : '_all',
124 | '_search'
125 | ];
126 |
127 | $result = $this->db->post($url, array_merge($this->options, $options), $body);
128 |
129 | return $result['suggest'];
130 | }
131 |
132 | /**
133 | * Inserts a document into an index
134 | * @param string $index Index that the document belongs to.
135 | * @param string|null $type Type that the document belongs to.
136 | * @param string|array $data json string or array of data to store
137 | * @param string|null $id the documents id. If not specified Id will be automatically chosen
138 | * @param array $options URL options
139 | * @return mixed
140 | * @throws Exception
141 | * @throws \yii\base\InvalidConfigException
142 | * @see https://www.elastic.co/guide/en/elasticsearch/reference/current/docs-index_.html
143 | */
144 | public function insert($index, $type, $data, $id = null, $options = [])
145 | {
146 | if (empty($data)) {
147 | $body = '{}';
148 | } else {
149 | $body = is_array($data) ? Json::encode($data) : $data;
150 | }
151 |
152 | if ($id !== null) {
153 | if ($this->db->dslVersion >= 7) {
154 | return $this->db->put([$index, '_doc', $id], $options, $body);
155 | } else {
156 | return $this->db->put([$index, $type, $id], $options, $body);
157 | }
158 | } else {
159 | if ($this->db->dslVersion >= 7) {
160 | return $this->db->post([$index, '_doc'], $options, $body);
161 | } else {
162 | return $this->db->post([$index, $type], $options, $body);
163 | }
164 | }
165 | }
166 |
167 | /**
168 | * gets a document from the index
169 | * @param string $index Index that the document belongs to.
170 | * @param string|null $type Type that the document belongs to.
171 | * @param string $id the documents id.
172 | * @param array $options URL options
173 | * @return mixed
174 | * @throws Exception
175 | * @throws \yii\base\InvalidConfigException
176 | * @see https://www.elastic.co/guide/en/elasticsearch/reference/current/docs-get.html
177 | */
178 | public function get($index, $type, $id, $options = [])
179 | {
180 | if ($this->db->dslVersion >= 7) {
181 | return $this->db->get([$index, '_doc', $id], $options);
182 | } else {
183 | return $this->db->get([$index, $type, $id], $options);
184 | }
185 | }
186 |
187 | /**
188 | * gets multiple documents from the index
189 | *
190 | * TODO allow specifying type and index + fields
191 | * @param string $index Index that the document belongs to.
192 | * @param string|null $type Type that the document belongs to.
193 | * @param string[] $ids the documents ids as values in array.
194 | * @param array $options URL options
195 | * @return mixed
196 | * @throws Exception
197 | * @throws \yii\base\InvalidConfigException
198 | * @see https://www.elastic.co/guide/en/elasticsearch/reference/current/docs-multi-get.html
199 | */
200 | public function mget($index, $type, $ids, $options = [])
201 | {
202 | $body = Json::encode(['ids' => array_values($ids)]);
203 |
204 | if ($this->db->dslVersion >= 7) {
205 | return $this->db->get([$index, '_mget'], $options, $body);
206 | } else {
207 | return $this->db->get([$index, $type, '_mget'], $options, $body);
208 | }
209 | }
210 |
211 | /**
212 | * gets a documents _source from the index (>=v0.90.1)
213 | * @param string $index Index that the document belongs to.
214 | * @param string|null $type Type that the document belongs to.
215 | * @param string $id the documents id.
216 | * @return mixed
217 | * @throws Exception
218 | * @throws \yii\base\InvalidConfigException
219 | * @see https://www.elastic.co/guide/en/elasticsearch/reference/current/docs-get.html#_source
220 | */
221 | public function getSource($index, $type, $id)
222 | {
223 | if ($this->db->dslVersion >= 7) {
224 | return $this->db->get([$index, '_doc', $id]);
225 | } else {
226 | return $this->db->get([$index, $type, $id]);
227 | }
228 | }
229 |
230 | /**
231 | * gets a document from the index
232 | * @param string $index Index that the document belongs to.
233 | * @param string|null $type Type that the document belongs to.
234 | * @param string $id the documents id.
235 | * @return mixed
236 | * @throws Exception
237 | * @throws \yii\base\InvalidConfigException
238 | * @see https://www.elastic.co/guide/en/elasticsearch/reference/current/docs-get.html
239 | */
240 | public function exists($index, $type, $id)
241 | {
242 | if ($this->db->dslVersion >= 7) {
243 | return $this->db->head([$index, '_doc', $id]);
244 | } else {
245 | return $this->db->head([$index, $type, $id]);
246 | }
247 | }
248 |
249 | /**
250 | * deletes a document from the index
251 | * @param string $index Index that the document belongs to.
252 | * @param string|null $type Type that the document belongs to.
253 | * @param string $id the documents id.
254 | * @param array $options URL options
255 | * @return mixed
256 | * @throws Exception
257 | * @throws \yii\base\InvalidConfigException
258 | * @see https://www.elastic.co/guide/en/elasticsearch/reference/current/docs-delete.html
259 | */
260 | public function delete($index, $type, $id, $options = [])
261 | {
262 | if ($this->db->dslVersion >= 7) {
263 | return $this->db->delete([$index, '_doc', $id], $options);
264 | } else {
265 | return $this->db->delete([$index, $type, $id], $options);
266 | }
267 | }
268 |
269 | /**
270 | * updates a document
271 | * @param string $index Index that the document belongs to.
272 | * @param string|null $type Type that the document belongs to.
273 | * @param string $id the documents id.
274 | * @param mixed $data
275 | * @param array $options URL options
276 | * @return mixed
277 | * @throws Exception
278 | * @throws \yii\base\InvalidConfigException
279 | * @see https://www.elastic.co/guide/en/elasticsearch/reference/current/docs-update.html
280 | */
281 | public function update($index, $type, $id, $data, $options = [])
282 | {
283 | $body = [
284 | 'doc' => empty($data) ? new \stdClass() : $data,
285 | ];
286 | if (isset($options["detect_noop"])) {
287 | $body["detect_noop"] = $options["detect_noop"];
288 | unset($options["detect_noop"]);
289 | }
290 |
291 | if ($this->db->dslVersion >= 7) {
292 | return $this->db->post([$index, '_update', $id], $options, Json::encode($body));
293 | } else {
294 | return $this->db->post([$index, $type, $id, '_update'], $options, Json::encode($body));
295 | }
296 | }
297 |
298 | // TODO bulk https://www.elastic.co/guide/en/elasticsearch/reference/current/docs-bulk.html
299 |
300 | /**
301 | * creates an index
302 | * @param string $index Index that the document belongs to.
303 | * @param null|array $configuration
304 | * @return mixed
305 | * @throws Exception
306 | * @throws \yii\base\InvalidConfigException
307 | * @see https://www.elastic.co/guide/en/elasticsearch/reference/current/indices-create-index.html
308 | */
309 | public function createIndex($index, $configuration = null)
310 | {
311 | $body = $configuration !== null ? Json::encode($configuration) : null;
312 |
313 | return $this->db->put([$index], [], $body);
314 | }
315 |
316 | /**
317 | * deletes an index
318 | * @param string $index Index that the document belongs to.
319 | * @return mixed
320 | * @throws Exception
321 | * @throws \yii\base\InvalidConfigException
322 | * @see https://www.elastic.co/guide/en/elasticsearch/reference/current/indices-delete-index.html
323 | */
324 | public function deleteIndex($index)
325 | {
326 | return $this->db->delete([$index]);
327 | }
328 |
329 | /**
330 | * deletes all indexes
331 | * @return mixed
332 | * @throws Exception
333 | * @throws \yii\base\InvalidConfigException
334 | * @see https://www.elastic.co/guide/en/elasticsearch/reference/current/indices-delete-index.html
335 | */
336 | public function deleteAllIndexes()
337 | {
338 | return $this->db->delete(['_all']);
339 | }
340 |
341 | /**
342 | * checks whether an index exists
343 | * @param string $index Index that the document belongs to.
344 | * @return mixed
345 | * @throws Exception
346 | * @throws \yii\base\InvalidConfigException
347 | * @see https://www.elastic.co/guide/en/elasticsearch/reference/current/indices-exists.html
348 | */
349 | public function indexExists($index)
350 | {
351 | return $this->db->head([$index]);
352 | }
353 |
354 | /**
355 | * @param string $index Index that the document belongs to.
356 | * @param string|null $type Type that the document belongs to.
357 | * @return mixed
358 | * @throws Exception
359 | * @throws \yii\base\InvalidConfigException
360 | * @see https://www.elastic.co/guide/en/elasticsearch/reference/current/indices-types-exists.html
361 | */
362 | public function typeExists($index, $type)
363 | {
364 | if ($this->db->dslVersion >= 7) {
365 | return $this->db->head([$index, '_doc']);
366 | } else {
367 | return $this->db->head([$index, $type]);
368 | }
369 | }
370 |
371 | /**
372 | * @param string $alias
373 | *
374 | * @return bool
375 | * @throws Exception
376 | * @throws \yii\base\InvalidConfigException
377 | */
378 | public function aliasExists($alias)
379 | {
380 | $indexes = $this->getIndexesByAlias($alias);
381 |
382 | return !empty($indexes);
383 | }
384 |
385 | /**
386 | * @return array
387 | * @throws Exception
388 | * @throws \yii\base\InvalidConfigException
389 | * @see https://www.elastic.co/guide/en/elasticsearch/reference/2.0/indices-aliases.html#alias-retrieving
390 | */
391 | public function getAliasInfo()
392 | {
393 | $aliasInfo = $this->db->get(['_alias', '*']);
394 | return $aliasInfo ?: [];
395 | }
396 |
397 | /**
398 | * @param string $alias
399 | *
400 | * @return array
401 | * @throws Exception
402 | * @throws \yii\base\InvalidConfigException
403 | * @see https://www.elastic.co/guide/en/elasticsearch/reference/2.0/indices-aliases.html#alias-retrieving
404 | */
405 | public function getIndexInfoByAlias($alias)
406 | {
407 | $responseData = $this->db->get(['_alias', $alias]);
408 | if (empty($responseData)) {
409 | return [];
410 | }
411 |
412 | return $responseData;
413 | }
414 |
415 | /**
416 | * @param string $alias
417 | *
418 | * @return array
419 | * @throws Exception
420 | * @throws \yii\base\InvalidConfigException
421 | */
422 | public function getIndexesByAlias($alias)
423 | {
424 | return array_keys($this->getIndexInfoByAlias($alias));
425 | }
426 |
427 | /**
428 | * @param string $index Index that the document belongs to.
429 | *
430 | * @return array
431 | * @throws Exception
432 | * @throws \yii\base\InvalidConfigException
433 | * @see https://www.elastic.co/guide/en/elasticsearch/reference/2.0/indices-aliases.html#alias-retrieving
434 | */
435 | public function getIndexAliases($index)
436 | {
437 | $responseData = $this->db->get([$index, '_alias', '*']);
438 | if (empty($responseData)) {
439 | return [];
440 | }
441 |
442 | return $responseData[$index]['aliases'];
443 | }
444 |
445 | /**
446 | * @param string $index Index that the document belongs to.
447 | * @param string $alias
448 | * @param array $aliasParameters
449 | *
450 | * @return bool
451 | * @throws Exception
452 | * @throws \yii\base\InvalidConfigException
453 | * @see https://www.elastic.co/guide/en/elasticsearch/reference/2.0/indices-aliases.html#alias-adding
454 | */
455 | public function addAlias($index, $alias, $aliasParameters = [])
456 | {
457 | return (bool)$this->db->put([$index, '_alias', $alias], [], json_encode((object)$aliasParameters));
458 | }
459 |
460 | /**
461 | * @param string $index Index that the document belongs to.
462 | * @param string $alias
463 | *
464 | * @return bool
465 | * @throws Exception
466 | * @throws \yii\base\InvalidConfigException
467 | * @see https://www.elastic.co/guide/en/elasticsearch/reference/2.0/indices-aliases.html#deleting
468 | */
469 | public function removeAlias($index, $alias)
470 | {
471 | return (bool)$this->db->delete([$index, '_alias', $alias]);
472 | }
473 |
474 | /**
475 | * Runs alias manipulations.
476 | * If you want to add alias1 to index1
477 | * and remove alias2 from index2 you can use following commands:
478 | * ~~~
479 | * $actions = [
480 | * ['add' => ['index' => 'index1', 'alias' => 'alias1']],
481 | * ['remove' => ['index' => 'index2', 'alias' => 'alias2']],
482 | * ];
483 | * ~~~
484 | * @param array $actions
485 | *
486 | * @return bool
487 | * @throws Exception
488 | * @throws \yii\base\InvalidConfigException
489 | * @see https://www.elastic.co/guide/en/elasticsearch/reference/2.0/indices-aliases.html#indices-aliases
490 | */
491 | public function aliasActions(array $actions)
492 | {
493 | return (bool)$this->db->post(['_aliases'], [], json_encode(['actions' => $actions]));
494 | }
495 |
496 | /**
497 | * Change specific index level settings in real time.
498 | * Note that update analyzers required to [[close()]] the index first and [[open()]] it after the changes are made,
499 | * use [[updateAnalyzers()]] for it.
500 | *
501 | * @param string $index Index that the document belongs to.
502 | * @param string|array $setting
503 | * @param array $options URL options
504 | * @return mixed
505 | * @throws Exception
506 | * @throws \yii\base\InvalidConfigException
507 | * @see https://www.elasticsearch.org/guide/en/elasticsearch/reference/current/indices-update-settings.html
508 | * @since 2.0.4
509 | */
510 | public function updateSettings($index, $setting, $options = [])
511 | {
512 | $body = $setting !== null ? (is_string($setting) ? $setting : Json::encode($setting)) : null;
513 | return $this->db->put([$index, '_settings'], $options, $body);
514 | }
515 |
516 | /**
517 | * Define new analyzers for the index.
518 | * For example if content analyzer hasn’t been defined on "myindex" yet
519 | * you can use the following commands to add it:
520 | *
521 | * ~~~
522 | * $setting = [
523 | * 'analysis' => [
524 | * 'analyzer' => [
525 | * 'ngram_analyzer_with_filter' => [
526 | * 'tokenizer' => 'ngram_tokenizer',
527 | * 'filter' => 'lowercase, snowball'
528 | * ],
529 | * ],
530 | * 'tokenizer' => [
531 | * 'ngram_tokenizer' => [
532 | * 'type' => 'nGram',
533 | * 'min_gram' => 3,
534 | * 'max_gram' => 10,
535 | * 'token_chars' => ['letter', 'digit', 'whitespace', 'punctuation', 'symbol']
536 | * ],
537 | * ],
538 | * ]
539 | * ];
540 | * $elasticQuery->createCommand()->updateAnalyzers('myindex', $setting);
541 | * ~~~
542 | *
543 | * @param string $index Index that the document belongs to.
544 | * @param string|array $setting
545 | * @param array $options URL options
546 | * @return mixed
547 | * @throws Exception
548 | * @throws \yii\base\InvalidConfigException
549 | * @see https://www.elastic.co/guide/en/elasticsearch/reference/current/indices-update-settings.html#update-settings-analysis
550 | * @since 2.0.4
551 | */
552 | public function updateAnalyzers($index, $setting, $options = [])
553 | {
554 | $this->closeIndex($index);
555 | $result = $this->updateSettings($index, $setting, $options);
556 | $this->openIndex($index);
557 | return $result;
558 | }
559 |
560 | // TODO https://www.elastic.co/guide/en/elasticsearch/reference/current/indices-get-settings.html
561 |
562 | // TODO https://www.elastic.co/guide/en/elasticsearch/reference/current/indices-warmers.html
563 |
564 | /**
565 | * @param string $index Index that the document belongs to.
566 | * @return mixed
567 | * @throws Exception
568 | * @throws \yii\base\InvalidConfigException
569 | * @see https://www.elastic.co/guide/en/elasticsearch/reference/current/indices-open-close.html
570 | */
571 | public function openIndex($index)
572 | {
573 | return $this->db->post([$index, '_open']);
574 | }
575 |
576 | /**
577 | * @param string $index Index that the document belongs to.
578 | * @return mixed
579 | * @throws Exception
580 | * @throws \yii\base\InvalidConfigException
581 | * @see https://www.elastic.co/guide/en/elasticsearch/reference/current/indices-open-close.html
582 | */
583 | public function closeIndex($index)
584 | {
585 | return $this->db->post([$index, '_close']);
586 | }
587 |
588 | /**
589 | * @param array $options URL options
590 | * @return mixed
591 | * @throws Exception
592 | * @throws \yii\base\InvalidConfigException
593 | * @see https://www.elastic.co/guide/en/elasticsearch/reference/current/search-request-scroll.html
594 | * @since 2.0.4
595 | */
596 | public function scroll($options = [])
597 | {
598 | $body = array_filter([
599 | 'scroll' => ArrayHelper::remove($options, 'scroll', null),
600 | 'scroll_id' => ArrayHelper::remove($options, 'scroll_id', null),
601 | ]);
602 | if (empty($body)) {
603 | $body = (object) [];
604 | }
605 |
606 | return $this->db->post(['_search', 'scroll'], $options, Json::encode($body));
607 | }
608 |
609 | /**
610 | * @param array $options URL options
611 | * @return mixed
612 | * @throws Exception
613 | * @throws \yii\base\InvalidConfigException
614 | * @see https://www.elastic.co/guide/en/elasticsearch/reference/current/search-request-scroll.html
615 | * @since 2.0.4
616 | */
617 | public function clearScroll($options = [])
618 | {
619 | $body = array_filter([
620 | 'scroll_id' => ArrayHelper::remove($options, 'scroll_id', null),
621 | ]);
622 | if (empty($body)) {
623 | $body = (object) [];
624 | }
625 |
626 | return $this->db->delete(['_search', 'scroll'], $options, Json::encode($body));
627 | }
628 |
629 | /**
630 | * @param string $index Index that the document belongs to.
631 | * @return mixed
632 | * @throws Exception
633 | * @throws \yii\base\InvalidConfigException
634 | * @see https://www.elastic.co/guide/en/elasticsearch/reference/current/indices-stats.html
635 | */
636 | public function getIndexStats($index = '_all')
637 | {
638 | return $this->db->get([$index, '_stats']);
639 | }
640 |
641 | /**
642 | * @param string $index Index that the document belongs to.
643 | * @return mixed
644 | * @throws Exception
645 | * @throws \yii\base\InvalidConfigException
646 | * @see https://www.elastic.co/guide/en/elasticsearch/reference/current/indices-recovery.html
647 | */
648 | public function getIndexRecoveryStats($index = '_all')
649 | {
650 | return $this->db->get([$index, '_recovery']);
651 | }
652 |
653 | // https://www.elastic.co/guide/en/elasticsearch/reference/current/indices-segments.html
654 |
655 | /**
656 | * @param string $index Index that the document belongs to.
657 | * @return mixed
658 | * @throws Exception
659 | * @throws \yii\base\InvalidConfigException
660 | * @see https://www.elastic.co/guide/en/elasticsearch/reference/current/indices-clearcache.html
661 | */
662 | public function clearIndexCache($index)
663 | {
664 | return $this->db->post([$index, '_cache', 'clear']);
665 | }
666 |
667 | /**
668 | * @param string $index Index that the document belongs to.
669 | * @return mixed
670 | * @throws Exception
671 | * @throws \yii\base\InvalidConfigException
672 | * @see https://www.elastic.co/guide/en/elasticsearch/reference/current/indices-flush.html
673 | */
674 | public function flushIndex($index = '_all')
675 | {
676 | return $this->db->post([$index, '_flush']);
677 | }
678 |
679 | /**
680 | * @param string $index Index that the document belongs to.
681 | * @return mixed
682 | * @throws Exception
683 | * @throws \yii\base\InvalidConfigException
684 | * @see https://www.elastic.co/guide/en/elasticsearch/reference/current/indices-refresh.html
685 | */
686 | public function refreshIndex($index)
687 | {
688 | return $this->db->post([$index, '_refresh']);
689 | }
690 |
691 | // TODO https://www.elastic.co/guide/en/elasticsearch/reference/current/indices-optimize.html
692 |
693 | // TODO https://www.elastic.co/guide/en/elasticsearch/reference/0.90/indices-gateway-snapshot.html
694 |
695 | /**
696 | * @param string $index Index that the document belongs to.
697 | * @param string|null $type Type that the document belongs to.
698 | * @param string|array $mapping
699 | * @param array $options URL options
700 | * @return mixed
701 | * @throws Exception
702 | * @throws \yii\base\InvalidConfigException
703 | * @see https://www.elastic.co/guide/en/elasticsearch/reference/current/indices-put-mapping.html
704 | */
705 | public function setMapping($index, $type, $mapping, $options = [])
706 | {
707 | $body = $mapping !== null ? (is_string($mapping) ? $mapping : Json::encode($mapping)) : null;
708 |
709 | if ($this->db->dslVersion >= 7) {
710 | $endpoint = [$index, '_mapping'];
711 | } else {
712 | $endpoint = [$index, '_mapping', $type];
713 | }
714 | return $this->db->put($endpoint, $options, $body);
715 | }
716 |
717 | /**
718 | * @param string $index Index that the document belongs to.
719 | * @param string|null $type Type that the document belongs to.
720 | * @return mixed
721 | * @throws Exception
722 | * @throws \yii\base\InvalidConfigException
723 | * @see https://www.elastic.co/guide/en/elasticsearch/reference/current/indices-get-mapping.html
724 | */
725 | public function getMapping($index = '_all', $type = null)
726 | {
727 | $url = [$index, '_mapping'];
728 | if ($this->db->dslVersion < 7 && $type !== null) {
729 | $url[] = $type;
730 | }
731 | return $this->db->get($url);
732 | }
733 |
734 | /**
735 | * @param string $index Index that the document belongs to.
736 | * @param string $type
737 | * @return mixed
738 | * @see https://www.elastic.co/guide/en/elasticsearch/reference/current/indices-get-field-mapping.html
739 | */
740 | // public function getFieldMapping($index, $type = '_all')
741 | // {
742 | // // TODO implement
743 | // return $this->db->put([$index, $type, '_mapping']);
744 | // }
745 |
746 | /**
747 | * @param $options
748 | * @param string $index Index that the document belongs to.
749 | * @return mixed
750 | * @see https://www.elastic.co/guide/en/elasticsearch/reference/current/indices-analyze.html
751 | */
752 | // public function analyze($options, $index = null)
753 | // {
754 | // // TODO implement
755 | //// return $this->db->put([$index]);
756 | // }
757 |
758 | /**
759 | * @param $name
760 | * @param $pattern
761 | * @param $settings
762 | * @param $mappings
763 | * @param int $order
764 | * @return mixed
765 | * @throws Exception
766 | * @throws \yii\base\InvalidConfigException
767 | * @see https://www.elastic.co/guide/en/elasticsearch/reference/current/indices-templates.html
768 | */
769 | public function createTemplate($name, $pattern, $settings, $mappings, $order = 0)
770 | {
771 | $body = Json::encode([
772 | 'template' => $pattern,
773 | 'order' => $order,
774 | 'settings' => (object) $settings,
775 | 'mappings' => (object) $mappings,
776 | ]);
777 |
778 | return $this->db->put(['_template', $name], [], $body);
779 |
780 | }
781 |
782 | /**
783 | * @param $name
784 | * @return mixed
785 | * @throws Exception
786 | * @throws \yii\base\InvalidConfigException
787 | * @see https://www.elastic.co/guide/en/elasticsearch/reference/current/indices-templates.html
788 | */
789 | public function deleteTemplate($name)
790 | {
791 | return $this->db->delete(['_template', $name]);
792 |
793 | }
794 |
795 | /**
796 | * @param $name
797 | * @return mixed
798 | * @throws Exception
799 | * @throws \yii\base\InvalidConfigException
800 | * @see https://www.elastic.co/guide/en/elasticsearch/reference/current/indices-templates.html
801 | */
802 | public function getTemplate($name)
803 | {
804 | return $this->db->get(['_template', $name]);
805 | }
806 | }
807 |
--------------------------------------------------------------------------------
/src/Connection.php:
--------------------------------------------------------------------------------
1 |
24 | * @since 2.0
25 | */
26 | class Connection extends Component
27 | {
28 | /**
29 | * @event Event an event that is triggered after a DB connection is established
30 | */
31 | const EVENT_AFTER_OPEN = 'afterOpen';
32 |
33 | /**
34 | * @var boolean whether to autodetect available cluster nodes on [[open()]]
35 | */
36 | public $autodetectCluster = true;
37 | /**
38 | * @var array The Elasticsearch cluster nodes to connect to.
39 | *
40 | * This is populated with the result of a cluster nodes request when [[autodetectCluster]] is true.
41 | *
42 | * Additional special options:
43 | *
44 | * - `auth`: overrides [[auth]] property. For example:
45 | *
46 | * ```php
47 | * [
48 | * 'http_address' => 'inet[/127.0.0.1:9200]',
49 | * 'auth' => ['username' => 'yiiuser', 'password' => 'yiipw'], // Overrides the `auth` property of the class with specific login and password
50 | * //'auth' => ['username' => 'yiiuser', 'password' => 'yiipw'], // Disabled auth regardless of `auth` property of the class
51 | * ]
52 | * ```
53 | *
54 | * - `protocol`: explicitly sets the protocol for the current node (useful when manually defining a HTTPS cluster)
55 | *
56 | * @see https://www.elastic.co/guide/en/elasticsearch/reference/current/cluster-nodes-info.html#cluster-nodes-info
57 | */
58 | public $nodes = [
59 | ['http_address' => 'inet[/127.0.0.1:9200]'],
60 | ];
61 | /**
62 | * @var string the active node. Key of one of the [[nodes]]. Will be randomly selected on [[open()]].
63 | */
64 | public $activeNode;
65 | /**
66 | * @var array Authentication data used to connect to the Elasticsearch node.
67 | *
68 | * Array elements:
69 | *
70 | * - `username`: the username for authentication.
71 | * - `password`: the password for authentication.
72 | *
73 | * Array either MUST contain both username and password on not contain any authentication credentials.
74 | * @see https://www.elastic.co/guide/en/elasticsearch/reference/current/security-api-authenticate.html
75 | */
76 | public $auth = [];
77 | /**
78 | * Elasticsearch has no knowledge of protocol used to access its nodes. Specifically, cluster autodetection request
79 | * returns node hosts and ports, but not the protocols to access them. Therefore we need to specify a default protocol here,
80 | * which can be overridden for specific nodes in the [[nodes]] property.
81 | * If [[autodetectCluster]] is true, all nodes received from cluster will be set to use the protocol defined by [[defaultProtocol]]
82 | * @var string Default protocol to connect to nodes
83 | * @since 2.0.5
84 | */
85 | public $defaultProtocol = 'http';
86 | /**
87 | * @var float timeout to use for connecting to an Elasticsearch node.
88 | * This value will be used to configure the curl `CURLOPT_CONNECTTIMEOUT` option.
89 | * If not set, no explicit timeout will be set for curl.
90 | */
91 | public $connectionTimeout = null;
92 | /**
93 | * @var float timeout to use when reading the response from an Elasticsearch node.
94 | * This value will be used to configure the curl `CURLOPT_TIMEOUT` option.
95 | * If not set, no explicit timeout will be set for curl.
96 | */
97 | public $dataTimeout = null;
98 | /**
99 | * @var array additional options used to configure curl session
100 | * @since 2.1.4
101 | */
102 | public $curlOptions = [];
103 | /**
104 | * @var integer version of the domain-specific language to use with the server.
105 | * This must be set to the major version of the Elasticsearch server in use, e.g. `5` for Elasticsearch 5.x.x,
106 | * `6` for Elasticsearch 6.x.x, and `7` for Elasticsearch 7.x.x.
107 | */
108 | public $dslVersion = 5;
109 |
110 | /**
111 | * @var resource the curl instance returned by [curl_init()](https://php.net/manual/en/function.curl-init.php).
112 | */
113 | private $_curl;
114 |
115 |
116 | public function init()
117 | {
118 | foreach ($this->nodes as &$node) {
119 | if (!isset($node['http_address'])) {
120 | throw new InvalidConfigException('Elasticsearch node needs at least a http_address configured.');
121 | }
122 | if (!isset($node['protocol'])) {
123 | $node['protocol'] = $this->defaultProtocol;
124 | }
125 | if (!in_array($node['protocol'], ['http', 'https'])) {
126 | throw new InvalidConfigException('Valid node protocol settings are "http" and "https".');
127 | }
128 | }
129 | }
130 |
131 | /**
132 | * Closes the connection when this component is being serialized.
133 | * @return array
134 | */
135 | public function __sleep()
136 | {
137 | $this->close();
138 |
139 | return array_keys(get_object_vars($this));
140 | }
141 |
142 | /**
143 | * Returns a value indicating whether the DB connection is established.
144 | * @return bool whether the DB connection is established
145 | */
146 | public function getIsActive()
147 | {
148 | return $this->activeNode !== null;
149 | }
150 |
151 | /**
152 | * Establishes a DB connection.
153 | * It does nothing if a DB connection has already been established.
154 | * @throws Exception if connection fails
155 | */
156 | public function open()
157 | {
158 | if ($this->activeNode !== null) {
159 | return;
160 | }
161 | if (empty($this->nodes)) {
162 | throw new InvalidConfigException('Elasticsearch needs at least one node to operate.');
163 | }
164 | $this->_curl = curl_init();
165 | if ($this->autodetectCluster) {
166 | $this->populateNodes();
167 | }
168 | $this->selectActiveNode();
169 | Yii::trace('Opening connection to Elasticsearch. Nodes in cluster: ' . count($this->nodes)
170 | . ', active node: ' . $this->nodes[$this->activeNode]['http_address'], __CLASS__);
171 | $this->initConnection();
172 | }
173 |
174 | /**
175 | * Populates [[nodes]] with the result of a cluster nodes request.
176 | * @throws Exception if no active node(s) found
177 | * @since 2.0.4
178 | */
179 | protected function populateNodes()
180 | {
181 | $node = reset($this->nodes);
182 | $host = $node['http_address'];
183 | $protocol = isset($node['protocol']) ? $node['protocol'] : $this->defaultProtocol;
184 | if (strncmp($host, 'inet[/', 6) === 0) {
185 | $host = substr($host, 6, -1);
186 | }
187 | $response = $this->httpRequest('GET', "$protocol://$host/_nodes/_all/http");
188 | if (!empty($response['nodes'])) {
189 | $nodes = $response['nodes'];
190 | } else {
191 | $nodes = [];
192 | }
193 |
194 | foreach ($nodes as $key => &$node) {
195 | // Make sure that nodes have an 'http_address' property, which is not the case if you're using AWS
196 | // Elasticsearch service (at least as of Oct., 2015). - TO BE VERIFIED
197 | // Temporary workaround - simply ignore all invalid nodes
198 | if (!isset($node['http']['publish_address'])) {
199 | unset($nodes[$key]);
200 | }
201 | $node['http_address'] = $node['http']['publish_address'];
202 |
203 | // Protocol is not a standard ES node property, so we add it manually
204 | $node['protocol'] = $this->defaultProtocol;
205 | }
206 |
207 | if (!empty($nodes)) {
208 | $this->nodes = array_values($nodes);
209 | } else {
210 | curl_close($this->_curl);
211 | throw new Exception('Cluster autodetection did not find any active node. Make sure a GET /_nodes reguest on the hosts defined in the config returns the "http_address" field for each node.');
212 | }
213 | }
214 |
215 | /**
216 | * select active node randomly
217 | */
218 | protected function selectActiveNode()
219 | {
220 | $keys = array_keys($this->nodes);
221 | $this->activeNode = $keys[random_int(0, count($keys) - 1)];
222 | }
223 |
224 | /**
225 | * Closes the currently active DB connection.
226 | * It does nothing if the connection is already closed.
227 | */
228 | public function close()
229 | {
230 | if ($this->activeNode === null) {
231 | return;
232 | }
233 | Yii::trace('Closing connection to Elasticsearch. Active node was: '
234 | . $this->nodes[$this->activeNode]['http']['publish_address'], __CLASS__);
235 | $this->activeNode = null;
236 | if ($this->_curl) {
237 | curl_close($this->_curl);
238 | $this->_curl = null;
239 | }
240 | }
241 |
242 | /**
243 | * Initializes the DB connection.
244 | * This method is invoked right after the DB connection is established.
245 | * The default implementation triggers an [[EVENT_AFTER_OPEN]] event.
246 | */
247 | protected function initConnection()
248 | {
249 | $this->trigger(self::EVENT_AFTER_OPEN);
250 | }
251 |
252 | /**
253 | * Returns the name of the DB driver for the current [[dsn]].
254 | * @return string name of the DB driver
255 | */
256 | public function getDriverName()
257 | {
258 | return 'elasticsearch';
259 | }
260 |
261 | /**
262 | * Creates a command for execution.
263 | * @param array $config the configuration for the Command class
264 | * @return Command the DB command
265 | */
266 | public function createCommand($config = [])
267 | {
268 | $this->open();
269 | $config['db'] = $this;
270 | $command = new Command($config);
271 |
272 | return $command;
273 | }
274 |
275 | /**
276 | * Creates a bulk command for execution.
277 | * @param array $config the configuration for the [[BulkCommand]] class
278 | * @return BulkCommand the DB command
279 | * @since 2.0.5
280 | */
281 | public function createBulkCommand($config = [])
282 | {
283 | $this->open();
284 | $config['db'] = $this;
285 | $command = new BulkCommand($config);
286 |
287 | return $command;
288 | }
289 |
290 | /**
291 | * Creates new query builder instance
292 | * @return QueryBuilder
293 | */
294 | public function getQueryBuilder()
295 | {
296 | return new QueryBuilder($this);
297 | }
298 |
299 | /**
300 | * Performs GET HTTP request
301 | *
302 | * @param string|array $url URL
303 | * @param array $options URL options
304 | * @param string $body request body
305 | * @param bool $raw if response body contains JSON and should be decoded
306 | * @return mixed response
307 | * @throws Exception
308 | * @throws InvalidConfigException
309 | */
310 | public function get($url, $options = [], $body = null, $raw = false)
311 | {
312 | $this->open();
313 | return $this->httpRequest('GET', $this->createUrl($url, $options), $body, $raw);
314 | }
315 |
316 | /**
317 | * Performs HEAD HTTP request
318 | *
319 | * @param string|array $url URL
320 | * @param array $options URL options
321 | * @param string $body request body
322 | * @return mixed response
323 | * @throws Exception
324 | * @throws InvalidConfigException
325 | */
326 | public function head($url, $options = [], $body = null)
327 | {
328 | $this->open();
329 | return $this->httpRequest('HEAD', $this->createUrl($url, $options), $body);
330 | }
331 |
332 | /**
333 | * Performs POST HTTP request
334 | *
335 | * @param string|array $url URL
336 | * @param array $options URL options
337 | * @param string $body request body
338 | * @param bool $raw if response body contains JSON and should be decoded
339 | * @return mixed response
340 | * @throws Exception
341 | * @throws InvalidConfigException
342 | */
343 | public function post($url, $options = [], $body = null, $raw = false)
344 | {
345 | $this->open();
346 | return $this->httpRequest('POST', $this->createUrl($url, $options), $body, $raw);
347 | }
348 |
349 | /**
350 | * Performs PUT HTTP request
351 | *
352 | * @param string|array $url URL
353 | * @param array $options URL options
354 | * @param string $body request body
355 | * @param bool $raw if response body contains JSON and should be decoded
356 | * @return mixed response
357 | * @throws Exception
358 | * @throws InvalidConfigException
359 | */
360 | public function put($url, $options = [], $body = null, $raw = false)
361 | {
362 | $this->open();
363 | return $this->httpRequest('PUT', $this->createUrl($url, $options), $body, $raw);
364 | }
365 |
366 | /**
367 | * Performs DELETE HTTP request
368 | *
369 | * @param string|array $url URL
370 | * @param array $options URL options
371 | * @param string $body request body
372 | * @param bool $raw if response body contains JSON and should be decoded
373 | * @return mixed response
374 | * @throws Exception
375 | * @throws InvalidConfigException
376 | */
377 | public function delete($url, $options = [], $body = null, $raw = false)
378 | {
379 | $this->open();
380 | return $this->httpRequest('DELETE', $this->createUrl($url, $options), $body, $raw);
381 | }
382 |
383 | /**
384 | * Creates URL
385 | *
386 | * @param string|array $path path
387 | * @param array $options URL options
388 | * @return array
389 | */
390 | private function createUrl($path, $options = [])
391 | {
392 | if (!is_string($path)) {
393 | $url = implode('/', array_map(function ($a) {
394 | return urlencode(is_array($a) ? implode(',', $a) : $a);
395 | }, $path));
396 | if (!empty($options)) {
397 | $url .= '?' . http_build_query($options);
398 | }
399 | } else {
400 | $url = $path;
401 | if (!empty($options)) {
402 | $url .= (strpos($url, '?') === false ? '?' : '&') . http_build_query($options);
403 | }
404 | }
405 |
406 | $node = $this->nodes[$this->activeNode];
407 | $protocol = isset($node['protocol']) ? $node['protocol'] : $this->defaultProtocol;
408 | $host = $node['http_address'];
409 |
410 | return [$protocol, $host, $url];
411 | }
412 |
413 | /**
414 | * Performs HTTP request
415 | *
416 | * @param string $method method name
417 | * @param string $url URL
418 | * @param string $requestBody request body
419 | * @param bool $raw if response body contains JSON and should be decoded
420 | * @return mixed if request failed
421 | * @throws Exception if request failed
422 | * @throws InvalidConfigException
423 | */
424 | protected function httpRequest($method, $url, $requestBody = null, $raw = false)
425 | {
426 | $method = strtoupper($method);
427 |
428 | // response body and headers
429 | $headers = [];
430 | $headersFinished = false;
431 | $body = '';
432 |
433 | $options = [
434 | CURLOPT_USERAGENT => 'Yii Framework ' . Yii::getVersion() . ' ' . __CLASS__,
435 | CURLOPT_RETURNTRANSFER => false,
436 | CURLOPT_HEADER => false,
437 | // https://www.php.net/manual/en/function.curl-setopt.php#82418
438 | CURLOPT_HTTPHEADER => [
439 | 'Expect:',
440 | 'Content-Type: application/json',
441 | ],
442 |
443 | CURLOPT_WRITEFUNCTION => function ($curl, $data) use (&$body) {
444 | $body .= $data;
445 | return mb_strlen($data, '8bit');
446 | },
447 | CURLOPT_HEADERFUNCTION => function ($curl, $data) use (&$headers, &$headersFinished) {
448 | if ($data === '') {
449 | $headersFinished = true;
450 | } elseif ($headersFinished) {
451 | $headersFinished = false;
452 | }
453 | if (!$headersFinished && ($pos = strpos($data, ':')) !== false) {
454 | $headers[strtolower(substr($data, 0, $pos))] = trim(substr($data, $pos + 1));
455 | }
456 | return mb_strlen($data, '8bit');
457 | },
458 | CURLOPT_CUSTOMREQUEST => $method,
459 | CURLOPT_FORBID_REUSE => false,
460 | ];
461 |
462 | foreach ($this->curlOptions as $key => $value) {
463 | $options[$key] = $value;
464 | }
465 |
466 | if (!empty($this->auth) || isset($this->nodes[$this->activeNode]['auth']) && $this->nodes[$this->activeNode]['auth'] !== false) {
467 | $auth = isset($this->nodes[$this->activeNode]['auth']) ? $this->nodes[$this->activeNode]['auth'] : $this->auth;
468 | if (empty($auth['username'])) {
469 | throw new InvalidConfigException('Username is required to use authentication');
470 | }
471 | if (empty($auth['password'])) {
472 | throw new InvalidConfigException('Password is required to use authentication');
473 | }
474 |
475 | $options[CURLOPT_HTTPAUTH] = CURLAUTH_BASIC;
476 | $options[CURLOPT_USERPWD] = $auth['username'] . ':' . $auth['password'];
477 | }
478 |
479 | if ($this->connectionTimeout !== null) {
480 | $options[CURLOPT_CONNECTTIMEOUT] = $this->connectionTimeout;
481 | }
482 | if ($this->dataTimeout !== null) {
483 | $options[CURLOPT_TIMEOUT] = $this->dataTimeout;
484 | }
485 | if ($requestBody !== null) {
486 | $options[CURLOPT_POSTFIELDS] = $requestBody;
487 | }
488 | if ($method == 'HEAD') {
489 | $options[CURLOPT_NOBODY] = true;
490 | unset($options[CURLOPT_WRITEFUNCTION]);
491 | } else {
492 | $options[CURLOPT_NOBODY] = false;
493 | }
494 |
495 | if (is_array($url)) {
496 | list($protocol, $host, $q) = $url;
497 | if (strncmp($host, 'inet[', 5) == 0) {
498 | $host = substr($host, 5, -1);
499 | if (($pos = strpos($host, '/')) !== false) {
500 | $host = substr($host, $pos + 1);
501 | }
502 | }
503 | $profile = "$method $q#$requestBody";
504 | $url = "$protocol://$host/$q";
505 | } else {
506 | $profile = false;
507 | }
508 |
509 | Yii::trace("Sending request to Elasticsearch node: $method $url\n$requestBody", __METHOD__);
510 | if ($profile !== false) {
511 | Yii::beginProfile($profile, __METHOD__);
512 | }
513 |
514 | $this->resetCurlHandle();
515 | curl_setopt($this->_curl, CURLOPT_URL, $url);
516 | curl_setopt_array($this->_curl, $options);
517 | if (curl_exec($this->_curl) === false) {
518 | throw new Exception('Elasticsearch request failed: ' . curl_errno($this->_curl) . ' - ' . curl_error($this->_curl), [
519 | 'requestMethod' => $method,
520 | 'requestUrl' => $url,
521 | 'requestBody' => $requestBody,
522 | 'responseHeaders' => $headers,
523 | 'responseBody' => $this->decodeErrorBody($body),
524 | ]);
525 | }
526 |
527 | $responseCode = curl_getinfo($this->_curl, CURLINFO_HTTP_CODE);
528 |
529 | if ($profile !== false) {
530 | Yii::endProfile($profile, __METHOD__);
531 | }
532 |
533 | if ($responseCode >= 200 && $responseCode < 300) {
534 | if ($method === 'HEAD') {
535 | return true;
536 | } else {
537 | if (isset($headers['content-length']) && ($len = mb_strlen($body, '8bit')) < $headers['content-length']) {
538 | throw new Exception("Incomplete data received from Elasticsearch: $len < {$headers['content-length']}", [
539 | 'requestMethod' => $method,
540 | 'requestUrl' => $url,
541 | 'requestBody' => $requestBody,
542 | 'responseCode' => $responseCode,
543 | 'responseHeaders' => $headers,
544 | 'responseBody' => $body,
545 | ]);
546 | }
547 | if (isset($headers['content-type'])) {
548 | if (!strncmp($headers['content-type'], 'application/json', 16)) {
549 | return $raw ? $body : Json::decode($body);
550 | }
551 | if (!strncmp($headers['content-type'], 'text/plain', 10)) {
552 | return $raw ? $body : array_filter(explode("\n", $body));
553 | }
554 | }
555 | throw new Exception('Unsupported data received from Elasticsearch: ' . $headers['content-type'], [
556 | 'requestMethod' => $method,
557 | 'requestUrl' => $url,
558 | 'requestBody' => $requestBody,
559 | 'responseCode' => $responseCode,
560 | 'responseHeaders' => $headers,
561 | 'responseBody' => $this->decodeErrorBody($body),
562 | ]);
563 | }
564 | } elseif ($responseCode == 404) {
565 | return false;
566 | } else {
567 | throw new Exception("Elasticsearch request failed with code $responseCode. Response body:\n{$body}", [
568 | 'requestMethod' => $method,
569 | 'requestUrl' => $url,
570 | 'requestBody' => $requestBody,
571 | 'responseCode' => $responseCode,
572 | 'responseHeaders' => $headers,
573 | 'responseBody' => $this->decodeErrorBody($body),
574 | ]);
575 | }
576 | }
577 |
578 | private function resetCurlHandle()
579 | {
580 | // these functions do not get reset by curl automatically
581 | static $unsetValues = [
582 | CURLOPT_HEADERFUNCTION => null,
583 | CURLOPT_WRITEFUNCTION => null,
584 | CURLOPT_READFUNCTION => null,
585 | CURLOPT_PROGRESSFUNCTION => null,
586 | CURLOPT_POSTFIELDS => null,
587 | ];
588 | curl_setopt_array($this->_curl, $unsetValues);
589 | if (function_exists('curl_reset')) { // since PHP 5.5.0
590 | curl_reset($this->_curl);
591 | }
592 | }
593 |
594 | /**
595 | * Try to decode error information if it is valid json, return it if not.
596 | * @param $body
597 | * @return mixed
598 | */
599 | protected function decodeErrorBody($body)
600 | {
601 | try {
602 | $decoded = Json::decode($body);
603 | if (isset($decoded['error']) && !is_array($decoded['error'])) {
604 | $decoded['error'] = preg_replace('/\b\w+?Exception\[/', "\\0\n ", $decoded['error']);
605 | }
606 | return $decoded;
607 | } catch(InvalidArgumentException $e) {
608 | return $body;
609 | }
610 | }
611 |
612 | public function getNodeInfo()
613 | {
614 | return $this->get([]);
615 | }
616 |
617 | public function getClusterState()
618 | {
619 | return $this->get(['_cluster', 'state']);
620 | }
621 | }
622 |
--------------------------------------------------------------------------------
/src/DebugAction.php:
--------------------------------------------------------------------------------
1 |
21 | * @since 2.0
22 | */
23 | class DebugAction extends Action
24 | {
25 | /**
26 | * @var string the connection id to use
27 | */
28 | public $db;
29 | /**
30 | * @var DebugPanel
31 | */
32 | public $panel;
33 | /**
34 | * @var \yii\debug\controllers\DefaultController
35 | */
36 | public $controller;
37 |
38 |
39 | public function run($logId, $tag)
40 | {
41 | $this->controller->loadData($tag);
42 |
43 | $timings = $this->panel->calculateTimings();
44 | ArrayHelper::multisort($timings, 3, SORT_DESC);
45 | if (!isset($timings[$logId])) {
46 | throw new HttpException(404, 'Log message not found.');
47 | }
48 | $message = $timings[$logId][1];
49 | if (($pos = mb_strpos($message, "#")) !== false) {
50 | $url = mb_substr($message, 0, $pos);
51 | $body = mb_substr($message, $pos + 1);
52 | } else {
53 | $url = $message;
54 | $body = null;
55 | }
56 | $method = mb_substr($url, 0, $pos = mb_strpos($url, ' '));
57 | $url = mb_substr($url, $pos + 1);
58 |
59 | $options = ['pretty' => 'true'];
60 |
61 | /* @var $db Connection */
62 | $db = \Yii::$app->get($this->db);
63 | $time = microtime(true);
64 | switch ($method) {
65 | case 'GET': $result = $db->get($url, $options, $body, true); break;
66 | case 'POST': $result = $db->post($url, $options, $body, true); break;
67 | case 'PUT': $result = $db->put($url, $options, $body, true); break;
68 | case 'DELETE': $result = $db->delete($url, $options, $body, true); break;
69 | case 'HEAD': $result = $db->head($url, $options, $body); break;
70 | default:
71 | throw new NotSupportedException("Request method '$method' is not supported by Elasticsearch.");
72 | }
73 | $time = microtime(true) - $time;
74 |
75 | if ($result === true) {
76 | $result = 'success';
77 | } elseif ($result === false) {
78 | $result = 'no success';
79 | }
80 |
81 | Yii::$app->response->format = Response::FORMAT_JSON;
82 |
83 | return [
84 | 'time' => sprintf('%.1f ms', $time * 1000),
85 | 'result' => $result,
86 | ];
87 | }
88 | }
89 |
--------------------------------------------------------------------------------
/src/DebugPanel.php:
--------------------------------------------------------------------------------
1 |
22 | * @since 2.0
23 | */
24 | class DebugPanel extends Panel
25 | {
26 | public $db = 'elasticsearch';
27 |
28 |
29 | public function init()
30 | {
31 | $this->actions['elasticsearch-query'] = [
32 | 'class' => 'yii\\elasticsearch\\DebugAction',
33 | 'panel' => $this,
34 | 'db' => $this->db,
35 | ];
36 | }
37 |
38 | /**
39 | * @inheritdoc
40 | */
41 | public function getName()
42 | {
43 | return 'Elasticsearch';
44 | }
45 |
46 | /**
47 | * @inheritdoc
48 | */
49 | public function getSummary()
50 | {
51 | $timings = $this->calculateTimings();
52 | $queryCount = count($timings);
53 | $queryTime = 0;
54 | foreach ($timings as $timing) {
55 | $queryTime += $timing[3];
56 | }
57 | $queryTime = number_format($queryTime * 1000) . ' ms';
58 | $url = $this->getUrl();
59 | $output = <<
61 |
62 | ES $queryCount $queryTime
63 |
64 |
65 | EOD;
66 |
67 | return $queryCount > 0 ? $output : '';
68 | }
69 |
70 | /**
71 | * @inheritdoc
72 | */
73 | public function getDetail()
74 | {
75 | //Register YiiAsset in order to inject csrf token in ajax requests
76 | YiiAsset::register(\Yii::$app->view);
77 |
78 | $timings = $this->calculateTimings();
79 | ArrayHelper::multisort($timings, 3, SORT_DESC);
80 | $rows = [];
81 | $i = 0;
82 | foreach ($timings as $logId => $timing) {
83 | $duration = sprintf('%.1f ms', $timing[3] * 1000);
84 | $message = $timing[1];
85 | $traces = $timing[4];
86 | if (($pos = mb_strpos($message, "#")) !== false) {
87 | $url = mb_substr($message, 0, $pos);
88 | $body = mb_substr($message, $pos + 1);
89 | } else {
90 | $url = $message;
91 | $body = null;
92 | }
93 | $traceString = '';
94 | if (!empty($traces)) {
95 | $traceString .= Html::ul($traces, [
96 | 'class' => 'trace',
97 | 'item' => function ($trace) {
98 | return "{$trace['file']}({$trace['line']})";
99 | },
100 | ]);
101 | }
102 | $ajaxUrl = Url::to(['elasticsearch-query', 'logId' => $logId, 'tag' => $this->tag]);
103 | \Yii::$app->view->registerJs(<<Error: ' + errorThrown + ' - ' + textStatus + '
' + jqXHR.responseText);
118 | },
119 | dataType: "json"
120 | });
121 |
122 | return false;
123 | });
124 | JS
125 | , View::POS_READY);
126 | $runLink = Html::a('run query', '#', ['id' => "elastic-link-$i"]) . '
';
127 | $rows[] = <<
129 | $duration |
130 | |
131 | $runLink |
132 |
133 | | |
134 | HTML;
135 | $i++;
136 | }
137 | $rows = implode("\n", $rows);
138 |
139 | return <<Elasticsearch Queries
141 |
142 |
143 |
144 |
145 | Time |
146 | Url / Query |
147 | Run Query on node |
148 |
149 |
150 |
151 | $rows
152 |
153 |
154 | HTML;
155 | }
156 |
157 | private $_timings;
158 |
159 | public function calculateTimings()
160 | {
161 | if ($this->_timings !== null) {
162 | return $this->_timings;
163 | }
164 | $messages = isset($this->data['messages']) ? $this->data['messages'] : [];
165 | $timings = [];
166 | $stack = [];
167 | foreach ($messages as $i => $log) {
168 | list($token, $level, $category, $timestamp) = $log;
169 | $log[5] = $i;
170 | if ($level == Logger::LEVEL_PROFILE_BEGIN) {
171 | $stack[] = $log;
172 | } elseif ($level == Logger::LEVEL_PROFILE_END) {
173 | if (($last = array_pop($stack)) !== null && $last[0] === $token) {
174 | $timings[$last[5]] = [count($stack), $token, $last[3], $timestamp - $last[3], $last[4]];
175 | }
176 | }
177 | }
178 |
179 | $now = microtime(true);
180 | while (($last = array_pop($stack)) !== null) {
181 | $delta = $now - $last[3];
182 | $timings[$last[5]] = [count($stack), $last[0], $last[2], $delta, $last[4]];
183 | }
184 | ksort($timings);
185 |
186 | return $this->_timings = $timings;
187 | }
188 |
189 | /**
190 | * @inheritdoc
191 | */
192 | public function save()
193 | {
194 | $target = $this->module->logTarget;
195 | $messages = $target->filterMessages($target->messages, Logger::LEVEL_PROFILE, ['yii\elasticsearch\Connection::httpRequest']);
196 |
197 | return ['messages' => $messages];
198 | }
199 | }
200 |
--------------------------------------------------------------------------------
/src/ElasticsearchTarget.php:
--------------------------------------------------------------------------------
1 |
23 | * @since 2.0.5
24 | */
25 | class ElasticsearchTarget extends Target
26 | {
27 | /**
28 | * @var string Elasticsearch index name.
29 | */
30 | public $index = 'yii';
31 | /**
32 | * @var string Elasticsearch type name.
33 | */
34 | public $type = 'log';
35 | /**
36 | * @var Connection|array|string the Elasticsearch connection object or the application component ID
37 | * of the Elasticsearch connection.
38 | */
39 | public $db = 'elasticsearch';
40 | /**
41 | * @var array $options URL options.
42 | */
43 | public $options = [];
44 | /**
45 | * @var boolean If true, context will be logged as a separate message after all other messages.
46 | */
47 | public $logContext = true;
48 | /**
49 | * @var boolean If true, context will be included in every message.
50 | * This is convenient if you log application errors and analyze them with tools like Kibana.
51 | */
52 | public $includeContext = false;
53 | /**
54 | * @var boolean If true, context message will cached once it's been created. Makes sense to use with [[includeContext]].
55 | */
56 | public $cacheContext = false;
57 |
58 | /**
59 | * @var string Context message cache (can be used multiple times if context is appended to every message)
60 | */
61 | protected $_contextMessage = null;
62 |
63 |
64 | /**
65 | * This method will initialize the [[elasticsearch]] property to make sure it refers to a valid Elasticsearch connection.
66 | * @throws InvalidConfigException if [[elasticsearch]] is invalid.
67 | */
68 | public function init()
69 | {
70 | parent::init();
71 | $this->db = Instance::ensure($this->db, Connection::className());
72 | }
73 |
74 | /**
75 | * @inheritdoc
76 | */
77 | public function export()
78 | {
79 | $messages = array_map([$this, 'prepareMessage'], $this->messages);
80 | $body = implode("\n", $messages) . "\n";
81 | if ($this->db->dslVersion >= 7) {
82 | $this->db->post([$this->index, '_bulk'], $this->options, $body);
83 | } else {
84 | $this->db->post([$this->index, $this->type, '_bulk'], $this->options, $body);
85 | }
86 | }
87 |
88 | /**
89 | * If [[includeContext]] property is false, returns context message normally.
90 | * If [[includeContext]] is true, returns an empty string (so that context message in [[collect]] is not generated),
91 | * expecting that context will be appended to every message in [[prepareMessage]].
92 | * @return array the context information
93 | */
94 | protected function getContextMessage()
95 | {
96 | if (null === $this->_contextMessage || !$this->cacheContext) {
97 | $this->_contextMessage = ArrayHelper::filter($GLOBALS, $this->logVars);
98 | }
99 |
100 | return $this->_contextMessage;
101 | }
102 |
103 | /**
104 | * Processes the given log messages.
105 | * This method will filter the given messages with [[levels]] and [[categories]].
106 | * And if requested, it will also export the filtering result to specific medium (e.g. email).
107 | * Depending on the [[includeContext]] attribute, a context message will be either created or ignored.
108 | * @param array $messages log messages to be processed. See [[Logger::messages]] for the structure
109 | * of each message.
110 | * @param bool $final whether this method is called at the end of the current application
111 | */
112 | public function collect($messages, $final)
113 | {
114 | $this->messages = array_merge($this->messages, static::filterMessages($messages, $this->getLevels(), $this->categories, $this->except));
115 | $count = count($this->messages);
116 | if ($count > 0 && ($final || $this->exportInterval > 0 && $count >= $this->exportInterval)) {
117 | if (!$this->includeContext && $this->logContext) {
118 | $context = $this->getContextMessage();
119 | if (!empty($context)) {
120 | $this->messages[] = [$context, Logger::LEVEL_INFO, 'application', YII_BEGIN_TIME];
121 | }
122 | }
123 |
124 | // set exportInterval to 0 to avoid triggering export again while exporting
125 | $oldExportInterval = $this->exportInterval;
126 | $this->exportInterval = 0;
127 | $this->export();
128 | $this->exportInterval = $oldExportInterval;
129 |
130 | $this->messages = [];
131 | }
132 | }
133 |
134 | /**
135 | * Prepares a log message.
136 | * @param array $message The log message to be formatted.
137 | * @return string
138 | */
139 | public function prepareMessage($message)
140 | {
141 | list($text, $level, $category, $timestamp) = $message;
142 |
143 | $result = [
144 | 'category' => $category,
145 | 'level' => Logger::getLevelName($level),
146 | '@timestamp' => date('c', $timestamp),
147 | ];
148 |
149 | if (isset($message[4])) {
150 | $result['trace'] = $message[4];
151 | }
152 |
153 | if (!is_string($text)) {
154 | // exceptions may not be serializable if in the call stack somewhere is a Closure
155 | if ($text instanceof \Throwable || $text instanceof \Exception) {
156 | $text = (string) $text;
157 | } else {
158 | $text = VarDumper::export($text);
159 | }
160 | }
161 | $result['message'] = $text;
162 |
163 | if ($this->includeContext) {
164 | $result['context'] = $this->getContextMessage();
165 | }
166 |
167 | $message = implode("\n", [
168 | Json::encode([
169 | 'index' => new \stdClass()
170 | ]),
171 | Json::encode($result)
172 | ]);
173 |
174 | return $message;
175 | }
176 | }
177 |
--------------------------------------------------------------------------------
/src/Exception.php:
--------------------------------------------------------------------------------
1 |
14 | * @since 2.0
15 | */
16 | class Exception extends \yii\db\Exception
17 | {
18 | /**
19 | * @return string the user-friendly name of this exception
20 | */
21 | public function getName()
22 | {
23 | return 'Elasticsearch Database Exception';
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/src/Query.php:
--------------------------------------------------------------------------------
1 | storedFields('id, name')
30 | * ->from('myindex', 'users')
31 | * ->limit(10);
32 | * // build and execute the query
33 | * $command = $query->createCommand();
34 | * $rows = $command->search(); // this way you get the raw output of Elasticsearch.
35 | * ~~~
36 | *
37 | * You would normally call `$query->search()` instead of creating a command as
38 | * this method adds the `indexBy()` feature and also removes some
39 | * inconsistencies from the response.
40 | *
41 | * Query also provides some methods to easier get some parts of the result only:
42 | *
43 | * - [[one()]]: returns a single record populated with the first row of data.
44 | * - [[all()]]: returns all records based on the query results.
45 | * - [[count()]]: returns the number of records.
46 | * - [[scalar()]]: returns the value of the first column in the first row of the query result.
47 | * - [[column()]]: returns the value of the first column in the query result.
48 | * - [[exists()]]: returns a value indicating whether the query result has data or not.
49 | *
50 | * NOTE: Elasticsearch limits the number of records returned to 10 records by
51 | * default. If you expect to get more records you should specify limit
52 | * explicitly.
53 | *
54 | * @author Carsten Brandt
55 | * @since 2.0
56 | */
57 | class Query extends Component implements QueryInterface
58 | {
59 | use QueryTrait;
60 |
61 | /**
62 | * @var array the fields being retrieved from the documents. For example,
63 | * `['id', 'name']`. If not set, this option will not be applied to the
64 | * query and no fields will be returned. In this case the `_source` field
65 | * will be returned by default which can be configured using [[source]].
66 | * Setting this to an empty array will result in no fields being retrieved,
67 | * which means that only the primaryKey of a record will be available in
68 | * the result.
69 | * > Note: Field values are [always returned as arrays] even if they only
70 | * > have one value.
71 | *
72 | * [always returned as arrays]: https://www.elastic.co/guide/en/elasticsearch/reference/current/array.html
73 | *
74 | * @see https://www.elastic.co/guide/en/elasticsearch/reference/current/search-request-stored-fields.html
75 | * @see storedFields()
76 | * @see source
77 | */
78 | public $storedFields;
79 | /**
80 | * @var array the scripted fields being retrieved from the documents.
81 | * Example:
82 | * ```php
83 | * $query->scriptFields = [
84 | * 'value_times_two' => [
85 | * 'script' => "doc['my_field_name'].value * 2",
86 | * ],
87 | * 'value_times_factor' => [
88 | * 'script' => "doc['my_field_name'].value * factor",
89 | * 'params' => [
90 | * 'factor' => 2.0
91 | * ],
92 | * ],
93 | * ]
94 | * ```
95 | *
96 | * > Note: Field values are [always returned as arrays] even if they only have one value.
97 | *
98 | * [always returned as arrays]: https://www.elastic.co/guide/en/elasticsearch/reference/current/array.html
99 | * [script field]: https://www.elastic.co/guide/en/elasticsearch/reference/current/search-request-script-fields.html
100 | *
101 | * @see https://www.elastic.co/guide/en/elasticsearch/reference/current/search-request-script-fields.html
102 | * @see scriptFields()
103 | * @see source
104 | */
105 | public $scriptFields;
106 | /**
107 | * @var array An array of runtime fields evaluated at query time
108 | * Example:
109 | * ```php
110 | * $query->$runtimeMappings = [
111 | * 'value_times_two' => [
112 | * 'type' => 'double',
113 | * 'script' => "emit(doc['my_field_name'].value * 2)",
114 | * ],
115 | * 'value_times_factor' => [
116 | * 'type' => 'double',
117 | * 'script' => "emit(doc['my_field_name'].value * factor)",
118 | * 'params' => [
119 | * 'factor' => 2.0
120 | * ],
121 | * ],
122 | * ]
123 | * ```
124 | *
125 | * @see https://www.elastic.co/guide/en/elasticsearch/reference/current/runtime-mapping-fields.html
126 | * @see runtimeMappings()
127 | * @see source
128 | */
129 | public $runtimeMappings;
130 | /**
131 | * @var array Use the fields parameter to retrieve the values of runtime fields. Runtime fields won’t display in
132 | * _source, but the fields API works for all fields, even those that were not sent as part of the original _source.
133 | * @see https://www.elastic.co/guide/en/elasticsearch/reference/current/runtime-retrieving-fields.html
134 | * @see fields()
135 | * @see fields
136 | */
137 | public $fields;
138 | /**
139 | * @var array this option controls how the `_source` field is returned from
140 | * the documents. For example, `['id', 'name']` means that only the `id`
141 | * and `name` field should be returned from `_source`. If not set, it
142 | * means retrieving the full `_source` field unless [[fields]] are
143 | * specified. Setting this option to `false` will disable return of the
144 | * `_source` field, this means that only the primaryKey of a record will be
145 | * available in the result.
146 | * @see https://www.elastic.co/guide/en/elasticsearch/reference/current/search-request-source-filtering.html
147 | * @see source()
148 | * @see fields
149 | */
150 | public $source;
151 | /**
152 | * @var string|array The index to retrieve data from. This can be a string
153 | * representing a single index or a an array of multiple indexes. If this
154 | * is not set, indexes are being queried.
155 | * @see from()
156 | */
157 | public $index;
158 | /**
159 | * @var string|array The type to retrieve data from. This can be a string
160 | * representing a single type or a an array of multiple types. If this is
161 | * not set, all types are being queried.
162 | * @see from()
163 | */
164 | public $type;
165 | /**
166 | * @var integer A search timeout, bounding the search request to be
167 | * executed within the specified time value and bail with the hits
168 | * accumulated up to that point when expired. Defaults to no timeout.
169 | * @see timeout()
170 | * @see https://www.elastic.co/guide/en/elasticsearch/reference/current/search.html#global-search-timeout
171 | */
172 | public $timeout;
173 | /**
174 | * @var array|string The query part of this search query. This is an array
175 | * or json string that follows the format of the elasticsearch
176 | * [Query DSL](https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl.html).
177 | */
178 | public $query;
179 | /**
180 | * @var array|string The filter part of this search query. This is an array
181 | * or json string that follows the format of the elasticsearch
182 | * [Query DSL](https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl.html).
183 | */
184 | public $filter;
185 | /**
186 | * @var string|array The `post_filter` part of the search query for
187 | * differentially filter search results and aggregations.
188 | * @see https://www.elastic.co/guide/en/elasticsearch/guide/current/_post_filter.html
189 | * @since 2.0.5
190 | */
191 | public $postFilter;
192 | /**
193 | * @var array The highlight part of this search query. This is an array that allows to highlight search results
194 | * on one or more fields.
195 | * @see https://www.elastic.co/guide/en/elasticsearch/reference/current/search-request-highlighting.html
196 | */
197 | public $highlight;
198 | /**
199 | * @var array List of aggregations to add to this query.
200 | * @see https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations.html
201 | */
202 | public $aggregations = [];
203 | /**
204 | * @var array the 'stats' part of the query. An array of groups to maintain
205 | * a statistics aggregation for.
206 | * @see https://www.elastic.co/guide/en/elasticsearch/reference/current/search.html#stats-groups
207 | */
208 | public $stats = [];
209 | /**
210 | * @var array list of suggesters to add to this query.
211 | * @see https://www.elastic.co/guide/en/elasticsearch/reference/current/search-suggesters.html
212 | */
213 | public $suggest = [];
214 | /**
215 | * @var array list of collapse to add to this query.
216 | * @see https://www.elastic.co/guide/en/elasticsearch/reference/current/search-suggesters.html
217 | * @since 2.1.0
218 | */
219 | public $collapse = [];
220 | /**
221 | * @var float Exclude documents which have a _score less than the minimum
222 | * specified in min_score
223 | * @see https://www.elastic.co/guide/en/elasticsearch/reference/current/search-request-min-score.html
224 | * @since 2.0.4
225 | */
226 | public $minScore;
227 | /**
228 | * @var array list of options that will passed to commands created by this query.
229 | * @see Command::$options
230 | * @since 2.0.4
231 | */
232 | public $options = [];
233 | /**
234 | * @var bool Enables explanation for each hit on how its score was computed.
235 | * @see https://www.elastic.co/guide/en/elasticsearch/reference/current/search-request-explain.html
236 | * @since 2.0.5
237 | */
238 | public $explain;
239 | /**
240 | * @var array Allows pagination of large datasets.
241 | * @see https://www.elastic.co/guide/en/elasticsearch/reference/current/paginate-search-results.html#search-after
242 | * @since 2.1.5
243 | */
244 | public $search_after = [];
245 |
246 |
247 | /**
248 | * @inheritdoc
249 | */
250 | public function init()
251 | {
252 | parent::init();
253 | // setting the default limit according to Elasticsearch defaults
254 | // https://www.elastic.co/guide/en/elasticsearch/reference/current/search-request-body.html#_parameters_5
255 | if ($this->limit === null) {
256 | $this->limit = 10;
257 | }
258 | }
259 |
260 | /**
261 | * Creates a DB command that can be used to execute this query.
262 | * @param Connection $db the database connection used to execute the query.
263 | * If this parameter is not given, the `elasticsearch` application
264 | * component will be used.
265 | * @return Command the created DB command instance.
266 | */
267 | public function createCommand($db = null)
268 | {
269 | if ($db === null) {
270 | $db = Yii::$app->get('elasticsearch');
271 | }
272 | $commandConfig = $db->getQueryBuilder()->build($this);
273 | return $db->createCommand($commandConfig);
274 | }
275 |
276 | /**
277 | * Executes the query and returns all results as an array.
278 | * @param Connection $db the database connection used to execute the query.
279 | * If this parameter is not given, the `elasticsearch` application component will be used.
280 | * @return array the query results. If the query results in nothing, an empty array will be returned.
281 | */
282 | public function all($db = null)
283 | {
284 | if ($this->emulateExecution) {
285 | return [];
286 | }
287 | $result = $this->createCommand($db)->search();
288 | if ($result === false) {
289 | throw new Exception('Elasticsearch search query failed.');
290 | }
291 | if (empty($result['hits']['hits'])) {
292 | return [];
293 | }
294 | $rows = $result['hits']['hits'];
295 | return $this->populate($rows);
296 | }
297 |
298 | /**
299 | * Converts the raw query results into the format as specified by this
300 | * query. This method is internally used to convert the data fetched from
301 | * database into the format as required by this query.
302 | * @param array $rows the raw query result from database
303 | * @return array the converted query result
304 | * @since 2.0.4
305 | */
306 | public function populate($rows)
307 | {
308 | if ($this->indexBy === null) {
309 | return $rows;
310 | }
311 | $models = [];
312 | foreach ($rows as $key => $row) {
313 | if ($this->indexBy !== null) {
314 | if (is_string($this->indexBy)) {
315 | $key = isset($row['fields'][$this->indexBy]) ?
316 | reset($row['fields'][$this->indexBy]) : $row['_source'][$this->indexBy];
317 | } else {
318 | $key = call_user_func($this->indexBy, $row);
319 | }
320 | }
321 | $models[$key] = $row;
322 | }
323 | return $models;
324 | }
325 |
326 | /**
327 | * Executes the query and returns a single row of result.
328 | * @param Connection $db the database connection used to execute the query.
329 | * If this parameter is not given, the `elasticsearch` application
330 | * component will be used.
331 | * @return array|bool the first row (in terms of an array) of the query
332 | * result. False is returned if the query results in nothing.
333 | */
334 | public function one($db = null)
335 | {
336 | if ($this->emulateExecution) {
337 | return false;
338 | }
339 | $result = $this->createCommand($db)->search(['size' => 1]);
340 | if ($result === false) {
341 | throw new Exception('Elasticsearch search query failed.');
342 | }
343 | if (empty($result['hits']['hits'])) {
344 | return false;
345 | }
346 | $record = reset($result['hits']['hits']);
347 |
348 | return $record;
349 | }
350 |
351 | /**
352 | * Executes the query and returns the complete search result including e.g.
353 | * hits, aggregations, suggesters, totalCount.
354 | * @param Connection $db the database connection used to execute the query.
355 | * If this parameter is not given, the `elasticsearch` application
356 | * component will be used.
357 | * @param array $options The options given with this query. Possible
358 | * options are:
359 | *
360 | * - [routing](https://www.elastic.co/guide/en/elasticsearch/reference/current/search.html#search-routing)
361 | *
362 | * @return array the query results.
363 | */
364 | public function search($db = null, $options = [])
365 | {
366 | if ($this->emulateExecution) {
367 | return [
368 | 'hits' => [
369 | 'total' => 0,
370 | 'hits' => [],
371 | ],
372 | ];
373 | }
374 | $result = $this->createCommand($db)->search($options);
375 | if ($result === false) {
376 | throw new Exception('Elasticsearch search query failed.');
377 | }
378 | if (!empty($result['hits']['hits']) && $this->indexBy !== null) {
379 | $rows = [];
380 | foreach ($result['hits']['hits'] as $key => $row) {
381 | if (is_string($this->indexBy)) {
382 | $key = isset($row['fields'][$this->indexBy]) ?
383 | $row['fields'][$this->indexBy] : $row['_source'][$this->indexBy];
384 | } else {
385 | $key = call_user_func($this->indexBy, $row);
386 | }
387 | $rows[$key] = $row;
388 | }
389 | $result['hits']['hits'] = $rows;
390 | }
391 | return $result;
392 | }
393 |
394 | /**
395 | * Executes the query and deletes all matching documents.
396 | *
397 | * Everything except query and filter will be ignored.
398 | *
399 | * @param Connection $db the database connection used to execute the query.
400 | * If this parameter is not given, the `elasticsearch` application
401 | * component will be used.
402 | * @param array $options The options given with this query.
403 | * @return array the query results.
404 | */
405 | public function delete($db = null, $options = [])
406 | {
407 | if ($this->emulateExecution) {
408 | return [];
409 | }
410 | return $this->createCommand($db)->deleteByQuery($options);
411 | }
412 |
413 | /**
414 | * Returns the query result as a scalar value. The value returned will be
415 | * the specified field in the first document of the query results.
416 | * @param string $field name of the attribute to select
417 | * @param Connection $db the database connection used to execute the query.
418 | * If this parameter is not given, the `elasticsearch` application
419 | * component will be used.
420 | * @return string the value of the specified attribute in the first record
421 | * of the query result. Null is returned if the query result is empty or
422 | * the field does not exist.
423 | */
424 | public function scalar($field, $db = null)
425 | {
426 | if ($this->emulateExecution) {
427 | return null;
428 | }
429 | $record = self::one($db);
430 | if ($record !== false) {
431 | if ($field === '_id') {
432 | return $record['_id'];
433 | } elseif (isset($record['_source'][$field])) {
434 | return $record['_source'][$field];
435 | } elseif (isset($record['fields'][$field])) {
436 | return count($record['fields'][$field]) == 1 ? reset($record['fields'][$field]) : $record['fields'][$field];
437 | }
438 | }
439 | return null;
440 | }
441 |
442 | /**
443 | * Executes the query and returns the first column of the result.
444 | * @param string $field the field to query over
445 | * @param Connection $db the database connection used to execute the query.
446 | * If this parameter is not given, the `elasticsearch` application
447 | * component will be used.
448 | * @return array the first column of the query result. An empty array is
449 | * returned if the query results in nothing.
450 | */
451 | public function column($field, $db = null)
452 | {
453 | if ($this->emulateExecution) {
454 | return [];
455 | }
456 | $command = $this->createCommand($db);
457 | $command->queryParts['_source'] = [$field];
458 | $result = $command->search();
459 | if ($result === false) {
460 | throw new Exception('Elasticsearch search query failed.');
461 | }
462 | if (empty($result['hits']['hits'])) {
463 | return [];
464 | }
465 | $column = [];
466 | foreach ($result['hits']['hits'] as $row) {
467 | if (isset($row['fields'][$field])) {
468 | $column[] = $row['fields'][$field];
469 | } elseif (isset($row['_source'][$field])) {
470 | $column[] = $row['_source'][$field];
471 | } else {
472 | $column[] = null;
473 | }
474 | }
475 | return $column;
476 | }
477 |
478 | /**
479 | * Returns the number of records.
480 | * @param string $q the COUNT expression. This parameter is ignored by this implementation.
481 | * @param Connection $db the database connection used to execute the query.
482 | * If this parameter is not given, the `elasticsearch` application
483 | * component will be used.
484 | * @return int number of records
485 | */
486 | public function count($q = '*', $db = null)
487 | {
488 | if ($this->emulateExecution) {
489 | return 0;
490 | }
491 |
492 | $command = $this->createCommand($db);
493 |
494 | // performing a query with return size of 0, is equal to getting result stats such as count
495 | // https://www.elastic.co/guide/en/elasticsearch/reference/5.6/breaking_50_search_changes.html#_literal_search_type_literal
496 | $searchOptions = ['size' => 0];
497 |
498 | // Set track_total_hits to 'true' for ElasticSearch version 6 and up
499 | // https://www.elastic.co/guide/en/elasticsearch/reference/master/search-your-data.html#track-total-hits
500 | if ($command->db->dslVersion >= 6) {
501 | $searchOptions['track_total_hits'] = 'true';
502 | }
503 |
504 | $result = $command->search($searchOptions);
505 |
506 | // since ES7 totals are returned as array (with count and precision values)
507 | if (isset($result['hits']['total'])) {
508 | return is_array($result['hits']['total']) ? (int)$result['hits']['total']['value'] : (int)$result['hits']['total'];
509 | }
510 | return 0;
511 | }
512 |
513 | /**
514 | * Returns a value indicating whether the query result contains any row of
515 | * data.
516 | * @param Connection $db the database connection used to execute the query.
517 | * If this parameter is not given, the `elasticsearch` application
518 | * component will be used.
519 | * @return bool whether the query result contains any row of data.
520 | */
521 | public function exists($db = null)
522 | {
523 | return self::one($db) !== false;
524 | }
525 |
526 | /**
527 | * Adds a 'stats' part to the query.
528 | * @param array $groups an array of groups to maintain a statistics aggregation for.
529 | * @return $this the query object itself
530 | * @see https://www.elastic.co/guide/en/elasticsearch/reference/current/search.html#stats-groups
531 | */
532 | public function stats($groups)
533 | {
534 | $this->stats = $groups;
535 | return $this;
536 | }
537 |
538 | /**
539 | * Sets a highlight parameters to retrieve from the documents.
540 | * @param array $highlight array of parameters to highlight results.
541 | * @return $this the query object itself
542 | * @see https://www.elastic.co/guide/en/elasticsearch/reference/current/search-request-highlighting.html
543 | */
544 | public function highlight($highlight)
545 | {
546 | $this->highlight = $highlight;
547 | return $this;
548 | }
549 |
550 | /**
551 | * @deprecated since 2.0.5 use addAggragate() instead
552 | *
553 | * Adds an aggregation to this query.
554 | * @param string $name the name of the aggregation
555 | * @param string $type the aggregation type. e.g. `terms`, `range`,
556 | * `histogram`, ...
557 | * @param string|array $options the configuration options for this
558 | * aggregation. Can be an array or a json string.
559 | * @return $this the query object itself
560 | * @see https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations.html
561 | */
562 | public function addAggregation($name, $type, $options)
563 | {
564 | return $this->addAggregate($name, [$type => $options]);
565 | }
566 |
567 | /**
568 | * @deprecated since 2.0.5 use addAggregate() instead
569 | *
570 | * Adds an aggregation to this query.
571 | *
572 | * This is an alias for [[addAggregation]].
573 | *
574 | * @param string $name the name of the aggregation
575 | * @param string $type the aggregation type. e.g. `terms`, `range`, `histogram`...
576 | * @param string|array $options the configuration options for this
577 | * aggregation. Can be an array or a json string.
578 | * @return $this the query object itself
579 | * @see https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations.html
580 | */
581 | public function addAgg($name, $type, $options)
582 | {
583 | return $this->addAggregate($name, [$type => $options]);
584 | }
585 |
586 | /**
587 | * Adds an aggregation to this query. Supports nested aggregations.
588 | * @param string $name the name of the aggregation
589 | * @param string|array $options the configuration options for this
590 | * aggregation. Can be an array or a json string.
591 | * @return $this the query object itself
592 | * @see https://www.elastic.co/guide/en/elasticsearch/reference/2.3/search-aggregations.html
593 | */
594 | public function addAggregate($name, $options)
595 | {
596 | $this->aggregations[$name] = $options;
597 | return $this;
598 | }
599 |
600 | /**
601 | * Adds a suggester to this query.
602 | * @param string $name the name of the suggester
603 | * @param string|array $definition the configuration options for this
604 | * suggester. Can be an array or a json string.
605 | * @return $this the query object itself
606 | * @see https://www.elastic.co/guide/en/elasticsearch/reference/current/search-suggesters.html
607 | */
608 | public function addSuggester($name, $definition)
609 | {
610 | $this->suggest[$name] = $definition;
611 | return $this;
612 | }
613 |
614 | /**
615 | * Adds a collapse to this query.
616 | * @param array $collapse the configuration options for collapse.
617 | * @return $this the query object itself
618 | * @see https://www.elastic.co/guide/en/elasticsearch/reference/5.3/search-request-collapse.html#search-request-collapse
619 | * @since 2.1.0
620 | */
621 | public function addCollapse($collapse)
622 | {
623 | $this->collapse = $collapse;
624 | return $this;
625 | }
626 |
627 | // TODO add validate query https://www.elastic.co/guide/en/elasticsearch/reference/current/search-validate.html
628 |
629 | // TODO support multi query via static method https://www.elastic.co/guide/en/elasticsearch/reference/current/search-multi-search.html
630 |
631 | /**
632 | * Sets the query part of this search query.
633 | * @param string|array $query
634 | * @return $this the query object itself
635 | */
636 | public function query($query)
637 | {
638 | $this->query = $query;
639 | return $this;
640 | }
641 |
642 | /**
643 | * Starts a batch query.
644 | *
645 | * A batch query supports fetching data in batches, which can keep the
646 | * memory usage under a limit. This method will return a [[BatchQueryResult]]
647 | * object which implements the [[\Iterator]] interface and can be traversed
648 | * to retrieve the data in batches.
649 | *
650 | * For example,
651 | *
652 | * ```php
653 | * $query = (new Query)->from('user');
654 | * foreach ($query->batch() as $rows) {
655 | * // $rows is an array of 10 or fewer rows from user table
656 | * }
657 | * ```
658 | *
659 | * Batch size is determined by the `limit` setting (note that in scan mode
660 | * batch limit is per shard).
661 | *
662 | * @param string $scrollWindow how long Elasticsearch should keep the
663 | * search context alive, in
664 | * [time units](https://www.elastic.co/guide/en/elasticsearch/reference/current/common-options.html#time-units)
665 | * @param Connection $db the database connection. If not set, the
666 | * `elasticsearch` application component will be used.
667 | * @return BatchQueryResult the batch query result. It implements the
668 | * [[\Iterator]] interface and can be traversed to retrieve the data in
669 | * batches.
670 | * @since 2.0.4
671 | */
672 | public function batch($scrollWindow = '1m', $db = null)
673 | {
674 | return Yii::createObject([
675 | 'class' => BatchQueryResult::className(),
676 | 'query' => $this,
677 | 'scrollWindow' => $scrollWindow,
678 | 'db' => $db,
679 | 'each' => false,
680 | ]);
681 | }
682 |
683 | /**
684 | * Starts a batch query and retrieves data row by row.
685 | *
686 | * This method is similar to [[batch()]] except that in each iteration of
687 | * the result, only one row of data is returned. For example,
688 | *
689 | * ```php
690 | * $query = (new Query)->from('user');
691 | * foreach ($query->each() as $row) {
692 | * }
693 | * ```
694 | *
695 | * @param string $scrollWindow how long Elasticsearch should keep the
696 | * search context alive, in
697 | * [time units](https://www.elastic.co/guide/en/elasticsearch/reference/current/common-options.html#time-units)
698 | * @param Connection $db the database connection. If not set, the
699 | * `elasticsearch` application component will be used.
700 | * @return BatchQueryResult the batch query result. It implements the
701 | * [[\Iterator]] interface and can be traversed to retrieve the data in
702 | * batches.
703 | * @since 2.0.4
704 | */
705 | public function each($scrollWindow = '1m', $db = null)
706 | {
707 | return Yii::createObject([
708 | 'class' => BatchQueryResult::className(),
709 | 'query' => $this,
710 | 'scrollWindow' => $scrollWindow,
711 | 'db' => $db,
712 | 'each' => true,
713 | ]);
714 | }
715 |
716 | /**
717 | * Sets the index and type to retrieve documents from.
718 | * @param string|array $index The index to retrieve data from. This can be
719 | * a string representing a single index or a an array of multiple indexes.
720 | * If this is `null` it means that all indexes are being queried.
721 | * @param string|array $type The type to retrieve data from. This can be a
722 | * string representing a single type or a an array of multiple types. If
723 | * this is `null` it means that all types are being queried.
724 | * @return $this the query object itself
725 | * @see https://www.elastic.co/guide/en/elasticsearch/reference/current/search-search.html#search-multi-index-type
726 | */
727 | public function from($index, $type = null)
728 | {
729 | $this->index = $index;
730 | $this->type = $type;
731 | return $this;
732 | }
733 |
734 | /**
735 | * Sets the fields to retrieve from the documents.
736 | *
737 | * Quote from the Elasticsearch doc:
738 | * > The stored_fields parameter is about fields that are explicitly marked
739 | * > as stored in the mapping, which is off by default and generally not
740 | * > recommended. Use source filtering instead to select subsets of the
741 | * > original source document to be returned.
742 | *
743 | * @param array $fields the fields to be selected.
744 | * @return $this the query object itself
745 | * @see https://www.elastic.co/guide/en/elasticsearch/reference/current/search-request-stored-fields.html
746 | */
747 | public function storedFields($fields)
748 | {
749 | if (is_array($fields) || $fields === null) {
750 | $this->storedFields = $fields;
751 | } else {
752 | $this->storedFields = func_get_args();
753 | }
754 | return $this;
755 | }
756 |
757 | /**
758 | * Sets the script fields to retrieve from the documents.
759 | * @param array $fields the fields to be selected.
760 | * @return $this the query object itself
761 | * @see https://www.elastic.co/guide/en/elasticsearch/reference/current/search-request-script-fields.html
762 | */
763 | public function scriptFields($fields)
764 | {
765 | if (is_array($fields) || $fields === null) {
766 | $this->scriptFields = $fields;
767 | } else {
768 | $this->scriptFields = func_get_args();
769 | }
770 | return $this;
771 | }
772 |
773 | /**
774 | * Sets the runtime mappings for this query
775 | * @param $mappings
776 | * @return $this the query object itself
777 | * @see https://www.elastic.co/guide/en/elasticsearch/reference/current/runtime.html
778 | */
779 | public function runtimeMappings($mappings)
780 | {
781 | if (is_array($mappings) || $mappings === null) {
782 | $this->runtimeMappings = $mappings;
783 | } else {
784 | $this->runtimeMappings = func_get_args();
785 | }
786 | return $this;
787 | }
788 |
789 | /**
790 | * Sets the runtime fields to retrieve from the documents.
791 | * @param array $fields the fields to be selected.
792 | * @return $this the query object itself
793 | * @see https://www.elastic.co/guide/en/elasticsearch/reference/current/runtime-retrieving-fields.html
794 | */
795 | public function fields($fields)
796 | {
797 | if (is_array($fields) || $fields === null) {
798 | $this->fields = $fields;
799 | } else {
800 | $this->fields = func_get_args();
801 | }
802 | return $this;
803 | }
804 |
805 | /**
806 | * Sets the source filtering, specifying how the `_source` field of the
807 | * document should be returned.
808 | * @param array|string|null|false $source the source patterns to be selected.
809 | * @return $this the query object itself
810 | * @see https://www.elastic.co/guide/en/elasticsearch/reference/current/search-request-source-filtering.html
811 | */
812 | public function source($source)
813 | {
814 | if (is_array($source) || $source === null || $source === false) {
815 | $this->source = $source;
816 | } else {
817 | $this->source = func_get_args();
818 | }
819 | return $this;
820 | }
821 |
822 | /**
823 | * Sets the search timeout.
824 | * @param int $timeout A search timeout, bounding the search request to
825 | * be executed within the specified time value and bail with the hits
826 | * accumulated up to that point when expired. Defaults to no timeout.
827 | * @return $this the query object itself
828 | * @see https://www.elastic.co/guide/en/elasticsearch/reference/current/search-request-body.html#_parameters_5
829 | */
830 | public function timeout($timeout)
831 | {
832 | $this->timeout = $timeout;
833 | return $this;
834 | }
835 |
836 | /**
837 | * @param float $minScore Exclude documents which have a `_score` less than
838 | * the minimum specified minScore
839 | * @return $this the query object itself
840 | * @see https://www.elastic.co/guide/en/elasticsearch/reference/current/search-request-min-score.html
841 | * @since 2.0.4
842 | */
843 | public function minScore($minScore)
844 | {
845 | $this->minScore = $minScore;
846 | return $this;
847 | }
848 |
849 | /**
850 | * Sets the options to be passed to the command created by this query.
851 | * @param array $options the options to be set.
852 | * @return $this the query object itself
853 | * @throws InvalidArgumentException if $options is not an array
854 | * @see Command::$options
855 | * @since 2.0.4
856 | */
857 | public function options($options)
858 | {
859 | if (!is_array($options)) {
860 | throw new InvalidArgumentException('Array parameter expected, ' . gettype($options) . ' received.');
861 | }
862 |
863 | $this->options = $options;
864 | return $this;
865 | }
866 |
867 | /**
868 | * Adds more options, overwriting existing options.
869 | * @param array $options the options to be added.
870 | * @return $this the query object itself
871 | * @throws InvalidArgumentException if $options is not an array
872 | * @see options()
873 | * @since 2.0.4
874 | */
875 | public function addOptions($options)
876 | {
877 | if (!is_array($options)) {
878 | throw new InvalidArgumentException('Array parameter expected, ' . gettype($options) . ' received.');
879 | }
880 |
881 | $this->options = array_merge($this->options, $options);
882 | return $this;
883 | }
884 |
885 | /**
886 | * @inheritdoc
887 | */
888 | public function andWhere($condition)
889 | {
890 | if ($this->where === null) {
891 | $this->where = $condition;
892 | } else if (isset($this->where[0]) && $this->where[0] === 'and') {
893 | $this->where[] = $condition;
894 | } else {
895 | $this->where = ['and', $this->where, $condition];
896 | }
897 | return $this;
898 | }
899 |
900 | /**
901 | * @inheritdoc
902 | */
903 | public function orWhere($condition)
904 | {
905 | if ($this->where === null) {
906 | $this->where = $condition;
907 | } else if (isset($this->where[0]) && $this->where[0] === 'or') {
908 | $this->where[] = $condition;
909 | } else {
910 | $this->where = ['or', $this->where, $condition];
911 | }
912 | return $this;
913 | }
914 |
915 | /**
916 | * Set the `post_filter` part of the search query.
917 | * @param string|array $filter
918 | * @return $this the query object itself
919 | * @see $postFilter
920 | * @since 2.0.5
921 | */
922 | public function postFilter($filter)
923 | {
924 | $this->postFilter = $filter;
925 | return $this;
926 | }
927 |
928 | /**
929 | * Explain for how the score of each document was computer
930 | * @param $explain
931 | * @return $this
932 | * @see $explain
933 | * @since 2.0.5
934 | */
935 | public function explain($explain)
936 | {
937 | $this->explain = $explain;
938 | return $this;
939 | }
940 | }
941 |
--------------------------------------------------------------------------------
/src/QueryBuilder.php:
--------------------------------------------------------------------------------
1 |
19 | * @since 2.0
20 | */
21 | class QueryBuilder extends BaseObject
22 | {
23 | /**
24 | * @var Connection the database connection.
25 | */
26 | public $db;
27 |
28 |
29 | /**
30 | * Constructor.
31 | * @param Connection $connection the database connection.
32 | * @param array $config name-value pairs that will be used to initialize the object properties
33 | */
34 | public function __construct($connection, $config = [])
35 | {
36 | $this->db = $connection;
37 | parent::__construct($config);
38 | }
39 |
40 | /**
41 | * Generates query from a [[Query]] object.
42 | * @param Query $query the [[Query]] object from which the query will be generated
43 | * @return array the generated SQL statement (the first array element) and the corresponding
44 | * parameters to be bound to the SQL statement (the second array element).
45 | */
46 | public function build($query)
47 | {
48 | $parts = [];
49 |
50 | if ($query->storedFields !== null) {
51 | $parts['stored_fields'] = $query->storedFields;
52 | }
53 | if ($query->scriptFields !== null) {
54 | $parts['script_fields'] = $query->scriptFields;
55 | }
56 | if ($query->runtimeMappings !== null) {
57 | $parts['runtime_mappings'] = $query->runtimeMappings;
58 | }
59 | if ($query->fields !== null) {
60 | $parts['fields'] = $query->fields;
61 | }
62 |
63 | if ($query->source !== null) {
64 | $parts['_source'] = $query->source;
65 | }
66 | if ($query->limit !== null && $query->limit >= 0) {
67 | $parts['size'] = $query->limit;
68 | }
69 | if ($query->offset > 0) {
70 | $parts['from'] = (int)$query->offset;
71 | }
72 | if (isset($query->minScore)) {
73 | $parts['min_score'] = (float)$query->minScore;
74 | }
75 | if (isset($query->explain)) {
76 | $parts['explain'] = $query->explain;
77 | }
78 |
79 | // combine query with where
80 | $conditionals = [];
81 | $whereQuery = $this->buildQueryFromWhere($query->where);
82 | if ($whereQuery) {
83 | $conditionals[] = $whereQuery;
84 | }
85 | if ($query->query) {
86 | $conditionals[] = $query->query;
87 | }
88 | if (count($conditionals) === 2) {
89 | $parts['query'] = ['bool' => ['must' => $conditionals]];
90 | } elseif (count($conditionals) === 1) {
91 | $parts['query'] = reset($conditionals);
92 | }
93 |
94 | if (!empty($query->highlight)) {
95 | $parts['highlight'] = $query->highlight;
96 | }
97 | if (!empty($query->aggregations)) {
98 | $parts['aggregations'] = $query->aggregations;
99 | }
100 | if (!empty($query->stats)) {
101 | $parts['stats'] = $query->stats;
102 | }
103 | if (!empty($query->suggest)) {
104 | $parts['suggest'] = $query->suggest;
105 | }
106 | if (!empty($query->postFilter)) {
107 | $parts['post_filter'] = $query->postFilter;
108 | }
109 | if (!empty($query->collapse)) {
110 | $parts['collapse'] = $query->collapse;
111 | }
112 | if ($query->search_after) {
113 | $parts['search_after'] = $query->search_after;
114 | }
115 |
116 | $sort = $this->buildOrderBy($query->orderBy);
117 | if (!empty($sort)) {
118 | $parts['sort'] = $sort;
119 | }
120 |
121 | $options = $query->options;
122 | if ($query->timeout !== null) {
123 | $options['timeout'] = $query->timeout;
124 | }
125 |
126 | return [
127 | 'queryParts' => $parts,
128 | 'index' => $query->index,
129 | 'type' => $query->type,
130 | 'options' => $options,
131 | ];
132 | }
133 |
134 | /**
135 | * adds order by condition to the query
136 | */
137 | public function buildOrderBy($columns)
138 | {
139 | if (empty($columns)) {
140 | return [];
141 | }
142 | $orders = [];
143 | foreach ($columns as $name => $direction) {
144 | if (is_string($direction)) {
145 | $column = $direction;
146 | $direction = SORT_ASC;
147 | } else {
148 | $column = $name;
149 | }
150 | if ($this->db->dslVersion < 7) {
151 | if ($column == '_id') {
152 | $column = '_uid';
153 | }
154 | }
155 |
156 | // allow Elasticsearch extended syntax as described in https://www.elastic.co/guide/en/elasticsearch/guide/master/_sorting.html
157 | if (is_array($direction)) {
158 | $orders[] = [$column => $direction];
159 | } else {
160 | $orders[] = [$column => ($direction === SORT_DESC ? 'desc' : 'asc')];
161 | }
162 | }
163 |
164 | return $orders;
165 | }
166 |
167 | public function buildQueryFromWhere($condition) {
168 | $where = $this->buildCondition($condition);
169 | if ($where) {
170 | $query = [
171 | 'constant_score' => [
172 | 'filter' => $where,
173 | ],
174 | ];
175 | return $query;
176 | } else {
177 | return null;
178 | }
179 | }
180 |
181 | /**
182 | * Parses the condition specification and generates the corresponding SQL expression.
183 | *
184 | * @param string|array $condition the condition specification. Please refer to [[Query::where()]] on how to specify a condition.
185 | * @throws \yii\base\InvalidArgumentException if unknown operator is used in query
186 | * @throws \yii\base\NotSupportedException if string conditions are used in where
187 | * @return string the generated SQL expression
188 | */
189 | public function buildCondition($condition)
190 | {
191 | static $builders = [
192 | 'not' => 'buildNotCondition',
193 | 'and' => 'buildBoolCondition',
194 | 'or' => 'buildBoolCondition',
195 | 'between' => 'buildBetweenCondition',
196 | 'not between' => 'buildBetweenCondition',
197 | 'in' => 'buildInCondition',
198 | 'not in' => 'buildInCondition',
199 | 'like' => 'buildLikeCondition',
200 | 'not like' => 'buildLikeCondition',
201 | 'or like' => 'buildLikeCondition',
202 | 'or not like' => 'buildLikeCondition',
203 | 'lt' => 'buildHalfBoundedRangeCondition',
204 | '<' => 'buildHalfBoundedRangeCondition',
205 | 'lte' => 'buildHalfBoundedRangeCondition',
206 | '<=' => 'buildHalfBoundedRangeCondition',
207 | 'gt' => 'buildHalfBoundedRangeCondition',
208 | '>' => 'buildHalfBoundedRangeCondition',
209 | 'gte' => 'buildHalfBoundedRangeCondition',
210 | '>=' => 'buildHalfBoundedRangeCondition',
211 | 'match' => 'buildMatchCondition',
212 | 'match_phrase' => 'buildMatchCondition',
213 | ];
214 |
215 | if (empty($condition)) {
216 | return [];
217 | }
218 | if (!is_array($condition)) {
219 | throw new NotSupportedException('String conditions in where() are not supported by Elasticsearch.');
220 | }
221 | if (isset($condition[0])) { // operator format: operator, operand 1, operand 2, ...
222 | $operator = strtolower($condition[0]);
223 | if (isset($builders[$operator])) {
224 | $method = $builders[$operator];
225 | array_shift($condition);
226 |
227 | return $this->$method($operator, $condition);
228 | } else {
229 | throw new InvalidArgumentException('Found unknown operator in query: ' . $operator);
230 | }
231 | } else { // hash format: 'column1' => 'value1', 'column2' => 'value2', ...
232 |
233 | return $this->buildHashCondition($condition);
234 | }
235 | }
236 |
237 | private function buildHashCondition($condition)
238 | {
239 | $parts = $emptyFields = [];
240 | foreach ($condition as $attribute => $value) {
241 | if ($attribute == '_id') {
242 | if ($value === null) { // there is no null pk
243 | $parts[] = ['bool' => ['must_not' => [['match_all' => new \stdClass()]]]]; // this condition is equal to WHERE false
244 | } else {
245 | $parts[] = ['ids' => ['values' => is_array($value) ? $value : [$value]]];
246 | }
247 | } else {
248 | if (is_array($value)) { // IN condition
249 | $parts[] = ['terms' => [$attribute => $value]];
250 | } else {
251 | if ($value === null) {
252 | $emptyFields[] = [ 'exists' => [ 'field' => $attribute ] ];
253 | } else {
254 | $parts[] = ['term' => [$attribute => $value]];
255 | }
256 | }
257 | }
258 | }
259 |
260 | $query = [ 'must' => $parts ];
261 | if ($emptyFields) {
262 | $query['must_not'] = $emptyFields;
263 | }
264 | return [ 'bool' => $query ];
265 | }
266 |
267 | private function buildNotCondition($operator, $operands)
268 | {
269 | if (count($operands) != 1) {
270 | throw new InvalidArgumentException("Operator '$operator' requires exactly one operand.");
271 | }
272 |
273 | $operand = reset($operands);
274 | if (is_array($operand)) {
275 | $operand = $this->buildCondition($operand);
276 | }
277 |
278 | return [
279 | 'bool' => [
280 | 'must_not' => $operand,
281 | ],
282 | ];
283 | }
284 |
285 | private function buildBoolCondition($operator, $operands)
286 | {
287 | $parts = [];
288 | if ($operator === 'and') {
289 | $clause = 'must';
290 | } else if ($operator === 'or') {
291 | $clause = 'should';
292 | } else {
293 | throw new InvalidArgumentException("Operator should be 'or' or 'and'");
294 | }
295 |
296 | foreach ($operands as $operand) {
297 | if (is_array($operand)) {
298 | $operand = $this->buildCondition($operand);
299 | }
300 | if (!empty($operand)) {
301 | $parts[] = $operand;
302 | }
303 | }
304 | if ($parts) {
305 | return [
306 | 'bool' => [
307 | $clause => $parts,
308 | ]
309 | ];
310 | } else {
311 | return null;
312 | }
313 | }
314 |
315 | private function buildBetweenCondition($operator, $operands)
316 | {
317 | if (!isset($operands[0], $operands[1], $operands[2])) {
318 | throw new InvalidArgumentException("Operator '$operator' requires three operands.");
319 | }
320 |
321 | list($column, $value1, $value2) = $operands;
322 | if ($column === '_id') {
323 | throw new NotSupportedException('Between condition is not supported for the _id field.');
324 | }
325 | $filter = ['range' => [$column => ['gte' => $value1, 'lte' => $value2]]];
326 | if ($operator === 'not between') {
327 | $filter = ['bool' => ['must_not'=>$filter]];
328 | }
329 |
330 | return $filter;
331 | }
332 |
333 | private function buildInCondition($operator, $operands)
334 | {
335 | if (!isset($operands[0], $operands[1]) || !is_array($operands)) {
336 | throw new InvalidArgumentException("Operator '$operator' requires array of two operands: column and values");
337 | }
338 |
339 | list($column, $values) = $operands;
340 |
341 | $values = (array)$values;
342 |
343 | if (empty($values) || $column === []) {
344 | return $operator === 'in' ? ['bool' => ['must_not' => [['match_all' => new \stdClass()]]]] : []; // this condition is equal to WHERE false
345 | }
346 |
347 | if (is_array($column)) {
348 | if (count($column) > 1) {
349 | return $this->buildCompositeInCondition($operator, $column, $values);
350 | }
351 | $column = reset($column);
352 | }
353 | $canBeNull = false;
354 | foreach ($values as $i => $value) {
355 | if (is_array($value)) {
356 | $values[$i] = $value = isset($value[$column]) ? $value[$column] : null;
357 | }
358 | if ($value === null) {
359 | $canBeNull = true;
360 | unset($values[$i]);
361 | }
362 | }
363 | if ($column === '_id') {
364 | if (empty($values) && $canBeNull) { // there is no null pk
365 | $filter = ['bool' => ['must_not' => [['match_all' => new \stdClass()]]]]; // this condition is equal to WHERE false
366 | } else {
367 | $filter = ['ids' => ['values' => array_values($values)]];
368 | if ($canBeNull) {
369 | $filter = [
370 | 'bool' => [
371 | 'should' => [
372 | $filter,
373 | 'bool' => ['must_not' => ['exists' => ['field'=>$column]]],
374 | ],
375 | ],
376 | ];
377 | }
378 | }
379 | } else {
380 | if (empty($values) && $canBeNull) {
381 | $filter = [
382 | 'bool' => [
383 | 'must_not' => [
384 | 'exists' => [ 'field' => $column ],
385 | ]
386 | ]
387 | ];
388 | } else {
389 | $filter = [ 'terms' => [$column => array_values($values)] ];
390 | if ($canBeNull) {
391 | $filter = [
392 | 'bool' => [
393 | 'should' => [
394 | $filter,
395 | 'bool' => ['must_not' => ['exists' => ['field'=>$column]]],
396 | ],
397 | ],
398 | ];
399 | }
400 | }
401 | }
402 |
403 | if ($operator === 'not in') {
404 | $filter = [
405 | 'bool' => [
406 | 'must_not' => $filter,
407 | ],
408 | ];
409 | }
410 |
411 | return $filter;
412 | }
413 |
414 | /**
415 | * Builds a half-bounded range condition
416 | * (for "gt", ">", "gte", ">=", "lt", "<", "lte", "<=" operators)
417 | * @param string $operator
418 | * @param array $operands
419 | * @return array Filter expression
420 | */
421 | private function buildHalfBoundedRangeCondition($operator, $operands)
422 | {
423 | if (!isset($operands[0], $operands[1])) {
424 | throw new InvalidArgumentException("Operator '$operator' requires two operands.");
425 | }
426 |
427 | list($column, $value) = $operands;
428 | if ($this->db->dslVersion < 7) {
429 | if ($column === '_id') {
430 | $column = '_uid';
431 | }
432 | }
433 |
434 | $range_operator = null;
435 |
436 | if (in_array($operator, ['gte', '>='])) {
437 | $range_operator = 'gte';
438 | } elseif (in_array($operator, ['lte', '<='])) {
439 | $range_operator = 'lte';
440 | } elseif (in_array($operator, ['gt', '>'])) {
441 | $range_operator = 'gt';
442 | } elseif (in_array($operator, ['lt', '<'])) {
443 | $range_operator = 'lt';
444 | }
445 |
446 | if ($range_operator === null) {
447 | throw new InvalidArgumentException("Operator '$operator' is not implemented.");
448 | }
449 |
450 | $filter = [
451 | 'range' => [
452 | $column => [
453 | $range_operator => $value
454 | ]
455 | ]
456 | ];
457 |
458 | return $filter;
459 | }
460 |
461 | protected function buildCompositeInCondition($operator, $columns, $values)
462 | {
463 | throw new NotSupportedException('composite in is not supported by Elasticsearch.');
464 | }
465 |
466 | private function buildLikeCondition($operator, $operands)
467 | {
468 | throw new NotSupportedException('like conditions are not supported by Elasticsearch.');
469 | }
470 |
471 | private function buildMatchCondition($operator, $operands)
472 | {
473 | return [
474 | $operator => [ $operands[0] => $operands[1] ]
475 | ];
476 | }
477 | }
478 |
--------------------------------------------------------------------------------