diff -u b/.travis.yml b/.travis.yml --- b/.travis.yml +++ b/.travis.yml @@ -63,6 +63,7 @@ - composer require drupal/search_api:1.x-dev - composer require drupal/search_api_autocomplete:1.x-dev - composer require drupal/search_api_solr:999.0.0 + - composer require drupal/search_api_location:1.x-dev - composer require drupal/facets:1.x-dev - composer require drupal/geofield:1.x-dev - composer require drush/drush reverted: --- b/.travis.yml.orig +++ /dev/null @@ -1,96 +0,0 @@ -language: php -cache: - bundler: true - directories: - - $HOME/tmp/drush - - $HOME/.bundle - - $HOME/.composer - - $HOME/downloads - apt: true - -git: - depth: 10000 - -php: - - 7.0 - - 5.5 - -env: - # 4.5.1 is the oldest version we support - - PATH=$PATH:/home/travis/.composer/vendor/bin SOLR_VERSION=4.5.1 SOLR_CORE=d8 SOLR_CONFS="$TRAVIS_BUILD_DIR/solr-conf/4.x" - # 5.5.0 introduced major changes for boolean operators. - - PATH=$PATH:/home/travis/.composer/vendor/bin SOLR_VERSION=5.5.4 SOLR_CORE=d8 SOLR_CONFS="$TRAVIS_BUILD_DIR/solr-conf/5.x" - # 6.4.2 is the latest currently supported release. - - PATH=$PATH:/home/travis/.composer/vendor/bin SOLR_VERSION=6.4.2 SOLR_CORE=d8 SOLR_CONFS="$TRAVIS_BUILD_DIR/solr-conf/6.x" - -notifications: - irc: - - "chat.freenode.net#drupal-search-api" - -# This will create the database -mysql: - database: drupal - username: root - encoding: utf8 - -# To be able to run a webbrowser -# If we need anything more powerful -# than e.g. phantomjs -before_install: - - phpenv config-rm xdebug.ini - - composer self-update - - composer global require "hirak/prestissimo:^0.3" - - sudo apt-get update -qq > /dev/null - - "export DISPLAY=:99.0" - - "sh -e /etc/init.d/xvfb start" - -install: - - git tag 999.0.0 - # Make sure we don't fail when checking out projects - - echo -e "Host github.com\n\tStrictHostKeyChecking no\n" >> ~/.ssh/config - - echo -e "Host git.drupal.org\n\tStrictHostKeyChecking no\n" >> ~/.ssh/config - # Set sendmail so drush doesn't throw an error during site install. - - echo "sendmail_path='true'" >> `php --ini | grep "Loaded Configuration" | awk '{print $4}'` - # Forward the errors to the syslog so we can print them - - echo "error_log=syslog" >> `php --ini | grep "Loaded Configuration" | awk '{print $4}'` - # Get latest Drupal 8 core - - cd $TRAVIS_BUILD_DIR/.. - - git clone --depth=1 --branch 8.3.x https://git.drupal.org/project/drupal.git - - cd $TRAVIS_BUILD_DIR/../drupal - - composer install - - composer config repositories.drupal composer https://packages.drupal.org/8 - - composer config repositories.search_api_solr vcs $TRAVIS_BUILD_DIR - - composer require drupal/search_api:1.x-dev - - composer require drupal/search_api_autocomplete:1.x-dev - - composer require drupal/search_api_solr:999.0.0 - - composer require drupal/facets:1.x-dev - - composer require drush/drush - # Patch template. - ######################################### - # to be removed once #2824932 is resolved - #- cd modules/search_api - #- curl https://www.drupal.org/files/issues/2824932.patch | patch -p1 - #- cd $TRAVIS_BUILD_DIR/../drupal - ######################################### - -before_script: - # Start the built-in php web server (mysql is already started) and - # suppress web-server access logs output. - - php -S localhost:8888 >& /dev/null & - # Install the site - - ./vendor/bin/drush -v site-install minimal --db-url=mysql://root:@localhost/drupal --yes - - ./vendor/bin/drush en --yes simpletest - # Install Solr - - cat $TRAVIS_BUILD_DIR/travis-solr.sh | bash - -script: - # Run the tests - - cd $TRAVIS_BUILD_DIR/../drupal - - export SIMPLETEST_DB=mysql://root:@localhost/drupal - - export SIMPLETEST_BASE_URL=http://localhost:8888 - - ./vendor/bin/phpunit -c core --group search_api_solr --verbose --debug | tee ; export TEST_PHPUNIT=${PIPESTATUS[0]} ; echo $TEST_PHPUNIT - # Re-enable when trying to get CodeSniffer doesn't return a 403 anymore. - #- /home/travis/.composer/vendor/bin/phpcs --standard=/home/travis/.composer/vendor/drupal/coder/coder_sniffer/Drupal --extensions=php,inc,test,module,install --ignore=css/ $TRAVIS_BUILD_DIR/../drupal/modules/search_api - # Exit the build - - echo $TEST_PHPUNIT - - if [ $TEST_PHPUNIT -eq 0 ]; then exit 0; else exit 1; fi diff -u b/src/Plugin/search_api/backend/SearchApiSolrBackend.php b/src/Plugin/search_api/backend/SearchApiSolrBackend.php --- b/src/Plugin/search_api/backend/SearchApiSolrBackend.php +++ b/src/Plugin/search_api/backend/SearchApiSolrBackend.php @@ -16,6 +16,7 @@ use Drupal\Core\Language\LanguageManagerInterface; use Drupal\Core\Plugin\PluginFormInterface; use Drupal\Core\TypedData\ComplexDataDefinitionInterface; +use Drupal\Core\TypedData\DataDefinition; use Drupal\Core\Url; use Drupal\search_api\Item\Field; use Drupal\search_api\Item\FieldInterface; @@ -46,7 +47,6 @@ use Solarium\QueryType\Suggester\Result\Result as SuggesterResult; use Solarium\QueryType\Update\Query\Document\Document; use Symfony\Component\DependencyInjection\ContainerInterface; -use Drupal\Core\TypedData\DataDefinition; /** * The minimum required Solr schema version. @@ -460,9 +460,9 @@ 'search_api_facets_operator_or', 'search_api_mlt', 'search_api_random_sort', + 'search_api_data_type_location', // 'search_api_grouping', // 'search_api_spellcheck', - 'search_api_data_type_location', // 'search_api_data_type_geohash', ]; } @@ -472,6 +472,7 @@ */ public function supportsDataType($type) { return in_array($type, [ + 'location', 'solr_text_ngram', 'solr_text_phonetic', 'solr_text_unstemmed', @@ -874,8 +875,6 @@ $connector = $this->getSolrConnector(); // Instantiate a Solarium select query. $solarium_query = $connector->getSelectQuery(); - // Clear all the fields so we start with a clean slate. - $solarium_query->clearFields(); $query_helper = $connector->getQueryHelper($solarium_query); // Extract keys. @@ -948,7 +947,7 @@ // Set sorts. $this->setSorts($solarium_query, $query, $field_names); - // Set facet fields. + // Set facet fields. setSpatial() might add more facets. $this->setFacets($query, $solarium_query, $field_names); // Handle spatial filters. @@ -980,7 +979,8 @@ } } - // @todo Make this more configurable so that views can choose which fields it wants to fetch + // @todo Make this more configurable so that views can choose which fields + // it wants to fetch. if (!empty($this->configuration['retrieve_data'])) { $solarium_query->addFields(['*', 'score']); } @@ -1130,7 +1130,10 @@ $name = $pref . '_' . $key; $ret[$key] = SearchApiSolrUtility::encodeSolrName($name); - // Add the distance pseudo field for location fields. + // Add a distance pseudo field for any location field. These fields + // don't really exist in the solr core, but we tell solr to name the + // distance calculation results that way. Later we directly pass these + // as "fields" to Drupal and especially Views. if ($type == 'location') { $ret[$key . '__distance'] = SearchApiSolrUtility::encodeSolrName($name . '__distance'); } @@ -1535,16 +1538,17 @@ if (isset($result_data['facet_counts']['facet_queries'])) { if ($spatials = $query->getOption('search_api_location')) { foreach ($result_data['facet_counts']['facet_queries'] as $key => $count) { - if (!preg_match('/^spatial-(.*)-(\d+(?:\.\d+)?)-(\d+(?:\.\d+)?)$/', $key, $m)) { + // This special key is defined in setSpatial(). + if (!preg_match('/^spatial-(.*)-(\d+(?:\.\d+)?)-(\d+(?:\.\d+)?)$/', $key, $matches)) { continue; } - if (empty($extract_facets[$m[1]])) { + if (empty($extract_facets[$matches[1]])) { continue; } - $facet = $extract_facets[$m[1]]; + $facet = $extract_facets[$matches[1]]; if ($count >= $facet['min_count']) { - $facets[$m[1]][] = array( - 'filter' => "[{$m[2]} {$m[3]}]", + $facets[$matches[1]][] = array( + 'filter' => "[{$matches[2]} {$matches[3]}]", 'count' => $count, ); } @@ -2116,6 +2120,7 @@ if (!empty($this->configuration['highlight_data'])) { $item_fields = $item->getFields(); foreach ($field_mapping as $search_api_property => $solr_property) { + // @todo We now have more fulltext field variants to be highlighted. if ((strpos($solr_property, 'ts_') === 0 || strpos($solr_property, 'tm_') === 0) && !empty($data['highlighting'][$solr_id][$solr_property])) { $snippets = []; foreach ($data['highlighting'][$solr_id][$solr_property] as $value) { @@ -2314,25 +2319,20 @@ * The search api query. * @param array $spatial_options * The spatial options to add. - * @param array $field_names + * @param $field_names * The field names, to add the spatial options for. */ protected function setSpatial(Query $solarium_query, QueryInterface $query, $spatial_options = array(), $field_names = array()) { - $helper = $solarium_query->getHelper(); - foreach ($spatial_options as $i => $spatial) { // Reset radius for each option. unset($radius); - unset($min_radius); if (empty($spatial['field']) || empty($spatial['lat']) || empty($spatial['lon'])) { continue; } $field = $field_names[$spatial['field']]; - $distance_field = $field . '__distance'; - $spatial['lat'] = (float) $spatial['lat']; - $spatial['lon'] = (float) $spatial['lon']; + $point = ((float) $spatial['lat']) . ',' . ((float) $spatial['lon']); // Prepare the filter settings. if (isset($spatial['radius'])) { @@ -2350,7 +2350,7 @@ // If the fq consists only of a filter on this field, replace it with // a range. $preg_field = preg_quote($field, '/'); - if (preg_match('/^' . $preg_field . ':\["?(\*|\d+(?:\.\d+)?)"? TO "?(\*|\d+(?:\.\d+)?)"?\]$/', $filter_query->getQuery(), $matches)) { + if (preg_match('/^' . $preg_field . ':\["?(\*|\d+(?:\.\d+)?)"? TO "?(\*|\d+(?:\.\d+)?)"?\]$/', $filter_query, $matches)) { unset($filter_queries[$key]); if ($matches[1] && is_numeric($matches[1])) { $min_radius = isset($min_radius) ? max($min_radius, $matches[1]) : $matches[1]; @@ -2375,28 +2375,32 @@ /** * Adds spatial features to the search query. * - * @todo This code is outdated and needs to be reviewed and refactored. - * * @param \Solarium\QueryType\Select\Query\Query $solarium_query * The solr query. * @param \Drupal\search_api\Query\QueryInterface $query * The search api query. * @param array $spatial_options * The spatial options to add. - * @param $field_names + * @param array $field_names * The field names, to add the spatial options for. */ protected function setSpatial(Query $solarium_query, QueryInterface $query, $spatial_options = array(), $field_names = array()) { + $helper = $solarium_query->getHelper(); + foreach ($spatial_options as $i => $spatial) { // Reset radius for each option. unset($radius); + unset($min_radius); if (empty($spatial['field']) || empty($spatial['lat']) || empty($spatial['lon'])) { continue; } - $field = $field_names[$spatial['field']]; - $point = ((float) $spatial['lat']) . ',' . ((float) $spatial['lon']); + $solr_field = $field_names[$spatial['field']]; + $distance_field = $spatial['field'] . '__distance'; + $solr_distance_field = $field_names[$distance_field]; + $spatial['lat'] = (float) $spatial['lat']; + $spatial['lon'] = (float) $spatial['lon']; // Prepare the filter settings. if (isset($spatial['radius'])) { @@ -2413,8 +2417,8 @@ foreach ($filter_queries as $key => $filter_query) { // If the fq consists only of a filter on this field, replace it with // a range. - $preg_field = preg_quote($field, '/'); - if (preg_match('/^' . $preg_field . ':\["?(\*|\d+(?:\.\d+)?)"? TO "?(\*|\d+(?:\.\d+)?)"?\]$/', $filter_query, $matches)) { + $preg_field = preg_quote($solr_field, '/'); + if (preg_match('/^' . $preg_field . ':\["?(\*|\d+(?:\.\d+)?)"? TO "?(\*|\d+(?:\.\d+)?)"?\]$/', $filter_query->getQuery(), $matches)) { unset($filter_queries[$key]); if ($matches[1] && is_numeric($matches[1])) { $min_radius = isset($min_radius) ? max($min_radius, $matches[1]) : $matches[1]; @@ -2428,7 +2432,7 @@ $solarium_query->clearFilterQueries(); $solarium_query->addFilterQueries($filter_queries); - $geodist = $helper->geodist($field, $spatial['lat'], $spatial['lon']); + $geodist = $helper->geodist($solr_field, $spatial['lat'], $spatial['lon']); // If either a radius was given in the option, or a filter was // encountered, set a filter for the lowest value. If a lower boundary @@ -2436,20 +2440,20 @@ // doesn't contains any colons. if (isset($min_radius)) { $upper = isset($radius) ? " u=$radius" : ''; - $solarium_query->createFilterQuery($field)->setQuery("{!frange l=$min_radius$upper}" . $geodist); + $solarium_query->createFilterQuery($solr_field)->setQuery("{!frange l=$min_radius$upper}" . $geodist); } elseif (isset($radius)) { - $solarium_query->createFilterQuery($field)->setQuery($helper->{$spatial_method}($field, $spatial['lat'], $spatial['lon'], $radius)); + $solarium_query->createFilterQuery($solr_field)->setQuery($helper->{$spatial_method}($solr_field, $spatial['lat'], $spatial['lon'], $radius)); } - $solarium_query->addField($distance_field . ':' . $geodist); + $solarium_query->addField($solr_distance_field . ':' . $geodist); $sorts = $solarium_query->getSorts(); // Change sort on the field, if set (and not already changed). - if (isset($sorts[$distance_field])) { + if (isset($sorts[$solr_distance_field])) { $solarium_query->setQuery('{!func}' . $geodist); - $sorts['score'] = $sorts[$distance_field]; - unset($sorts[$distance_field]); + $sorts['score'] = $sorts[$solr_distance_field]; + unset($sorts[$solr_distance_field]); $solarium_query->clearSorts(); $solarium_query->addSorts($sorts); } @@ -2462,7 +2466,7 @@ $facets = $facet_set->getFacets(); foreach ($facets as $delta => $facet) { $facet_options = $facet->getOptions(); - if ($facet_options['field'] != $distance_field) { + if ($facet_options['field'] != $solr_distance_field) { continue; } $facet_set->removeFacet($delta); @@ -2477,8 +2481,9 @@ for ($distance_min = $min; $distance_min <= $max - $step; $distance_min += $step) { $distance_max = $distance_min + $step - 1; - $key = "spatial-{$spatial['field']}__distance-{$distance_min}-{$distance_max}"; - + // Define our own facet key to transport the min and max values. + // These will be extracted in extractFacets(). + $key = "spatial-{$distance_field}-{$distance_min}-{$distance_max}"; // Due to a limitation/bug in Solarium, it is not possible to use // setQuery method for geo facets. // So the key is misused to get a correct query. @@ -2613,18 +2618,8 @@ /** * {@inheritdoc} */ - public function supportsDataType($type) { - $supported_data_types = [ - 'location', - ]; - return in_array($type, $supported_data_types); - } - - /** - * {@inheritdoc} - */ public function getBackendDefinedFields(IndexInterface $index) { - $location_distance_fields = array(); + $location_distance_fields = []; foreach ($index->getFields() as $field) { if ($field->getType() == 'location') { diff -u b/tests/src/Kernel/SearchApiSolrLocationTest.php b/tests/src/Kernel/SearchApiSolrLocationTest.php --- b/tests/src/Kernel/SearchApiSolrLocationTest.php +++ b/tests/src/Kernel/SearchApiSolrLocationTest.php @@ -24,7 +24,7 @@ * * @var string[] */ - public static $modules = array( + public static $modules = [ 'system', 'search_api', 'search_api_solr', @@ -34,7 +34,7 @@ 'entity_test', 'geofield', 'field', - ); + ]; /** * A Search API server ID. @@ -81,11 +81,11 @@ $this->installEntitySchema('field_config'); // Create a location field and storage for testing. - FieldStorageConfig::create(array( + FieldStorageConfig::create([ 'field_name' => 'location', 'entity_type' => 'entity_test_mulrev_changed', 'type' => 'geofield', - ))->save(); + ])->save(); FieldConfig::create([ 'entity_type' => 'entity_test_mulrev_changed', 'field_name' => 'location', @@ -97,11 +97,11 @@ /** @var \Drupal\search_api\Entity\Index $index */ $index = Index::load($this->indexId); - $info = array( + $info = [ 'datasource_id' => 'entity:entity_test_mulrev_changed', 'property_path' => 'location', 'type' => 'location', - ); + ]; $fieldsHelper = $this->container->get('search_api.fields_helper'); @@ -180,14 +180,14 @@ public function testBackend() { // Search 500km from Antwerp. - $location_options = array( - array( + $location_options = [ + [ 'field' => 'location', 'lat' => '51.260197', 'lon' => '4.402771', 'radius' => '500', - ), - ); + ], + ]; /** @var \Drupal\search_api\Query\ResultSet $result */ $query = $this->buildSearch(NULL, [], NULL, FALSE) ->sort('location__distance'); @@ -204,15 +204,15 @@ $this->assertEquals(42.5263374675, $distance, 'The distance is correctly returned'); // Search between 100km and 6000km from Antwerp. - $location_options = array( - array( + $location_options = [ + [ 'field' => 'location', 'lat' => '51.260197', 'lon' => '4.402771', - ), - ); + ], + ]; $query = $this->buildSearch(NULL, [], NULL, FALSE) - ->addCondition('location', array('100', '6000'), 'BETWEEN') + ->addCondition('location', ['100', '6000'], 'BETWEEN') ->sort('location__distance', 'DESC'); $query->setOption('search_api_location', $location_options); @@ -220,31 +220,29 @@ $this->assertResults([2, 1], $result, 'Search between 100 and 6000km from Antwerp ordered by distance descending'); - $facets = array(); - $facets['location__distance'] = array( + $facets_options['location__distance'] = [ 'field' => 'location__distance', 'limit' => 10, 'min_count' => 0, 'missing' => TRUE, - ); + ]; // Search 1000km from Antwerp. - $location_options = array( - array( + $location_options = [ + [ 'field' => 'location', 'lat' => '51.260197', 'lon' => '4.402771', 'radius' => '1000', - ), - ); + ], + ]; $query = $this->buildSearch(NULL, [], NULL, FALSE) ->sort('location__distance'); $query->setOption('search_api_location', $location_options); - $query->setOption('search_api_facets', $facets); + $query->setOption('search_api_facets', $facets_options); $result = $query->execute(); - - $facets = $result->getExtraData('search_api_facets', array())['location__distance']; + $facets = $result->getExtraData('search_api_facets', [])['location__distance']; $expected = [ [ @@ -271,33 +269,32 @@ $this->assertEquals($expected, $facets, 'The correct location facets are returned'); - $facets = array(); - $facets['location__distance'] = array( + $facets_options['location__distance'] = [ 'field' => 'location__distance', 'limit' => 3, 'min_count' => 1, 'missing' => TRUE, - ); + ]; // Search between 100km and 1000km from Antwerp. - $location_options = array( - array( + $location_options = [ + [ 'field' => 'location', 'lat' => '51.260197', 'lon' => '4.402771', 'radius' => '1000', - ), - ); + ], + ]; $query = $this->buildSearch(NULL, [], NULL, FALSE) - ->addCondition('location', array('100', '1000'), 'BETWEEN') + ->addCondition('location',['100', '1000'], 'BETWEEN') ->sort('location__distance'); $query->setOption('search_api_location', $location_options); - $query->setOption('search_api_facets', $facets); + $query->setOption('search_api_facets', $facets_options); $result = $query->execute(); - $facets = $result->getExtraData('search_api_facets', array())['location__distance']; + $facets = $result->getExtraData('search_api_facets', [])['location__distance']; $expected = [ [ @@ -311,30 +308,22 @@ /** - * Tests the correct setup of the server backend. + * {@inheritdoc} */ - protected function checkServerBackend() { - // TODO: Implement checkServerBackend() method. - } + protected function checkServerBackend() {} /** - * Checks whether changes to the index's fields are picked up by the server. + * {@inheritdoc} */ - protected function updateIndex() { - // TODO: Implement updateIndex() method. - } + protected function updateIndex() {} /** - * Tests that a second server doesn't interfere with the first. + * {@inheritdoc} */ - protected function checkSecondServer() { - // TODO: Implement checkSecondServer() method. - } + protected function checkSecondServer() {} /** - * Tests whether removing the configuration again works as it should. + * {@inheritdoc} */ - protected function checkModuleUninstall() { - // TODO: Implement checkModuleUninstall() method. - } + protected function checkModuleUninstall() {} }