diff --git a/.travis.yml b/.travis.yml index b84ca7c..139393c 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,36 +1,58 @@ +sudo: false + 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 - env: WP_VERSION=latest ES_VERSION=2.2.0 - - php: 5.6 - env: WP_VERSION=nightly ES_VERSION=2.2.0 - - php: 5.6 - env: WP_VERSION=latest ES_VERSION=2.1.2 - - php: 5.6 - env: WP_VERSION=latest ES_VERSION=2.0.2 - fast_finish: true +jdk: + - oraclejdk8 + +addons: + apt: + packages: + - oracle-java8-installer + +notifications: + email: + on_success: never + on_failure: change branches: only: - master + - master-1.x -before_install: - - curl -O -L https://download.elastic.co/elasticsearch/elasticsearch/elasticsearch-$ES_VERSION.deb && sudo dpkg -i --force-confnew elasticsearch-$ES_VERSION.deb - - sudo service elasticsearch start +env: + global: + - JAVA_HOME=/usr/lib/jvm/java-8-oracle + +matrix: + include: + - php: 5.4 + env: WP_VERSION=4.4.2 ES_VERSION=2.4.6 + - php: 7.0 + env: WP_VERSION=nightly ES_VERSION=5.6.8 + - php: 7.1 + env: WP_VERSION=latest ES_VERSION=2.4.6 + - php: 7.1 + env: WP_VERSION=latest ES_VERSION=6.2.2 + - php: 7.2 + env: WP_VERSION=nightly ES_VERSION=5.6.8 + fast_finish: true + +install: + - bash bin/install-es.sh $ES_VERSION before_script: - - bash bin/install-wp-tests.sh wordpress_test root '' localhost $WP_VERSION - - sleep 5 # ensure ES is running + - export PATH="$HOME/.composer/vendor/bin:$PATH" + - composer global require "phpunit/phpunit=4.8.*|5.7.*" + - which phpunit + - phpunit --version + - bash bin/install-wp-tests.sh wordpress_test root '' localhost $WP_VERSION + - curl localhost:9200 && echo "ES is up" || (cat /tmp/elasticsearch.log && exit 1) script: - find . -path ./vendor -prune -o -type "f" -iname "*.php" | xargs -L "1" php -l - phpunit - - if [[ "$ES_VERSION" == "2.2.0" ]]; then phpunit -c multisite.xml; fi; + - phpunit -c multisite.xml + +after_script: + - cat /tmp/elasticsearch.log diff --git a/adapters/travis.php b/adapters/travis.php index 7c66213..30358f5 100644 --- a/adapters/travis.php +++ b/adapters/travis.php @@ -6,11 +6,19 @@ class ES_WP_Query extends ES_WP_Query_Wrapper { protected function query_es( $es_args ) { - $response = wp_remote_post( 'http://localhost:9200/es-wp-query-unit-tests/post/_search', array( 'body' => json_encode( $es_args ) ) ); + $response = wp_remote_post( 'http://localhost:9200/es-wp-query-unit-tests/post/_search', array( + 'body' => json_encode( $es_args ), + 'headers' => array( + 'Content-Type' => 'application/json', + ), + ) ); return json_decode( wp_remote_retrieve_body( $response ), true ); } } +class ES_Index_Exception extends \Exception { +} + function travis_es_field_map( $es_map ) { return wp_parse_args( array( 'post_meta' => 'post_meta.%s.value', @@ -33,6 +41,11 @@ function es_wp_query_verify_es_is_running( $tries = 5, $sleep = 3 ) { $body = json_decode( wp_remote_retrieve_body( $response ), true ); if ( ! empty( $body['version']['number'] ) ) { printf( "Elasticsearch is up and running, using version %s.\n", $body['version']['number'] ); + if ( ! defined( 'ES_VERSION' ) ) { + define( 'ES_VERSION', $body['version']['number'] ); + } elseif ( ES_VERSION !== $body['version']['number'] ) { + printf( "WARNING! ES_VERSION is set to %s, but Elasticsearch is reporting %s\n", ES_VERSION, $body['version']['number'] ); + } break; } else { sleep( $sleep ); @@ -43,214 +56,224 @@ function es_wp_query_verify_es_is_running( $tries = 5, $sleep = 3 ) { } } while ( --$tries ); - // If we didn't end with a 200 status code, exit - travis_es_verify_response_code( $response ); + // If we didn't end with a 200 status code, bail. + return travis_es_verify_response_code( $response ); } function es_wp_query_index_test_data() { // Ensure the index is empty wp_remote_request( 'http://localhost:9200/es-wp-query-unit-tests/', array( 'method' => 'DELETE' ) ); + $analyzed = 'text'; + $not_analyzed = 'keyword'; + if ( version_compare( ES_VERSION, '5.0.0', '<' ) ) { + $analyzed = 'string'; + $not_analyzed = 'string", "index": "not_analyzed'; + } + // Add the mapping - $response = wp_remote_request( 'http://localhost:9200/es-wp-query-unit-tests/', array( 'method' => 'PUT', 'body' => ' - { - "settings": { - "analysis": { - "analyzer": { - "default": { - "tokenizer": "standard", - "filter": [ - "standard", - "travis_word_delimiter", - "lowercase", - "stop", - "travis_snowball" - ], - "language": "English" - } - }, - "filter": { - "travis_word_delimiter": { - "type": "word_delimiter", - "preserve_original": true + $response = wp_remote_request( 'http://localhost:9200/es-wp-query-unit-tests/', array( + 'method' => 'PUT', + 'body' => sprintf( ' + { + "settings": { + "analysis": { + "analyzer": { + "default": { + "tokenizer": "standard", + "filter": [ + "standard", + "travis_word_delimiter", + "lowercase", + "stop", + "travis_snowball" + ], + "language": "English" + } }, - "travis_snowball": { - "type": "snowball", - "language": "English" + "filter": { + "travis_word_delimiter": { + "type": "word_delimiter", + "preserve_original": true + }, + "travis_snowball": { + "type": "snowball", + "language": "English" + } } } - } - }, - "mappings": { - "post": { - "_all" : { "enabled" : false }, - "date_detection": false, - "dynamic_templates": [ - { - "template_meta": { - "path_match": "post_meta.*", - "mapping": { - "type": "object", - "properties": { - "value": { - "type": "string", - "index": "not_analyzed" - }, - "analyzed": { - "type": "string" - }, - "boolean": { - "type": "boolean" - }, - "long": { - "type": "long" - }, - "double": { - "type": "double" - }, - "date": { - "format": "YYYY-MM-dd", - "type": "date" - }, - "datetime": { - "format": "YYYY-MM-dd HH:mm:ss", - "type": "date" - }, - "time": { - "format": "HH:mm:ss", - "type": "date" + }, + "mappings": { + "post": { + "_all" : { "enabled" : false }, + "date_detection": false, + "dynamic_templates": [ + { + "template_meta": { + "path_match": "post_meta.*", + "mapping": { + "type": "object", + "properties": { + "value": { + "type": "%2$s" + }, + "analyzed": { + "type": "%1$s" + }, + "boolean": { + "type": "boolean" + }, + "long": { + "type": "long" + }, + "double": { + "type": "double" + }, + "date": { + "format": "YYYY-MM-dd", + "type": "date" + }, + "datetime": { + "format": "YYYY-MM-dd HH:mm:ss", + "type": "date" + }, + "time": { + "format": "HH:mm:ss", + "type": "date" + } } } } - } - }, - { - "template_terms": { - "path_match": "terms.*", - "mapping": { - "type": "object", - "properties": { - "name": { "type": "string", "index": "not_analyzed" }, - "term_id": { "type": "long" }, - "term_taxonomy_id": { "type": "long" }, - "slug": { "type": "string", "index": "not_analyzed" } + }, + { + "template_terms": { + "path_match": "terms.*", + "mapping": { + "type": "object", + "properties": { + "name": { "type": "%2$s" }, + "term_id": { "type": "long" }, + "term_taxonomy_id": { "type": "long" }, + "slug": { "type": "%2$s" } + } } } } + ], + "properties": { + "post_id": { "type": "long" }, + "post_author": { + "type": "object", + "properties": { + "user_id": { "type": "long" }, + "user_nicename": { "type": "%2$s" } + } + }, + "post_title": { + "type": "%2$s", + "fields": { + "analyzed": { "type": "%1$s" } + } + }, + "post_excerpt": { "type": "%1$s" }, + "post_content": { + "type": "%2$s", + "fields": { + "analyzed": { "type": "%1$s" } + } + }, + "post_status": { "type": "%2$s" }, + "post_name": { "type": "%2$s" }, + "post_parent": { "type": "long" }, + "post_type": { "type": "%2$s" }, + "post_mime_type": { "type": "%2$s" }, + "post_password": { "type": "%2$s" }, + "post_date": { + "type": "object", + "properties": { + "date": { "type": "date", "format": "YYYY-MM-dd HH:mm:ss" }, + "year": { "type": "short" }, + "month": { "type": "byte" }, + "day": { "type": "byte" }, + "hour": { "type": "byte" }, + "minute": { "type": "byte" }, + "second": { "type": "byte" }, + "week": { "type": "byte" }, + "day_of_week": { "type": "byte" }, + "day_of_year": { "type": "short" }, + "seconds_from_day": { "type": "integer" }, + "seconds_from_hour": { "type": "short" } + } + }, + "post_date_gmt": { + "type": "object", + "properties": { + "date": { "type": "date", "format": "YYYY-MM-dd HH:mm:ss" }, + "year": { "type": "short" }, + "month": { "type": "byte" }, + "day": { "type": "byte" }, + "hour": { "type": "byte" }, + "minute": { "type": "byte" }, + "second": { "type": "byte" }, + "week": { "type": "byte" }, + "day_of_week": { "type": "byte" }, + "day_of_year": { "type": "short" }, + "seconds_from_day": { "type": "integer" }, + "seconds_from_hour": { "type": "short" } + } + }, + "post_modified": { + "type": "object", + "properties": { + "date": { "type": "date", "format": "YYYY-MM-dd HH:mm:ss" }, + "year": { "type": "short" }, + "month": { "type": "byte" }, + "day": { "type": "byte" }, + "hour": { "type": "byte" }, + "minute": { "type": "byte" }, + "second": { "type": "byte" }, + "week": { "type": "byte" }, + "day_of_week": { "type": "byte" }, + "day_of_year": { "type": "short" }, + "seconds_from_day": { "type": "integer" }, + "seconds_from_hour": { "type": "short" } + } + }, + "post_modified_gmt": { + "type": "object", + "properties": { + "date": { "type": "date", "format": "YYYY-MM-dd HH:mm:ss" }, + "year": { "type": "short" }, + "month": { "type": "byte" }, + "day": { "type": "byte" }, + "hour": { "type": "byte" }, + "minute": { "type": "byte" }, + "second": { "type": "byte" }, + "week": { "type": "byte" }, + "day_of_week": { "type": "byte" }, + "day_of_year": { "type": "short" }, + "seconds_from_day": { "type": "integer" }, + "seconds_from_hour": { "type": "short" } + } + }, + "menu_order" : { "type" : "integer" }, + "terms": { "type": "object" }, + "post_meta": { "type": "object" } } - ], - "properties": { - "post_id": { "type": "long" }, - "post_author": { - "type": "object", - "properties": { - "user_id": { "type": "long" }, - "user_nicename": { "type": "string", "index": "not_analyzed" } - } - }, - "post_title": { - "type": "string", - "index": "not_analyzed", - "fields": { - "analyzed": { "type": "string" } - } - }, - "post_excerpt": { "type": "string" }, - "post_content": { - "type": "string", - "index": "not_analyzed", - "fields": { - "analyzed": { "type": "string" } - } - }, - "post_status": { "type": "string", "index": "not_analyzed" }, - "post_name": { "type": "string", "index": "not_analyzed" }, - "post_parent": { "type": "long" }, - "post_type": { "type": "string", "index": "not_analyzed" }, - "post_mime_type": { "type": "string", "index": "not_analyzed" }, - "post_password": { "type": "string", "index": "not_analyzed" }, - "post_date": { - "type": "object", - "properties": { - "date": { "type": "date", "format": "YYYY-MM-dd HH:mm:ss" }, - "year": { "type": "short" }, - "month": { "type": "byte" }, - "day": { "type": "byte" }, - "hour": { "type": "byte" }, - "minute": { "type": "byte" }, - "second": { "type": "byte" }, - "week": { "type": "byte" }, - "day_of_week": { "type": "byte" }, - "day_of_year": { "type": "short" }, - "seconds_from_day": { "type": "integer" }, - "seconds_from_hour": { "type": "short" } - } - }, - "post_date_gmt": { - "type": "object", - "properties": { - "date": { "type": "date", "format": "YYYY-MM-dd HH:mm:ss" }, - "year": { "type": "short" }, - "month": { "type": "byte" }, - "day": { "type": "byte" }, - "hour": { "type": "byte" }, - "minute": { "type": "byte" }, - "second": { "type": "byte" }, - "week": { "type": "byte" }, - "day_of_week": { "type": "byte" }, - "day_of_year": { "type": "short" }, - "seconds_from_day": { "type": "integer" }, - "seconds_from_hour": { "type": "short" } - } - }, - "post_modified": { - "type": "object", - "properties": { - "date": { "type": "date", "format": "YYYY-MM-dd HH:mm:ss" }, - "year": { "type": "short" }, - "month": { "type": "byte" }, - "day": { "type": "byte" }, - "hour": { "type": "byte" }, - "minute": { "type": "byte" }, - "second": { "type": "byte" }, - "week": { "type": "byte" }, - "day_of_week": { "type": "byte" }, - "day_of_year": { "type": "short" }, - "seconds_from_day": { "type": "integer" }, - "seconds_from_hour": { "type": "short" } - } - }, - "post_modified_gmt": { - "type": "object", - "properties": { - "date": { "type": "date", "format": "YYYY-MM-dd HH:mm:ss" }, - "year": { "type": "short" }, - "month": { "type": "byte" }, - "day": { "type": "byte" }, - "hour": { "type": "byte" }, - "minute": { "type": "byte" }, - "second": { "type": "byte" }, - "week": { "type": "byte" }, - "day_of_week": { "type": "byte" }, - "day_of_year": { "type": "short" }, - "seconds_from_day": { "type": "integer" }, - "seconds_from_hour": { "type": "short" } - } - }, - "menu_order" : { "type" : "integer" }, - "terms": { "type": "object" }, - "post_meta": { "type": "object" } } } } - } - ' ) ); + ', $analyzed, $not_analyzed ), + 'headers' => array( + 'Content-Type' => 'application/json', + ), + ) ); travis_es_verify_response_code( $response ); // Index the content $posts = get_posts( array( 'posts_per_page' => -1, - 'post_type' => 'any', + 'post_type' => array_values( get_post_types() ), 'post_status' => array_values( get_post_stati() ), 'orderby' => 'ID', 'order' => 'ASC', @@ -271,7 +294,10 @@ function es_wp_query_index_test_data() { 'http://localhost:9200/es-wp-query-unit-tests/post/_bulk', array( 'method' => 'PUT', - 'body' => wp_check_invalid_utf8( implode( "\n", $body ), true ) . "\n" + 'body' => wp_check_invalid_utf8( implode( "\n", $body ), true ) . "\n", + 'headers' => array( + 'Content-Type' => 'application/json', + ), ) ); travis_es_verify_response_code( $response ); @@ -280,37 +306,45 @@ function es_wp_query_index_test_data() { foreach ( (array) $itemized_response->items as $post ) { // Status should be 200 or 201, depending on if we're updating or creating respectively if ( ! isset( $post->index->status ) || ! in_array( $post->index->status, array( 200, 201 ) ) ) { - echo "Error indexing post {$post->index->_id}; HTTP response code: {$post->index->status}"; + $error_message = "Error indexing post {$post->index->_id}; HTTP response code: {$post->index->status}"; if ( ! empty( $post->index->error ) ) { - echo "\n" . $post->index->error; + $error_message .= "\n" . $post->index->error; } - exit( 1 ); + $error_message .= 'Backtrace:' . travis_es_debug_backtrace_summary(); + throw new ES_Index_Exception( $error_message ); } } - $resposne = wp_remote_post( 'http://localhost:9200/es-wp-query-unit-tests/_refresh' ); + $resposne = wp_remote_post( 'http://localhost:9200/es-wp-query-unit-tests/_refresh', array( + 'headers' => array( + 'Content-Type' => 'application/json', + ), + ) ); travis_es_verify_response_code( $response ); } function travis_es_verify_response_code( $response ) { if ( '200' != wp_remote_retrieve_response_code( $response ) ) { - printf( "Could not index posts!\nResponse code %s\n", wp_remote_retrieve_response_code( $response ) ); + $message = [ 'Failed to index posts!' ]; if ( is_wp_error( $response ) ) { - printf( "Message: %s\n", $response->get_error_message() ); + $message[] = sprintf( 'Message: %s', $response->get_error_message() ); + } else { + $message[] = sprintf( 'Response code %s', wp_remote_retrieve_response_code( $response ) ); + $message[] = sprintf( 'Message: %s', wp_remote_retrieve_body( $response ) ); } - printf( "Backtrace: %s\n", travis_es_debug_backtrace_summary() ); - exit( 1 ); + $message[] = sprintf( "Backtrace:%s", travis_es_debug_backtrace_summary() ); + throw new ES_Index_Exception( implode( "\n", $message ) ); } + + return true; } function travis_es_debug_backtrace_summary() { $backtrace = wp_debug_backtrace_summary( null, 0, false ); - foreach ( $backtrace as $k => $call ) { - if ( preg_match( '/PHPUnit_(TextUI_(Command|TestRunner)|Framework_(TestSuite|TestCase|TestResult))|ReflectionMethod|travis_es_(verify_response_code|debug_backtrace_summary)/', $call ) ) { - unset( $backtrace[ $k ] ); - } - } - return join( ', ', array_reverse( $backtrace ) ); + $backtrace = array_filter( $backtrace, function( $call ) { + return ! preg_match( '/PHPUnit_(TextUI_(Command|TestRunner)|Framework_(TestSuite|TestCase|TestResult))|ReflectionMethod|travis_es_(verify_response_code|debug_backtrace_summary)/', $call ); + } ); + return "\n\t" . join( "\n\t", $backtrace ); } /** diff --git a/bin/install-es.sh b/bin/install-es.sh new file mode 100644 index 0000000..98a488c --- /dev/null +++ b/bin/install-es.sh @@ -0,0 +1,34 @@ +#!/usr/bin/env bash + +if [ $# -lt 1 ]; then + echo "usage: $0 " + exit 1 +fi + +ES_VERSION=$1 +echo $JAVA_HOME + +setup_es() { + download_url=$1 + mkdir /tmp/elasticsearch + wget -O - $download_url | tar xz --directory=/tmp/elasticsearch --strip-components=1 +} + +start_es() { + echo "Starting Elasticsearch $ES_VERSION..." + echo "/tmp/elasticsearch/bin/elasticsearch $1 > /tmp/elasticsearch.log &" + /tmp/elasticsearch/bin/elasticsearch $1 > /tmp/elasticsearch.log & +} + +if [[ "$ES_VERSION" == 2.* ]]; then + setup_es https://download.elastic.co/elasticsearch/release/org/elasticsearch/distribution/tar/elasticsearch/${ES_VERSION}/elasticsearch-${ES_VERSION}.tar.gz +else + setup_es https://artifacts.elastic.co/downloads/elasticsearch/elasticsearch-${ES_VERSION}.tar.gz +fi + +# java_home='/usr/lib/jvm/java-8-oracle' +if [[ "$ES_VERSION" == 2.* ]]; then + start_es '-Des.path.repo=/tmp' +else + start_es '-Epath.repo=/tmp -Enetwork.host=_local_' +fi diff --git a/bin/install-wp-tests.sh b/bin/install-wp-tests.sh index 0e2e738..73bb4c7 100755 --- a/bin/install-wp-tests.sh +++ b/bin/install-wp-tests.sh @@ -1,7 +1,7 @@ #!/usr/bin/env bash if [ $# -lt 3 ]; then - echo "usage: $0 [db-host] [wp-version]" + echo "usage: $0 [db-host] [wp-version] [skip-database-creation]" exit 1 fi @@ -10,6 +10,7 @@ DB_USER=$2 DB_PASS=$3 DB_HOST=${4-localhost} WP_VERSION=${5-latest} +SKIP_DB_CREATE=${6-false} WP_TESTS_DIR=${WP_TESTS_DIR-/tmp/wordpress-tests-lib} WP_CORE_DIR=${WP_CORE_DIR-/tmp/wordpress/} @@ -79,16 +80,14 @@ install_test_suite() { # set up testing suite mkdir -p $WP_TESTS_DIR svn co --quiet https://develop.svn.wordpress.org/${WP_TESTS_TAG}/tests/phpunit/includes/ $WP_TESTS_DIR/includes + svn co --quiet https://develop.svn.wordpress.org/${WP_TESTS_TAG}/tests/phpunit/data/ $WP_TESTS_DIR/data fi - cd $WP_TESTS_DIR - - # Fix an error with trac referencing - mkdir data - if [ ! -f wp-tests-config.php ]; then download https://develop.svn.wordpress.org/${WP_TESTS_TAG}/wp-tests-config-sample.php "$WP_TESTS_DIR"/wp-tests-config.php - sed $ioption "s:dirname( __FILE__ ) . '/src/':'$WP_CORE_DIR':" "$WP_TESTS_DIR"/wp-tests-config.php + # remove all forward slashes in the end + WP_CORE_DIR=$(echo $WP_CORE_DIR | sed "s:/\+$::") + sed $ioption "s:dirname( __FILE__ ) . '/src/':'$WP_CORE_DIR/':" "$WP_TESTS_DIR"/wp-tests-config.php sed $ioption "s/youremptytestdbnamehere/$DB_NAME/" "$WP_TESTS_DIR"/wp-tests-config.php sed $ioption "s/yourusernamehere/$DB_USER/" "$WP_TESTS_DIR"/wp-tests-config.php sed $ioption "s/yourpasswordhere/$DB_PASS/" "$WP_TESTS_DIR"/wp-tests-config.php @@ -98,6 +97,11 @@ install_test_suite() { } install_db() { + + if [ ${SKIP_DB_CREATE} = "true" ]; then + return 0 + fi + # parse DB_HOST for port or socket references local PARTS=(${DB_HOST//\:/ }) local DB_HOSTNAME=${PARTS[0]}; @@ -120,4 +124,4 @@ install_db() { install_wp install_test_suite -install_db \ No newline at end of file +install_db diff --git a/class-es-wp-date-query.php b/class-es-wp-date-query.php index 0c20c87..fd2006b 100644 --- a/class-es-wp-date-query.php +++ b/class-es-wp-date-query.php @@ -23,7 +23,11 @@ public function get_dsl( $es_query ) { if ( 1 == count( $filter_parts ) ) { $filter[] = reset( $filter_parts ); } else { - $filter[] = array( 'and' => $filter_parts ); + $filter[] = array( + 'bool' => array( + 'filter' => $filter_parts, + ), + ); } } } @@ -32,7 +36,16 @@ public function get_dsl( $es_query ) { if ( 1 == count( $filter ) ) { $filter = reset( $filter ); } elseif ( ! empty( $filter ) ) { - $filter = array( strtolower( $this->relation ) => $filter ); + if ( 'or' === strtolower( $this->relation ) ) { + $relation = 'should'; + } else { + $relation = 'filter'; + } + $filter = array( + 'bool' => array( + $relation => $filter, + ), + ); } else { $filter = array(); } @@ -283,7 +296,11 @@ public function build_dsl_part( $field, $value, $compare, $sanitize = 'intval' ) } if ( ! empty( $part ) && in_array( $compare, array( '!=', 'NOT IN', 'NOT BETWEEN' ) ) ) { - return array( 'not' => $part ); + return array( + 'bool' => array( + 'must_not' => $part, + ), + ); } else { return $part; } diff --git a/class-es-wp-meta-query.php b/class-es-wp-meta-query.php index 1ca3c0b..447918d 100644 --- a/class-es-wp-meta-query.php +++ b/class-es-wp-meta-query.php @@ -86,12 +86,18 @@ protected function get_dsl_for_query( &$query ) { $filters = array_filter( $filters ); $this->clauses = array_filter( $this->clauses ); - if ( empty( $relation ) ) { - $relation = 'and'; + if ( ! empty( $relation ) && 'or' === strtolower( $relation ) ) { + $relation = 'should'; + } else { + $relation = 'filter'; } if ( count( $filters ) > 1 ) { - $filters = array( strtolower( $relation ) => $filters ); + $filters = array( + 'bool' => array( + $relation => $filters, + ), + ); } elseif ( ! empty( $filters ) ) { $filters = reset( $filters ); } @@ -204,9 +210,9 @@ public function get_dsl_for_clause( &$clause, $query, $clause_key = '' ) { case 'LIKE' : case 'NOT LIKE' : if ( '*' == $clause['key'] ) { - $filter = array( 'query' => $this->es_query->dsl_multi_match( $this->es_query->meta_map( $clause['key'], 'analyzed' ), $clause['value'] ) ); + $filter = $this->es_query->dsl_multi_match( $this->es_query->meta_map( $clause['key'], 'analyzed' ), $clause['value'] ); } else { - $filter = array( 'query' => $this->es_query->dsl_match( $this->es_query->meta_map( $clause['key'], 'analyzed' ), $clause['value'] ) ); + $filter = $this->es_query->dsl_match( $this->es_query->meta_map( $clause['key'], 'analyzed' ), $clause['value'] ); } break; @@ -241,7 +247,7 @@ public function get_dsl_for_clause( &$clause, $query, $clause_key = '' ) { default : if ( '*' == $clause['key'] ) { - $filter = array( 'query' => $this->es_query->dsl_multi_match( $this->es_query->meta_map( $clause['key'], $clause['type'] ), $clause['value'] ) ); + $filter = $this->es_query->dsl_multi_match( $this->es_query->meta_map( $clause['key'], $clause['type'] ), $clause['value'] ); } else { $filter = $this->es_query->dsl_terms( $this->es_query->meta_map( $clause['key'], $clause['type'] ), $clause['value'] ); } @@ -254,9 +260,11 @@ public function get_dsl_for_clause( &$clause, $query, $clause_key = '' ) { // query, we still only query posts where the meta key exists. if ( in_array( $clause['compare'], array( 'NOT IN', '!=', 'NOT BETWEEN', 'NOT LIKE' ) ) ) { return array( - 'and' => array( - $this->es_query->dsl_exists( $this->es_query->meta_map( $clause['key'] ) ), - array( 'not' => $filter ), + 'bool' => array( + 'filter' => array( + $this->es_query->dsl_exists( $this->es_query->meta_map( $clause['key'] ) ), + ), + 'must_not' => $filter, ), ); } else { diff --git a/class-es-wp-query-shoehorn.php b/class-es-wp-query-shoehorn.php index 3d7c938..7bbace7 100644 --- a/class-es-wp-query-shoehorn.php +++ b/class-es-wp-query-shoehorn.php @@ -21,6 +21,11 @@ function es_wp_query_arg( $vars ) { * @return void */ function es_wp_query_shoehorn( &$query ) { + // Prevent infinite loops! + if ( $query instanceof ES_WP_Query ) { + return; + } + if ( true == $query->get( 'es' ) ) { $conditionals = array( 'is_single' => false, @@ -55,24 +60,29 @@ function es_wp_query_shoehorn( &$query ) { $conditionals[ $key ] = $query->$key; } - $es_query_vars = $query->query_vars; $query_args = $query->query; + + // Run this query through ES. + $es_query_vars = $query->query_vars; $es_query_vars['fields'] = 'ids'; $es_query = new ES_WP_Query( $es_query_vars ); + // Make the post query use the post IDs from the ES results instead. $query->parse_query( array( - 'post_type' => 'any', - 'post_status' => 'any', + 'post_type' => $query->get( 'post_type' ), + 'post_status' => $query->get( 'post_status' ), 'post__in' => $es_query->posts, 'posts_per_page' => $es_query->post_count, 'fields' => $query->get( 'fields' ), 'orderby' => 'post__in', 'order' => 'ASC', ) ); + # Reinsert all the conditionals from the original query foreach ( $conditionals as $key => $value ) { $query->$key = $value; } + $shoehorn = new ES_WP_Query_Shoehorn( $query, $es_query, $query_args ); } } @@ -172,7 +182,7 @@ public function filter__found_posts( $found_posts, $query ) { * @param object $query WP_Query object. * @return string The SQL query to get posts. */ - public function filter__posts_request( $sql, &$query ) { + public function filter__posts_request( $sql, $query ) { if ( spl_object_hash( $query ) == $this->hash ) { remove_filter( 'posts_request', array( $this, 'filter__posts_request' ), 1000, 2 ); $this->reboot_query_vars( $query ); @@ -199,6 +209,7 @@ public function filter__posts_request( $sql, &$query ) { private function reboot_query_vars( &$query ) { $q =& $query->query_vars; + // Remove custom query vars used for the ES query in es_wp_query_shoehorn(). $current_query_vars = $q; unset( $current_query_vars['post_type'], @@ -214,9 +225,27 @@ private function reboot_query_vars( &$query ) { $query->parse_query(); $q = array_merge( $current_query_vars, $q ); - // Restore some necessary defaults if we zapped 'em + // Restore some necessary defaults if we zapped 'em. if ( empty( $q['posts_per_page'] ) ) { $q['posts_per_page'] = get_option( 'posts_per_page' ); } + + // Restore the author ID which is normally added during get_posts() in WP_Query. + // Required for handle_404() in WP class to not mark empty author archives as 404s. + if ( $query->is_author() && ! empty( $q['author_name'] ) ) { + if ( false !== strpos( $q['author_name'], '/' ) ) { + $q['author_name'] = explode( '/', $q['author_name'] ); + if ( $q['author_name'][ count( $q['author_name'] ) - 1 ] ) { + $q['author_name'] = $q['author_name'][ count( $q['author_name'] ) - 1 ]; // no trailing slash. + } else { + $q['author_name'] = $q['author_name'][ count( $q['author_name'] ) - 2 ]; // there was a trailing slash. + } + } + $author = get_user_by( 'slug', sanitize_title_for_query( $q['author_name'] ) ); + + if ( isset( $author->ID ) ) { + $q['author'] = $author->ID; + } + } } -} \ No newline at end of file +} diff --git a/class-es-wp-query-wrapper.php b/class-es-wp-query-wrapper.php index 5c64f0a..d268d49 100644 --- a/class-es-wp-query-wrapper.php +++ b/class-es-wp-query-wrapper.php @@ -44,15 +44,15 @@ protected function set_posts( $q, $es_response ) { switch ( $q['fields'] ) { case 'ids' : foreach ( $es_response['hits']['hits'] as $hit ) { - $post_id = (array) $hit['fields'][ $this->es_map( 'post_id' ) ]; + $post_id = (array) $hit['_source'][ $this->es_map( 'post_id' ) ]; $this->posts[] = reset( $post_id ); } return; case 'id=>parent' : foreach ( $es_response['hits']['hits'] as $hit ) { - $post_id = (array) $hit['fields'][ $this->es_map( 'post_id' ) ]; - $post_parent = (array) $hit['fields'][ $this->es_map( 'post_parent' ) ]; + $post_id = (array) $hit['_source'][ $this->es_map( 'post_id' ) ]; + $post_parent = (array) $hit['_source'][ $this->es_map( 'post_parent' ) ]; $this->posts[ reset( $post_id ) ] = reset( $post_parent ); } return; @@ -64,7 +64,7 @@ protected function set_posts( $q, $es_response ) { } else { $post_ids = array(); foreach ( $es_response['hits']['hits'] as $hit ) { - $post_id = (array) $hit['fields'][ $this->es_map( 'post_id' ) ]; + $post_id = (array) $hit['_source'][ $this->es_map( 'post_id' ) ]; $post_ids[] = absint( reset( $post_id ) ); } $post_ids = array_filter( $post_ids ); @@ -480,7 +480,11 @@ public function get_posts() { $filter[] = $this->dsl_terms( $this->es_map( 'post_id' ), $post__in ); } elseif ( $q['post__not_in'] ) { $post__not_in = array_map( 'absint', $q['post__not_in'] ); - $filter[] = array( 'not' => $this->dsl_terms( $this->es_map( 'post_id' ), $post__not_in ) ); + $filter[] = array( + 'bool' => array( + 'must_not' => $this->dsl_terms( $this->es_map( 'post_id' ), $post__not_in ) + ), + ); } if ( is_numeric( $q['post_parent'] ) ) { @@ -490,7 +494,11 @@ public function get_posts() { $filter[] = $this->dsl_terms( $this->es_map( 'post_parent' ), $post_parent__in ); } elseif ( $q['post_parent__not_in'] ) { $post_parent__not_in = array_map( 'absint', $q['post_parent__not_in'] ); - $filter[] = array( 'not' => $this->dsl_terms( $this->es_map( 'post_parent' ), $post_parent__not_in ) ); + $filter[] = array( + 'bool' => array( + 'must_not' => $this->dsl_terms( $this->es_map( 'post_parent' ), $post_parent__not_in ), + ), + ); } if ( $q['page_id'] ) { @@ -514,10 +522,14 @@ public function get_posts() { if ( ! empty( $search ) ) { $query['must'] = apply_filters_ref_array( 'es_posts_search', array( $search, &$this ) ); if ( ! is_user_logged_in() ) { - $filter[] = array( 'or' => array( - $this->dsl_terms( $this->es_map( 'post_password' ), '' ), - $this->dsl_missing( $this->es_map( 'post_password' ) ) - ) ); + $filter[] = array( + 'bool' => array( + 'should' => array( + $this->dsl_terms( $this->es_map( 'post_password' ), '' ), + $this->dsl_missing( $this->es_map( 'post_password' ) ), + ), + ), + ); } } @@ -628,7 +640,11 @@ public function get_posts() { if ( ! empty( $q['author__not_in'] ) ) { $author__not_in = array_map( 'absint', array_unique( (array) $q['author__not_in'] ) ); - $filter[] = array( 'not' => $this->dsl_terms( $this->es_map( 'post_author' ), $author__not_in ) ); + $filter[] = array( + 'bool' => array( + 'must_not' => $this->dsl_terms( $this->es_map( 'post_author' ), $author__not_in ), + ), + ); } elseif ( ! empty( $q['author__in'] ) ) { $author__in = array_map( 'absint', array_unique( (array) $q['author__in'] ) ); $filter[] = $this->dsl_terms( $this->es_map( 'post_author' ), $author__in ); @@ -813,15 +829,23 @@ public function get_posts() { if ( ! empty( $e_status ) ) { // $statuswheres[] = "(" . join( ' AND ', $e_status ) . ")"; - $status_ands[] = array( 'not' => $this->dsl_terms( $this->es_map( 'post_status' ), $e_status ) ); + $status_ands[] = array( + 'bool' => array( + 'must_not' => $this->dsl_terms( $this->es_map( 'post_status' ), $e_status ), + ), + ); } if ( ! empty( $r_status ) ) { if ( !empty($q['perm'] ) && 'editable' == $q['perm'] && !current_user_can($edit_others_cap) ) { // $statuswheres[] = "($wpdb->posts.post_author = $user_id " . "AND (" . join( ' OR ', $r_status ) . "))"; - $status_ands[] = array( 'bool' => array( 'must' => array( - $this->dsl_terms( $this->es_map( 'post_author' ), $user_id ), - $this->dsl_terms( $this->es_map( 'post_status' ), $r_status ) - ) ) ); + $status_ands[] = array( + 'bool' => array( + 'filter' => array( + $this->dsl_terms( $this->es_map( 'post_author' ), $user_id ), + $this->dsl_terms( $this->es_map( 'post_status' ), $r_status ), + ), + ), + ); } else { // $statuswheres[] = "(" . join( ' OR ', $r_status ) . ")"; $status_ands[] = $this->dsl_terms( $this->es_map( 'post_status' ), $r_status ); @@ -830,10 +854,14 @@ public function get_posts() { if ( ! empty( $p_status ) ) { if ( ! empty( $q['perm'] ) && 'readable' == $q['perm'] && ! current_user_can( $read_private_cap ) ) { // $statuswheres[] = "($wpdb->posts.post_author = $user_id " . "AND (" . join( ' OR ', $p_status ) . "))"; - $status_ands[] = array( 'bool' => array( 'must' => array( - $this->dsl_terms( $this->es_map( 'post_author' ), $user_id ), - $this->dsl_terms( $this->es_map( 'post_status' ), $p_status ) - ) ) ); + $status_ands[] = array( + 'bool' => array( + 'filter' => array( + $this->dsl_terms( $this->es_map( 'post_author' ), $user_id ), + $this->dsl_terms( $this->es_map( 'post_status' ), $p_status ), + ), + ), + ); } else { // $statuswheres[] = "(" . join( ' OR ', $p_status ) . ")"; $status_ands[] = $this->dsl_terms( $this->es_map( 'post_status' ), $p_status ); @@ -868,10 +896,14 @@ public function get_posts() { if ( current_user_can( $read_private_cap ) ) { $singular_states[] = $state; } else { - $singular_states_ors[] = array( 'and' => array( - $this->dsl_terms( $this->es_map( 'post_author' ), $user_id ), - $this->dsl_terms( $this->es_map( 'post_status' ), $state ) - ) ); + $singular_states_ors[] = array( + 'bool' => array( + 'filter' => array( + $this->dsl_terms( $this->es_map( 'post_author' ), $user_id ), + $this->dsl_terms( $this->es_map( 'post_status' ), $state ), + ), + ), + ); } } } @@ -880,7 +912,11 @@ public function get_posts() { $singular_states_filter = $this->dsl_terms( $this->es_map( 'post_status' ), $singular_states ); if ( ! empty( $singular_states_ors ) ) { $singular_states_ors[] = $singular_states_filter; - $filter[] = array( 'or' => $singular_states_ors ); + $filter[] = array( + 'bool' => array( + 'should' => $singular_states_ors, + ), + ); } else { $filter[] = $singular_states_filter; } @@ -952,21 +988,22 @@ public function get_posts() { $where = "AND 0"; } - // Run cleanup on our filter and query + // Run cleanup on our filter and query. $filter = array_filter( $filter ); - if ( ! empty( $filter ) ) { - $filter = array( 'and' => $filter ); - } $query = array_filter( $query ); if ( ! empty( $query ) ) { - if ( 1 == count( $query ) && ! empty( $query['must'] ) && 1 == count( $query['must'] ) ) { + if ( + 1 === count( $query ) + && ! empty( $query['must'] ) + && 1 === count( $query['must'] ) + && empty( $filter ) + ) { $query = $query['must']; } else { - $query = array( 'bool' => $query ); - if ( ! empty( $query['bool']['should'] ) ) { - $query['bool']['minimum_should_match'] = 1; - } + $query = array( + 'bool' => $query, + ); } } @@ -1006,13 +1043,20 @@ public function get_posts() { $$piece = isset( $clauses[ $piece ] ) ? $clauses[ $piece ] : ''; } + // Add the filters to the query. + if ( ! empty( $filter ) ) { + if ( empty( $query['bool']['filter'] ) ) { + $query['bool']['filter'] = array(); + } + $query['bool']['filter'] = array_merge( $query['bool']['filter'], $filter ); + } + $this->es_args = array( - 'filter' => $filter, - 'query' => $query, - 'sort' => $sort, - 'fields' => $fields, - 'from' => $from, - 'size' => $size + 'query' => $query, + 'sort' => $sort, + '_source' => $fields, + 'from' => $from, + 'size' => $size, ); // Remove empty criteria @@ -1385,7 +1429,13 @@ public static function dsl_exists( $field ) { } public static function dsl_missing( $field, $args = array() ) { - return array( 'missing' => array_merge( array( 'field' => $field ), $args ) ); + return array( + 'bool' => array( + 'must_not' => array( + 'exists' => array_merge( array( 'field' => $field ), $args ), + ), + ), + ); } public static function dsl_match( $field, $value, $args = array() ) { @@ -1401,6 +1451,6 @@ public static function dsl_all_terms( $field, $values ) { foreach ( $values as $value ) { $queries[] = array( 'term' => array( $field => $value ) ); } - return array( 'bool' => array( 'must' => $queries ) ); + return array( 'bool' => array( 'filter' => $queries ) ); } } diff --git a/class-es-wp-tax-query.php b/class-es-wp-tax-query.php index cd53948..873cd25 100644 --- a/class-es-wp-tax-query.php +++ b/class-es-wp-tax-query.php @@ -97,12 +97,18 @@ protected function get_dsl_for_query( &$query ) { // Filter to remove empties. $filters = array_filter( $filters ); - if ( empty( $relation ) ) { - $relation = 'and'; + if ( ! empty( $relation ) && 'or' === strtolower( $relation ) ) { + $relation = 'should'; + } else { + $relation = 'filter'; } if ( count( $filters ) > 1 ) { - $filters = array( strtolower( $relation ) => $filters ); + $filters = array( + 'bool' => array( + $relation => $filters, + ), + ); } elseif ( ! empty( $filters ) ) { $filters = reset( $filters ); } @@ -183,7 +189,7 @@ public function get_dsl_for_clause( &$clause, $query ) { if ( count( $matches ) > 1 ) { $current_filter = array( 'bool' => array( - ( 'AND' == $clause['operator'] ? 'must' : 'should' ) => $matches, + ( 'AND' == $clause['operator'] ? 'filter' : 'should' ) => $matches, ), ); } else { @@ -200,7 +206,11 @@ public function get_dsl_for_clause( &$clause, $query ) { } if ( 'NOT IN' == $clause['operator'] ) { - return array( 'not' => $current_filter ); + return array( + 'bool' => array( + 'must_not' => $current_filter, + ), + ); } else { return $current_filter; } diff --git a/tests/bootstrap.php b/tests/bootstrap.php index 3a12201..bf6f1d6 100644 --- a/tests/bootstrap.php +++ b/tests/bootstrap.php @@ -2,8 +2,15 @@ define( 'ES_WP_QUERY_TEST_ENV', true ); -$_tests_dir = getenv('WP_TESTS_DIR'); -if ( !$_tests_dir ) $_tests_dir = '/tmp/wordpress-tests-lib'; +$_tests_dir = getenv( 'WP_TESTS_DIR' ); +if ( ! $_tests_dir ) { + $_tests_dir = '/tmp/wordpress-tests-lib'; +} + +$_es_version = getenv( 'ES_VERSION' ); +if ( ! defined( 'ES_VERSION' ) && $_es_version ) { + define( 'ES_VERSION', $_es_version ); +} require_once $_tests_dir . '/includes/functions.php'; @@ -24,8 +31,22 @@ function _manually_load_plugin() { exit( 1 ); } - es_wp_query_verify_es_is_running(); + if ( ! es_wp_query_verify_es_is_running() ) { + echo "\n\nFatal: bootstrap check failed!\n"; + exit( 1 ); + } } tests_add_filter( 'muplugins_loaded', '_manually_load_plugin' ); +/** + * Set `'es' => true` on the given WP_Query object. + * + * This is a helper intended to be used with `pre_get_posts`. + * + * @param \WP_Query $query WP_Query object. + */ +function _es_wp_query_set_es_to_true( \WP_Query $query ) { + $query->set( 'es', true ); +} + require $_tests_dir . '/includes/bootstrap.php'; diff --git a/tests/query/author.php b/tests/query/author.php new file mode 100644 index 0000000..3c2c163 --- /dev/null +++ b/tests/query/author.php @@ -0,0 +1,21 @@ +set_permalink_structure( '/%year%/%monthnum%/%day%/%postname%/' ); + } + + function test_author_with_no_posts() { + add_action( 'pre_get_posts', '_es_wp_query_set_es_to_true' ); + $user_id = self::factory()->user->create( array( 'user_login' => 'user-a' ) ); + $this->go_to( '/author/user-a/' ); + $this->assertQueryTrue( 'is_archive', 'is_author' ); + } +} diff --git a/tests/query/shoehorn.php b/tests/query/shoehorn.php index 83d364d..818a403 100644 --- a/tests/query/shoehorn.php +++ b/tests/query/shoehorn.php @@ -54,7 +54,6 @@ public function setUp() { es_wp_query_index_test_data(); - unset( $this->q ); $this->q = new WP_Query(); }