diff --git a/README.md b/README.md index e8965af00e..267c03d92d 100644 --- a/README.md +++ b/README.md @@ -3,9 +3,6 @@ ElasticPress [![Build Status](https://travis-ci.org/10up/ElasticPress.svg?branch Integrate [Elasticsearch](http://www.elasticsearch.org/) with [WordPress](http://wordpress.org/). -* **Latest Stable**: [v1.0](https://github.com/10up/ElasticPress/releases/tag/v1.0) -* **Contributors**: [@aaronholbrook](https://github.com/AaronHolbrook), [@tlovett1](https://github.com/tlovett1), [@mattonomics](https://github.com/mattonomics), [@ivanlopez](https://github.com/ivanlopez), [@colegeissinger](https://github.com/colegeissinger), [@cmmarslender](https://github.com/cmmarslender), [@ghosttoast](https://github.com/ghoasttoast) - ## Background Let's face it, WordPress search is rudimentary at best. Poor performance, inflexible and rigid matching algorithms (which means no comprehension of 'close' queries), the inability to search metadata and taxonomy information, no way to determine categories of your results and most importantly the overall relevancy of results is poor. @@ -123,6 +120,14 @@ After running an index, ElasticPress integrates with WP_Query. The end goal is t * ```author_name``` (*string*) Show posts associated with certain author. Use ```user_nicename``` (NOT name). + +* ```orderby``` (*string*) + + Order results by field name instead of relevance. Currently only supports: ```title```, ```name```, and ```relevance``` (default). + +* ```order``` (*string*) + + Which direction to order results in. Accepts ```ASC``` and ```DESC```. Default is ```DESC```. The following are special parameters that are only supported by ElasticPress. diff --git a/bin/wp-cli.php b/bin/wp-cli.php index d3acfe71ec..bb8ca301d9 100644 --- a/bin/wp-cli.php +++ b/bin/wp-cli.php @@ -254,6 +254,7 @@ public function index( $args, $assoc_args ) { * @return array */ private function _index_helper( $no_bulk = false, $posts_per_page) { + global $wpdb, $wp_object_cache; $synced = 0; $errors = array(); $offset = 0; @@ -297,6 +298,20 @@ private function _index_helper( $no_bulk = false, $posts_per_page) { $offset += $posts_per_page; usleep( 500 ); + + // Avoid running out of memory + $wpdb->queries = array(); + + if ( is_object( $wp_object_cache ) ) { + $wp_object_cache->group_ops = array(); + $wp_object_cache->stats = array(); + $wp_object_cache->memcache_debug = array(); + $wp_object_cache->cache = array(); + + if ( is_callable( $wp_object_cache, '__remoteset' ) ) { + call_user_func( array( $wp_object_cache, '__remoteset' ) ); // important + } + } } if ( ! $no_bulk ) { @@ -553,8 +568,8 @@ private function _connect_check() { WP_CLI::error( __( 'EP_HOST is not defined! Check wp-config.php', 'elasticpress' ) ); } - if ( false === ep_is_alive() ) { + if ( false === ep_elasticsearch_alive() ) { 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 3e40a53f98..cc9c04e661 100644 --- a/classes/class-ep-api.php +++ b/classes/class-ep-api.php @@ -220,38 +220,6 @@ public function get_post( $post_id ) { return false; } - /** - * This function checks two things - that the plugin is currently 'activated' and that it can successfully reach the - * server. - * - * @since 0.1.1 - * @return bool - */ - public function is_alive() { - $activated_status = ep_is_activated(); - - // If this has been disabled for some reason, then abort early - if ( ! $activated_status ) { - return false; - } - - // Otherwise proceed with our check to the server - $is_alive = false; - - $url = ep_get_index_url() . '/_status'; - - $request = wp_remote_request( $url ); - - if ( ! is_wp_error( $request ) ) { - if ( isset( $request['response']['code'] ) && 200 === $request['response']['code'] ) { - $is_alive = true; - } - } - - // Return our status and cache it - return $is_alive; - } - /** * Delete the network index alias * @@ -452,10 +420,20 @@ public function put_mapping() { 'include_in_all' => false ), 'post_title' => array( - 'type' => 'string', - '_boost' => 3.0, - 'store' => 'yes', - 'analyzer' => 'standard' + 'type' => 'multi_field', + 'fields' => array( + 'post_title' => array( + 'type' => 'string', + 'analyzer' => 'standard', + '_boost' => 3.0, + 'store' => 'yes' + ), + 'raw' => array( + 'type' => 'string', + 'index' => 'not_analyzed', + 'include_in_all' => false + ) + ) ), 'post_excerpt' => array( 'type' => 'string', @@ -709,14 +687,43 @@ public function format_args( $args ) { $formatted_args = array( 'from' => 0, 'size' => $posts_per_page, - 'sort' => array( + ); + + /** + * Order and Orderby arguments + * + * Used for how Elasticsearch will sort results + * + * @since 1.1 + */ + // Set sort order, default is 'desc' + if ( ! empty( $args['order'] ) ) { + $order = $this->parse_order( $args['order'] ); + } else { + $order = 'desc'; + } + + // Set sort type + if ( ! empty( $args['orderby'] ) ) { + $sort = $this->parse_orderby( $args['orderby'], $order ); + + if ( false !== $sort ) { + $formatted_args['sort'] = $sort; + } + } + + // Either nothing was passed or the parse_orderby failed, use default sort + if ( empty( $args['orderby'] ) || false === $sort ) { + + // Default sort is to use the score (based on relevance) + $formatted_args['sort'] = array( array( '_score' => array( - 'order' => 'desc', + 'order' => $order, ), ), - ), - ); + ); + } $filter = array( 'and' => array(), @@ -928,6 +935,7 @@ public function bulk_index_posts( $body ) { * * @param $query * @return bool + * @since 0.9.2 */ public function elasticpress_enabled( $query ) { $enabled = false; @@ -941,17 +949,153 @@ public function elasticpress_enabled( $query ) { return apply_filters( 'ep_elasticpress_enabled', $enabled, $query ); } - public function is_activated() { - return get_site_option( 'ep_is_active', false, false ); - } - + /** + * Deactivate ElasticPress. Disallow EP to override the main WP_Query for search queries + * + * @return bool + * @since 1.0.0 + */ public function deactivate() { return delete_site_option( 'ep_is_active' ); } + /** + * Activate ElasticPress. Allow EP to override the main WP_Query for search queries + * + * @return bool + * @since 1.0.0 + */ public function activate() { return update_site_option( 'ep_is_active', true ); } + + /** + * Parse an 'order' query variable and cast it to ASC or DESC as necessary. + * + * @since 1.1 + * @access protected + * + * @param string $order The 'order' query variable. + * @return string The sanitized 'order' query variable. + */ + protected function parse_order( $order ) { + if ( ! is_string( $order ) || empty( $order ) ) { + return 'desc'; + } + + if ( 'ASC' === strtoupper( $order ) ) { + return 'asc'; + } else { + return 'desc'; + } + } + + /** + * If the passed orderby value is allowed, convert the alias to a + * properly-prefixed sort value. + * + * @since 1.1 + * @access protected + * + * @param string $orderby Alias for the field to order by. + * @return array|bool Array formatted value to used in the sort DSL. False otherwise. + */ + protected function parse_orderby( $orderby, $order ) { + // Used to filter values. + $allowed_keys = array( + 'relevance', + 'name', + 'title', + ); + + if ( ! in_array( $orderby, $allowed_keys ) ) { + return false; + } + + switch ( $orderby ) { + case 'relevance': + default: + $sort = array( + array( + '_score' => array( + 'order' => $order, + ), + ), + ); + break; + case 'name': + case 'title': + $sort = array( + array( + 'post_' . $orderby . '.raw' => array( + 'order' => $order, + ), + ), + ); + break; + } + + return $sort; + } + + /** + * Check to see if ElasticPress is currently active (can be disabled during syncing, etc) + * + * @return mixed + * @since 0.9.2 + */ + public function is_activated() { + return get_site_option( 'ep_is_active', false, false ); + } + + /** + * This function checks two things - that the plugin is currently 'activated' and that it can successfully reach the + * server. + * + * @since 1.1.0 + * @return bool + */ + public function elasticsearch_alive() { + $elasticsearch_alive = false; + + $url = EP_HOST; + + $request = wp_remote_request( $url ); + + if ( ! is_wp_error( $request ) ) { + if ( isset( $request['response']['code'] ) && 200 === $request['response']['code'] ) { + $elasticsearch_alive = true; + } + } + + 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(); @@ -976,10 +1120,6 @@ function ep_delete_post( $post_id ) { return EP_API::factory()->delete_post( $post_id ); } -function ep_is_alive() { - return EP_API::factory()->is_alive(); -} - function ep_put_mapping() { return EP_API::factory()->put_mapping(); } @@ -1020,14 +1160,22 @@ function ep_elasticpress_enabled( $query ) { return EP_API::factory()->elasticpress_enabled( $query ); } -function ep_is_activated() { - return EP_API::factory()->is_activated(); -} - function ep_activate() { return EP_API::factory()->activate(); } function ep_deactivate() { return EP_API::factory()->deactivate(); +} + +function ep_is_activated() { + return EP_API::factory()->is_activated(); +} + +function ep_elasticsearch_alive() { + return EP_API::factory()->elasticsearch_alive(); +} + +function ep_index_exists() { + return EP_API::factory()->index_exists(); } \ No newline at end of file diff --git a/classes/class-ep-wp-query-integration.php b/classes/class-ep-wp-query-integration.php index 6110fb570e..494fc0a423 100644 --- a/classes/class-ep-wp-query-integration.php +++ b/classes/class-ep-wp-query-integration.php @@ -17,27 +17,41 @@ class EP_WP_Query_Integration { public function __construct() { } public function setup() { - if ( ( ! is_admin() || apply_filters( 'ep_admin_wp_query_integration', false ) ) && ep_is_alive() ) { - // Make sure we return nothing for MySQL posts query - add_filter( 'posts_request', array( $this, 'filter_posts_request' ), 10, 2 ); - add_action( 'pre_get_posts', array( $this, 'action_pre_get_posts' ), 5 ); + // Ensure we aren't on the admin (unless overridden) + if ( is_admin() && ! apply_filters( 'ep_admin_wp_query_integration', false ) ) { + return; + } + + // Ensure that we are currently allowing ElasticPress to override the normal WP_Query search + if ( ! ep_is_activated() ) { + return; + } + + // If we can't reach the Elasticsearch service, don't bother with the rest of this + if ( ! ep_index_exists() ) { + return; + } - // Nukes the FOUND_ROWS() database query - add_filter( 'found_posts_query', array( $this, 'filter_found_posts_query' ), 5, 2 ); + // Make sure we return nothing for MySQL posts query + add_filter( 'posts_request', array( $this, 'filter_posts_request' ), 10, 2 ); - // Search and filter in EP_Posts to WP_Query - add_filter( 'the_posts', array( $this, 'filter_the_posts' ), 10, 2 ); + add_action( 'pre_get_posts', array( $this, 'action_pre_get_posts' ), 5 ); - // Ensure we're in a loop before we allow blog switching - add_action( 'loop_start', array( $this, 'action_loop_start' ) ); + // Nukes the FOUND_ROWS() database query + add_filter( 'found_posts_query', array( $this, 'filter_found_posts_query' ), 5, 2 ); - // Properly restore blog if necessary - add_action( 'loop_end', array( $this, 'action_loop_end' ) ); + // Search and filter in EP_Posts to WP_Query + add_filter( 'the_posts', array( $this, 'filter_the_posts' ), 10, 2 ); - // Properly switch to blog if necessary - add_action( 'the_post', array( $this, 'action_the_post' ), 10, 1 ); - } + // Ensure we're in a loop before we allow blog switching + add_action( 'loop_start', array( $this, 'action_loop_start' ) ); + + // Properly restore blog if necessary + add_action( 'loop_end', array( $this, 'action_loop_end' ) ); + + // Properly switch to blog if necessary + add_action( 'the_post', array( $this, 'action_the_post' ), 10, 1 ); } public function action_pre_get_posts( $query ) { diff --git a/elasticpress.php b/elasticpress.php index 5463fb5de2..9ce818fc4c 100644 --- a/elasticpress.php +++ b/elasticpress.php @@ -3,10 +3,16 @@ /** * Plugin Name: ElasticPress * Description: Integrate WordPress search with Elasticsearch - * Version: 1.0 + * Version: 1.1 * Author: Aaron Holbrook, Taylor Lovett, Matt Gross, 10up * Author URI: http://10up.com * License: MIT + * + * This program derives work from Alley Interactive's SearchPress + * and Automattic's VIP search plugin: + * + * Copyright (C) 2012-2013 Automattic + * Copyright (C) 2013 SearchPress */ require_once( 'classes/class-ep-config.php' ); diff --git a/readme.txt b/readme.txt index fba2ffc209..a59252d424 100644 --- a/readme.txt +++ b/readme.txt @@ -4,8 +4,8 @@ Author URI: http://10up.com 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.0 -Stable tag: 1.0 +Tested up to: 4.1 +Stable tag: 1.1 License: MIT License URI: http://opensource.org/licenses/MIT diff --git a/tests/test-single-site.php b/tests/test-single-site.php index 7a9ad289b2..ce35e060bf 100644 --- a/tests/test-single-site.php +++ b/tests/test-single-site.php @@ -81,7 +81,7 @@ public function filter_post_sync_args( $post_args ) { * @since 0.9 */ public function testPostSync() { - add_action( 'ep_sync_on_transition', array( $this, 'action_sync_on_transition'), 10, 0 ); + add_action( 'ep_sync_on_transition', array( $this, 'action_sync_on_transition' ), 10, 0 ); $post_id = ep_create_and_sync_post(); @@ -99,7 +99,7 @@ public function testPostSync() { * @since 0.9.3 */ public function testPostUnpublish() { - add_action( 'ep_delete_post', array( $this, 'action_delete_post'), 10, 0 ); + add_action( 'ep_delete_post', array( $this, 'action_delete_post' ), 10, 0 ); $post_id = ep_create_and_sync_post(); @@ -278,7 +278,7 @@ public function testPagination() { $found_posts = array(); $args = array( - 's' => 'findme', + 's' => 'findme', 'posts_per_page' => 1, ); @@ -290,9 +290,9 @@ public function testPagination() { $found_posts[] = $query->posts[0]->ID; $args = array( - 's' => 'findme', + 's' => 'findme', 'posts_per_page' => 1, - 'paged' => 2, + 'paged' => 2, ); $query = new WP_Query( $args ); @@ -303,9 +303,9 @@ public function testPagination() { $found_posts[] = $query->posts[0]->ID; $args = array( - 's' => 'findme', + 's' => 'findme', 'posts_per_page' => 1, - 'paged' => 3, + 'paged' => 3, ); $query = new WP_Query( $args ); @@ -316,9 +316,9 @@ public function testPagination() { $found_posts[] = $query->posts[0]->ID; $args = array( - 's' => 'findme', + 's' => 'findme', 'posts_per_page' => 1, - 'paged' => 4, + 'paged' => 4, ); $query = new WP_Query( $args ); @@ -342,12 +342,12 @@ public function testTaxQuery() { ep_refresh_index(); $args = array( - 's' => 'findme', + 's' => 'findme', 'tax_query' => array( array( 'taxonomy' => 'post_tag', - 'terms' => array( 'one' ), - 'field' => 'slug', + 'terms' => array( 'one' ), + 'field' => 'slug', ) ) ); @@ -373,7 +373,7 @@ public function testAuthorIDQuery() { ep_refresh_index(); $args = array( - 's' => 'findme', + 's' => 'findme', 'author' => $user_id, ); @@ -398,7 +398,7 @@ public function testAuthorNameQuery() { ep_refresh_index(); $args = array( - 's' => 'findme', + 's' => 'findme', 'author_name' => 'john', ); @@ -421,7 +421,7 @@ public function testPostTypeQuery() { ep_refresh_index(); $args = array( - 's' => 'findme', + 's' => 'findme', 'post_type' => 'page', ); @@ -443,7 +443,7 @@ public function testSearchMetaQuery() { ep_refresh_index(); $args = array( - 's' => 'findme', + 's' => 'findme', 'search_fields' => array( 'post_title', 'post_excerpt', @@ -470,7 +470,7 @@ public function testSearchTaxQuery() { ep_refresh_index(); $args = array( - 's' => 'one findme two', + 's' => 'one findme two', 'search_fields' => array( 'post_title', 'post_excerpt', @@ -500,7 +500,7 @@ public function testSearchAuthorQuery() { ep_refresh_index(); $args = array( - 's' => 'john boy', + 's' => 'john boy', 'search_fields' => array( 'post_title', 'post_excerpt', @@ -527,32 +527,32 @@ public function testAdvancedQuery() { ep_create_and_sync_post( array( 'post_content' => 'findme', 'post_type' => 'ep_test' ) ); ep_create_and_sync_post( array( 'post_content' => 'findme', - 'post_type' => 'ep_test', - 'tags_input' => array( 'superterm' ) + 'post_type' => 'ep_test', + 'tags_input' => array( 'superterm' ) ) ); ep_create_and_sync_post( array( 'post_content' => 'findme', - 'post_type' => 'ep_test', - 'tags_input' => array( 'superterm' ), - 'post_author' => $user_id, + 'post_type' => 'ep_test', + 'tags_input' => array( 'superterm' ), + 'post_author' => $user_id, ) ); ep_create_and_sync_post( array( 'post_content' => 'findme', - 'post_type' => 'ep_test', - 'tags_input' => array( 'superterm' ), - 'post_author' => $user_id, + 'post_type' => 'ep_test', + 'tags_input' => array( 'superterm' ), + 'post_author' => $user_id, ), array( 'test_key' => 'meta value' ) ); ep_refresh_index(); $args = array( - 's' => 'meta value', - 'post_type' => 'ep_test', - 'tax_query' => array( + 's' => 'meta value', + 'post_type' => 'ep_test', + 'tax_query' => array( array( 'taxonomy' => 'post_tag', - 'terms' => array( 'superterm' ), - 'field' => 'slug', + 'terms' => array( 'superterm' ), + 'field' => 'slug', ) ), 'search_fields' => array( @@ -561,7 +561,7 @@ public function testAdvancedQuery() { 'post_content', 'meta' => 'test_key' ), - 'author' => $user_id, + 'author' => $user_id, ); $query = new WP_Query( $args ); @@ -569,4 +569,165 @@ public function testAdvancedQuery() { $this->assertEquals( 1, $query->post_count ); $this->assertEquals( 1, $query->found_posts ); } + + /** + * Test post_title orderby query + * + * @since 1.1 + */ + public function testSearchPostTitleOrderbyQuery() { + ep_create_and_sync_post( array( 'post_title' => 'ordertest 333' ) ); + ep_create_and_sync_post( array( 'post_title' => 'ordertest 111' ) ); + ep_create_and_sync_post( array( 'post_title' => 'ordertest 222' ) ); + + ep_refresh_index(); + + $args = array( + 's' => 'ordertest', + 'orderby' => 'title', + 'order' => 'DESC', + ); + + $query = new WP_Query( $args ); + + $this->assertEquals( 3, $query->post_count ); + $this->assertEquals( 3, $query->found_posts ); + $this->assertEquals( 'ordertest 333', $query->posts[0]->post_title ); + $this->assertEquals( 'ordertest 222', $query->posts[1]->post_title ); + $this->assertEquals( 'ordertest 111', $query->posts[2]->post_title ); + } + + /** + * Test relevance orderby query + * + * @since 1.1 + */ + public function testSearchRelevanceOrderbyQuery() { + ep_create_and_sync_post(); + ep_create_and_sync_post( array( 'post_title' => 'ordertet' ) ); + ep_create_and_sync_post( array( 'post_title' => 'ordertest' ) ); + + ep_refresh_index(); + + $args = array( + 's' => 'ordertest', + 'orderby' => 'relevance', + ); + + $query = new WP_Query( $args ); + + $this->assertEquals( 2, $query->post_count ); + $this->assertEquals( 2, $query->found_posts ); + $this->assertEquals( 'ordertest', $query->posts[0]->post_title ); + $this->assertEquals( 'ordertet', $query->posts[1]->post_title ); + } + + /** + * Test post_name orderby query + * + * @since 1.1 + */ + public function testSearchPostNameOrderbyQuery() { + ep_create_and_sync_post( array( 'post_title' => 'postname-ordertest-333' ) ); + ep_create_and_sync_post( array( 'post_title' => 'postname-ordertest-111' ) ); + ep_create_and_sync_post( array( 'post_title' => 'postname-ordertest-222' ) ); + + + ep_refresh_index(); + + $args = array( + 's' => 'postname ordertest', + 'orderby' => 'name', + 'order' => 'ASC', + ); + + $query = new WP_Query( $args ); + + $this->assertEquals( 3, $query->post_count ); + $this->assertEquals( 3, $query->found_posts ); + $this->assertEquals( 'postname-ordertest-111', $query->posts[0]->post_name ); + $this->assertEquals( 'postname-ordertest-222', $query->posts[1]->post_name ); + $this->assertEquals( 'postname-ordertest-333', $query->posts[2]->post_name ); + } + + /** + * Test default sort and order parameters + * + * Default is to use _score and 'desc' + * + * @since 1.1 + */ + public function testSearchDefaultOrderbyQuery() { + ep_create_and_sync_post(); + ep_create_and_sync_post( array( 'post_title' => 'ordertet' ) ); + ep_create_and_sync_post( array( 'post_title' => 'ordertest' ) ); + + ep_refresh_index(); + + $args = array( + 's' => 'ordertest', + ); + + $query = new WP_Query( $args ); + + $this->assertEquals( 2, $query->post_count ); + $this->assertEquals( 2, $query->found_posts ); + $this->assertEquals( 'ordertest', $query->posts[0]->post_title ); + $this->assertEquals( 'ordertet', $query->posts[1]->post_title ); + } + + /** + * Test default sort and ASC order parameters + * + * Default is to use _score orderby; using 'asc' order + * + * @since 1.1 + */ + public function testSearchDefaultOrderbyASCOrderQuery() { + ep_create_and_sync_post(); + ep_create_and_sync_post( array( 'post_title' => 'ordertest' ) ); + ep_create_and_sync_post( array( 'post_title' => 'ordertestt' ) ); + + ep_refresh_index(); + + $args = array( + 's' => 'ordertest', + 'order' => 'ASC', + ); + + $query = new WP_Query( $args ); + + $this->assertEquals( 2, $query->post_count ); + $this->assertEquals( 2, $query->found_posts ); + $this->assertEquals( 'ordertestt', $query->posts[0]->post_title ); + $this->assertEquals( 'ordertest', $query->posts[1]->post_title ); + } + + /** + * Test unallowed orderby parameter + * + * Will revert to default _score orderby + * + * @since 1.1 + */ + public function testSearchUnallowedOrderbyQuery() { + ep_create_and_sync_post(); + ep_create_and_sync_post( array( 'post_title' => 'ordertestt' ) ); + ep_create_and_sync_post( array( 'post_title' => 'ordertest' ) ); + + ep_refresh_index(); + + $args = array( + 's' => 'ordertest', + 'orderby' => 'SUPERRELEVANCE', + 'order' => 'ASC', + ); + + $query = new WP_Query( $args ); + + $this->assertEquals( 2, $query->post_count ); + $this->assertEquals( 2, $query->found_posts ); + $this->assertEquals( 'ordertestt', $query->posts[0]->post_title ); + $this->assertEquals( 'ordertest', $query->posts[1]->post_title ); + } } \ No newline at end of file