diff --git a/.travis.yml b/.travis.yml index 7fab69336a..6a7e88279f 100644 --- a/.travis.yml +++ b/.travis.yml @@ -14,7 +14,14 @@ services: - elasticsearch before_script: + - mkdir -p build/logs + - if [ "$(phpenv version-name)" != "5.2" ]; then composer install --dev; fi; - bash bin/install-wp-tests.sh wordpress_test root '' localhost $WP_VERSION - sleep 5 -script: phpunit \ No newline at end of file +script: + - if [ "$(phpenv version-name)" != "5.2" ]; then phpunit --coverage-clover build/logs/clover.xml; else phpunit; fi; + +after_script: + - ./vendor/bin/test-reporter --stdout > codeclimate.json + - "if [ \"$(phpenv version-name)\" != \"5.2\" ]; then curl -X POST -d @codeclimate.json -H 'Content-Type: application/json' -H 'User-Agent: Code Climate (PHP Test Reporter v0.1.1)' https://codeclimate.com/test_reports; fi;" \ No newline at end of file diff --git a/README.md b/README.md index ddd7dbfea2..e55f88a110 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -ElasticPress [![Build Status](https://travis-ci.org/10up/ElasticPress.svg?branch=master)](https://travis-ci.org/10up/ElasticPress) +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) ============= Integrate [Elasticsearch](http://www.elasticsearch.org/) with [WordPress](http://wordpress.org/). @@ -48,7 +48,7 @@ The proceeding sets depend on whether you are configuring for single site or mul ### Single Site 2. Activate the plugin. -3. Using wp-cli, do an initial sync (with mapping) with your ES server by running the following commands: +3. Using WP-CLI, do an initial sync (with mapping) with your ES server by running the following commands: ```bash wp elasticpress index --setup @@ -57,7 +57,7 @@ wp elasticpress index --setup ### Multisite Cross-site Search 2. Network activate the plugin -3. Using wp-cli, do an initial sync (with mapping) with your ES server by running the following commands: +3. Using WP-CLI, do an initial sync (with mapping) with your ES server by running the following commands: ```bash wp elasticpress index --setup --network-wide @@ -71,7 +71,7 @@ Creating indices is handled automatically by ElasticPress. Index names are autom ## Usage -After running an index, ElasticPress integrates with WP_Query. The end goal is to support all the parameters available to WP_Query so the transition is completely transparent. Right now, our WP_Query integration supports *many* of the relevant WP_Query parameters and adds a couple special ones. +After running an index, ElasticPress integrates with `WP_Query` if and only if the query is a search or the `ep_integrate` parameter is passed (see below). The end goal is to support all the parameters available to WP_Query so the transition is completely transparent. Right now, our WP_Query integration supports *many* of the relevant WP_Query parameters and adds a couple special ones. ### Supported WP_Query Parameters @@ -101,9 +101,55 @@ After running an index, ElasticPress integrates with WP_Query. The end goal is t ```tax_query``` accepts an array of arrays where each inner array *only* supports ```taxonomy``` (string) and ```terms``` (string|array) parameters. ```terms``` is a slug, either in string or array form. +* ```meta_query``` (*array*) + + Filter posts by post meta conditions. Takes an array of form: + + ```php + new WP_Query( array( + 's' => 'search phrase', + 'meta_query' => array( + array( + 'key' => 'key_name', + 'value' => 'meta value', + 'compare' => '=', + ), + ), + ) ); + ``` + + ```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: + + * ```=``` - 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```. + * ```EXISTS``` - Posts will be returned that have a post meta key corresponding to ```key```. + * ```NOT EXISTS``` - Posts will be returned that do not have a post meta key corresponding to ```key```. + + The outer array also supports a ```relation``` (string) parameter. By default ```relation``` is set to ```AND```: + ```php + new WP_Query( array( + 's' => 'search phrase', + 'meta_query' => array( + array( + 'key' => 'key_name', + 'value' => 'meta value', + 'compare' => '=', + ), + array( + 'key' => 'key_name2', + 'value' => 'meta value', + 'compare' => '!=', + ), + 'relation' => 'AND', + ), + ) ); + ``` + + 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. + * ```post_type``` (*string*/*array*) - Filter posts by post type. ```any``` wil search all public post types. + 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`. * ```offset``` (*int*) @@ -256,14 +302,14 @@ The following are special parameters that are only supported by ElasticPress. _Note:_ Nesting cross-site `WP_Query` loops can result in unexpected behavior. -* ```ep_match_all``` (*bool*) +* ```ep_integrate``` (*bool*) - Allows you to perform queries without passing a search parameter. For example: + Allows you to perform queries without passing a search parameter. This is pretty powerful as you can leverage Elasticsearch to retrieve queries that are too complex for MySQL (such as a 5-dimensional taxonomy query). For example: Get 20 of the lastest posts ```php new WP_Query( array( - 'ep_match_all' => true, + 'ep_integrate' => true, 'post_type' => 'post', 'posts_per_page' => 20, ) ); @@ -272,13 +318,52 @@ The following are special parameters that are only supported by ElasticPress. Get all posts with a specific category ```php new WP_Query( array( - 'ep_match_all' => true, + 'ep_integrate' => true, 'post_type' => 'post', 'posts_per_page' => -1, 'category' => 5, ) ); ``` +### Supported WP-CLI Commands + +The following commands are supported by ElasticPress: + +* `wp elasticpress index [--setup] [--network-wide] [--posts-per-page] [--no-bulk]` + + Index all posts in the current blog. `--network-wide` will force indexing on all the blogs in 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. + +* `wp elasticpress activate` + + Turns on ElasticPress integration. Integration is automatically deactivated during indexing. + +* `wp elasticpress deactivate` + + Turns off ElasticPress integration. Integration is automatically deactivated during indexing. + +* `wp elasticpress delete-index [--network-wide]` + + Deletes the current blog index. `--network-wide` will force every index on the network to be deleted. + +* `wp elasticpress is-active` + + Checks whether ElasticPress is currently integration active. This is different than whether the plugin is WordPress active or not. During indexing, integration will be deactivated automatically. + +* `wp elasticpress put-mapping [--network-wide]` + + Sends plugin put mapping to the current blog index. `--network-wide` will force mappings to be sent for every index in the network. + +* `wp elasticpress recreate-network-alias` + + Recreates the alias index which points to every index in the network. + +* `wp elasticpress stats` + + Returns basic stats on Elasticsearch instance i.e. number of documents in current index as well as disk space used. + +* `wp elasticpress status` + + ## Development ### Setup @@ -318,3 +403,8 @@ EP_HOST="http://192.168.50.4:9200" phpunit ### Issues If you identify any errors or have an idea for improving the plugin, please [open an issue](https://github.com/10up/ElasticPress/issues?state=open). We're excited to see what the community thinks of this project, and we would love your input! + + +## 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. \ No newline at end of file diff --git a/bin/wp-cli.php b/bin/wp-cli.php index bb8ca301d9..414bfa623c 100644 --- a/bin/wp-cli.php +++ b/bin/wp-cli.php @@ -34,7 +34,7 @@ class ElasticPress_CLI_Command extends WP_CLI_Command { public function put_mapping( $args, $assoc_args ) { $this->_connect_check(); - if ( ! empty( $assoc_args['network-wide'] ) ) { + if ( ! empty( $assoc_args['network-wide'] ) && is_multisite() ) { $sites = ep_get_sites(); foreach ( $sites as $site ) { @@ -85,7 +85,7 @@ public function put_mapping( $args, $assoc_args ) { public function delete_index( $args, $assoc_args ) { $this->_connect_check(); - if ( ! empty( $assoc_args['network-wide'] ) ) { + if ( ! empty( $assoc_args['network-wide'] ) && is_multisite() ) { $sites = ep_get_sites(); foreach ( $sites as $site ) { @@ -166,7 +166,7 @@ private function _create_network_alias() { /** * Index all posts for a site or network wide * - * @synopsis [--setup] [--network-wide] [--posts-per-page] + * @synopsis [--setup] [--network-wide] [--posts-per-page] [--no-bulk] * @param array $args * * @since 0.1.2 @@ -190,13 +190,13 @@ public function index( $args, $assoc_args ) { timer_start(); // Run setup if flag was passed - if ( true === $assoc_args['setup'] ) { + if ( isset( $assoc_args['setup'] ) && true === $assoc_args['setup'] ) { // Right now setup is just the put_mapping command, as this also deletes the index(s) first $this->put_mapping( $args, $assoc_args ); } - if ( ! empty( $assoc_args['network-wide'] ) ) { + if ( ! empty( $assoc_args['network-wide'] ) && is_multisite() ) { WP_CLI::log( __( 'Indexing posts network-wide...', 'elasticpress' ) ); @@ -249,6 +249,7 @@ public function index( $args, $assoc_args ) { * Helper method for indexing posts * * @param bool $no_bulk disable bulk indexing + * @param int $posts_per_page * * @since 0.9 * @return array @@ -264,7 +265,7 @@ private function _index_helper( $no_bulk = false, $posts_per_page) { $args = apply_filters( 'ep_index_posts_args', array( 'posts_per_page' => $posts_per_page, 'post_type' => ep_get_indexable_post_types(), - 'post_status' => 'publish', + 'post_status' => ep_get_indexable_post_status(), 'offset' => $offset, 'ignore_sticky_posts' => true ) ); @@ -428,7 +429,10 @@ private function send_bulk_errors() { if ( ! empty( $this->failed_posts ) ) { $email_text = __( "The following posts failed to index:\r\n\r\n", 'elasticpress' ); foreach ( $this->failed_posts as $failed ) { - $email_text .= "- {$failed}: " . get_post( $failed )->post_title . "\r\n"; + $failed_post = get_post( $failed ); + if ( $failed_post ) { + $email_text .= "- {$failed}: " . get_post( $failed )->post_title . "\r\n"; + } } $send_mail = wp_mail( get_option( 'admin_email' ), wp_specialchars_decode( get_option( 'blogname' ) ) . __( ': ElasticPress Index Errors', 'elasticpress' ), $email_text ); @@ -445,7 +449,6 @@ private function send_bulk_errors() { /** * Ping the Elasticsearch server and retrieve a status. * - * @synopsis [--raw] [--index=] * @since 0.9.1 */ public function status() { diff --git a/classes/class-ep-api.php b/classes/class-ep-api.php index b7407d8b9a..09dc2d97ca 100644 --- a/classes/class-ep-api.php +++ b/classes/class-ep-api.php @@ -138,7 +138,7 @@ public function search( $args, $scope = 'current' ) { return array( 'found_posts' => $response['hits']['total'], 'posts' => $posts ); } - return array( 'found_posts' => 0, 'posts' => array() ); + return false; } /** @@ -292,7 +292,7 @@ public function put_mapping() { 'default' => array( 'tokenizer' => 'standard', 'filter' => array( 'standard', 'ewp_word_delimiter', 'lowercase', 'stop', 'ewp_snowball' ), - 'language' => 'English' + 'language' => apply_filters( 'ep_analyzer_language', 'English' ), ), 'shingle_analyzer' => array( 'type' => 'custom', @@ -312,8 +312,8 @@ public function put_mapping() { ), 'ewp_snowball' => array( 'type' => 'snowball', - 'language' => 'English' - ), + 'language' => apply_filters( 'ep_analyzer_language', 'English' ), + ), 'edge_ngram' => array( 'side' => 'front', 'max_gram' => 10, @@ -447,7 +447,7 @@ public function put_mapping() { ), 'post_status' => array( 'type' => 'string', - 'index' => 'no' + 'index' => 'not_analyzed' ), 'post_name' => array( 'type' => 'multi_field', @@ -515,6 +515,8 @@ public function put_mapping() { $request = wp_remote_request( $index_url, array( 'body' => json_encode( $mapping ), 'method' => 'PUT' ) ); + $request = apply_filters( 'ep_config_mapping_request', $request, $index_url, $mapping ); + if ( ! is_wp_error( $request ) && 200 === wp_remote_retrieve_response_code( $request ) ) { $response_body = wp_remote_retrieve_body( $request ); @@ -583,7 +585,7 @@ public function prepare_post( $post_id ) { 'post_title' => get_the_title( $post_id ), 'post_excerpt' => $post->post_excerpt, 'post_content' => apply_filters( 'the_content', $post->post_content ), - 'post_status' => 'publish', + 'post_status' => $post->post_status, 'post_name' => $post->post_name, 'post_modified' => $post_modified, 'post_modified_gmt' => $post_modified_gmt, @@ -817,6 +819,99 @@ public function format_args( $args ) { $use_filters = true; } + /** + * 'meta_query' arg support. + * + * 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 + */ + if ( ! empty( $args['meta_query'] ) ) { + $meta_filter = array(); + + $relation = 'must'; + if ( ! empty( $args['meta_query']['relation'] ) && 'or' === strtolower( $args['meta_query']['relation'] ) ) { + $relation = 'should'; + } + + foreach( $args['meta_query'] as $single_meta_query ) { + if ( ! empty( $single_meta_query['key'] ) ) { + + $terms_obj = false; + + $compare = '='; + if ( ! empty( $single_meta_query['compare'] ) ) { + $compare = strtolower( $single_meta_query['compare'] ); + } + + switch ( $compare ) { + case '!=': + if ( isset( $single_meta_query['value'] ) ) { + $terms_obj = array( + 'bool' => array( + 'must_not' => array( + array( + 'terms' => array( + 'post_meta.' . $single_meta_query['key'] . '.raw' => (array) $single_meta_query['value'], + ), + ), + ), + ), + ); + } + + break; + case 'exists': + $terms_obj = array( + 'exists' => array( + 'field' => 'post_meta.' . $single_meta_query['key'], + ), + ); + + break; + case 'not exists': + $terms_obj = array( + 'bool' => array( + 'must_not' => array( + array( + 'exists' => array( + 'field' => 'post_meta.' . $single_meta_query['key'], + ), + ), + ), + ), + ); + + break; + case '=': + default: + if ( isset( $single_meta_query['value'] ) ) { + $terms_obj = array( + 'terms' => array( + 'post_meta.' . $single_meta_query['key'] . '.raw' => (array) $single_meta_query['value'], + ), + ); + } + + break; + } + + // Add the meta query filter + if ( false !== $terms_obj ) { + $meta_filter[] = $terms_obj; + } + } + } + + if ( ! empty( $meta_filter ) ) { + $filter['and'][]['bool'][$relation] = $meta_filter; + + $use_filters = true; + } + } + /** * Allow for search field specification * @@ -883,28 +978,45 @@ public function format_args( $args ) { ), ), ); - if ( ! empty( $args['s'] ) && ! isset( $args['ep_match_all'] ) ) { + + /** + * We are using ep_integrate instead of ep_match_all. ep_match_all will be + * supported for legacy code but may be deprecated and removed eventually. + * + * @since 1.3 + */ + + if ( ! empty( $args['s'] ) && empty( $args['ep_match_all'] ) && empty( $args['ep_integrate'] ) ) { $query['bool']['should'][1]['fuzzy_like_this']['like_text'] = $args['s']; $query['bool']['should'][0]['multi_match']['query'] = $args['s']; $formatted_args['query'] = $query; - } else if ( isset( $args['ep_match_all'] ) && true === $args['ep_match_all'] ) { + } else if ( ! empty( $args['ep_match_all'] ) || ! empty( $args['ep_integrate'] ) ) { $formatted_args['query']['match_all'] = array(); } - if ( isset( $args['post_type'] ) ) { - $post_types = (array) $args['post_type']; - $terms_map_name = 'terms'; - if ( count( $post_types ) < 2 ) { - $terms_map_name = 'term'; - } + /** + * Like WP_Query in search context, if no post_type is specified we default to "any". To + * be safe you should ALWAYS specify the post_type parameter UNLIKE with WP_Query. + * + * @since 1.3 + */ + if ( ! empty( $args['post_type'] ) ) { + // should NEVER be "any" but just in case + if ( 'any' !== $args['post_type'] ) { + $post_types = (array) $args['post_type']; + $terms_map_name = 'terms'; + if ( count( $post_types ) < 2 ) { + $terms_map_name = 'term'; + } - $filter['and'][] = array( - $terms_map_name => array( - 'post_type.raw' => $post_types, - ), - ); + $filter['and'][] = array( + $terms_map_name => array( + 'post_type.raw' => $post_types, + ), + ); - $use_filters = true; + $use_filters = true; + } } if ( isset( $args['offset'] ) ) { @@ -948,7 +1060,7 @@ public function format_args( $args ) { } } - return apply_filters( 'ep_formatted_args', $formatted_args ); + return apply_filters( 'ep_formatted_args', $formatted_args, $args ); } /** @@ -987,7 +1099,9 @@ public function elasticpress_enabled( $query ) { if ( $query->is_search() ) { $enabled = true; - } else if ( isset( $query->query['ep_match_all'] ) && true === $query->query['ep_match_all'] ) { + } elseif ( ! empty( $query->query['ep_match_all'] ) ) { // ep_match_all is supported for legacy reasons + $enabled = true; + } elseif ( ! empty( $query->query['ep_integrate'] ) ) { $enabled = true; } @@ -1115,32 +1229,6 @@ public function elasticsearch_alive() { return $elasticsearch_alive; } - - /** - * Ensures that this index exists - * - * @param null $index - * - * @return bool - * @since 1.1.0 - */ - public function index_exists( $index = null ) { - $index_exists = false; - - $index_url = ep_get_index_url( $index ); - - $url = $index_url . '/_status'; - - $request = wp_remote_request( $url ); - - if ( ! is_wp_error( $request ) ) { - if ( isset( $request['response']['code'] ) && 200 === $request['response']['code'] ) { - $index_exists = true; - } - } - - return $index_exists; - } } EP_API::factory(); diff --git a/classes/class-ep-config.php b/classes/class-ep-config.php index 435187ed80..ca4ec482c2 100644 --- a/classes/class-ep-config.php +++ b/classes/class-ep-config.php @@ -71,6 +71,16 @@ public function get_indexable_post_types() { return apply_filters( 'ep_indexable_post_types', $post_types ); } + /** + * Return indexable post_status for the current site + * + * @since 1.3 + * @return array + */ + public function get_indexable_post_status() { + return apply_filters( 'ep_indexable_post_status', array( 'publish' ) ); + } + /** * Generate network index name for alias * @@ -106,6 +116,10 @@ function ep_get_indexable_post_types() { return EP_Config::factory()->get_indexable_post_types(); } +function ep_get_indexable_post_status() { + return EP_Config::factory()->get_indexable_post_status(); +} + function ep_get_network_alias() { return EP_Config::factory()->get_network_alias(); } diff --git a/classes/class-ep-sync-manager.php b/classes/class-ep-sync-manager.php index 3ed5d7758d..315b4a536a 100644 --- a/classes/class-ep-sync-manager.php +++ b/classes/class-ep-sync-manager.php @@ -52,7 +52,9 @@ public function action_sync_on_transition( $new_status, $old_status, $post ) { return; } - if ( 'publish' !== $new_status && 'publish' !== $old_status ) { + $indexable_post_statuses = ep_get_indexable_post_status(); + + if ( ! in_array( $new_status, $indexable_post_statuses ) && ! in_array( $old_status, $indexable_post_statuses ) ) { return; } @@ -61,7 +63,7 @@ public function action_sync_on_transition( $new_status, $old_status, $post ) { } // Our post was published, but is no longer, so let's remove it from the Elasticsearch index - if ( 'publish' !== $new_status ) { + if ( ! in_array( $new_status, $indexable_post_statuses ) ) { $this->action_delete_post( $post->ID ); } else { $post_type = get_post_type( $post->ID ); diff --git a/classes/class-ep-wp-query-integration.php b/classes/class-ep-wp-query-integration.php index b9d51a0ff6..9be55ab4b3 100644 --- a/classes/class-ep-wp-query-integration.php +++ b/classes/class-ep-wp-query-integration.php @@ -9,6 +9,8 @@ class EP_WP_Query_Integration { */ private $query_stack = array(); + private $posts_by_query = array(); + /** * Placeholder method * @@ -16,8 +18,11 @@ class EP_WP_Query_Integration { */ public function __construct() { } + /** + * Checks to see if we should be integrating and if so, sets up the appropriate actions and filters. + * @since 0.9 + */ public function setup() { - // Ensure we aren't on the admin (unless overridden) if ( is_admin() && ! apply_filters( 'ep_admin_wp_query_integration', false ) ) { return; @@ -28,14 +33,10 @@ public function setup() { return; } - // If we can't reach the Elasticsearch service, don't bother with the rest of this - if ( ! ep_index_exists() ) { - return; - } - // Make sure we return nothing for MySQL posts query add_filter( 'posts_request', array( $this, 'filter_posts_request' ), 10, 2 ); + // Add header add_action( 'pre_get_posts', array( $this, 'action_pre_get_posts' ), 5 ); // Nukes the FOUND_ROWS() database query @@ -54,12 +55,26 @@ public function setup() { add_action( 'the_post', array( $this, 'action_the_post' ), 10, 1 ); } + /** + * Disables cache_results, adds header. + * + * @param $query + * @since 0.9 + */ public function action_pre_get_posts( $query ) { if ( ! ep_elasticpress_enabled( $query ) || apply_filters( 'ep_skip_query_integration', false, $query ) ) { return; } $query->set( 'cache_results', false ); + + if ( ! headers_sent() ) { + /** + * Manually setting a header as $wp_query isn't yet initialized + * when we call: add_filter('wp_headers', 'filter_wp_headers'); + */ + header( 'X-ElasticPress-Search: true' ); + } } /** @@ -128,20 +143,87 @@ public function action_loop_end( $query ) { } /** - * Filter the posts array to contain ES search results in EP_Post form. + * Filter the posts array to contain ES search results in EP_Post form. Pull previously search posts. * * @param array $posts * @param object &$query * @return array */ public function filter_the_posts( $posts, &$query ) { - if ( ! ep_elasticpress_enabled( $query ) || apply_filters( 'ep_skip_query_integration', false, $query ) ) { + if ( ! ep_elasticpress_enabled( $query ) || apply_filters( 'ep_skip_query_integration', false, $query ) || ! isset( $this->posts_by_query[spl_object_hash( $query )] ) ) { return $posts; } + $new_posts = $this->posts_by_query[spl_object_hash( $query )]; + + return $new_posts; + } + + /** + * Remove the found_rows from the SQL Query + * + * @param string $sql + * @param object $query + * @since 0.9 + * @return string + */ + public function filter_found_posts_query( $sql, $query ) { + if ( ! ep_elasticpress_enabled( $query ) || apply_filters( 'ep_skip_query_integration', false, $query ) ) { + return $sql; + } + + return ''; + } + + /** + * Filter query string used for get_posts(). Search for posts and save for later. + * Return a query that will return nothing. + * + * @param string $request + * @param object $query + * @since 0.9 + * @return string + */ + public function filter_posts_request( $request, $query ) { + if ( ! ep_elasticpress_enabled( $query ) || apply_filters( 'ep_skip_query_integration', false, $query ) ) { + return $request; + } + $query_vars = $query->query_vars; - if ( 'any' == $query_vars['post_type'] ) { - unset( $query_vars['post_type'] ); + if ( 'any' === $query_vars['post_type'] ) { + + if ( $query->is_search() ) { + + /* + * This is a search query + * To follow WordPress conventions, + * make sure we only search 'searchable' post types + */ + $searchable_post_types = get_post_types( array( 'exclude_from_search' => false ) ); + + // If we have no searchable post types, there's no point going any further + if ( empty( $searchable_post_types ) ) { + + // Have to return something or it improperly calculates the found_posts + return "WHERE 0 = 1"; + } + + // Conform the post types array to an acceptable format for ES + $post_types = array(); + foreach( $searchable_post_types as $type ) { + $post_types[] = $type; + } + + // These are now the only post types we will search + $query_vars['post_type'] = $post_types; + } else { + + /* + * This is not a search query + * so unset the post_type query var + */ + unset( $query_vars['post_type'] ); + } } $scope = 'current'; @@ -153,10 +235,14 @@ public function filter_the_posts( $posts, &$query ) { $search = ep_search( $formatted_args, $scope ); + if ( false === $search ) { + return $request; + } + $query->found_posts = $search['found_posts']; $query->max_num_pages = ceil( $search['found_posts'] / $query->get( 'posts_per_page' ) ); - $posts = array(); + $new_posts = array(); foreach ( $search['posts'] as $post_array ) { $post = new stdClass(); @@ -168,6 +254,7 @@ public function filter_the_posts( $posts, &$query ) { $post->site_id = $post_array['site_id']; } + $post->post_type = $post_array['post_type']; $post->post_name = $post_array['post_name']; $post->post_status = $post_array['post_status']; $post->post_title = $post_array['post_title']; @@ -177,48 +264,18 @@ public function filter_the_posts( $posts, &$query ) { $post->post_date_gmt = $post_array['post_date_gmt']; $post->post_modified = $post_array['post_modified']; $post->post_modified_gmt = $post_array['post_modified_gmt']; + $post->elasticsearch = true; // Super useful for debugging // Run through get_post() to add all expected properties (even if they're empty) $post = get_post( $post ); if ( $post ) { - $posts[] = $post; + $new_posts[] = $post; } } + $this->posts_by_query[spl_object_hash( $query )] = $new_posts; - do_action( 'ep_wp_query_search', $posts, $search, $query ); - - return $posts; - } - - /** - * Remove the found_rows from the SQL Query - * - * @param string $sql - * @param object $query - * @since 0.9 - * @return string - */ - public function filter_found_posts_query( $sql, $query ) { - if ( ! ep_elasticpress_enabled( $query ) || apply_filters( 'ep_skip_query_integration', false, $query ) ) { - return $sql; - } - - return ''; - } - - /** - * Filter query string used for get_posts(). Return a query that will return nothing. - * - * @param string $request - * @param object $query - * @since 0.9 - * @return string - */ - public function filter_posts_request( $request, $query ) { - if ( ! ep_elasticpress_enabled( $query ) || apply_filters( 'ep_skip_query_integration', false, $query ) ) { - return $request; - } + do_action( 'ep_wp_query_search', $new_posts, $search, $query ); global $wpdb; @@ -236,7 +293,7 @@ public static function factory() { if ( ! $instance ) { $instance = new self(); - $instance->setup(); + add_action( 'init', array( $instance, 'setup' ) ); } return $instance; diff --git a/composer.json b/composer.json new file mode 100644 index 0000000000..e276215b0f --- /dev/null +++ b/composer.json @@ -0,0 +1,28 @@ +{ + "name": "10up/ElasticPress", + "description": "Integrate Elasticsearch with WordPress.", + "type": "wordpress-plugin", + "keywords": ["wordpress", "plugin", "elasticsearch", "elasticpress", "search"], + "license": ["MIT"], + "authors": [ + { + "name": "Aaron Holbrook", + "email": "aaron@10up.com", + "homepage": "http://aaronjholbrook.com" + }, + { + "name": "Taylor Lovett", + "email": "taylorl@get10up.com" + }, + { + "name": "10up", + "homepage": "http://10up.com" + } + ], + "require": { + "php": ">=5.2" + }, + "require-dev": { + "codeclimate/php-test-reporter": "dev-master" + } +} diff --git a/composer.lock b/composer.lock new file mode 100644 index 0000000000..ab9b320469 --- /dev/null +++ b/composer.lock @@ -0,0 +1,581 @@ +{ + "_readme": [ + "This file locks the dependencies of your project to a known state", + "Read more about it at http://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file", + "This file is @generated automatically" + ], + "hash": "332f73e3674c21dc09f838e8b7fff5b8", + "packages": [], + "packages-dev": [ + { + "name": "codeclimate/php-test-reporter", + "version": "dev-master", + "source": { + "type": "git", + "url": "https://github.com/codeclimate/php-test-reporter.git", + "reference": "2dd8395f81874333d15de3a598f722997ba42fb5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/codeclimate/php-test-reporter/zipball/2dd8395f81874333d15de3a598f722997ba42fb5", + "reference": "2dd8395f81874333d15de3a598f722997ba42fb5", + "shasum": "" + }, + "require": { + "ext-curl": "*", + "php": ">=5.3", + "satooshi/php-coveralls": "0.6.*", + "symfony/console": ">=2.0" + }, + "require-dev": { + "phpunit/phpunit": "3.7.*@stable" + }, + "bin": [ + "composer/bin/test-reporter" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "0.1.x-dev" + } + }, + "autoload": { + "psr-0": { + "CodeClimate\\Component": "src/", + "CodeClimate\\Bundle": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Code Climate", + "email": "hello@codeclimate.com", + "homepage": "https://codeclimate.com" + } + ], + "description": "PHP client for reporting test coverage to Code Climate", + "homepage": "https://github.com/codeclimate/php-test-reporter", + "keywords": [ + "codeclimate", + "coverage" + ], + "time": "2014-12-29 16:17:04" + }, + { + "name": "guzzle/guzzle", + "version": "v3.9.2", + "source": { + "type": "git", + "url": "https://github.com/guzzle/guzzle3.git", + "reference": "54991459675c1a2924122afbb0e5609ade581155" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/guzzle3/zipball/54991459675c1a2924122afbb0e5609ade581155", + "reference": "54991459675c1a2924122afbb0e5609ade581155", + "shasum": "" + }, + "require": { + "ext-curl": "*", + "php": ">=5.3.3", + "symfony/event-dispatcher": "~2.1" + }, + "replace": { + "guzzle/batch": "self.version", + "guzzle/cache": "self.version", + "guzzle/common": "self.version", + "guzzle/http": "self.version", + "guzzle/inflection": "self.version", + "guzzle/iterator": "self.version", + "guzzle/log": "self.version", + "guzzle/parser": "self.version", + "guzzle/plugin": "self.version", + "guzzle/plugin-async": "self.version", + "guzzle/plugin-backoff": "self.version", + "guzzle/plugin-cache": "self.version", + "guzzle/plugin-cookie": "self.version", + "guzzle/plugin-curlauth": "self.version", + "guzzle/plugin-error-response": "self.version", + "guzzle/plugin-history": "self.version", + "guzzle/plugin-log": "self.version", + "guzzle/plugin-md5": "self.version", + "guzzle/plugin-mock": "self.version", + "guzzle/plugin-oauth": "self.version", + "guzzle/service": "self.version", + "guzzle/stream": "self.version" + }, + "require-dev": { + "doctrine/cache": "~1.3", + "monolog/monolog": "~1.0", + "phpunit/phpunit": "3.7.*", + "psr/log": "~1.0", + "symfony/class-loader": "~2.1", + "zendframework/zend-cache": "2.*,<2.3", + "zendframework/zend-log": "2.*,<2.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.9-dev" + } + }, + "autoload": { + "psr-0": { + "Guzzle": "src/", + "Guzzle\\Tests": "tests/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + }, + { + "name": "Guzzle Community", + "homepage": "https://github.com/guzzle/guzzle/contributors" + } + ], + "description": "Guzzle is a PHP HTTP client library and framework for building RESTful web service clients", + "homepage": "http://guzzlephp.org/", + "keywords": [ + "client", + "curl", + "framework", + "http", + "http client", + "rest", + "web service" + ], + "time": "2014-08-11 04:32:36" + }, + { + "name": "psr/log", + "version": "1.0.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/log.git", + "reference": "fe0936ee26643249e916849d48e3a51d5f5e278b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/log/zipball/fe0936ee26643249e916849d48e3a51d5f5e278b", + "reference": "fe0936ee26643249e916849d48e3a51d5f5e278b", + "shasum": "" + }, + "type": "library", + "autoload": { + "psr-0": { + "Psr\\Log\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "http://www.php-fig.org/" + } + ], + "description": "Common interface for logging libraries", + "keywords": [ + "log", + "psr", + "psr-3" + ], + "time": "2012-12-21 11:40:51" + }, + { + "name": "satooshi/php-coveralls", + "version": "v0.6.1", + "source": { + "type": "git", + "url": "https://github.com/satooshi/php-coveralls.git", + "reference": "dd0df95bd37a7cf5c5c50304dfe260ffe4b50760" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/satooshi/php-coveralls/zipball/dd0df95bd37a7cf5c5c50304dfe260ffe4b50760", + "reference": "dd0df95bd37a7cf5c5c50304dfe260ffe4b50760", + "shasum": "" + }, + "require": { + "ext-curl": "*", + "ext-json": "*", + "ext-simplexml": "*", + "guzzle/guzzle": ">=3.0", + "php": ">=5.3", + "psr/log": "1.0.0", + "symfony/config": ">=2.0", + "symfony/console": ">=2.0", + "symfony/stopwatch": ">=2.2", + "symfony/yaml": ">=2.0" + }, + "require-dev": { + "apigen/apigen": "2.8.*@stable", + "pdepend/pdepend": "dev-master", + "phpmd/phpmd": "dev-master", + "phpunit/php-invoker": ">=1.1.0,<1.2.0", + "phpunit/phpunit": "3.7.*@stable", + "sebastian/finder-facade": "dev-master", + "sebastian/phpcpd": "1.4.*@stable", + "squizlabs/php_codesniffer": "1.4.*@stable", + "theseer/fdomdocument": "dev-master" + }, + "bin": [ + "composer/bin/coveralls" + ], + "type": "library", + "autoload": { + "psr-0": { + "Contrib\\Component": "src/", + "Contrib\\Bundle": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Kitamura Satoshi", + "email": "with.no.parachute@gmail.com", + "homepage": "https://www.facebook.com/satooshi.jp" + } + ], + "description": "PHP client library for Coveralls API", + "homepage": "https://github.com/satooshi/php-coveralls", + "keywords": [ + "ci", + "coverage", + "github", + "test" + ], + "time": "2013-05-04 08:07:33" + }, + { + "name": "symfony/config", + "version": "v2.6.1", + "target-dir": "Symfony/Component/Config", + "source": { + "type": "git", + "url": "https://github.com/symfony/Config.git", + "reference": "84c0c150c1520995f09ea9e47e817068b353cb0f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/Config/zipball/84c0c150c1520995f09ea9e47e817068b353cb0f", + "reference": "84c0c150c1520995f09ea9e47e817068b353cb0f", + "shasum": "" + }, + "require": { + "php": ">=5.3.3", + "symfony/filesystem": "~2.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.6-dev" + } + }, + "autoload": { + "psr-0": { + "Symfony\\Component\\Config\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Symfony Community", + "homepage": "http://symfony.com/contributors" + }, + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + } + ], + "description": "Symfony Config Component", + "homepage": "http://symfony.com", + "time": "2014-12-02 20:19:20" + }, + { + "name": "symfony/console", + "version": "v2.6.1", + "target-dir": "Symfony/Component/Console", + "source": { + "type": "git", + "url": "https://github.com/symfony/Console.git", + "reference": "ef825fd9f809d275926547c9e57cbf14968793e8" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/Console/zipball/ef825fd9f809d275926547c9e57cbf14968793e8", + "reference": "ef825fd9f809d275926547c9e57cbf14968793e8", + "shasum": "" + }, + "require": { + "php": ">=5.3.3" + }, + "require-dev": { + "psr/log": "~1.0", + "symfony/event-dispatcher": "~2.1", + "symfony/process": "~2.1" + }, + "suggest": { + "psr/log": "For using the console logger", + "symfony/event-dispatcher": "", + "symfony/process": "" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.6-dev" + } + }, + "autoload": { + "psr-0": { + "Symfony\\Component\\Console\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Symfony Community", + "homepage": "http://symfony.com/contributors" + }, + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + } + ], + "description": "Symfony Console Component", + "homepage": "http://symfony.com", + "time": "2014-12-02 20:19:20" + }, + { + "name": "symfony/event-dispatcher", + "version": "v2.6.1", + "target-dir": "Symfony/Component/EventDispatcher", + "source": { + "type": "git", + "url": "https://github.com/symfony/EventDispatcher.git", + "reference": "720fe9bca893df7ad1b4546649473b5afddf0216" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/EventDispatcher/zipball/720fe9bca893df7ad1b4546649473b5afddf0216", + "reference": "720fe9bca893df7ad1b4546649473b5afddf0216", + "shasum": "" + }, + "require": { + "php": ">=5.3.3" + }, + "require-dev": { + "psr/log": "~1.0", + "symfony/config": "~2.0", + "symfony/dependency-injection": "~2.6", + "symfony/expression-language": "~2.6", + "symfony/stopwatch": "~2.2" + }, + "suggest": { + "symfony/dependency-injection": "", + "symfony/http-kernel": "" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.6-dev" + } + }, + "autoload": { + "psr-0": { + "Symfony\\Component\\EventDispatcher\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Symfony Community", + "homepage": "http://symfony.com/contributors" + }, + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + } + ], + "description": "Symfony EventDispatcher Component", + "homepage": "http://symfony.com", + "time": "2014-12-02 20:19:20" + }, + { + "name": "symfony/filesystem", + "version": "v2.6.1", + "target-dir": "Symfony/Component/Filesystem", + "source": { + "type": "git", + "url": "https://github.com/symfony/Filesystem.git", + "reference": "ff6efc95256cb33031933729e68b01d720b5436b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/Filesystem/zipball/ff6efc95256cb33031933729e68b01d720b5436b", + "reference": "ff6efc95256cb33031933729e68b01d720b5436b", + "shasum": "" + }, + "require": { + "php": ">=5.3.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.6-dev" + } + }, + "autoload": { + "psr-0": { + "Symfony\\Component\\Filesystem\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Symfony Community", + "homepage": "http://symfony.com/contributors" + }, + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + } + ], + "description": "Symfony Filesystem Component", + "homepage": "http://symfony.com", + "time": "2014-12-02 20:19:20" + }, + { + "name": "symfony/stopwatch", + "version": "v2.6.1", + "target-dir": "Symfony/Component/Stopwatch", + "source": { + "type": "git", + "url": "https://github.com/symfony/Stopwatch.git", + "reference": "261abd360cfb6ac65ea93ffd82073e2011d034fc" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/Stopwatch/zipball/261abd360cfb6ac65ea93ffd82073e2011d034fc", + "reference": "261abd360cfb6ac65ea93ffd82073e2011d034fc", + "shasum": "" + }, + "require": { + "php": ">=5.3.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.6-dev" + } + }, + "autoload": { + "psr-0": { + "Symfony\\Component\\Stopwatch\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Symfony Community", + "homepage": "http://symfony.com/contributors" + }, + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + } + ], + "description": "Symfony Stopwatch Component", + "homepage": "http://symfony.com", + "time": "2014-12-02 20:19:20" + }, + { + "name": "symfony/yaml", + "version": "v2.6.1", + "target-dir": "Symfony/Component/Yaml", + "source": { + "type": "git", + "url": "https://github.com/symfony/Yaml.git", + "reference": "3346fc090a3eb6b53d408db2903b241af51dcb20" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/Yaml/zipball/3346fc090a3eb6b53d408db2903b241af51dcb20", + "reference": "3346fc090a3eb6b53d408db2903b241af51dcb20", + "shasum": "" + }, + "require": { + "php": ">=5.3.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.6-dev" + } + }, + "autoload": { + "psr-0": { + "Symfony\\Component\\Yaml\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Symfony Community", + "homepage": "http://symfony.com/contributors" + }, + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + } + ], + "description": "Symfony Yaml Component", + "homepage": "http://symfony.com", + "time": "2014-12-02 20:19:20" + } + ], + "aliases": [], + "minimum-stability": "stable", + "stability-flags": { + "codeclimate/php-test-reporter": 20 + }, + "prefer-stable": false, + "prefer-lowest": false, + "platform": { + "php": ">=5.2" + }, + "platform-dev": [] +} diff --git a/elasticpress.php b/elasticpress.php index fa7126bac5..87611c5b3b 100644 --- a/elasticpress.php +++ b/elasticpress.php @@ -3,10 +3,10 @@ /** * Plugin Name: ElasticPress * Description: Integrate WordPress search with Elasticsearch - * Version: 1.2 + * Version: 1.3 * Author: Aaron Holbrook, Taylor Lovett, Matt Gross, 10up * Author URI: http://10up.com - * License: MIT + * License: GPLv2 or later * * This program derives work from Alley Interactive's SearchPress * and Automattic's VIP search plugin: diff --git a/readme.txt b/readme.txt index d8d9a39291..8c04349882 100644 --- a/readme.txt +++ b/readme.txt @@ -5,9 +5,9 @@ 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.1 -Stable tag: 1.2 -License: MIT -License URI: http://opensource.org/licenses/MIT +Stable tag: 1.3 +License: GPLv2 or later +License URI: http://www.gnu.org/licenses/gpl-2.0.html Integrate Elasticsearch with WordPress. @@ -58,6 +58,18 @@ configuring single site and multi-site cross-site search are slightly different. == Changelog == += 1.3 = +* Support `meta_query` in WP_Query integration +* Improved documentation. Each WP-CLI command has been documented +* Add `elasticsearch` property to global post object to assist in debugging +* `ep_integrate` param added to allow for WP_Query integration without search. (Formally called ep_match_all) +* Bug fix: check if failed post exists in indexing. Props [elliot-stocks](https://github.com/elliott-stocks) +* Bug fix: properly check if setup is defined in indexing. Props [elliot-stocks](https://github.com/elliott-stocks) +* Bug fix: add WP_Query integration on init rather than plugins loaded. Props [adamsilverstein](https://github.com/adamsilverstein) +* Bug fix: Properly set global post object post type in loop. Props [tott](https://github.com/tott) +* Bug fix: Do not check if index exists on every page load. Refactor so we can revert to MySQL after failed ES ping. +* Bug fix: Make sure we check `is_multisite()` if `--network-wide` is provided [ivankruchkoff](https://github.com/ivankruchkoff) + = 1.2 = * Allow number of shards and replicas to be configurable. * Improved searching algorithm. Favor exact matches over fuzzy matches. diff --git a/tests/test-multisite.php b/tests/test-multisite.php index 29aba11e6a..b1e4d6958e 100644 --- a/tests/test-multisite.php +++ b/tests/test-multisite.php @@ -463,11 +463,11 @@ public function testTaxQuery() { } /** - * Test a post type query search + * Test a post type query search for pages * - * @since 1.0 + * @since 1.3 */ - public function testPostTypeQuery() { + public function testPostTypeSearchQueryPage() { $sites = ep_get_sites(); $i = 0; @@ -500,6 +500,118 @@ public function testPostTypeQuery() { $this->assertEquals( $query->found_posts, 2 ); } + /** + * Test a post type query search for posts + * + * @since 1.3 + */ + public function testPostTypeSearchQueryPost() { + $sites = ep_get_sites(); + + $i = 0; + + foreach ( $sites as $site ) { + switch_to_blog( $site['blog_id'] ); + + ep_create_and_sync_post( array( 'post_content' => 'findme', 'post_type' => 'page' ) ); + + if ( $i > 0 ) { + ep_create_and_sync_post( array( 'post_content' => 'findme' ) ); + } + + ep_refresh_index(); + + restore_current_blog(); + + $i++; + } + + $args = array( + 's' => 'findme', + 'sites' => 'all', + 'post_type' => 'post', + ); + + $query = new WP_Query( $args ); + + $this->assertEquals( $query->post_count, 2 ); + $this->assertEquals( $query->found_posts, 2 ); + } + + /** + * Test a post type query search where no post type is specified + * + * @since 1.3 + */ + public function testNoPostTypeSearchQuery() { + $sites = ep_get_sites(); + + $i = 0; + + foreach ( $sites as $site ) { + switch_to_blog( $site['blog_id'] ); + + ep_create_and_sync_post( array( 'post_content' => 'findme', 'post_type' => 'page' ) ); + + if ( $i > 0 ) { + ep_create_and_sync_post( array( 'post_content' => 'findme' ) ); + } + + ep_refresh_index(); + + restore_current_blog(); + + $i++; + } + + $args = array( + 's' => 'findme', + 'sites' => 'all', + ); + + $query = new WP_Query( $args ); + + $this->assertEquals( $query->post_count, 5 ); + $this->assertEquals( $query->found_posts, 5 ); + } + + /** + * Test a post type query non-search where no post type is specified + * + * @since 1.3 + */ + public function testNoPostTypeNoSearchQuery() { + $sites = ep_get_sites(); + + $i = 0; + + foreach ( $sites as $site ) { + switch_to_blog( $site['blog_id'] ); + + ep_create_and_sync_post( array( 'post_content' => 'findme', 'post_type' => 'page' ) ); + + if ( $i > 0 ) { + ep_create_and_sync_post( array( 'post_content' => 'findme' ) ); + } + + ep_refresh_index(); + + restore_current_blog(); + + $i++; + } + + $args = array( + 'ep_integrate' => true, + 'sites' => 'all', + ); + + $query = new WP_Query( $args ); + + $this->assertEquals( $query->post_count, 5 ); + $this->assertEquals( $query->found_posts, 5 ); + } + /** * Test an author ID query * @@ -623,6 +735,58 @@ public function testSearchMetaQuery() { $this->assertEquals( $query->found_posts, 2 ); } + /** + * Test a search with a filter on meta + * + * @since 1.3 + */ + public function testFilterMetaQuery() { + $sites = ep_get_sites(); + + $i = 0; + + foreach ( $sites as $site ) { + switch_to_blog( $site['blog_id'] ); + + ep_create_and_sync_post( array( 'post_content' => 'post content findme' ), array( 'test_key' => 'findme', 'test_key2' => 'findme3', ) ); + + if ( $i > 0 ) { + ep_create_and_sync_post( array( 'post_content' => 'post content findme' ), array( 'test_key2' => 'findme', 'test_key' => 'value2', 'test_key3' => 'findme' ) ); + } + + ep_refresh_index(); + + restore_current_blog(); + + $i++; + } + + $args = array( + 's' => 'findme', + 'sites' => 'all', + 'meta_query' => array( + array( + 'key' => 'test_key', + 'value' => 'value2' + ), + array( + 'key' => 'test_key2', + 'value' => 'findme3', + 'compare' => '!=', + ), + array( + 'key' => 'test_key3', + 'compare' => 'exists', + ) + ) + ); + + $query = new WP_Query( $args ); + + $this->assertEquals( $query->post_count, 2 ); + $this->assertEquals( $query->found_posts, 2 ); + } + /** * Test a fuzzy search on taxonomy terms * diff --git a/tests/test-single-site.php b/tests/test-single-site.php index 2d1a9a6254..821bfa8794 100644 --- a/tests/test-single-site.php +++ b/tests/test-single-site.php @@ -409,11 +409,11 @@ public function testAuthorNameQuery() { } /** - * Test a post type query + * Test a post type query for pages * - * @since 1.0 + * @since 1.3 */ - public function testPostTypeQuery() { + public function testPostTypeQueryPage() { ep_create_and_sync_post( array( 'post_content' => 'findme test 1', 'post_type' => 'page' ) ); ep_create_and_sync_post( array( 'post_content' => 'findme test 2' ) ); ep_create_and_sync_post( array( 'post_content' => 'findme test 3', 'post_type' => 'page' ) ); @@ -431,6 +431,99 @@ public function testPostTypeQuery() { $this->assertEquals( 2, $query->found_posts ); } + + /** + * Test a post type query for posts + * + * @since 1.3 + */ + public function testPostTypeQueryPost() { + ep_create_and_sync_post( array( 'post_content' => 'findme test 1', 'post_type' => 'page' ) ); + ep_create_and_sync_post( array( 'post_content' => 'findme test 2' ) ); + ep_create_and_sync_post( array( 'post_content' => 'findme test 3', 'post_type' => 'page' ) ); + + ep_refresh_index(); + + $args = array( + 's' => 'findme', + 'post_type' => 'post', + ); + + $query = new WP_Query( $args ); + + $this->assertEquals( 1, $query->post_count ); + $this->assertEquals( 1, $query->found_posts ); + } + + /** + * Test a query with no post type + * + * @since 1.3 + */ + public function testNoPostTypeSearchQuery() { + ep_create_and_sync_post( array( 'post_content' => 'findme test 1', 'post_type' => 'page' ) ); + ep_create_and_sync_post( array( 'post_content' => 'findme test 2' ) ); + ep_create_and_sync_post( array( 'post_content' => 'findme test 3' ) ); + + ep_refresh_index(); + + // post_type defaults to "any" + $args = array( + 's' => 'findme', + ); + + $query = new WP_Query( $args ); + + $this->assertEquals( 3, $query->post_count ); + $this->assertEquals( 3, $query->found_posts ); + } + + /** + * Test a query with no post type on non-search query + * + * @since 1.3 + */ + public function testNoPostTypeNonSearchQuery() { + ep_create_and_sync_post( array( 'post_content' => 'findme test 1', 'post_type' => 'page' ) ); + ep_create_and_sync_post( array( 'post_content' => 'findme test 2' ) ); + ep_create_and_sync_post( array( 'post_content' => 'findme test 3' ) ); + + ep_refresh_index(); + + // post_type defaults to "any" + $args = array( + 'ep_integrate' => true, + ); + + $query = new WP_Query( $args ); + + $this->assertEquals( 3, $query->post_count ); + $this->assertEquals( 3, $query->found_posts ); + } + + /** + * Test a query with "any" post type + * + * @since 1.3 + */ + public function testAnyPostTypeQuery() { + ep_create_and_sync_post( array( 'post_content' => 'findme test 1', 'post_type' => 'page' ) ); + ep_create_and_sync_post( array( 'post_content' => 'findme test 2' ) ); + ep_create_and_sync_post( array( 'post_content' => 'findme test 3', 'post_type' => 'page' ) ); + + ep_refresh_index(); + + $args = array( + 's' => 'findme', + 'post_type' => 'any', + ); + + $query = new WP_Query( $args ); + + $this->assertEquals( 3, $query->post_count ); + $this->assertEquals( 3, $query->found_posts ); + } + /** * Test a query that fuzzy searches meta * @@ -869,4 +962,267 @@ public function testEmptySearchString() { $this->assertEquals( 2, $query->post_count ); $this->assertEquals( 2, $query->found_posts ); } + + /** + * Test a query that searches and filters by a meta equal query + * + * @since 1.3 + */ + public function testMetaQueryEquals() { + ep_create_and_sync_post( array( 'post_content' => 'the post content' ) ); + 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' => 'value' ) ); + + ep_refresh_index(); + $args = array( + 's' => 'findme', + 'meta_query' => array( + array( + 'key' => 'test_key', + 'value' => 'value', + ) + ), + ); + + $query = new WP_Query( $args ); + + $this->assertEquals( 1, $query->post_count ); + $this->assertEquals( 1, $query->found_posts ); + } + + /** + * Test a query that searches and filters by a meta not equal query + * + * @since 1.3 + */ + public function testMetaQueryNotEquals() { + ep_create_and_sync_post( array( 'post_content' => 'the post content findme' ) ); + 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' => 'value' ) ); + + ep_refresh_index(); + $args = array( + 's' => 'findme', + 'meta_query' => array( + array( + 'key' => 'test_key', + 'value' => 'value', + 'compare' => '!=', + ) + ), + ); + + $query = new WP_Query( $args ); + + $this->assertEquals( 2, $query->post_count ); + $this->assertEquals( 2, $query->found_posts ); + } + + /** + * Test a query that searches and filters by a meta exists query + * + * @since 1.3 + */ + public function testMetaQueryExists() { + ep_create_and_sync_post( array( 'post_content' => 'the post content findme' ) ); + 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' => 'value' ) ); + + ep_refresh_index(); + $args = array( + 's' => 'findme', + 'meta_query' => array( + array( + 'key' => 'test_key', + 'compare' => 'exists', + ) + ), + ); + + $query = new WP_Query( $args ); + + $this->assertEquals( 1, $query->post_count ); + $this->assertEquals( 1, $query->found_posts ); + } + + /** + * Test a query that searches and filters by a meta not exists query + * + * @since 1.3 + */ + public function testMetaQueryNotExists() { + ep_create_and_sync_post( array( 'post_content' => 'the post content findme' ) ); + 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' => 'value' ) ); + + ep_refresh_index(); + $args = array( + 's' => 'findme', + 'meta_query' => array( + array( + 'key' => 'test_key', + 'compare' => 'not exists', + ) + ), + ); + + $query = new WP_Query( $args ); + + $this->assertEquals( 2, $query->post_count ); + $this->assertEquals( 2, $query->found_posts ); + } + + /** + * Test an advanced meta filter query + * + * @since 1.3 + */ + public function testMetaQueryOrRelation() { + ep_create_and_sync_post( array( 'post_content' => 'the post content findme' ), array( 'test_key5' => 'value1' ) ); + ep_create_and_sync_post( array( 'post_content' => 'the post content findme' ), array( 'test_key' => 'value1', 'test_key2' => 'value' ) ); + ep_create_and_sync_post( array( 'post_content' => 'post content findme' ), array( 'test_key6' => 'value', 'test_key2' => 'value2', 'test_key3' => 'value' ) ); + + ep_refresh_index(); + $args = array( + 's' => 'findme', + 'meta_query' => array( + array( + 'key' => 'test_key5', + 'compare' => 'exists', + ), + array( + 'key' => 'test_key6', + 'value' => 'value', + 'compare' => '=', + ), + 'relation' => 'or', + ), + ); + + $query = new WP_Query( $args ); + + $this->assertEquals( 2, $query->post_count ); + $this->assertEquals( 2, $query->found_posts ); + } + + /** + * Test an advanced meta filter query + * + * @since 1.3 + */ + public function testMetaQueryAdvanced() { + ep_create_and_sync_post( array( 'post_content' => 'the post content findme' ), array( 'test_key' => 'value1' ) ); + ep_create_and_sync_post( array( 'post_content' => 'the post content findme' ), array( 'test_key' => 'value1', 'test_key2' => 'value' ) ); + ep_create_and_sync_post( array( 'post_content' => 'post content findme' ), array( 'test_key' => 'value', 'test_key2' => 'value2', 'test_key3' => 'value' ) ); + + ep_refresh_index(); + $args = array( + 's' => 'findme', + 'meta_query' => array( + array( + 'key' => 'test_key3', + 'compare' => 'exists', + ), + array( + 'key' => 'test_key2', + 'value' => 'value2', + 'compare' => '=', + ), + array( + 'key' => 'test_key', + 'value' => 'value1', + 'compare' => '!=', + ) + ), + ); + + $query = new WP_Query( $args ); + + $this->assertEquals( 1, $query->post_count ); + $this->assertEquals( 1, $query->found_posts ); + } + + /** + * Test exclude_from_search post type flag + * Ensure that we do not search that post type when all post types are searched + * + * @since 1.3 + */ + public function testExcludeFromSearch() { + $post_ids = array(); + + $post_ids[0] = ep_create_and_sync_post(); + $post_ids[1] = ep_create_and_sync_post(); + $post_ids[2] = ep_create_and_sync_post( array( 'post_content' => 'findme' ) ); + $post_ids[3] = ep_create_and_sync_post(); + $post_ids[4] = ep_create_and_sync_post( array( 'post_content' => 'findme' ) ); + + register_post_type( 'exclude-me', array( + 'public' => true, + 'exclude_from_search' => true, + ) ); + + $post_ids[5] = ep_create_and_sync_post( array( 'post_type' => 'exclude-me', 'post_content' => 'findme' ) ); + + ep_refresh_index(); + + $args = array( + 's' => 'findme', + ); + + add_action( 'ep_wp_query_search', array( $this, 'action_wp_query_search' ), 10, 0 ); + + $query = new WP_Query( $args ); + + $this->assertTrue( ! empty( $this->fired_actions['ep_wp_query_search'] ) ); + + $this->assertEquals( $query->post_count, 2 ); + $this->assertEquals( $query->found_posts, 2 ); + + wp_reset_postdata(); + } + + /** + * Test what happens when no post types are available to be searched + * + * @since 1.3 + */ + public function testNoAvailablePostTypesToSearch() { + $post_ids = array(); + + $post_ids[0] = ep_create_and_sync_post(); + $post_ids[1] = ep_create_and_sync_post(); + $post_ids[2] = ep_create_and_sync_post( array( 'post_content' => 'findme' ) ); + $post_ids[3] = ep_create_and_sync_post(); + $post_ids[4] = ep_create_and_sync_post( array( 'post_content' => 'findme' ) ); + + $GLOBALS['wp_post_types']; + + $backup_post_types = $GLOBALS['wp_post_types']; + + // Set all post types to be excluded from search + foreach ( $GLOBALS['wp_post_types'] as $post_type ) { + $post_type->exclude_from_search = true; + } + + ep_refresh_index(); + + $args = array( + 's' => 'findme', + ); + + add_action( 'ep_wp_query_search', array( $this, 'action_wp_query_search' ), 10, 0 ); + + $query = new WP_Query( $args ); + + $this->assertTrue( empty( $this->fired_actions['ep_wp_query_search'] ) ); + + $this->assertEquals( $query->post_count, 0 ); + $this->assertEquals( $query->found_posts, 0 ); + + wp_reset_postdata(); + + // Reset the main $wp_post_types item + $GLOBALS['wp_post_types'] = $backup_post_types; + } } \ No newline at end of file