├── .gitignore
├── .travis.yml
├── README.md
├── adapters
├── jetpack-search.php
├── searchpress.php
├── travis.php
└── vip-search.php
├── bin
├── install-es.sh
└── install-wp-tests.sh
├── class-es-wp-date-query.php
├── class-es-wp-meta-query.php
├── class-es-wp-query-shoehorn.php
├── class-es-wp-query-wrapper.php
├── class-es-wp-tax-query.php
├── composer.json
├── es-wp-query.php
├── functions.php
├── multisite.xml
├── phpcs.xml.dist
├── phpunit.xml.dist
└── tests
├── bootstrap.php
└── query
├── author.php
├── date.php
├── dateQuery.php
├── loggedIn.php
├── metaQuery.php
├── post.php
├── query.php
├── results.php
├── shoehorn.php
└── taxQuery.php
/.gitignore:
--------------------------------------------------------------------------------
1 | /tests/es.php
2 | wpcom-helper.php
3 | .DS_Store
4 | /report/
5 | .idea
6 | .vscode
7 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | sudo: false
2 |
3 | # Xenial does not start mysql by default
4 | services:
5 | - mysql
6 |
7 | language: php
8 |
9 | notifications:
10 | email:
11 | on_success: never
12 | on_failure: change
13 |
14 | branches:
15 | only:
16 | - master
17 | - master-1.x
18 |
19 | cache:
20 | directories:
21 | - $HOME/.composer/cache
22 | - $HOME/.config/composer/cache
23 |
24 | matrix:
25 | include:
26 | - php: 5.6
27 | env: WP_VERSION=latest PHP_LINT=1 ES_VERSION=2.4.6
28 | dist: trusty
29 | - php: 7.3
30 | env: WP_VERSION=latest WP_PHPCS=1 ES_VERSION=5.6.16
31 | dist: xenial
32 | - php: 7.3
33 | env: WP_VERSION=latest PHP_LINT=1 ES_VERSION=6.8.8
34 | dist: xenial
35 | - php: 7.4
36 | env: WP_VERSION=nightly PHP_LINT=1 ES_VERSION=7.6.2
37 | dist: xenial
38 | fast_finish: true
39 |
40 | install:
41 | - bash bin/install-es.sh $ES_VERSION
42 |
43 | before_script:
44 | - export PATH="$HOME/.config/composer/vendor/bin:$HOME/.composer/vendor/bin:$PATH"
45 |
46 | # Turn off Xdebug. See https://core.trac.wordpress.org/changeset/40138.
47 | - phpenv config-rm xdebug.ini || echo "Xdebug not available"
48 |
49 | # Couple the PHPUnit version to the PHP version.
50 | - |
51 | case "$TRAVIS_PHP_VERSION" in
52 | 5.6)
53 | echo "Using PHPUnit 4.8"
54 | composer global require "phpunit/phpunit=4.8.*"
55 | ;;
56 | *)
57 | echo "Using PHPUnit 6.1"
58 | composer global require "phpunit/phpunit=6.1.*"
59 | ;;
60 | esac
61 |
62 | - |
63 | if [[ ! -z "$WP_VERSION" ]] ; then
64 | bash bin/install-wp-tests.sh wordpress_test root '' localhost $WP_VERSION
65 | fi
66 |
67 | - |
68 | if [[ "$WP_PHPCS" == "1" ]]; then
69 | composer global require automattic/vipwpcs
70 | phpcs --config-set installed_paths $HOME/.composer/vendor/wp-coding-standards/wpcs,$HOME/.composer/vendor/automattic/vipwpcs
71 | fi
72 |
73 | # Wait up to 60 seconds until ES is up, or die if it never comes up.
74 | - |
75 | failures=0
76 | curl localhost:9200;
77 | while [[ $? -ne 0 && $failures -lt 60 ]]; do
78 | sleep 1
79 | ((failures++))
80 | curl localhost:9200
81 | done
82 |
83 | if [ $? -ne 0 ]; then
84 | echo "Elasticsearch is unavailable."
85 | cat /tmp/elasticsearch.log
86 | exit 1
87 | fi
88 | - phpunit --version
89 |
90 |
91 | script:
92 | - if [[ "$PHP_LINT" == "1" ]]; then find . -type "f" -iname "*.php" | xargs -L "1" php -l; fi
93 | - if [[ "$WP_PHPCS" == "1" ]]; then phpcs; fi
94 | - phpunit
95 | - phpunit -c multisite.xml
96 |
97 | after_script:
98 | - cat /tmp/elasticsearch.log
99 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |

