├── .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 | } --------------------------------------------------------------------------------