431 lines
15 KiB
PHP
431 lines
15 KiB
PHP
<?php
|
|
|
|
namespace Drupal\advanced_search;
|
|
|
|
use Drupal\block\Entity\Block;
|
|
use Drupal\Core\EventSubscriber\MainContentViewSubscriber;
|
|
use Drupal\Core\Form\FormBuilderInterface;
|
|
use Drupal\Core\Url;
|
|
use Drupal\advanced_search\Form\SettingsForm;
|
|
use Drupal\advanced_search\Plugin\Block\AdvancedSearchBlock;
|
|
use Drupal\search_api\Query\QueryInterface as DrupalQueryInterface;
|
|
use Drupal\views\ViewExecutable;
|
|
use Solarium\Core\Query\QueryInterface as SolariumQueryInterface;
|
|
use Symfony\Component\HttpFoundation\Request;
|
|
use Drupal\search_api_solr\Utility\Utility as SearchAPISolrUtility;
|
|
|
|
/**
|
|
* Alter current search query / view from using URL parameters.
|
|
*/
|
|
class AdvancedSearchQuery {
|
|
|
|
use GetConfigTrait;
|
|
|
|
// User can set this configuration for the module.
|
|
const DEFAULT_QUERY_PARAM = 'a';
|
|
const DEFAULT_RECURSE_PARAM = 'r';
|
|
|
|
/**
|
|
* The query parameter is how terms are passed to the query.
|
|
*
|
|
* @var string
|
|
*/
|
|
protected $queryParameter;
|
|
|
|
/**
|
|
* The recurse parameter indicates the search should be recursive or not.
|
|
*
|
|
* @var string
|
|
*/
|
|
protected $recurseParameter;
|
|
|
|
/**
|
|
* Constructs a FacetBlockAjaxController object.
|
|
*
|
|
* @param string $query_parameter
|
|
* The field to search against.
|
|
* @param string $recurse_parameter
|
|
* The field that signifies the search should be recursive.
|
|
*/
|
|
public function __construct(string $query_parameter = self::DEFAULT_QUERY_PARAM, string $recurse_parameter = self::DEFAULT_RECURSE_PARAM) {
|
|
$this->queryParameter = $query_parameter;
|
|
$this->recurseParameter = $recurse_parameter;
|
|
}
|
|
|
|
/**
|
|
* Gets the query parameter to use that stores the search terms.
|
|
*
|
|
* @return string
|
|
* The query parameter to use that stores the search terms.
|
|
*/
|
|
public static function getQueryParameter() {
|
|
return self::getConfig(SettingsForm::SEARCH_QUERY_PARAMETER, self::DEFAULT_QUERY_PARAM);
|
|
}
|
|
|
|
/**
|
|
* Gets the query parameter to use that stores the search terms.
|
|
*
|
|
* @return string
|
|
* The recurse parameter used to indicate that the search should be
|
|
* recursive.
|
|
*/
|
|
public static function getRecurseParameter() {
|
|
return self::getConfig(SettingsForm::SEARCH_RECURSIVE_PARAMETER, self::DEFAULT_RECURSE_PARAM);
|
|
}
|
|
|
|
/**
|
|
* Extracts a list of AdvancedSearchQueryTerms from the given request.
|
|
*
|
|
* @param \Symfony\Component\HttpFoundation\Request $request
|
|
* The request to parse terms from.
|
|
*
|
|
* @return \Drupal\advanced_search\AdvancedSearchQueryTerm[]
|
|
* A list of search terms.
|
|
*/
|
|
public function getTerms(Request $request) {
|
|
$terms = [];
|
|
if ($request->query->has($this->queryParameter)) {
|
|
$query_params = $request->query->get($this->queryParameter);
|
|
if (is_array($query_params)) {
|
|
foreach ($query_params as $params) {
|
|
$terms[] = AdvancedSearchQueryTerm::fromQueryParams($params);
|
|
}
|
|
}
|
|
}
|
|
return array_filter($terms);
|
|
}
|
|
|
|
/**
|
|
* Checks if the query should recursively include sub-collections.
|
|
*
|
|
* @param \Symfony\Component\HttpFoundation\Request $request
|
|
* The request to parse.
|
|
*
|
|
* @return bool
|
|
* TRUE if the search should recurse FALSE otherwise.
|
|
*/
|
|
public function shouldRecurse(Request $request) {
|
|
if ($request->query->has($this->recurseParameter)) {
|
|
$recurse_param = $request->query->get($this->recurseParameter);
|
|
return filter_var($recurse_param, FILTER_VALIDATE_BOOLEAN);
|
|
}
|
|
return FALSE;
|
|
}
|
|
|
|
/**
|
|
* Checks if the all of the given terms are negations or not.
|
|
*
|
|
* @param \Drupal\advanced_search\AdvancedSearchQueryTerm[] $terms
|
|
* The terms to search for.
|
|
*
|
|
* @return bool
|
|
* TRUE if all terms are to be excluded otherwise FALSE.
|
|
*/
|
|
protected function negativeQuery(array $terms) {
|
|
foreach ($terms as $term) {
|
|
if ($term->getInclude()) {
|
|
return FALSE;
|
|
}
|
|
}
|
|
return TRUE;
|
|
}
|
|
|
|
/**
|
|
* Alters the given query using search terms provided in the given request.
|
|
*
|
|
* @param \Symfony\Component\HttpFoundation\Request $request
|
|
* The request to parse terms from.
|
|
* @param \Solarium\Core\Query\QueryInterface $solarium_query
|
|
* The solr query to modify.
|
|
* @param \Drupal\search_api\Query\QueryInterface $search_api_query
|
|
* The search api query from which the solr query was build.
|
|
*/
|
|
public function alterQuery(Request $request, SolariumQueryInterface &$solarium_query, DrupalQueryInterface $search_api_query) {
|
|
// Only apply if a Advanced Search Query was made.
|
|
$terms = $this->getTerms($request);
|
|
if (!empty($terms)) {
|
|
$index = $search_api_query->getIndex();
|
|
/** @var \Drupal\search_api_solr\Plugin\search_api\backend\SearchApiSolrBackend $backend */
|
|
$backend = $index->getServerInstance()->getBackend();
|
|
$language_ids = $search_api_query->getLanguages();
|
|
$field_mapping = $backend->getSolrFieldNamesKeyedByLanguage($language_ids, $index);
|
|
|
|
// Disable for Lucene and wildcard
|
|
// $q[] = "{!boost b=boost_document}";
|
|
// Create a flag for active/inactive dismax.
|
|
$config = \Drupal::config(SettingsForm::CONFIG_NAME);
|
|
$isDismax = $config->get(SettingsForm::EDISMAX_SEARCH_FLAG);
|
|
if (!isset($isDismax)) {
|
|
$isDismax = TRUE;
|
|
}
|
|
$isSearchAllFields = FALSE;
|
|
$fields_list = [];
|
|
|
|
if (!$isDismax) {
|
|
// To support negative queries we must first bring in all documents.
|
|
$q[] = $this->negativeQuery($terms) ? "*:*" : "";
|
|
}
|
|
|
|
$term = array_shift($terms);
|
|
$q[] = $term->toSolrQuery($field_mapping);
|
|
|
|
// New.
|
|
$fields_list[] = $term->toSolrFields($field_mapping);
|
|
|
|
// Set edismax is enabled if the field set to "all".
|
|
if ($term->getField() === "all") {
|
|
$isSearchAllFields = TRUE;
|
|
|
|
}
|
|
|
|
// For multiple conditions.
|
|
foreach ($terms as $term) {
|
|
$q[] = $term->getConjunction();
|
|
$q[] = $term->toSolrQuery($field_mapping);
|
|
|
|
// New.
|
|
$fields_list[] = $term->toSolrFields($field_mapping);
|
|
|
|
// Set dismax is enabled if the field set to "all".
|
|
if ($term->getField() === "all") {
|
|
$isSearchAllFields = TRUE;
|
|
|
|
}
|
|
|
|
}
|
|
$q = implode(' ', $q);
|
|
|
|
// Limit extra processing if Luncene Search is enable.
|
|
if ($isDismax) {
|
|
|
|
if ((strpos($q, "*") !== FALSE || strpos($q, "?") !== FALSE)) {
|
|
// If the query string contain '*','?',a single world,enable wildcard.
|
|
$tmp = str_replace('"', "", trim($q));
|
|
$query_fields = [];
|
|
|
|
if ($isSearchAllFields) {
|
|
foreach ($field_mapping as $key => $field) {
|
|
foreach ($field as $f => $item) {
|
|
// bs_ are boolean fields, do not work well with text search.
|
|
if (substr($item, 0, 3) !== "bs_"
|
|
&& !in_array($item, ['score', 'random', 'boost_document'])
|
|
&& ((strpos($item, "sm_") === 0)
|
|
|| (strpos($item, "tm_") === 0)
|
|
|| (strpos($item, "sort_ss_") === 0)
|
|
|| (strpos($item, "ts_") === 0)
|
|
|| (strpos($item, "ss_") === 0)
|
|
)) {
|
|
array_push($query_fields, '(' . $item . ':' . $tmp . ')');
|
|
}
|
|
}
|
|
}
|
|
}
|
|
else {
|
|
foreach ($fields_list as $f) {
|
|
$parts = explode(" ", $f);
|
|
foreach ($parts as $p) {
|
|
array_push($query_fields, '(' . $p . ':' . $tmp . ')');
|
|
}
|
|
}
|
|
}
|
|
$q = implode(" ", array_unique($query_fields));
|
|
}
|
|
else {
|
|
|
|
// Enable dismax search query option.
|
|
/** @var Solarium\QueryType\Select\Query\Component\DisMax $dismax */
|
|
$dismax = $solarium_query->getEDisMax();
|
|
$dismax->setQueryParser('edismax');
|
|
$query_fields = [];
|
|
|
|
if ($isSearchAllFields) {
|
|
foreach ($field_mapping as $key => $field) {
|
|
foreach ($field as $f => $item) {
|
|
// bs_ are boolean fields, do not work well with text search.
|
|
if (substr($item, 0, 3) !== "bs_") {
|
|
array_push($query_fields, $item);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
else {
|
|
$query_fields = $fields_list;
|
|
|
|
}
|
|
$query_fields = implode(" ", array_unique($query_fields));
|
|
$dismax->setQueryFields($query_fields);
|
|
}
|
|
}
|
|
|
|
if ($backend->getConfiguration()['highlight_data']) {
|
|
// Just highlight string and text fields to avoid Solr exceptions.
|
|
$highlighted_fields = array_filter(array_unique($fields_list), function ($v) {
|
|
return preg_match('/^t.*?[sm]_/', $v) || preg_match('/^s[sm]_/', $v);
|
|
});
|
|
|
|
if (empty($highlighted_fields)) {
|
|
$highlighted_fields = ['*'];
|
|
}
|
|
|
|
$this->setHighlighting($solarium_query, $search_api_query, $highlighted_fields);
|
|
|
|
// The Search API Highlight processor checks if the 'keys' field of
|
|
// the Search API Query is non-empty before creating an excerpt.
|
|
// Since we are getting the highlighting result from Solr instead
|
|
// of using the Search API processor to create one, we just need
|
|
// make this field non-empty.
|
|
$search_api_query->keys("advanced search");
|
|
}
|
|
|
|
$solarium_query->setQuery($q);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Alters the given view to be recursive if applicable.
|
|
*
|
|
* @param \Symfony\Component\HttpFoundation\Request $request
|
|
* The request to parse terms from.
|
|
* @param \Drupal\views\ViewExecutable $view
|
|
* The view to modify.
|
|
* @param string $display_id
|
|
* The view display to potentially alter.
|
|
*/
|
|
public function alterView(Request $request, ViewExecutable $view, $display_id) {
|
|
$views = Utilities::getAdvancedSearchViewDisplays();
|
|
// Only specify contextual filters for views which the advanced search
|
|
// blocks are derived from.
|
|
$block_id = array_search([$view->id(), $display_id], $views);
|
|
if ($block_id !== FALSE) {
|
|
$block = Block::load($block_id);
|
|
$settings = $block->get('settings');
|
|
// Ignore the immediate children contextual filter in the query to allow
|
|
// for recursive search.
|
|
if (isset($settings[AdvancedSearchBlock::SETTING_CONTEXTUAL_FILTER])) {
|
|
$display = $view->getDisplay();
|
|
$display_arguments = $display->getOption('arguments');
|
|
$immediate_children_contextual_filter = $settings[AdvancedSearchBlock::SETTING_CONTEXTUAL_FILTER];
|
|
$index = array_search($immediate_children_contextual_filter, array_keys($display_arguments));
|
|
if ($this->shouldRecurse($request)) {
|
|
// Change the argument to the exception value which should cause the
|
|
// contextual filter to be ignored.
|
|
$view->args[$index] = $display_arguments[$immediate_children_contextual_filter]['exception']['value'];
|
|
}
|
|
else {
|
|
// Explicitly set the default argument for AJAX requests.
|
|
// We need to restore the default as that functionality is currently
|
|
// broken. @see https://www.drupal.org/project/drupal/issues/3173778
|
|
//
|
|
// We fake the current request from the refer only to set the default
|
|
// argument in case it is build from the URL. If this is not an AJAX
|
|
// request this logic can be ignored.
|
|
if ($request->isXmlHttpRequest()) {
|
|
$view->initHandlers();
|
|
$request_stack = \Drupal::requestStack();
|
|
$refer = Request::create($request->server->get('HTTP_REFERER'));
|
|
$refer->getPathInfo();
|
|
$refer->attributes->add(\Drupal::getContainer()->get('router')->matchRequest($refer));
|
|
$request_stack->push($refer);
|
|
if (isset($view->argument[$immediate_children_contextual_filter])) {
|
|
$plugin = $view->argument[$immediate_children_contextual_filter]->getPlugin('argument_default');
|
|
if ($plugin) {
|
|
$view->args[$index] = $plugin->getArgument();
|
|
}
|
|
}
|
|
$request_stack->pop();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get query parameter for all search terms.
|
|
*
|
|
* @return \Drupal\Core\Url
|
|
* Url for the given request combined with search query parameters.
|
|
*/
|
|
public function toUrl(Request $request, array $terms, bool $recurse, $route = NULL) {
|
|
$query_params = $request->query->all();
|
|
if ($route) {
|
|
$url = Url::fromRoute($route);
|
|
// The form that built the url may use AJAX, but we are redirecting to a
|
|
// new page, so it should be disabled.
|
|
unset($query_params[FormBuilderInterface::AJAX_FORM_REQUEST]);
|
|
unset($query_params[MainContentViewSubscriber::WRAPPER_FORMAT]);
|
|
}
|
|
else {
|
|
$url = Url::createFromRequest($request);
|
|
}
|
|
unset($query_params[$this->queryParameter]);
|
|
foreach ($terms as $term) {
|
|
$query_params[$this->queryParameter][] = $term->toQueryParams();
|
|
}
|
|
if ($recurse) {
|
|
$query_params[$this->recurseParameter] = '1';
|
|
}
|
|
else {
|
|
unset($query_params[$this->recurseParameter]);
|
|
}
|
|
$url->setOptions(['query' => $query_params]);
|
|
return $url;
|
|
}
|
|
|
|
/**
|
|
* Sets the highlighting parameters.
|
|
*
|
|
* @param \Solarium\Core\Query\QueryInterface $solarium_query
|
|
* The Solarium select query object.
|
|
* @param \Drupal\search_api\Query\QueryInterface $search_api_query
|
|
* The query object.
|
|
* @param array $highlighted_fields
|
|
* (optional) The solr fields to be highlighted.
|
|
*/
|
|
protected function setHighlighting(SolariumQueryInterface $solarium_query, DrupalQueryInterface $search_api_query, array $highlighted_fields = []) {
|
|
$index = $search_api_query->getIndex();
|
|
$settings = SearchAPISolrUtility::getIndexSolrSettings($index);
|
|
$highlighter = $settings['highlighter'];
|
|
|
|
$hl = $solarium_query->getHighlighting();
|
|
$hl->setSimplePrefix('[HIGHLIGHT]');
|
|
$hl->setSimplePostfix('[/HIGHLIGHT]');
|
|
$hl->setSnippets($highlighter['highlight']['snippets']);
|
|
$hl->setFragSize($highlighter['highlight']['fragsize']);
|
|
$hl->setMergeContiguous($highlighter['highlight']['mergeContiguous']);
|
|
$hl->setRequireFieldMatch($highlighter['highlight']['requireFieldMatch']);
|
|
|
|
// Overwrite Solr default values only if required to have shorter request
|
|
// strings.
|
|
if (51200 != $highlighter['maxAnalyzedChars']) {
|
|
$hl->setMaxAnalyzedChars($highlighter['maxAnalyzedChars']);
|
|
}
|
|
if ('gap' !== $highlighter['fragmenter']) {
|
|
$hl->setFragmenter($highlighter['fragmenter']);
|
|
if ('regex' !== $highlighter['fragmenter']) {
|
|
$hl->setRegexPattern($highlighter['regex']['pattern']);
|
|
if (0.5 != $highlighter['regex']['slop']) {
|
|
$hl->setRegexSlop($highlighter['regex']['slop']);
|
|
}
|
|
if (10000 != $highlighter['regex']['maxAnalyzedChars']) {
|
|
$hl->setRegexMaxAnalyzedChars($highlighter['regex']['maxAnalyzedChars']);
|
|
}
|
|
}
|
|
}
|
|
if (!$highlighter['usePhraseHighlighter']) {
|
|
$hl->setUsePhraseHighlighter(FALSE);
|
|
}
|
|
if (!$highlighter['highlightMultiTerm']) {
|
|
$hl->setHighlightMultiTerm(FALSE);
|
|
}
|
|
if ($highlighter['preserveMulti']) {
|
|
$hl->setPreserveMulti(TRUE);
|
|
}
|
|
|
|
foreach ($highlighted_fields as $highlighted_field) {
|
|
// We must not set the fields at once using setFields() to not break
|
|
// the altered queries.
|
|
$hl->addField($highlighted_field);
|
|
}
|
|
}
|
|
|
|
}
|