diff --git a/.travis.yml b/.travis.yml index b84ca7c..fefe759 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,10 +2,6 @@ language: php matrix: include: - - php: 5.3 - env: WP_VERSION=4.3.3 ES_VERSION=2.2.0 - - php: 5.3 - env: WP_VERSION=latest ES_VERSION=2.2.0 - php: 5.6 env: WP_VERSION=latest ES_VERSION=2.2.0 - php: 7.0 @@ -27,6 +23,13 @@ before_install: - sudo service elasticsearch start before_script: + - export PATH="$HOME/.composer/vendor/bin:$PATH" + - | + if [[ ${TRAVIS_PHP_VERSION:0:2} == "7." ]]; then + composer global require "phpunit/phpunit=5.7.*" + else + composer global require "phpunit/phpunit=4.8.*" + fi - bash bin/install-wp-tests.sh wordpress_test root '' localhost $WP_VERSION - sleep 5 # ensure ES is running diff --git a/adapters/searchpress.php b/adapters/searchpress.php index 0f2b5fb..3633d89 100644 --- a/adapters/searchpress.php +++ b/adapters/searchpress.php @@ -11,6 +11,7 @@ protected function query_es( $es_args ) { function sp_es_field_map( $es_map ) { return wp_parse_args( array( + 'ID' => 'post_id', 'post_name' => 'post_name.raw', 'post_title' => 'post_title.raw', 'post_title.analyzed' => 'post_title', @@ -74,4 +75,4 @@ function es_wp_query_index_test_data() { SP_API()->post( '_refresh' ); } -} \ No newline at end of file +} diff --git a/adapters/travis.php b/adapters/travis.php index 7c66213..07dc6d9 100644 --- a/adapters/travis.php +++ b/adapters/travis.php @@ -13,6 +13,7 @@ protected function query_es( $es_args ) { function travis_es_field_map( $es_map ) { return wp_parse_args( array( + 'ID' => 'post_id', 'post_meta' => 'post_meta.%s.value', 'post_author' => 'post_author.user_id', 'post_date' => 'post_date.date', diff --git a/adapters/wpcom-vip.php b/adapters/wpcom-vip.php index f012bb3..e88ff28 100644 --- a/adapters/wpcom-vip.php +++ b/adapters/wpcom-vip.php @@ -20,6 +20,8 @@ protected function set_posts( $q, $es_response ) { $post_id = (array) $hit['fields'][ $this->es_map( 'post_id' ) ]; $this->posts[] = reset( $post_id ); } + + $this->posts = $this->post_query_sort_handler( $this->posts, $q ); return; case 'id=>parent' : @@ -33,7 +35,6 @@ protected function set_posts( $q, $es_response ) { default : if ( apply_filters( 'es_query_use_source', false ) ) { $this->posts = wp_list_pluck( $es_response['results']['hits'], '_source' ); - return; } else { $post_ids = array(); foreach ( $es_response['results']['hits'] as $hit ) { @@ -46,8 +47,10 @@ protected function set_posts( $q, $es_response ) { $post__in = implode( ',', $post_ids ); $this->posts = $wpdb->get_results( "SELECT $wpdb->posts.* FROM $wpdb->posts WHERE ID IN ($post__in) ORDER BY FIELD( {$wpdb->posts}.ID, $post__in )" ); } - return; } + + $this->posts = $this->post_query_sort_handler( $this->posts, $q ); + return; } } else { $this->posts = array(); @@ -73,6 +76,7 @@ public function set_found_posts( $q, $es_response ) { function vip_es_field_map( $es_map ) { return wp_parse_args( array( + 'ID' => 'post_id', 'post_author' => 'author_id', 'post_author.user_nicename' => 'author_login', 'post_date' => 'date', @@ -100,8 +104,8 @@ function vip_es_field_map( $es_map ) { 'post_title' => 'title', 'post_title.analyzed' => 'title', 'post_excerpt' => 'excerpt', - 'post_password' => 'post_password', // this isn't indexed on vip - 'post_name' => 'post_name', // this isn't indexed on vip + 'post_password' => 'post_password', // this isn't indexed on vip + 'post_name' => 'slug', 'post_modified' => 'modified', 'post_modified.year' => 'modified_token.year', 'post_modified.month' => 'modified_token.month', @@ -123,9 +127,9 @@ function vip_es_field_map( $es_map ) { 'post_modified_gmt.minute' => 'modified_gmt_token.minute', 'post_modified_gmt.second' => 'modified_gmt_token.second', 'post_parent' => 'parent_post_id', - 'menu_order' => 'menu_order', // this isn't indexed on vip + 'menu_order' => 'menu_order', 'post_mime_type' => 'post_mime_type', // this isn't indexed on vip - 'comment_count' => 'comment_count', // this isn't indexed on vip + 'comment_count' => 'discussion.comment_count', 'post_meta' => 'meta.%s.value.raw_lc', 'post_meta.analyzed' => 'meta.%s.value', 'post_meta.long' => 'meta.%s.long', diff --git a/class-es-wp-query-shoehorn.php b/class-es-wp-query-shoehorn.php index 3d7c938..6818570 100644 --- a/class-es-wp-query-shoehorn.php +++ b/class-es-wp-query-shoehorn.php @@ -219,4 +219,4 @@ private function reboot_query_vars( &$query ) { $q['posts_per_page'] = get_option( 'posts_per_page' ); } } -} \ No newline at end of file +} diff --git a/class-es-wp-query-wrapper.php b/class-es-wp-query-wrapper.php index 5c64f0a..a35a417 100644 --- a/class-es-wp-query-wrapper.php +++ b/class-es-wp-query-wrapper.php @@ -47,6 +47,8 @@ protected function set_posts( $q, $es_response ) { $post_id = (array) $hit['fields'][ $this->es_map( 'post_id' ) ]; $this->posts[] = reset( $post_id ); } + + $this->posts = $this->post_query_sort_handler( $this->posts, $q ); return; case 'id=>parent' : @@ -60,7 +62,6 @@ protected function set_posts( $q, $es_response ) { default : if ( apply_filters( 'es_query_use_source', false ) ) { $this->posts = wp_list_pluck( $es_response['hits']['hits'], '_source' ); - return; } else { $post_ids = array(); foreach ( $es_response['hits']['hits'] as $hit ) { @@ -73,14 +74,88 @@ protected function set_posts( $q, $es_response ) { $post__in = implode( ',', $post_ids ); $this->posts = $wpdb->get_results( "SELECT $wpdb->posts.* FROM $wpdb->posts WHERE ID IN ($post__in) ORDER BY FIELD( {$wpdb->posts}.ID, $post__in )" ); } - return; } + + $this->posts = $this->post_query_sort_handler( $this->posts, $q ); } } else { $this->posts = array(); } } + /** + * Post query sort handler + * Handle sorting by `post__in`, `post__in` and `post_parent__in`. + * + * @param array $posts Query result posts. + * @param array $query Initial query. + * @return array Sorted posts. + */ + protected function post_query_sort_handler( $posts, $query ) { + if ( empty( $query['orderby'] ) ) { + return $posts; + } + + // Determine the key to sort by. + switch ( $query['orderby'] ) { + case 'post__in' : + $key = 'ID'; + break; + + case 'post_name__in' : + $key = 'post_name'; + break; + + case 'post_parent__in' : + $key = 'post_parent'; + break; + + default : + return $posts; + } + + // Flip the order to allow retrieval by index. + $order = array_flip( $query[ $query['orderby'] ] ); + + // Support raw Elasticsearch documents. + $use_source = apply_filters( 'es_query_use_source', false ); + if ( $use_source ) { + $key = $this->es_map( $key ); + } + + usort( $posts, function( $a, $b ) use ( $order, $key, $use_source ) { + // Add support for using the Elasticsearch _source field. + if ( $use_source ) { + // Cast the array to object to mock the `WP_Post` object. + $a = (object) $a; + $b = (object) $b; + } else { + // Add support for a query of only post ID fields. + if ( ! ( $a instanceof WP_Post ) ) { + $a = get_post( $a ); + } + + if ( ! ( $b instanceof WP_Post ) ) { + $b = get_post( $b ); + } + } + + if ( + ! isset( $a->$key ) || ! isset( $b->$key ) + || ! isset( $order[ $a->$key ] ) || ! isset( $order[ $b->$key ] ) ) { + return 0; + } + + if ( $order[ $a->$key ] === $order[ $b->$key ] ) { + return 0; + } else { + return $order[ $a->$key ] < $order[ $b->$key ] ? -1 : 1; + } + } ); + + return $posts; + } + // @todo: Core queries where 1=0 here, which probably happens for good reason. // We're just going to abandon ship for now, but if it causes issues we'll switch // to a mysql query where 1=0 @@ -462,6 +537,9 @@ public function get_posts() { $q['attachment'] = sanitize_title_for_query( wp_basename( $q['attachment'] ) ); $q['name'] = $q['attachment']; $filter[] = $this->dsl_terms( $this->es_map( 'post_name' ), $q['attachment'] ); + } elseif ( ! empty( $q['post_name__in'] ) ) { + $post_name__in = $q['post_name__in']; + $filter[] = $this->dsl_terms( $this->es_map( 'post_name' ), $post_name__in ); } @@ -682,12 +760,8 @@ public function get_posts() { } } elseif ( 'none' == $q['orderby'] ) { // nothing to see here - } elseif ( $q['orderby'] == 'post__in' && ! empty( $post__in ) ) { - // @todo: Figure this out... Elasticsearch doesn't have an equivalent of this - // $orderby = "FIELD( {$wpdb->posts}.ID, $post__in )"; - } elseif ( $q['orderby'] == 'post_parent__in' && ! empty( $post_parent__in ) ) { - // (see above) - // $orderby = "FIELD( {$wpdb->posts}.post_parent, $post_parent__in )"; + } elseif ( in_array( $q['orderby'], array( 'post__in', 'post_parent__in', 'post_name__in' ) ) ) { + // Handled post-query by `ES_WP_Query_Wrapper::post_query_sort_handler()`. } else { if ( is_array( $q['orderby'] ) ) { foreach ( $q['orderby'] as $_orderby => $order ) { diff --git a/tests/query/query.php b/tests/query/query.php index d51f4bd..0164808 100644 --- a/tests/query/query.php +++ b/tests/query/query.php @@ -67,4 +67,147 @@ function test_orderby() { $q5 = new ES_WP_Query( array( 'orderby' => array() ) ); $this->assertFalse( isset( $q5->es_args['sort'] ) ); } + + function test_orderby_post__in() { + $p_a = $this->factory->post->create(); + $p_b = $this->factory->post->create(); + $p_c = $this->factory->post->create(); + $p_d = $this->factory->post->create(); + es_wp_query_index_test_data(); + + $post__in = [ + $p_c, + $p_a, + $p_d, + $p_b, + ]; + + $q = new ES_WP_Query( [ + 'post__in' => $post__in, + 'orderby' => 'post__in', + 'order' => 'ASC', + 'posts_per_page' => 4, + ] ); + + $this->assertNotEmpty( $q->posts ); + + // Verify that the post is in the proper array. + foreach ( $q->posts as $post ) { + $this->assertTrue( in_array( $post->ID, $post__in, true ) ); + } + + // Assert that the order matches + foreach ( $post__in as $i => $post_ID ) { + $this->assertEquals( + $post_ID, + $q->posts[ $i ]->ID, + 'Post not in expected order from `post__in`.' + ); + } + + // Query only the ID field. + $q2 = new ES_WP_Query( [ + 'post__in' => $post__in, + 'orderby' => 'post__in', + 'order' => 'ASC', + 'posts_per_page' => 4, + 'fields' => 'ids', + ] ); + + $this->assertNotEmpty( $q2->posts ); + + // Verify that the post is in the proper array. + foreach ( $q2->posts as $post ) { + $this->assertTrue( in_array( $post, $post__in, true ) ); + } + + // Assert that the order matches + foreach ( $post__in as $i => $post_ID ) { + $this->assertEquals( + $post_ID, + $q2->posts[ $i ], + 'Post not in expected order from `post__in`.' + ); + } + } + + function test_post_name__in() { + $post_a = $this->factory->post->create( [ 'post_name' => 'post-a' ] ); + $post_b = $this->factory->post->create( [ 'post_name' => 'post-b' ] ); + $post_c = $this->factory->post->create( [ 'post_name' => 'post-c' ] ); + es_wp_query_index_test_data(); + + $post_name__in = [ + 'post-c', + 'post-a', + 'post-b', + ]; + + $q = new ES_WP_Query( [ + 'post_name__in' => $post_name__in, + 'orderby' => 'post_name__in', + 'order' => 'ASC', + ] ); + + $this->assertNotEmpty( $q->posts ); + + // Verify that the post name is in the proper array. + foreach ( $q->posts as $post ) { + $this->assertTrue( in_array( $post->post_name, $post_name__in, true ) ); + } + + // Assert that the order matches. + foreach ( $post_name__in as $i => $post_name ) { + $this->assertEquals( + $post_name, + $q->posts[ $i ]->post_name, + 'Post not in expected order from `post_name__in`.' + ); + } + } + + public function test_post_parent__in() { + $parent_a = $this->factory->post->create(); + $parent_b = $this->factory->post->create(); + $parent_c = $this->factory->post->create(); + $children = []; + + foreach( [ $parent_a, $parent_b, $parent_c ] as $parent ) { + $children[ $parent ] = $this->factory->post->create( [ 'post_parent' => $parent ] ); + } + + es_wp_query_index_test_data(); + + $post_parent__in = [ + $parent_b, + $parent_a, + $parent_c, + ]; + + $q = new ES_WP_Query( [ + 'post_parent__in' => $post_parent__in, + 'orderby' => 'post_parent__in', + 'order' => 'ASC', + ] ); + + $this->assertNotEmpty( $q->posts ); + + // Verify that the post is in the proper children array. + foreach ( $q->posts as $post ) { + $this->assertEquals( + $post->ID, + $children[ $post->post_parent ], + 'Unexpected post returned from `post_parent__in`.' + ); + } + + // Verify the order of `post_parent__in`. + foreach ( $post_parent__in as $i => $post_parent_id ) { + $this->assertEquals( + $post_parent_id, + $q->posts[ $i ]->post_parent, + 'Post not in expected order from `post_parent__in`.' + ); + } + } }