').appendTo(this.$wrapper);
24 | this.$message.html("Starting… Don't close this page until the process is finished\n");
25 | this.updateProgress();
26 | this.run();
27 | },
28 |
29 | run: function () {
30 | this.requestReindex(this.sites[this.currentSite], this.indexedPosts, this.interval);
31 | },
32 |
33 | requestReindex: function (site, from, size) {
34 | $.post(ajaxurl, {
35 | action: 'es_reindex',
36 | site: site,
37 | from: from,
38 | size: size
39 | })
40 | .done(this.httpSuccess.bind(this))
41 | .fail(this.httpError.bind(this));
42 | },
43 |
44 | httpSuccess: function (data) {
45 | if (typeof data !== 'object') {
46 | this.$errors.append('
' + data + '
');
47 | return;
48 | }
49 |
50 | if (data.success == false) {
51 | this.$errors.append('
' + data.message + '
');
52 | return;
53 | }
54 |
55 | this.indexedPosts = data.indexed;
56 | this.totalPosts = data.total;
57 | this.$message.html('Indexed ' + this.indexedPosts + '/' + this.totalPosts + ' posts.')
58 | this.updateProgress();
59 |
60 | if (this.indexedPosts >= this.totalPosts) {
61 | // finised with current site.
62 | if (this.currentSite + 1 < this.sites.length) {
63 | this.currentSite++;
64 | this.indexedPosts = 0;
65 | this.totalPosts = 0;
66 | this.$message.append(' Finished. Switching blog.');
67 | } else {
68 | // no more sites to index
69 | this.$message.append(' Done.');
70 | this.isIndexing = false;
71 | $('.esi-reindex').removeClass('button-disabled');
72 | this.$progressBar.removeClass('active');
73 | return;
74 | }
75 | }
76 |
77 | this.run();
78 | },
79 |
80 | httpError: function (data) {
81 | this.$errors.append('
' + data + '
');
82 | },
83 |
84 | updateProgress: function () {
85 | var percent = 0;
86 | if (this.totalPosts) {
87 | percent = this.indexedPosts / this.totalPosts * 100;
88 | }
89 | this.$progressBar.width(percent + '%').html(Math.floor(percent) + '%');
90 | }
91 |
92 | };
93 |
94 | $('.esi-reindex').click(function (e) {
95 | e.preventDefault();
96 | var sites = $(e.target).attr('data-sites').split(',');
97 | indexer.start(sites, 500, '.esi-reindex-output');
98 | });
99 | });
100 |
--------------------------------------------------------------------------------
/assets/admin/style.css:
--------------------------------------------------------------------------------
1 |
2 | .esi-box {
3 | box-shadow: 0 1px 1px rgba(0, 0, 0, .04);
4 | border: 1px solid #e5e5e5;
5 | background: #fff;
6 | color: #555;
7 | margin: 16px 0;
8 | padding: 23px 10px 0;
9 | }
10 |
11 | .esi-status {
12 | font-size: 13px;
13 | line-height: 1.42857143;
14 | font-family: Menlo, Monaco, Consolas, "Courier New", monospace;
15 | background-color: #f7f7f9;
16 | border: 1px solid #e1e1e8;
17 | padding: 9px 14px;
18 | word-break: break-all;
19 | word-wrap: break-word;
20 | }
21 |
22 | .esi-settings textarea {
23 | width: 100%;
24 | max-width: 650px;
25 | }
26 |
27 | .esi-connection {
28 | margin: 10px 0;
29 | }
30 |
31 | .esi-connection td {
32 | padding: 0 10px 0 0;
33 | }
34 |
35 | .esi-connection-ok {
36 | color: #3c763d
37 | }
38 |
39 | .esi-connection-warning {
40 | color: #aa6708
41 | }
42 |
43 | .esi-connection-error {
44 | color: #ce4844
45 | }
46 |
47 | .esi-indexer-message {
48 | margin-bottom: 23px;
49 | }
50 |
51 | .esi-indexer-progress {
52 | height: 20px;
53 | margin-bottom: 12px;
54 | overflow: hidden;
55 | background-color: #f5f5f5;
56 | border-radius: 4px;
57 | -webkit-box-shadow: inset 0 1px 2px rgba(0, 0, 0, .1);
58 | box-shadow: inset 0 1px 2px rgba(0, 0, 0, .1);
59 | }
60 |
61 | .esi-indexer-progress-bar {
62 | background-color: #00a0d2;
63 | float: left;
64 | width: 0;
65 | height: 100%;
66 | font-size: 12px;
67 | line-height: 20px;
68 | color: #fff;
69 | text-align: center;
70 | -webkit-box-shadow: inset 0 -1px 0 rgba(0, 0, 0, .15);
71 | box-shadow: inset 0 -1px 0 rgba(0, 0, 0, .15);
72 | -webkit-transition: width .6s ease;
73 | -o-transition: width .6s ease;
74 | transition: width .6s ease;
75 | }
76 |
77 | .esi-indexer-progress-bar.active {
78 | background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent);
79 | background-image: -o-linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent);
80 | background-image: linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent);
81 | background-size: 40px 40px;
82 | -webkit-animation: progress-bar-stripes 2s linear infinite;
83 | -o-animation: progress-bar-stripes 2s linear infinite;
84 | animation: progress-bar-stripes 2s linear infinite;
85 | }
86 |
87 | @-webkit-keyframes progress-bar-stripes {
88 | from { background-position: 40px 0; }
89 | to { background-position: 0 0; }
90 | }
91 |
92 | @keyframes progress-bar-stripes {
93 | from { background-position: 40px 0; }
94 | to { background-position: 0 0; }
95 | }
96 |
97 | .esi-box-footer {
98 | margin-bottom: 23px;
99 | text-align: center;
100 | }
101 |
102 | .button-primary.esi-reindex {
103 | padding: 6px 18px;
104 | height: auto;
105 | outline: none;
106 | }
107 |
--------------------------------------------------------------------------------
/assets/profiler/style.css:
--------------------------------------------------------------------------------
1 |
2 | #esi-profiler {
3 | font-size: 8px;
4 | background: #fff;
5 | margin: 8px;
6 | }
7 |
8 | .wp-admin #esi-profiler {
9 | margin-left: 168px;
10 | }
11 |
12 | #esi-profiler table {
13 | width: 100%;
14 | margin-bottom: 20px;
15 | border-collapse: separate;
16 | border-spacing: 2px;
17 | }
18 |
19 | #esi-profiler {
20 | font-family: Menlo, Monaco, Consolas, "Courier New", monospace;
21 | }
22 |
23 | #esi-profiler td {
24 | padding: 2px 9px;
25 | font-size: 10px;
26 | line-height: 1.42857143;
27 | color: #333;
28 | word-break: break-all;
29 | word-wrap: break-word;
30 | background-color: #f5f5f5;
31 | text-align: left;
32 | vertical-align: top;
33 | }
34 |
35 | #esi-profiler th {
36 | text-align: left;
37 | font-size: 10px;
38 | font-weight: bold;
39 | }
40 |
41 | #esi-profiler .esi-time {
42 | word-break: normal;
43 | word-wrap: normal;
44 | }
45 |
46 | #esi-profiler .esi-totals {
47 | width: 100%;
48 | max-width: 500px;
49 | }
50 |
51 | #esi-profiler .esi-wpquery-args span,
52 | #esi-profiler .esi-elasticsearch-args span {
53 | display: block;
54 | white-space: pre;
55 | line-height: 1.1;
56 | }
57 |
58 | #esi-profiler .esi-wpquery-args {
59 | width: 38.2%;
60 | }
61 |
--------------------------------------------------------------------------------
/bootstrap.php:
--------------------------------------------------------------------------------
1 |
7 | *
8 | * For the full copyright and license information, please view the LICENSE
9 | * file that was distributed with this source code.
10 | */
11 |
12 | if (defined('DISABLE_ES') && DISABLE_ES) {
13 | return;
14 | }
15 |
16 | require_once ESI_PATH.'vendor/autoload.php';
17 | require_once ESI_PATH.'functions.php';
18 |
19 | Wallmander\ElasticsearchIndexer\Hooks::setup();
20 |
21 | do_action('esi_after_setup');
22 |
--------------------------------------------------------------------------------
/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "wallmanderco/elasticsearch-indexer",
3 | "description": "Elasticsearch indexer for WordPress and WooCommerce",
4 | "license": "GPLv2",
5 | "keywords": ["wordpress", "wordpress-plugin", "elasticsearch", "indexer", "search", "performance"],
6 | "authors": [
7 | {
8 | "name": "Mikael Mattsson",
9 | "email": "mikael@wallmanderco.se"
10 | }
11 | ],
12 | "require": {
13 | "php": ">=5.4.7",
14 | "elasticsearch/elasticsearch": "~1.0"
15 | },
16 | "require-dev": {
17 | "fabpot/php-cs-fixer": "2.0.*@dev"
18 | },
19 | "autoload": {
20 | "psr-4": {
21 | "Wallmander\\ElasticsearchIndexer\\": "src/"
22 | }
23 | },
24 | "extra": {
25 | "branch-alias": {
26 | "dev-master": "1.0-dev"
27 | }
28 | },
29 | "minimum-stability": "dev",
30 | "prefer-stable": true,
31 | "config": {
32 | "preferred-install": "dist"
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/config/defaults.php:
--------------------------------------------------------------------------------
1 |
7 | *
8 | * For the full copyright and license information, please view the LICENSE
9 | * file that was distributed with this source code.
10 | */
11 |
12 | use Wallmander\ElasticsearchIndexer\Model\Config;
13 |
14 | return [
15 | // System variables
16 | 'plugin_index_version' => 5, // Update this value to prompt the user to reindex
17 | 'user_index_version' => 1,
18 | 'is_indexing' => false,
19 |
20 | // User settings
21 | 'integration_level' => Config::INTEGRATION_LEVEL_FULL,
22 | 'hosts' => '127.0.0.1:9200',
23 | 'index_name' => null,
24 | 'shards' => 5,
25 | 'replicas' => 1,
26 | 'index_private_post_types' => true,
27 | 'profile_admin' => false,
28 | 'profile_frontend' => false,
29 | ];
30 |
--------------------------------------------------------------------------------
/config/mappings.php:
--------------------------------------------------------------------------------
1 |
7 | *
8 | * For the full copyright and license information, please view the LICENSE
9 | * file that was distributed with this source code.
10 | */
11 |
12 | /**
13 | * Full Credits to 10up/ElasticPress.
14 | */
15 | $dateTermFields = [
16 | //4 digit year (e.g. 2011)
17 | 'year' => [
18 | 'type' => 'integer',
19 | ],
20 | //Month number (from 1 to 12) alternate name "monthnum"
21 | 'month' => [
22 | 'type' => 'integer',
23 | ],
24 | //YearMonth (For e.g.: 201307)
25 | 'm' => [
26 | 'type' => 'integer',
27 | ],
28 | //Week of the year (from 0 to 53) alternate name "w"
29 | 'week' => [
30 | 'type' => 'integer',
31 | ],
32 | //Day of the month (from 1 to 31)
33 | 'day' => [
34 | 'type' => 'integer',
35 | ],
36 | //Accepts numbers 1-7 (1 is Sunday)
37 | 'dayofweek' => [
38 | 'type' => 'integer',
39 | ],
40 | //Accepts numbers 1-7 (1 is Monday)
41 | 'dayofweek_iso' => [
42 | 'type' => 'integer',
43 | ],
44 | //Accepts numbers 1-366
45 | 'dayofyear' => [
46 | 'type' => 'integer',
47 | ],
48 | //Hour (from 0 to 23)
49 | 'hour' => [
50 | 'type' => 'integer',
51 | ],
52 | //Minute (from 0 to 59)
53 | 'minute' => [
54 | 'type' => 'integer',
55 | ],
56 | //Second (0 to 59)
57 | 'second' => [
58 | 'type' => 'integer',
59 | ],
60 | ];
61 |
62 | return [
63 | 'post' => [
64 | 'date_detection' => false,
65 | 'dynamic_templates' => [
66 | [
67 | 'template_meta' => [
68 | 'path_match' => 'post_meta.*',
69 | 'mapping' => [
70 | 'type' => 'multi_field',
71 | 'path' => 'full',
72 | 'fields' => [
73 | '{name}' => [
74 | 'type' => 'string',
75 | 'index' => 'analyzed',
76 | 'analyzer' => 'esi_search_analyzer',
77 | ],
78 | 'raw' => [
79 | 'type' => 'string',
80 | 'index' => 'not_analyzed',
81 | 'ignore_above' => 256,
82 | ],
83 | ],
84 | ],
85 | ],
86 | ],
87 | [
88 | 'template_meta_num' => [
89 | 'path_match' => 'post_meta_num.*',
90 | 'mapping' => [
91 | 'type' => 'long',
92 | ],
93 | ],
94 | ],
95 | [
96 | 'template_terms' => [
97 | 'path_match' => 'terms.*',
98 | 'mapping' => [
99 | 'type' => 'object',
100 | 'path' => 'full',
101 | 'properties' => [
102 | 'name' => [
103 | 'type' => 'string',
104 | 'index' => 'analyzed',
105 | 'analyzer' => 'esi_search_analyzer',
106 | ],
107 | 'term_id' => [
108 | 'type' => 'long',
109 | ],
110 | 'parent' => [
111 | 'type' => 'long',
112 | ],
113 | 'slug' => [
114 | 'type' => 'string',
115 | 'index' => 'not_analyzed',
116 | ],
117 | 'all_slugs' => [
118 | 'type' => 'string',
119 | 'index' => 'not_analyzed',
120 | ],
121 | ],
122 | ],
123 | ],
124 | ],
125 | [
126 | 'term_suggest' => [
127 | 'path_match' => 'term_suggest_*',
128 | 'mapping' => [
129 | 'type' => 'completion',
130 | 'analyzer' => 'esi_search_analyzer',
131 | ],
132 | ],
133 | ],
134 | ],
135 | '_all' => [
136 | 'enabled' => false,
137 | ],
138 | 'properties' => [
139 | 'post_id' => [
140 | 'type' => 'long',
141 | 'index' => 'not_analyzed',
142 | ],
143 | 'post_author' => [
144 | 'type' => 'object',
145 | 'path' => 'full',
146 | 'fields' => [
147 | 'display_name' => [
148 | 'type' => 'string',
149 | 'analyzer' => 'standard',
150 | ],
151 | 'login' => [
152 | 'type' => 'string',
153 | 'analyzer' => 'standard',
154 | ],
155 | 'id' => [
156 | 'type' => 'long',
157 | 'index' => 'not_analyzed',
158 | ],
159 | 'raw' => [
160 | 'type' => 'string',
161 | 'index' => 'not_analyzed',
162 | ],
163 | ],
164 | ],
165 | 'post_date' => [
166 | 'type' => 'date',
167 | 'format' => 'YYYY-MM-dd HH:mm:ss',
168 | ],
169 | 'post_date_gmt' => [
170 | 'type' => 'date',
171 | 'format' => 'YYYY-MM-dd HH:mm:ss',
172 | ],
173 | 'post_title' => [
174 | 'type' => 'multi_field',
175 | 'fields' => [
176 | 'post_title' => [
177 | 'type' => 'string',
178 | 'analyzer' => 'esi_search_analyzer',
179 | 'store' => 'yes',
180 | ],
181 | 'raw' => [
182 | 'type' => 'string',
183 | 'index' => 'not_analyzed',
184 | ],
185 | ],
186 | ],
187 | 'post_excerpt' => [
188 | 'type' => 'string',
189 | ],
190 | 'post_content' => [
191 | 'type' => 'string',
192 | 'analyzer' => 'esi_search_analyzer',
193 | 'index' => 'analyzed',
194 | ],
195 | 'post_status' => [
196 | 'type' => 'string',
197 | 'index' => 'not_analyzed',
198 | ],
199 | 'post_name' => [
200 | 'type' => 'string',
201 | 'index' => 'not_analyzed',
202 | ],
203 | 'post_modified' => [
204 | 'type' => 'date',
205 | 'format' => 'YYYY-MM-dd HH:mm:ss',
206 | ],
207 | 'post_modified_gmt' => [
208 | 'type' => 'date',
209 | 'format' => 'YYYY-MM-dd HH:mm:ss',
210 | ],
211 | 'post_parent' => [
212 | 'type' => 'long',
213 | 'index' => 'not_analyzed',
214 | ],
215 | 'post_type' => [
216 | 'type' => 'string',
217 | 'index' => 'not_analyzed',
218 | ],
219 | 'post_mime_type' => [
220 | 'type' => 'string',
221 | 'index' => 'not_analyzed',
222 | ],
223 | 'permalink' => [
224 | 'type' => 'string',
225 | 'index' => 'not_analyzed',
226 | ],
227 | 'terms' => [
228 | 'type' => 'object',
229 | ],
230 | 'post_meta' => [
231 | 'type' => 'object',
232 | ],
233 | 'post_meta_num' => [
234 | 'type' => 'object',
235 | ],
236 | 'post_date_object' => [
237 | 'type' => 'object',
238 | 'path' => 'full',
239 | 'fields' => $dateTermFields,
240 | ],
241 | 'post_date_gmt_object' => [
242 | 'type' => 'object',
243 | 'path' => 'full',
244 | 'fields' => $dateTermFields,
245 | ],
246 | 'post_modified_object' => [
247 | 'type' => 'object',
248 | 'path' => 'full',
249 | 'fields' => $dateTermFields,
250 | ],
251 | 'post_modified_gmt_object' => [
252 | 'type' => 'object',
253 | 'path' => 'full',
254 | 'fields' => $dateTermFields,
255 | ],
256 | 'menu_order' => [
257 | 'type' => 'long',
258 | 'index' => 'not_analyzed',
259 | ],
260 | 'comment_count' => [
261 | 'type' => 'long',
262 | 'index' => 'not_analyzed',
263 | ],
264 | 'guid' => [
265 | 'type' => 'string',
266 | 'index' => 'not_analyzed',
267 | ],
268 | 'order_item_names' => [
269 | 'type' => 'string',
270 | 'index' => 'not_analyzed',
271 | ],
272 | ],
273 | ],
274 | ];
275 |
--------------------------------------------------------------------------------
/config/settings.php:
--------------------------------------------------------------------------------
1 |
7 | *
8 | * For the full copyright and license information, please view the LICENSE
9 | * file that was distributed with this source code.
10 | */
11 |
12 | use Wallmander\ElasticsearchIndexer\Model\Config;
13 |
14 | return [
15 | 'index' => [
16 | 'number_of_shards' => (int) Config::option('shards'),
17 | 'number_of_replicas' => (int) Config::option('replicas'),
18 | ],
19 | 'analysis' => [
20 | 'analyzer' => [
21 | 'esi_search_analyzer' => [
22 | 'type' => 'custom',
23 | 'tokenizer' => 'standard',
24 | 'filter' => ['standard', 'lowercase', 'stop', 'esi_ngram', 'esi_snowball'],
25 | 'language' => apply_filters('esi_analyzer_language', 'English'),
26 | ],
27 | 'esi_index_analyzer' => [
28 | 'type' => 'custom',
29 | 'tokenizer' => 'keyword',
30 | 'filter' => ['standard', 'lowercase'],
31 | ],
32 | 'esi_simple_analyzer' => [
33 | 'type' => 'custom',
34 | 'tokenizer' => 'standard',
35 | 'filter' => ['standard', 'lowercase', 'keyword_repeat', 'porter_stem'],
36 | ],
37 | ],
38 | 'filter' => [
39 | 'esi_ngram' => [
40 | 'type' => 'nGram',
41 | 'min_gram' => 3,
42 | 'max_gram' => 20,
43 | ],
44 | 'esi_snowball' => [
45 | 'type' => 'snowball',
46 | 'language' => apply_filters('esi_analyzer_language', 'English'),
47 | ],
48 | ],
49 | ],
50 | ];
51 |
--------------------------------------------------------------------------------
/elasticsearch-indexer.php:
--------------------------------------------------------------------------------
1 |
7 | *
8 | * For the full copyright and license information, please view the LICENSE
9 | * file that was distributed with this source code.
10 | */
11 |
12 | /**
13 | * @wordpress-plugin
14 | * Plugin URI: http://wallmanderco.github.io/elasticsearch-indexer/
15 | * Plugin Name: Elasticsearch Indexer
16 | * Description: Elasticsearch indexer for WordPress and WooCommerce
17 | * Version: 1.6.1
18 | * Author: Mikael Mattsson
19 | * Text Domain: elasticsearch-indexer
20 | */
21 | define('ESI_PLUGINFILE', __FILE__);
22 | define('ESI_PATH', dirname(ESI_PLUGINFILE).'/');
23 | define('ESI_URL', plugins_url('/', __FILE__));
24 |
25 | if (version_compare(phpversion(), '5.4.0', '<') === true) {
26 | function esi_php_version_failed()
27 | {
28 | deactivate_plugins(ESI_PLUGINFILE);
29 | wp_die(__('Elastic search indexer requires at least php version 5.4', 'elasticsearch-indexer'));
30 | }
31 | add_action('admin_init', 'esi_php_version_failed');
32 |
33 | return;
34 | }
35 |
36 | require_once ESI_PATH.'bootstrap.php';
37 |
--------------------------------------------------------------------------------
/functions.php:
--------------------------------------------------------------------------------
1 |
7 | *
8 | * For the full copyright and license information, please view the LICENSE
9 | * file that was distributed with this source code.
10 | */
11 |
12 | use Wallmander\ElasticsearchIndexer\Model\Query;
13 |
14 | /**
15 | * @return bool
16 | */
17 | function esi_plugin_activated()
18 | {
19 | return true;
20 | }
21 |
22 | /**
23 | * @param null|WP_Query $wpQuery
24 | *
25 | * @return \Wallmander\ElasticsearchIndexer\Model\Query
26 | */
27 | function es_query($wpQuery = null)
28 | {
29 | $esq = new Query();
30 | if ($wpQuery instanceof WP_Query) {
31 | $esq->applyWpQuery($wpQuery);
32 | }
33 |
34 | return $esq;
35 | }
36 |
--------------------------------------------------------------------------------
/src/Controller/Admin.php:
--------------------------------------------------------------------------------
1 |
7 | *
8 | * For the full copyright and license information, please view the LICENSE
9 | * file that was distributed with this source code.
10 | */
11 |
12 | namespace Wallmander\ElasticsearchIndexer\Controller;
13 |
14 | use Exception;
15 | use Wallmander\ElasticsearchIndexer\Model\Config;
16 | use Wallmander\ElasticsearchIndexer\Model\Indexer;
17 | use Wallmander\ElasticsearchIndexer\Model\Log;
18 | use Wallmander\ElasticsearchIndexer\Service\Elasticsearch;
19 | use Wallmander\ElasticsearchIndexer\Service\WordPress;
20 | use WP_Admin_Bar;
21 |
22 | /**
23 | * Class Admin.
24 | *
25 | * @author Mikael Mattsson
26 | */
27 | class Admin
28 | {
29 | /**
30 | * Hooked on admin_menu. Adds the menu items to the admin sidebar.
31 | */
32 | public static function actionAdminMenu()
33 | {
34 | add_menu_page(
35 | 'ES Indexer',
36 | 'ES Indexer',
37 | 'manage_options',
38 | 'esindexer_index',
39 | [get_class(), 'getIndex'],
40 | 'dashicons-networking',
41 | 30
42 | );
43 | add_submenu_page(
44 | 'esindexer_index',
45 | 'Settings',
46 | 'Settings',
47 | 'manage_options',
48 | 'esindexer_indexer',
49 | [get_class(), 'getSettings']
50 | );
51 | add_submenu_page(
52 | 'esindexer_index',
53 | 'Status',
54 | 'Status',
55 | 'manage_options',
56 | 'esindexer_status',
57 | [get_class(), 'getStatus']
58 | );
59 | }
60 |
61 | /**
62 | * Hooked on admin_init. Registers the options and enqueues admin style and javascript.
63 | */
64 | public static function actionAdminInit()
65 | {
66 | wp_enqueue_style('elasticsearch-indexer', ESI_URL.'assets/admin/style.css');
67 | wp_enqueue_script('elasticsearch-indexer', ESI_URL.'assets/admin/script.js', ['jquery']);
68 | foreach (Config::load('defaults') as $key => $value) {
69 | register_setting('esi_options_group', Config::OPTION_PREFIX.$key);
70 | }
71 | }
72 |
73 | /**
74 | * Admin Indexing Page.
75 | */
76 | public static function getIndex()
77 | {
78 | $sites = WordPress::getSites();
79 | require ESI_PATH.'/views/admin/index.php';
80 | }
81 |
82 | /**
83 | * Admin Settings Page.
84 | */
85 | public static function getSettings()
86 | {
87 | $hostsStatus = [];
88 | foreach (Config::getHosts() as $host) {
89 | $hostsStatus[] = Elasticsearch::ping($host);
90 | }
91 |
92 | require ESI_PATH.'/views/admin/settings.php';
93 | }
94 |
95 | /**
96 | * Admin Status Page.
97 | */
98 | public static function getStatus()
99 | {
100 | $indices = Elasticsearch::getIndices();
101 | $logs = Log::get();
102 | require ESI_PATH.'/views/admin/status.php';
103 | }
104 |
105 | /**
106 | * Admin reindex, requested by the index page.
107 | */
108 | public static function ajaxReindex()
109 | {
110 | if (!isset($_POST['site']) || !isset($_POST['from']) || empty($_POST['size'])) {
111 | die('invalid request');
112 | }
113 |
114 | $site = (int) $_POST['site'];
115 | $from = (int) $_POST['from'];
116 | $size = (int) $_POST['size'];
117 |
118 | try {
119 | $indexer = new Indexer();
120 | list($indexed, $total) = $indexer->reindex($site, $from, $size);
121 | $data = (object) [
122 | 'success' => false,
123 | 'indexed' => $indexed,
124 | 'total' => $total,
125 | ];
126 | $data->success = true;
127 | header('Content-Type: application/json');
128 | echo json_encode($data);
129 | } catch (Exception $e) {
130 | $data = (object) [
131 | 'success' => false,
132 | 'message' => $e->getMessage(),
133 | ];
134 | header('Content-Type: application/json');
135 | echo json_encode($data);
136 | }
137 |
138 | die();
139 | }
140 |
141 | public static function actionAdminBarMenu(WP_Admin_Bar $adminBar)
142 | {
143 | $statusText = static::getStatusText();
144 | $args = [
145 | 'id' => 'esindexer',
146 | 'title' => 'ES Indexer: '.$statusText[0].'',
147 | 'href' => get_admin_url(null, 'admin.php?page=esindexer_index'),
148 | 'meta' => [
149 | 'title' => Elasticsearch::getErrorMessage(),
150 | ],
151 | ];
152 | $adminBar->add_node($args);
153 | }
154 |
155 | private static function getStatusText()
156 | {
157 | if (!Elasticsearch::isAvailable()) {
158 | return ['Unable to connect', '#e14d43'];
159 | }
160 |
161 | if (Config::option('user_index_version') < Config::option('plugin_index_version')) {
162 | return ['Reindex required', '#e14d43'];
163 | }
164 |
165 | if ($time = Config::option('is_indexing')) {
166 | if ($time + 20 < time()) {
167 | return ['Indexing process interrupted', '#e14d43'];
168 | }
169 |
170 | return ['Indexing...', '#ccaf0b'];
171 | }
172 |
173 | if (!Config::enabledIntegration()) {
174 | return ['Integration Disabled', '#999'];
175 | }
176 |
177 | return ['Enabled', '#a3b745'];
178 | }
179 | }
180 |
--------------------------------------------------------------------------------
/src/Controller/Install.php:
--------------------------------------------------------------------------------
1 |
7 | *
8 | * For the full copyright and license information, please view the LICENSE
9 | * file that was distributed with this source code.
10 | */
11 |
12 | namespace Wallmander\ElasticsearchIndexer\Controller;
13 |
14 | use Wallmander\ElasticsearchIndexer\Model\Config;
15 |
16 | /**
17 | * Class Profiler.
18 | *
19 | * @author Mikael Mattsson
20 | */
21 | class Install
22 | {
23 | /**
24 | * Hooked on plugin activation.
25 | */
26 | public static function actionActivate()
27 | {
28 | $logDir = ESI_PATH.'../../uploads/logs/';
29 | if (!file_exists($logDir)) {
30 | mkdir($logDir, 0777, true);
31 | }
32 | Config::getIndexName(get_current_blog_id()); // Will generate a name if not set.
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/src/Controller/Profiler.php:
--------------------------------------------------------------------------------
1 |
7 | *
8 | * For the full copyright and license information, please view the LICENSE
9 | * file that was distributed with this source code.
10 | */
11 |
12 | namespace Wallmander\ElasticsearchIndexer\Controller;
13 |
14 | use Wallmander\ElasticsearchIndexer\Model\Profiler as ProfilerModel;
15 | use Wallmander\ElasticsearchIndexer\Model\Query;
16 | use WP_Query;
17 |
18 | /**
19 | * Class Profiler.
20 | *
21 | * @author Mikael Mattsson
22 | */
23 | class Profiler
24 | {
25 | /**
26 | * Hooked on admin_menu. Adds the menu items to the admin sidebar.
27 | */
28 | public static function setup()
29 | {
30 | if (!defined('SAVEQUERIES')) {
31 | define('SAVEQUERIES', true);
32 | }
33 |
34 | add_action('wp_enqueue_scripts', [get_class(), 'actionWpEnqueueScripts']);
35 | add_action('admin_enqueue_scripts', [get_class(), 'actionWpEnqueueScripts']);
36 | add_action('esi_after_format_args', [get_class(), 'actionAfterFormatArgs'], 90, 2);
37 | add_action('shutdown', [get_class(), 'actionShutdown']);
38 | }
39 |
40 | /**
41 | * Add profiler style.
42 | */
43 | public static function actionWpEnqueueScripts()
44 | {
45 | wp_enqueue_style('elasticsearch-indexer-profiler', ESI_URL.'assets/profiler/style.css');
46 | }
47 |
48 | /**
49 | * Dump the collected data.
50 | */
51 | public static function actionShutdown()
52 | {
53 | $queries = ProfilerModel::getMySQLQueries();
54 | $totalTime = ProfilerModel::getMySQLQueriesTime();
55 | $elasticQueries = ProfilerModel::getElasticQueries();
56 |
57 | require ESI_PATH.'/views/profiler/footer.php';
58 | }
59 |
60 | /**
61 | * Save the elasticsearch query.
62 | *
63 | * @param Query $query
64 | * @param WP_Query $wpQuery
65 | */
66 | public static function actionAfterFormatArgs(Query $query, WP_Query $wpQuery)
67 | {
68 | ProfilerModel::addElasticsearchQueryArgs($query->getArgs(), $wpQuery->query_vars);
69 | }
70 | }
71 |
--------------------------------------------------------------------------------
/src/Controller/QueryIntegration.php:
--------------------------------------------------------------------------------
1 |
7 | *
8 | * For the full copyright and license information, please view the LICENSE
9 | * file that was distributed with this source code.
10 | */
11 |
12 | namespace Wallmander\ElasticsearchIndexer\Controller;
13 |
14 | use Wallmander\ElasticsearchIndexer\Model\Config;
15 | use Wallmander\ElasticsearchIndexer\Model\Query;
16 | use Wallmander\ElasticsearchIndexer\Model\Query\WpConverter;
17 | use WP_Query;
18 |
19 | /**
20 | * Class QueryIntegration.
21 | *
22 | * @author Mikael Mattsson
23 | */
24 | class QueryIntegration
25 | {
26 | private static $queryStack = [];
27 |
28 | /**
29 | * Filter query string used for get_posts(). Search for posts and save for later.
30 | * Return a query that will return nothing.
31 | *
32 | * @param string $request
33 | * @param \WP_Query $query
34 | *
35 | * @return string
36 | */
37 | public static function filterPostsRequest($request, WP_Query $query)
38 | {
39 | if (apply_filters('esi_skip_query_integration', false, $query)) {
40 | return $request;
41 | }
42 |
43 | if (!$query->is_search() && !Config::enabledFullIntegration()) {
44 | $query->is_elasticsearch_compatible = false;
45 |
46 | return $request;
47 | }
48 |
49 | if (!WpConverter::isCompatible($query)) {
50 | $query->is_elasticsearch_compatible = false;
51 |
52 | return $request;
53 | }
54 |
55 | $query->is_elasticsearch_compatible = true;
56 |
57 | global $wpdb;
58 |
59 | return "SELECT * FROM $wpdb->posts WHERE 1=0";
60 | }
61 |
62 | /**
63 | * Remove the found_rows from the SQL Query.
64 | *
65 | * @param string $sql
66 | * @param \WP_Query $query
67 | *
68 | * @return string
69 | */
70 | public static function filterFoundPostsQuery($sql, WP_Query $query)
71 | {
72 | if (apply_filters('esi_skip_query_integration', false, $query)) {
73 | return $sql;
74 | }
75 |
76 | if (empty($query->is_elasticsearch_compatible)) {
77 | return $sql;
78 | }
79 |
80 | return '';
81 | }
82 |
83 | /**
84 | * Disables cache_results, adds header.
85 | *
86 | * @param \WP_Query $query
87 | */
88 | public static function actionPreGetPosts(WP_Query $query)
89 | {
90 | if (apply_filters('esi_skip_query_integration', false, $query)) {
91 | return;
92 | }
93 |
94 | //$query->query_vars['suppress_filters'] = false;
95 | $query->set('cache_results', false);
96 |
97 | if (!headers_sent()) {
98 | header('X-ElasticsearchIndexer: true');
99 | }
100 | }
101 |
102 | /**
103 | * @param array $posts
104 | * @param \WP_Query &$query
105 | *
106 | * @return array
107 | */
108 | public static function filterThePosts($posts, WP_Query $query)
109 | {
110 | if (apply_filters('esi_skip_query_integration', false, $query)) {
111 | return $posts;
112 | }
113 |
114 | if (empty($query->is_elasticsearch_compatible)) {
115 | return $posts;
116 | }
117 |
118 | return Query::fromWpQuery($query)->getPosts();
119 | }
120 |
121 | /**
122 | * Switch to the correct site if the post site id is different than the actual one.
123 | *
124 | * @param array $post
125 | */
126 | public static function actionThePost($post)
127 | {
128 | if (!is_multisite()) {
129 | return;
130 | }
131 |
132 | if (empty(static::$queryStack)) {
133 | return;
134 | }
135 |
136 | if (!esi_plugin_activated(static::$queryStack[0]) || apply_filters('esi_skip_query_integration', false,
137 | static::$queryStack[0])
138 | ) {
139 | return;
140 | }
141 |
142 | if (!empty($post->site_id) && get_current_blog_id() != $post->site_id) {
143 | restore_current_blog();
144 |
145 | switch_to_blog($post->site_id);
146 |
147 | remove_action('the_post', [get_class(), 'actionThePost'], 10, 1);
148 | setup_postdata($post);
149 | add_action('the_post', [get_class(), 'actionThePost'], 10, 1);
150 | }
151 | }
152 |
153 | /**
154 | * Ensure we've started a loop before we allow ourselves to change the blog.
155 | *
156 | * @param \WP_Query $query
157 | */
158 | public static function actionLoopStart(WP_Query $query)
159 | {
160 | if (!is_multisite()) {
161 | return;
162 | }
163 |
164 | array_unshift(static::$queryStack, $query);
165 | }
166 |
167 | /**
168 | * Make sure the correct blog is restored.
169 | *
170 | * @param \WP_Query $query
171 | */
172 | public static function actionLoopEnd(WP_Query $query)
173 | {
174 | if (!is_multisite()) {
175 | return;
176 | }
177 |
178 | array_pop(static::$queryStack);
179 |
180 | if (apply_filters('esi_skip_query_integration', false, $query)) {
181 | return;
182 | }
183 |
184 | if (!empty($GLOBALS['switched'])) {
185 | restore_current_blog();
186 | }
187 | }
188 | }
189 |
--------------------------------------------------------------------------------
/src/Controller/Sync.php:
--------------------------------------------------------------------------------
1 |
7 | *
8 | * For the full copyright and license information, please view the LICENSE
9 | * file that was distributed with this source code.
10 | */
11 |
12 | namespace Wallmander\ElasticsearchIndexer\Controller;
13 |
14 | use Wallmander\ElasticsearchIndexer\Model\Indexer;
15 |
16 | /**
17 | * Class Sync.
18 | *
19 | * @author Mikael Mattsson
20 | */
21 | class Sync
22 | {
23 | /**
24 | * Hooked on save_post. Called when a post is updated.
25 | *
26 | * @param int $postID
27 | */
28 | public static function actionSavePost($postID)
29 | {
30 | global $importer;
31 |
32 | // If we have an importer we must be doing an import - let's abort
33 | if (!empty($importer)) {
34 | return;
35 | }
36 |
37 | $post = get_post($postID);
38 |
39 | if (!in_array($post->post_status, Indexer::getIndexablePostStati())) {
40 | // The post is not indexable but might have been. Try to delete.
41 | $indexer = new Indexer();
42 | $indexer->deletePost($post->ID);
43 |
44 | return;
45 | }
46 |
47 | if (in_array($post->post_type, Indexer::getIndexablePostTypes())) {
48 | do_action('esi_before_post_save', $post);
49 |
50 | $indexer = new Indexer();
51 | $indexer->indexPost($post);
52 | }
53 | }
54 |
55 | /**
56 | * Hooked on delete_post. Called when a post is deleted.
57 | *
58 | * @param int $postsID
59 | */
60 | public static function actionDeletePost($postsID)
61 | {
62 | $indexer = new Indexer();
63 | $indexer->deletePost($postsID);
64 | }
65 |
66 | /**
67 | * Hooked on added_post_meta, updated_post_meta and deleted_post_meta. Called when post meta data is modified.
68 | *
69 | * @param int $incrementID
70 | * @param int $postID
71 | * @param string $metaKey
72 | * @param $metaValue
73 | */
74 | public static function actionUpdatedPostMeta($incrementID, $postID, $metaKey, $metaValue)
75 | {
76 | $data = [
77 | 'post_meta' => [
78 | $metaKey => get_post_meta($postID, $metaKey),
79 | ],
80 | ];
81 | $indexer = new Indexer();
82 | $indexer->updatePost($postID, $data);
83 | }
84 | }
85 |
--------------------------------------------------------------------------------
/src/Controller/WooCommerce.php:
--------------------------------------------------------------------------------
1 |
7 | *
8 | * For the full copyright and license information, please view the LICENSE
9 | * file that was distributed with this source code.
10 | */
11 |
12 | namespace Wallmander\ElasticsearchIndexer\Controller;
13 |
14 | use WP_Query;
15 |
16 | class WooCommerce
17 | {
18 | /**
19 | * Hooked on pre_get_posts.
20 | *
21 | * @param WP_Query $wpQuery
22 | */
23 | public static function actionPreGetPosts(WP_Query $wpQuery)
24 | {
25 | /*
26 | * Remove WooCommerce hook on product search
27 | */
28 | if ($wpQuery->is_main_query() && $wpQuery->is_search()) {
29 | remove_action('wp', [WC()->query, 'get_products_in_view'], 2);
30 | }
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/src/Controller/WooCommerceAdmin.php:
--------------------------------------------------------------------------------
1 |
7 | *
8 | * For the full copyright and license information, please view the LICENSE
9 | * file that was distributed with this source code.
10 | */
11 |
12 | namespace Wallmander\ElasticsearchIndexer\Controller;
13 |
14 | use stdClass;
15 | use Wallmander\ElasticsearchIndexer\Model\Query;
16 | use WP_Post;
17 |
18 | /**
19 | * Class WooCommerceAdmin.
20 | *
21 | * @author Mikael Mattsson
22 | */
23 | class WooCommerceAdmin
24 | {
25 | /**
26 | * Search custom fields as well as content.
27 | * Replaces WC_Admin_Post_Types::Replaces shop_order_search_custom_fields.
28 | *
29 | * @param \Wallmander\ElasticsearchIndexer\Model\Query $query
30 | */
31 | public static function actionOrderSearch(Query $query)
32 | {
33 | global $pagenow;
34 |
35 | $wpQuery = $query->wp_query;
36 |
37 | if ('edit.php' != $pagenow || !$wpQuery->get('s') || $wpQuery->get('post_type') != 'shop_order') {
38 | return;
39 | }
40 |
41 | $search = str_replace('Order #', '', $wpQuery->get('s'));
42 |
43 | $searchFields = apply_filters('woocommerce_shop_order_search_fields', [
44 | '_billing_first_name',
45 | '_billing_last_name',
46 | '_shipping_first_name',
47 | '_shipping_last_name',
48 | //'_order_key',
49 | '_billing_company',
50 | '_billing_address_1',
51 | '_billing_address_2',
52 | '_billing_city',
53 | '_billing_postcode',
54 | '_billing_country',
55 | '_billing_state',
56 | '_billing_email',
57 | '_billing_phone',
58 | '_shipping_address_1',
59 | '_shipping_address_2',
60 | '_shipping_city',
61 | '_shipping_postcode',
62 | '_shipping_country',
63 | '_shipping_state',
64 | ]);
65 |
66 | foreach ($searchFields as $key => $value) {
67 | $searchFields[$key] = 'post_meta.'.$value;
68 | }
69 |
70 | $searchFields[] = 'order_item_names';
71 | $searchFields[] = '_id';
72 |
73 | $query->setQuery([
74 | 'bool' => [
75 | 'should' => [
76 | [
77 | 'multi_match' => [
78 | 'fields' => $searchFields,
79 | 'type' => 'phrase_prefix',
80 | 'analyzer' => 'esi_simple_analyzer',
81 | 'query' => $search,
82 | ],
83 | ],
84 | [
85 | 'multi_match' => [
86 | 'fields' => $searchFields,
87 | 'type' => 'cross_fields',
88 | 'operator' => 'and',
89 | 'analyzer' => 'esi_simple_analyzer',
90 | 'query' => $search,
91 | ],
92 | ],
93 | ],
94 | ],
95 | ]);
96 |
97 | if (!$wpQuery->get('orderby')) {
98 | $query->setSort('post_date', 'desc');
99 | }
100 | }
101 |
102 | /**
103 | * Add more fields to the index.
104 | *
105 | * @param stdClass $queryArgs
106 | * @param WP_Post $post
107 | *
108 | * @return array
109 | */
110 | public static function filterPostSyncArgs(stdClass $queryArgs, WP_Post $post)
111 | {
112 | global $wpdb;
113 | if ($post->post_type !== 'shop_order') {
114 | return $queryArgs;
115 | }
116 |
117 | $orderItemNames = $wpdb->get_col(
118 | $wpdb->prepare("
119 | SELECT order_item_name
120 | FROM {$wpdb->prefix}woocommerce_order_items as order_items
121 | WHERE order_id = %d
122 | ",
123 | $post->ID
124 | )
125 | );
126 |
127 | $queryArgs->order_item_names = $orderItemNames;
128 |
129 | return $queryArgs;
130 | }
131 | }
132 |
--------------------------------------------------------------------------------
/src/Hooks.php:
--------------------------------------------------------------------------------
1 |
7 | *
8 | * For the full copyright and license information, please view the LICENSE
9 | * file that was distributed with this source code.
10 | */
11 |
12 | namespace Wallmander\ElasticsearchIndexer;
13 |
14 | use Wallmander\ElasticsearchIndexer\Model\Config;
15 | use Wallmander\ElasticsearchIndexer\Service\Elasticsearch;
16 |
17 | /**
18 | * Class Hooks.
19 | *
20 | * @author Mikael Mattsson
21 | */
22 | class Hooks
23 | {
24 | /**
25 | * Setup hooks.
26 | */
27 | public static function setup()
28 | {
29 | static::setupInstall();
30 | static::setupProfiler();
31 | static::setupAdmin();
32 |
33 | if (!Elasticsearch::isAvailable()) {
34 | return;
35 | }
36 |
37 | if (Config::option('user_index_version') < Config::option('plugin_index_version')) {
38 | return;
39 | }
40 |
41 | static::setupSync();
42 | static::setupQueryIntegration();
43 |
44 | add_action('init', function () {
45 | /*
46 | * Need to be set up after WooCommerce.
47 | */
48 | static::setupWooCommerce();
49 | static::setupWooCommerceAdmin();
50 | }, 15);
51 | }
52 |
53 | /**
54 | * Setup Installer hook.
55 | */
56 | public static function setupInstall()
57 | {
58 | $class = __NAMESPACE__.'\Controller\Install';
59 | register_activation_hook(ESI_PLUGINFILE, [$class, 'actionActivate']);
60 | }
61 |
62 | /**
63 | * Setup Profiler Admin hooks.
64 | */
65 | public static function setupProfiler()
66 | {
67 | if (defined('DOING_AJAX') && DOING_AJAX) {
68 | return;
69 | }
70 | if (!is_admin() && !Config::option('profile_frontend')) {
71 | return;
72 | }
73 | if (is_admin() && !Config::option('profile_admin')) {
74 | return;
75 | }
76 |
77 | $class = __NAMESPACE__.'\Controller\Profiler';
78 | $class = apply_filters('esi_controller_profiler', $class);
79 |
80 | $class::setup();
81 | }
82 |
83 | /**
84 | * Setup Admin hooks.
85 | */
86 | public static function setupAdmin()
87 | {
88 | $class = __NAMESPACE__.'\Controller\Admin';
89 | $class = apply_filters('esi_controller_admin', $class);
90 |
91 | add_action('admin_bar_menu', [$class, 'actionAdminBarMenu'], 80);
92 |
93 | if (is_admin()) {
94 | add_action('admin_menu', [$class, 'actionAdminMenu']);
95 | add_action('admin_init', [$class, 'actionAdminInit']);
96 | add_action('wp_ajax_es_reindex', [$class, 'ajaxReindex']);
97 | }
98 | }
99 |
100 | /**
101 | * Setup Sync hooks.
102 | */
103 | public static function setupSync()
104 | {
105 | $class = __NAMESPACE__.'\Controller\Sync';
106 | $class = apply_filters('esi_controller_sync', $class);
107 |
108 | // Sync post on create or update
109 | add_action('save_post', [$class, 'actionSavePost'], 90, 3);
110 |
111 | // Sync post delete
112 | add_action('delete_post', [$class, 'actionDeletePost']);
113 |
114 | // Sync new, deleted or changed metadata
115 | add_action('added_post_meta', [$class, 'actionUpdatedPostMeta'], 10, 4);
116 | add_action('updated_post_meta', [$class, 'actionUpdatedPostMeta'], 10, 4);
117 | add_action('deleted_post_meta', [$class, 'actionUpdatedPostMeta'], 10, 4);
118 | }
119 |
120 | /**
121 | * Setup QueryIntegration hooks.
122 | */
123 | public static function setupQueryIntegration()
124 | {
125 | if (!Config::enabledIntegration() || Config::option('is_indexing')) {
126 | return;
127 | }
128 |
129 | $class = __NAMESPACE__.'\Controller\QueryIntegration';
130 | $class = apply_filters('esi_controller_queryintegration', $class);
131 |
132 | // Make sure we return nothing for MySQL posts query
133 | add_filter('posts_request', [$class, 'filterPostsRequest'], 10, 2);
134 |
135 | // Add header
136 | add_action('pre_get_posts', [$class, 'actionPreGetPosts'], 5);
137 |
138 | // Nukes the FOUND_ROWS() database query
139 | add_filter('found_posts_query', [$class, 'filterFoundPostsQuery'], 5, 2);
140 |
141 | // Search and filter in EP_Posts to WP_Query
142 | add_filter('posts_results', [$class, 'filterThePosts'], 10, 2);
143 |
144 | // Ensure we're in a loop before we allow blog switching
145 | //add_action('loop_start', [$class, 'actionLoopStart'], 10, 1);
146 |
147 | // Properly restore blog if necessary
148 | //add_action('loop_end', [$class, 'actionLoopEnd'], 10, 1);
149 |
150 | // Properly switch to blog if necessary
151 | //add_action('the_post', [$class, 'actionThePost'], 10, 1);
152 |
153 | //add_filter('split_the_query', '__return_false', 40);
154 | }
155 |
156 | /**
157 | * Setup WooCommerce hooks.
158 | */
159 | public static function setupWooCommerce()
160 | {
161 | if (!class_exists('WooCommerce') || !Config::enabledIntegration()) {
162 | return;
163 | }
164 |
165 | $class = __NAMESPACE__.'\Controller\WooCommerce';
166 | $class = apply_filters('esi_controller_woocommerce', $class);
167 |
168 | add_filter('pre_get_posts', [$class, 'actionPreGetPosts'], 15);
169 |
170 | static::forceRemoveAction('posts_search', 'product_search');
171 | }
172 |
173 | /**
174 | * Setup WooCommerceAdmin hooks.
175 | */
176 | public static function setupWooCommerceAdmin()
177 | {
178 | if (!class_exists('WooCommerce') || !Config::option('index_private_post_types')) {
179 | return;
180 | }
181 |
182 | $class = __NAMESPACE__.'\Controller\WooCommerceAdmin';
183 | $class = apply_filters('esi_controller_woocommerceadmin', $class);
184 | add_filter('esi_post_sync_args', [$class, 'filterPostSyncArgs'], 10, 2);
185 |
186 | if (Config::enabledFullIntegration()) {
187 | static::forceRemoveAction('parse_query', 'shop_order_search_custom_fields');
188 | add_action('esi_after_format_args', [$class, 'actionOrderSearch']);
189 | }
190 | }
191 |
192 | /**
193 | * Remove a hook without a reference to the instance.
194 | *
195 | * @param string $tag
196 | * @param string $functionToRemove
197 | * @param int $priority
198 | */
199 | public static function forceRemoveAction($tag, $functionToRemove, $priority = 10)
200 | {
201 | global $wp_filter;
202 |
203 | if (!empty($wp_filter[$tag][$priority])) {
204 | foreach ($wp_filter[$tag][$priority] as $key => $function) {
205 | if (substr($key, 32) == $functionToRemove) {
206 | unset($wp_filter[$tag][$priority][$key]);
207 | }
208 | }
209 | }
210 | }
211 | }
212 |
--------------------------------------------------------------------------------
/src/Model/Client.php:
--------------------------------------------------------------------------------
1 |
7 | *
8 | * For the full copyright and license information, please view the LICENSE
9 | * file that was distributed with this source code.
10 | */
11 |
12 | namespace Wallmander\ElasticsearchIndexer\Model;
13 |
14 | use Elasticsearch\Client as ElasticSearchClient;
15 | use Wallmander\ElasticsearchIndexer\Service\Elasticsearch;
16 |
17 | /**
18 | * A connection to Elasticsearch.
19 | *
20 | * @author Mikael Mattsson
21 | */
22 | class Client extends ElasticSearchClient
23 | {
24 | protected $blogID;
25 |
26 | /**
27 | * @param int|null $blogId
28 | */
29 | public function __construct($blogId = null)
30 | {
31 | $this->setBlog($blogId);
32 |
33 | return parent::__construct([
34 | 'hosts' => Config::getHosts(),
35 | 'logging' => true,
36 | 'logPath' => Log::getFilePath('elasticsearch'),
37 | ]);
38 | }
39 |
40 | /**
41 | * @param int|null $blogID
42 | *
43 | * @return string
44 | */
45 | public function getIndexName($blogID = null)
46 | {
47 | if ($blogID === null) {
48 | $blogID = $this->blogID;
49 | }
50 |
51 | return Config::getIndexName($blogID);
52 | }
53 |
54 | /**
55 | * @param int|null $blogId
56 | *
57 | * @return $this
58 | */
59 | public function setBlog($blogId = null)
60 | {
61 | $this->blogID = $blogId ? $blogId : get_current_blog_id();
62 |
63 | return $this;
64 | }
65 | }
66 |
--------------------------------------------------------------------------------
/src/Model/Config.php:
--------------------------------------------------------------------------------
1 |
7 | *
8 | * For the full copyright and license information, please view the LICENSE
9 | * file that was distributed with this source code.
10 | */
11 |
12 | namespace Wallmander\ElasticsearchIndexer\Model;
13 |
14 | /**
15 | * Fetches config files and handles WordPress options.
16 | *
17 | * @author Mikael Mattsson
18 | */
19 | class Config
20 | {
21 | /**
22 | * Prefix for options.
23 | */
24 | const OPTION_PREFIX = 'esi_';
25 |
26 | /**
27 | * Prefix for options.
28 | */
29 | const INTEGRATION_LEVEL_OFF = 0;
30 |
31 | /**
32 | * Prefix for options.
33 | */
34 | const INTEGRATION_LEVEL_SEARCH = 1;
35 |
36 | /**
37 | * Prefix for options.
38 | */
39 | const INTEGRATION_LEVEL_FULL = 2;
40 |
41 | /**
42 | * Fetch config array from a file in the config directory.
43 | *
44 | * @param string $config
45 | *
46 | * @return array
47 | */
48 | public static function load($config)
49 | {
50 | return require ESI_PATH.'config/'.$config.'.php';
51 | }
52 |
53 | /**
54 | * Get option from wp_options table.
55 | *
56 | * @param $key
57 | *
58 | * @return mixed|void
59 | */
60 | public static function option($key)
61 | {
62 | $o = get_option(static::OPTION_PREFIX.$key, null);
63 | if ($o !== null) {
64 | return $o;
65 | }
66 | $defaults = static::load('defaults');
67 |
68 | return $defaults[$key];
69 | }
70 |
71 | /**
72 | * Save an option to wp_options table.
73 | *
74 | * @param $key
75 | * @param $value
76 | * @param null|string $autoload
77 | */
78 | public static function setOption($key, $value, $autoload = null)
79 | {
80 | update_option(static::OPTION_PREFIX.$key, $value, $autoload);
81 | }
82 |
83 | /**
84 | * Prepend the option prefix to a key.
85 | *
86 | * @param $key
87 | *
88 | * @return string
89 | */
90 | public static function optionKey($key)
91 | {
92 | return static::OPTION_PREFIX.$key;
93 | }
94 |
95 | public static function getHosts()
96 | {
97 | $hosts = [];
98 | // hosts separated by comma (,) is deprecated.
99 | $option = str_replace(',', "\n", static::option('hosts'));
100 | foreach (explode("\n", $option) as $h) {
101 | if (strpos($h, '://') === false) {
102 | $hosts[] = trim('http://'.$h);
103 | } else {
104 | $hosts[] = trim($h);
105 | }
106 | }
107 |
108 | return $hosts;
109 | }
110 |
111 | public static function getFirstHost()
112 | {
113 | return static::getHosts()[0];
114 | }
115 |
116 | public static function getIndexName($blogID)
117 | {
118 | $indexName = static::option('index_name');
119 |
120 | if (!$indexName) {
121 | // Generate a name
122 | $siteUrl = get_site_url($blogID);
123 |
124 | $indexName = preg_replace('#https?://(www\.)?#i', '', $siteUrl);
125 | $indexName = preg_replace('#[^\w]#', '', $indexName);
126 | static::setOption('index_name', $indexName);
127 | }
128 |
129 | $indexName .= '-'.$blogID;
130 |
131 | return apply_filters('esi_index_name', $indexName);
132 | }
133 |
134 | public static function enabledIntegration()
135 | {
136 | return self::option('integration_level') != self::INTEGRATION_LEVEL_OFF;
137 | }
138 |
139 | public static function enabledFullIntegration()
140 | {
141 | return self::option('integration_level') == self::INTEGRATION_LEVEL_FULL;
142 | }
143 | }
144 |
--------------------------------------------------------------------------------
/src/Model/Indexer.php:
--------------------------------------------------------------------------------
1 |
7 | *
8 | * For the full copyright and license information, please view the LICENSE
9 | * file that was distributed with this source code.
10 | */
11 |
12 | namespace Wallmander\ElasticsearchIndexer\Model;
13 |
14 | use Exception;
15 | use Wallmander\ElasticsearchIndexer\Service\Elasticsearch;
16 | use Wallmander\ElasticsearchIndexer\Service\WordPress;
17 | use WP_Query;
18 | use WP_User;
19 |
20 | /**
21 | * Keeps the MySQL database in sync with the Elasticsearch database.
22 | *
23 | * @author Mikael Mattsson
24 | */
25 | class Indexer extends Client
26 | {
27 | /**
28 | * Called in admin to reindex all posts in all blogs.
29 | *
30 | * @param int $site
31 | * @param int $from
32 | * @param int $size
33 | *
34 | * @return array
35 | */
36 | public function reindex($site, $from, $size)
37 | {
38 | add_filter('esi_skip_query_integration', '__return_true');
39 |
40 | WordPress::switchToBlog($site);
41 | $this->setBlog($site);
42 |
43 | Config::setOption('is_indexing', time());
44 | Config::setOption('user_index_version', Config::option('plugin_index_version'));
45 |
46 | list($indexed, $total) = $this->reindexBlog($from, $size);
47 |
48 | if ($indexed >= $total) {
49 | Config::setOption('is_indexing', false);
50 | Elasticsearch::optimize();
51 | }
52 |
53 | WordPress::restoreCurrentBlog();
54 | $this->setBlog();
55 |
56 | return [$indexed, $total];
57 | }
58 |
59 | /**
60 | * Reindex all posts in current blog.
61 | *
62 | * @param int $offset
63 | * @param int $postsPerPage
64 | *
65 | * @return array
66 | */
67 | protected function reindexBlog($offset, $postsPerPage = 500)
68 | {
69 | set_time_limit(200);
70 |
71 | if ($offset == 0) {
72 | $this->flush();
73 | }
74 |
75 | $args = apply_filters('esi_index_posts_args', [
76 | 'posts_per_page' => $postsPerPage,
77 | 'post_type' => static::getIndexablePostTypes(),
78 | 'post_status' => static::getIndexablePostStati(),
79 | 'offset' => $offset,
80 | 'ignore_sticky_posts' => true,
81 | 'orderby' => 'id',
82 | 'order' => 'asc',
83 | 'suppress_filters' => true,
84 | ]);
85 |
86 | $query = new WP_Query($args);
87 |
88 | if ($query->have_posts()) {
89 | $this->indexPosts($query->posts);
90 | }
91 |
92 | return [$query->post_count + $offset, (int) $query->found_posts];
93 | }
94 |
95 | /**
96 | * Delete existing index, create new index and add mappings.
97 | */
98 | protected function flush()
99 | {
100 | $indexName = $this->getIndexName();
101 | if ($this->indices()->exists(['index' => $indexName])) {
102 | $this->indices()->delete(['index' => $indexName]);
103 | }
104 | $this->indices()->create([
105 | 'index' => $indexName,
106 | 'body' => [
107 | 'settings' => Config::load('settings'),
108 | 'mappings' => Config::load('mappings'),
109 | ],
110 | ]);
111 | }
112 |
113 | /**
114 | * Set refresh_interval on all indexes.
115 | *
116 | * @param string $interval
117 | */
118 | public function setRefreshInterval($interval = '1s')
119 | {
120 | $sites = is_multisite() ? wp_get_sites() : [['blog_id' => get_current_blog_id()]];
121 | foreach ($sites as $site) {
122 | $index = $this->getIndexName($site['blog_id']);
123 | Elasticsearch::setSettings($index, [
124 | 'index' => ['refresh_interval' => $interval],
125 | ]);
126 | }
127 | }
128 |
129 | /**
130 | * @param int|object $post
131 | *
132 | * @return array|bool
133 | */
134 | public function indexPost($post)
135 | {
136 | if (!is_object($post)) {
137 | $post = get_post($post);
138 | }
139 | $postArgs = static::preparePost($post);
140 |
141 | if (apply_filters('esi_post_sync_kill', false, $postArgs, $post->ID)) {
142 | return false;
143 | }
144 |
145 | try {
146 | $response = $this->index([
147 | 'index' => $this->getIndexName(),
148 | 'type' => 'post',
149 | 'id' => $postArgs->post_id,
150 | 'body' => $postArgs,
151 | ]);
152 | } catch (Exception $e) {
153 | Log::add('Failed to index post '.$postArgs->post_id.'. Message: '.$e->getMessage());
154 |
155 | return false;
156 | }
157 |
158 | return $response;
159 | }
160 |
161 | /**
162 | * @param array $posts
163 | */
164 | public function indexPosts(array $posts)
165 | {
166 | $indexName = $this->getIndexName();
167 | $body = [];
168 | foreach ($posts as $post) {
169 | $body[] = [
170 | 'index' => [
171 | '_index' => $indexName,
172 | '_type' => 'post',
173 | '_id' => $post->ID,
174 | ],
175 | ];
176 | $body[] = static::preparePost($post);
177 | }
178 | $responses = $this->bulk(['body' => $body]);
179 | if ($responses['errors']) {
180 | if ($responses['items']) {
181 | foreach ($responses['items'] as $item) {
182 | if ($item['index']['status'] !== 201) {
183 | Log::add('Failed to index post '.$item['index']['_id'].' Message: '.$item['index']['error']);
184 | }
185 | }
186 | } else {
187 | //Failed for some other reason
188 | Log::add('indexer failed. Response: '.print_r($responses['errors'], 1));
189 | }
190 | }
191 | }
192 |
193 | /**
194 | * Update post index.
195 | *
196 | * @param int $postsID
197 | */
198 | public function updatePost($postsID, $data)
199 | {
200 | try {
201 | $this->update([
202 | 'index' => $this->getIndexName(),
203 | 'type' => 'post',
204 | 'id' => $postsID,
205 | 'body' => [
206 | 'doc' => $data,
207 | ],
208 | ]);
209 | } catch (Exception $e) {
210 | Log::add('Unable to update post '.$postsID);
211 | }
212 | }
213 |
214 | /**
215 | * Delete post index.
216 | *
217 | * @param int $postsID
218 | */
219 | public function deletePost($postsID)
220 | {
221 | try {
222 | $this->delete([
223 | 'index' => $this->getIndexName(),
224 | 'type' => 'post',
225 | 'id' => $postsID,
226 | ]);
227 | } catch (Exception $e) {
228 | }
229 | }
230 |
231 | /**
232 | * @param $post
233 | *
234 | * @return object
235 | *
236 | * @author 10up/ElasticPress
237 | */
238 | public static function preparePost($post)
239 | {
240 | if (!is_object($post)) {
241 | $post = get_post($post);
242 | }
243 |
244 | $user = get_userdata($post->post_author);
245 |
246 | if ($user instanceof WP_User) {
247 | $user_data = [
248 | 'raw' => $user->user_login,
249 | 'login' => $user->user_login,
250 | 'display_name' => $user->display_name,
251 | 'id' => $user->ID,
252 | ];
253 | } else {
254 | $user_data = [
255 | 'raw' => '',
256 | 'login' => '',
257 | 'display_name' => '',
258 | 'id' => '',
259 | ];
260 | }
261 |
262 | $post_date = $post->post_date;
263 | $post_date_gmt = $post->post_date_gmt;
264 | $post_modified = $post->post_modified;
265 | $post_modified_gmt = $post->post_modified_gmt;
266 |
267 | if (strtotime($post_date) <= 0) {
268 | $post_date = null;
269 | }
270 |
271 | if (strtotime($post_date_gmt) <= 0) {
272 | $post_date_gmt = null;
273 | }
274 |
275 | if (strtotime($post_modified) <= 0) {
276 | $post_modified = null;
277 | }
278 |
279 | if (strtotime($post_modified_gmt) <= 0) {
280 | $post_modified_gmt = null;
281 | }
282 |
283 | $post_args = (object) [
284 | 'post_id' => $post->ID,
285 | 'post_author' => $user_data,
286 | 'post_date' => $post_date,
287 | 'post_date_gmt' => $post_date_gmt,
288 | 'post_title' => $post->post_title,
289 | 'post_excerpt' => $post->post_excerpt,
290 | 'post_content' => $post->post_content,
291 | 'post_status' => $post->post_status,
292 | 'post_name' => $post->post_name,
293 | 'post_modified' => $post_modified,
294 | 'post_modified_gmt' => $post_modified_gmt,
295 | 'post_parent' => $post->post_parent,
296 | 'post_type' => $post->post_type,
297 | 'post_mime_type' => $post->post_mime_type,
298 | 'permalink' => get_permalink($post->ID),
299 | 'terms' => static::prepareTerms($post),
300 | 'post_meta' => $meta = static::prepareMeta($post),
301 | 'post_meta_num' => static::prepareMetaNum($meta),
302 | 'post_date_object' => static::prepareDateTerms($post_date),
303 | 'post_date_gmt_object' => static::prepareDateTerms($post_date_gmt),
304 | 'post_modified_object' => static::prepareDateTerms($post_modified),
305 | 'post_modified_gmt_object' => static::prepareDateTerms($post_modified_gmt),
306 | 'menu_order' => $post->menu_order,
307 | 'guid' => $post->guid,
308 | 'comment_count' => $post->comment_count,
309 | ];
310 |
311 | $post_args = apply_filters('esi_post_sync_args', $post_args, $post);
312 |
313 | return $post_args;
314 | }
315 |
316 | /**
317 | * @param $post_date_gmt
318 | *
319 | * @return array
320 | *
321 | * @author 10up/ElasticPress
322 | */
323 | protected static function prepareDateTerms($post_date_gmt)
324 | {
325 | $timestamp = strtotime($post_date_gmt);
326 | $date_terms = [
327 | 'year' => (int) date('Y', $timestamp),
328 | 'month' => (int) date('m', $timestamp),
329 | 'week' => (int) date('W', $timestamp),
330 | 'dayofyear' => (int) date('z', $timestamp),
331 | 'day' => (int) date('d', $timestamp),
332 | 'dayofweek' => (int) date('d', $timestamp),
333 | 'dayofweek_iso' => (int) date('N', $timestamp),
334 | 'hour' => (int) date('H', $timestamp),
335 | 'minute' => (int) date('i', $timestamp),
336 | 'second' => (int) date('s', $timestamp),
337 | 'm' => (int) (date('Y', $timestamp).date('m', $timestamp)), // yearmonth
338 | ];
339 |
340 | return $date_terms;
341 | }
342 |
343 | /**
344 | * @param $post
345 | *
346 | * @return array
347 | */
348 | protected static function prepareTerms($post)
349 | {
350 | $taxonomies = get_object_taxonomies($post->post_type, 'objects');
351 | $terms = [];
352 |
353 | foreach ($taxonomies as $taxonomy) {
354 | $objectTerms = get_the_terms($post->ID, $taxonomy->name);
355 |
356 | if (is_wp_error($objectTerms)) {
357 | continue;
358 | }
359 |
360 | if (!$objectTerms) {
361 | $terms[$taxonomy->name] = [];
362 | continue;
363 | }
364 |
365 | foreach ($objectTerms as $term) {
366 | $allSlugs = [$term->slug];
367 |
368 | //Add parent slug
369 | if ($parent = get_term_by('id', $term->parent, $term->taxonomy)) {
370 | $allSlugs[] = $parent->slug;
371 | }
372 |
373 | $terms[$term->taxonomy][] = [
374 | 'term_id' => $term->term_id,
375 | 'slug' => $term->slug,
376 | 'name' => $term->name,
377 | 'parent' => $term->parent,
378 | 'all_slugs' => $allSlugs,
379 | ];
380 | }
381 | }
382 |
383 | return $terms;
384 | }
385 |
386 | /**
387 | * @param $post
388 | *
389 | * @return array
390 | */
391 | public static function prepareMeta($post)
392 | {
393 | $meta = update_meta_cache('post', [$post->ID])[$post->ID];
394 |
395 | if (empty($meta)) {
396 | return [];
397 | }
398 |
399 | return array_map('maybe_unserialize', $meta);
400 | }
401 |
402 | /**
403 | * @param array $meta
404 | *
405 | * @return array
406 | */
407 | public static function prepareMetaNum(array $meta)
408 | {
409 | foreach ($meta as $key => $value) {
410 | $meta[$key] = array_map('intval', $value);
411 | }
412 |
413 | return $meta;
414 | }
415 |
416 | /**
417 | * @return array
418 | */
419 | public static function getIndexablePostTypes()
420 | {
421 | if (Config::option('index_private_post_types')) {
422 | return get_post_types();
423 | }
424 |
425 | return get_post_types(['exclude_from_search' => false]);
426 | }
427 |
428 | /**
429 | * @return array
430 | */
431 | public static function getIndexablePostStati()
432 | {
433 | if (Config::option('index_private_post_types')) {
434 | return get_post_stati();
435 | }
436 |
437 | return get_post_stati(['exclude_from_search' => false]);
438 | }
439 |
440 | /**
441 | * @return array
442 | */
443 | public static function getSearchablePostTypes()
444 | {
445 | return get_post_types(['exclude_from_search' => false]);
446 | }
447 | }
448 |
--------------------------------------------------------------------------------
/src/Model/Log.php:
--------------------------------------------------------------------------------
1 |
7 | *
8 | * For the full copyright and license information, please view the LICENSE
9 | * file that was distributed with this source code.
10 | */
11 |
12 | namespace Wallmander\ElasticsearchIndexer\Model;
13 |
14 | use Monolog\Handler\StreamHandler;
15 | use Monolog\Logger;
16 |
17 | /**
18 | * Fetches posts.
19 | *
20 | * @author Mikael Mattsson
21 | */
22 | class Log
23 | {
24 | /**
25 | * @var Logger|null
26 | */
27 | public static $log = null;
28 |
29 | /**
30 | * Get the logger instance.
31 | *
32 | * @return \Monolog\Logger
33 | */
34 | public static function getLoggerInstance()
35 | {
36 | if (static::$log === null) {
37 | static::$log = new Logger('elasticsearch-indexer');
38 | static::$log->pushHandler(new StreamHandler(static::getFilePath(), Logger::ERROR));
39 | }
40 |
41 | return static::$log;
42 | }
43 |
44 | /**
45 | * Add a line to the log.
46 | *
47 | * @param string $message
48 | */
49 | public static function add($message)
50 | {
51 | static::getLoggerInstance()->addError($message);
52 | }
53 |
54 | /**
55 | * Get entire log.
56 | *
57 | * @return string
58 | */
59 | public static function get()
60 | {
61 | if (file_exists(static::getFilePath())) {
62 | return file_get_contents(static::getFilePath());
63 | }
64 |
65 | return '';
66 | }
67 |
68 | /**
69 | * Get the full path to a log file.
70 | *
71 | * @param string $filename
72 | *
73 | * @return string
74 | */
75 | public static function getFilePath($filename = 'elasticsearch-indexer')
76 | {
77 | return ESI_PATH.'../../uploads/logs/'.$filename.'.log';
78 | }
79 | }
80 |
--------------------------------------------------------------------------------
/src/Model/Profiler.php:
--------------------------------------------------------------------------------
1 |
7 | *
8 | * For the full copyright and license information, please view the LICENSE
9 | * file that was distributed with this source code.
10 | */
11 |
12 | namespace Wallmander\ElasticsearchIndexer\Model;
13 |
14 | class Profiler
15 | {
16 | /**
17 | * List of performed Elasticsearch queries.
18 | */
19 | private static $elasticQueries = [];
20 |
21 | /**
22 | * @return array
23 | */
24 | public static function getMySQLQueries()
25 | {
26 | global $wpdb;
27 |
28 | $queries = $wpdb->queries;
29 |
30 | usort($queries, function ($a, $b) {
31 | return ($a[1] < $b[1]) * 2 - 1;
32 | });
33 |
34 | return $queries;
35 | }
36 |
37 | /**
38 | * @return float
39 | */
40 | public static function getMySQLQueriesTime()
41 | {
42 | global $wpdb;
43 | $totalTime = (float) 0;
44 |
45 | foreach ($wpdb->queries as $query) {
46 | $totalTime += $query[1];
47 | }
48 |
49 | return $totalTime;
50 | }
51 |
52 | /**
53 | * @return array
54 | */
55 | public static function getElasticQueries()
56 | {
57 | return static::$elasticQueries;
58 | }
59 |
60 | /**
61 | * @param array $elasticQueries
62 | * @param array $queryArgs
63 | */
64 | public static function addElasticsearchQueryArgs($elasticQueries, array $queryVars)
65 | {
66 | self::$elasticQueries[] = [$elasticQueries, $queryVars];
67 | }
68 | }
69 |
--------------------------------------------------------------------------------
/src/Model/Query.php:
--------------------------------------------------------------------------------
1 |
7 | *
8 | * For the full copyright and license information, please view the LICENSE
9 | * file that was distributed with this source code.
10 | */
11 |
12 | namespace Wallmander\ElasticsearchIndexer\Model;
13 |
14 | use stdClass;
15 | use Wallmander\ElasticsearchIndexer\Model\Query\BuilderTrait;
16 | use Wallmander\ElasticsearchIndexer\Model\Query\WpConverter;
17 | use WP_Post;
18 | use WP_Query;
19 |
20 | /**
21 | * Fetches posts.
22 | *
23 | * @author Mikael Mattsson
24 | */
25 | class Query extends Client
26 | {
27 | /**
28 | * @var WP_Query|null
29 | */
30 | public $wp_query;
31 |
32 | /**
33 | * @var int
34 | */
35 | public $found_posts = 0;
36 |
37 | /**
38 | * @var array
39 | */
40 | public $posts = [];
41 |
42 | /**
43 | * @var bool
44 | */
45 | public $disabled = false;
46 |
47 | /**
48 | * @var bool
49 | */
50 | public $updatePostTermCache = false;
51 |
52 | /**
53 | * @var bool
54 | */
55 | public $updatePostMetaCache = false;
56 |
57 | use BuilderTrait;
58 |
59 | public function __construct()
60 | {
61 | parent::__construct();
62 | $this->builderConstruct();
63 | }
64 |
65 | /**
66 | * @param \WP_Query $wpQuery
67 | *
68 | * @return \Wallmander\ElasticsearchIndexer\Model\Query
69 | */
70 | public static function fromWpQuery(WP_Query $wpQuery)
71 | {
72 | $q = new static();
73 | $q->applyWpQuery($wpQuery);
74 |
75 | return $q;
76 | }
77 |
78 | /**
79 | * @param \WP_Query $wpQuery
80 | */
81 | public function applyWpQuery(WP_Query $wpQuery)
82 | {
83 | $this->wp_query = $wpQuery;
84 | $this->formatArgs($wpQuery);
85 | }
86 |
87 | /**
88 | * @return array|bool
89 | */
90 | public function getPosts()
91 | {
92 | if ($this->disabled) {
93 | return false;
94 | }
95 | $result = $this->search([
96 | 'index' => $this->getIndexName(),
97 | 'type' => 'post',
98 | 'body' => $this->args,
99 | ]);
100 |
101 | $this->found_posts = $result['hits']['total'];
102 | if ($this->wp_query) {
103 | $wpQuery = $this->wp_query;
104 | $wpQuery->found_posts = $this->found_posts;
105 | $ppp = $wpQuery->get('posts_per_page') ? $wpQuery->get('posts_per_page') : get_option('posts_per_page');
106 | $wpQuery->max_num_pages = ceil($this->found_posts / $ppp);
107 | }
108 |
109 | $this->posts = [];
110 |
111 | foreach ($result['hits']['hits'] as $p) {
112 | $p = $p['_source'];
113 | $this->posts[] = $post = new WP_Post(new stdClass());
114 | $post->ID = $p['post_id'];
115 | $post->site_id = get_current_blog_id();
116 | $post->post_author = $p['post_author']['id'];
117 | if (empty($p['site_id'])) {
118 | $post->site_id = get_current_blog_id();
119 | } else {
120 | $post->site_id = $p['site_id'];
121 | }
122 | $post->post_type = $p['post_type'];
123 | $post->post_name = $p['post_name'];
124 | $post->post_status = $p['post_status'];
125 | $post->post_title = $p['post_title'];
126 | $post->post_parent = $p['post_parent'];
127 | $post->post_content = $p['post_content'];
128 | $post->post_excerpt = $p['post_excerpt'];
129 | $post->post_date = $p['post_date'];
130 | $post->post_date_gmt = $p['post_date_gmt'];
131 | $post->post_modified = $p['post_modified'];
132 | $post->post_modified_gmt = $p['post_modified_gmt'];
133 | $post->permalink = $p['permalink'];
134 | $post->post_mime_type = $p['post_mime_type'];
135 | $post->menu_order = $p['menu_order'];
136 | $post->guid = $p['guid'];
137 | $post->comment_count = $p['comment_count'];
138 | $post->filter = 'raw';
139 | $post->elasticsearch = $p;
140 |
141 | if ($this->updatePostTermCache) {
142 | foreach ($p['terms'] as $taxonomy => $terms) {
143 | foreach ($terms as $key => $value) {
144 | $terms[$key] = $value = (object) $value;
145 | $value->taxonomy = $taxonomy;
146 | $value->filter = 'raw';
147 | }
148 | wp_cache_add($post->ID, $terms, $taxonomy.'_relationships');
149 | }
150 | }
151 |
152 | if ($this->updatePostMetaCache) {
153 | wp_cache_add($post->ID, $p['post_meta'], 'post_meta');
154 | }
155 | }
156 |
157 | return $this->posts;
158 | }
159 |
160 | public function formatArgs(WP_Query $wpQuery)
161 | {
162 | WpConverter::formatArgs($this, $wpQuery);
163 |
164 | return $this;
165 | }
166 | }
167 |
--------------------------------------------------------------------------------
/src/Model/Query/BuilderTrait.php:
--------------------------------------------------------------------------------
1 |
7 | *
8 | * For the full copyright and license information, please view the LICENSE
9 | * file that was distributed with this source code.
10 | */
11 |
12 | namespace Wallmander\ElasticsearchIndexer\Model\Query;
13 |
14 | /**
15 | * Querybuilder for Elasticsearch.
16 | *
17 | * @author Mikael Mattsson
18 | */
19 | trait BuilderTrait
20 | {
21 | /**
22 | * @var array
23 | */
24 | protected $args = [];
25 |
26 | /**
27 | * @var null|object
28 | */
29 | protected $filterBuildingPoint = null;
30 |
31 | /**
32 | * @var bool
33 | */
34 | public $isSingle = false;
35 |
36 | protected function builderConstruct()
37 | {
38 | $this->args['filter']['and'] = [];
39 | $this->filterBuildingPoint = &$this->args['filter']['and'];
40 | }
41 |
42 | public function setQuery($query)
43 | {
44 | if ($query) {
45 | $this->args['query'] = $query;
46 | } elseif (isset($this->args['query'])) {
47 | unset($this->args['query']);
48 | }
49 |
50 | return $this;
51 | }
52 |
53 | public function setSort($field, $order = 'asc')
54 | {
55 | $this->args['sort'] = [];
56 | $this->addSort($field, $order);
57 |
58 | return $this;
59 | }
60 |
61 | public function addSort($field, $order = 'asc')
62 | {
63 | if (!isset($this->args['sort'])) {
64 | $this->args['sort'] = [];
65 | }
66 | if (is_array($field)) {
67 | foreach ($field as $key => $value) {
68 | $this->args['sort'][] = [$key => ['order' => $value]];
69 | }
70 | } else {
71 | $this->args['sort'][] = [$field => ['order' => $order]];
72 | }
73 |
74 | return $this;
75 | }
76 |
77 | public function setMinScore($score)
78 | {
79 | if ($score) {
80 | $this->args['min_score'] = $score;
81 | } elseif (isset($this->args['min_score'])) {
82 | unset($this->args['min_score']);
83 | }
84 |
85 | return $this;
86 | }
87 |
88 | public function bool($callable, $relation = 'must')
89 | {
90 | $tmp = &$this->filterBuildingPoint;
91 | $relation = strtolower($relation);
92 | if ($relation == 'and') { //alias
93 | $relation = 'must';
94 | }
95 | if ($relation == 'or') { //alias
96 | $relation = 'should';
97 | }
98 | $this->filterBuildingPoint = &$this->filterBuildingPoint[]['bool'][$relation];
99 | $this->filterBuildingPoint = [];
100 | $callable($this);
101 | $this->filterBuildingPoint = &$tmp;
102 |
103 | return $this;
104 | }
105 |
106 | /**
107 | * examples: where(['age' => 20, 'name' => 'John'])
108 | * where('age', 20)
109 | * where('age', [20, 21]) //warning: handled as ”in”
110 | * where('age', 'in', [20, 21]) // 20 or 21
111 | * where('age', '=', [20, 21]) // exact match
112 | * where('age', '==', [20, 21]) // same as above
113 | * where('age', '!=', 20)
114 | * where('age', 'exists', true) //warning: arg3 is needed or else it will check if age = 'exists'.
115 | *
116 | * @param string|array $arg1
117 | * @param string|array|null $arg2
118 | * @param string|array|null $arg3
119 | * @param bool $not
120 | *
121 | * @return $this
122 | */
123 | public function where($arg1, $arg2 = null, $arg3 = null, $not = false)
124 | {
125 | if (is_array($arg1)) {
126 | $this->filterBuildingPoint[] = [
127 | 'terms' => $arg1,
128 | ];
129 |
130 | return $this;
131 | }
132 |
133 | if ($arg3 === null) {
134 | $arg3 = $arg2;
135 | $arg2 = 'in';
136 | }
137 |
138 | $must = $not ? 'must_not' : 'must';
139 |
140 | switch (strtolower($arg2)) {
141 | case '!=': // used by meta_query
142 | $must = 'must_not';
143 | case '=': // used by meta_query
144 | case '==':
145 | $this->filterBuildingPoint[]['bool'][$must] = [
146 | 'term' => [$arg1 => $arg3],
147 | ];
148 | break;
149 |
150 | case 'in':
151 | $this->filterBuildingPoint[]['bool'][$must] = [
152 | is_array($arg3) ? 'terms' : 'term' => [$arg1 => $arg3],
153 | ];
154 | break;
155 |
156 | case 'not exists': // used by meta_query
157 | $must = 'must_not';
158 | case 'exists': // used by meta_query
159 | case 'has':
160 | $this->filterBuildingPoint[]['bool'][$must]['exists'] = [
161 | 'field' => $arg1,
162 | ];
163 | break;
164 |
165 | case '>=': // used by meta_query
166 | case 'gte':
167 | $this->filterBuildingPoint[]['bool'][$must]['range'] = [
168 | $arg1 => ['gte' => $arg3],
169 | ];
170 | break;
171 |
172 | case '<=': // used by meta_query
173 | case 'lte':
174 | $this->filterBuildingPoint[]['bool'][$must]['range'] = [
175 | $arg1 => ['lte' => $arg3],
176 | ];
177 | break;
178 |
179 | case '>': // used by meta_query
180 | case 'gt':
181 | $this->filterBuildingPoint[]['bool'][$must]['range'] = [
182 | $arg1 => ['gt' => $arg3],
183 | ];
184 | break;
185 |
186 | case '<': // used by meta_query
187 | case 'lt':
188 | $this->filterBuildingPoint[]['bool'][$must]['range'] = [
189 | $arg1 => ['lt' => $arg3],
190 | ];
191 | break;
192 |
193 | case 'between':
194 | $this->filterBuildingPoint[]['bool'][$must]['range'] = [
195 | $arg1 => ['gt' => $arg3[0], 'lt' => $arg3[1]],
196 | ];
197 | break;
198 |
199 | }
200 |
201 | return $this;
202 | }
203 |
204 | public function whereNot($arg1, $arg2 = null, $arg3 = null)
205 | {
206 | $this->where($arg1, $arg2, $arg3, 1);
207 |
208 | return $this;
209 | }
210 |
211 | public function should($input)
212 | {
213 | $should = [];
214 | foreach ($input as $key => $value) {
215 | $should[] = [
216 | is_array($value) ? 'terms' : 'term' => [$key => $value],
217 | ];
218 | }
219 | $this->filterBuildingPoint[] = [
220 | 'bool' => ['should' => $should],
221 | ];
222 |
223 | return $this;
224 | }
225 |
226 | public function must($input)
227 | {
228 | $must = [];
229 | foreach ($input as $key => $value) {
230 | $must[] = [
231 | is_array($value) ? 'terms' : 'term' => [$key => $value],
232 | ];
233 | }
234 | $this->filterBuildingPoint[] = [
235 | 'bool' => ['must' => $must],
236 | ];
237 |
238 | return $this;
239 | }
240 |
241 | public function setFrom($from)
242 | {
243 | $this->args['from'] = max($from, 0);
244 |
245 | return $this;
246 | }
247 |
248 | public function setSize($size)
249 | {
250 | $this->args['size'] = max($size, 0);
251 |
252 | return $this;
253 | }
254 |
255 | public function getArgs()
256 | {
257 | return $this->args;
258 | }
259 | }
260 |
--------------------------------------------------------------------------------
/src/Model/Query/WpConverter.php:
--------------------------------------------------------------------------------
1 |
7 | *
8 | * For the full copyright and license information, please view the LICENSE
9 | * file that was distributed with this source code.
10 | */
11 |
12 | namespace Wallmander\ElasticsearchIndexer\Model\Query;
13 |
14 | use Wallmander\ElasticsearchIndexer\Model\Indexer;
15 | use Wallmander\ElasticsearchIndexer\Model\Query;
16 | use WP_Date_Query;
17 | use Wp_Query;
18 |
19 | /**
20 | * Builds an Elasticsearch query from Wp_Query using the query builder.
21 | *
22 | * @author Mikael Mattsson
23 | */
24 | class WpConverter
25 | {
26 | public static function isCompatible(Wp_Query $wpQuery)
27 | {
28 | $q = $wpQuery->query_vars;
29 |
30 | $unsupportedQueryArgs = [
31 | 'suppress_filters',
32 | 'has_password',
33 | 'post_password',
34 | 'preview',
35 | 'fields',
36 | ];
37 |
38 | foreach ($q as $key => $value) {
39 | if ($value && in_array($key, $unsupportedQueryArgs)) {
40 | return false;
41 | }
42 | }
43 |
44 | if ($q['fields'] == 'ids' || $q['fields'] == 'id=>parent') {
45 | return false;
46 | }
47 |
48 | if (!empty($q['post_status'])) {
49 | if (is_string($q['post_status'])) {
50 | $q['post_status'] = explode(' ', str_replace(',', ' ', $q['post_status']));
51 | }
52 | $ips = Indexer::getIndexablePostStati();
53 | foreach ($q['post_status'] as $value) {
54 | if (!in_array($value, $ips)) {
55 | return false;
56 | }
57 | }
58 | }
59 |
60 | if (!empty($q['post_type']) && $q['post_type'] !== 'any') {
61 | if (is_string($q['post_type'])) {
62 | $q['post_type'] = explode(' ', str_replace(',', ' ', $q['post_type']));
63 | }
64 | $ipt = Indexer::getIndexablePostTypes();
65 | foreach ($q['post_type'] as $value) {
66 | if (!in_array($value, $ipt)) {
67 | return false;
68 | }
69 | }
70 | }
71 |
72 | return true;
73 | }
74 |
75 | public static function formatArgs(Query $query, Wp_Query $wpQuery)
76 | {
77 | // Fill again in case pre_get_posts unset some vars.
78 | $q = $wpQuery->fill_query_vars($wpQuery->query_vars);
79 |
80 | $q = apply_filters('esi_before_format_args', $q, $query);
81 |
82 | if ($wpQuery->is_posts_page) {
83 | $q['pagename'] = '';
84 | }
85 |
86 | // Defaults
87 | if (!empty($q['nopaging'])) {
88 | $q['posts_per_page'] = '-1';
89 | }
90 | if (empty($q['posts_per_page'])) {
91 | $q['posts_per_page'] = get_option('posts_per_page');
92 | }
93 | if (empty($q['post_type'])) {
94 | if (!empty($q['wc_query']) && $q['wc_query'] == 'product_query') {
95 | $q['post_type'] = 'product';
96 | } elseif (!empty($q['tax_query'])) {
97 | $q['post_type'] = 'any';
98 | } else {
99 | $q['post_type'] = 'post';
100 | }
101 | } elseif (is_string($q['post_type'])) {
102 | $q['post_type'] = explode(' ', str_replace(',', ' ', $q['post_type']));
103 | }
104 | if (empty($q['orderby'])) {
105 | $q['orderby'] = 'none';
106 | }
107 | if ($wpQuery->is_search() && $wpQuery->get('s')) {
108 | if ($q['orderby'] == 'none' || $q['orderby'] == 'menu_order title') {
109 | $q['orderby'] = 'relevance';
110 | }
111 | }
112 | if (empty($q['post_status'])) {
113 | $q['post_status'] = ['publish'];
114 | if (is_user_logged_in()) {
115 | $q['post_status'][] = 'private';
116 | }
117 | if (is_admin()) {
118 | $q['post_status'][] = 'future';
119 | $q['post_status'][] = 'draft';
120 | $q['post_status'][] = 'pending';
121 | }
122 | } elseif (is_string($q['post_status'])) {
123 | $q['post_status'] = explode(' ', str_replace(',', ' ', $q['post_status']));
124 | }
125 |
126 | $q = apply_filters('esi_before_query_building', $q, $query);
127 |
128 | // Loop all query arguments
129 | foreach ($q as $key => $value) {
130 | if (!$value) {
131 | continue;
132 | }
133 | $f = 'arg'.str_replace(' ', '', ucwords(str_replace('_', ' ', $key)));
134 | $c = get_class();
135 | if (method_exists($c, $f)) {
136 | $c::$f($query, $q[$key], $q);
137 | }
138 | }
139 |
140 | do_action('esi_after_format_args', $query, $wpQuery);
141 | }
142 |
143 | public static function argPostStatus(Query $query, $value, &$q)
144 | {
145 | if ($value == 'any') {
146 | $ips = Indexer::getIndexablePostStati();
147 | $query->where('post_status', $ips);
148 | } else {
149 | $query->where('post_status', $value);
150 | }
151 | }
152 |
153 | public static function argP(Query $query, $value, &$q)
154 | {
155 | $query->where('post_id', $value);
156 | $query->isSingle = true;
157 | }
158 |
159 | public static function argPostParent(Query $query, $value, &$q)
160 | {
161 | $query->where('post_parent', $value);
162 | }
163 |
164 | /**
165 | * Automatic alias for subpost.
166 | *
167 | * @param $value
168 | * @param $q
169 | */
170 | public static function argAttachment(Query $query, $value, &$q)
171 | {
172 | $q['attachment'] = sanitize_title_for_query(wp_basename($value));
173 | $q['name'] = $q['attachment'];
174 | $query->isSingle = true;
175 | $q['post_type'] = 'attachment';
176 | }
177 |
178 | /**
179 | * Automatic alias for subpost_id.
180 | *
181 | * @param $value
182 | * @param $q
183 | */
184 | public static function argAttachmentId(Query $query, $value, &$q)
185 | {
186 | $query->where('post_id', $value);
187 | $query->isSingle = true;
188 | $q['post_type'] = 'attachment';
189 | }
190 |
191 | public static function argName(Query $query, $value, &$q)
192 | {
193 | $query->where('post_name', $value);
194 | $query->isSingle = true;
195 | }
196 |
197 | public static function argStatic(Query $query, $value, &$q)
198 | {
199 | $query->where('post_type', 'page');
200 | $query->isSingle = false;
201 | }
202 |
203 | public static function argPagename(Query $query, $value, &$q)
204 | {
205 | $query->where('post_name', $value);
206 | $query->isSingle = true;
207 | $q['post_type'] = 'page';
208 | }
209 |
210 | public static function argPageId(Query $query, $value, &$q)
211 | {
212 | $query->where('post_id', $value);
213 | $query->isSingle = true;
214 | $q['post_type'] = 'page';
215 | }
216 |
217 | public static function argSecond(Query $query, $value, &$q)
218 | {
219 | $query->where('post_date_object.second', $value);
220 | }
221 |
222 | public static function argMinute(Query $query, $value, &$q)
223 | {
224 | $query->where('post_date_object.minute', $value);
225 | }
226 |
227 | public static function argHour(Query $query, $value, &$q)
228 | {
229 | $query->where('post_date_object.hour', $value);
230 | }
231 |
232 | public static function argDay(Query $query, $value, &$q)
233 | {
234 | $query->where('post_date_object.day', $value);
235 | }
236 |
237 | public static function argMonthnum(Query $query, $value, &$q)
238 | {
239 | $query->where('post_date_object.month', $value);
240 | }
241 |
242 | public static function argYear(Query $query, $value, &$q)
243 | {
244 | $query->where('post_date_object.year', $value);
245 | }
246 |
247 | public static function argW(Query $query, $value, &$q)
248 | {
249 | $query->where('post_date_object.week', $value);
250 | }
251 |
252 | public static function argM(Query $query, $value, &$q)
253 | {
254 | $query->where('post_date_object.m', $value);
255 | }
256 |
257 | public static function argCategoryName(Query $query, $value, &$q)
258 | {
259 | $query->where('terms.category.slug', $value);
260 | }
261 |
262 | public static function argTag(Query $query, $value, &$q)
263 | {
264 | $query->where('terms.post_tag.slug', $value);
265 | }
266 |
267 | public static function argCat(Query $query, $value, &$q)
268 | {
269 | /*
270 | * See https://codex.wordpress.org/Class_Reference/WP_Query#Category_Parameters
271 | */
272 | $in = [];
273 | $notIn = [];
274 | foreach ($value as $id) {
275 | if ((int) $id < 0) {
276 | $notIn[] = -$id;
277 | } else {
278 | $in[] = $id;
279 | }
280 | }
281 | if ($in) {
282 | $query->where('terms.category.term_id', $in);
283 | }
284 | if ($notIn) {
285 | $query->whereNot('terms.category.term_id', $notIn);
286 | }
287 | }
288 |
289 | public static function argTagId(Query $query, $value, &$q)
290 | {
291 | $query->where('terms.post_tag.term_id', $value);
292 | }
293 |
294 | public static function argAuthor(Query $query, $value, &$q)
295 | {
296 | $query->where('post_author.id', $value);
297 | }
298 |
299 | public static function argAuthorName(Query $query, $value, &$q)
300 | {
301 | $query->where('post_author.raw', $value);
302 | }
303 |
304 | public static function argFeed(Query $query, $value, &$q)
305 | {
306 | //
307 | }
308 |
309 | public static function argTb(Query $query, $value, &$q)
310 | {
311 | //
312 | }
313 |
314 | public static function argPaged(Query $query, $value, &$q)
315 | {
316 | $query->setFrom(($value - 1) * $q['posts_per_page']);
317 | }
318 |
319 | public static function argCommentsPopup(Query $query, $value, &$q)
320 | {
321 | //
322 | }
323 |
324 | public static function argMetaKey(Query $query, $value, &$q)
325 | {
326 | //
327 | }
328 |
329 | public static function argMetaValue(Query $query, $value, &$q)
330 | {
331 | //
332 | }
333 |
334 | public static function argPreview(Query $query, $value, &$q)
335 | {
336 | //
337 | }
338 |
339 | public static function argS(Query $query, $value, &$q)
340 | {
341 | $query->setQuery([
342 | 'bool' => [
343 | 'should' => [
344 | [
345 | 'multi_match' => [
346 | 'fields' => apply_filters('esi_search_fields_multi_match', [
347 | 'post_title^10',
348 | 'terms.*.name^4',
349 | 'post_excerpt^2',
350 | 'post_content',
351 | ]),
352 | 'query' => $value,
353 | ],
354 | ],
355 | [
356 | 'fuzzy_like_this' => [
357 | 'fields' => apply_filters('esi_search_fields_fuzzy', [
358 | 'post_title',
359 | 'post_excerpt',
360 | ]),
361 | 'like_text' => $value,
362 | 'min_similarity' => apply_filters('esi_min_similarity', 0.75),
363 | ],
364 | ],
365 | ],
366 | ],
367 | ]);
368 | }
369 |
370 | public static function argSentence(Query $query, $value, &$q)
371 | {
372 | //
373 | }
374 |
375 | public static function argFields(Query $query, $value, &$q)
376 | {
377 | //
378 | }
379 |
380 | public static function argMenuOrder(Query $query, $value, &$q)
381 | {
382 | $query->where('menu_order', $value);
383 | }
384 |
385 | public static function argCategoryIn(Query $query, $value, &$q)
386 | {
387 | $query->where('terms.category.term_id', array_values($value));
388 | }
389 |
390 | public static function argCategoryNotIn(Query $query, $value, &$q)
391 | {
392 | $query->whereNot('terms.category.term_id', array_values($value));
393 | }
394 |
395 | public static function argCategoryAnd(Query $query, $value, &$q)
396 | {
397 | $query->where('terms.category.term_id', '=', array_values($value));
398 | }
399 |
400 | public static function argPostIn(Query $query, $value, &$q)
401 | {
402 | $query->where('post_id', array_values($value));
403 | }
404 |
405 | public static function argPostNotIn(Query $query, $value, &$q)
406 | {
407 | $query->whereNot('post_id', array_values($value));
408 | }
409 |
410 | public static function argTagIn(Query $query, $value, &$q)
411 | {
412 | $query->where('terms.post_tag.term_id', array_values($value));
413 | }
414 |
415 | public static function argTagNotIn(Query $query, $value, &$q)
416 | {
417 | $query->whereNot('terms.post_tag.term_id', array_values($value));
418 | }
419 |
420 | public static function argTagAnd(Query $query, $value, &$q)
421 | {
422 | $query->where('terms.post_tag.term_id', '=', array_values($value));
423 | }
424 |
425 | public static function argTagSlugIn(Query $query, $value, &$q)
426 | {
427 | $query->where('terms.post_tag.slug', array_values($value));
428 | }
429 |
430 | public static function argTagSlugAnd(Query $query, $value, &$q)
431 | {
432 | $query->where('terms.post_tag.slug', '=', array_values($value));
433 | }
434 |
435 | public static function argPostParentIn(Query $query, $value, &$q)
436 | {
437 | $query->where('post_parent', array_values($value));
438 | }
439 |
440 | public static function argPostParentNotIn(Query $query, $value, &$q)
441 | {
442 | $query->whereNot('post_parent', array_values($value));
443 | }
444 |
445 | public static function argAuthorIn(Query $query, $value, &$q)
446 | {
447 | $query->where('post_author.id', array_values($value));
448 | }
449 |
450 | public static function argAuthorNotIn(Query $query, $value, &$q)
451 | {
452 | $query->whereNot('post_author.id', array_values($value));
453 | }
454 |
455 | public static function argCacheResults(Query $query, $value, &$q)
456 | {
457 | //
458 | }
459 |
460 | public static function argIgnoreStickyPosts(Query $query, $value, &$q)
461 | {
462 | //
463 | }
464 |
465 | public static function argSuppressFilters(Query $query, $value, &$q)
466 | {
467 | //
468 | }
469 |
470 | public static function argUpdatePostTermCache(Query $query, $value, &$q)
471 | {
472 | $query->updatePostTermCache = (bool) $value;
473 | }
474 |
475 | public static function argUpdatePostMetaCache(Query $query, $value, &$q)
476 | {
477 | $query->updatePostMetaCache = (bool) $value;
478 | }
479 |
480 | public static function argPostType(Query $query, $value, &$q)
481 | {
482 | if ($value == 'any' || isset($value[0]) && $value[0] == 'any') {
483 | $pt = Indexer::getSearchablePostTypes();
484 | $query->where('post_type', array_values($pt));
485 | } else {
486 | $query->where('post_type', $value);
487 | }
488 | }
489 |
490 | public static function argPostsPerPage(Query $query, $value, &$q)
491 | {
492 | if ($query->isSingle) {
493 | $query->setSize(1);
494 | } elseif ($value == '-1') {
495 | $query->setSize(10000000);
496 | } else {
497 | $query->setSize((int) $value);
498 | }
499 | }
500 |
501 | public static function argNopaging(Query $query, $value, &$q)
502 | {
503 | $query->setSize(10000000);
504 | }
505 |
506 | public static function argCommentsPerPage(Query $query, $value, &$q)
507 | {
508 | //
509 | }
510 |
511 | public static function argNoFoundRows(Query $query, $value, &$q)
512 | {
513 | //
514 | }
515 |
516 | public static function argOrderby(Query $query, $value, &$q)
517 | {
518 | if ($query->isSingle) {
519 | return;
520 | }
521 |
522 | $o = !empty($q['order']) ? strtolower($q['order']) : 'desc';
523 | foreach (explode(' ', $value) as $key) {
524 | $key = str_replace('wp_posts.', '', $key);
525 | switch ($key) {
526 | case 'ID':
527 | $query->addSort('post_id', $o);
528 | break;
529 |
530 | case 'post_author':
531 | case 'author':
532 | $query->addSort('post_author.id', $o);
533 | break;
534 |
535 | case 'post_title':
536 | case 'title':
537 | $query->addSort('post_title.raw', $o);
538 | break;
539 |
540 | case 'post_name':
541 | case 'post_type':
542 | case 'post_date':
543 | case 'post_modified':
544 | case 'post_parent':
545 | case 'post_comment_count':
546 | case 'menu_order':
547 | $query->addSort($key, $o);
548 | break;
549 |
550 | case 'rand':
551 | // not supported
552 | break;
553 |
554 | case 'meta_value_num':
555 | $query->addSort('post_meta_num.'.$q['meta_key'], $o);
556 | break;
557 |
558 | case 'meta_value':
559 | $query->addSort('post_meta.'.$q['meta_key'], $o);
560 | break;
561 |
562 | case 'relevance':
563 | $query->addSort([
564 | '_score' => 'desc',
565 | 'menu_order' => 'asc',
566 | 'post_title' => 'asc',
567 | ]);
568 | break;
569 |
570 | case 'none':
571 | case '':
572 | $query->addSort('post_date', 'desc');
573 | break;
574 |
575 | default:
576 | $query->addSort('post_'.$key, $o);
577 | break;
578 | }
579 | }
580 | }
581 |
582 | public static function argTaxonomy(Query $query, $value, &$q)
583 | {
584 | if (!empty($q['tax_query'])) {
585 | return;
586 | }
587 | if (!empty($q['term'])) {
588 | $query->should([
589 | 'terms.'.$value.'.all_slugs' => $q['term'],
590 | ]
591 | );
592 | } elseif (!empty($q['term_id'])) {
593 | $query->should([
594 | 'terms.'.$value.'.term_id' => $q['term_id'],
595 | 'terms.'.$value.'.parent' => $q['term_id'],
596 | ]
597 | );
598 | }
599 | }
600 |
601 | public static function argTaxQuery(Query $query, $value, &$q)
602 | {
603 | $currentValue = $value;
604 | $function = function (Query $query) use (&$currentValue, &$function) {
605 | foreach ($currentValue as $key => $tax) {
606 | if ($key === 'relation') {
607 | continue;
608 | }
609 | if (!isset($tax[0]) || !is_array($tax[0])) {
610 | // not nested
611 | $include_children = !isset($tax['include_children']) || $tax['include_children'] != false;
612 | $compare = empty($tax['operator']) ? 'in' : $tax['operator'];
613 | $terms = $tax['terms'];
614 | if (is_string($terms)) {
615 | if (strpos($terms, '+') !== false) {
616 | $terms = preg_split('/[+]+/', $terms);
617 | } else {
618 | $terms = preg_split('/[,]+/', $terms);
619 | }
620 | }
621 | switch ($tax['field']) {
622 | case 'term_id' :
623 | $query->where("terms.$tax[taxonomy].term_id", $compare, $terms);
624 | if ($include_children) {
625 | $query->where("terms.$tax[taxonomy].parent", $compare, $terms);
626 | }
627 | break;
628 | case 'slug' :
629 | if ($include_children) {
630 | $query->where("terms.$tax[taxonomy].all_slugs", $compare, $terms);
631 | } else {
632 | $query->where("terms.$tax[taxonomy].slug", $compare, $terms);
633 | }
634 | break;
635 | case 'name' :
636 | // plugin exclusive feature.
637 | $query->where("terms.$tax[taxonomy].name", $compare, $terms);
638 | break;
639 | }
640 | } else {
641 | // nested
642 | $currentValue = $tax;
643 | $query->bool($function, !empty($currentValue['relation']) ? $currentValue['relation'] : 'and');
644 | }
645 | }
646 | };
647 |
648 | $query->bool($function, !empty($value['relation']) ? $value['relation'] : 'and');
649 | }
650 |
651 | public static function argMetaQuery(Query $query, $value, &$q)
652 | {
653 | $currentValue = $value;
654 | $function = function (Query $query) use (&$currentValue, &$function) {
655 | foreach ($currentValue as $key => $mq) {
656 | if ($key === 'relation') {
657 | continue;
658 | }
659 | if (!isset($mq[0]) || !is_array($mq[0])) {
660 | // not nested
661 |
662 | if (empty($mq['compare']) || $mq['compare'] == '=') {
663 | $compare = 'in'; // ”=” is handled as ”in” in meta query
664 | } else {
665 | $compare = $mq['compare'];
666 | }
667 |
668 | $value = $mq['value'];
669 | $type = (!empty($mq['type'])) ? strtolower($mq['type']) : 'char';
670 |
671 | switch ($type) {
672 | case 'numeric' :
673 | $query->where("post_meta_num.$mq[key]", $compare, $value);
674 | break;
675 | case 'char' :
676 | default :
677 | $query->where("post_meta.$mq[key].raw", $compare, $value);
678 | break;
679 | }
680 | } else {
681 | // nested
682 | $currentValue = $mq;
683 | $query->bool($function, !empty($currentValue['relation']) ? $currentValue['relation'] : 'and');
684 | }
685 | }
686 | };
687 |
688 | $query->bool($function, !empty($value['relation']) ? $value['relation'] : 'and');
689 | }
690 |
691 | public static function argDateQuery(Query $query, $value, &$q)
692 | {
693 | $query->bool(function (Query $query) use ($value, $q) {
694 | foreach ($value as $dq) {
695 | $column = !empty($dq['column']) ? $dq['column'] : 'post_date';
696 | $inclusive = !empty($dq['inclusive']);
697 | foreach ($dq as $key => $value) {
698 | switch ($key) {
699 | case 'before':
700 | $date = static::buildDatetime($value, $inclusive);
701 | $comparator = 'lt';
702 | if ($inclusive) {
703 | $comparator .= 'e';
704 | }
705 | $query->where($column, $comparator, $date);
706 | break;
707 | case 'after':
708 | $date = static::buildDatetime($value, !$inclusive);
709 | $comparator = 'gt';
710 | if ($inclusive) {
711 | $comparator .= 'e';
712 | }
713 | $query->where($column, $comparator, $date);
714 | break;
715 | case 'week' :
716 | case 'w' :
717 | $query->where($column.'_object.week', $value);
718 | break;
719 | case 'year' :
720 | case 'month' :
721 | case 'dayofyear' :
722 | case 'day' :
723 | case 'dayofweek' :
724 | case 'dayofweek_iso' :
725 | case 'hour' :
726 | case 'minute' :
727 | case 'second' :
728 | $query->where($column.'_object.'.$key, $value);
729 | break;
730 | }
731 | }
732 | }
733 | }, !empty($value['relation']) ? $value['relation'] : 'and');
734 | }
735 |
736 | public static function buildDatetime($date, $inclusive = false)
737 | {
738 | $wpDateQuery = new WP_Date_Query([]);
739 |
740 | return $wpDateQuery->build_mysql_datetime($date, $inclusive);
741 | }
742 | }
743 |
--------------------------------------------------------------------------------
/src/Service/Elasticsearch.php:
--------------------------------------------------------------------------------
1 |
7 | *
8 | * For the full copyright and license information, please view the LICENSE
9 | * file that was distributed with this source code.
10 | */
11 |
12 | namespace Wallmander\ElasticsearchIndexer\Service;
13 |
14 | use Exception;
15 | use Guzzle\Http\Client as HttpClient;
16 | use Guzzle\Http\Exception\RequestException;
17 | use Wallmander\ElasticsearchIndexer\Model\Client;
18 | use Wallmander\ElasticsearchIndexer\Model\Config;
19 |
20 | /**
21 | * Service layer for Elasticsearch.
22 | *
23 | * @author Mikael Mattsson
24 | */
25 | class Elasticsearch
26 | {
27 | protected static $isAvailable = null;
28 |
29 | protected static $errorMessage = '';
30 |
31 | /**
32 | * Send a simple get request to the Elasticsearch server.
33 | *
34 | * @param $uri
35 | *
36 | * @return \Guzzle\Http\Message\Response
37 | */
38 | public static function httpGet($uri)
39 | {
40 | $client = new HttpClient();
41 | $host = Config::getFirstHost();
42 |
43 | return $client->get($host.'/'.$uri)->send();
44 | }
45 |
46 | /**
47 | * Send a simple post request to the Elasticsearch server.
48 | *
49 | * @param $uri
50 | * @param array $data
51 | *
52 | * @return \Guzzle\Http\Message\Response
53 | */
54 | public static function httpPost($uri, $data = null)
55 | {
56 | $client = new HttpClient();
57 | $host = Config::getFirstHost();
58 | if ($data) {
59 | $data = json_encode($data);
60 | }
61 |
62 | return $client->post($host.'/'.$uri, null, $data)->send();
63 | }
64 |
65 | /**
66 | * Send a simple put request to the Elasticsearch server.
67 | *
68 | * @param $uri
69 | * @param array $data
70 | *
71 | * @return \Guzzle\Http\Message\Response
72 | */
73 | public static function httpPut($uri, $data = null)
74 | {
75 | $client = new HttpClient();
76 | $host = Config::getFirstHost();
77 | if ($data) {
78 | $data = json_encode($data);
79 | }
80 |
81 | return $client->put($host.'/'.$uri, null, $data)->send();
82 | }
83 |
84 | /**
85 | * Check if Elasticsearch is running and the index exists.
86 | *
87 | * @param $index
88 | *
89 | * @return bool|\Guzzle\Http\EntityBodyInterface|string
90 | */
91 | public static function indicesExists($index)
92 | {
93 | try {
94 | return static::httpGet($index)->getBody();
95 | } catch (RequestException $e) {
96 | return false;
97 | }
98 | }
99 |
100 | /**
101 | * Get a neat list of all indexes as a single string.
102 | *
103 | * @return \Guzzle\Http\EntityBodyInterface|string
104 | */
105 | public static function getIndices()
106 | {
107 | try {
108 | return static::httpGet('_cat/indices?v')->getBody();
109 | } catch (RequestException $e) {
110 | return $e->getRequest()->getResponse();
111 | }
112 | }
113 |
114 | /**
115 | * Get Elasticsearch status.
116 | *
117 | * @return bool|\Guzzle\Http\EntityBodyInterface|string
118 | */
119 | public static function getStatus()
120 | {
121 | try {
122 | return static::httpGet('_status')->getBody();
123 | } catch (RequestException $e) {
124 | return false;
125 | }
126 | }
127 |
128 | /**
129 | * @param $index
130 | * @param array|object $data
131 | *
132 | * @return bool|\Guzzle\Http\EntityBodyInterface|string
133 | */
134 | public static function setSettings($index, $data)
135 | {
136 | try {
137 | return static::httpPut($index.'/_settings', $data)->getBody();
138 | } catch (RequestException $e) {
139 | echo $e->getRequest()->getResponse();
140 |
141 | return false;
142 | }
143 | }
144 |
145 | /**
146 | * Optimize the index for searches.
147 | *
148 | * @param $index
149 | *
150 | * @return bool|\Guzzle\Http\EntityBodyInterface|string
151 | */
152 | public static function optimize($index = null)
153 | {
154 | try {
155 | if ($index) {
156 | return static::httpPost($index.'/_optimize')->getBody();
157 | }
158 |
159 | return static::httpPost('_optimize')->getBody();
160 | } catch (RequestException $e) {
161 | return false;
162 | }
163 | }
164 |
165 | /**
166 | * Ping a server and get the status and time.
167 | *
168 | * @param string $host
169 | *
170 | * @return array
171 | */
172 | public static function ping($host)
173 | {
174 | $start = microtime(true);
175 | $message = 'OK';
176 | $success = true;
177 | try {
178 | $client = new HttpClient();
179 | $client->get($host)->send();
180 | } catch (Exception $e) {
181 | $message = $e->getMessage();
182 | $success = false;
183 | }
184 | $end = microtime(true);
185 |
186 | return [
187 | 'status' => $message,
188 | 'time' => ($end - $start) * 1000,
189 | 'success' => $success,
190 | ];
191 | }
192 |
193 | /**
194 | * Evaluate if the we can search for posts.
195 | *
196 | * @return bool
197 | */
198 | public static function isAvailable()
199 | {
200 | if (static::$isAvailable !== null) {
201 | return static::$isAvailable;
202 | }
203 |
204 | $client = new Client();
205 |
206 | try {
207 | static::$isAvailable = (bool) self::indicesExists($client->getIndexName());
208 | } catch (Exception $e) {
209 | static::$errorMessage = $e->getMessage();
210 | static::$isAvailable = false;
211 | }
212 |
213 | return static::$isAvailable;
214 | }
215 |
216 | /**
217 | * Returns a latest saved status message if any.
218 | *
219 | * @return string
220 | */
221 | public static function getErrorMessage()
222 | {
223 | return self::$errorMessage;
224 | }
225 | }
226 |
--------------------------------------------------------------------------------
/src/Service/WordPress.php:
--------------------------------------------------------------------------------
1 |
7 | *
8 | * For the full copyright and license information, please view the LICENSE
9 | * file that was distributed with this source code.
10 | */
11 |
12 | namespace Wallmander\ElasticsearchIndexer\Service;
13 |
14 | /**
15 | * Service layer for WordPress.
16 | *
17 | * @author Mikael Mattsson
18 | */
19 | class WordPress
20 | {
21 | /**
22 | * Get a list of wordpress site ids.
23 | *
24 | * @return \Guzzle\Http\Message\Response
25 | */
26 | public static function getSites()
27 | {
28 | if (is_multisite() && $sites = wp_get_sites()) {
29 | $blogIDs = [];
30 | foreach ($sites as $value) {
31 | $blogIDs[] = $value['blog_id'];
32 | }
33 |
34 | return $blogIDs;
35 | }
36 |
37 | return [get_current_blog_id()];
38 | }
39 |
40 | /**
41 | * Wrapper for switch_to_blog.
42 | *
43 | * @param int $id
44 | */
45 | public static function switchToBlog($id)
46 | {
47 | if (is_multisite()) {
48 | switch_to_blog($id);
49 | }
50 | }
51 |
52 | /**
53 | * Wrapper for restore_current_blog.
54 | */
55 | public static function restoreCurrentBlog()
56 | {
57 | if (is_multisite()) {
58 | restore_current_blog();
59 | }
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/views/admin/index.php:
--------------------------------------------------------------------------------
1 |
2 |
Elasticsearch Indexer Dashboard
3 |
9 |
10 |
--------------------------------------------------------------------------------
/views/admin/settings.php:
--------------------------------------------------------------------------------
1 |
6 |
7 |
Elasticsearch Indexer Settings
8 |
9 |
Remember to reindex posts after changing options
10 |
11 |
159 |
160 |
--------------------------------------------------------------------------------
/views/admin/status.php:
--------------------------------------------------------------------------------
1 |
2 |
Elasticsearch Status
3 |
4 |
5 |
Logs
6 |
7 |
8 |
--------------------------------------------------------------------------------
/views/profiler/footer.php:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Total MySQL Queries |
6 | Total MySQL Time |
7 | Total Elasticsearch Queries |
8 |
9 |
10 |
11 |
12 |
13 |
14 | |
15 |
16 | ms
17 | |
18 |
19 |
20 | |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 | |
31 |
32 | ms
33 | |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 | |
45 |
46 |
47 | |
48 |
49 |
50 |
51 |
52 |
53 |
--------------------------------------------------------------------------------