diff --git a/Dockunit.json b/Dockunit.json index 3c4bd7b320..903a04897b 100644 --- a/Dockunit.json +++ b/Dockunit.json @@ -1,8 +1,8 @@ { "containers": [ { - "prettyName": "PHP 5.6 FPM WordPress Latest", - "image": "dockunit/prebuilt-images:php-mysql-phpunit-elasticsearch-5.6-fpm", + "prettyName": "PHP 7.0 RC 6 FPM WordPress Latest with Elasticsearch 2.0", + "image": "dockunit/prebuilt-images:php-7.0-rc-6-fpm-mysql-phpunit-elasticsearch-2.0", "beforeScripts": [ "service mysql start", "service elasticsearch start", @@ -11,8 +11,18 @@ "testCommand": "phpunit" }, { - "prettyName": "PHP 5.2 FPM WordPress Latest", - "image": "dockunit/prebuilt-images:php-mysql-phpunit-elasticsearch-5.2-fpm", + "prettyName": "PHP 5.6 FPM WordPress Latest with Elasticsearch 2.0", + "image": "dockunit/prebuilt-images:php-5.6-fpm-mysql-phpunit-elasticsearch-2.0", + "beforeScripts": [ + "service mysql start", + "service elasticsearch start", + "bash bin/install-wp-tests.sh wordpress_test root '' localhost latest" + ], + "testCommand": "phpunit" + }, + { + "prettyName": "PHP 5.2 FPM WordPress Latest with Elasticsearch 1.7", + "image": "dockunit/prebuilt-images:php-5.2-fpm-mysql-phpunit-elasticsearch-1.7", "beforeScripts": [ "service mysql start", "service elasticsearch start", diff --git a/README.md b/README.md index b7113b2c8e..6c32b23a71 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,11 @@ -ElasticPress [![Build Status](https://travis-ci.org/10up/ElasticPress.svg?branch=master)](https://travis-ci.org/10up/ElasticPress) [![Code Climate](https://img.shields.io/codeclimate/coverage/github/10up/ElasticPress.svg?style=flat)](https://codeclimate.com/github/10up/ElasticPress) [![Dockunit Status](http://dockunit.io/svg/10up/ElasticPress?master)](http://dockunit.io/projects/10up/ElasticPress#master) +ElasticPress [![Build Status](https://travis-ci.org/10up/ElasticPress.svg?branch=master)](https://travis-ci.org/10up/ElasticPress) [![Dockunit Status](http://dockunit.io/svg/10up/ElasticPress?master)](http://dockunit.io/projects/10up/ElasticPress#master) ============= Integrate [Elasticsearch](http://www.elasticsearch.org/) with [WordPress](http://wordpress.org/). -**Please note:** the master branch is the stable branch +**Please note:** the master branch is the stable + +**Upgrade Notice:** Versions 1.6.1, 1.6.2, and 1.7 require re-indexing. ## Background @@ -227,7 +229,7 @@ After running an index, ElasticPress integrates with `WP_Query` if and only if t * ```meta_query``` (*array*) - Filter posts by post meta conditions. Takes an array of form: + Filter posts by post meta conditions. Meta arrays and objects are serialized due to limitations of Elasticsearch. Takes an array of form: ```php new WP_Query( array( @@ -242,7 +244,8 @@ After running an index, ElasticPress integrates with `WP_Query` if and only if t ) ); ``` - ```meta_query``` accepts an array of arrays where each inner array *only* supports ```key``` (string), ```value``` (string|array|int), and ```compare``` (string) parameters. ```compare``` supports the following: + ```meta_query``` accepts an array of arrays where each inner array *only* supports ```key``` (string), + ```type``` (string), ```value``` (string|array|int), and ```compare``` (string) parameters. ```compare``` supports the following: * ```=``` - Posts will be returned that have a post meta key corresponding to ```key``` and a value that equals the value passed to ```value```. * ```!=``` - Posts will be returned that have a post meta key corresponding to ```key``` and a value that does NOT equal the value passed to ```value```. @@ -275,6 +278,25 @@ After running an index, ElasticPress integrates with `WP_Query` if and only if t Possible values for ```relation``` are ```OR``` and ```AND```. If ```relation``` is set to ```AND```, all inner queries must be true for a post to be returned. If ```relation``` is set to ```OR```, only one of the inner meta queries must be true for the post to be returned. + ```type``` supports the following values: 'NUMERIC', 'BINARY', 'CHAR', 'DATE', 'DATETIME', + 'DECIMAL', 'SIGNED', 'TIME', and 'UNSIGNED'. By default WordPress casts meta values to these types + in MySQL so some of these don't make sense in the context of Elasticsearch. ElasticPress does no "runtime" + casting but instead compares the value to a different type compiled during indexing + + * `NUMERIC` - Compares query `value` to integer version of stored meta value. + * `SIGNED` - Compares query `value` to integer version of stored meta value. + * `UNSIGNED` - Compares query `value` to integer version of stored meta value. + * `BINARY` - Compares query `value` to raw, unanalyzed version of stored meta value. For actual attachment searches, check out [this](https://github.com/elastic/elasticsearch-mapper-attachments). + * `CHAR` - Compares query `value` to raw, unanalyzed version of stored meta value. + * `DECIMAL` - Compares query `value` to float version of stored meta value. + * `DATE` - Compares query `value` to date version of stored meta value. Query `value` must be formated like `2015-11-14` + * `DATETIME` - Compares query `value` to date/time version of stored meta value. Query `value` must be formated like `2012-01-02 05:00:00` or `yyyy:mm:dd hh:mm:ss`. + * `TIME` - Compares query `value` to time version of stored meta value. Query `value` must be formated like `17:00:00` or `hh:mm:ss`. + + If no type is specified, ElasticPress will just deduce the type from the comparator used. ```type``` + is very rarely needed to be used. + + * ```post_type``` (*string*/*array*) Filter posts by post type. ```any``` wil search all public post types. `WP_Query` defaults to either `post` or `any` if no `post_type` is provided depending on the context of the query. This is confusing. ElasticPress will ALWAYS default to `any` if no `post_type` is provided. If you want to search for `post` posts, you MUST specify `post` as the `post_type`. @@ -467,9 +489,17 @@ The following are special parameters that are only supported by ElasticPress. The following commands are supported by ElasticPress: -* `wp elasticpress index [--setup] [--network-wide] [--posts-per-page] [--no-bulk] [--offset]` +* `wp elasticpress index [--setup] [--network-wide] [--posts-per-page] [--no-bulk] [--offset] [--show-bulk-errors] [--post-type]` + + Index all posts in the current blog. - Index all posts in the current blog. `--network-wide` will force indexing on all the blogs in the network. `--network-wide` takes an optional argument to limit the number of blogs to be indexed across where 0 is no limit. For example, `--network-wide=5` would limit indexing to only 5 blogs on the network. `--setup` will clear the index first and re-send the put mapping. `--posts-per-page` let's you determine the amount of posts to be indexed per bulk index (or cycle). `--no-bulk` let's you disable bulk indexing. `--offset` let's you skip the first n posts (don't forget to remove the `--setup` flag when resuming or the index will be emptied before starting again). + * `--network-wide` will force indexing on all the blogs in the network. `--network-wide` takes an optional argument to limit the number of blogs to be indexed across where 0 is no limit. For example, `--network-wide=5` would limit indexing to only 5 blogs on the network. + * `--setup` will clear the index first and re-send the put mapping. + * `--posts-per-page` let's you determine the amount of posts to be indexed per bulk index (or cycle). + * `--no-bulk` let's you disable bulk indexing. + * `--offset` let's you skip the first n posts (don't forget to remove the `--setup` flag when resuming or the index will be emptied before starting again). + * `--show-bulk-errors` displays the error message returned from Elasticsearch when a post fails to index (as opposed to just the title and ID of the post). + * `--post-type` let's you specify which post types will be indexed (by default: all indexable post types are indexed). For example, `--post-type="my_custom_post_type"` would limit indexing to only posts from the post type "my_custom_post_type". Accepts multiple post types separated by comma. * `wp elasticpress activate` @@ -553,4 +583,4 @@ If you identify any errors or have an idea for improving the plugin, please [ope ## License -ElasticPress is free software; you can redistribute it and/or modify it under the terms of the [GNU General Public License](http://www.gnu.org/licenses/gpl-2.0.html) as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. +ElasticPress is free software; you can redistribute it and/or modify it under the terms of the [GNU General Public License](http://www.gnu.org/licenses/gpl-2.0.html) as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. \ No newline at end of file diff --git a/bin/wp-cli.php b/bin/wp-cli.php index 25ee5d7759..af22961c88 100644 --- a/bin/wp-cli.php +++ b/bin/wp-cli.php @@ -23,6 +23,13 @@ class ElasticPress_CLI_Command extends WP_CLI_Command { */ private $failed_posts = array(); + /** + * Holds error messages for individual posts that failed to index (assuming they're available). + * + * @since 1.7 + */ + private $failed_posts_message = array(); + /** * Add the document mapping * @@ -174,7 +181,7 @@ private function _create_network_alias() { /** * Index all posts for a site or network wide * - * @synopsis [--setup] [--network-wide] [--posts-per-page] [--no-bulk] [--offset] + * @synopsis [--setup] [--network-wide] [--posts-per-page] [--no-bulk] [--offset] [--show-bulk-errors] [--post-type] * @param array $args * * @since 0.1.2 @@ -196,6 +203,10 @@ public function index( $args, $assoc_args ) { $assoc_args['offset'] = 0; } + if ( empty( $assoc_args['post-type'] ) ) { + $assoc_args['post-type'] = null; + } + $total_indexed = 0; /** @@ -230,14 +241,14 @@ public function index( $args, $assoc_args ) { foreach ( $sites as $site ) { switch_to_blog( $site['blog_id'] ); - $result = $this->_index_helper( isset( $assoc_args['no-bulk'] ), $assoc_args['posts-per-page'], $assoc_args['offset'] ); + $result = $this->_index_helper( $assoc_args ); $total_indexed += $result['synced']; - WP_CLI::log( sprintf( __( 'Number of posts synced on site %d: %d', 'elasticpress' ), $site['blog_id'], $result['synced'] ) ); + WP_CLI::log( sprintf( __( 'Number of posts indexed on site %d: %d', 'elasticpress' ), $site['blog_id'], $result['synced'] ) ); if ( ! empty( $result['errors'] ) ) { - WP_CLI::error( sprintf( __( 'Number of post sync errors on site %d: %d', 'elasticpress' ), $site['blog_id'], count( $result['errors'] ) ) ); + WP_CLI::error( sprintf( __( 'Number of post index errors on site %d: %d', 'elasticpress' ), $site['blog_id'], count( $result['errors'] ) ) ); } restore_current_blog(); @@ -253,12 +264,12 @@ public function index( $args, $assoc_args ) { WP_CLI::log( __( 'Indexing posts...', 'elasticpress' ) ); - $result = $this->_index_helper( isset( $assoc_args['no-bulk'] ), $assoc_args['posts-per-page'], $assoc_args['offset'] ); + $result = $this->_index_helper( $assoc_args ); - WP_CLI::log( sprintf( __( 'Number of posts synced on site %d: %d', 'elasticpress' ), get_current_blog_id(), $result['synced'] ) ); + WP_CLI::log( sprintf( __( 'Number of posts indexed on site %d: %d', 'elasticpress' ), get_current_blog_id(), $result['synced'] ) ); if ( ! empty( $result['errors'] ) ) { - WP_CLI::error( sprintf( __( 'Number of post sync errors on site %d: %d', 'elasticpress' ), get_current_blog_id(), count( $result['errors'] ) ) ); + WP_CLI::error( sprintf( __( 'Number of post index errors on site %d: %d', 'elasticpress' ), get_current_blog_id(), count( $result['errors'] ) ) ); } } @@ -273,26 +284,56 @@ public function index( $args, $assoc_args ) { /** * Helper method for indexing posts * - * @param bool $no_bulk disable bulk indexing - * @param int $posts_per_page - * @param int $offset + * @param array $args * * @since 0.9 * @return array */ - private function _index_helper( $no_bulk = false, $posts_per_page, $offset = 0) { + private function _index_helper( $args ) { global $wpdb, $wp_object_cache; $synced = 0; $errors = array(); + $no_bulk = false; + + if ( isset( $args['no-bulk'] ) ) { + $no_bulk = true; + } + + $show_bulk_errors = false; + + if ( isset( $args['show-bulk-errors'] ) ) { + $show_bulk_errors = true; + } + + $posts_per_page = 350; + + if ( ! empty( $args['posts-per-page'] ) ) { + $posts_per_page = absint( $args['posts-per-page'] ); + } + + $offset = 0; + + if ( ! empty( $args['offset'] ) ) { + $offset = absint( $args['offset'] ); + } + + $post_type = ep_get_indexable_post_types(); + + if ( ! empty( $args['post-type'] ) ) { + $post_type = explode( ',', $args['post-type'] ); + $post_type = array_map( 'trim', $post_type ); + } + while ( true ) { $args = apply_filters( 'ep_index_posts_args', array( 'posts_per_page' => $posts_per_page, - 'post_type' => ep_get_indexable_post_types(), + 'post_type' => $post_type, 'post_status' => ep_get_indexable_post_status(), 'offset' => $offset, - 'ignore_sticky_posts' => true + 'ignore_sticky_posts' => true, + 'orderby' => array( 'ID' => 'DESC' ), ) ); $query = new WP_Query( $args ); @@ -306,12 +347,12 @@ private function _index_helper( $no_bulk = false, $posts_per_page, $offset = 0) // index the posts one-by-one. not sure why someone may want to do this. $result = ep_sync_post( get_the_ID() ); } else { - $result = $this->queue_post( get_the_ID(), $query->post_count ); + $result = $this->queue_post( get_the_ID(), $query->post_count, $show_bulk_errors ); } if ( ! $result ) { $errors[] = get_the_ID(); - } else { + } elseif ( true === $result ) { $synced ++; } } @@ -319,7 +360,7 @@ private function _index_helper( $no_bulk = false, $posts_per_page, $offset = 0) break; } - WP_CLI::log( 'Indexed ' . ( $query->post_count + $offset ) . '/' . $query->found_posts . ' entries. . .' ); + WP_CLI::log( 'Processed ' . ( $query->post_count + $offset ) . '/' . $query->found_posts . ' entries. . .' ); $offset += $posts_per_page; @@ -356,47 +397,76 @@ private function _index_helper( $no_bulk = false, $posts_per_page, $offset = 0) * * @param $post_id * @param $bulk_trigger + * @param bool $show_bulk_errors true to show individual post error messages for bulk errors * - * @return bool + * @return bool|int true if successfully synced, false if not or 2 if post was killed before sync */ - private function queue_post( $post_id, $bulk_trigger ) { + private function queue_post( $post_id, $bulk_trigger, $show_bulk_errors = false ) { static $post_count = 0; + static $killed_post_count = 0; + $killed_post = false; $post_args = ep_prepare_post( $post_id ); // Mimic EP_Sync_Manager::sync_post( $post_id ), otherwise posts can slip // through the kill filter... that would be bad! if ( apply_filters( 'ep_post_sync_kill', false, $post_args, $post_id ) ) { - return true; - } - // put the post into the queue - $this->posts[$post_id][] = '{ "index": { "_id": "' . absint( $post_id ) . '" } }'; - $this->posts[$post_id][] = addcslashes( json_encode( $post_args ), "\n" ); + $killed_post_count++; + $killed_post = true; // Save status for return. + + } else { // Post wasn't killed so process it. - // augment the counter - ++$post_count; + // put the post into the queue + $this->posts[ $post_id ][] = '{ "index": { "_id": "' . absint( $post_id ) . '" } }'; - // if we have hit the trigger, initiate the bulk request - if ( $post_count === absint( $bulk_trigger ) ) { - $this->bulk_index(); + if ( function_exists( 'wp_json_encode' ) ) { + + $this->posts[ $post_id ][] = addcslashes( wp_json_encode( $post_args ), "\n" ); + + } else { + + $this->posts[ $post_id ][] = addcslashes( json_encode( $post_args ), "\n" ); + + } + + // augment the counter + ++ $post_count; + + } + + // If we have hit the trigger, initiate the bulk request. + if ( ( $post_count + $killed_post_count ) === absint( $bulk_trigger ) ) { + + // Don't waste time if we've killed all the posts. + if ( ! empty( $this->posts ) ) { + $this->bulk_index( $show_bulk_errors ); + } // reset the post count $post_count = 0; + $killed_post_count = 0; // reset the posts $this->posts = array(); } + if ( true === $killed_post ) { + return 2; + } + return true; + } /** * Perform the bulk index operation * + * @param bool $show_bulk_errors true to show individual post error messages for bulk errors + * * @since 0.9.2 */ - private function bulk_index() { + private function bulk_index( $show_bulk_errors = false ) { // monitor how many times we attempt to add this particular bulk request static $attempts = 0; @@ -438,11 +508,14 @@ private function bulk_index() { unset( $this->posts[$item['index']['_id']] ); } } - $this->bulk_index(); + $this->bulk_index( $show_bulk_errors ); } else { foreach ( $response['items'] as $item ) { if ( ! empty( $item['index']['_id'] ) ) { $this->failed_posts[] = $item['index']['_id']; + if ( $show_bulk_errors ) { + $this->failed_posts_message[$item['index']['_id']] = $item['index']['error']; + } } } $attempts = 0; @@ -465,6 +538,9 @@ private function send_bulk_errors() { $failed_post = get_post( $failed ); if ( $failed_post ) { $error_text .= "- {$failed}: " . $failed_post->post_title . "\r\n"; + if ( array_key_exists( $failed, $this->failed_posts_message ) ) { + $error_text .= "\t" . $this->failed_posts_message[ $failed ] . PHP_EOL; + } } } @@ -472,6 +548,7 @@ private function send_bulk_errors() { // clear failed posts after printing to the screen $this->failed_posts = array(); + $this->failed_posts_message = array(); } } @@ -612,4 +689,4 @@ private function _connect_check() { WP_CLI::error( __( 'Unable to reach Elasticsearch Server! Check that service is running.', 'elasticpress' ) ); } } -} +} \ No newline at end of file diff --git a/classes/class-ep-api.php b/classes/class-ep-api.php index 8baf10c96c..3eb81ca7b2 100644 --- a/classes/class-ep-api.php +++ b/classes/class-ep-api.php @@ -40,8 +40,18 @@ public function index_post( $post ) { $path = $index . 'post/' . $post['post_id']; + if ( function_exists( 'wp_json_encode' ) ) { + + $encoded_post = wp_json_encode( $post ); + + } else { + + $encoded_post = json_encode( $post ); + + } + $request_args = array( - 'body' => json_encode( $post ), + 'body' => $encoded_post, 'method' => 'PUT', 'timeout' => 15, ); @@ -475,6 +485,11 @@ public function prepare_post( $post_id ) { * This filter is named poorly but has to stay to keep backwards compat */ $post_args = apply_filters( 'ep_post_sync_args', $post_args, $post_id ); + + $post_args['meta'] = $this->prepare_meta_types( $post_args['post_meta'] ); + + $post_args = apply_filters( 'ep_post_sync_args_post_prepare_meta', $post_args, $post_id ); + return $post_args; } @@ -599,13 +614,139 @@ public function prepare_meta( $post ) { $prepared_meta = array(); + /** + * Filter index-able private meta + * + * Allows for specifying private meta keys that may be indexed in the same manor as public meta keys. + * + * @since 1.7 + * + * @param array Array of index-able private meta keys. + * @param WP_Post $post The current post to be indexed. + */ + $allowed_protected_keys = apply_filters( 'ep_prepare_meta_allowed_protected_keys', array(), $post ); + + /** + * Filter non-indexed public meta + * + * Allows for specifying public meta keys that should be excluded from the ElasticPress index. + * + * @since 1.7 + * + * @param array Array of public meta keys to exclude from index. + * @param WP_Post $post The current post to be indexed. + */ + $excluded_public_keys = apply_filters( 'ep_prepare_meta_excluded_public_keys', array(), $post ); + foreach ( $meta as $key => $value ) { - if ( ! is_protected_meta( $key ) ) { - $prepared_meta[$key] = maybe_unserialize( $value ); + + $allow_index = false; + + if ( is_protected_meta( $key ) ) { + + if ( true === $allowed_protected_keys || in_array( $key, $allowed_protected_keys ) ) { + $allow_index = true; + } + } else { + + if ( true !== $excluded_public_keys && ! in_array( $key, $excluded_public_keys ) ) { + $allow_index = true; + } + } + + if ( true === $allow_index ) { + $prepared_meta[ $key ] = maybe_unserialize( $value ); } } return $prepared_meta; + + } + + /** + * Prepare post meta type values to send to ES + * + * @param array $post_meta + * + * @return array + * + * @since x.x.x + */ + public function prepare_meta_types( $post_meta ) { + + $meta = array(); + + foreach ( $post_meta as $meta_key => $meta_values ) { + if ( ! is_array( $meta_values ) ) { + $meta_values = array( $meta_values ); + } + + $meta[ $meta_key ] = array_map( array( $this, 'prepare_meta_value_types' ), $meta_values ); + } + + return $meta; + + } + + /** + * Prepare meta types for meta value + * + * @param mixed $meta_value + * + * @return array + */ + public function prepare_meta_value_types( $meta_value ) { + + $max_java_int_value = 9223372036854775807; + + $meta_types = array(); + + if ( is_array( $meta_value ) || is_object( $meta_value ) ) { + $meta_value = serialize( $meta_value ); + } + + $meta_types['value'] = $meta_value; + $meta_types['raw'] = $meta_value; + + if ( is_numeric( $meta_value ) ) { + $long = intval( $meta_value ); + + if ( $max_java_int_value < $long ) { + $long = $max_java_int_value; + } + + $double = floatval( $meta_value ); + + if ( ! is_finite( $double ) ) { + $double = 0; + } + + $meta_types['long'] = $long; + $meta_types['double'] = $double; + } + + $meta_types['boolean'] = filter_var( $meta_value, FILTER_VALIDATE_BOOLEAN ); + + if ( is_string( $meta_value ) ) { + $timestamp = strtotime( $meta_value ); + + $date = '1971-01-01'; + $datetime = '1971-01-01 00:00:01'; + $time = '00:00:01'; + + if ( false !== $timestamp ) { + $date = date_i18n( 'Y-m-d', $timestamp ); + $datetime = date_i18n( 'Y-m-d H:i:s', $timestamp ); + $time = date_i18n( 'H:i:s', $timestamp ); + } + + $meta_types['date'] = $date; + $meta_types['datetime'] = $datetime; + $meta_types['time'] = $time; + } + + return $meta_types; + } /** @@ -674,10 +815,10 @@ public function index_exists( $index_name = null ) { * @return array */ public function format_args( $args ) { - if ( ! empty( $args['post_per_page'] ) ) { - $posts_per_page = $args['post_per_page']; + if ( ! empty( $args['posts_per_page'] ) ) { + $posts_per_page = (int) $args['posts_per_page']; } else { - $posts_per_page = get_option( 'posts_per_page' ); + $posts_per_page = (int) get_option( 'posts_per_page' ); } $formatted_args = array( @@ -786,6 +927,36 @@ public function format_args( $args ) { $use_filters = true; } + /** + * 'post__in' arg support. + * + * @since x.x + */ + if ( ! empty( $args['post__in'] ) ) { + $filter['and'][]['bool']['must'] = array( + 'terms' => array( + 'post_id' => (array) $args['post__in'], + ), + ); + + $use_filters = true; + } + + /** + * 'post__not_in' arg support. + * + * @since x.x + */ + if ( ! empty( $args['post__not_in'] ) ) { + $filter['and'][]['bool']['must_not'] = array( + 'terms' => array( + 'post_id' => (array) $args['post__not_in'], + ), + ); + + $use_filters = true; + } + /** * Author query support * @@ -841,7 +1012,6 @@ public function format_args( $args ) { * * Relation supports 'AND' and 'OR'. 'AND' is the default. For each individual query, the * following 'compare' values are supported: =, !=, EXISTS, NOT EXISTS. '=' is the default. - * 'type' is NOT support at this time. * * @since 1.3 */ @@ -853,6 +1023,18 @@ public function format_args( $args ) { $relation = 'should'; } + $meta_query_type_mapping = array( + 'numeric' => 'long', + 'binary' => 'raw', + 'char' => 'raw', + 'date' => 'date', + 'datetime' => 'datetime', + 'decimal' => 'double', + 'signed' => 'long', + 'time' => 'time', + 'unsigned' => 'long', + ); + foreach( $args['meta_query'] as $single_meta_query ) { if ( ! empty( $single_meta_query['key'] ) ) { @@ -863,6 +1045,25 @@ public function format_args( $args ) { $compare = strtolower( $single_meta_query['compare'] ); } + $type = null; + if ( ! empty( $single_meta_query['type'] ) ) { + $type = strtolower( $single_meta_query['type'] ); + } + + // Comparisons need to look at different paths + if ( in_array( $compare, array( 'exists', 'not exists' ) ) ) { + $meta_key_path = 'meta.' . $single_meta_query['key']; + } elseif ( in_array( $compare, array( '=', '!=' ) ) && ! $type ) { + $meta_key_path = 'meta.' . $single_meta_query['key'] . '.raw'; + } elseif ( $type && isset( $meta_query_type_mapping[ $type ] ) ) { + // Map specific meta field types to different ElasticSearch core types + $meta_key_path = 'meta.' . $single_meta_query['key'] . '.' . $meta_query_type_mapping[ $type ]; + } elseif ( in_array( $compare, array( '>=', '<=', '>', '<' ) ) ) { + $meta_key_path = 'meta.' . $single_meta_query['key'] . '.double'; + } else { + $meta_key_path = 'meta.' . $single_meta_query['key'] . '.value'; + } + switch ( $compare ) { case '!=': if ( isset( $single_meta_query['value'] ) ) { @@ -871,7 +1072,7 @@ public function format_args( $args ) { 'must_not' => array( array( 'terms' => array( - 'post_meta.' . $single_meta_query['key'] . '.raw' => (array) $single_meta_query['value'], + $meta_key_path => (array) $single_meta_query['value'], ), ), ), @@ -883,7 +1084,7 @@ public function format_args( $args ) { case 'exists': $terms_obj = array( 'exists' => array( - 'field' => 'post_meta.' . $single_meta_query['key'], + 'field' => $meta_key_path, ), ); @@ -894,7 +1095,7 @@ public function format_args( $args ) { 'must_not' => array( array( 'exists' => array( - 'field' => 'post_meta.' . $single_meta_query['key'], + 'field' => $meta_key_path, ), ), ), @@ -909,7 +1110,7 @@ public function format_args( $args ) { 'must' => array( array( 'range' => array( - 'post_meta.' . $single_meta_query['key'] . '.raw' => array( + $meta_key_path => array( "gte" => $single_meta_query['value'], ), ), @@ -927,8 +1128,8 @@ public function format_args( $args ) { 'must' => array( array( 'range' => array( - 'post_meta.' . $single_meta_query['key'] . '.raw' => array( - "lte" => $single_meta_query['value'], + $meta_key_path => array( + 'lte' => $single_meta_query['value'], ), ), ), @@ -945,8 +1146,8 @@ public function format_args( $args ) { 'must' => array( array( 'range' => array( - 'post_meta.' . $single_meta_query['key'] . '.raw' => array( - "gt" => $single_meta_query['value'], + $meta_key_path => array( + 'gt' => $single_meta_query['value'], ), ), ), @@ -963,8 +1164,8 @@ public function format_args( $args ) { 'must' => array( array( 'range' => array( - 'post_meta.' . $single_meta_query['key'] . '.raw' => array( - "lt" => $single_meta_query['value'], + $meta_key_path => array( + 'lt' => $single_meta_query['value'], ), ), ), @@ -978,8 +1179,8 @@ public function format_args( $args ) { if ( isset( $single_meta_query['value'] ) ) { $terms_obj = array( 'query' => array( - "match" => array( - 'post_meta.' . $single_meta_query['key'] => $single_meta_query['value'], + 'match' => array( + $meta_key_path => $single_meta_query['value'], ) ), ); @@ -990,7 +1191,7 @@ public function format_args( $args ) { if ( isset( $single_meta_query['value'] ) ) { $terms_obj = array( 'terms' => array( - 'post_meta.' . $single_meta_query['key'] . '.raw' => (array) $single_meta_query['value'], + $meta_key_path => (array) $single_meta_query['value'], ), ); } @@ -1035,7 +1236,7 @@ public function format_args( $args ) { $metas = (array) $search_field_args['meta']; foreach ( $metas as $meta ) { - $search_fields[] = 'post_meta.' . $meta; + $search_fields[] = 'meta.' . $meta . '.value'; } unset( $search_field_args['meta'] ); @@ -1126,7 +1327,10 @@ public function format_args( $args ) { $formatted_args['from'] = $args['offset']; } - if ( isset( $args['posts_per_page'] ) ) { + if ( isset( $args['post_per_page'] ) ) { + // For backwards compatibility for those using this since EP 1.4 + $formatted_args['size'] = $args['post_per_page']; + } elseif ( isset( $args['posts_per_page'] ) ) { $formatted_args['size'] = $args['posts_per_page']; } diff --git a/classes/class-ep-sync-manager.php b/classes/class-ep-sync-manager.php index f2259d5733..e8eb6f9d77 100644 --- a/classes/class-ep-sync-manager.php +++ b/classes/class-ep-sync-manager.php @@ -24,6 +24,7 @@ public function setup() { add_action( 'edit_attachment', array( $this, 'action_sync_on_update' ), 999, 3 ); add_action( 'delete_post', array( $this, 'action_delete_post' ) ); add_action( 'delete_blog', array( $this, 'action_delete_blog_from_index') ); + add_action( 'make_spam_blog', array( $this, 'action_delete_blog_from_index') ); add_action( 'archive_blog', array( $this, 'action_delete_blog_from_index') ); add_action( 'deactivate_blog', array( $this, 'action_delete_blog_from_index') ); } @@ -39,10 +40,16 @@ public function destroy() { remove_action( 'edit_attachment', array( $this, 'action_sync_on_update' ), 999, 3 ); remove_action( 'delete_post', array( $this, 'action_delete_post' ) ); remove_action( 'delete_blog', array( $this, 'action_delete_blog_from_index') ); + remove_action( 'make_spam_blog', array( $this, 'action_delete_blog_from_index') ); remove_action( 'archive_blog', array( $this, 'action_delete_blog_from_index') ); remove_action( 'deactivate_blog', array( $this, 'action_delete_blog_from_index') ); } + /** + * Remove blog from index when a site is deleted, archived, or deactivated + * + * @param $blog_id + */ public function action_delete_blog_from_index( $blog_id ) { if ( ep_index_exists( ep_get_index_name( $blog_id ) ) && ! apply_filters( 'ep_keep_index', false ) ) { ep_delete_index( ep_get_index_name( $blog_id ) ); @@ -83,7 +90,11 @@ public function action_sync_on_update( $post_ID ) { $indexable_post_statuses = ep_get_indexable_post_status(); $post_type = get_post_type( $post_ID ); - if ( ( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE ) || ! current_user_can( 'edit_post', $post_ID ) || 'revision' === $post_type ) { + if ( ( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE ) || 'revision' === $post_type ) { + // Bypass saving if doing autosave or post type is revision + return; + } elseif ( ! current_user_can( 'edit_post', $post_ID ) && ( ! defined( 'DOING_CRON' ) || ! DOING_CRON ) ) { + // Bypass saving if user does not have access to edit post and we're not in a cron process return; } diff --git a/classes/class-ep-wp-query-integration.php b/classes/class-ep-wp-query-integration.php index e96be8adb0..2a2c1d4f3a 100644 --- a/classes/class-ep-wp-query-integration.php +++ b/classes/class-ep-wp-query-integration.php @@ -127,7 +127,7 @@ public function action_the_post( $post ) { restore_current_blog(); switch_to_blog( $post->site_id ); - + remove_action( 'the_post', array( $this, 'action_the_post' ), 10, 1 ); setup_postdata( $post ); add_action( 'the_post', array( $this, 'action_the_post' ), 10, 1 ); @@ -218,7 +218,7 @@ public function filter_posts_request( $request, $query ) { $query_vars = $query->query_vars; if ( 'any' === $query_vars['post_type'] ) { - + if ( $query->is_search() ) { /* diff --git a/elasticpress.php b/elasticpress.php index 5ddbd4377d..46ad48ad77 100644 --- a/elasticpress.php +++ b/elasticpress.php @@ -3,7 +3,7 @@ /** * Plugin Name: ElasticPress * Description: Integrate WordPress search with Elasticsearch - * Version: 1.6.2 + * Version: 1.7 * Author: Aaron Holbrook, Taylor Lovett, Matt Gross, 10up * Author URI: http://10up.com * License: GPLv2 or later diff --git a/includes/mappings.php b/includes/mappings.php index ca35f8beca..5215bb883f 100644 --- a/includes/mappings.php +++ b/includes/mappings.php @@ -11,7 +11,7 @@ 'default' => array( 'tokenizer' => 'standard', 'filter' => array( 'standard', 'ewp_word_delimiter', 'lowercase', 'stop', 'ewp_snowball' ), - 'language' => apply_filters( 'ep_analyzer_language', 'English' ), + 'language' => apply_filters( 'ep_analyzer_language', 'english', 'analyzer_default' ), ), 'shingle_analyzer' => array( 'type' => 'custom', @@ -31,7 +31,7 @@ ), 'ewp_snowball' => array( 'type' => 'snowball', - 'language' => apply_filters( 'ep_analyzer_language', 'English' ), + 'language' => apply_filters( 'ep_analyzer_language', 'english', 'filter_ewp_snowball' ), ), 'edge_ngram' => array( 'side' => 'front', @@ -66,6 +66,52 @@ ), ), ), + array( + 'template_meta_types' => array( + 'path_match' => 'meta.*', + 'mapping' => array( + 'type' => 'object', + 'path' => 'full', + 'properties' => array( + 'value' => array( + 'type' => 'string', + ), + 'raw' => array( + 'type' => 'string', + 'index' => 'not_analyzed', + 'include_in_all' => false, + ), + 'long' => array( + 'type' => 'long', + 'index' => 'not_analyzed', + ), + 'double' => array( + 'type' => 'double', + 'index' => 'not_analyzed', + ), + 'boolean' => array( + 'type' => 'boolean', + 'index' => 'not_analyzed', + ), + 'date' => array( + 'type' => 'date', + 'format' => 'yyyy-MM-dd', + 'index' => 'not_analyzed', + ), + 'datetime' => array( + 'type' => 'date', + 'format' => 'yyyy-MM-dd HH:mm:ss', + 'index' => 'not_analyzed', + ), + 'time' => array( + 'type' => 'date', + 'format' => 'HH:mm:ss', + 'index' => 'not_analyzed', + ), + ), + ), + ), + ), array( 'template_terms' => array( 'path_match' => 'terms.*', @@ -216,12 +262,19 @@ 'permalink' => array( 'type' => 'string', ), + 'guid' => array( + 'type' => 'string', + 'index' => 'not_analyzed' + ), 'terms' => array( 'type' => 'object', ), 'post_meta' => array( 'type' => 'object', ), + 'meta' => array( + 'type' => 'object', + ), 'date_terms' => array( 'type' => 'object', 'properties' => array( @@ -263,4 +316,4 @@ ), ), ), -); +); \ No newline at end of file diff --git a/readme.txt b/readme.txt index f01bf7af50..7d6cbf3587 100644 --- a/readme.txt +++ b/readme.txt @@ -5,7 +5,7 @@ Plugin URI: https://github.com/10up/ElasticPress Tags: search, elasticsearch, fuzzy, facet, searching, autosuggest, suggest, elastic, advanced search Requires at least: 3.7.1 Tested up to: 4.4 -Stable tag: 1.6.2 +Stable tag: 1.7 License: GPLv2 or later License URI: http://www.gnu.org/licenses/gpl-2.0.html @@ -60,14 +60,37 @@ configuring single site and multi-site cross-site search are slightly different. == Changelog == -= 1.6.2 = += 1.7 (Mapping change, requires reindex) = + +ElasticPress 1.7 restructures meta mapping for posts for much more flexible meta queries. The `post_meta` Elasticsearch post property has been left for backwards compatibility. As of this version, post meta will be stored in the `meta` Elasticsearch property. `meta` is structured as follows: + +* `meta.value` (string) +* `meta.raw` (unanalyzed string) +* `meta.long` (unanalyzed number) +* `meta.double` (unanalyzed number) +* `meta.boolean` (unanalyzed number) +* `meta.date` (unanalyzed yyyy-MM-dd date) +* `meta.datetime` (unanalyzed yyyy-MM-dd HH:mm:ss datetime) +* `time` (unanalyzed HH:mm:ss time) + +When querying posts, you will get back `meta.value`. However, if you plan to mess with the new post mapping, it's important to understand the intricacies. + +The real implications of this is in `meta_query`. You can now effectively search by meta types. See the new section in README.md for details on this. + +1.7 also contains the following bugs/enhancements: + +* (Bug) Prevent missed post indexing when duplicate post dates. Props [lukaspawlik](https://github.com/lukaspawlik) +* (Bug) Complex meta types are automatically serialized upon storage. +* (Enhancement) Index posts according to post type. Props [sc0ttkclark](https://github.com/sc0ttkclark) + += 1.6.2 (Mapping change, requires reindex) = ElasticPress 1.6.2 fixes ALL backwards compatibility issues with Elasticsearch 2.0: * Removes `fuzzy_like_this` query and uses `multi_match` instead. * Uses string instead of array for post type term when there is only one term. -= 1.6.1 = += 1.6.1 (Mapping change, requires reindex) = ElasticPress 1.6.1 fixes mapping backwards compatibility issues with Elasticsearch 2.0: @@ -204,4 +227,4 @@ ElasticPress 1.6 contains a number of important enhancements and bug fixes. Most * Renamed `statii` to `status` = 0.1.0 = -* Initial plugin +* Initial plugin \ No newline at end of file diff --git a/tests/test-multisite.php b/tests/test-multisite.php index 4c11cf0445..f55e3fe71a 100644 --- a/tests/test-multisite.php +++ b/tests/test-multisite.php @@ -1749,7 +1749,65 @@ public function testDeleteIndex( ) { $this->assertNotEquals( $count_indexes, $post_count_indexes ); } - + + /** + * Tests deletion of index when a blog is deleted + * @group 392 + * @link https://github.com/10up/ElasticPress/issues/392 + */ + public function testDeactivateSite( ) { + $index_count = ep_count_indexes(); + + $count_indexes = $index_count['total_indexes']; + $last_blog_id = $index_count['last_blog_id_with_index']; + + do_action( 'deactivate_blog', $last_blog_id ); + update_blog_status( $last_blog_id, 'deleted', '1' ); + + $post_delete_count = ep_count_indexes(); + $post_count_indexes = $post_delete_count['total_indexes']; + + $this->assertNotEquals( $count_indexes, $post_count_indexes ); + } + + /** + * Tests deletion of index when a blog is marked as spam + * @group 392 + * @link https://github.com/10up/ElasticPress/issues/392 + */ + public function testSpamSite( ) { + $index_count = ep_count_indexes(); + + $count_indexes = $index_count['total_indexes']; + $last_blog_id = $index_count['last_blog_id_with_index']; + + update_blog_status( $last_blog_id, 'spam', '1' ); + + $post_delete_count = ep_count_indexes(); + $post_count_indexes = $post_delete_count['total_indexes']; + + $this->assertNotEquals( $count_indexes, $post_count_indexes ); + } + + /** + * Tests deletion of index when a blog is marked as archived + * @group 392 + * @link https://github.com/10up/ElasticPress/issues/392 + */ + public function testArchivedSite( ) { + $index_count = ep_count_indexes(); + + $count_indexes = $index_count['total_indexes']; + $last_blog_id = $index_count['last_blog_id_with_index']; + + update_blog_status( $last_blog_id, 'archived', '1' ); + + $post_delete_count = ep_count_indexes(); + $post_count_indexes = $post_delete_count['total_indexes']; + + $this->assertNotEquals( $count_indexes, $post_count_indexes ); + } + /** * Check if elasticpress_enabled() properly handles an object without the is_search() method. * @group 285 diff --git a/tests/test-single-site.php b/tests/test-single-site.php index f590fb6d9e..ddd62ca34e 100644 --- a/tests/test-single-site.php +++ b/tests/test-single-site.php @@ -1,7 +1,12 @@ assertEquals( 1, $query->post_count ); + $this->assertEquals( 1, count( $query->posts ) ); $this->assertEquals( 3, $query->found_posts ); $found_posts[] = $query->posts[0]->ID; @@ -483,6 +489,7 @@ public function testPagination() { $query = new WP_Query( $args ); $this->assertEquals( 1, $query->post_count ); + $this->assertEquals( 1, count( $query->posts ) ); $this->assertEquals( 3, $query->found_posts ); $found_posts[] = $query->posts[0]->ID; @@ -496,6 +503,7 @@ public function testPagination() { $query = new WP_Query( $args ); $this->assertEquals( 1, $query->post_count ); + $this->assertEquals( 1, count( $query->posts ) ); $this->assertEquals( 3, $query->found_posts ); $found_posts[] = $query->posts[0]->ID; @@ -509,6 +517,7 @@ public function testPagination() { $query = new WP_Query( $args ); $this->assertEquals( 0, $query->post_count ); + $this->assertEquals( 0, count( $query->posts ) ); $this->assertEquals( 3, $query->found_posts ); $this->assertEquals( 3, count( array_unique( $found_posts ) ) ); @@ -569,6 +578,56 @@ public function testCategoryNameQuery() { $this->assertEquals( 2, $query->found_posts ); } + /** + * Test a post__in query + * + * @since 1.5 + */ + public function testPostInQuery() { + $post_ids = array(); + + $post_ids[0] = ep_create_and_sync_post( array( 'post_content' => 'findme test 1' ) ); + $post_ids[1] = ep_create_and_sync_post( array( 'post_content' => 'findme test 2' ) ); + $post_ids[2] = ep_create_and_sync_post( array( 'post_content' => 'findme test 3' ) ); + + ep_refresh_index(); + + $args = array( + 's' => 'findme', + 'post__in' => array( $post_ids[0], $post_ids[1] ), + ); + + $query = new WP_Query( $args ); + + $this->assertEquals( 2, $query->post_count ); + $this->assertEquals( 2, $query->found_posts ); + } + + /** + * Test a post__not_in query + * + * @since 1.5 + */ + public function testPostNotInQuery() { + $post_ids = array(); + + $post_ids[0] = ep_create_and_sync_post( array( 'post_content' => 'findme test 1' ) ); + $post_ids[1] = ep_create_and_sync_post( array( 'post_content' => 'findme test 2' ) ); + $post_ids[2] = ep_create_and_sync_post( array( 'post_content' => 'findme test 3' ) ); + + ep_refresh_index(); + + $args = array( + 's' => 'findme', + 'post__not_in' => array( $post_ids[0] ), + ); + + $query = new WP_Query( $args ); + + $this->assertEquals( 2, $query->post_count ); + $this->assertEquals( 2, $query->found_posts ); + } + /** * Test an author ID query * @@ -790,6 +849,75 @@ public function testAnyPostTypeQuery() { $this->assertEquals( 3, $query->found_posts ); } + /** + * Test meta mapping for complex arrays. All complex arrays are serialized + * + * @since 1.7 + */ + public function testSearchMetaMappingComplexArray() { + ep_create_and_sync_post( array( 'post_content' => 'post content' ), array( 'test_key' => array( 'test' ) ) ); + + ep_refresh_index(); + $args = array( + 'ep_integrate' => true, + ); + + $query = new WP_Query( $args ); + + $this->assertEquals( 1, $query->post_count ); + + $this->assertEquals( 1, count( $query->posts[0]->post_meta['test_key'] ) ); // Make sure there is only one value + + $this->assertTrue( is_array( unserialize( $query->posts[0]->post_meta['test_key'][0] ) ) ); // Make sure value is properly serialized + } + + /** + * Test meta mapping for complex objects. All complex objects are serialized + * + * @since 1.7 + */ + public function testSearchMetaMappingComplexObject() { + $object = new stdClass(); + $object->test = 'hello'; + + ep_create_and_sync_post( array( 'post_content' => 'post content' ), array( 'test_key' => $object ) ); + + ep_refresh_index(); + $args = array( + 'ep_integrate' => true, + ); + + $query = new WP_Query( $args ); + + $this->assertEquals( 1, $query->post_count ); + + $this->assertEquals( 1, count( $query->posts[0]->post_meta['test_key'] ) ); // Make sure there is only one value + + $this->assertEquals( 'hello', unserialize( $query->posts[0]->post_meta['test_key'][0] )->test ); // Make sure value is properly serialized + } + + /** + * Test meta mapping for simple string + * + * @since 1.7 + */ + public function testSearchMetaMappingString() { + ep_create_and_sync_post( array( 'post_content' => 'post content' ), array( 'test_key' => 'test' ) ); + + ep_refresh_index(); + $args = array( + 'ep_integrate' => true, + ); + + $query = new WP_Query( $args ); + + $this->assertEquals( 1, $query->post_count ); + + $this->assertEquals( 1, count( $query->posts[0]->post_meta['test_key'] ) ); // Make sure there is only one value + + $this->assertEquals( 'test', $query->posts[0]->post_meta['test_key'][0] ); + } + /** * Test a query that fuzzy searches meta * @@ -987,6 +1115,36 @@ public function testSearchPostDateOrderbyQuery() { $this->assertEquals( 'ordertes 333', $query->posts[2]->post_title ); } + /** + * Test post_date default order for ep_integrate query with no search + * + * @since 1.7 + */ + public function testSearchPostDateOrderbyQueryEPIntegrate() { + ep_create_and_sync_post( array( 'post_title' => 'ordertest 333' ) ); + sleep( 3 ); + + ep_create_and_sync_post( array( 'post_title' => 'ordertest ordertest order test 111' ) ); + sleep( 3 ); + + ep_create_and_sync_post( array( 'post_title' => 'ordertest 222' ) ); + + ep_refresh_index(); + + $args = array( + 'ep_integrate' => true, + 'order' => 'desc', + ); + + $query = new WP_Query( $args ); + + $this->assertEquals( 3, $query->post_count ); + $this->assertEquals( 3, $query->found_posts ); + $this->assertEquals( 'ordertest 222', $query->posts[0]->post_title ); + $this->assertEquals( 'ordertest ordertest order test 111', $query->posts[1]->post_title ); + $this->assertEquals( 'ordertest 333', $query->posts[2]->post_title ); + } + /** * Test relevance orderby query advanced * @@ -1820,82 +1978,23 @@ public function testGetHost() { } /** - * Test wrapper around wp_remote_request + * Helper method for mocking indexable post statuses + * + * @param array $post_statuses + * @return array */ - public function testEPRemoteRequest() { - - global $ep_backup_host; - - $ep_backup_host = false; - - define( 'EP_FORCE_HOST_REFRESH', true ); - - //Test with EP_HOST constant - $request_1 = false; - $request = ep_remote_request( '', array() ); - - if ( ! is_wp_error( $request ) ) { - if ( isset( $request['response']['code'] ) && 200 === $request['response']['code'] ) { - $request_1 = true; - } - } - - //Test with only backups - - define( 'EP_HOST_USE_ONLY_BACKUPS', true ); - - $request_2 = false; - $ep_backup_host = array( 'http://127.0.0.1:9200' ); - $request = ep_remote_request( '', array() ); - - if ( ! is_wp_error( $request ) ) { - if ( isset( $request['response']['code'] ) && 200 === $request['response']['code'] ) { - $request_2 = true; - } - } - - $request_3 = false; - $ep_backup_host = array( 'bad host 1', 'bad host 2' ); - $request = ep_remote_request( '', array() ); - - if ( is_wp_error( $request ) ) { - $request_3 = $request; - } - - $request_4 = false; - $ep_backup_host = array( 'http://127.0.0.1:9200', 'bad host 2' ); - $request = ep_remote_request( '', array() ); - - if ( ! is_wp_error( $request ) ) { - if ( isset( $request['response']['code'] ) && 200 === $request['response']['code'] ) { - $request_4 = true; - } - } - - $request_5 = false; - $ep_backup_host = array( 'bad host 1', 'http://127.0.0.1:9200' ); - $request = ep_remote_request( '', array() ); - - if ( ! is_wp_error( $request ) ) { - if ( isset( $request['response']['code'] ) && 200 === $request['response']['code'] ) { - $request_5 = true; - } - } - - $this->assertTrue( $request_1 ); - $this->assertTrue( $request_2 ); - $this->assertWPError( $request_3 ); - $this->assertTrue( $request_4 ); - $this->assertTrue( $request_5 ); - - } - - public function mock_indexable_post_status($post_statuses){ + public function mock_indexable_post_status( $post_statuses ) { $post_statuses = array(); $post_statuses[] = "draft"; return $post_statuses; } + /** + * Test invalid post date time + * + * @param array $post_statuses + * @return array + */ public function testPostInvalidDateTime(){ add_filter( 'ep_indexable_post_status', array( $this, 'mock_indexable_post_status' ), 10, 1 ); $post_id = ep_create_and_sync_post( array( 'post_status' => 'draft' ) ); @@ -1923,7 +2022,7 @@ public function testPostInvalidDateTime(){ /** * Test to verify that a post type that is set to exclude_from_search isn't indexable. - * @group 321 + * * @since 1.6 * @link https://github.com/10up/ElasticPress/issues/321 */ @@ -1932,5 +2031,437 @@ public function testExcludeIndexablePostType() { $this->assertArrayNotHasKey( 'ep_test_excluded', $post_types ); $this->assertArrayNotHasKey( 'ep_test_not_public', $post_types ); } + + /** + * Test to make sure that brand new posts with 'auto-draft' post status do not fire delete or sync. + * + * @since 1.6 + * @link https://github.com/10up/ElasticPress/issues/343 + */ + public function testAutoDraftPostStatus() { + // Let's test inserting an 'auto-draft' post. + add_action( 'http_api_debug', array( $this, '_check_404' ), 10, 5 ); + $new_post = wp_insert_post( array( 'post_title' => 'Auto Draft', 'post_status' => 'auto-draft' ) ); + + $this->assertFalse( $this->is_404, 'auto-draft post status on wp_insert_post action.' ); + + // Now let's test inserting a 'publish' post. + $this->is_404 = false; + add_action( 'http_api_debug', array( $this, '_check_404' ), 10, 5 ); + $new_post = wp_insert_post( array( 'post_title' => 'Published', 'post_status' => 'publish' ) ); + + $this->assertFalse( $this->is_404, 'publish post status on wp_insert_post action.' ); + } + + /** + * Runs on http_api_debug action to check for a returned 404 status code. + * + * @param array|WP_Error $response HTTP response or WP_Error object. + * @param string $type Context under which the hook is fired. + * @param string $class HTTP transport used. + * @param array $args HTTP request arguments. + * @param string $url The request URL. + */ + function _check_404( $response, $type, $class, $args, $url ) { + $response_code = $response[ 'response' ][ 'code' ]; + if ( 404 == $response_code ) { + $this->is_404 = true; + } + remove_action( 'http_api_debug', array( $this, '_check_404' ) ); + } + + /** + * Test to verify meta array is built correctly. + * + * @since 1.7 + */ + public function testPrepareMeta() { + + $post_id = ep_create_and_sync_post(); + $post = get_post( $post_id ); + $meta_values = array( + 'value 1', + 'value 2', + ); + + add_post_meta( $post_id, 'test_meta_1', 'value 1' ); + add_post_meta( $post_id, 'test_meta_1', 'value 2' ); + add_post_meta( $post_id, 'test_meta_1', $meta_values ); + add_post_meta( $post_id, '_test_private_meta_1', 'value 1' ); + add_post_meta( $post_id, '_test_private_meta_1', 'value 2' ); + add_post_meta( $post_id, '_test_private_meta_1', $meta_values ); + + $api = new EP_API(); + + $meta_1 = $api->prepare_meta( $post ); + + add_filter( 'ep_prepare_meta_allowed_protected_keys', array( $this, 'filter_ep_prepare_meta_allowed_protected_keys' ) ); + + $meta_2 = $api->prepare_meta( $post ); + + add_filter( 'ep_prepare_meta_excluded_public_keys', array( $this, 'filter_ep_prepare_meta_excluded_public_keys' ) ); + + $meta_3 = $api->prepare_meta( $post ); + + $this->assertTrue( is_array( $meta_1 ) && 1 === sizeof( $meta_1 ) ); + $this->assertTrue( is_array( $meta_1 ) && array_key_exists( 'test_meta_1', $meta_1 ) ); + $this->assertTrue( is_array( $meta_2 ) && 2 === sizeof( $meta_2 ) ); + $this->assertTrue( is_array( $meta_2 ) && array_key_exists( 'test_meta_1', $meta_2 ) && array_key_exists( '_test_private_meta_1', $meta_2 ) ); + $this->assertTrue( is_array( $meta_3 ) && 1 === sizeof( $meta_3 ) ); + $this->assertTrue( is_array( $meta_3 ) && array_key_exists( '_test_private_meta_1', $meta_3 ) ); + + } + + /** + * Helper method for filtering private meta keys + * + * @param array $meta_keys + * @return array + */ + public function filter_ep_prepare_meta_allowed_protected_keys( $meta_keys ) { + + $meta_keys[] = '_test_private_meta_1'; + + return $meta_keys; + + } + + /** + * Helper method for filtering excluded meta keys + * + * @param array $meta_keys + * @return array + */ + public function filter_ep_prepare_meta_excluded_public_keys( $meta_keys ) { + + $meta_keys[] = 'test_meta_1'; + + return $meta_keys; + + } + + /** + * Test meta preparation + * + * Tests meta perparation + * + * @since 1.7 + */ + public function testMetaValueTypes() { + + $api = new EP_API(); + + $intval = $api->prepare_meta_value_types( 13 ); + $floatval = $api->prepare_meta_value_types( 13.43 ); + $textval = $api->prepare_meta_value_types( 'some text' ); + $bool_false_val = $api->prepare_meta_value_types( false ); + $bool_true_val = $api->prepare_meta_value_types( true ); + $dateval = $api->prepare_meta_value_types( '2015-01-01' ); + + $this->assertTrue( is_array( $intval ) && 5 === sizeof( $intval ) ); + $this->assertTrue( is_array( $intval ) && array_key_exists( 'long', $intval ) && 13 === $intval['long'] ); + $this->assertTrue( is_array( $floatval ) && 5 === sizeof( $floatval ) ); + $this->assertTrue( is_array( $floatval ) && array_key_exists( 'double', $floatval ) && 13.43 === $floatval['double'] ); + $this->assertTrue( is_array( $textval ) && 6 === sizeof( $textval ) ); + $this->assertTrue( is_array( $textval ) && array_key_exists( 'raw', $textval ) && 'some text' === $textval['raw'] ); + $this->assertTrue( is_array( $bool_false_val ) && 3 === sizeof( $bool_false_val ) ); + $this->assertTrue( is_array( $bool_false_val ) && array_key_exists( 'boolean', $bool_false_val ) && false === $bool_false_val['boolean'] ); + $this->assertTrue( is_array( $bool_true_val ) && 3 === sizeof( $bool_true_val ) ); + $this->assertTrue( is_array( $bool_true_val ) && array_key_exists( 'boolean', $bool_true_val ) && true === $bool_true_val['boolean'] ); + $this->assertTrue( is_array( $dateval ) && 6 === sizeof( $dateval ) ); + $this->assertTrue( is_array( $dateval ) && array_key_exists( 'datetime', $dateval ) && '2015-01-01 00:00:00' === $dateval['datetime'] ); + + } + + /** + * Test numeric integer meta queries + * + * @since 1.7 + */ + public function testMetaValueTypeQueryNumeric() { + + ep_create_and_sync_post( array( 'post_content' => 'the post content findme' ) ); + ep_create_and_sync_post( array( 'post_content' => 'post content findme' ), array( 'test_key' => 100 ) ); + ep_create_and_sync_post( array( 'post_content' => 'the post content findme' ) ); + ep_create_and_sync_post( array( 'post_content' => 'post content findme' ), array( 'test_key' => 101 ) ); + + ep_refresh_index(); + $args = array( + 's' => 'findme', + 'meta_query' => array( + array( + 'key' => 'test_key', + 'value' => 101, + 'compare' => '>=', + 'type' => 'numeric', + ) + ), + ); + + $query = new WP_Query( $args ); -} + $this->assertEquals( 1, $query->post_count ); + $this->assertEquals( 1, $query->found_posts ); + + $args = array( + 's' => 'findme', + 'meta_query' => array( + array( + 'key' => 'test_key', + 'value' => 100, + 'compare' => '=', + 'type' => 'numeric', + ) + ), + ); + + $query = new WP_Query( $args ); + + $this->assertEquals( 1, $query->post_count ); + $this->assertEquals( 1, $query->found_posts ); + + $args = array( + 's' => 'findme', + 'meta_query' => array( + array( + 'key' => 'test_key', + 'value' => 103, + 'compare' => '<=', + 'type' => 'numeric', + ) + ), + ); + + $query = new WP_Query( $args ); + + $this->assertEquals( 2, $query->post_count ); + $this->assertEquals( 2, $query->found_posts ); + + } + + /** + * Test decimal meta queries + * + * @since 1.7 + */ + public function testMetaValueTypeQueryDecimal() { + + ep_create_and_sync_post( array( 'post_content' => 'the post content findme' ) ); + ep_create_and_sync_post( array( 'post_content' => 'post content findme' ), array( 'test_key' => 15.5 ) ); + ep_create_and_sync_post( array( 'post_content' => 'the post content findme' ) ); + ep_create_and_sync_post( array( 'post_content' => 'post content findme' ), array( 'test_key' => 16.5 ) ); + + ep_refresh_index(); + $args = array( + 's' => 'findme', + 'meta_query' => array( + array( + 'key' => 'test_key', + 'value' => 16.5, + 'compare' => '<', + 'type' => 'decimal', + ) + ), + ); + + $query = new WP_Query( $args ); + + $this->assertEquals( 1, $query->post_count ); + $this->assertEquals( 1, $query->found_posts ); + + $args = array( + 's' => 'findme', + 'meta_query' => array( + array( + 'key' => 'test_key', + 'value' => 16.5, + 'compare' => '=', + 'type' => 'decimal', + ) + ), + ); + + $query = new WP_Query( $args ); + + $this->assertEquals( 1, $query->post_count ); + $this->assertEquals( 1, $query->found_posts ); + } + + /** + * Test character meta queries. Really just defaults to a normal string query + * + * @since 1.7 + */ + public function testMetaValueTypeQueryChar() { + + ep_create_and_sync_post( array( 'post_content' => 'the post content findme' ) ); + ep_create_and_sync_post( array( 'post_content' => 'post content findme' ), array( 'test_key' => 'abc' ) ); + ep_create_and_sync_post( array( 'post_content' => 'the post content findme' ) ); + ep_create_and_sync_post( array( 'post_content' => 'post content findme' ), array( 'test_key' => 'acc' ) ); + + ep_refresh_index(); + $args = array( + 's' => 'findme', + 'meta_query' => array( + array( + 'key' => 'test_key', + 'value' => 'abc', + 'compare' => '=', + 'type' => 'char', + ) + ), + ); + + $query = new WP_Query( $args ); + + $this->assertEquals( 1, $query->post_count ); + $this->assertEquals( 1, $query->found_posts ); + } + + /** + * Test date meta queries + * + * @since 1.7 + */ + public function testMetaValueTypeQueryDate() { + ep_create_and_sync_post( array( 'post_content' => 'post content findme' ), array( 'test_key' => '11/13/15' ) ); + ep_create_and_sync_post( array( 'post_content' => 'the post content findme' ) ); + ep_create_and_sync_post( array( 'post_content' => 'post content findme' ), array( 'test_key' => '11/15/15' ) ); + + ep_refresh_index(); + $args = array( + 's' => 'findme', + 'meta_query' => array( + array( + 'key' => 'test_key', + 'value' => '2015-11-14', + 'compare' => '>', + 'type' => 'date', + ) + ), + ); + + $query = new WP_Query( $args ); + + $this->assertEquals( 1, $query->post_count ); + + $args = array( + 's' => 'findme', + 'meta_query' => array( + array( + 'key' => 'test_key', + 'value' => '2015-11-15', + 'compare' => '=', + 'type' => 'date', + ) + ), + ); + + $query = new WP_Query( $args ); + + $this->assertEquals( 1, $query->post_count ); + + } + + /** + * Test time meta queries + * + * @since 1.7 + */ + public function testMetaValueTypeQueryTime() { + ep_create_and_sync_post( array( 'post_content' => 'the post content findme' ) ); + ep_create_and_sync_post( array( 'post_content' => 'post content findme' ), array( 'test_key' => '5:00am' ) ); + + ep_refresh_index(); + $args = array( + 's' => 'findme', + 'meta_query' => array( + array( + 'key' => 'test_key', + 'value' => '17:00:00', + 'compare' => '<', + 'type' => 'time', + ) + ), + ); + + $query = new WP_Query( $args ); + + $this->assertEquals( 1, $query->post_count ); + + $args = array( + 's' => 'findme', + 'meta_query' => array( + array( + 'key' => 'test_key', + 'value' => '05:00:00', + 'compare' => '=', + 'type' => 'time', + ) + ), + ); + + $query = new WP_Query( $args ); + + $this->assertEquals( 1, $query->post_count ); + } + + /** + * Test date time meta queries + * + * @since 1.7 + */ + public function testMetaValueTypeQueryDatetime() { + ep_create_and_sync_post( array( 'post_content' => 'the post content findme' ) ); + ep_create_and_sync_post( array( 'post_content' => 'post content findme' ), array( 'test_key' => '5:00am 1/2/12' ) ); + + ep_refresh_index(); + $args = array( + 's' => 'findme', + 'meta_query' => array( + array( + 'key' => 'test_key', + 'value' => '2013-03-02 06:00:15', + 'compare' => '<', + 'type' => 'datetime', + ) + ), + ); + + $query = new WP_Query( $args ); + + $this->assertEquals( 1, $query->post_count ); + + $args = array( + 's' => 'findme', + 'meta_query' => array( + array( + 'key' => 'test_key', + 'value' => '2012-01-02 05:00:00', + 'compare' => '=', + 'type' => 'datetime', + ) + ), + ); + + $query = new WP_Query( $args ); + + $this->assertEquals( 1, $query->post_count ); + + $args = array( + 's' => 'findme', + 'meta_query' => array( + array( + 'key' => 'test_key', + 'value' => '2011-01-02 07:30:00', + 'compare' => '>', + 'type' => 'datetime', + ) + ), + ); + + $query = new WP_Query( $args ); + + $this->assertEquals( 1, $query->post_count ); + } +} \ No newline at end of file