2 |
3 | # Elasticsearch Wrapper for WP_Query
4 |
5 | A drop-in replacement for WP_Query to leverage Elasticsearch for complex queries.
6 |
7 | ## Warning!
8 |
9 | This plugin is currently in beta development, and as such, no part of it is guaranteed. It works (the unit tests prove that), but we won't be concerned about backwards compatibility until the first release. If you choose to use this, please pay close attention to the commit log to make sure we don't break anything you've implemented.
10 |
11 |
12 | ## Instructions for use
13 |
14 | This is actually more of a library than it is a plugin. With that, it is plugin-agnostic with regards to how you're connecting to Elasticsearch. It therefore generates Elasticsearch DSL, but does not actually connect to an Elasticsearch server to execute these queries. It also does no indexing of data, it doesn't add a mapping, etc. If you need an Elasticsearch WordPress plugin, we also offer a free and open-source option called [SearchPress](https://github.com/alleyinteractive/searchpress).
15 |
16 | Once you have your Elasticsearch plugin setup and you have your data indexed, you need to tell this library how to use it. If the implementation you're using has an included adapter, you can load it like so:
17 |
18 | es_wp_query_load_adapter( 'adapter-name' );
19 |
20 |
21 | If your Elasticsearch implementation doesn't have an included adapter, you need to create a class called `ES_WP_Query` which extends `ES_WP_Query_Wrapper`. That class should, at the least, have a method `query_es()` which executes the query on the Elasticsearch server. Here's an example:
22 |
23 | class ES_WP_Query extends ES_WP_Query_Wrapper {
24 | protected function query_es( $es_args ) {
25 | return wp_remote_post( 'http://localhost:9200/wordpress/post/_search', array( 'body' => json_encode( $es_args ) ) );
26 | }
27 | }
28 |
29 | See the [included adapters](https://github.com/alleyinteractive/es-wp-query/tree/master/adapters) for examples and inspiration.
30 |
31 |
32 | Once you have an adapter setup, there are two ways you can use this library.
33 |
34 | The first, and preferred, way to use this library is to instantiate `ES_WP_Query` instead of `WP_Query`. For instance:
35 |
36 | $q = new ES_WP_Query( array( 'post_type' => 'event', 'posts_per_page' => 20 ) );
37 |
38 | This will guarantee that your query will be run using Elasticsearch (assuming that the request can and should use Elasticsearch) and you should have no conflicts with themes or plugins. The resulting object (`$q` in this example) works just like WP_Query outside of how it gets the posts.
39 |
40 | The second way to use this library is to add `'es' => true` to your WP_Query arguments. Here's an example:
41 |
42 | $q = new WP_Query( array( 'post_type' => 'event', 'posts_per_page' => 20, 'es' => true ) );
43 |
44 | In one regard, this is a safer way to use this library, because it will fall back on good 'ole `WP_Query` if the library ever goes missing. However, because it depends on the normal processing of WP_Query, it's possible for a plugin or theme to create conflicts, where that plugin or theme is trying to modify WP_Query through one of its provided filters (see below for additional details). In that regard, this can be a very unsafe way to use this library.
45 |
46 | Regardless of which way you use the library, everything else about the object should work as per usual.
47 |
48 | ## Differences with WP_Query and Unsupported Features
49 |
50 | ### Meta Queries
51 |
52 | * **Regexp comparisons are not supported.** The regular expression syntax is slightly different in Elasticsearch vs. PHP, so even if we tried to support them, it would result in a lot of unexpected behaviors. Furthermore, regular expressions are very resource-intensive in Elasticsearch, so you're probably better off just using WP_Query for these queries regardless.
53 | * If you try to use a regexp query, ES_WP_Query will throw a `_doing_it_wrong()` notice.
54 | * **LIKE comparisons are incongruous with MySQL.** In ES_WP_Query, LIKE-comparison meta queries will run a `match` query against the analyzed meta values. This will behave similar to a keyword search and will generally be more useful than a LIKE query in MySQL. However, there are notably differences with the MySQL implementation and ES_WP_Query will very likely produce different search results, so don't expect it to be a drop-in replacement.
55 |
56 |
57 | ## A note about WP_Query filters
58 |
59 | Since this library removes MySQL from most of the equation, the typical WP_Query filters (`posts_where`, `posts_join`, etc.) become irrelevant or -- in some extreme situations -- conflicting.
60 |
61 | The gist of what happens whn you use `WP_Query( 'es=true' )` is that on `pre_get_posts`, the query vars are sent to a new instance of `ES_WP_Query`. The query vars are then replaced with a simple `post__in` query using the IDs which Elasticsearch found. Because the generated SQL query is far simpler than the query vars would suggest, a plugin or theme might try to manipualte the SQL and break it.
62 |
63 | | Action/Filter | Using `ES_WP_Query` | `ES_WP_Query` Equivalent | Using `WP_Query` with `'es' => true` |
64 | | -------------------------- | ------------------- | --------------------------------------------------- | ------------------------------------ |
65 | | `pre_get_posts` | No issues | `es_pre_get_posts` | Potential conflicts |
66 | | `posts_search` | N/A | `es_posts_search` | Should be N/A |
67 | | `posts_search_orderby` | N/A | `es_posts_search_orderby` | Should be N/A |
68 | | `posts_where` | N/A | `es_query_filter` | Potential conflicts |
69 | | `posts_join` | N/A | | Potential conflicts |
70 | | `comment_feed_join` | N/A | | Potential conflicts |
71 | | `comment_feed_where` | N/A | | Potential conflicts |
72 | | `comment_feed_groupby` | N/A | | Potential conflicts |
73 | | `comment_feed_orderby` | N/A | | Potential conflicts |
74 | | `comment_feed_limits` | N/A | | Potential conflicts |
75 | | `posts_where_paged` | N/A | `es_posts_filter_paged`, `es_posts_query_paged` | Potential conflicts |
76 | | `posts_groupby` | N/A | | Potential conflicts |
77 | | `posts_join_paged` | N/A | | Potential conflicts |
78 | | `posts_orderby` | N/A | `es_posts_sort` | Potential conflicts |
79 | | `posts_distinct` | N/A | | Potential conflicts |
80 | | `post_limits` | N/A | `es_posts_size`, `es_posts_from` | Potential conflicts |
81 | | `posts_fields` | N/A | `es_posts_fields` | No issues |
82 | | `posts_clauses` | N/A | `es_posts_clauses` | Potential conflicts |
83 | | `posts_selection` | N/A | `es_posts_selection` | Potential conflicts |
84 | | `posts_where_request` | N/A | `es_posts_filter_request`, `es_posts_query_request` | Potential conflicts |
85 | | `posts_groupby_request` | N/A | | Potential conflicts |
86 | | `posts_join_request` | N/A | | Potential conflicts |
87 | | `posts_orderby_request` | N/A | `es_posts_sort_request` | Potential conflicts |
88 | | `posts_distinct_request` | N/A | | Potential conflicts |
89 | | `posts_fields_request` | N/A | `es_posts_fields_request` | No issues |
90 | | `post_limits_request` | N/A | `es_posts_size_request`, `es_posts_from_request` | Potential conflicts |
91 | | `posts_clauses_request` | N/A | `es_posts_clauses_request` | Potential conflicts |
92 | | `posts_request` | N/A | `es_posts_request` | Potential conflicts |
93 | | `split_the_query` | N/A | | Potential conflicts |
94 | | `posts_request_ids` | N/A | | Potential conflicts |
95 | | `posts_results` | N/A | `es_posts_results` | No issues |
96 | | `comment_feed_join` | N/A | | Potential conflicts |
97 | | `comment_feed_where` | N/A | | Potential conflicts |
98 | | `comment_feed_groupby` | N/A | | Potential conflicts |
99 | | `comment_feed_orderby` | N/A | | Potential conflicts |
100 | | `comment_feed_limits` | N/A | | Potential conflicts |
101 | | `the_preview` | N/A | `es_the_preview` | Potential conflicts |
102 | | `the_posts` | N/A | `es_the_posts` | No issues |
103 | | `found_posts_query` | N/A | | Potential conflicts |
104 | | `found_posts` | N/A | `es_found_posts` | Potential conflicts |
105 | | `wp_search_stopwords` | N/A | | N/A |
106 | | `get_meta_sql` | N/A | `get_meta_dsl` | N/A |
107 | | `date_query_valid_columns` | No issues | | No issues |
108 | | `get_date_sql` | N/A | `get_date_dsl` | N/A |
109 |
110 | Note that in the "Using `WP_Query` with `'es' => true`" column, "no issues" and "N/A" are not guaranteed. For instance, in almost every filter, the `WP_Query` object is passed by reference. If a plugin or theme modified that object, it could create a conflict. The "no issues" and "N/A" notes assume that filters are being used as intended. Lastly, everything is dependant on `pre_get_posts`. If a plugin or theme were to hook in at a priority > 1000, it could render everything a potential conflict.
111 |
112 | ## Contributing
113 |
114 | Any help on this plugin is welcome and appreciated!
115 |
116 | ### Bugs
117 |
118 | If you find a bug, [check the current issues](https://github.com/alleyinteractive/es-wp-query/issues) and if your bug isn't listed, [file a new one](https://github.com/alleyinteractive/es-wp-query/issues/new). If you'd like to also fix the bug you found, please indicate that in the issue before working on it (just in case we have other plans which might affect that bug, we don't want you to waste any time).
119 |
120 | ### Feature Requests
121 |
122 | The scope of this plugin is very tight; it should cover as much of WP_Query as possible, and nothing more. If you think this is missing something within that scope, or you think some part of it can be improved, [we'd love to hear about it](https://github.com/alleyinteractive/es-wp-query/issues/new)!
123 |
124 |
125 | ## Unit Tests
126 |
127 | Unit tests are included using phpunit. In order to run the tests, you need to add an adapter for your Elasticsearch implementation.
128 |
129 | 1. You need to create a file called `es.php` and add it to the `tests/` directory.
130 | 2. `es.php` can simply load one of the included adapters which is setup for testing. Otherwise, you'll need to do some additional setup.
131 | 3. If you're not using one of the provided adapters:
132 | * `es.php` needs to contain or include a function named `es_wp_query_index_test_data()`. This function gets called whenever data is added, to give you an opportunity to index it. You should force Elasticsearch to refresh after indexing, to ensure that the data is immediately searchable.
133 | * **NOTE: Even with refreshing, I've noticed that probably <0.1% of the time, a test may fail for no reason, and I think this is related. If a test sporadically and unexpectedly fails for you, you should re-run it to double-check.**
134 | * `es.php` must also contain or include a class `ES_WP_Query` which extends `ES_WP_Query_Wrapper`. At a minimum, this class should contain a `protected function query_es( $es_args )` which queries your Elasticsearch server.
135 | * This file can also contain anything else you need to get everything working properly, e.g. adjustments to the field map.
136 | * See the included adapters, especially `travis.php`, for examples.
137 |
138 |
--------------------------------------------------------------------------------
/adapters/jetpack-search.php:
--------------------------------------------------------------------------------
1 | search( $es_args );
34 | }
35 | }
36 |
37 | /**
38 | * Sets the posts array to the list of found post IDs.
39 | *
40 | * @param array $q Query arguments.
41 | * @param array|WP_Error $es_response Response from the Elasticsearch server.
42 | * @access protected
43 | */
44 | protected function set_posts( $q, $es_response ) {
45 | $this->posts = array();
46 | if ( ! is_wp_error( $es_response ) && isset( $es_response['results']['hits'] ) ) {
47 | switch ( $q['fields'] ) {
48 | case 'ids':
49 | foreach ( $es_response['results']['hits'] as $hit ) {
50 | $post_id = (array) $hit['fields'][ $this->es_map( 'post_id' ) ];
51 | $this->posts[] = reset( $post_id );
52 | }
53 | return;
54 |
55 | case 'id=>parent':
56 | foreach ( $es_response['results']['hits'] as $hit ) {
57 | $post_id = (array) $hit['fields'][ $this->es_map( 'post_id' ) ];
58 | $post_parent = (array) $hit['fields'][ $this->es_map( 'post_parent' ) ];
59 | $this->posts[ reset( $post_id ) ] = reset( $post_parent );
60 | }
61 | return;
62 |
63 | default:
64 | if ( apply_filters( 'es_query_use_source', false ) ) {
65 | $this->posts = wp_list_pluck( $es_response['results']['hits'], '_source' );
66 | return;
67 | } else {
68 | $post_ids = array();
69 | foreach ( $es_response['results']['hits'] as $hit ) {
70 | $post_id = (array) $hit['fields'][ $this->es_map( 'post_id' ) ];
71 | $post_ids[] = absint( reset( $post_id ) );
72 | }
73 | $post_ids = array_filter( $post_ids );
74 | if ( ! empty( $post_ids ) ) {
75 | global $wpdb;
76 | $post__in = implode( ',', $post_ids );
77 | $this->posts = $wpdb->get_results( "SELECT $wpdb->posts.* FROM $wpdb->posts WHERE ID IN ($post__in) ORDER BY FIELD( {$wpdb->posts}.ID, $post__in )" ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, WordPress.VIP.DirectDatabaseQuery.NoCaching, WordPress.VIP.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.DirectDatabaseQuery.DirectQuery
78 | }
79 | return;
80 | }
81 | }
82 | } else {
83 | $this->posts = array();
84 | }
85 | }
86 |
87 | /**
88 | * Set up the amount of found posts and the number of pages (if limit clause was used)
89 | * for the current query.
90 | *
91 | * @param array $q Query arguments.
92 | * @param array|WP_Error $es_response The response from the Elasticsearch server.
93 | * @access public
94 | */
95 | public function set_found_posts( $q, $es_response ) {
96 | if ( ! is_wp_error( $es_response ) && isset( $es_response['results']['total'] ) ) {
97 | $this->found_posts = absint( $es_response['results']['total'] );
98 | } else {
99 | $this->found_posts = 0;
100 | }
101 | $this->found_posts = apply_filters_ref_array( 'es_found_posts', array( $this->found_posts, &$this ) );
102 | $this->max_num_pages = ceil( $this->found_posts / $q['posts_per_page'] );
103 | }
104 | }
105 |
106 | /**
107 | * Maps Elasticsearch DSL keys to their VIP-specific naming conventions.
108 | *
109 | * @param array $es_map Additional fields to map.
110 | * @return array The final field mapping.
111 | */
112 | function vip_es_field_map( $es_map ) {
113 | return wp_parse_args(
114 | array(
115 | 'post_author' => 'author_id',
116 | 'post_author.user_nicename' => 'author_login',
117 | 'post_date' => 'date',
118 | 'post_date.year' => 'date_token.year',
119 | 'post_date.month' => 'date_token.month',
120 | 'post_date.week' => 'date_token.week',
121 | 'post_date.day' => 'date_token.day',
122 | 'post_date.day_of_year' => 'date_token.day_of_year',
123 | 'post_date.day_of_week' => 'date_token.day_of_week',
124 | 'post_date.hour' => 'date_token.hour',
125 | 'post_date.minute' => 'date_token.minute',
126 | 'post_date.second' => 'date_token.second',
127 | 'post_date_gmt' => 'date_gmt',
128 | 'post_date_gmt.year' => 'date_gmt_token.year',
129 | 'post_date_gmt.month' => 'date_gmt_token.month',
130 | 'post_date_gmt.week' => 'date_gmt_token.week',
131 | 'post_date_gmt.day' => 'date_gmt_token.day',
132 | 'post_date_gmt.day_of_year' => 'date_gmt_token.day_of_year',
133 | 'post_date_gmt.day_of_week' => 'date_gmt_token.day_of_week',
134 | 'post_date_gmt.hour' => 'date_gmt_token.hour',
135 | 'post_date_gmt.minute' => 'date_gmt_token.minute',
136 | 'post_date_gmt.second' => 'date_gmt_token.second',
137 | 'post_content' => 'content',
138 | 'post_content.analyzed' => 'content',
139 | 'post_title' => 'title',
140 | 'post_title.analyzed' => 'title',
141 | 'post_excerpt' => 'excerpt',
142 | 'post_password' => 'post_password', // This isn't indexed on VIP.
143 | 'post_name' => 'post_name', // This isn't indexed on VIP.
144 | 'post_modified' => 'modified',
145 | 'post_modified.year' => 'modified_token.year',
146 | 'post_modified.month' => 'modified_token.month',
147 | 'post_modified.week' => 'modified_token.week',
148 | 'post_modified.day' => 'modified_token.day',
149 | 'post_modified.day_of_year' => 'modified_token.day_of_year',
150 | 'post_modified.day_of_week' => 'modified_token.day_of_week',
151 | 'post_modified.hour' => 'modified_token.hour',
152 | 'post_modified.minute' => 'modified_token.minute',
153 | 'post_modified.second' => 'modified_token.second',
154 | 'post_modified_gmt' => 'modified_gmt',
155 | 'post_modified_gmt.year' => 'modified_gmt_token.year',
156 | 'post_modified_gmt.month' => 'modified_gmt_token.month',
157 | 'post_modified_gmt.week' => 'modified_gmt_token.week',
158 | 'post_modified_gmt.day' => 'modified_gmt_token.day',
159 | 'post_modified_gmt.day_of_year' => 'modified_gmt_token.day_of_year',
160 | 'post_modified_gmt.day_of_week' => 'modified_gmt_token.day_of_week',
161 | 'post_modified_gmt.hour' => 'modified_gmt_token.hour',
162 | 'post_modified_gmt.minute' => 'modified_gmt_token.minute',
163 | 'post_modified_gmt.second' => 'modified_gmt_token.second',
164 | 'post_parent' => 'parent_post_id',
165 | 'menu_order' => 'menu_order', // This isn't indexed on VIP.
166 | 'post_mime_type' => 'post_mime_type', // This isn't indexed on VIP.
167 | 'comment_count' => 'comment_count', // This isn't indexed on VIP.
168 | 'post_meta' => 'meta.%s.value.raw_lc',
169 | 'post_meta.analyzed' => 'meta.%s.value',
170 | 'post_meta.long' => 'meta.%s.long',
171 | 'post_meta.double' => 'meta.%s.double',
172 | 'post_meta.binary' => 'meta.%s.boolean',
173 | 'term_id' => 'taxonomy.%s.term_id',
174 | 'term_slug' => 'taxonomy.%s.slug',
175 | 'term_name' => 'taxonomy.%s.name.raw_lc',
176 | 'category_id' => 'category.term_id',
177 | 'category_slug' => 'category.slug',
178 | 'category_name' => 'category.name.raw',
179 | 'tag_id' => 'tag.term_id',
180 | 'tag_slug' => 'tag.slug',
181 | 'tag_name' => 'tag.name.raw',
182 | ),
183 | $es_map
184 | );
185 | }
186 | add_filter( 'es_field_map', 'vip_es_field_map' );
187 |
188 | /**
189 | * Returns the lowercase version of a meta value.
190 | *
191 | * @param mixed $meta_value The meta value.
192 | * @param string $meta_key The meta key.
193 | * @param string $meta_compare The comparison operation.
194 | * @param string $meta_type The type of meta (post, user, term, etc).
195 | * @return mixed If value is a string, returns the lowercase version. Otherwise, returns the original value, unmodified.
196 | */
197 | function vip_es_meta_value_tolower( $meta_value, $meta_key, $meta_compare, $meta_type ) {
198 | if ( ! is_string( $meta_value ) || empty( $meta_value ) ) {
199 | return $meta_value;
200 | }
201 | return strtolower( $meta_value );
202 | }
203 | add_filter( 'es_meta_query_meta_value', 'vip_es_meta_value_tolower', 10, 4 );
204 |
205 | /**
206 | * Normalise term name to lowercase as we are mapping that against raw_lc field.
207 | *
208 | * @param string|mixed $term Term's name which should be normalised to
209 | * lowercase.
210 | * @param string $taxonomy Taxonomy of the term.
211 | * @return mixed If $term is a string, lowercased string is returned. Otherwise
212 | * original value is return unchanged.
213 | */
214 | function vip_es_term_name_slug_tolower( $term, $taxonomy ) {
215 | if ( ! is_string( $term ) || empty( $term ) ) {
216 | return $term;
217 | }
218 | return strtolower( $term );
219 | }
220 | add_filter( 'es_tax_query_term_name', 'vip_es_term_name_slug_tolower', 10, 2 );
221 |
222 | /**
223 | * Advanced Post Cache and es-wp-query do not work well together. In
224 | * particular, the WP_Query->found_posts attribute gets corrupted when using
225 | * both of these plugins, so here we disable Advanced Post Cache completely
226 | * when queries are being made using Elasticsearch.
227 | *
228 | * On the other hand, if a non-Elasticsearch query is run, and we disabled
229 | * Advanced Post Cache earlier, we enable it again, to make use of its caching
230 | * features.
231 | *
232 | * Note that this applies only to calls done via WP_Query(), and not
233 | * ES_WP_Query()
234 | *
235 | * @param WP_Query|ES_WP_Query|ES_WP_Query_Wrapper $query The query to examine.
236 | */
237 | function vip_es_disable_advanced_post_cache( &$query ) {
238 | global $advanced_post_cache_object;
239 |
240 | static $disabled_apc = false;
241 |
242 | if ( empty( $advanced_post_cache_object ) || ! is_object( $advanced_post_cache_object ) ) {
243 | return;
244 | }
245 |
246 | /*
247 | * These two might be passsed to us; we only
248 | * handle WP_Query, so ignore these.
249 | */
250 | if (
251 | ( $query instanceof ES_WP_Query_Wrapper ) ||
252 | ( $query instanceof ES_WP_Query )
253 | ) {
254 | return;
255 | }
256 |
257 | if ( $query->get( 'es' ) ) {
258 | if ( true === $disabled_apc ) {
259 | // Already disabled, don't try again.
260 | return;
261 | }
262 |
263 | /*
264 | * An Elasticsearch-enabled query is being run. Disable Advanced Post Cache
265 | * entirely.
266 | *
267 | * Note that there is one action-hook that is not deactivated: The switch_blog
268 | * action is not deactivated, because it might be called in-between
269 | * Elasticsearch-enabled query, and a non-Elasticsearch query, and because it
270 | * does not have an effect on WP_Query()-results directly.
271 | */
272 |
273 | remove_filter( 'posts_request', array( $advanced_post_cache_object, 'posts_request' ) );
274 | remove_filter( 'posts_results', array( $advanced_post_cache_object, 'posts_results' ) );
275 |
276 | remove_filter( 'post_limits_request', array( $advanced_post_cache_object, 'post_limits_request' ), 999 );
277 |
278 | remove_filter( 'found_posts_query', array( $advanced_post_cache_object, 'found_posts_query' ) );
279 | remove_filter( 'found_posts', array( $advanced_post_cache_object, 'found_posts' ) );
280 |
281 | $disabled_apc = true;
282 | } else {
283 | // A non-ES query.
284 | if ( true === $disabled_apc ) {
285 | /*
286 | * Earlier, we disabled Advanced Post Cache
287 | * entirely, but now a non-Elasticsearch query is
288 | * being run, and in such cases it might be useful
289 | * to have the Cache enabled. Here we enable
290 | * it again.
291 | */
292 | $advanced_post_cache_object->__construct();
293 |
294 | $disabled_apc = false;
295 | }
296 | }
297 | }
298 | add_action( 'pre_get_posts', 'vip_es_disable_advanced_post_cache', -100 );
299 |
--------------------------------------------------------------------------------
/adapters/searchpress.php:
--------------------------------------------------------------------------------
1 | search( wp_json_encode( $es_args ), array( 'output' => ARRAY_A ) );
24 | }
25 | }
26 |
27 | /**
28 | * Provides a mapping between WordPress fields and Elasticsearch DSL fields.
29 | *
30 | * @param array $es_map Custom mappings to merge with the defaults.
31 | * @return array
32 | */
33 | function sp_es_field_map( $es_map ) {
34 | return wp_parse_args(
35 | array(
36 | 'post_name' => 'post_name.raw',
37 | 'post_title' => 'post_title.raw',
38 | 'post_title.analyzed' => 'post_title',
39 | 'post_content.analyzed' => 'post_content',
40 | 'post_author' => 'post_author.user_id',
41 | 'post_date' => 'post_date.date',
42 | 'post_date_gmt' => 'post_date_gmt.date',
43 | 'post_modified' => 'post_modified.date',
44 | 'post_modified_gmt' => 'post_modified_gmt.date',
45 | 'post_type' => 'post_type.raw',
46 | 'post_meta' => 'post_meta.%s.raw',
47 | 'post_meta.analyzed' => 'post_meta.%s.value',
48 | 'post_meta.signed' => 'post_meta.%s.long',
49 | 'post_meta.unsigned' => 'post_meta.%s.long',
50 | 'term_name' => 'terms.%s.name.raw',
51 | 'term_tt_id' => 'terms.%s.term_id',
52 | 'category_name' => 'terms.%s.name.raw',
53 | 'category_tt_id' => 'terms.%s.term_id',
54 | 'tag_name' => 'terms.%s.name.raw',
55 | 'tag_tt_id' => 'terms.%s.term_id',
56 | ),
57 | $es_map
58 | );
59 | }
60 | add_filter( 'es_field_map', 'sp_es_field_map' );
61 |
62 | // This section only used for unit tests.
63 | // phpcs:disable WordPress.Security.EscapeOutput.OutputNotEscaped, WordPress.PHP.DevelopmentFunctions.error_log_print_r
64 | if ( defined( 'ES_WP_QUERY_TEST_ENV' ) && ES_WP_QUERY_TEST_ENV ) {
65 |
66 | remove_action( 'save_post', array( SP_Sync_Manager(), 'sync_post' ) );
67 | remove_action( 'delete_post', array( SP_Sync_Manager(), 'delete_post' ) );
68 | remove_action( 'trashed_post', array( SP_Sync_Manager(), 'delete_post' ) );
69 |
70 | add_filter(
71 | 'sp_post_allowed_meta',
72 | function() {
73 | return array(
74 | 'numeric_value' => array( 'long', 'double' ),
75 | 'decimal_value' => array( 'value', 'long', 'double' ),
76 | 'time' => array( 'value', 'long' ),
77 | 'foo' => array( 'value', 'long' ),
78 | 'foo2' => array( 'value' ),
79 | 'foo3' => array( 'value' ),
80 | 'foo4' => array( 'value' ),
81 | 'number_of_colors' => array( 'value', 'long' ),
82 | 'oof' => array( 'value' ),
83 | 'bar' => array( 'value' ),
84 | 'bar1' => array( 'value' ),
85 | 'bar2' => array( 'value' ),
86 | 'baz' => array( 'value' ),
87 | 'froo' => array( 'value' ),
88 | 'tango' => array( 'value' ),
89 | 'color' => array( 'value' ),
90 | 'vegetable' => array( 'value' ),
91 | 'city' => array( 'value' ),
92 | 'address' => array( 'value' ),
93 | );
94 | }
95 | );
96 |
97 | /**
98 | * Verifies that the Elasticsearch server is up and accepting connections.
99 | *
100 | * @param int $tries The number of retries to attempt.
101 | * @param int $sleep The amount of time to sleep between retries.
102 | * @return bool True if the server is up, false if not.
103 | * @throws ES_Index_Exception If the indexing operation fails.
104 | */
105 | function es_wp_query_verify_es_is_running( $tries = 5, $sleep = 3 ) {
106 | // If your ES server is not at localhost:9200, you need to set $_ENV['SEARCHPRESS_HOST'].
107 | $host = getenv( 'SEARCHPRESS_HOST' );
108 | if ( empty( $host ) ) {
109 | $host = 'http://localhost:9200';
110 | }
111 |
112 | if ( defined( 'SP_VERSION' ) ) {
113 | $sp_version = SP_VERSION;
114 | } elseif ( defined( 'SP_PLUGIN_DIR' ) ) {
115 | require_once ABSPATH . '/wp-admin/includes/plugin.php';
116 | $plugin_data = get_plugin_data( SP_PLUGIN_DIR . '/searchpress.php' );
117 | $sp_version = ! empty( $plugin_data['Version'] ) ? $plugin_data['Version'] : '[unknown version]';
118 | } else {
119 | $sp_version = '[unknown version]';
120 | }
121 |
122 | printf(
123 | "Testing with SearchPress adapter, using SearchPress version %s and host %s\n",
124 | $sp_version,
125 | $host
126 | );
127 |
128 | // Make sure ES is running and responding.
129 | $tries = 5;
130 | $sleep = 3;
131 | do {
132 | $response = wp_remote_get( $host );
133 | if ( 200 === wp_remote_retrieve_response_code( $response ) ) {
134 | $body = json_decode( wp_remote_retrieve_body( $response ), true );
135 | if ( ! empty( $body['version']['number'] ) ) {
136 | printf( "Elasticsearch is up and running, using version %s.\n", $body['version']['number'] );
137 | }
138 | break;
139 | } else {
140 | printf( "\nInvalid response from ES (%s), sleeping %d seconds and trying again...\n", wp_remote_retrieve_response_code( $response ), $sleep );
141 | sleep( $sleep );
142 | }
143 | } while ( --$tries );
144 |
145 | // If we didn't end with a 200 status code, exit
146 | sp_adapter_verify_response_code( $response );
147 |
148 | $i = 0;
149 | while ( ! ( $beat = SP_Heartbeat()->check_beat( true ) ) && $i++ < 5 ) {
150 | echo "\nHeartbeat failed, sleeping 2 seconds and trying again...\n";
151 | sleep( 2 );
152 | }
153 | if ( ! $beat && ! SP_Heartbeat()->check_beat( true ) ) {
154 | echo "\nCould not find a heartbeat!";
155 | exit( 1 );
156 | }
157 |
158 | return true;
159 | }
160 |
161 | function sp_adapter_verify_response_code( $response ) {
162 | if ( '200' != wp_remote_retrieve_response_code( $response ) ) {
163 | printf( "Could not index posts!\nResponse code %s\n", wp_remote_retrieve_response_code( $response ) );
164 | if ( is_wp_error( $response ) ) {
165 | printf( "Message: %s\n", $response->get_error_message() );
166 | }
167 | exit( 1 );
168 | }
169 | }
170 |
171 | /**
172 | * A function to make test data available in the index.
173 | */
174 | function es_wp_query_index_test_data() {
175 | // If your ES server is not at localhost:9200, you need to set $_ENV['searchpress_host'].
176 | $host = ! empty( $_ENV['searchpress_host'] ) ? $_ENV['searchpress_host'] : 'http://localhost:9200';
177 |
178 | SP_Config()->update_settings(
179 | array(
180 | 'active' => false,
181 | 'host' => $host,
182 | )
183 | );
184 | SP_API()->index = 'es-wp-query-tests';
185 |
186 | SP_Config()->flush();
187 | SP_Config()->create_mapping();
188 |
189 | $posts = get_posts( // phpcs:ignore WordPressVIPMinimum.VIP.RestrictedFunctions.get_posts_get_posts
190 | array(
191 | 'posts_per_page' => -1, // phpcs:ignore WordPress.VIP.PostsPerPage.posts_per_page_posts_per_page
192 | 'post_type' => 'any',
193 | 'post_status' => array_values( get_post_stati() ),
194 | 'orderby' => 'ID',
195 | 'order' => 'ASC',
196 | )
197 | );
198 |
199 | $sp_posts = array();
200 | foreach ( $posts as $post ) {
201 | $sp_posts[] = new SP_Post( $post );
202 | }
203 |
204 | $response = SP_API()->index_posts( $sp_posts );
205 | if ( 200 !== intval( SP_API()->last_request['response_code'] ) ) {
206 | echo( "ES response not 200!\n" . print_r( $response, 1 ) );
207 | } elseif ( ! is_object( $response ) || ! is_array( $response->items ) ) {
208 | echo( "Error indexing data! Response:\n" . print_r( $response, 1 ) );
209 | }
210 |
211 | SP_Config()->update_settings(
212 | array(
213 | 'active' => true,
214 | 'must_init' => false,
215 | )
216 | );
217 |
218 | SP_API()->post( '_refresh' );
219 | }
220 | }
221 | // phpcs:enable WordPress.Security.EscapeOutput.OutputNotEscaped, WordPress.PHP.DevelopmentFunctions.error_log_print_r
222 |
--------------------------------------------------------------------------------
/adapters/travis.php:
--------------------------------------------------------------------------------
1 | wp_json_encode( $es_args ),
30 | 'headers' => array(
31 | 'Content-Type' => 'application/json',
32 | ),
33 | )
34 | );
35 | return json_decode( wp_remote_retrieve_body( $response ), true );
36 | }
37 | }
38 |
39 | /**
40 | * A class to represent an exception that fires when indexing fails.
41 | */
42 | class ES_Index_Exception extends Exception {
43 | }
44 |
45 | /**
46 | * Provides a mapping between WordPress fields and Elasticsearch DSL keys.
47 | *
48 | * @param array $es_map Additional mappings to layer on top of the default.
49 | * @return array Mappings to use.
50 | */
51 | function travis_es_field_map( $es_map ) {
52 | return wp_parse_args(
53 | array(
54 | 'post_meta' => 'post_meta.%s.value',
55 | 'post_author' => 'post_author.user_id',
56 | 'post_date' => 'post_date.date',
57 | 'post_date_gmt' => 'post_date_gmt.date',
58 | 'post_modified' => 'post_modified.date',
59 | 'post_modified_gmt' => 'post_modified_gmt.date',
60 | ),
61 | $es_map
62 | );
63 | }
64 | add_filter( 'es_field_map', 'travis_es_field_map' );
65 |
66 | if ( defined( 'ES_WP_QUERY_TEST_ENV' ) && ES_WP_QUERY_TEST_ENV ) {
67 |
68 | /**
69 | * Verifies that the Elasticsearch server is up and accepting connections.
70 | *
71 | * @param int $tries The number of retries to attempt.
72 | * @param int $sleep The amount of time to sleep between retries.
73 | * @return bool True if the server is up, false if not.
74 | * @throws ES_Index_Exception If the indexing operation fails.
75 | */
76 | function es_wp_query_verify_es_is_running( $tries = 5, $sleep = 3 ) {
77 | // Make sure ES is running and responding.
78 | do {
79 | $response = wp_remote_get( 'http://localhost:9200/' );
80 | if ( 200 === intval( wp_remote_retrieve_response_code( $response ) ) ) {
81 | $body = json_decode( wp_remote_retrieve_body( $response ), true );
82 | if ( ! empty( $body['version']['number'] ) ) {
83 | printf( "Elasticsearch is up and running, using version %s.\n", $body['version']['number'] );
84 | if ( ! defined( 'ES_VERSION' ) ) {
85 | define( 'ES_VERSION', $body['version']['number'] );
86 | } elseif ( ES_VERSION !== $body['version']['number'] ) {
87 | printf( "WARNING! ES_VERSION is set to %s, but Elasticsearch is reporting %s\n", ES_VERSION, $body['version']['number'] );
88 | }
89 | break;
90 | } else {
91 | sleep( $sleep );
92 | }
93 | } else {
94 | printf( "\nInvalid response from ES (%s), sleeping %d seconds and trying again...\n", wp_remote_retrieve_response_code( $response ), $sleep );
95 | sleep( $sleep );
96 | }
97 | } while ( --$tries );
98 |
99 | // If we didn't end with a 200 status code, bail.
100 | return travis_es_verify_response_code( $response );
101 | }
102 |
103 | /**
104 | * Indexes test data.
105 | *
106 | * @throws ES_Index_Exception If the indexing operation fails.
107 | */
108 | function es_wp_query_index_test_data() {
109 | global $es_wp_query_travis_doc_type;
110 | $es_wp_query_travis_doc_type = '_doc';
111 |
112 | // Ensure the index is empty.
113 | wp_remote_request( 'http://localhost:9200/es-wp-query-unit-tests/', array( 'method' => 'DELETE' ) );
114 |
115 | $analyzed = 'text';
116 | $not_analyzed = 'keyword';
117 | $doc_type_open = '';
118 | $doc_type_close = '';
119 | if ( version_compare( ES_VERSION, '5.0.0', '<' ) ) {
120 | $analyzed = 'string';
121 | $not_analyzed = 'string", "index": "not_analyzed';
122 | }
123 | if ( version_compare( ES_VERSION, '6.0.0', '<' ) ) {
124 | // ES < 6 doesn't support the doc type _doc.
125 | $es_wp_query_travis_doc_type = 'post';
126 | }
127 | if ( version_compare( ES_VERSION, '7.0.0', '<' ) ) {
128 | $doc_type_open = sprintf( '"%s": {', $es_wp_query_travis_doc_type );
129 | $doc_type_close = '}';
130 | }
131 |
132 | // Add the mapping.
133 | $response = wp_remote_request(
134 | 'http://localhost:9200/es-wp-query-unit-tests/',
135 | array(
136 | 'method' => 'PUT',
137 | 'body' => sprintf(
138 | '
139 | {
140 | "settings": {
141 | "analysis": {
142 | "analyzer": {
143 | "default": {
144 | "tokenizer": "standard",
145 | "filter": [
146 | "travis_word_delimiter",
147 | "lowercase",
148 | "stop",
149 | "travis_snowball"
150 | ],
151 | "language": "English"
152 | }
153 | },
154 | "filter": {
155 | "travis_word_delimiter": {
156 | "type": "word_delimiter",
157 | "preserve_original": true
158 | },
159 | "travis_snowball": {
160 | "type": "snowball",
161 | "language": "English"
162 | }
163 | }
164 | }
165 | },
166 | "mappings": {
167 | %3$s
168 | "date_detection": false,
169 | "dynamic_templates": [
170 | {
171 | "template_meta": {
172 | "path_match": "post_meta.*",
173 | "mapping": {
174 | "type": "object",
175 | "properties": {
176 | "value": {
177 | "type": "%2$s"
178 | },
179 | "analyzed": {
180 | "type": "%1$s"
181 | },
182 | "boolean": {
183 | "type": "boolean"
184 | },
185 | "long": {
186 | "type": "long"
187 | },
188 | "double": {
189 | "type": "double"
190 | },
191 | "date": {
192 | "format": "yyyy-MM-dd",
193 | "type": "date"
194 | },
195 | "datetime": {
196 | "format": "yyyy-MM-dd HH:mm:ss",
197 | "type": "date"
198 | },
199 | "time": {
200 | "format": "HH:mm:ss",
201 | "type": "date"
202 | }
203 | }
204 | }
205 | }
206 | },
207 | {
208 | "template_terms": {
209 | "path_match": "terms.*",
210 | "mapping": {
211 | "type": "object",
212 | "properties": {
213 | "name": { "type": "%2$s" },
214 | "term_id": { "type": "long" },
215 | "term_taxonomy_id": { "type": "long" },
216 | "slug": { "type": "%2$s" }
217 | }
218 | }
219 | }
220 | }
221 | ],
222 | "properties": {
223 | "post_id": { "type": "long" },
224 | "post_author": {
225 | "type": "object",
226 | "properties": {
227 | "user_id": { "type": "long" },
228 | "user_nicename": { "type": "%2$s" }
229 | }
230 | },
231 | "post_title": {
232 | "type": "%2$s",
233 | "fields": {
234 | "analyzed": { "type": "%1$s" }
235 | }
236 | },
237 | "post_excerpt": { "type": "%1$s" },
238 | "post_content": {
239 | "type": "%2$s",
240 | "fields": {
241 | "analyzed": { "type": "%1$s" }
242 | }
243 | },
244 | "post_status": { "type": "%2$s" },
245 | "post_name": { "type": "%2$s" },
246 | "post_parent": { "type": "long" },
247 | "post_type": { "type": "%2$s" },
248 | "post_mime_type": { "type": "%2$s" },
249 | "post_password": { "type": "%2$s" },
250 | "post_date": {
251 | "type": "object",
252 | "properties": {
253 | "date": { "type": "date", "format": "yyyy-MM-dd HH:mm:ss" },
254 | "year": { "type": "short" },
255 | "month": { "type": "byte" },
256 | "day": { "type": "byte" },
257 | "hour": { "type": "byte" },
258 | "minute": { "type": "byte" },
259 | "second": { "type": "byte" },
260 | "week": { "type": "byte" },
261 | "day_of_week": { "type": "byte" },
262 | "day_of_year": { "type": "short" },
263 | "seconds_from_day": { "type": "integer" },
264 | "seconds_from_hour": { "type": "short" }
265 | }
266 | },
267 | "post_date_gmt": {
268 | "type": "object",
269 | "properties": {
270 | "date": { "type": "date", "format": "yyyy-MM-dd HH:mm:ss" },
271 | "year": { "type": "short" },
272 | "month": { "type": "byte" },
273 | "day": { "type": "byte" },
274 | "hour": { "type": "byte" },
275 | "minute": { "type": "byte" },
276 | "second": { "type": "byte" },
277 | "week": { "type": "byte" },
278 | "day_of_week": { "type": "byte" },
279 | "day_of_year": { "type": "short" },
280 | "seconds_from_day": { "type": "integer" },
281 | "seconds_from_hour": { "type": "short" }
282 | }
283 | },
284 | "post_modified": {
285 | "type": "object",
286 | "properties": {
287 | "date": { "type": "date", "format": "yyyy-MM-dd HH:mm:ss" },
288 | "year": { "type": "short" },
289 | "month": { "type": "byte" },
290 | "day": { "type": "byte" },
291 | "hour": { "type": "byte" },
292 | "minute": { "type": "byte" },
293 | "second": { "type": "byte" },
294 | "week": { "type": "byte" },
295 | "day_of_week": { "type": "byte" },
296 | "day_of_year": { "type": "short" },
297 | "seconds_from_day": { "type": "integer" },
298 | "seconds_from_hour": { "type": "short" }
299 | }
300 | },
301 | "post_modified_gmt": {
302 | "type": "object",
303 | "properties": {
304 | "date": { "type": "date", "format": "yyyy-MM-dd HH:mm:ss" },
305 | "year": { "type": "short" },
306 | "month": { "type": "byte" },
307 | "day": { "type": "byte" },
308 | "hour": { "type": "byte" },
309 | "minute": { "type": "byte" },
310 | "second": { "type": "byte" },
311 | "week": { "type": "byte" },
312 | "day_of_week": { "type": "byte" },
313 | "day_of_year": { "type": "short" },
314 | "seconds_from_day": { "type": "integer" },
315 | "seconds_from_hour": { "type": "short" }
316 | }
317 | },
318 | "menu_order" : { "type" : "integer" },
319 | "terms": { "type": "object" },
320 | "post_meta": { "type": "object" }
321 | }
322 | %4$s
323 | }
324 | }
325 | ',
326 | $analyzed,
327 | $not_analyzed,
328 | $doc_type_open,
329 | $doc_type_close
330 | ),
331 | 'headers' => array(
332 | 'Content-Type' => 'application/json',
333 | ),
334 | )
335 | );
336 | if ( true !== travis_es_verify_response_code( $response ) ) {
337 | exit( 1 );
338 | }
339 |
340 | // Index the content.
341 | $posts = get_posts(
342 | array(
343 | 'posts_per_page' => -1,
344 | 'post_type' => array_values( get_post_types() ),
345 | 'post_status' => array_values( get_post_stati() ),
346 | 'orderby' => 'ID',
347 | 'order' => 'ASC',
348 | )
349 | );
350 |
351 | $es_posts = array();
352 | foreach ( $posts as $post ) {
353 | $es_posts[] = new Travis_ES_Post( $post );
354 | }
355 |
356 | $body = array();
357 | foreach ( $es_posts as $post ) {
358 | $body[] = '{ "index": { "_id" : ' . $post->data['post_id'] . ' } }';
359 | $body[] = addcslashes( $post->to_json(), "\n" );
360 | }
361 |
362 | $response = wp_remote_request(
363 | "http://localhost:9200/es-wp-query-unit-tests/{$es_wp_query_travis_doc_type}/_bulk",
364 | array(
365 | 'method' => 'PUT',
366 | 'body' => wp_check_invalid_utf8( implode( "\n", $body ), true ) . "\n",
367 | 'headers' => array(
368 | 'Content-Type' => 'application/json',
369 | ),
370 | )
371 | );
372 | travis_es_verify_response_code( $response );
373 |
374 | $itemized_response = json_decode( wp_remote_retrieve_body( $response ) );
375 | foreach ( (array) $itemized_response->items as $post ) {
376 | // Status should be 200 or 201, depending on if we're updating or creating respectively.
377 | if ( ! isset( $post->index->status ) || ! in_array( intval( $post->index->status ), array( 200, 201 ), true ) ) {
378 | $error_message = "Error indexing post {$post->index->_id}; HTTP response code: {$post->index->status}";
379 | if ( ! empty( $post->index->error ) ) {
380 | if ( is_string( $post->index->error ) ) {
381 | $error_message .= "\n{$post->index->error}";
382 | } elseif ( ! empty( $post->index->error->reason ) && ! empty( $post->index->error->type ) ) {
383 | $error_message .= "\n{$post->index->error->type}: {$post->index->error->reason}";
384 | }
385 | }
386 | $error_message .= 'Backtrace:' . travis_es_debug_backtrace_summary();
387 | throw new ES_Index_Exception( $error_message );
388 | }
389 | }
390 |
391 | $response = wp_remote_post(
392 | 'http://localhost:9200/es-wp-query-unit-tests/_refresh',
393 | array(
394 | 'headers' => array(
395 | 'Content-Type' => 'application/json',
396 | ),
397 | )
398 | );
399 | travis_es_verify_response_code( $response );
400 | }
401 |
402 | /**
403 | * Verifies the Elasticsearch response code.
404 | *
405 | * @param WP_Error|array $response The response from wp_remote_request.
406 | * @return bool
407 | * @throws ES_Index_Exception If the indexing fails.
408 | */
409 | function travis_es_verify_response_code( $response ) {
410 | if ( 200 !== intval( wp_remote_retrieve_response_code( $response ) ) ) {
411 | $message = [ 'Failed to index posts!' ];
412 | if ( is_wp_error( $response ) ) {
413 | $message[] = sprintf( 'Message: %s', $response->get_error_message() );
414 | } else {
415 | $message[] = sprintf( 'Response code %s', wp_remote_retrieve_response_code( $response ) );
416 | $message[] = sprintf( 'Message: %s', wp_remote_retrieve_body( $response ) );
417 | }
418 | $message[] = sprintf( 'Backtrace:%s', travis_es_debug_backtrace_summary() );
419 | throw new ES_Index_Exception( implode( "\n", $message ) );
420 | }
421 |
422 | return true;
423 | }
424 |
425 | /**
426 | * Provides a backtrace summary for error reporting in Travis tests.
427 | *
428 | * @return string
429 | */
430 | function travis_es_debug_backtrace_summary() {
431 | $backtrace = wp_debug_backtrace_summary( null, 0, false );
432 | $backtrace = array_filter(
433 | $backtrace,
434 | function( $call ) {
435 | return ! preg_match( '/PHPUnit_(TextUI_(Command|TestRunner)|Framework_(TestSuite|TestCase|TestResult))|ReflectionMethod|travis_es_(verify_response_code|debug_backtrace_summary)/', $call );
436 | }
437 | );
438 | return "\n\t" . join( "\n\t", $backtrace );
439 | }
440 |
441 | /**
442 | * Taken from SearchPress.
443 | */
444 | class Travis_ES_Post {
445 |
446 | /**
447 | * This stores what will eventually become our JSON.
448 | *
449 | * @access public
450 | * @var array
451 | */
452 | public $data = array();
453 |
454 | /**
455 | * A list of users.
456 | *
457 | * @access protected
458 | * @var array
459 | */
460 | protected static $users = array();
461 |
462 | /**
463 | * Travis_ES_Post constructor.
464 | *
465 | * @param WP_Post $post The post object to use.
466 | * @access public
467 | */
468 | public function __construct( $post ) {
469 | if ( is_numeric( $post ) && 0 !== intval( $post ) ) {
470 | $post = get_post( intval( $post ) );
471 | }
472 | if ( ! is_object( $post ) ) {
473 | return;
474 | }
475 |
476 | $this->fill( $post );
477 | }
478 |
479 | /**
480 | * Populate this object with all of the post's properties.
481 | *
482 | * @param WP_Post $post The post to use when filling post properties.
483 | * @access public
484 | */
485 | public function fill( $post ) {
486 | $this->data = array(
487 | 'post_id' => $post->ID,
488 | 'post_author' => $this->get_user( $post->post_author ),
489 | 'post_title' => $post->post_title,
490 | 'post_excerpt' => $post->post_excerpt,
491 | 'post_content' => $post->post_content,
492 | 'post_status' => $post->post_status,
493 | 'post_name' => $post->post_name,
494 | 'post_parent' => $post->post_parent,
495 | 'post_type' => $post->post_type,
496 | 'post_mime_type' => $post->post_mime_type,
497 | 'post_password' => $post->post_password,
498 | 'terms' => $this->get_terms( $post ),
499 | 'post_meta' => $this->get_meta( $post->ID ),
500 | );
501 | foreach ( array( 'post_date', 'post_date_gmt', 'post_modified', 'post_modified_gmt' ) as $field ) {
502 | $value = $this->get_date( $post->$field );
503 | if ( ! empty( $value ) ) {
504 | $this->data[ $field ] = $value;
505 | }
506 | }
507 | }
508 |
509 | /**
510 | * Get post meta for a given post ID.
511 | * Some post meta is removed (you can filter it), and serialized data gets unserialized
512 | *
513 | * @param int $post_id The ID of the post for which to retrieve meta.
514 | * @return array 'meta_key' => array( value 1, value 2... )
515 | */
516 | public function get_meta( $post_id ) {
517 | $meta = (array) get_post_meta( $post_id );
518 |
519 | // Remove a filtered set of meta that we don't want indexed.
520 | $ignored_meta = array(
521 | '_edit_lock',
522 | '_edit_last',
523 | '_wp_old_slug',
524 | '_wp_trash_meta_time',
525 | '_wp_trash_meta_status',
526 | '_previous_revision',
527 | '_wpas_done_all',
528 | '_encloseme',
529 | );
530 | foreach ( $ignored_meta as $key ) {
531 | unset( $meta[ $key ] );
532 | }
533 |
534 | foreach ( $meta as &$values ) {
535 | $values = array_map( array( $this, 'cast_meta_types' ), $values );
536 | }
537 |
538 | return $meta;
539 | }
540 |
541 | /**
542 | * Split the meta values into different types for meta query casting.
543 | *
544 | * @param string $value Meta value.
545 | * @return array
546 | */
547 | public function cast_meta_types( $value ) {
548 | $return = array(
549 | 'value' => $value,
550 | 'analyzed' => $value,
551 | 'boolean' => (bool) $value,
552 | );
553 |
554 | if ( is_numeric( $value ) ) {
555 | $return['long'] = intval( $value );
556 | $return['double'] = floatval( $value );
557 | }
558 |
559 | // Correct boolean values.
560 | if ( ( 'false' === $value ) || ( 'FALSE' === $value ) ) {
561 | $return['boolean'] = false;
562 | } elseif ( ( 'true' === $value ) || ( 'TRUE' === $value ) ) {
563 | $return['boolean'] = true;
564 | }
565 |
566 | // Add date/time if we have it.
567 | $time = strtotime( $value );
568 | if ( false !== $time ) {
569 | $return['date'] = date( 'Y-m-d', $time );
570 | $return['datetime'] = date( 'Y-m-d H:i:s', $time );
571 | $return['time'] = date( 'H:i:s', $time );
572 | }
573 |
574 | return $return;
575 | }
576 |
577 | /**
578 | * Get all terms across all taxonomies for a given post
579 | *
580 | * @param WP_Post $post The post to process.
581 | * @access public
582 | * @return array
583 | */
584 | public function get_terms( $post ) {
585 | $object_terms = array();
586 | $taxonomies = get_object_taxonomies( $post->post_type );
587 | foreach ( $taxonomies as $taxonomy ) {
588 | $these_terms = get_the_terms( $post->ID, $taxonomy );
589 | if ( $these_terms && ! is_wp_error( $these_terms ) ) {
590 | $object_terms = array_merge( $object_terms, $these_terms );
591 | }
592 | }
593 |
594 | if ( empty( $object_terms ) ) {
595 | return array();
596 | }
597 |
598 | $terms = array();
599 | foreach ( (array) $object_terms as $term ) {
600 | $terms[ $term->taxonomy ][] = array(
601 | 'term_id' => $term->term_id,
602 | 'term_taxonomy_id' => $term->term_taxonomy_id,
603 | 'slug' => $term->slug,
604 | 'name' => $term->name,
605 | );
606 | }
607 |
608 | return $terms;
609 | }
610 |
611 |
612 | /**
613 | * Parse out the properties of a date.
614 | *
615 | * @param string $date A date, expected to be in mysql format.
616 | * @return array|false The parsed date on success, false on failure.
617 | */
618 | public function get_date( $date ) {
619 | $ts = strtotime( $date );
620 | if ( $ts <= 0 ) {
621 | return false;
622 | }
623 |
624 | return array(
625 | 'date' => $date,
626 | 'year' => date( 'Y', $ts ),
627 | 'month' => date( 'm', $ts ),
628 | 'day' => date( 'd', $ts ),
629 | 'hour' => date( 'H', $ts ),
630 | 'minute' => date( 'i', $ts ),
631 | 'second' => date( 's', $ts ),
632 | 'week' => date( 'W', $ts ),
633 | 'day_of_week' => date( 'N', $ts ),
634 | 'day_of_year' => date( 'z', $ts ),
635 | 'seconds_from_day' => mktime( date( 'H', $ts ), date( 'i', $ts ), date( 's', $ts ), 1, 1, 1970 ),
636 | 'seconds_from_hour' => mktime( 0, date( 'i', $ts ), date( 's', $ts ), 1, 1, 1970 ),
637 | );
638 | }
639 |
640 |
641 | /**
642 | * Get information about a post author
643 | *
644 | * @param int $user_id The user ID to look up.
645 | * @access public
646 | * @return array
647 | */
648 | public function get_user( $user_id ) {
649 | if ( empty( self::$users[ $user_id ] ) ) {
650 | $user = get_userdata( $user_id );
651 | $data = array( 'user_id' => absint( $user_id ) );
652 | if ( $user instanceof WP_User ) {
653 | $data['user_nicename'] = strval( $user->user_nicename );
654 | } else {
655 | $data['user_nicename'] = '';
656 | }
657 | self::$users[ $user_id ] = $data;
658 | }
659 |
660 | return self::$users[ $user_id ];
661 | }
662 |
663 |
664 | /**
665 | * Return this object as JSON
666 | *
667 | * @return string
668 | */
669 | public function to_json() {
670 | return wp_json_encode( $this->data );
671 | }
672 | }
673 | }
674 |
--------------------------------------------------------------------------------
/adapters/vip-search.php:
--------------------------------------------------------------------------------
1 | query_es( 'post', $es_args );
28 |
29 | return $result;
30 | }
31 | }
32 | }
33 |
34 | /**
35 | * Sets the posts array to the list of found post IDs.
36 | *
37 | * @param array $q Query arguments.
38 | * @param array|WP_Error $es_response Response from VIP Search.
39 | * @access protected
40 | */
41 | protected function set_posts( $q, $es_response ) {
42 | $this->posts = array();
43 | if ( ! is_wp_error( $es_response ) && isset( $es_response['documents'] ) ) {
44 | switch ( $q['fields'] ) {
45 | case 'ids':
46 | foreach ( $es_response['documents'] as $hit ) {
47 | $post_id = (array) $hit[ $this->es_map( 'post_id' ) ];
48 | $this->posts[] = reset( $post_id );
49 | }
50 | return;
51 |
52 | case 'id=>parent':
53 | foreach ( $es_response['documents'] as $hit ) {
54 | $post_id = (array) $hit[ $this->es_map( 'post_id' ) ];
55 | $post_parent = (array) $hit[ $this->es_map( 'post_parent' ) ];
56 | $this->posts[ reset( $post_id ) ] = reset( $post_parent );
57 | }
58 | return;
59 |
60 | default:
61 | if ( apply_filters( 'es_query_use_source', false ) ) {
62 | $this->posts = wp_list_pluck( $es_response['documents'], '_source' );
63 | return;
64 | } else {
65 | $post_ids = array();
66 | foreach ( $es_response['documents'] as $hit ) {
67 | $post_id = (array) $hit[ $this->es_map( 'post_id' ) ];
68 | $post_ids[] = absint( reset( $post_id ) );
69 | }
70 | $post_ids = array_filter( $post_ids );
71 | if ( ! empty( $post_ids ) ) {
72 | global $wpdb;
73 | $post__in = implode( ',', $post_ids );
74 | $this->posts = $wpdb->get_results( "SELECT $wpdb->posts.* FROM $wpdb->posts WHERE ID IN ($post__in) ORDER BY FIELD( {$wpdb->posts}.ID, $post__in )" ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, WordPress.VIP.DirectDatabaseQuery.NoCaching, WordPress.VIP.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.DirectDatabaseQuery.DirectQuery
75 | }
76 | return;
77 | }
78 | }
79 | } else {
80 | $this->posts = array();
81 | }
82 | }
83 |
84 | /**
85 | * Set up the amount of found posts and the number of pages (if limit clause was used)
86 | * for the current query.
87 | *
88 | * @param array $q Query arguments.
89 | * @param array|WP_Error $es_response The response from the Elasticsearch server.
90 | * @access public
91 | */
92 | public function set_found_posts( $q, $es_response ) {
93 | if ( ! is_wp_error( $es_response ) && isset( $es_response['found_documents']['value'] ) ) {
94 | $this->found_posts = absint( $es_response['found_documents']['value'] );
95 | } else {
96 | $this->found_posts = 0;
97 | }
98 | $this->found_posts = apply_filters_ref_array( 'es_found_posts', array( $this->found_posts, &$this ) );
99 | $this->max_num_pages = ceil( $this->found_posts / $q['posts_per_page'] );
100 | }
101 | }
102 |
103 | /**
104 | * Maps Elasticsearch DSL keys to their VIP-specific naming conventions.
105 | *
106 | * @param array $es_map Additional fields to map.
107 | * @return array The final field mapping.
108 | */
109 | function vip_es_field_map( $es_map ) {
110 | return wp_parse_args(
111 | array(
112 | 'post_author' => 'post_author.id',
113 | 'post_author.user_nicename' => 'post_author.login.raw',
114 | 'post_date' => 'post_date',
115 | 'post_date.year' => 'date_terms.year',
116 | 'post_date.month' => 'date_terms.month',
117 | 'post_date.week' => 'date_terms.week',
118 | 'post_date.day' => 'date_terms.day',
119 | 'post_date.day_of_year' => 'date_terms.dayofyear',
120 | 'post_date.day_of_week' => 'date_terms.dayofweek',
121 | 'post_date.hour' => 'date_terms.hour',
122 | 'post_date.minute' => 'date_terms.minute',
123 | 'post_date.second' => 'date_terms.second',
124 | 'post_date_gmt' => 'post_date_gmt',
125 | 'post_date_gmt.year' => 'date_gmt_terms.year',
126 | 'post_date_gmt.month' => 'date_gmt_terms.month',
127 | 'post_date_gmt.week' => 'date_gmt_terms.week',
128 | 'post_date_gmt.day' => 'date_gmt_terms.day',
129 | 'post_date_gmt.day_of_year' => 'date_gmt_terms.day_of_year',
130 | 'post_date_gmt.day_of_week' => 'date_gmt_terms.day_of_week',
131 | 'post_date_gmt.hour' => 'date_gmt_terms.hour',
132 | 'post_date_gmt.minute' => 'date_gmt_terms.minute',
133 | 'post_date_gmt.second' => 'date_gmt_terms.second',
134 | 'post_content' => 'post_content',
135 | 'post_content.analyzed' => 'post_content',
136 | 'post_title' => 'post_title.raw',
137 | 'post_title.analyzed' => 'post_title',
138 | 'post_type' => 'post_type.raw',
139 | 'post_excerpt' => 'post_excerpt',
140 | 'post_password' => 'post_password', // This isn't indexed on VIP.
141 | 'post_name' => 'post_name.raw',
142 | 'post_modified' => 'post_modified',
143 | 'post_modified.year' => 'modified_date_terms.year',
144 | 'post_modified.month' => 'modified_date_terms.month',
145 | 'post_modified.week' => 'modified_date_terms.week',
146 | 'post_modified.day' => 'modified_date_terms.day',
147 | 'post_modified.day_of_year' => 'modified_date_terms.day_of_year',
148 | 'post_modified.day_of_week' => 'modified_date_terms.day_of_week',
149 | 'post_modified.hour' => 'modified_date_terms.hour',
150 | 'post_modified.minute' => 'modified_date_terms.minute',
151 | 'post_modified.second' => 'modified_date_terms.second',
152 | 'post_modified_gmt' => 'post_modified_gmt',
153 | 'post_modified_gmt.year' => 'modified_date_gmt_terms.year',
154 | 'post_modified_gmt.month' => 'modified_date_gmt_terms.month',
155 | 'post_modified_gmt.week' => 'modified_date_gmt_terms.week',
156 | 'post_modified_gmt.day' => 'modified_date_gmt_terms.day',
157 | 'post_modified_gmt.day_of_year' => 'modified_date_gmt_terms.day_of_year',
158 | 'post_modified_gmt.day_of_week' => 'modified_date_gmt_terms.day_of_week',
159 | 'post_modified_gmt.hour' => 'modified_date_gmt_terms.hour',
160 | 'post_modified_gmt.minute' => 'modified_date_gmt_terms.minute',
161 | 'post_modified_gmt.second' => 'modified_date_gmt_terms.second',
162 | 'post_parent' => 'post_parent',
163 | 'menu_order' => 'menu_order',
164 | 'post_mime_type' => 'post_mime_type',
165 | 'comment_count' => 'comment_count',
166 | 'post_meta' => 'meta.%s.value.sortable',
167 | 'post_meta.analyzed' => 'meta.%s.value',
168 | 'post_meta.long' => 'meta.%s.long',
169 | 'post_meta.double' => 'meta.%s.double',
170 | 'post_meta.binary' => 'meta.%s.boolean',
171 | 'term_id' => 'terms.%s.term_id',
172 | 'term_slug' => 'terms.%s.slug',
173 | 'term_name' => 'terms.%s.name.sortable',
174 | 'category_id' => 'terms.category.term_id',
175 | 'category_slug' => 'terms.category.slug',
176 | 'category_name' => 'terms.category.name.sortable',
177 | 'tag_id' => 'terms.post_tag.term_id',
178 | 'tag_slug' => 'terms.post_tag.slug',
179 | 'tag_name' => 'terms.post_tag.name.sortable',
180 | ),
181 | $es_map
182 | );
183 | }
184 | add_filter( 'es_field_map', 'vip_es_field_map' );
185 |
186 | /**
187 | * Returns the lowercase version of a meta value.
188 | *
189 | * @param mixed $meta_value The meta value.
190 | * @param string $meta_key The meta key.
191 | * @param string $meta_compare The comparison operation.
192 | * @param string $meta_type The type of meta (post, user, term, etc).
193 | * @return mixed If value is a string, returns the lowercase version. Otherwise, returns the original value, unmodified.
194 | */
195 | function vip_es_meta_value_tolower( $meta_value, $meta_key, $meta_compare, $meta_type ) {
196 | if ( ! is_string( $meta_value ) || empty( $meta_value ) ) {
197 | return $meta_value;
198 | }
199 | return strtolower( $meta_value );
200 | }
201 | add_filter( 'es_meta_query_meta_value', 'vip_es_meta_value_tolower', 10, 4 );
202 |
203 | /**
204 | * Normalise term name to lowercase as we are mapping that against the "sortable" field, which is a lowercased keyword.
205 | *
206 | * @param string|mixed $term Term's name which should be normalised to
207 | * lowercase.
208 | * @param string $taxonomy Taxonomy of the term.
209 | * @return mixed If $term is a string, lowercased string is returned. Otherwise
210 | * original value is return unchanged.
211 | */
212 | function vip_es_term_name_slug_tolower( $term, $taxonomy ) {
213 | if ( ! is_string( $term ) || empty( $term ) ) {
214 | return $term;
215 | }
216 | return strtolower( $term );
217 | }
218 | add_filter( 'es_tax_query_term_name', 'vip_es_term_name_slug_tolower', 10, 2 );
219 |
220 | /**
221 | * Advanced Post Cache and es-wp-query do not work well together. In
222 | * particular, the WP_Query->found_posts attribute gets corrupted when using
223 | * both of these plugins, so here we disable Advanced Post Cache completely
224 | * when queries are being made using Elasticsearch.
225 | *
226 | * On the other hand, if a non-Elasticsearch query is run, and we disabled
227 | * Advanced Post Cache earlier, we enable it again, to make use of its caching
228 | * features.
229 | *
230 | * Note that this applies only to calls done via WP_Query(), and not
231 | * ES_WP_Query()
232 | *
233 | * @param WP_Query|ES_WP_Query|ES_WP_Query_Wrapper $query The query to examine.
234 | */
235 | function vip_es_disable_advanced_post_cache( &$query ) {
236 | global $advanced_post_cache_object;
237 |
238 | static $disabled_apc = false;
239 |
240 | if ( empty( $advanced_post_cache_object ) || ! is_object( $advanced_post_cache_object ) ) {
241 | return;
242 | }
243 |
244 | /*
245 | * These two might be passsed to us; we only
246 | * handle WP_Query, so ignore these.
247 | */
248 | if (
249 | ( $query instanceof ES_WP_Query_Wrapper ) ||
250 | ( $query instanceof ES_WP_Query )
251 | ) {
252 | return;
253 | }
254 |
255 | if ( $query->get( 'es' ) ) {
256 | if ( true === $disabled_apc ) {
257 | // Already disabled, don't try again.
258 | return;
259 | }
260 |
261 | /*
262 | * An Elasticsearch-enabled query is being run. Disable Advanced Post Cache
263 | * entirely.
264 | *
265 | * Note that there is one action-hook that is not deactivated: The switch_blog
266 | * action is not deactivated, because it might be called in-between
267 | * Elasticsearch-enabled query, and a non-Elasticsearch query, and because it
268 | * does not have an effect on WP_Query()-results directly.
269 | */
270 |
271 | remove_filter( 'posts_request', array( $advanced_post_cache_object, 'posts_request' ) );
272 | remove_filter( 'posts_results', array( $advanced_post_cache_object, 'posts_results' ) );
273 |
274 | remove_filter( 'post_limits_request', array( $advanced_post_cache_object, 'post_limits_request' ), 999 );
275 |
276 | remove_filter( 'found_posts_query', array( $advanced_post_cache_object, 'found_posts_query' ) );
277 | remove_filter( 'found_posts', array( $advanced_post_cache_object, 'found_posts' ) );
278 |
279 | $disabled_apc = true;
280 | } else {
281 | // A non-ES query.
282 | if ( true === $disabled_apc ) {
283 | /*
284 | * Earlier, we disabled Advanced Post Cache
285 | * entirely, but now a non-Elasticsearch query is
286 | * being run, and in such cases it might be useful
287 | * to have the Cache enabled. Here we enable
288 | * it again.
289 | */
290 | $advanced_post_cache_object->__construct();
291 |
292 | $disabled_apc = false;
293 | }
294 | }
295 | }
296 | add_action( 'pre_get_posts', 'vip_es_disable_advanced_post_cache', -100 );
297 |
--------------------------------------------------------------------------------
/bin/install-es.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | if [ $# -lt 1 ]; then
4 | echo "usage: $0 "
5 | exit 1
6 | fi
7 |
8 | ES_VERSION=$1
9 |
10 | setup_es() {
11 | download_url=$1
12 | mkdir /tmp/elasticsearch
13 | wget -O - $download_url | tar xz --directory=/tmp/elasticsearch --strip-components=1
14 | }
15 |
16 | start_es() {
17 | echo "Starting Elasticsearch $ES_VERSION..."
18 | echo "/tmp/elasticsearch/bin/elasticsearch $1 > /tmp/elasticsearch.log &"
19 | /tmp/elasticsearch/bin/elasticsearch $1 > /tmp/elasticsearch.log &
20 | }
21 |
22 | if [[ "$ES_VERSION" == 1.* ]]; then
23 | setup_es https://download.elastic.co/elasticsearch/elasticsearch/elasticsearch-${ES_VERSION}.tar.gz
24 | elif [[ "$ES_VERSION" == 2.* ]]; then
25 | setup_es https://download.elastic.co/elasticsearch/release/org/elasticsearch/distribution/tar/elasticsearch/${ES_VERSION}/elasticsearch-${ES_VERSION}.tar.gz
26 | elif [[ "$ES_VERSION" == [56].* ]]; then
27 | setup_es https://artifacts.elastic.co/downloads/elasticsearch/elasticsearch-${ES_VERSION}.tar.gz
28 | else
29 | setup_es https://artifacts.elastic.co/downloads/elasticsearch/elasticsearch-${ES_VERSION}-linux-x86_64.tar.gz
30 | fi
31 |
32 | if [[ "$ES_VERSION" == [12].* ]]; then
33 | start_es '-Des.path.repo=/tmp'
34 | else
35 | start_es '-Epath.repo=/tmp -Enetwork.host=_local_'
36 | fi
37 |
--------------------------------------------------------------------------------
/bin/install-wp-tests.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | if [ $# -lt 3 ]; then
4 | echo "usage: $0 [db-host] [wp-version] [skip-database-creation]"
5 | exit 1
6 | fi
7 |
8 | DB_NAME=$1
9 | DB_USER=$2
10 | DB_PASS=$3
11 | DB_HOST=${4-localhost}
12 | WP_VERSION=${5-latest}
13 | SKIP_DB_CREATE=${6-false}
14 |
15 | WP_TESTS_DIR=${WP_TESTS_DIR-/tmp/wordpress-tests-lib}
16 | WP_CORE_DIR=${WP_CORE_DIR-/tmp/wordpress/}
17 |
18 | download() {
19 | if [ `which curl` ]; then
20 | curl -s "$1" > "$2";
21 | elif [ `which wget` ]; then
22 | wget -nv -O "$2" "$1"
23 | fi
24 | }
25 |
26 | if [[ $WP_VERSION =~ [0-9]+\.[0-9]+(\.[0-9]+)? ]]; then
27 | WP_TESTS_TAG="tags/$WP_VERSION"
28 | elif [[ $WP_VERSION == 'nightly' || $WP_VERSION == 'trunk' ]]; then
29 | WP_TESTS_TAG="trunk"
30 | else
31 | # http serves a single offer, whereas https serves multiple. we only want one
32 | download http://api.wordpress.org/core/version-check/1.7/ /tmp/wp-latest.json
33 | grep '[0-9]+\.[0-9]+(\.[0-9]+)?' /tmp/wp-latest.json
34 | LATEST_VERSION=$(grep -o '"version":"[^"]*' /tmp/wp-latest.json | sed 's/"version":"//')
35 | if [[ -z "$LATEST_VERSION" ]]; then
36 | echo "Latest WordPress version could not be found"
37 | exit 1
38 | fi
39 | WP_TESTS_TAG="tags/$LATEST_VERSION"
40 | fi
41 |
42 | set -ex
43 |
44 | install_wp() {
45 |
46 | if [ -d $WP_CORE_DIR ]; then
47 | return;
48 | fi
49 |
50 | mkdir -p $WP_CORE_DIR
51 |
52 | if [[ $WP_VERSION == 'nightly' || $WP_VERSION == 'trunk' ]]; then
53 | mkdir -p /tmp/wordpress-nightly
54 | download https://wordpress.org/nightly-builds/wordpress-latest.zip /tmp/wordpress-nightly/wordpress-nightly.zip
55 | unzip -q /tmp/wordpress-nightly/wordpress-nightly.zip -d /tmp/wordpress-nightly/
56 | mv /tmp/wordpress-nightly/wordpress/* $WP_CORE_DIR
57 | else
58 | if [ $WP_VERSION == 'latest' ]; then
59 | local ARCHIVE_NAME='latest'
60 | else
61 | local ARCHIVE_NAME="wordpress-$WP_VERSION"
62 | fi
63 | download https://wordpress.org/${ARCHIVE_NAME}.tar.gz /tmp/wordpress.tar.gz
64 | tar --strip-components=1 -zxmf /tmp/wordpress.tar.gz -C $WP_CORE_DIR
65 | fi
66 |
67 | download https://raw.github.com/markoheijnen/wp-mysqli/master/db.php $WP_CORE_DIR/wp-content/db.php
68 | }
69 |
70 | install_test_suite() {
71 | # portable in-place argument for both GNU sed and Mac OSX sed
72 | if [[ $(uname -s) == 'Darwin' ]]; then
73 | local ioption='-i .bak'
74 | else
75 | local ioption='-i'
76 | fi
77 |
78 | # set up testing suite if it doesn't yet exist
79 | if [ ! -d $WP_TESTS_DIR ]; then
80 | # set up testing suite
81 | mkdir -p $WP_TESTS_DIR
82 | svn co --quiet https://develop.svn.wordpress.org/${WP_TESTS_TAG}/tests/phpunit/includes/ $WP_TESTS_DIR/includes
83 | svn co --quiet https://develop.svn.wordpress.org/${WP_TESTS_TAG}/tests/phpunit/data/ $WP_TESTS_DIR/data
84 | fi
85 |
86 | if [ ! -f wp-tests-config.php ]; then
87 | download https://develop.svn.wordpress.org/${WP_TESTS_TAG}/wp-tests-config-sample.php "$WP_TESTS_DIR"/wp-tests-config.php
88 | # remove all forward slashes in the end
89 | WP_CORE_DIR=$(echo $WP_CORE_DIR | sed "s:/\+$::")
90 | sed $ioption "s:dirname( __FILE__ ) . '/src/':'$WP_CORE_DIR/':" "$WP_TESTS_DIR"/wp-tests-config.php
91 | sed $ioption "s/youremptytestdbnamehere/$DB_NAME/" "$WP_TESTS_DIR"/wp-tests-config.php
92 | sed $ioption "s/yourusernamehere/$DB_USER/" "$WP_TESTS_DIR"/wp-tests-config.php
93 | sed $ioption "s/yourpasswordhere/$DB_PASS/" "$WP_TESTS_DIR"/wp-tests-config.php
94 | sed $ioption "s|localhost|${DB_HOST}|" "$WP_TESTS_DIR"/wp-tests-config.php
95 | fi
96 |
97 | }
98 |
99 | install_db() {
100 |
101 | if [ ${SKIP_DB_CREATE} = "true" ]; then
102 | return 0
103 | fi
104 |
105 | # parse DB_HOST for port or socket references
106 | local PARTS=(${DB_HOST//\:/ })
107 | local DB_HOSTNAME=${PARTS[0]};
108 | local DB_SOCK_OR_PORT=${PARTS[1]};
109 | local EXTRA=""
110 |
111 | if ! [ -z $DB_HOSTNAME ] ; then
112 | if [ $(echo $DB_SOCK_OR_PORT | grep -e '^[0-9]\{1,\}$') ]; then
113 | EXTRA=" --host=$DB_HOSTNAME --port=$DB_SOCK_OR_PORT --protocol=tcp"
114 | elif ! [ -z $DB_SOCK_OR_PORT ] ; then
115 | EXTRA=" --socket=$DB_SOCK_OR_PORT"
116 | elif ! [ -z $DB_HOSTNAME ] ; then
117 | EXTRA=" --host=$DB_HOSTNAME --protocol=tcp"
118 | fi
119 | fi
120 |
121 | # create database
122 | mysqladmin create $DB_NAME --user="$DB_USER" --password="$DB_PASS"$EXTRA
123 | }
124 |
125 | install_wp
126 | install_test_suite
127 | install_db
128 |
--------------------------------------------------------------------------------
/class-es-wp-date-query.php:
--------------------------------------------------------------------------------
1 | queries as $query ) {
25 | $filter_parts = $this->get_es_subquery( $query, $es_query );
26 | if ( ! empty( $filter_parts ) ) {
27 | // Combine the parts of this subquery.
28 | if ( 1 === count( $filter_parts ) ) {
29 | $filter[] = reset( $filter_parts );
30 | } else {
31 | $filter[] = array(
32 | 'bool' => array(
33 | 'filter' => $filter_parts,
34 | ),
35 | );
36 | }
37 | }
38 | }
39 |
40 | // Combine the subqueries.
41 | if ( 1 === count( $filter ) ) {
42 | $filter = reset( $filter );
43 | } elseif ( ! empty( $filter ) ) {
44 | if ( 'or' === strtolower( $this->relation ) ) {
45 | $relation = 'should';
46 | } else {
47 | $relation = 'filter';
48 | }
49 | $filter = array(
50 | 'bool' => array(
51 | $relation => $filter,
52 | ),
53 | );
54 | } else {
55 | $filter = array();
56 | }
57 |
58 | /**
59 | * Filter the date query WHERE clause.
60 | *
61 | * @param string $where WHERE clause of the date query.
62 | * @param WP_Date_Query $this The WP_Date_Query instance.
63 | */
64 | return apply_filters( 'get_date_dsl', $filter, $this ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound
65 | }
66 |
67 | /**
68 | * Turns a single date subquery into elasticsearch filters.
69 | *
70 | * @param array $query The date subquery.
71 | * @param ES_WP_Query_Wrapper $es_query The ES_WP_Query object.
72 | * @access protected
73 | * @return array
74 | */
75 | protected function get_es_subquery( $query, $es_query ) {
76 | // Ensure $query is an array before proceeding.
77 | if ( ! is_array( $query ) ) {
78 | return array();
79 | }
80 |
81 | // The sub-parts of a $where part.
82 | $filter_parts = array();
83 |
84 | $field = ( ! empty( $query['column'] ) ) ? esc_sql( $query['column'] ) : $this->column;
85 | $field = $this->validate_column( $field );
86 |
87 | // We don't actually want the mysql column here, so we'll remove it.
88 | $field = preg_replace( '/^.*\./', '', $field );
89 |
90 | $compare = $this->get_compare( $query );
91 |
92 | // Range queries, we like range queries.
93 | if ( ! empty( $query['after'] ) || ! empty( $query['before'] ) ) {
94 | $inclusive = ! empty( $query['inclusive'] );
95 |
96 | if ( $inclusive ) {
97 | $lt = 'lte';
98 | $gt = 'gte';
99 | } else {
100 | $lt = 'lt';
101 | $gt = 'gt';
102 | }
103 |
104 | $range = array();
105 |
106 | if ( ! empty( $query['after'] ) ) {
107 | $range[ $gt ] = $this->build_datetime( $query['after'], ! $inclusive );
108 | }
109 |
110 | if ( ! empty( $query['before'] ) ) {
111 | $range[ $lt ] = $this->build_datetime( $query['before'], $inclusive );
112 | }
113 |
114 | if ( ! empty( $range ) ) {
115 | $filter_parts[] = $es_query->dsl_range( $es_query->es_map( $field ), $range );
116 | }
117 | unset( $range );
118 | }
119 |
120 | // Legacy support and field renaming.
121 | if ( isset( $query['monthnum'] ) ) {
122 | $query['month'] = $query['monthnum'];
123 | }
124 | if ( isset( $query['w'] ) ) {
125 | $query['week'] = $query['w'];
126 | }
127 | if ( isset( $query['w'] ) ) {
128 | $query['week'] = $query['w'];
129 | }
130 | if ( isset( $query['dayofyear'] ) ) {
131 | $query['day_of_year'] = $query['dayofyear'];
132 | }
133 | if ( isset( $query['dayofweek'] ) ) {
134 | // We encourage you to store the day_of_week according to ISO-8601 standards.
135 | $day_of_week = 1 === $query['dayofweek'] ? 7 : $query['dayofweek'] - 1;
136 |
137 | // This is, of course, optional. Use this filter to manipualte the value however you'd like.
138 | $query['day_of_week'] = apply_filters( 'es_date_query_dayofweek', $day_of_week, $query['dayofweek'] );
139 | }
140 |
141 | foreach ( array( 'year', 'month', 'week', 'day', 'day_of_year', 'day_of_week' ) as $date_token ) {
142 | if ( isset( $query[ $date_token ] ) ) {
143 | $part = $this->build_dsl_part(
144 | $es_query->es_map( "{$field}.{$date_token}" ),
145 | $query[ $date_token ],
146 | $compare
147 | );
148 | if ( false !== $part ) {
149 | $filter_parts[] = $part;
150 | }
151 | }
152 | }
153 |
154 | // Avoid notices.
155 | $query = wp_parse_args(
156 | $query,
157 | array(
158 | 'hour' => null,
159 | 'minute' => null,
160 | 'second' => null,
161 | )
162 | );
163 |
164 | $time = $this->build_es_time( $compare, $query['hour'], $query['minute'], $query['second'] );
165 | if ( false === $time ) {
166 | foreach ( array( 'hour', 'minute', 'second' ) as $date_token ) {
167 | if ( isset( $query[ $date_token ] ) ) {
168 | $part = $this->build_dsl_part(
169 | $es_query->es_map( "{$field}.{$date_token}" ),
170 | $query[ $date_token ],
171 | $compare
172 | );
173 | if ( false !== $part ) {
174 | $filter_parts[] = $part;
175 | }
176 | }
177 | }
178 | } else {
179 | if ( 1 > $time ) {
180 | $filter_parts[] = $this->build_dsl_part( $es_query->es_map( "{$field}.seconds_from_hour" ), $time, $compare, 'floatval' );
181 | } else {
182 | $filter_parts[] = $this->build_dsl_part( $es_query->es_map( "{$field}.seconds_from_day" ), $time, $compare, 'floatval' );
183 | }
184 | }
185 |
186 | return $filter_parts;
187 | }
188 |
189 | /**
190 | * Builds a MySQL format date/time based on some query parameters.
191 | *
192 | * This is a clone of build_mysql_datetime, but specifically for static usage.
193 | *
194 | * You can pass an array of values (year, month, etc.) with missing parameter values being defaulted to
195 | * either the maximum or minimum values (controlled by the $default_to parameter). Alternatively you can
196 | * pass a string that that will be run through strtotime().
197 | *
198 | * @static
199 | * @access public
200 | *
201 | * @param string|array $datetime An array of parameters or a strotime() string.
202 | * @param string|bool $default_to_max Controls what values default to if they are missing from $datetime. Pass "min" or "max".
203 | * @return string|false A MySQL format date/time or false on failure
204 | */
205 | public static function build_datetime( $datetime, $default_to_max = false ) {
206 | $now = current_time( 'timestamp' );
207 |
208 | if ( ! is_array( $datetime ) ) {
209 | // @todo Timezone issues here possibly
210 | return gmdate( 'Y-m-d H:i:s', strtotime( $datetime, $now ) );
211 | }
212 |
213 | $datetime = array_map( 'absint', $datetime );
214 |
215 | if ( ! isset( $datetime['year'] ) ) {
216 | $datetime['year'] = gmdate( 'Y', $now );
217 | }
218 |
219 | if ( ! isset( $datetime['month'] ) ) {
220 | $datetime['month'] = ( $default_to_max ) ? 12 : 1;
221 | }
222 |
223 | if ( ! isset( $datetime['day'] ) ) {
224 | $datetime['day'] = ( $default_to_max ) ? (int) date( 't', mktime( 0, 0, 0, $datetime['month'], 1, $datetime['year'] ) ) : 1;
225 | }
226 |
227 | if ( ! isset( $datetime['hour'] ) ) {
228 | $datetime['hour'] = ( $default_to_max ) ? 23 : 0;
229 | }
230 |
231 | if ( ! isset( $datetime['minute'] ) ) {
232 | $datetime['minute'] = ( $default_to_max ) ? 59 : 0;
233 | }
234 |
235 | if ( ! isset( $datetime['second'] ) ) {
236 | $datetime['second'] = ( $default_to_max ) ? 59 : 0;
237 | }
238 |
239 | return sprintf( '%04d-%02d-%02d %02d:%02d:%02d', $datetime['year'], $datetime['month'], $datetime['day'], $datetime['hour'], $datetime['minute'], $datetime['second'] );
240 | }
241 |
242 | /**
243 | * Given one or two dates and comparison operators for each, builds a date
244 | * query that encompasses the requested range.
245 | *
246 | * @param string|array $date An array of parameters or a strotime() string.
247 | * @param string $compare The comparison operator for the date.
248 | * @param string|array|null $date2 Optional. An array of parameters or a strotime() string. Defaults to null.
249 | * @param string|null $compare2 Optional. The comparison operator for the date. Defaults to null.
250 | * @access public
251 | * @return array
252 | */
253 | public static function build_date_range( $date, $compare, $date2 = null, $compare2 = null ) {
254 | // If we pass two dates, create a range for both.
255 | if ( isset( $date2 ) && isset( $compare2 ) ) {
256 | return array_merge( self::build_date_range( $date, $compare ), self::build_date_range( $date2, $compare2 ) );
257 | }
258 |
259 | // To improve readability.
260 | $upper_edge = true;
261 | $lower_edge = false;
262 |
263 | switch ( $compare ) {
264 | case '!=':
265 | case '=':
266 | return array(
267 | 'gte' => self::build_datetime( $date, $lower_edge ),
268 | 'lte' => self::build_datetime( $date, $upper_edge ),
269 | );
270 |
271 | case '>':
272 | return array( 'gt' => self::build_datetime( $date, $upper_edge ) );
273 | case '>=':
274 | return array( 'gte' => self::build_datetime( $date, $lower_edge ) );
275 |
276 | case '<':
277 | return array( 'lt' => self::build_datetime( $date, $lower_edge ) );
278 | case '<=':
279 | return array( 'lte' => self::build_datetime( $date, $upper_edge ) );
280 | }
281 | }
282 |
283 | /**
284 | * Builds and validates a value string based on the comparison operator.
285 | *
286 | * @access public
287 | *
288 | * @param string $field The field name.
289 | * @param string|array $value The value.
290 | * @param string $compare The compare operator to use.
291 | * @param string $sanitize Optional. The sanitization function to use. Defaults to 'intval'.
292 | * @return string|int|false The value to be used in DSL or false on error.
293 | */
294 | public function build_dsl_part( $field, $value, $compare, $sanitize = 'intval' ) {
295 | if ( ! isset( $value ) ) {
296 | return false;
297 | }
298 |
299 | $part = false;
300 | switch ( $compare ) {
301 | case 'IN':
302 | case 'NOT IN':
303 | $part = ES_WP_Query_Wrapper::dsl_terms( $field, array_map( $sanitize, (array) $value ) );
304 | break;
305 |
306 | case 'BETWEEN':
307 | case 'NOT BETWEEN':
308 | if ( ! is_array( $value ) ) {
309 | $value = array( $value, $value );
310 | } elseif ( count( $value ) >= 2 && ( ! isset( $value[0] ) || ! isset( $value[1] ) ) ) {
311 | $value = array( array_shift( $value ), array_shift( $value ) );
312 | } elseif ( count( $value ) ) {
313 | $value = reset( $value );
314 | $value = array( $value, $value );
315 | }
316 |
317 | if ( ! isset( $value[0] ) || ! isset( $value[1] ) ) {
318 | return false;
319 | }
320 |
321 | $value = array_map( $sanitize, $value );
322 | sort( $value );
323 |
324 | $part = ES_WP_Query_Wrapper::dsl_range(
325 | $field,
326 | array(
327 | 'gte' => $value[0],
328 | 'lte' => $value[1],
329 | )
330 | );
331 | break;
332 |
333 | case '>':
334 | case '>=':
335 | case '<':
336 | case '<=':
337 | switch ( $compare ) {
338 | case '>':
339 | $operator = 'gt';
340 | break;
341 | case '>=':
342 | $operator = 'gte';
343 | break;
344 | case '<':
345 | $operator = 'lt';
346 | break;
347 | case '<=':
348 | $operator = 'lte';
349 | break;
350 | }
351 | $part = ES_WP_Query_Wrapper::dsl_range( $field, array( $operator => $sanitize( $value ) ) );
352 | break;
353 |
354 | default:
355 | $part = ES_WP_Query_Wrapper::dsl_terms( $field, $sanitize( $value ) );
356 | break;
357 | }
358 |
359 | if ( ! empty( $part ) && in_array( $compare, array( '!=', 'NOT IN', 'NOT BETWEEN' ), true ) ) {
360 | return array(
361 | 'bool' => array(
362 | 'must_not' => $part,
363 | ),
364 | );
365 | } else {
366 | return $part;
367 | }
368 | }
369 |
370 | /**
371 | * Builds a query string for comparing time values (hour, minute, second).
372 | *
373 | * If just hour, minute, or second is set than a normal comparison will be done.
374 | * However if multiple values are passed, a pseudo-decimal time will be created
375 | * in order to be able to accurately compare against.
376 | *
377 | * @access public
378 | *
379 | * @param string $compare The comparison operator. Needs to be pre-validated.
380 | * @param int|null $hour Optional. An hour value (0-23).
381 | * @param int|null $minute Optional. A minute value (0-59).
382 | * @param int|null $second Optional. A second value (0-59).
383 | * @return string|false A query part or false on failure.
384 | */
385 | public function build_es_time( $compare, $hour = null, $minute = null, $second = null ) {
386 | // Complex combined queries aren't supported for multi-value queries.
387 | if ( in_array( $compare, array( 'IN', 'NOT IN', 'BETWEEN', 'NOT BETWEEN' ), true ) ) {
388 | return false;
389 | }
390 |
391 | // Lastly, ignore cases where just one unit is set or $minute is null.
392 | if ( count( array_filter( array( $hour, $minute, $second ), 'is_null' ) ) > 1 || is_null( $minute ) ) {
393 | return false;
394 | }
395 |
396 | // Hour.
397 | if ( ! $hour ) {
398 | $hour = 0;
399 | }
400 |
401 | return mktime( $hour, $minute, $second, 1, 1, 1970 );
402 | }
403 | }
404 |
--------------------------------------------------------------------------------
/class-es-wp-meta-query.php:
--------------------------------------------------------------------------------
1 | sanitize_query() gets called
35 | * amongst other stuff.
36 | */
37 | parent::__construct( $meta_query );
38 |
39 | $this->queries_types_all = $this->queries_types_all_get(
40 | $this->queries
41 | );
42 | }
43 |
44 | /**
45 | * Returns a simplified version of the meta-queries given as an argument.
46 | * The simplification pertains to returning only the key/type values of
47 | * each part of the meta-query. Also, the simplification takes care to flatten
48 | * out the result.
49 | *
50 | * If the meta-query is composed of multiple joining queries, these are
51 | * processed by recursively walking through them, and calling this
52 | * function to process each.
53 | *
54 | * @param array $meta_clauses Meta clauses to be processed.
55 | * @access protected
56 | * @return array All queries, but only with key and type key/values pairs.
57 | */
58 | protected function queries_types_all_get( $meta_clauses ) {
59 | $queries_types = array();
60 |
61 | if ( ! is_array( $meta_clauses ) ) {
62 | return array();
63 | }
64 |
65 | if ( empty( $meta_clauses ) ) {
66 | return $meta_clauses;
67 | }
68 |
69 | foreach ( array_keys( $meta_clauses ) as $meta_clause_key ) {
70 | if ( $this->is_first_order_clause(
71 | $meta_clauses[ $meta_clause_key ]
72 | ) ) {
73 | /*
74 | * Save this part of the meta-query, but keep only
75 | * the key/type pairs.
76 | *
77 | *
78 | * Note: If there are multiple sub-queries with the same
79 | * key, this will overwrite the previous one (if any).
80 | * As a result, the last one will be the one who prevails.
81 | */
82 |
83 |
84 | if ( isset( $meta_clauses[ $meta_clause_key ]['key'] ) ) {
85 | $queries_types[
86 | $meta_clause_key
87 | ] = array(
88 | 'key' => $meta_clauses[ $meta_clause_key ]['key'],
89 | );
90 | } else {
91 | $queries_types[
92 | $meta_clause_key
93 | ] = array(
94 | 'key' => $meta_clause_key,
95 | );
96 | }
97 |
98 |
99 | if ( isset(
100 | $meta_clauses[ $meta_clause_key ]['type']
101 | ) ) {
102 | $queries_types[ $meta_clause_key ]['type'] =
103 | $meta_clauses[ $meta_clause_key ]['type'];
104 | }
105 | } else {
106 | /*
107 | * Recursively process the clause.
108 | */
109 | $recursive_result = $this->queries_types_all_get(
110 | $meta_clauses[ $meta_clause_key ]
111 | );
112 |
113 | /*
114 | * Only save the result if an array, and
115 | * it is not empty.
116 | */
117 | if (
118 | ( is_array( $recursive_result ) ) &&
119 | ( ! empty( $recursive_result ) )
120 | ) {
121 | $queries_types = array_merge(
122 | $queries_types,
123 | $recursive_result
124 | );
125 | }
126 | }
127 | }
128 |
129 | return $queries_types;
130 | }
131 |
132 | /**
133 | * Turns an array of meta query parameters into ES Query DSL
134 | *
135 | * @access public
136 | *
137 | * @param object $es_query Any object which extends ES_WP_Query_Wrapper.
138 | * @param string $type Type of meta. Currently, only 'post' is supported.
139 | * @return array ES filters
140 | */
141 | public function get_dsl( $es_query, $type ) {
142 | // Currently only 'post' is supported.
143 | if ( 'post' !== $type ) {
144 | return false;
145 | }
146 |
147 | $this->es_query = $es_query;
148 |
149 | $filters = $this->get_dsl_clauses();
150 |
151 | return apply_filters_ref_array( 'get_meta_dsl', array( $filters, $this->queries, $type, $this->es_query ) ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound
152 | }
153 |
154 | /**
155 | * Generate ES Filter clauses to be appended to a main query.
156 | *
157 | * Called by the public {@see ES_WP_Meta_Query::get_dsl()}, this method
158 | * is abstracted out to maintain parity with the other Query classes.
159 | *
160 | * @access protected
161 | *
162 | * @return array
163 | */
164 | protected function get_dsl_clauses() {
165 | /*
166 | * $queries are passed by reference to
167 | * `ES_WP_Meta_Query::get_dsl_for_query()` for recursion. To keep
168 | * $this->queries unaltered, pass a copy.
169 | */
170 | $queries = $this->queries;
171 | return $this->get_dsl_for_query( $queries );
172 | }
173 |
174 | /**
175 | * Generate ES filters for a single query array.
176 | *
177 | * If nested subqueries are found, this method recurses the tree to produce
178 | * the properly nested DSL.
179 | *
180 | * @access protected
181 | *
182 | * @param array $query Query to parse, passed by reference.
183 | * @return array Array containing nested ES filter clauses.
184 | */
185 | protected function get_dsl_for_query( &$query ) {
186 | $filters = array();
187 |
188 | foreach ( $query as $key => &$clause ) {
189 | if ( 'relation' === $key ) {
190 | $relation = $query['relation'];
191 | } elseif ( is_array( $clause ) ) {
192 | if ( $this->is_first_order_clause( $clause ) ) {
193 | // This is a first-order clause.
194 | $filters[] = $this->get_dsl_for_clause( $clause, $query, $key );
195 | } else {
196 | // This is a subquery, so we recurse.
197 | $filters[] = $this->get_dsl_for_query( $clause );
198 | }
199 | }
200 | }
201 |
202 | // Filter to remove empties.
203 | $filters = array_filter( $filters );
204 | $this->clauses = array_filter( $this->clauses );
205 |
206 | if ( ! empty( $relation ) && 'or' === strtolower( $relation ) ) {
207 | $relation = 'should';
208 | } else {
209 | $relation = 'filter';
210 | }
211 |
212 | if ( count( $filters ) > 1 ) {
213 | $filters = array(
214 | 'bool' => array(
215 | $relation => $filters,
216 | ),
217 | );
218 | } elseif ( ! empty( $filters ) ) {
219 | $filters = reset( $filters );
220 | }
221 |
222 | return $filters;
223 | }
224 |
225 | /**
226 | * Generate ES filter clauses for a first-order query clause.
227 | *
228 | * "First-order" means that it's an array with a 'key' or 'value'.
229 | *
230 | * @access public
231 | *
232 | * @param array $clause Query clause, passed by reference.
233 | * @param array $query Parent query array.
234 | * @param string $clause_key Optional. The array key used to name the
235 | * clause in the original `$meta_query`
236 | * parameters. If not provided, a key will be
237 | * generated automatically.
238 | * @return array ES filter clause component.
239 | */
240 | public function get_dsl_for_clause( &$clause, $query, $clause_key = '' ) {
241 | // Key must be a string, so fallback for clause keys is 'meta-clause'.
242 | if ( is_int( $clause_key ) || ! $clause_key ) {
243 | $clause_key = 'meta-clause';
244 | }
245 |
246 | // Ensure unique clause keys, so none are overwritten.
247 | $iterator = 1;
248 | $clause_key_base = $clause_key;
249 | while ( isset( $this->clauses[ $clause_key ] ) ) {
250 | $clause_key = $clause_key_base . '-' . $iterator;
251 | $iterator++;
252 | }
253 |
254 | // Split out 'exists' and 'not exists' queries. These may also be
255 | // queries missing a value or with an empty array as the value.
256 | if ( isset( $clause['compare'] ) && ! empty( $clause['value'] ) ) {
257 | if ( 'EXISTS' === strtoupper( $clause['compare'] ) ) {
258 | $clause['compare'] = is_array( $clause['value'] ) ? 'IN' : '=';
259 | } elseif ( 'NOT EXISTS' === strtoupper( $clause['compare'] ) ) {
260 | unset( $clause['value'] );
261 | }
262 | }
263 |
264 | if ( ( isset( $clause['value'] ) && is_array( $clause['value'] ) && empty( $clause['value'] ) ) || ( ! array_key_exists( 'value', $clause ) && ! empty( $clause['key'] ) ) ) {
265 | $this->clauses[ $clause_key ] =& $clause;
266 | if ( isset( $clause['compare'] ) && 'NOT EXISTS' === strtoupper( $clause['compare'] ) ) {
267 | return $this->es_query->dsl_missing( $this->es_query->meta_map( trim( $clause['key'] ) ) );
268 | } else {
269 | return $this->es_query->dsl_exists( $this->es_query->meta_map( trim( $clause['key'] ) ) );
270 | }
271 | }
272 |
273 | $clause['key'] = isset( $clause['key'] ) ? trim( $clause['key'] ) : '*';
274 |
275 | if ( array_key_exists( 'value', $clause ) && is_null( $clause['value'] ) ) {
276 | $clause['value'] = '';
277 | }
278 |
279 | $clause['value'] = isset( $clause['value'] ) ? $clause['value'] : null;
280 |
281 | if ( isset( $clause['compare'] ) ) {
282 | $clause['compare'] = strtoupper( $clause['compare'] );
283 | } else {
284 | $clause['compare'] = is_array( $clause['value'] ) ? 'IN' : '=';
285 | }
286 |
287 | if ( in_array( $clause['compare'], array( 'IN', 'NOT IN', 'BETWEEN', 'NOT BETWEEN' ), true ) ) {
288 | if ( ! is_array( $clause['value'] ) ) {
289 | $clause['value'] = preg_split( '/[,\s]+/', $clause['value'] );
290 | }
291 |
292 | if ( empty( $clause['value'] ) ) {
293 | // This compare type requires an array of values. If we don't
294 | // have one, we bail on this query.
295 | return array();
296 | }
297 | } else {
298 | $clause['value'] = trim( $clause['value'] );
299 | }
300 |
301 | // Store the clause in our flat array.
302 | $this->clauses[ $clause_key ] =& $clause;
303 |
304 | if ( '*' === $clause['key'] && ! in_array( $clause['compare'], array( '=', '!=', 'LIKE', 'NOT LIKE' ), true ) ) {
305 | return apply_filters( 'es_meta_query_keyless_query', array(), $clause['value'], $clause['compare'], $this, $this->es_query );
306 | }
307 |
308 | $clause['type'] = $this->get_cast_for_type( isset( $clause['type'] ) ? $clause['type'] : '' );
309 |
310 | // Allow adapters to normalize meta values (like `strtolower` if mapping to `raw_lc`).
311 | $clause['value'] = apply_filters( 'es_meta_query_meta_value', $clause['value'], $clause['key'], $clause['compare'], $clause['type'] );
312 |
313 | switch ( $clause['compare'] ) {
314 | case '>':
315 | case '>=':
316 | case '<':
317 | case '<=':
318 | switch ( $clause['compare'] ) {
319 | case '>':
320 | $operator = 'gt';
321 | break;
322 | case '>=':
323 | $operator = 'gte';
324 | break;
325 | case '<':
326 | $operator = 'lt';
327 | break;
328 | case '<=':
329 | $operator = 'lte';
330 | break;
331 | }
332 | $filter = $this->es_query->dsl_range( $this->es_query->meta_map( $clause['key'], $clause['type'] ), array( $operator => $clause['value'] ) );
333 | break;
334 |
335 | case 'LIKE':
336 | case 'NOT LIKE':
337 | if ( '*' === $clause['key'] ) {
338 | $filter = $this->es_query->dsl_multi_match( $this->es_query->meta_map( $clause['key'], 'analyzed' ), $clause['value'] );
339 | } else {
340 | $filter = $this->es_query->dsl_match( $this->es_query->meta_map( $clause['key'], 'analyzed' ), $clause['value'] );
341 | }
342 | break;
343 |
344 | case 'BETWEEN':
345 | case 'NOT BETWEEN':
346 | // These may produce unexpected results depending on how your data is indexed.
347 | $clause['value'] = array_slice( $clause['value'], 0, 2 );
348 | if ( 'DATETIME' === $clause['type'] ) {
349 | $date1 = strtotime( $clause['value'][0] );
350 | $date2 = strtotime( $clause['value'][1] );
351 | if ( $date1 && $date2 ) {
352 | $clause['value'] = array( $date1, $date2 );
353 | sort( $clause['value'] );
354 | $filter = $this->es_query->dsl_range(
355 | $this->es_query->meta_map( $clause['key'], $clause['type'] ),
356 | ES_WP_Date_Query::build_date_range( $clause['value'][0], '>=', $clause['value'][1], '<=' )
357 | );
358 | }
359 | } else {
360 | natcasesort( $clause['value'] );
361 | $filter = $this->es_query->dsl_range(
362 | $this->es_query->meta_map( $clause['key'], $clause['type'] ),
363 | array(
364 | 'gte' => $clause['value'][0],
365 | 'lte' => $clause['value'][1],
366 | )
367 | );
368 | }
369 | break;
370 |
371 | case 'REGEXP':
372 | case 'NOT REGEXP':
373 | case 'RLIKE':
374 | _doing_it_wrong( 'ES_WP_Query', esc_html__( 'ES_WP_Query does not support regular expression meta queries.', 'es-wp-query' ), '0.1' );
375 | // Empty out $clause, since this will be disregarded.
376 | $clause = array();
377 | return array();
378 |
379 | default:
380 | if ( '*' === $clause['key'] ) {
381 | $filter = $this->es_query->dsl_multi_match( $this->es_query->meta_map( $clause['key'], $clause['type'] ), $clause['value'] );
382 | } else {
383 | $filter = $this->es_query->dsl_terms( $this->es_query->meta_map( $clause['key'], $clause['type'] ), $clause['value'] );
384 | }
385 | break;
386 |
387 | }
388 |
389 | if ( ! empty( $filter ) ) {
390 | // To maintain parity with WP_Query, if we're doing a negation
391 | // query, we still only query posts where the meta key exists.
392 | if ( in_array( $clause['compare'], array( 'NOT IN', '!=', 'NOT BETWEEN', 'NOT LIKE' ), true ) ) {
393 | return array(
394 | 'bool' => array(
395 | 'filter' => array(
396 | $this->es_query->dsl_exists( $this->es_query->meta_map( $clause['key'] ) ),
397 | ),
398 | 'must_not' => $filter,
399 | ),
400 | );
401 | } else {
402 | return $filter;
403 | }
404 | }
405 |
406 | }
407 |
408 | /**
409 | * Get the ES mapping suffix for the given type.
410 | *
411 | * @param string $type Meta_Query type. See Meta_Query docs.
412 | * @return string
413 | */
414 | public function get_cast_for_type( $type = '' ) {
415 | $type = preg_replace( '/^([A-Z]+).*$/', '$1', strtoupper( $type ) );
416 | switch ( $type ) {
417 | case 'NUMERIC':
418 | return 'long';
419 | case 'SIGNED':
420 | return 'long';
421 | case 'UNSIGNED':
422 | return 'long';
423 | case 'BINARY':
424 | return 'boolean';
425 | case 'DECIMAL':
426 | return 'double';
427 | case 'DATE':
428 | return 'date';
429 | case 'DATETIME':
430 | return 'datetime';
431 | case 'TIME':
432 | return 'time';
433 | }
434 | return '';
435 | }
436 | }
437 |
--------------------------------------------------------------------------------
/class-es-wp-query-shoehorn.php:
--------------------------------------------------------------------------------
1 | true`, use Elasticsearch to run the meat of the query.
25 | * This is fires on the "pre_get_posts" action.
26 | *
27 | * @param WP_Query $query - Current full WP_Query object.
28 | * @return void
29 | */
30 | function es_wp_query_shoehorn( &$query ) {
31 | // Prevent infinite loops!
32 | if ( $query instanceof ES_WP_Query ) {
33 | return;
34 | }
35 |
36 | if ( ! empty( $query->get( 'es' ) ) ) {
37 | // Backup the conditionals to restore later.
38 | $conditionals = array(
39 | 'is_single' => false,
40 | 'is_preview' => false,
41 | 'is_page' => false,
42 | 'is_archive' => false,
43 | 'is_date' => false,
44 | 'is_year' => false,
45 | 'is_month' => false,
46 | 'is_day' => false,
47 | 'is_time' => false,
48 | 'is_author' => false,
49 | 'is_category' => false,
50 | 'is_tag' => false,
51 | 'is_tax' => false,
52 | 'is_search' => false,
53 | 'is_feed' => false,
54 | 'is_comment_feed' => false,
55 | 'is_trackback' => false,
56 | 'is_home' => false,
57 | 'is_404' => false,
58 | 'is_comments_popup' => false,
59 | 'is_paged' => false,
60 | 'is_admin' => false,
61 | 'is_attachment' => false,
62 | 'is_singular' => false,
63 | 'is_robots' => false,
64 | 'is_posts_page' => false,
65 | 'is_post_type_archive' => false,
66 | );
67 | foreach ( $conditionals as $key => $value ) {
68 | $conditionals[ $key ] = $query->$key;
69 | }
70 |
71 | // Backup the query args to restore later.
72 | $query_args = $query->query;
73 |
74 | /*
75 | * Run this query through ES. By passing `WP_Query::$query` along to the
76 | * subquery, we ensure that the subquery is as similar to the original
77 | * query as possible.
78 | */
79 | $es_query_args = $query->query;
80 | $es_query_args['fields'] = 'ids';
81 | $es_query_args['es_is_main_query'] = $query->is_main_query();
82 | $es_query = new ES_WP_Query( $es_query_args );
83 |
84 | // Make the post query use the post IDs from the ES results instead.
85 | $query->parse_query(
86 | array(
87 | 'post_type' => $query->get( 'post_type' ),
88 | 'post_status' => $query->get( 'post_status' ),
89 | 'post__in' => $es_query->posts,
90 | 'posts_per_page' => $es_query->post_count,
91 | 'fields' => $query->get( 'fields' ),
92 | 'orderby' => 'post__in',
93 | 'order' => 'ASC',
94 | )
95 | );
96 |
97 | // Reinsert all the conditionals from the original query.
98 | foreach ( $conditionals as $key => $value ) {
99 | $query->$key = $value;
100 | }
101 |
102 | new ES_WP_Query_Shoehorn( $query, $es_query, $query_args );
103 | }
104 | }
105 | add_action( 'pre_get_posts', 'es_wp_query_shoehorn', 1000 );
106 |
107 |
108 | /**
109 | * Add an 'es' query var to WP_Query which offers seamless integration.
110 | *
111 | * It's worth noting the bug in https://core.trac.wordpress.org/ticket/21169,
112 | * as it could potentially play a role here. Each hook removes itself, and if
113 | * it were the only action or filter at that priority, and there was another at
114 | * a later priority (e.g. 1001), that one wouldn't fire.
115 | */
116 | class ES_WP_Query_Shoehorn {
117 |
118 | /**
119 | * Keeps track of a hash of the query arguments.
120 | *
121 | * @access private
122 | * @var string
123 | */
124 | private $hash;
125 |
126 | /**
127 | * Whether to execute the found_posts query or not.
128 | *
129 | * @access private
130 | * @var bool
131 | */
132 | private $do_found_posts = true;
133 |
134 | /**
135 | * Keeps track of the number of posts returned by this query.
136 | *
137 | * @access private
138 | * @var int
139 | */
140 | private $post_count;
141 |
142 | /**
143 | * Keeps track of the total number of found posts matching the query.
144 | *
145 | * @access private
146 | * @var int
147 | */
148 | private $found_posts;
149 |
150 | /**
151 | * Keeps track of the original query args from the query.
152 | *
153 | * @access private
154 | * @var array
155 | */
156 | private $original_query_args;
157 |
158 | /**
159 | * Keeps track of the number of posts per page from the query.
160 | *
161 | * @access private
162 | * @var int
163 | */
164 | private $posts_per_page;
165 |
166 | /**
167 | * ES_WP_Query_Shoehorn constructor.
168 | *
169 | * @param WP_Query $query The WP_Query object to augment.
170 | * @param ES_WP_Query $es_query The ES_WP_Query object to augment.
171 | * @param array $query_args Arguments passed to the original query.
172 | * @access public
173 | */
174 | public function __construct( &$query, &$es_query, $query_args ) {
175 | $this->hash = spl_object_hash( $query );
176 | $this->posts_per_page = $es_query->get( 'posts_per_page' );
177 |
178 | if ( $query->get( 'no_found_rows' ) || -1 === intval( $query->get( 'posts_per_page' ) ) || true === $query->get( 'nopaging' ) ) {
179 | $this->do_found_posts = false;
180 | } else {
181 | $this->do_found_posts = true;
182 | $this->found_posts = $es_query->found_posts;
183 | }
184 | $this->post_count = $es_query->post_count;
185 | $this->original_query_args = $query_args;
186 | $this->add_query_hooks();
187 | }
188 |
189 | /**
190 | * Add hooks to WP_Query to modify the bits that we're replacing.
191 | *
192 | * Each hook removes itself when it fires so this doesn't affect all WP_Query requests.
193 | *
194 | * @return void
195 | */
196 | public function add_query_hooks() {
197 | if ( $this->post_count ) {
198 | // Kills the FOUND_ROWS() database query.
199 | add_filter( 'found_posts_query', array( $this, 'filter__found_posts_query' ), 1000, 2 );
200 | // Since the FOUND_ROWS() query was killed, we need to supply the total number of found posts.
201 | add_filter( 'found_posts', array( $this, 'filter__found_posts' ), 1000, 2 );
202 | }
203 |
204 | add_filter( 'posts_request', array( $this, 'filter__posts_request' ), 1000, 2 );
205 | }
206 |
207 | /**
208 | * Kill the found_posts query if this was run with ES.
209 | *
210 | * @param string $sql SQL query to kill.
211 | * @param object $query WP_Query object.
212 | * @return string
213 | */
214 | public function filter__found_posts_query( $sql, $query ) {
215 | if ( spl_object_hash( $query ) === $this->hash ) {
216 | remove_filter( 'found_posts_query', array( $this, 'filter__found_posts_query' ), 1000, 2 );
217 | if ( $this->do_found_posts ) {
218 | return '';
219 | }
220 | }
221 | return $sql;
222 | }
223 |
224 | /**
225 | * If we killed the found_posts query, set the found posts via ES.
226 | *
227 | * @param int $found_posts The total number of posts found when running the query.
228 | * @param object $query WP_Query object.
229 | * @return int
230 | */
231 | public function filter__found_posts( $found_posts, $query ) {
232 | if ( spl_object_hash( $query ) === $this->hash ) {
233 | remove_filter( 'found_posts', array( $this, 'filter__found_posts' ), 1000, 2 );
234 | if ( $this->do_found_posts ) {
235 | return $this->found_posts;
236 | }
237 | }
238 | return $found_posts;
239 | }
240 |
241 | /**
242 | * IF the ES query didn't find any posts, use a query which returns no results.
243 | *
244 | * @param string $sql The SQL query to get posts.
245 | * @param object $query WP_Query object.
246 | * @return string The SQL query to get posts.
247 | */
248 | public function filter__posts_request( $sql, $query ) {
249 | if ( spl_object_hash( $query ) === $this->hash ) {
250 | remove_filter( 'posts_request', array( $this, 'filter__posts_request' ), 1000, 2 );
251 | $this->reboot_query_vars( $query );
252 |
253 | if ( ! $this->post_count ) {
254 | global $wpdb;
255 | return "SELECT * FROM {$wpdb->posts} WHERE 1=0 /* ES_WP_Query Shoehorn */";
256 | } elseif ( ! empty( $sql ) ) {
257 | return $sql . ' /* ES_WP_Query Shoehorn */';
258 | }
259 | }
260 | return $sql;
261 | }
262 |
263 | /**
264 | * Restore query args/vars to their original glory. This allows us to run
265 | * $query->get_posts() multiple times.
266 | *
267 | * @access private
268 | *
269 | * @param object $query WP_Query object, passed by reference.
270 | * @return void
271 | */
272 | private function reboot_query_vars( &$query ) {
273 | $q =& $query->query_vars;
274 |
275 | // Remove custom query vars used for the ES query in es_wp_query_shoehorn().
276 | $current_query_vars = $q;
277 | unset(
278 | $current_query_vars['post_type'],
279 | $current_query_vars['post_status'],
280 | $current_query_vars['post__in'],
281 | $current_query_vars['posts_per_page'],
282 | $current_query_vars['fields'],
283 | $current_query_vars['orderby'],
284 | $current_query_vars['order']
285 | );
286 |
287 | $query->query = $this->original_query_args;
288 | $q = $query->query;
289 | $query->parse_query();
290 | $q = array_merge( $current_query_vars, $q );
291 |
292 | // Restore some necessary defaults if we zapped 'em.
293 | if ( empty( $q['posts_per_page'] ) ) {
294 | $q['posts_per_page'] = $this->posts_per_page;
295 | }
296 |
297 | // Allow sitemap.xml redirect to wp-sitemap.xml page.
298 | if ( 'sitemap.xml' === $q['pagename'] ) {
299 | $q['pagename'] = sanitize_title_for_query( wp_basename( $q['pagename'] ) );
300 | }
301 |
302 | // Restore the author ID which is normally added during get_posts() in WP_Query.
303 | // Required for handle_404() in WP class to not mark empty author archives as 404s.
304 | if ( $query->is_author() && ! empty( $q['author_name'] ) ) {
305 | if ( false !== strpos( $q['author_name'], '/' ) ) {
306 | $q['author_name'] = explode( '/', $q['author_name'] );
307 | if ( $q['author_name'][ count( $q['author_name'] ) - 1 ] ) {
308 | $q['author_name'] = $q['author_name'][ count( $q['author_name'] ) - 1 ]; // no trailing slash.
309 | } else {
310 | $q['author_name'] = $q['author_name'][ count( $q['author_name'] ) - 2 ]; // there was a trailing slash.
311 | }
312 | }
313 | $author = get_user_by( 'slug', sanitize_title_for_query( $q['author_name'] ) );
314 |
315 | if ( isset( $author->ID ) ) {
316 | $q['author'] = $author->ID;
317 | }
318 | }
319 | }
320 | }
321 |
--------------------------------------------------------------------------------
/class-es-wp-tax-query.php:
--------------------------------------------------------------------------------
1 | queries );
29 | $q->relation = $tax_query->relation;
30 | return $q;
31 | }
32 |
33 | /**
34 | * Get a (light) ES filter that will always produce no results. This allows
35 | * individual tax query clauses to fail without breaking the rest of them.
36 | *
37 | * @return array ES term query for post_id:0.
38 | */
39 | protected function get_no_results_clause() {
40 | return $this->es_query->dsl_terms( $this->es_query->es_map( 'post_id' ), 0 );
41 | }
42 |
43 | /**
44 | * Turns an array of tax query parameters into ES Query DSL
45 | *
46 | * @access public
47 | *
48 | * @param object $es_query Any object which extends ES_WP_Query_Wrapper.
49 | * @return array ES filters
50 | */
51 | public function get_dsl( $es_query ) {
52 | $this->es_query = $es_query;
53 |
54 | $filters = $this->get_dsl_clauses();
55 |
56 | return apply_filters_ref_array( 'es_wp_tax_query_dsl', array( $filters, $this->queries, $this->es_query ) );
57 | }
58 |
59 | /**
60 | * Generate ES Filter clauses to be appended to a main query.
61 | *
62 | * Called by the public {@see ES_WP_Meta_Query::get_dsl()}, this method
63 | * is abstracted out to maintain parity with the other Query classes.
64 | *
65 | * @access protected
66 | *
67 | * @return array
68 | */
69 | protected function get_dsl_clauses() {
70 | /*
71 | * $queries are passed by reference to
72 | * `ES_WP_Meta_Query::get_dsl_for_query()` for recursion. To keep
73 | * $this->queries unaltered, pass a copy.
74 | */
75 | $queries = $this->queries;
76 | return $this->get_dsl_for_query( $queries );
77 | }
78 |
79 | /**
80 | * Generate ES filters for a single query array.
81 | *
82 | * If nested subqueries are found, this method recurses the tree to produce
83 | * the properly nested DSL.
84 | *
85 | * @access protected
86 | *
87 | * @param array $query Query to parse, passed by reference.
88 | * @return boolarray Array containing nested ES filter clauses on success or
89 | * false on error.
90 | */
91 | protected function get_dsl_for_query( &$query ) {
92 | $filters = array();
93 |
94 | foreach ( $query as $key => &$clause ) {
95 | if ( 'relation' === $key ) {
96 | $relation = $query['relation'];
97 | } elseif ( is_array( $clause ) ) {
98 | if ( $this->is_first_order_clause( $clause ) ) {
99 | // This is a first-order clause.
100 | $filters[] = $this->get_dsl_for_clause( $clause, $query );
101 | } else {
102 | // This is a subquery, so we recurse.
103 | $filters[] = $this->get_dsl_for_query( $clause );
104 | }
105 | }
106 | }
107 |
108 | // Filter to remove empties.
109 | $filters = array_values( array_filter( $filters ) );
110 |
111 | if ( ! empty( $relation ) && 'or' === strtolower( $relation ) ) {
112 | $relation = 'should';
113 | } else {
114 | $relation = 'filter';
115 | }
116 |
117 | if ( count( $filters ) > 1 ) {
118 | $filters = array(
119 | 'bool' => array(
120 | $relation => $filters,
121 | ),
122 | );
123 | } elseif ( ! empty( $filters ) ) {
124 | $filters = reset( $filters );
125 | }
126 |
127 | return $filters;
128 | }
129 |
130 | /**
131 | * Generate ES filter clauses for a first-order query clause.
132 | *
133 | * "First-order" means that it's an array with a 'key' or 'value'.
134 | *
135 | * @access public
136 | *
137 | * @param array $clause Query clause, passed by reference.
138 | * @param array $query Parent query array.
139 | * @return bool|array ES filter clause on success, or false on error.
140 | */
141 | public function get_dsl_for_clause( &$clause, $query ) {
142 | $current_filter = null;
143 |
144 | $this->clean_query( $clause );
145 |
146 | if ( is_wp_error( $clause ) ) {
147 | return $this->get_no_results_clause();
148 | }
149 |
150 | // If the comparison is EXISTS or NOT EXISTS, handle that first since
151 | // it's quick and easy.
152 | if ( 'EXISTS' === $clause['operator'] || 'NOT EXISTS' === $clause['operator'] ) {
153 | if ( empty( $clause['taxonomy'] ) ) {
154 | return $this->get_no_results_clause();
155 | }
156 |
157 | if ( 'EXISTS' === $clause['operator'] ) {
158 | return $this->es_query->dsl_exists( $this->es_query->tax_map( $clause['taxonomy'], 'term_id' ) );
159 | } elseif ( 'NOT EXISTS' === $clause['operator'] ) {
160 | return $this->es_query->dsl_missing( $this->es_query->tax_map( $clause['taxonomy'], 'term_id' ) );
161 | }
162 | }
163 |
164 | if ( 'AND' === $clause['operator'] ) {
165 | $terms_method = array( $this->es_query, 'dsl_all_terms' );
166 | } else {
167 | $terms_method = array( $this->es_query, 'dsl_terms' );
168 | }
169 |
170 | if ( empty( $clause['terms'] ) ) {
171 | if ( 'NOT IN' === $clause['operator'] || 'AND' === $clause['operator'] ) {
172 | return array();
173 | } elseif ( 'IN' === $clause['operator'] ) {
174 | return $this->get_no_results_clause();
175 | }
176 | }
177 |
178 | switch ( $clause['field'] ) {
179 | case 'slug':
180 | case 'name':
181 | foreach ( $clause['terms'] as &$term ) {
182 | /*
183 | * 0 is the $term_id parameter. We don't have a term ID yet, but it doesn't
184 | * matter because `sanitize_term_field()` ignores the $term_id param when the
185 | * context is 'db'.
186 | */
187 | $term = sanitize_term_field( $clause['field'], $term, 0, $clause['taxonomy'], 'db' );
188 |
189 | /**
190 | * Allow adapters to normalize term value (like `strtolower` if mapping to
191 | * `raw_lc`).
192 | *
193 | * The dynamic portion of the filter name, `$clause['field']`, refers to the
194 | * term field.
195 | *
196 | * @param mixed $term Term's slug or name sanitized using
197 | * `sanitize_term_field` function for db context.
198 | * @param string $taxonomy Term's taxonomy slug.
199 | */
200 | $term = apply_filters( "es_tax_query_term_{$clause['field']}", $term, $clause['taxonomy'] );
201 |
202 | }
203 | $current_filter = call_user_func( $terms_method, $this->es_query->tax_map( $clause['taxonomy'], 'term_' . $clause['field'] ), $clause['terms'] );
204 | break;
205 |
206 | case 'term_taxonomy_id':
207 | if ( ! empty( $clause['taxonomy'] ) ) {
208 | $current_filter = call_user_func( $terms_method, $this->es_query->tax_map( $clause['taxonomy'], 'term_tt_id' ), $clause['terms'] );
209 | } else {
210 | $matches = array();
211 | foreach ( $clause['terms'] as &$term ) {
212 | $matches[] = $this->es_query->dsl_multi_match( $this->es_query->tax_map( '*', 'term_tt_id' ), $term );
213 | }
214 | if ( count( $matches ) > 1 ) {
215 | $current_filter = array(
216 | 'bool' => array(
217 | ( 'AND' === $clause['operator'] ? 'filter' : 'should' ) => $matches,
218 | ),
219 | );
220 | } else {
221 | $current_filter = reset( $matches );
222 | }
223 | }
224 |
225 | break;
226 |
227 | default:
228 | $terms = array_map( 'absint', array_values( $clause['terms'] ) );
229 | $current_filter = call_user_func( $terms_method, $this->es_query->tax_map( $clause['taxonomy'], 'term_id' ), $terms );
230 | break;
231 | }
232 |
233 | if ( 'NOT IN' === $clause['operator'] ) {
234 | return array(
235 | 'bool' => array(
236 | 'must_not' => $current_filter,
237 | ),
238 | );
239 | } else {
240 | return $current_filter;
241 | }
242 | }
243 |
244 | /**
245 | * Validates a single query.
246 | *
247 | * This is copied from core verbatim, because the core method is private.
248 | *
249 | * @access private
250 | *
251 | * @param array $query The single query.
252 | */
253 | private function clean_query( &$query ) {
254 | if ( empty( $query['taxonomy'] ) ) {
255 | if ( 'term_taxonomy_id' !== $query['field'] ) {
256 | $query = new WP_Error( 'Invalid taxonomy' );
257 | return;
258 | }
259 |
260 | // So long as there are shared terms, include_children requires that a taxonomy is set.
261 | $query['include_children'] = false;
262 | } elseif ( ! taxonomy_exists( $query['taxonomy'] ) ) {
263 | $query = new WP_Error( 'Invalid taxonomy' );
264 | return;
265 | }
266 |
267 | $query['terms'] = array_values( array_unique( (array) $query['terms'] ) );
268 |
269 | if ( is_taxonomy_hierarchical( $query['taxonomy'] ) && $query['include_children'] ) {
270 | $this->transform_query( $query, 'term_id' );
271 |
272 | if ( is_wp_error( $query ) ) {
273 | return;
274 | }
275 |
276 | $children = array();
277 | foreach ( $query['terms'] as $term ) {
278 | $children = array_merge( $children, get_term_children( $term, $query['taxonomy'] ) );
279 | $children[] = $term;
280 | }
281 | $query['terms'] = $children;
282 | }
283 |
284 | // If we have a term_taxonomy_id, use mysql, as that's almost certainly not stored in ES.
285 | // However, you can override this.
286 | if ( 'term_taxonomy_id' === $query['field'] && ! empty( $query['taxonomy'] ) ) {
287 | if ( apply_filters( 'es_use_mysql_for_term_taxonomy_id', true ) ) {
288 | $this->transform_query( $query, 'term_id' );
289 | }
290 | }
291 | }
292 |
293 | /**
294 | * Transforms a single query, from one field to another.
295 | *
296 | * @param array $query The single query.
297 | * @param string $resulting_field The resulting field.
298 | */
299 | public function transform_query( &$query, $resulting_field ) {
300 | if ( empty( $query['terms'] ) ) {
301 | return;
302 | }
303 |
304 | if ( $query['field'] === $resulting_field ) {
305 | return;
306 | }
307 |
308 | $resulting_field = sanitize_key( $resulting_field );
309 |
310 | // Empty 'terms' always results in a null transformation.
311 | $terms = array_values( array_filter( $query['terms'] ) );
312 | if ( empty( $terms ) ) {
313 | $query['terms'] = array();
314 | $query['field'] = $resulting_field;
315 | return;
316 | }
317 |
318 | $args = array(
319 | 'get' => 'all',
320 | 'number' => 0,
321 | 'taxonomy' => $query['taxonomy'],
322 | 'update_term_meta_cache' => false,
323 | 'orderby' => 'none',
324 | );
325 |
326 | // Term query parameter name depends on the 'field' being searched on.
327 | switch ( $query['field'] ) {
328 | case 'slug':
329 | $args['slug'] = $terms;
330 | break;
331 | case 'name':
332 | $args['name'] = $terms;
333 | break;
334 | case 'term_taxonomy_id':
335 | $args['term_taxonomy_id'] = $terms;
336 | break;
337 | default:
338 | $args['include'] = wp_parse_id_list( $terms );
339 | break;
340 | }
341 |
342 | if ( ! is_taxonomy_hierarchical( $query['taxonomy'] ) ) {
343 | $args['number'] = count( $terms );
344 | }
345 |
346 | $term_query = new WP_Term_Query();
347 | $term_list = $term_query->query( $args );
348 |
349 | if ( is_wp_error( $term_list ) ) {
350 | $query = $term_list;
351 | return;
352 | }
353 |
354 | if ( 'AND' === $query['operator'] && count( $term_list ) < count( $query['terms'] ) ) {
355 | $query = new WP_Error( 'inexistent_terms', __( 'Inexistent terms.', 'es-wp-query' ) );
356 | return;
357 | }
358 |
359 | $query['terms'] = wp_list_pluck( $term_list, $resulting_field );
360 | $query['field'] = $resulting_field;
361 | }
362 | }
363 |
--------------------------------------------------------------------------------
/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "alleyinteractive/es-wp-query",
3 | "type": "wordpress-plugin",
4 | "description": "Elasticsearch Wrapper for WP_Query",
5 | "homepage": "https://github.com/alleyinteractive/es-wp-query",
6 | "license": "GPL-2.0-or-later",
7 | "authors": [
8 | {
9 | "name": "Alley",
10 | "homepage": "https://alley.co/"
11 | }
12 | ],
13 | "support": {
14 | "issues": "https://github.com/alleyinteractive/es-wp-query/issues",
15 | "source": "https://github.com/alleyinteractive/es-wp-query"
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/es-wp-query.php:
--------------------------------------------------------------------------------
1 | 5,
39 | 'offset' => 0,
40 | 'category' => 0,
41 | 'orderby' => 'date',
42 | 'order' => 'DESC',
43 | 'include' => array(),
44 | 'exclude' => array(),
45 | 'meta_key' => '', // phpcs:ignore WordPress.VIP.SlowDBQuery.slow_db_query_meta_key
46 | 'meta_value' => '', // phpcs:ignore WordPress.VIP.SlowDBQuery.slow_db_query_meta_value
47 | 'post_type' => 'post',
48 | 'suppress_filters' => true, // phpcs:ignore WordPressVIPMinimum.VIP.WPQueryParams.suppressFiltersTrue
49 | );
50 |
51 | $r = wp_parse_args( $args, $defaults );
52 | if ( empty( $r['post_status'] ) ) {
53 | $r['post_status'] = ( 'attachment' === $r['post_type'] ) ? 'inherit' : 'publish';
54 | }
55 | if ( ! empty( $r['numberposts'] ) && empty( $r['posts_per_page'] ) ) {
56 | $r['posts_per_page'] = $r['numberposts'];
57 | }
58 | if ( ! empty( $r['category'] ) ) {
59 | $r['cat'] = $r['category'];
60 | }
61 | if ( ! empty( $r['include'] ) ) {
62 | $incposts = wp_parse_id_list( $r['include'] );
63 | $r['posts_per_page'] = count( $incposts ); // Only the number of posts included.
64 | $r['post__in'] = $incposts;
65 | } elseif ( ! empty( $r['exclude'] ) ) {
66 | $r['post__not_in'] = wp_parse_id_list( $r['exclude'] ); // phpcs:ignore WordPressVIPMinimum.VIP.WPQueryParams.post__not_in
67 | }
68 |
69 | $r['ignore_sticky_posts'] = true;
70 | $r['no_found_rows'] = true;
71 |
72 | $get_posts = new ES_WP_Query();
73 | return $get_posts->query( $r );
74 |
75 | }
76 | }
77 |
78 |
79 | /**
80 | * Loads one of the included adapters.
81 | *
82 | * @param string $adapter Which adapter to include. Currently allows searchpress, wpcom-vip, travis, jetpack-search, and vip-search.
83 | * @return void
84 | */
85 | function es_wp_query_load_adapter( $adapter ) {
86 | if ( in_array( $adapter, array( 'searchpress', 'travis', 'jetpack-search', 'vip-search' ), true ) ) {
87 | require_once ES_WP_QUERY_PATH . "/adapters/{$adapter}.php";
88 | }
89 | }
90 |
--------------------------------------------------------------------------------
/multisite.xml:
--------------------------------------------------------------------------------
1 |
9 |
10 |
11 |
12 |
13 |
14 | ./tests/query/
15 |
16 |
17 |
18 |
19 | ./
20 |
21 | ./tests/
22 | ./bin/
23 | ./adapters/
24 |
25 |
26 |
27 |
--------------------------------------------------------------------------------
/phpcs.xml.dist:
--------------------------------------------------------------------------------
1 |
2 |
3 | Sniffs for the coding standards of the ES WP Query plugin
4 |
5 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 | .
26 |
27 |
28 | */node_modules/*
29 | bin/
30 | tests/
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
--------------------------------------------------------------------------------
/phpunit.xml.dist:
--------------------------------------------------------------------------------
1 |
9 |
10 |
11 | ./tests/query/
12 |
13 |
14 |
15 |
16 | ./
17 |
18 | ./tests/
19 | ./bin/
20 | ./adapters/
21 |
22 |
23 |
24 |
25 |
--------------------------------------------------------------------------------
/tests/bootstrap.php:
--------------------------------------------------------------------------------
1 | true` on the given WP_Query object.
43 | *
44 | * This is a helper intended to be used with `pre_get_posts`.
45 | *
46 | * @param \WP_Query $query WP_Query object.
47 | */
48 | function _es_wp_query_set_es_to_true( \WP_Query $query ) {
49 | $query->set( 'es', true );
50 | }
51 |
52 | require $_tests_dir . '/includes/bootstrap.php';
53 |
--------------------------------------------------------------------------------
/tests/query/author.php:
--------------------------------------------------------------------------------
1 | set_permalink_structure( '/%year%/%monthnum%/%day%/%postname%/' );
13 | }
14 |
15 | function test_author_with_no_posts() {
16 | add_action( 'pre_get_posts', '_es_wp_query_set_es_to_true' );
17 | $user_id = self::factory()->user->create( array( 'user_login' => 'user-a' ) );
18 | $this->go_to( '/author/user-a/' );
19 | $this->assertQueryTrue( 'is_archive', 'is_author' );
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/tests/query/date.php:
--------------------------------------------------------------------------------
1 | factory->post->create( array( 'post_date' => $post_date ) );
45 | }
46 |
47 | es_wp_query_index_test_data();
48 |
49 | unset( $this->q );
50 | $this->q = new ES_WP_Query();
51 | }
52 |
53 | public function _get_query_result( $args = array() ) {
54 | $args = wp_parse_args( $args, array(
55 | 'post_status' => 'any', // For the future post
56 | 'posts_per_page' => '-1', // To make sure results are accurate
57 | 'orderby' => 'ID', // Same order they were created
58 | 'order' => 'ASC',
59 | ) );
60 |
61 | return $this->q->query( $args );
62 | }
63 |
64 | public function test_simple_year_expecting_results() {
65 | $posts = $this->_get_query_result( array(
66 | 'year' => 2008,
67 | ) );
68 |
69 | $expected_dates = array(
70 | '2008-03-29 09:04:25',
71 | '2008-07-15 11:32:26',
72 | '2008-12-10 13:06:27',
73 | );
74 |
75 | $this->assertEquals( $expected_dates, wp_list_pluck( $posts, 'post_date' ) );
76 | }
77 |
78 | public function test_simple_year_expecting_noresults() {
79 | $posts = $this->_get_query_result( array(
80 | 'year' => 2000,
81 | ) );
82 |
83 | $this->assertCount( 0, $posts );
84 | }
85 |
86 | public function test_simple_m_with_year_expecting_results() {
87 | $posts = $this->_get_query_result( array(
88 | 'm' => '2007',
89 | ) );
90 |
91 | $expected_dates = array(
92 | '2007-01-22 03:49:21',
93 | '2007-05-16 17:32:22',
94 | '2007-09-24 07:17:23',
95 | );
96 |
97 | $this->assertEquals( $expected_dates, wp_list_pluck( $posts, 'post_date' ) );
98 | }
99 |
100 | public function test_simple_m_with_year_expecting_noresults() {
101 | $posts = $this->_get_query_result( array(
102 | 'm' => '1999',
103 | ) );
104 |
105 | $this->assertCount( 0, $posts );
106 | }
107 |
108 | public function test_simple_m_with_yearmonth_expecting_results() {
109 | $posts = $this->_get_query_result( array(
110 | 'm' => '202504',
111 | ) );
112 |
113 | $expected_dates = array(
114 | '2025-04-20 10:13:00',
115 | '2025-04-20 10:13:01',
116 | );
117 |
118 | $this->assertEquals( $expected_dates, wp_list_pluck( $posts, 'post_date' ) );
119 | }
120 |
121 | public function test_simple_m_with_yearmonth_expecting_noresults() {
122 | $posts = $this->_get_query_result( array(
123 | 'm' => '202502',
124 | ) );
125 |
126 | $this->assertCount( 0, $posts );
127 | }
128 |
129 | public function test_simple_m_with_yearmonthday_expecting_results() {
130 | $posts = $this->_get_query_result( array(
131 | 'm' => '20250420',
132 | ) );
133 |
134 | $expected_dates = array(
135 | '2025-04-20 10:13:00',
136 | '2025-04-20 10:13:01',
137 | );
138 |
139 | $this->assertEquals( $expected_dates, wp_list_pluck( $posts, 'post_date' ) );
140 | }
141 |
142 | public function test_simple_m_with_yearmonthday_expecting_noresults() {
143 | $posts = $this->_get_query_result( array(
144 | 'm' => '20250419',
145 | ) );
146 |
147 | $this->assertCount( 0, $posts );
148 | }
149 |
150 | public function test_simple_m_with_yearmonthdayhour_expecting_results() {
151 | $posts = $this->_get_query_result( array(
152 | 'm' => '2025042010',
153 | ) );
154 |
155 | $expected_dates = array(
156 | '2025-04-20 10:13:00',
157 | '2025-04-20 10:13:01',
158 | );
159 |
160 | $this->assertEquals( $expected_dates, wp_list_pluck( $posts, 'post_date' ) );
161 | }
162 |
163 | public function test_simple_m_with_yearmonthdayhour_expecting_noresults() {
164 | $posts = $this->_get_query_result( array(
165 | 'm' => '2025042009',
166 | ) );
167 |
168 | $this->assertCount( 0, $posts );
169 | }
170 |
171 | /**
172 | * @ticket 24884
173 | */
174 | public function test_simple_m_with_yearmonthdayhourminute_expecting_results() {
175 | $posts = $this->_get_query_result( array(
176 | 'm' => '202504201013',
177 | ) );
178 |
179 | $expected_dates = array(
180 | '2025-04-20 10:13:00',
181 | '2025-04-20 10:13:01',
182 | );
183 |
184 | $this->assertEquals( $expected_dates, wp_list_pluck( $posts, 'post_date' ) );
185 | }
186 |
187 | /**
188 | * @ticket 24884
189 | */
190 | public function test_simple_m_with_yearmonthdayhourminute_expecting_noresults() {
191 | $posts = $this->_get_query_result( array(
192 | 'm' => '202504201012',
193 | ) );
194 |
195 | $this->assertCount( 0, $posts );
196 | }
197 |
198 | /**
199 | * @ticket 24884
200 | */
201 | public function test_simple_m_with_yearmonthdayhourminutesecond_expecting_results() {
202 | $posts = $this->_get_query_result( array(
203 | 'm' => '20250420101301',
204 | ) );
205 |
206 | $expected_dates = array(
207 | '2025-04-20 10:13:01',
208 | );
209 |
210 | $this->assertEquals( $expected_dates, wp_list_pluck( $posts, 'post_date' ) );
211 | }
212 |
213 | /**
214 | * @ticket 24884
215 | */
216 | public function test_simple_m_with_yearmonthdayhourminutesecond_expecting_noresults() {
217 | $posts = $this->_get_query_result( array(
218 | 'm' => '20250420101302',
219 | ) );
220 |
221 | $this->assertCount( 0, $posts );
222 | }
223 |
224 | /**
225 | * @ticket 24884
226 | */
227 | public function test_simple_m_with_yearmonthdayhourminutesecond_and_dashes_expecting_results() {
228 | $posts = $this->_get_query_result( array(
229 | 'm' => '2025-04-20 10:13:00',
230 | ) );
231 |
232 | $expected_dates = array(
233 | '2025-04-20 10:13:00',
234 | );
235 |
236 | $this->assertEquals( $expected_dates, wp_list_pluck( $posts, 'post_date' ) );
237 | }
238 |
239 | /**
240 | * @ticket 24884
241 | */
242 | public function test_simple_m_with_yearmonthdayhourminutesecond_and_dashesletters_expecting_results() {
243 | $posts = $this->_get_query_result( array(
244 | 'm' => 'alpha2025-04-20 10:13:00',
245 | ) );
246 |
247 | $expected_dates = array(
248 | '2025-04-20 10:13:00',
249 | );
250 |
251 | $this->assertEquals( $expected_dates, wp_list_pluck( $posts, 'post_date' ) );
252 | }
253 |
254 | public function test_simple_monthnum_expecting_results() {
255 | $posts = $this->_get_query_result( array(
256 | 'monthnum' => 5,
257 | ) );
258 |
259 | $expected_dates = array(
260 | '1972-05-24 14:53:45',
261 | '2003-05-27 22:45:07',
262 | '2004-05-22 12:34:12',
263 | '2007-05-16 17:32:22',
264 | '2025-05-20 10:13:01',
265 | );
266 |
267 | $this->assertEquals( $expected_dates, wp_list_pluck( $posts, 'post_date' ) );
268 | }
269 |
270 | public function test_simple_monthnum_expecting_noresults() {
271 | $posts = $this->_get_query_result( array(
272 | 'monthnum' => 8,
273 | ) );
274 |
275 | $this->assertCount( 0, $posts );
276 | }
277 |
278 | public function test_simple_w_as_in_week_expecting_results() {
279 | $posts = $this->_get_query_result( array(
280 | 'w' => 24,
281 | ) );
282 |
283 | $expected_dates = array(
284 | '2009-06-11 21:30:28',
285 | '2010-06-17 17:09:30',
286 | '2012-06-13 14:03:34',
287 | );
288 |
289 | $this->assertEquals( $expected_dates, wp_list_pluck( $posts, 'post_date' ) );
290 | }
291 |
292 | public function test_simple_w_as_in_week_expecting_noresults() {
293 | $posts = $this->_get_query_result( array(
294 | 'w' => 2,
295 | ) );
296 |
297 | $this->assertCount( 0, $posts );
298 | }
299 |
300 | public function test_simple_day_expecting_results() {
301 | $posts = $this->_get_query_result( array(
302 | 'day' => 22,
303 | ) );
304 |
305 | $expected_dates = array(
306 | '2004-05-22 12:34:12',
307 | '2007-01-22 03:49:21',
308 | );
309 |
310 | $this->assertEquals( $expected_dates, wp_list_pluck( $posts, 'post_date' ) );
311 | }
312 |
313 | public function test_simple_day_expecting_noresults() {
314 | $posts = $this->_get_query_result( array(
315 | 'day' => 30,
316 | ) );
317 |
318 | $this->assertCount( 0, $posts );
319 | }
320 |
321 | public function test_simple_hour_expecting_results() {
322 | $posts = $this->_get_query_result( array(
323 | 'hour' => 21,
324 | ) );
325 |
326 | $expected_dates = array(
327 | '2009-06-11 21:30:28',
328 | );
329 |
330 | $this->assertEquals( $expected_dates, wp_list_pluck( $posts, 'post_date' ) );
331 | }
332 |
333 | public function test_simple_hour_expecting_noresults() {
334 | $posts = $this->_get_query_result( array(
335 | 'hour' => 2,
336 | ) );
337 |
338 | $this->assertCount( 0, $posts );
339 | }
340 |
341 | public function test_simple_minute_expecting_results() {
342 | $posts = $this->_get_query_result( array(
343 | 'minute' => 32,
344 | ) );
345 |
346 | $expected_dates = array(
347 | '2007-05-16 17:32:22',
348 | '2008-07-15 11:32:26',
349 | );
350 |
351 | $this->assertEquals( $expected_dates, wp_list_pluck( $posts, 'post_date' ) );
352 | }
353 |
354 | public function test_simple_minute_expecting_noresults() {
355 | $posts = $this->_get_query_result( array(
356 | 'minute' => 1,
357 | ) );
358 |
359 | $this->assertCount( 0, $posts );
360 | }
361 |
362 | public function test_simple_second_expecting_results() {
363 | $posts = $this->_get_query_result( array(
364 | 'second' => 30,
365 | ) );
366 |
367 | $expected_dates = array(
368 | '2010-06-17 17:09:30',
369 | );
370 |
371 | $this->assertEquals( $expected_dates, wp_list_pluck( $posts, 'post_date' ) );
372 | }
373 |
374 | public function test_simple_second_expecting_noresults() {
375 | $posts = $this->_get_query_result( array(
376 | 'second' => 50,
377 | ) );
378 |
379 | $this->assertCount( 0, $posts );
380 | }
381 | }
--------------------------------------------------------------------------------
/tests/query/dateQuery.php:
--------------------------------------------------------------------------------
1 | factory->post->create( array( 'post_date' => $post_date ) );
48 | }
49 |
50 | es_wp_query_index_test_data();
51 |
52 | unset( $this->q );
53 | $this->q = new ES_WP_Query();
54 | }
55 |
56 | public function _get_query_result( $args = array() ) {
57 | $args = wp_parse_args( $args, array(
58 | 'post_status' => 'any', // For the future post
59 | 'posts_per_page' => '-1', // To make sure results are accurate
60 | 'orderby' => 'ID', // Same order they were created
61 | 'order' => 'ASC',
62 | ) );
63 |
64 | return $this->q->query( $args );
65 | }
66 |
67 | public function test_date_query_before_array() {
68 | $posts = $this->_get_query_result( array(
69 | 'date_query' => array(
70 | array(
71 | 'before' => array(
72 | 'year' => 2008,
73 | 'month' => 6,
74 | ),
75 | ),
76 | ),
77 | ) );
78 |
79 | $expected_dates = array(
80 | '1972-05-24 14:53:45',
81 | '1984-07-28 19:28:56',
82 | '2003-05-27 22:45:07',
83 | '2004-01-03 08:54:10',
84 | '2004-05-22 12:34:12',
85 | '2005-02-17 00:00:15',
86 | '2005-12-31 23:59:20',
87 | '2007-01-22 03:49:21',
88 | '2007-05-16 17:32:22',
89 | '2007-09-24 07:17:23',
90 | '2008-03-29 09:04:25',
91 | );
92 |
93 | $this->assertEquals( $expected_dates, wp_list_pluck( $posts, 'post_date' ) );
94 | }
95 |
96 | /**
97 | * Specifically tests to make sure values are defaulting to
98 | * their minimum values when being used with "before".
99 | */
100 | public function test_date_query_before_array_test_defaulting() {
101 | $posts = $this->_get_query_result( array(
102 | 'date_query' => array(
103 | array(
104 | 'before' => array(
105 | 'year' => 2008,
106 | ),
107 | ),
108 | ),
109 | ) );
110 |
111 | $expected_dates = array(
112 | '1972-05-24 14:53:45',
113 | '1984-07-28 19:28:56',
114 | '2003-05-27 22:45:07',
115 | '2004-01-03 08:54:10',
116 | '2004-05-22 12:34:12',
117 | '2005-02-17 00:00:15',
118 | '2005-12-31 23:59:20',
119 | '2007-01-22 03:49:21',
120 | '2007-05-16 17:32:22',
121 | '2007-09-24 07:17:23',
122 | );
123 |
124 | $this->assertEquals( $expected_dates, wp_list_pluck( $posts, 'post_date' ) );
125 | }
126 |
127 | public function test_date_query_before_string() {
128 | $posts = $this->_get_query_result( array(
129 | 'date_query' => array(
130 | array(
131 | 'before' => 'May 4th, 2008',
132 | ),
133 | ),
134 | ) );
135 |
136 | $expected_dates = array(
137 | '1972-05-24 14:53:45',
138 | '1984-07-28 19:28:56',
139 | '2003-05-27 22:45:07',
140 | '2004-01-03 08:54:10',
141 | '2004-05-22 12:34:12',
142 | '2005-02-17 00:00:15',
143 | '2005-12-31 23:59:20',
144 | '2007-01-22 03:49:21',
145 | '2007-05-16 17:32:22',
146 | '2007-09-24 07:17:23',
147 | '2008-03-29 09:04:25',
148 | );
149 |
150 | $this->assertEquals( $expected_dates, wp_list_pluck( $posts, 'post_date' ) );
151 | }
152 |
153 | public function test_date_query_after_array() {
154 | $posts = $this->_get_query_result( array(
155 | 'date_query' => array(
156 | array(
157 | 'after' => array(
158 | 'year' => 2009,
159 | 'month' => 12,
160 | 'day' => 31,
161 | ),
162 | ),
163 | ),
164 | ) );
165 |
166 | $expected_dates = array(
167 | '2010-06-17 17:09:30',
168 | '2011-02-23 12:12:31',
169 | '2011-07-04 01:56:32',
170 | '2011-12-12 16:39:33',
171 | '2012-06-13 14:03:34',
172 | '2025-04-20 10:13:00',
173 | '2025-04-20 10:13:01',
174 | '2025-05-20 10:13:01',
175 | );
176 |
177 | $this->assertEquals( $expected_dates, wp_list_pluck( $posts, 'post_date' ) );
178 | }
179 |
180 | /**
181 | * Specifically tests to make sure values are defaulting to
182 | * their maximum values when being used with "after".
183 | */
184 | public function test_date_query_after_array_test_defaulting() {
185 | $posts = $this->_get_query_result( array(
186 | 'date_query' => array(
187 | array(
188 | 'after' => array(
189 | 'year' => 2008,
190 | ),
191 | ),
192 | ),
193 | ) );
194 |
195 | $expected_dates = array(
196 | '2009-06-11 21:30:28',
197 | '2009-12-18 10:42:29',
198 | '2010-06-17 17:09:30',
199 | '2011-02-23 12:12:31',
200 | '2011-07-04 01:56:32',
201 | '2011-12-12 16:39:33',
202 | '2012-06-13 14:03:34',
203 | '2025-04-20 10:13:00',
204 | '2025-04-20 10:13:01',
205 | '2025-05-20 10:13:01',
206 | );
207 |
208 | $this->assertEquals( $expected_dates, wp_list_pluck( $posts, 'post_date' ) );
209 | }
210 |
211 | public function test_date_query_after_string() {
212 | $posts = $this->_get_query_result( array(
213 | 'date_query' => array(
214 | array(
215 | 'after' => '2009-12-18 10:42:29',
216 | ),
217 | ),
218 | ) );
219 |
220 | $expected_dates = array(
221 | '2010-06-17 17:09:30',
222 | '2011-02-23 12:12:31',
223 | '2011-07-04 01:56:32',
224 | '2011-12-12 16:39:33',
225 | '2012-06-13 14:03:34',
226 | '2025-04-20 10:13:00',
227 | '2025-04-20 10:13:01',
228 | '2025-05-20 10:13:01',
229 | );
230 |
231 | $this->assertEquals( $expected_dates, wp_list_pluck( $posts, 'post_date' ) );
232 | }
233 |
234 | public function test_date_query_after_string_inclusive() {
235 | $posts = $this->_get_query_result( array(
236 | 'date_query' => array(
237 | array(
238 | 'after' => '2009-12-18 10:42:29',
239 | 'inclusive' => true,
240 | ),
241 | ),
242 | ) );
243 |
244 | $expected_dates = array(
245 | '2009-12-18 10:42:29',
246 | '2010-06-17 17:09:30',
247 | '2011-02-23 12:12:31',
248 | '2011-07-04 01:56:32',
249 | '2011-12-12 16:39:33',
250 | '2012-06-13 14:03:34',
251 | '2025-04-20 10:13:00',
252 | '2025-04-20 10:13:01',
253 | '2025-05-20 10:13:01',
254 | );
255 |
256 | $this->assertEquals( $expected_dates, wp_list_pluck( $posts, 'post_date' ) );
257 | }
258 |
259 | public function test_date_query_year_expecting_results() {
260 | $posts = $this->_get_query_result( array(
261 | 'date_query' => array(
262 | array(
263 | 'year' => 2009,
264 | ),
265 | ),
266 | ) );
267 |
268 | $expected_dates = array(
269 | '2009-06-11 21:30:28',
270 | '2009-12-18 10:42:29',
271 | );
272 |
273 | $this->assertEquals( $expected_dates, wp_list_pluck( $posts, 'post_date' ) );
274 | }
275 |
276 | public function test_date_query_year_expecting_noresults() {
277 | $posts = $this->_get_query_result( array(
278 | 'date_query' => array(
279 | array(
280 | 'year' => 2001,
281 | ),
282 | ),
283 | ) );
284 |
285 | $this->assertCount( 0, $posts );
286 | }
287 |
288 | public function test_date_query_month_expecting_results() {
289 | $posts = $this->_get_query_result( array(
290 | 'date_query' => array(
291 | array(
292 | 'month' => 12,
293 | ),
294 | ),
295 | ) );
296 |
297 | $expected_dates = array(
298 | '2005-12-31 23:59:20',
299 | '2008-12-10 13:06:27',
300 | '2009-12-18 10:42:29',
301 | '2011-12-12 16:39:33',
302 | );
303 |
304 | $this->assertEquals( $expected_dates, wp_list_pluck( $posts, 'post_date' ) );
305 | }
306 |
307 | public function test_date_query_month_expecting_noresults() {
308 | $posts = $this->_get_query_result( array(
309 | 'date_query' => array(
310 | array(
311 | 'month' => 8,
312 | ),
313 | ),
314 | ) );
315 |
316 | $this->assertCount( 0, $posts );
317 | }
318 |
319 | public function test_date_query_week_expecting_results() {
320 | $posts = $this->_get_query_result( array(
321 | 'date_query' => array(
322 | array(
323 | 'week' => 1,
324 | ),
325 | ),
326 | ) );
327 |
328 | $expected_dates = array(
329 | '2004-01-03 08:54:10',
330 | );
331 |
332 | $this->assertEquals( $expected_dates, wp_list_pluck( $posts, 'post_date' ) );
333 | }
334 |
335 | public function test_date_query_week_expecting_noresults() {
336 | $posts = $this->_get_query_result( array(
337 | 'date_query' => array(
338 | array(
339 | 'week' => 10,
340 | ),
341 | ),
342 | ) );
343 |
344 | $this->assertCount( 0, $posts );
345 | }
346 |
347 | public function test_date_query_day_expecting_results() {
348 | $posts = $this->_get_query_result( array(
349 | 'date_query' => array(
350 | array(
351 | 'day' => 17,
352 | ),
353 | ),
354 | ) );
355 |
356 | $expected_dates = array(
357 | '2005-02-17 00:00:15',
358 | '2010-06-17 17:09:30',
359 | );
360 |
361 | $this->assertEquals( $expected_dates, wp_list_pluck( $posts, 'post_date' ) );
362 | }
363 |
364 | public function test_date_query_day_expecting_noresults() {
365 | $posts = $this->_get_query_result( array(
366 | 'date_query' => array(
367 | array(
368 | 'day' => 19,
369 | ),
370 | ),
371 | ) );
372 |
373 | $this->assertCount( 0, $posts );
374 | }
375 |
376 | public function test_date_query_dayofweek_expecting_results() {
377 | $posts = $this->_get_query_result( array(
378 | 'date_query' => array(
379 | array(
380 | 'dayofweek' => 7,
381 | ),
382 | ),
383 | ) );
384 |
385 | $expected_dates = array(
386 | '1984-07-28 19:28:56',
387 | '2004-01-03 08:54:10',
388 | '2004-05-22 12:34:12',
389 | '2005-12-31 23:59:20',
390 | '2008-03-29 09:04:25',
391 | );
392 |
393 | $this->assertEquals( $expected_dates, wp_list_pluck( $posts, 'post_date' ) );
394 | }
395 |
396 | public function test_date_query_hour_expecting_results() {
397 | $posts = $this->_get_query_result( array(
398 | 'date_query' => array(
399 | array(
400 | 'hour' => 13,
401 | ),
402 | ),
403 | ) );
404 |
405 | $expected_dates = array(
406 | '2008-12-10 13:06:27',
407 | );
408 |
409 | $this->assertEquals( $expected_dates, wp_list_pluck( $posts, 'post_date' ) );
410 | }
411 |
412 | public function test_date_query_hour_expecting_noresults() {
413 | $posts = $this->_get_query_result( array(
414 | 'date_query' => array(
415 | array(
416 | 'hour' => 2,
417 | ),
418 | ),
419 | ) );
420 |
421 | $this->assertCount( 0, $posts );
422 | }
423 |
424 | public function test_date_query_minute_expecting_results() {
425 | $posts = $this->_get_query_result( array(
426 | 'date_query' => array(
427 | array(
428 | 'minute' => 56,
429 | ),
430 | ),
431 | ) );
432 |
433 | $expected_dates = array(
434 | '2011-07-04 01:56:32',
435 | );
436 |
437 | $this->assertEquals( $expected_dates, wp_list_pluck( $posts, 'post_date' ) );
438 | }
439 |
440 | public function test_date_query_minute_expecting_noresults() {
441 | $posts = $this->_get_query_result( array(
442 | 'date_query' => array(
443 | array(
444 | 'minute' => 2,
445 | ),
446 | ),
447 | ) );
448 |
449 | $this->assertCount( 0, $posts );
450 | }
451 |
452 | public function test_date_query_second_expecting_results() {
453 | $posts = $this->_get_query_result( array(
454 | 'date_query' => array(
455 | array(
456 | 'second' => 21,
457 | ),
458 | ),
459 | ) );
460 |
461 | $expected_dates = array(
462 | '2007-01-22 03:49:21',
463 | );
464 |
465 | $this->assertEquals( $expected_dates, wp_list_pluck( $posts, 'post_date' ) );
466 | }
467 |
468 | public function test_date_query_second_expecting_noresults() {
469 | $posts = $this->_get_query_result( array(
470 | 'date_query' => array(
471 | array(
472 | 'second' => 2,
473 | ),
474 | ),
475 | ) );
476 |
477 | $this->assertCount( 0, $posts );
478 | }
479 |
480 | public function test_date_query_between_two_times() {
481 | $posts = $this->_get_query_result( array(
482 | 'date_query' => array(
483 | array(
484 | 'hour' => 9,
485 | 'minute' => 0,
486 | 'compare' => '>=',
487 | ),
488 | array(
489 | 'hour' => '17',
490 | 'minute' => '0',
491 | 'compare' => '<=',
492 | ),
493 | ),
494 | ) );
495 |
496 | $expected_dates = array(
497 | '1972-05-24 14:53:45',
498 | '2004-05-22 12:34:12',
499 | '2008-03-29 09:04:25',
500 | '2008-07-15 11:32:26',
501 | '2008-12-10 13:06:27',
502 | '2009-12-18 10:42:29',
503 | '2011-02-23 12:12:31',
504 | '2011-12-12 16:39:33',
505 | '2012-06-13 14:03:34',
506 | '2025-04-20 10:13:00',
507 | '2025-04-20 10:13:01',
508 | '2025-05-20 10:13:01',
509 | );
510 |
511 | $this->assertEquals( $expected_dates, wp_list_pluck( $posts, 'post_date' ) );
512 | }
513 |
514 | public function test_date_query_relation_or() {
515 | $posts = $this->_get_query_result( array(
516 | 'date_query' => array(
517 | array(
518 | 'hour' => 14,
519 | ),
520 | array(
521 | 'minute' => 34,
522 | ),
523 | 'relation' => 'OR',
524 | ),
525 | ) );
526 |
527 | $expected_dates = array(
528 | '1972-05-24 14:53:45',
529 | '2004-05-22 12:34:12',
530 | '2012-06-13 14:03:34',
531 | );
532 |
533 | $this->assertEquals( $expected_dates, wp_list_pluck( $posts, 'post_date' ) );
534 | }
535 |
536 | public function test_date_query_compare_greater_than_or_equal_to() {
537 | $posts = $this->_get_query_result( array(
538 | 'date_query' => array(
539 | array(
540 | 'hour' => 14,
541 | 'minute' => 34,
542 | ),
543 | 'compare' => '>=',
544 | ),
545 | ) );
546 |
547 | $expected_dates = array(
548 | '1972-05-24 14:53:45',
549 | '1984-07-28 19:28:56',
550 | '2003-05-27 22:45:07',
551 | '2005-12-31 23:59:20',
552 | '2007-05-16 17:32:22',
553 | '2009-06-11 21:30:28',
554 | '2010-06-17 17:09:30',
555 | '2011-12-12 16:39:33',
556 | );
557 |
558 | $this->assertEquals( $expected_dates, wp_list_pluck( $posts, 'post_date' ) );
559 | }
560 | }
--------------------------------------------------------------------------------
/tests/query/loggedIn.php:
--------------------------------------------------------------------------------
1 | factory->post->create( array( 'post_status' => $status, 'post_date' => date( $year . '-m-d 00:00:00', $date ) ) );
21 | }
22 |
23 | es_wp_query_index_test_data();
24 |
25 | unset( $this->q );
26 | $this->q = new ES_WP_Query();
27 | }
28 |
29 | function test_query_not_logged_in_default() {
30 | $posts = $this->q->query( 'posts_per_page=100' );
31 |
32 | // the output should be the only published post
33 | $expected = array_values( get_post_stati( array( 'public' => true ) ) );
34 | sort( $expected );
35 |
36 | $actual = wp_list_pluck( $posts, 'post_status' );
37 | sort( $actual );
38 |
39 | $this->assertEquals( $expected, wp_list_pluck( $posts, 'post_status' ) );
40 | }
41 |
42 | function test_query_not_logged_in_any_status() {
43 | $posts = $this->q->query( 'post_status=any&posts_per_page=100' );
44 |
45 | // the output should be the only post statuses not set to exclude from search
46 | $expected = array_values( get_post_stati( array( 'exclude_from_search' => false ) ) );
47 | sort( $expected );
48 |
49 | $actual = wp_list_pluck( $posts, 'post_status' );
50 | sort( $actual );
51 |
52 | $this->assertEquals( $expected, $actual );
53 | }
54 |
55 | function test_query_not_logged_in_all_statuses() {
56 | $posts = $this->q->query( array(
57 | 'post_status' => array_values( get_post_stati() ),
58 | 'posts_per_page' => 100,
59 | ) );
60 |
61 | // the output should be the only post statuses not set to exclude from search
62 | $expected = array_values( get_post_stati() );
63 | sort( $expected );
64 |
65 | $actual = wp_list_pluck( $posts, 'post_status' );
66 | sort( $actual );
67 | $this->assertEquals( $expected, $actual );
68 | }
69 |
70 | function test_query_admin_logged_in_default() {
71 | $current_user = get_current_user_id();
72 | wp_set_current_user( $this->factory->user->create( array( 'role' => 'administrator' ) ) );
73 |
74 | $posts = $this->q->query( 'posts_per_page=100' );
75 |
76 | // the output should be the private and published posts
77 | $public = array_values( get_post_stati( array( 'public' => true ) ) );
78 | $private = array_values( get_post_stati( array( 'private' => true ) ) );
79 | $expected = array_unique( array_merge( $public, $private ) );
80 | sort( $expected );
81 |
82 | $actual = wp_list_pluck( $posts, 'post_status' );
83 | sort( $actual );
84 |
85 | $this->assertEquals( $expected, wp_list_pluck( $posts, 'post_status' ) );
86 |
87 | wp_set_current_user( $current_user );
88 | }
89 |
90 | function test_query_admin_logged_in_any_status() {
91 | $current_user = get_current_user_id();
92 | wp_set_current_user( $this->factory->user->create( array( 'role' => 'administrator' ) ) );
93 |
94 | $posts = $this->q->query( 'post_status=any&posts_per_page=100' );
95 |
96 | // the output should be the only post statuses not set to exclude from search
97 | $expected = array_values( get_post_stati( array( 'exclude_from_search' => false ) ) );
98 | sort( $expected );
99 |
100 | $actual = wp_list_pluck( $posts, 'post_status' );
101 | sort( $actual );
102 |
103 | $this->assertEquals( $expected, $actual );
104 |
105 | wp_set_current_user( $current_user );
106 | }
107 |
108 | function test_query_admin_logged_in_all_statuses() {
109 | $current_user = get_current_user_id();
110 | wp_set_current_user( $this->factory->user->create( array( 'role' => 'administrator' ) ) );
111 |
112 | $posts = $this->q->query( array(
113 | 'post_status' => array_values( get_post_stati() ),
114 | 'posts_per_page' => 100,
115 | ) );
116 |
117 | // the output should be the only post statuses not set to exclude from search
118 | $expected = array_values( get_post_stati() );
119 | sort( $expected );
120 |
121 | $actual = wp_list_pluck( $posts, 'post_status' );
122 | sort( $actual );
123 | $this->assertEquals( $expected, $actual );
124 |
125 | wp_set_current_user( $current_user );
126 | }
127 |
128 | function test_query_subscriber_logged_in_default() {
129 | $current_user = get_current_user_id();
130 | wp_set_current_user( $this->factory->user->create( array( 'role' => 'subscriber' ) ) );
131 |
132 | $posts = $this->q->query( 'posts_per_page=100' );
133 |
134 | // the output should be the only published post
135 | $expected = array_values( get_post_stati( array( 'public' => true ) ) );
136 | sort( $expected );
137 |
138 | $actual = wp_list_pluck( $posts, 'post_status' );
139 | sort( $actual );
140 |
141 | $this->assertEquals( $expected, wp_list_pluck( $posts, 'post_status' ) );
142 |
143 | wp_set_current_user( $current_user );
144 | }
145 |
146 | function test_query_subscriber_logged_in_any_status() {
147 | $current_user = get_current_user_id();
148 | wp_set_current_user( $this->factory->user->create( array( 'role' => 'subscriber' ) ) );
149 |
150 | $posts = $this->q->query( 'post_status=any&posts_per_page=100' );
151 |
152 | // the output should be the only post statuses not set to exclude from search
153 | $expected = array_values( get_post_stati( array( 'exclude_from_search' => false ) ) );
154 | sort( $expected );
155 |
156 | $actual = wp_list_pluck( $posts, 'post_status' );
157 | sort( $actual );
158 |
159 | $this->assertEquals( $expected, $actual );
160 |
161 | wp_set_current_user( $current_user );
162 | }
163 |
164 | function test_query_subscriber_logged_in_all_statuses() {
165 | $current_user = get_current_user_id();
166 | wp_set_current_user( $this->factory->user->create( array( 'role' => 'subscriber' ) ) );
167 |
168 | $posts = $this->q->query( array(
169 | 'post_status' => array_values( get_post_stati() ),
170 | 'posts_per_page' => 100,
171 | ) );
172 |
173 | // the output should be the only post statuses not set to exclude from search
174 | $expected = array_values( get_post_stati() );
175 | sort( $expected );
176 |
177 | $actual = wp_list_pluck( $posts, 'post_status' );
178 | sort( $actual );
179 | $this->assertEquals( $expected, $actual );
180 |
181 | wp_set_current_user( $current_user );
182 | }
183 |
184 | }
185 |
--------------------------------------------------------------------------------
/tests/query/post.php:
--------------------------------------------------------------------------------
1 | factory->post->create();
13 | add_post_meta( $post_id, 'foo', rand_str() );
14 | add_post_meta( $post_id, 'foo', rand_str() );
15 | $post_id2 = $this->factory->post->create();
16 | add_post_meta( $post_id2, 'bar', 'val2' );
17 | $post_id3 = $this->factory->post->create();
18 | add_post_meta( $post_id3, 'baz', rand_str() );
19 | $post_id4 = $this->factory->post->create();
20 | add_post_meta( $post_id4, 'froo', rand_str() );
21 | $post_id5 = $this->factory->post->create();
22 | add_post_meta( $post_id5, 'tango', 'val2' );
23 | $post_id6 = $this->factory->post->create();
24 | add_post_meta( $post_id6, 'bar', 'val1' );
25 |
26 | es_wp_query_index_test_data();
27 |
28 | $query = new ES_WP_Query( array(
29 | 'meta_query' => array(
30 | array(
31 | 'key' => 'foo'
32 | ),
33 | array(
34 | 'key' => 'bar',
35 | 'value' => 'val2'
36 | ),
37 | array(
38 | 'key' => 'baz'
39 | ),
40 | array(
41 | 'key' => 'froo'
42 | ),
43 | 'relation' => 'OR',
44 | ),
45 | ) );
46 |
47 | $posts = $query->get_posts();
48 | $this->assertEquals( 4, count( $posts ) );
49 | foreach ( $posts as $post ) {
50 | $this->assertInstanceOf( 'WP_Post', $post );
51 | $this->assertEquals( 'raw', $post->filter );
52 | }
53 |
54 | $post_ids = wp_list_pluck( $posts, 'ID' );
55 | $this->assertEqualSets( array( $post_id, $post_id2, $post_id3, $post_id4 ), $post_ids );
56 | }
57 |
58 | function test_meta_key_and_query() {
59 | $post_id = $this->factory->post->create();
60 | add_post_meta( $post_id, 'foo', rand_str() );
61 | add_post_meta( $post_id, 'foo', rand_str() );
62 | $post_id2 = $this->factory->post->create();
63 | add_post_meta( $post_id2, 'bar', 'val2' );
64 | add_post_meta( $post_id2, 'foo', rand_str() );
65 | $post_id3 = $this->factory->post->create();
66 | add_post_meta( $post_id3, 'baz', rand_str() );
67 | $post_id4 = $this->factory->post->create();
68 | add_post_meta( $post_id4, 'froo', rand_str() );
69 | $post_id5 = $this->factory->post->create();
70 | add_post_meta( $post_id5, 'tango', 'val2' );
71 | $post_id6 = $this->factory->post->create();
72 | add_post_meta( $post_id6, 'bar', 'val1' );
73 | add_post_meta( $post_id6, 'foo', rand_str() );
74 | $post_id7 = $this->factory->post->create();
75 | add_post_meta( $post_id7, 'foo', rand_str() );
76 | add_post_meta( $post_id7, 'froo', rand_str() );
77 | add_post_meta( $post_id7, 'baz', rand_str() );
78 | add_post_meta( $post_id7, 'bar', 'val2' );
79 |
80 | es_wp_query_index_test_data();
81 |
82 | $query = new ES_WP_Query( array(
83 | 'meta_query' => array(
84 | array(
85 | 'key' => 'foo'
86 | ),
87 | array(
88 | 'key' => 'bar',
89 | 'value' => 'val2'
90 | ),
91 | array(
92 | 'key' => 'baz'
93 | ),
94 | array(
95 | 'key' => 'froo'
96 | ),
97 | 'relation' => 'AND',
98 | ),
99 | ) );
100 |
101 | $posts = $query->get_posts();
102 | $this->assertEquals( 1, count( $posts ) );
103 | foreach ( $posts as $post ) {
104 | $this->assertInstanceOf( 'WP_Post', $post );
105 | $this->assertEquals( 'raw', $post->filter );
106 | }
107 |
108 | $post_ids = wp_list_pluck( $posts, 'ID' );
109 | $this->assertEquals( array( $post_id7 ), $post_ids );
110 |
111 | $query = new ES_WP_Query( array(
112 | 'meta_query' => array(
113 | array(
114 | 'key' => 'foo'
115 | ),
116 | array(
117 | 'key' => 'bar',
118 | ),
119 | 'relation' => 'AND',
120 | ),
121 | ) );
122 |
123 | $posts = $query->get_posts();
124 | $this->assertEquals( 3, count( $posts ) );
125 | foreach ( $posts as $post ) {
126 | $this->assertInstanceOf( 'WP_Post', $post );
127 | $this->assertEquals( 'raw', $post->filter );
128 | }
129 |
130 | $post_ids = wp_list_pluck( $posts, 'ID' );
131 | $this->assertEqualSets( array( $post_id2, $post_id6, $post_id7 ), $post_ids );
132 | }
133 |
134 | /**
135 | * @ticket 18158
136 | */
137 | function test_meta_key_not_exists() {
138 | $post_id = $this->factory->post->create();
139 | add_post_meta( $post_id, 'foo', rand_str() );
140 | $post_id2 = $this->factory->post->create();
141 | add_post_meta( $post_id2, 'bar', rand_str() );
142 | $post_id3 = $this->factory->post->create();
143 | add_post_meta( $post_id3, 'bar', rand_str() );
144 | $post_id4 = $this->factory->post->create();
145 | add_post_meta( $post_id4, 'baz', rand_str() );
146 | $post_id5 = $this->factory->post->create();
147 | add_post_meta( $post_id5, 'foo', rand_str() );
148 |
149 | es_wp_query_index_test_data();
150 |
151 | $query = new ES_WP_Query( array(
152 | 'meta_query' => array(
153 | array(
154 | 'key' => 'foo',
155 | 'compare' => 'NOT EXISTS',
156 | ),
157 | ),
158 | ) );
159 |
160 | $posts = $query->get_posts();
161 | $this->assertEquals( 3, count( $posts ) );
162 | foreach ( $posts as $post ) {
163 | $this->assertInstanceOf( 'WP_Post', $post );
164 | $this->assertEquals( 'raw', $post->filter );
165 | }
166 |
167 | $query = new ES_WP_Query( array(
168 | 'meta_query' => array(
169 | array(
170 | 'key' => 'foo',
171 | 'compare' => 'NOT EXISTS',
172 | ),
173 | array(
174 | 'key' => 'bar',
175 | 'compare' => 'NOT EXISTS',
176 | ),
177 | ),
178 | ) );
179 |
180 | $posts = $query->get_posts();
181 | $this->assertEquals( 1, count( $posts ) );
182 | foreach ( $posts as $post ) {
183 | $this->assertInstanceOf( 'WP_Post', $post );
184 | $this->assertEquals( 'raw', $post->filter );
185 | }
186 |
187 | $query = new ES_WP_Query( array(
188 | 'meta_query' => array(
189 | array(
190 | 'key' => 'foo',
191 | 'compare' => 'NOT EXISTS',
192 | ),
193 | array(
194 | 'key' => 'bar',
195 | 'compare' => 'NOT EXISTS',
196 | ),
197 | array(
198 | 'key' => 'baz',
199 | 'compare' => 'NOT EXISTS',
200 | ),
201 | )
202 | ) );
203 |
204 | $posts = $query->get_posts();
205 | $this->assertEquals( 0, count( $posts ) );
206 | }
207 |
208 |
209 | function test_meta_query_decimal_ordering() {
210 | $post_1 = $this->factory->post->create();
211 | $post_2 = $this->factory->post->create();
212 | $post_3 = $this->factory->post->create();
213 | $post_4 = $this->factory->post->create();
214 | $post_5 = $this->factory->post->create();
215 |
216 | update_post_meta( $post_1, 'numeric_value', '1' );
217 | update_post_meta( $post_2, 'numeric_value', '200' );
218 | update_post_meta( $post_3, 'numeric_value', '30' );
219 | update_post_meta( $post_4, 'numeric_value', '400.5' );
220 | update_post_meta( $post_5, 'numeric_value', '400.499' );
221 |
222 | es_wp_query_index_test_data();
223 |
224 | $query = new ES_WP_Query( array(
225 | 'orderby' => 'meta_value',
226 | 'order' => 'DESC',
227 | 'meta_key' => 'numeric_value',
228 | 'meta_type' => 'DECIMAL'
229 | ) );
230 | $this->assertEquals( array( $post_4, $post_5, $post_2, $post_3, $post_1 ), wp_list_pluck( $query->posts, 'ID' ) );
231 | }
232 |
233 | /**
234 | * @ticket 20604
235 | */
236 | function test_taxonomy_empty_or() {
237 | // An empty tax query should return an empty array, not all posts.
238 |
239 | $this->factory->post->create_many( 10 );
240 |
241 | es_wp_query_index_test_data();
242 |
243 | $query = new ES_WP_Query( array(
244 | 'fields' => 'ids',
245 | 'tax_query' => array(
246 | 'relation' => 'OR',
247 | array(
248 | 'taxonomy' => 'post_tag',
249 | 'field' => 'id',
250 | 'terms' => false,
251 | 'operator' => 'IN'
252 | ),
253 | array(
254 | 'taxonomy' => 'category',
255 | 'field' => 'id',
256 | 'terms' => false,
257 | 'operator' => 'IN'
258 | )
259 | )
260 | ) );
261 |
262 | $posts = $query->get_posts();
263 | $this->assertEquals( 0 , count( $posts ) );
264 | }
265 | }
266 |
--------------------------------------------------------------------------------
/tests/query/query.php:
--------------------------------------------------------------------------------
1 | array(
12 | 'type' => 'DESC',
13 | 'name' => 'ASC'
14 | )
15 | ) );
16 | $this->assertEquals( 'desc', $q1->es_args['sort'][0][ $q1->es_map( 'post_type' ) ] );
17 | $this->assertEquals( 'asc', $q1->es_args['sort'][1][ $q1->es_map( 'post_name' ) ] );
18 |
19 | $q2 = new ES_WP_Query( array( 'orderby' => array() ) );
20 | $this->assertFalse( isset( $q2->es_args['sort'] ) );
21 |
22 | $q3 = new ES_WP_Query( array( 'post_type' => 'post' ) );
23 | $this->assertEquals( 'desc', $q3->es_args['sort'][0][ $q1->es_map( 'post_date' ) ] );
24 | }
25 |
26 | /**
27 | *
28 | * @ticket 17065
29 | */
30 | function test_order() {
31 | $q1 = new ES_WP_Query( array(
32 | 'orderby' => array(
33 | 'post_type' => 'foo'
34 | )
35 | ) );
36 | $this->assertEquals( 'desc', $q1->es_args['sort'][0][ $q1->es_map( 'post_type' ) ] );
37 |
38 | $q2 = new ES_WP_Query( array(
39 | 'orderby' => 'title',
40 | 'order' => 'foo'
41 | ) );
42 | $this->assertEquals( 'desc', $q2->es_args['sort'][0][ $q1->es_map( 'post_title' ) ] );
43 |
44 | $q3 = new ES_WP_Query( array(
45 | 'order' => 'asc'
46 | ) );
47 | $this->assertEquals( 'asc', $q3->es_args['sort'][0][ $q1->es_map( 'post_date' ) ] );
48 | }
49 |
50 | /**
51 | * @ticket 29629
52 | */
53 | function test_orderby() {
54 | // 'none' is a valid value
55 | $q3 = new ES_WP_Query( array( 'orderby' => 'none' ) );
56 | $this->assertFalse( isset( $q3->es_args['sort'] ) );
57 |
58 | // false is a valid value
59 | $q4 = new ES_WP_Query( array( 'orderby' => false ) );
60 | $this->assertFalse( isset( $q4->es_args['sort'] ) );
61 |
62 | // empty array() is a valid value
63 | $q5 = new ES_WP_Query( array( 'orderby' => array() ) );
64 | $this->assertFalse( isset( $q5->es_args['sort'] ) );
65 | }
66 | }
67 |
--------------------------------------------------------------------------------
/tests/query/results.php:
--------------------------------------------------------------------------------
1 | factory->term->create( array( 'taxonomy' => 'category', 'name' => 'cat-a' ) );
17 | $cat_b = $this->factory->term->create( array( 'taxonomy' => 'category', 'name' => 'cat-b' ) );
18 | $cat_c = $this->factory->term->create( array( 'taxonomy' => 'category', 'name' => 'cat-c' ) );
19 |
20 | $this->factory->post->create( array( 'post_title' => 'tag-נ', 'tags_input' => array( 'tag-נ' ), 'post_date' => '2008-11-01 00:00:00' ) );
21 | $this->factory->post->create( array( 'post_title' => 'cats-a-b-c', 'post_date' => '2008-12-01 00:00:00', 'post_category' => array( $cat_a, $cat_b, $cat_c ) ) );
22 | $this->factory->post->create( array( 'post_title' => 'cats-a-and-b', 'post_date' => '2009-01-01 00:00:00', 'post_category' => array( $cat_a, $cat_b ) ) );
23 | $this->factory->post->create( array( 'post_title' => 'cats-b-and-c', 'post_date' => '2009-02-01 00:00:00', 'post_category' => array( $cat_b, $cat_c ) ) );
24 | $this->factory->post->create( array( 'post_title' => 'cats-a-and-c', 'post_date' => '2009-03-01 00:00:00', 'post_category' => array( $cat_a, $cat_c ) ) );
25 | $this->factory->post->create( array( 'post_title' => 'cat-a', 'post_date' => '2009-04-01 00:00:00', 'post_category' => array( $cat_a ) ) );
26 | $this->factory->post->create( array( 'post_title' => 'cat-b', 'post_date' => '2009-05-01 00:00:00', 'post_category' => array( $cat_b ) ) );
27 | $this->factory->post->create( array( 'post_title' => 'cat-c', 'post_date' => '2009-06-01 00:00:00', 'post_category' => array( $cat_c ) ) );
28 | $this->factory->post->create( array( 'post_title' => 'lorem-ipsum', 'post_date' => '2009-07-01 00:00:00' ) );
29 | $this->factory->post->create( array( 'post_title' => 'comment-test', 'post_date' => '2009-08-01 00:00:00' ) );
30 | $this->factory->post->create( array( 'post_title' => 'one-trackback', 'post_date' => '2009-09-01 00:00:00' ) );
31 | $this->factory->post->create( array( 'post_title' => 'many-trackbacks', 'post_date' => '2009-10-01 00:00:00' ) );
32 | $this->factory->post->create( array( 'post_title' => 'no-comments', 'post_date' => '2009-10-02 00:00:00' ) );
33 | $this->factory->post->create( array( 'post_title' => 'one-comment', 'post_date' => '2009-11-01 00:00:00' ) );
34 | $this->factory->post->create( array( 'post_title' => 'contributor-post-approved', 'post_date' => '2009-12-01 00:00:00' ) );
35 | $this->factory->post->create( array( 'post_title' => 'embedded-video', 'post_date' => '2010-01-01 00:00:00' ) );
36 | $this->factory->post->create( array( 'post_title' => 'simple-markup-test', 'post_date' => '2010-02-01 00:00:00' ) );
37 | $this->factory->post->create( array( 'post_title' => 'raw-html-code', 'post_date' => '2010-03-01 00:00:00' ) );
38 | $this->factory->post->create( array( 'post_title' => 'tags-a-b-c', 'tags_input' => array( 'tag-a', 'tag-b', 'tag-c' ), 'post_date' => '2010-04-01 00:00:00' ) );
39 | $this->factory->post->create( array( 'post_title' => 'tag-a', 'tags_input' => array( 'tag-a' ), 'post_date' => '2010-05-01 00:00:00' ) );
40 | $this->factory->post->create( array( 'post_title' => 'tag-b', 'tags_input' => array( 'tag-b' ), 'post_date' => '2010-06-01 00:00:00' ) );
41 | $this->factory->post->create( array( 'post_title' => 'tag-c', 'tags_input' => array( 'tag-c' ), 'post_date' => '2010-07-01 00:00:00' ) );
42 | $this->factory->post->create( array( 'post_title' => 'tags-a-and-b', 'tags_input' => array( 'tag-a', 'tag-b' ), 'post_date' => '2010-08-01 00:00:00' ) );
43 | $this->factory->post->create( array( 'post_title' => 'tags-b-and-c', 'tags_input' => array( 'tag-b', 'tag-c' ), 'post_date' => '2010-09-01 00:00:00' ) );
44 | $this->factory->post->create( array( 'post_title' => 'tags-a-and-c', 'tags_input' => array( 'tag-a', 'tag-c' ), 'post_date' => '2010-10-01 00:00:00' ) );
45 |
46 | $this->parent_one = $this->factory->post->create( array( 'post_title' => 'parent-one', 'post_date' => '2007-01-01 00:00:00' ) );
47 | $this->parent_two = $this->factory->post->create( array( 'post_title' => 'parent-two', 'post_date' => '2007-01-01 00:00:00' ) );
48 | $this->parent_three = $this->factory->post->create( array( 'post_title' => 'parent-three', 'post_date' => '2007-01-01 00:00:00' ) );
49 | $this->factory->post->create( array( 'post_title' => 'child-one', 'post_parent' => $this->parent_one, 'post_date' => '2007-01-01 00:00:01' ) );
50 | $this->factory->post->create( array( 'post_title' => 'child-two', 'post_parent' => $this->parent_one, 'post_date' => '2007-01-01 00:00:02' ) );
51 | $this->factory->post->create( array( 'post_title' => 'child-three', 'post_parent' => $this->parent_two, 'post_date' => '2007-01-01 00:00:03' ) );
52 | $this->factory->post->create( array( 'post_title' => 'child-four', 'post_parent' => $this->parent_two, 'post_date' => '2007-01-01 00:00:04' ) );
53 |
54 | es_wp_query_index_test_data();
55 |
56 | unset( $this->q );
57 | $this->q = new ES_WP_Query();
58 | }
59 |
60 | function test_query_default() {
61 | $posts = $this->q->query('');
62 |
63 | // the output should be the most recent 10 posts as listed here
64 | $expected = array(
65 | 0 => 'tags-a-and-c',
66 | 1 => 'tags-b-and-c',
67 | 2 => 'tags-a-and-b',
68 | 3 => 'tag-c',
69 | 4 => 'tag-b',
70 | 5 => 'tag-a',
71 | 6 => 'tags-a-b-c',
72 | 7 => 'raw-html-code',
73 | 8 => 'simple-markup-test',
74 | 9 => 'embedded-video',
75 | );
76 |
77 | $this->assertEquals( $expected, wp_list_pluck( $posts, 'post_name' ) );
78 | }
79 |
80 | function test_query_tag_a() {
81 | $posts = $this->q->query('tag=tag-a');
82 |
83 | // there are 4 posts with Tag A
84 | $this->assertCount( 4, $posts );
85 | $this->assertEquals( 'tags-a-and-c', $posts[0]->post_name );
86 | $this->assertEquals( 'tags-a-and-b', $posts[1]->post_name );
87 | $this->assertEquals( 'tag-a', $posts[2]->post_name );
88 | $this->assertEquals( 'tags-a-b-c', $posts[3]->post_name );
89 | }
90 |
91 | function test_query_tag_b() {
92 | $posts = $this->q->query('tag=tag-b');
93 |
94 | // there are 4 posts with Tag A
95 | $this->assertCount( 4, $posts );
96 | $this->assertEquals( 'tags-b-and-c', $posts[0]->post_name );
97 | $this->assertEquals( 'tags-a-and-b', $posts[1]->post_name );
98 | $this->assertEquals( 'tag-b', $posts[2]->post_name );
99 | $this->assertEquals( 'tags-a-b-c', $posts[3]->post_name );
100 | }
101 |
102 | /**
103 | * @ticket 21779
104 | */
105 | function test_query_tag_nun() {
106 | $posts = $this->q->query('tag=tag-נ');
107 |
108 | // there is 1 post with Tag נ
109 | $this->assertCount( 1, $posts );
110 | $this->assertEquals( 'tag-%d7%a0', $posts[0]->post_name );
111 | }
112 |
113 | function test_query_tag_id() {
114 | $tag = tag_exists('tag-a');
115 | $posts = $this->q->query( "tag_id=" . $tag['term_id'] );
116 |
117 | // there are 4 posts with Tag A
118 | $this->assertCount( 4, $posts );
119 | $this->assertEquals( 'tags-a-and-c', $posts[0]->post_name );
120 | $this->assertEquals( 'tags-a-and-b', $posts[1]->post_name );
121 | $this->assertEquals( 'tag-a', $posts[2]->post_name );
122 | $this->assertEquals( 'tags-a-b-c', $posts[3]->post_name );
123 | }
124 |
125 | function test_query_tag_slug__in() {
126 | $posts = $this->q->query("tag_slug__in[]=tag-b&tag_slug__in[]=tag-c");
127 |
128 | // there are 4 posts with either Tag B or Tag C
129 | $this->assertCount( 6, $posts );
130 | $this->assertEquals( 'tags-a-and-c', $posts[0]->post_name );
131 | $this->assertEquals( 'tags-b-and-c', $posts[1]->post_name );
132 | $this->assertEquals( 'tags-a-and-b', $posts[2]->post_name );
133 | $this->assertEquals( 'tag-c', $posts[3]->post_name );
134 | $this->assertEquals( 'tag-b', $posts[4]->post_name );
135 | $this->assertEquals( 'tags-a-b-c', $posts[5]->post_name );
136 | }
137 |
138 |
139 | function test_query_tag__in() {
140 | $tag_a = tag_exists('tag-a');
141 | $tag_b = tag_exists('tag-b');
142 | $posts = $this->q->query( "tag__in[]=". $tag_a['term_id'] . "&tag__in[]=" . $tag_b['term_id'] );
143 |
144 | // there are 6 posts with either Tag A or Tag B
145 | $this->assertCount( 6, $posts );
146 | $this->assertEquals( 'tags-a-and-c', $posts[0]->post_name );
147 | $this->assertEquals( 'tags-b-and-c', $posts[1]->post_name );
148 | $this->assertEquals( 'tags-a-and-b', $posts[2]->post_name );
149 | $this->assertEquals( 'tag-b', $posts[3]->post_name );
150 | $this->assertEquals( 'tag-a', $posts[4]->post_name );
151 | $this->assertEquals( 'tags-a-b-c', $posts[5]->post_name );
152 | }
153 |
154 | function test_query_tag__not_in() {
155 | $tag_a = tag_exists('tag-a');
156 | $posts = $this->q->query( "tag__not_in[]=" . $tag_a['term_id'] );
157 |
158 | // the most recent 10 posts with Tag A excluded
159 | // (note the different between this and test_query_default)
160 | $expected = array (
161 | 0 => 'tags-b-and-c',
162 | 1 => 'tag-c',
163 | 2 => 'tag-b',
164 | 3 => 'raw-html-code',
165 | 4 => 'simple-markup-test',
166 | 5 => 'embedded-video',
167 | 6 => 'contributor-post-approved',
168 | 7 => 'one-comment',
169 | 8 => 'no-comments',
170 | 9 => 'many-trackbacks',
171 | );
172 |
173 | $this->assertEquals( $expected, wp_list_pluck( $posts, 'post_name' ) );
174 | }
175 |
176 | function test_query_tag__in_but__not_in() {
177 | $tag_a = tag_exists('tag-a');
178 | $tag_b = tag_exists('tag-b');
179 | $posts = $this->q->query( "tag__in[]=" . $tag_a['term_id'] . "&tag__not_in[]=" . $tag_b['term_id'] );
180 |
181 | // there are 4 posts with Tag A, only 2 when we exclude Tag B
182 | $this->assertCount( 2, $posts );
183 | $this->assertEquals( 'tags-a-and-c', $posts[0]->post_name );
184 | $this->assertEquals( 'tag-a', $posts[1]->post_name );
185 | }
186 |
187 |
188 |
189 | function test_query_category_name() {
190 | $posts = $this->q->query('category_name=cat-a');
191 |
192 | // there are 4 posts with Cat A, we'll check for them by name
193 | $this->assertCount( 4, $posts );
194 | $this->assertEquals( 'cat-a', $posts[0]->post_name );
195 | $this->assertEquals( 'cats-a-and-c', $posts[1]->post_name );
196 | $this->assertEquals( 'cats-a-and-b', $posts[2]->post_name );
197 | $this->assertEquals( 'cats-a-b-c', $posts[3]->post_name );
198 | }
199 |
200 | function test_query_cat() {
201 | $cat = category_exists('cat-b');
202 | $posts = $this->q->query("cat=$cat");
203 |
204 | // there are 4 posts with Cat B
205 | $this->assertCount( 4, $posts );
206 | $this->assertEquals( 'cat-b', $posts[0]->post_name );
207 | $this->assertEquals( 'cats-b-and-c', $posts[1]->post_name );
208 | $this->assertEquals( 'cats-a-and-b', $posts[2]->post_name );
209 | $this->assertEquals( 'cats-a-b-c', $posts[3]->post_name );
210 | }
211 |
212 | function test_query_posts_per_page() {
213 | $posts = $this->q->query('posts_per_page=5');
214 |
215 | $expected = array (
216 | 0 => 'tags-a-and-c',
217 | 1 => 'tags-b-and-c',
218 | 2 => 'tags-a-and-b',
219 | 3 => 'tag-c',
220 | 4 => 'tag-b',
221 | );
222 |
223 | $this->assertCount( 5, $posts );
224 | $this->assertEquals( $expected, wp_list_pluck( $posts, 'post_name' ) );
225 | }
226 |
227 | function test_query_offset() {
228 | $posts = $this->q->query('offset=2');
229 |
230 | $expected = array (
231 | 0 => 'tags-a-and-b',
232 | 1 => 'tag-c',
233 | 2 => 'tag-b',
234 | 3 => 'tag-a',
235 | 4 => 'tags-a-b-c',
236 | 5 => 'raw-html-code',
237 | 6 => 'simple-markup-test',
238 | 7 => 'embedded-video',
239 | 8 => 'contributor-post-approved',
240 | 9 => 'one-comment',
241 | );
242 |
243 | $this->assertCount( 10, $posts );
244 | $this->assertEquals( $expected, wp_list_pluck( $posts, 'post_name' ) );
245 | }
246 |
247 | function test_query_paged() {
248 | $posts = $this->q->query('paged=2');
249 |
250 | $expected = array (
251 | 0 => 'contributor-post-approved',
252 | 1 => 'one-comment',
253 | 2 => 'no-comments',
254 | 3 => 'many-trackbacks',
255 | 4 => 'one-trackback',
256 | 5 => 'comment-test',
257 | 6 => 'lorem-ipsum',
258 | 7 => 'cat-c',
259 | 8 => 'cat-b',
260 | 9 => 'cat-a',
261 | );
262 |
263 | $this->assertCount( 10, $posts );
264 | $this->assertTrue( $this->q->is_paged() );
265 | $this->assertEquals( $expected, wp_list_pluck( $posts, 'post_name' ) );
266 | }
267 |
268 | function test_query_paged_and_posts_per_page() {
269 | $posts = $this->q->query('paged=4&posts_per_page=4');
270 |
271 | $expected = array (
272 | 0 => 'no-comments',
273 | 1 => 'many-trackbacks',
274 | 2 => 'one-trackback',
275 | 3 => 'comment-test',
276 | );
277 |
278 | $this->assertCount( 4, $posts );
279 | $this->assertTrue( $this->q->is_paged() );
280 | $this->assertEquals( $expected, wp_list_pluck( $posts, 'post_name' ) );
281 | }
282 |
283 | /**
284 | * @ticket 18897
285 | */
286 | function test_query_offset_and_paged() {
287 | $posts = $this->q->query('paged=2&offset=3');
288 |
289 | $expected = array (
290 | 0 => 'many-trackbacks',
291 | 1 => 'one-trackback',
292 | 2 => 'comment-test',
293 | 3 => 'lorem-ipsum',
294 | 4 => 'cat-c',
295 | 5 => 'cat-b',
296 | 6 => 'cat-a',
297 | 7 => 'cats-a-and-c',
298 | 8 => 'cats-b-and-c',
299 | 9 => 'cats-a-and-b',
300 | );
301 |
302 | $this->assertCount( 10, $posts );
303 | $this->assertTrue( $this->q->is_paged() );
304 | $this->assertEquals( $expected, wp_list_pluck( $posts, 'post_name' ) );
305 | }
306 |
307 | /**
308 | * @ticket 11056
309 | */
310 | function test_query_post_parent__in() {
311 | // Query for first parent's children
312 | $posts = $this->q->query( array(
313 | 'post_parent__in' => array( $this->parent_one ),
314 | 'orderby' => 'date',
315 | 'order' => 'asc',
316 | ) );
317 |
318 | $this->assertEquals( array(
319 | 'child-one',
320 | 'child-two',
321 | ), wp_list_pluck( $posts, 'post_title' ) );
322 |
323 | // Second parent's children
324 | $posts = $this->q->query( array(
325 | 'post_parent__in' => array( $this->parent_two ),
326 | 'orderby' => 'date',
327 | 'order' => 'asc',
328 | ) );
329 |
330 | $this->assertEquals( array(
331 | 'child-three',
332 | 'child-four',
333 | ), wp_list_pluck( $posts, 'post_title' ) );
334 |
335 | // Both first and second parent's children
336 | $posts = $this->q->query( array(
337 | 'post_parent__in' => array( $this->parent_one, $this->parent_two ),
338 | 'orderby' => 'date',
339 | 'order' => 'asc',
340 | ) );
341 |
342 | $this->assertEquals( array(
343 | 'child-one',
344 | 'child-two',
345 | 'child-three',
346 | 'child-four',
347 | ), wp_list_pluck( $posts, 'post_title' ) );
348 |
349 | // Third parent's children
350 | $posts = $this->q->query( array(
351 | 'post_parent__in' => array( $this->parent_three ),
352 | ) );
353 |
354 | $this->assertEquals( array(), wp_list_pluck( $posts, 'post_title' ) );
355 | }
356 |
357 | function test_exlude_from_search_empty() {
358 | global $wp_post_types;
359 | foreach ( array_keys( $wp_post_types ) as $slug )
360 | $wp_post_types[$slug]->exclude_from_search = true;
361 |
362 | $posts = $this->q->query( array( 'post_type' => 'any' ) );
363 |
364 | $this->assertEmpty( $posts );
365 |
366 | foreach ( array_keys( $wp_post_types ) as $slug )
367 | $wp_post_types[$slug]->exclude_from_search = false;
368 |
369 | $posts2 = $this->q->query( array( 'post_type' => 'any' ) );
370 |
371 | $this->assertNotEmpty( $posts2 );
372 | }
373 |
374 | function test_query_search() {
375 | $posts = $this->q->query( array( 's' => 'foobar' ) );
376 | $this->assertEmpty( $posts );
377 |
378 | $posts2 = $this->q->query( array( 's' => 'lorem ipsum' ) );
379 | $this->assertEquals( array( 'lorem-ipsum' ), wp_list_pluck( $posts2, 'post_title' ) );
380 | }
381 |
382 | function test_query_author_vars() {
383 | $author_1 = $this->factory->user->create( array( 'user_login' => 'admin1', 'user_pass' => rand_str(), 'role' => 'author' ) );
384 | $post_1 = $this->factory->post->create( array( 'post_title' => rand_str(), 'post_author' => $author_1, 'post_date' => '2007-01-01 00:00:00' ) );
385 |
386 | $author_2 = $this->factory->user->create( array( 'user_login' => rand_str(), 'user_pass' => rand_str(), 'role' => 'author' ) );
387 | $post_2 = $this->factory->post->create( array( 'post_title' => rand_str(), 'post_author' => $author_2, 'post_date' => '2007-01-01 00:00:00' ) );
388 |
389 | $author_3 = $this->factory->user->create( array( 'user_login' => rand_str(), 'user_pass' => rand_str(), 'role' => 'author' ) );
390 | $post_3 = $this->factory->post->create( array( 'post_title' => rand_str(), 'post_author' => $author_3, 'post_date' => '2007-01-01 00:00:00' ) );
391 |
392 | $author_4 = $this->factory->user->create( array( 'user_login' => rand_str(), 'user_pass' => rand_str(), 'role' => 'author' ) );
393 | $post_4 = $this->factory->post->create( array( 'post_title' => rand_str(), 'post_author' => $author_4, 'post_date' => '2007-01-01 00:00:00' ) );
394 |
395 | es_wp_query_index_test_data();
396 |
397 | $posts = $this->q->query( array(
398 | 'author' => '',
399 | 'post__in' => array( $post_1, $post_2, $post_3, $post_4 )
400 | ) );
401 | $author_ids = array_unique( wp_list_pluck( $posts, 'post_author' ) );
402 | $this->assertEqualSets( array( $author_1, $author_2, $author_3, $author_4 ), $author_ids );
403 |
404 | $posts = $this->q->query( array(
405 | 'author' => 0,
406 | 'post__in' => array( $post_1, $post_2, $post_3, $post_4 )
407 | ) );
408 | $author_ids = array_unique( wp_list_pluck( $posts, 'post_author' ) );
409 | $this->assertEqualSets( array( $author_1, $author_2, $author_3, $author_4 ), $author_ids );
410 |
411 | $posts = $this->q->query( array(
412 | 'author' => '0',
413 | 'post__in' => array( $post_1, $post_2, $post_3, $post_4 )
414 | ) );
415 | $author_ids = array_unique( wp_list_pluck( $posts, 'post_author' ) );
416 | $this->assertEqualSets( array( $author_1, $author_2, $author_3, $author_4 ), $author_ids );
417 |
418 | $posts = $this->q->query( array(
419 | 'author' => $author_1,
420 | 'post__in' => array( $post_1, $post_2, $post_3, $post_4 )
421 | ) );
422 | $author_ids = array_unique( wp_list_pluck( $posts, 'post_author' ) );
423 | $this->assertEqualSets( array( $author_1 ), $author_ids );
424 |
425 | $posts = $this->q->query( array(
426 | 'author' => "$author_1",
427 | 'post__in' => array( $post_1, $post_2, $post_3, $post_4 )
428 | ) );
429 | $author_ids = array_unique( wp_list_pluck( $posts, 'post_author' ) );
430 | $this->assertEqualSets( array( $author_1 ), $author_ids );
431 |
432 | $posts = $this->q->query( array(
433 | 'author' => "{$author_1},{$author_2}",
434 | 'post__in' => array( $post_1, $post_2, $post_3, $post_4 )
435 | ) );
436 | $author_ids = array_unique( wp_list_pluck( $posts, 'post_author' ) );
437 | $this->assertEqualSets( array( $author_1, $author_2 ), $author_ids );
438 |
439 | $posts = $this->q->query( array(
440 | 'author' => "-{$author_1},{$author_2}",
441 | 'post__in' => array( $post_1, $post_2, $post_3, $post_4 )
442 | ) );
443 | $author_ids = array_unique( wp_list_pluck( $posts, 'post_author' ) );
444 | $this->assertEqualSets( array( $author_2, $author_3, $author_4 ), $author_ids );
445 |
446 | $posts = $this->q->query( array(
447 | 'author' => "{$author_1},-{$author_2}",
448 | 'post__in' => array( $post_1, $post_2, $post_3, $post_4 )
449 | ) );
450 | $author_ids = array_unique( wp_list_pluck( $posts, 'post_author' ) );
451 | $this->assertEqualSets( array( $author_1, $author_3, $author_4 ), $author_ids );
452 |
453 | $posts = $this->q->query( array(
454 | 'author' => "-{$author_1},-{$author_2}",
455 | 'post__in' => array( $post_1, $post_2, $post_3, $post_4 )
456 | ) );
457 | $author_ids = array_unique( wp_list_pluck( $posts, 'post_author' ) );
458 | $this->assertEqualSets( array( $author_3, $author_4 ), $author_ids );
459 |
460 | $posts = $this->q->query( array(
461 | 'author__in' => array( $author_1, $author_2 ),
462 | 'post__in' => array( $post_1, $post_2, $post_3, $post_4 )
463 | ) );
464 | $author_ids = array_unique( wp_list_pluck( $posts, 'post_author' ) );
465 | $this->assertEqualSets( array( $author_1, $author_2 ), $author_ids );
466 |
467 | $posts = $this->q->query( array(
468 | 'author__not_in' => array( $author_1, $author_2 ),
469 | 'post__in' => array( $post_1, $post_2, $post_3, $post_4 )
470 | ) );
471 | $author_ids = array_unique( wp_list_pluck( $posts, 'post_author' ) );
472 | $this->assertEqualSets( array( $author_3, $author_4 ), $author_ids );
473 |
474 | $posts = $this->q->query( array(
475 | 'author_name' => 'admin1',
476 | 'post__in' => array( $post_1, $post_2, $post_3, $post_4 )
477 | ) );
478 | $author_ids = array_unique( wp_list_pluck( $posts, 'post_author' ) );
479 | $this->assertEqualSets( array( $author_1 ), $author_ids );
480 | }
481 |
482 | }
483 |
--------------------------------------------------------------------------------
/tests/query/shoehorn.php:
--------------------------------------------------------------------------------
1 | true in the arguments.
6 | * We're testing against a known data set, so we can check that specific posts are included in the output.
7 | *
8 | * @group query
9 | */
10 | class Tests_Query_Shoehorn extends WP_UnitTestCase {
11 |
12 | public $q;
13 |
14 | public $subquery_assertions = array();
15 |
16 | public function setUp() {
17 | global $wp_query;
18 |
19 | parent::setUp();
20 |
21 | $cat_a = $this->factory->term->create( array( 'taxonomy' => 'category', 'name' => 'cat-a' ) );
22 | $cat_b = $this->factory->term->create( array( 'taxonomy' => 'category', 'name' => 'cat-b' ) );
23 | $cat_c = $this->factory->term->create( array( 'taxonomy' => 'category', 'name' => 'cat-c' ) );
24 |
25 | $this->factory->post->create( array( 'post_title' => 'tag-נ', 'tags_input' => array( 'tag-נ' ), 'post_date' => '2008-11-01 00:00:00' ) );
26 | $this->factory->post->create( array( 'post_title' => 'cats-a-b-c', 'post_date' => '2008-12-01 00:00:00', 'post_category' => array( $cat_a, $cat_b, $cat_c ) ) );
27 | $this->factory->post->create( array( 'post_title' => 'cats-a-and-b', 'post_date' => '2009-01-01 00:00:00', 'post_category' => array( $cat_a, $cat_b ) ) );
28 | $this->factory->post->create( array( 'post_title' => 'cats-b-and-c', 'post_date' => '2009-02-01 00:00:00', 'post_category' => array( $cat_b, $cat_c ) ) );
29 | $this->factory->post->create( array( 'post_title' => 'cats-a-and-c', 'post_date' => '2009-03-01 00:00:00', 'post_category' => array( $cat_a, $cat_c ) ) );
30 | $this->factory->post->create( array( 'post_title' => 'cat-a', 'post_date' => '2009-04-01 00:00:00', 'post_category' => array( $cat_a ) ) );
31 | $this->factory->post->create( array( 'post_title' => 'cat-b', 'post_date' => '2009-05-01 00:00:00', 'post_category' => array( $cat_b ) ) );
32 | $this->factory->post->create( array( 'post_title' => 'cat-c', 'post_date' => '2009-06-01 00:00:00', 'post_category' => array( $cat_c ) ) );
33 | $this->factory->post->create( array( 'post_title' => 'lorem-ipsum', 'post_date' => '2009-07-01 00:00:00' ) );
34 | $this->factory->post->create( array( 'post_title' => 'comment-test', 'post_date' => '2009-08-01 00:00:00' ) );
35 | $this->factory->post->create( array( 'post_title' => 'one-trackback', 'post_date' => '2009-09-01 00:00:00' ) );
36 | $this->factory->post->create( array( 'post_title' => 'many-trackbacks', 'post_date' => '2009-10-01 00:00:00' ) );
37 | $this->factory->post->create( array( 'post_title' => 'no-comments', 'post_date' => '2009-10-02 00:00:00' ) );
38 | $this->factory->post->create( array( 'post_title' => 'one-comment', 'post_date' => '2009-11-01 00:00:00' ) );
39 | $this->factory->post->create( array( 'post_title' => 'contributor-post-approved', 'post_date' => '2009-12-01 00:00:00' ) );
40 | $this->factory->post->create( array( 'post_title' => 'embedded-video', 'post_date' => '2010-01-01 00:00:00' ) );
41 | $this->factory->post->create( array( 'post_title' => 'simple-markup-test', 'post_date' => '2010-02-01 00:00:00' ) );
42 | $this->factory->post->create( array( 'post_title' => 'raw-html-code', 'post_date' => '2010-03-01 00:00:00' ) );
43 | $this->factory->post->create( array( 'post_title' => 'tags-a-b-c', 'tags_input' => array( 'tag-a', 'tag-b', 'tag-c' ), 'post_date' => '2010-04-01 00:00:00' ) );
44 | $this->factory->post->create( array( 'post_title' => 'tag-a', 'tags_input' => array( 'tag-a' ), 'post_date' => '2010-05-01 00:00:00' ) );
45 | $this->factory->post->create( array( 'post_title' => 'tag-b', 'tags_input' => array( 'tag-b' ), 'post_date' => '2010-06-01 00:00:00' ) );
46 | $this->factory->post->create( array( 'post_title' => 'tag-c', 'tags_input' => array( 'tag-c' ), 'post_date' => '2010-07-01 00:00:00' ) );
47 | $this->factory->post->create( array( 'post_title' => 'tags-a-and-b', 'tags_input' => array( 'tag-a', 'tag-b' ), 'post_date' => '2010-08-01 00:00:00' ) );
48 | $this->factory->post->create( array( 'post_title' => 'tags-b-and-c', 'tags_input' => array( 'tag-b', 'tag-c' ), 'post_date' => '2010-09-01 00:00:00' ) );
49 | $this->factory->post->create( array( 'post_title' => 'tags-a-and-c', 'tags_input' => array( 'tag-a', 'tag-c' ), 'post_date' => '2010-10-01 00:00:00' ) );
50 |
51 | $this->parent_one = $this->factory->post->create( array( 'post_title' => 'parent-one', 'post_date' => '2007-01-01 00:00:00' ) );
52 | $this->parent_two = $this->factory->post->create( array( 'post_title' => 'parent-two', 'post_date' => '2007-01-01 00:00:00' ) );
53 | $this->parent_three = $this->factory->post->create( array( 'post_title' => 'parent-three', 'post_date' => '2007-01-01 00:00:00' ) );
54 | $this->child_one = $this->factory->post->create( array( 'post_title' => 'child-one', 'post_parent' => $this->parent_one, 'post_date' => '2007-01-01 00:00:01' ) );
55 | $this->child_two = $this->factory->post->create( array( 'post_title' => 'child-two', 'post_parent' => $this->parent_one, 'post_date' => '2007-01-01 00:00:02' ) );
56 | $this->child_three = $this->factory->post->create( array( 'post_title' => 'child-three', 'post_parent' => $this->parent_two, 'post_date' => '2007-01-01 00:00:03' ) );
57 | $this->child_four = $this->factory->post->create( array( 'post_title' => 'child-four', 'post_parent' => $this->parent_two, 'post_date' => '2007-01-01 00:00:04' ) );
58 |
59 | es_wp_query_index_test_data();
60 |
61 | // Set the query to be the global query so we can assert query conditionals.
62 | $this->q =& $wp_query;
63 | $this->q = new WP_Query();
64 | }
65 |
66 | public function tearDown() {
67 | $this->reset_post_types();
68 | parent::tearDown();
69 | }
70 |
71 | function test_wp_query_default() {
72 | $posts = $this->q->query('');
73 |
74 | // the output should be the most recent 10 posts as listed here
75 | $expected = array(
76 | 0 => 'tags-a-and-c',
77 | 1 => 'tags-b-and-c',
78 | 2 => 'tags-a-and-b',
79 | 3 => 'tag-c',
80 | 4 => 'tag-b',
81 | 5 => 'tag-a',
82 | 6 => 'tags-a-b-c',
83 | 7 => 'raw-html-code',
84 | 8 => 'simple-markup-test',
85 | 9 => 'embedded-video',
86 | );
87 |
88 | $this->assertEquals( $expected, wp_list_pluck( $posts, 'post_name' ) );
89 | $this->assertEquals( 0, substr_count( $this->q->request, 'ES_WP_Query Shoehorn' ) );
90 | $this->assertQueryTrue( 'is_home', 'is_front_page' );
91 | }
92 |
93 | function test_wp_query() {
94 | $posts = $this->q->query( 'es=true' );
95 |
96 | // the output should be the most recent 10 posts as listed here
97 | $expected = array(
98 | 0 => 'tags-a-and-c',
99 | 1 => 'tags-b-and-c',
100 | 2 => 'tags-a-and-b',
101 | 3 => 'tag-c',
102 | 4 => 'tag-b',
103 | 5 => 'tag-a',
104 | 6 => 'tags-a-b-c',
105 | 7 => 'raw-html-code',
106 | 8 => 'simple-markup-test',
107 | 9 => 'embedded-video',
108 | );
109 |
110 | $this->assertEquals( $expected, wp_list_pluck( $posts, 'post_name' ) );
111 | $this->assertEquals( 1, substr_count( $this->q->request, 'ES_WP_Query Shoehorn' ) );
112 | $this->assertQueryTrue( 'is_home', 'is_front_page' );
113 | }
114 |
115 | function test_wp_query_posts_per_page() {
116 | $posts = $this->q->query('posts_per_page=5&es=true');
117 |
118 | $expected = array (
119 | 0 => 'tags-a-and-c',
120 | 1 => 'tags-b-and-c',
121 | 2 => 'tags-a-and-b',
122 | 3 => 'tag-c',
123 | 4 => 'tag-b',
124 | );
125 |
126 | $this->assertCount( 5, $posts );
127 | $this->assertEquals( $expected, wp_list_pluck( $posts, 'post_name' ) );
128 | $this->assertEquals( 1, substr_count( $this->q->request, 'ES_WP_Query Shoehorn' ) );
129 | $this->assertQueryTrue( 'is_home', 'is_front_page' );
130 | }
131 |
132 | function test_wp_query_offset() {
133 | $posts = $this->q->query('offset=2&es=true');
134 |
135 | $expected = array (
136 | 0 => 'tags-a-and-b',
137 | 1 => 'tag-c',
138 | 2 => 'tag-b',
139 | 3 => 'tag-a',
140 | 4 => 'tags-a-b-c',
141 | 5 => 'raw-html-code',
142 | 6 => 'simple-markup-test',
143 | 7 => 'embedded-video',
144 | 8 => 'contributor-post-approved',
145 | 9 => 'one-comment',
146 | );
147 |
148 | $this->assertCount( 10, $posts );
149 | $this->assertEquals( $expected, wp_list_pluck( $posts, 'post_name' ) );
150 | $this->assertEquals( 1, substr_count( $this->q->request, 'ES_WP_Query Shoehorn' ) );
151 | $this->assertQueryTrue( 'is_home', 'is_front_page' );
152 | }
153 |
154 | function test_wp_query_paged() {
155 | $posts = $this->q->query('paged=2&es=true');
156 |
157 | $expected = array (
158 | 0 => 'contributor-post-approved',
159 | 1 => 'one-comment',
160 | 2 => 'no-comments',
161 | 3 => 'many-trackbacks',
162 | 4 => 'one-trackback',
163 | 5 => 'comment-test',
164 | 6 => 'lorem-ipsum',
165 | 7 => 'cat-c',
166 | 8 => 'cat-b',
167 | 9 => 'cat-a',
168 | );
169 |
170 | $this->assertCount( 10, $posts );
171 | $this->assertEquals( $expected, wp_list_pluck( $posts, 'post_name' ) );
172 | $this->assertEquals( 1, substr_count( $this->q->request, 'ES_WP_Query Shoehorn' ) );
173 | $this->assertQueryTrue( 'is_home', 'is_front_page', 'is_paged' );
174 | }
175 |
176 | function test_wp_query_paged_and_posts_per_page() {
177 | $posts = $this->q->query('paged=4&posts_per_page=4&es=true');
178 |
179 | $expected = array (
180 | 0 => 'no-comments',
181 | 1 => 'many-trackbacks',
182 | 2 => 'one-trackback',
183 | 3 => 'comment-test',
184 | );
185 |
186 | $this->assertCount( 4, $posts );
187 | $this->assertEquals( $expected, wp_list_pluck( $posts, 'post_name' ) );
188 | $this->assertEquals( 1, substr_count( $this->q->request, 'ES_WP_Query Shoehorn' ) );
189 | $this->assertQueryTrue( 'is_home', 'is_front_page', 'is_paged' );
190 | }
191 |
192 | /**
193 | * @ticket 18897
194 | */
195 | function test_wp_query_offset_and_paged() {
196 | $posts = $this->q->query('paged=2&offset=3&es=true');
197 |
198 | $expected = array (
199 | 0 => 'many-trackbacks',
200 | 1 => 'one-trackback',
201 | 2 => 'comment-test',
202 | 3 => 'lorem-ipsum',
203 | 4 => 'cat-c',
204 | 5 => 'cat-b',
205 | 6 => 'cat-a',
206 | 7 => 'cats-a-and-c',
207 | 8 => 'cats-b-and-c',
208 | 9 => 'cats-a-and-b',
209 | );
210 |
211 | $this->assertCount( 10, $posts );
212 | $this->assertEquals( $expected, wp_list_pluck( $posts, 'post_name' ) );
213 | $this->assertEquals( 1, substr_count( $this->q->request, 'ES_WP_Query Shoehorn' ) );
214 | $this->assertQueryTrue( 'is_home', 'is_front_page', 'is_paged' );
215 | }
216 |
217 | function test_wp_query_no_results() {
218 | $posts = $this->q->query( 'year=2000&es=true' );
219 |
220 | $this->assertEmpty( $posts );
221 | $this->assertEquals( 1, substr_count( $this->q->request, 'ES_WP_Query Shoehorn' ) );
222 | $this->assertQueryTrue( 'is_date', 'is_archive', 'is_year' );
223 | }
224 |
225 | function test_wp_query_rule_changes() {
226 | global $wp_post_types;
227 | foreach ( array_keys( $wp_post_types ) as $slug )
228 | $wp_post_types[$slug]->exclude_from_search = true;
229 |
230 | $posts = $this->q->query( array( 'post_type' => 'any', 'es' => true ) );
231 |
232 | $this->assertEmpty( $posts );
233 | $this->assertEquals( 1, substr_count( $this->q->request, 'ES_WP_Query Shoehorn' ) );
234 | $this->assertQueryTrue( 'is_home', 'is_front_page' );
235 |
236 | foreach ( array_keys( $wp_post_types ) as $slug )
237 | $wp_post_types[$slug]->exclude_from_search = false;
238 |
239 | $posts2 = $this->q->query( array( 'post_type' => 'any', 'es' => true ) );
240 |
241 | $this->assertNotEmpty( $posts2 );
242 | $this->assertEquals( 1, substr_count( $this->q->request, 'ES_WP_Query Shoehorn' ) );
243 | $this->assertQueryTrue( 'is_home', 'is_front_page' );
244 | }
245 |
246 | /**
247 | * Same query is run multiple times via WP_Query::get_posts().
248 | */
249 | function test_wp_query_multiple_get_posts() {
250 | $expected = array(
251 | 'tags-a-and-c',
252 | 'tags-a-and-b',
253 | 'tag-a',
254 | 'tags-a-b-c'
255 | );
256 |
257 | $posts = $this->q->query( 'tag=tag-a&es=true' );
258 | $this->assertQueryTrue( 'is_tag', 'is_archive' );
259 |
260 | $this->assertCount( 4, $posts );
261 | $this->assertEquals( $expected, wp_list_pluck( $posts, 'post_name' ) );
262 | $this->assertEquals( 1, substr_count( $this->q->request, 'ES_WP_Query Shoehorn' ) );
263 |
264 | $posts = $this->q->get_posts();
265 | $this->assertCount( 4, $posts );
266 | $this->assertEquals( $expected, wp_list_pluck( $posts, 'post_name' ) );
267 | $this->assertEquals( 1, substr_count( $this->q->request, 'ES_WP_Query Shoehorn' ) );
268 |
269 | $posts = $this->q->get_posts();
270 | $this->assertCount( 4, $posts );
271 | $this->assertEquals( $expected, wp_list_pluck( $posts, 'post_name' ) );
272 | $this->assertEquals( 1, substr_count( $this->q->request, 'ES_WP_Query Shoehorn' ) );
273 | }
274 |
275 | /**
276 | * Same query object used multiple times, but with different query vars.
277 | */
278 | function test_wp_query_change_query_vars() {
279 | $posts = $this->q->query( 'tag=tag-b&es=true' );
280 | $this->assertQueryTrue( 'is_tag', 'is_archive' );
281 | $expected = array(
282 | 'tags-b-and-c',
283 | 'tags-a-and-b',
284 | 'tag-b',
285 | 'tags-a-b-c'
286 | );
287 | $this->assertCount( 4, $posts );
288 | $this->assertEquals( $expected, wp_list_pluck( $posts, 'post_name' ) );
289 | $this->assertEquals( 1, substr_count( $this->q->request, 'ES_WP_Query Shoehorn' ) );
290 |
291 | $posts = $this->q->query( 'tag=tag-c&es=true' );
292 | $this->assertQueryTrue( 'is_tag', 'is_archive' );
293 | $expected = array(
294 | 'tags-a-and-c',
295 | 'tags-b-and-c',
296 | 'tag-c',
297 | 'tags-a-b-c'
298 | );
299 | $this->assertCount( 4, $posts );
300 | $this->assertEquals( $expected, wp_list_pluck( $posts, 'post_name' ) );
301 | $this->assertEquals( 1, substr_count( $this->q->request, 'ES_WP_Query Shoehorn' ) );
302 | }
303 |
304 | function test_wp_query_return_ids() {
305 | $posts = $this->q->query( array(
306 | 'm' => '20070101000000',
307 | 'fields' => 'ids',
308 | 'orderby' => 'ID',
309 | 'order' => 'ASC',
310 | 'es' => true
311 | ) );
312 | $this->assertQueryTrue( 'is_date', 'is_time', 'is_archive' );
313 | $expected = array(
314 | $this->parent_one,
315 | $this->parent_two,
316 | $this->parent_three
317 | );
318 |
319 | $this->assertCount( 3, $posts );
320 | $this->assertEquals( $expected, $posts );
321 | $this->assertEquals( 1, substr_count( $this->q->request, 'ES_WP_Query Shoehorn' ) );
322 | }
323 |
324 | function test_wp_query_return_ids_parents() {
325 | $posts = $this->q->query( array(
326 | 'post_parent__in' => array( $this->parent_one, $this->parent_two ),
327 | 'fields' => 'id=>parent',
328 | 'orderby' => 'date',
329 | 'order' => 'ASC',
330 | 'es' => true
331 | ) );
332 | $this->assertQueryTrue( 'is_home', 'is_front_page' );
333 |
334 | $expected = array(
335 | $this->child_one => $this->parent_one,
336 | $this->child_two => $this->parent_one,
337 | $this->child_three => $this->parent_two,
338 | $this->child_four => $this->parent_two,
339 | );
340 |
341 | $this->assertCount( 4, $posts );
342 | $this->assertEquals( $expected, $posts );
343 | $this->assertEquals( 1, substr_count( $this->q->request, 'ES_WP_Query Shoehorn' ) );
344 | }
345 |
346 | function _run_another_basic_query( &$query ) {
347 | if ( 2 == $query->get( 'es' ) ) {
348 | $another_query = new WP_Query;
349 | $posts = $another_query->query( 'category_name=cat-a' );
350 | $expected = array(
351 | 'cat-a',
352 | 'cats-a-and-b',
353 | 'cats-a-and-c',
354 | 'cats-a-b-c'
355 | );
356 | $this->assertCount( 4, $posts );
357 | $this->assertEquals( $expected, wp_list_pluck( $posts, 'post_name' ) );
358 | $this->assertEquals( 0, substr_count( $another_query->request, 'ES_WP_Query Shoehorn' ) );
359 | }
360 | }
361 |
362 | function _run_another_es_query( &$query ) {
363 | if ( 2 == $query->get( 'es' ) ) {
364 | $another_query = new WP_Query;
365 | $posts = $another_query->query( 'category_name=cat-a&es=true' );
366 | $expected = array(
367 | 'cat-a',
368 | 'cats-a-and-b',
369 | 'cats-a-and-c',
370 | 'cats-a-b-c'
371 | );
372 | $this->assertCount( 4, $posts );
373 | $this->assertEquals( $expected, wp_list_pluck( $posts, 'post_name' ) );
374 | $this->assertEquals( 0, substr_count( $another_query->request, 'ES_WP_Query Shoehorn' ) );
375 | }
376 | }
377 |
378 | function test_wp_query_mixed_queries() {
379 | add_action( 'pre_get_posts', array( $this, '_run_another_basic_query' ), 1001 );
380 | add_action( 'pre_get_posts', array( $this, '_run_another_es_query' ), 1001 );
381 |
382 | $posts = $this->q->query( 'category_name=cat-b&es=2' );
383 | $this->assertQueryTrue( 'is_category', 'is_archive' );
384 | $expected = array(
385 | 'cat-b',
386 | 'cats-b-and-c',
387 | 'cats-a-and-b',
388 | 'cats-a-b-c'
389 | );
390 |
391 | $this->assertCount( 4, $posts );
392 | $this->assertEquals( $expected, wp_list_pluck( $posts, 'post_name' ) );
393 | $this->assertEquals( 1, substr_count( $this->q->request, 'ES_WP_Query Shoehorn' ) );
394 |
395 | remove_action( 'pre_get_posts', array( $this, '_run_another_basic_query' ), 1001 );
396 | remove_action( 'pre_get_posts', array( $this, '_run_another_es_query' ), 1001 );
397 | }
398 |
399 | function test_wp_query_data_changes_between_queries() {
400 |
401 | $posts = $this->q->query( 'tag=tag-a&es=true' );
402 | $this->assertQueryTrue( 'is_tag', 'is_archive' );
403 | $expected = array(
404 | 'tags-a-and-c',
405 | 'tags-a-and-b',
406 | 'tag-a',
407 | 'tags-a-b-c'
408 | );
409 | $this->assertCount( 4, $posts );
410 | $this->assertEquals( $expected, wp_list_pluck( $posts, 'post_name' ) );
411 | $this->assertEquals( 1, substr_count( $this->q->request, 'ES_WP_Query Shoehorn' ) );
412 |
413 | $this->factory->post->create( array( 'post_title' => 'between_queries', 'tags_input' => array( 'tag-a' ), 'post_date' => '2010-11-01 00:00:00' ) );
414 | es_wp_query_index_test_data();
415 |
416 | $posts = $this->q->get_posts();
417 | $expected = array(
418 | 'between_queries',
419 | 'tags-a-and-c',
420 | 'tags-a-and-b',
421 | 'tag-a',
422 | 'tags-a-b-c'
423 | );
424 | $this->assertCount( 5, $posts );
425 | $this->assertEquals( $expected, wp_list_pluck( $posts, 'post_name' ) );
426 | $this->assertEquals( 1, substr_count( $this->q->request, 'ES_WP_Query Shoehorn' ) );
427 | }
428 |
429 | /**
430 | * Hook for pre_get_posts to test subqueries.
431 | *
432 | * @param \WP_Query $query WP_Query (or ES_WP_Query) object querying for posts.
433 | */
434 | public function _check_subquery_conditionals( $query ) {
435 | global $wp_query;
436 | if ( $query instanceof \ES_WP_Query ) {
437 | $backup_query = $wp_query;
438 | $wp_query = $query;
439 | call_user_func_array( array( $this, 'assertQueryTrue' ), $this->subquery_assertions );
440 | $wp_query = $backup_query;
441 | $this->subquery_assertions = array();
442 | }
443 | }
444 |
445 | public function test_non_search_archive_flags() {
446 | // Define the assertions the subquery should make.
447 | $this->subquery_assertions = array( 'is_year', 'is_date', 'is_archive' );
448 |
449 | add_action( 'pre_get_posts', array( $this, '_check_subquery_conditionals' ) );
450 | $posts = $this->q->query( 'year=2009&es=true' );
451 | remove_action( 'pre_get_posts', array( $this, '_check_subquery_conditionals' ) );
452 |
453 | /*
454 | * This is a roundabout way of verifying that the pre_get_posts filter
455 | * ran successfully.
456 | */
457 | $this->assertEmpty( $this->subquery_assertions );
458 | }
459 | }
--------------------------------------------------------------------------------