/** * Test capitalizeBooleans functionality. * * @return void * @access public */ public function testCapitalizeBooleans() { // Set up an array of expected inputs and outputs: // @codingStandardsIgnoreStart $tests = array(array('this not that', 'this NOT that'), array('this and that', 'this AND that'), array('this or that', 'this OR that'), array('apples and oranges (not that)', 'apples AND oranges (NOT that)'), array('"this not that"', '"this not that"'), array('"this and that"', '"this and that"'), array('"this or that"', '"this or that"'), array('"apples and oranges (not that)"', '"apples and oranges (not that)"'), array('this AND that', 'this AND that'), array('and and and', 'and AND and'), array('andornot noted andy oranges', 'andornot noted andy oranges'), array('(this or that) and (apples not oranges)', '(this OR that) AND (apples NOT oranges)'), array('this aNd that', 'this AND that'), array('this nOt that', 'this NOT that')); // @codingStandardsIgnoreEnd // Test all the operations: foreach ($tests as $current) { $this->assertEquals(VuFindSolrUtils::capitalizeBooleans($current[0]), $current[1]); } }
/** * Build Query string from search parameters * * @param array $search An array of search parameters * * @return string The query * @access protected */ protected function buildQuery($search) { $groups = array(); $excludes = array(); if (is_array($search)) { $query = ''; foreach ($search as $params) { // Advanced Search if (isset($params['group'])) { $thisGroup = array(); // Process each search group foreach ($params['group'] as $group) { // Build this group individually as a basic search $thisGroup[] = $this->buildQuery(array($group)); } // Is this an exclusion (NOT) group or a normal group? if ($params['group'][0]['bool'] == 'NOT') { $excludes[] = join(" OR ", $thisGroup); } else { $groups[] = join(" " . $params['group'][0]['bool'] . " ", $thisGroup); } } // Basic Search if (isset($params['lookfor']) && $params['lookfor'] != '') { // Clean and validate input -- note that index may be in a // different field depending on whether this is a basic or // advanced search. $lookfor = $params['lookfor']; if (isset($params['field'])) { $index = $params['field']; } else { if (isset($params['index'])) { $index = $params['index']; } else { $index = 'AllFields'; } } // Force boolean operators to uppercase if we are in a // case-insensitive mode: if (!$this->caseSensitiveBooleans) { $lookfor = VuFindSolrUtils::capitalizeBooleans($lookfor); } // Prepend the index name, unless it's the special "AllFields" // index: if ($index != 'AllFields') { $query .= "{$index}=({$lookfor})"; } else { $query .= "WRD=({$lookfor})"; } } } } // Put our advanced search together if (count($groups) > 0) { $query = "(" . join(") " . $search[0]['join'] . " (", $groups) . ")"; } // and concatenate exclusion after that if (count($excludes) > 0) { $query .= " NOT ((" . join(") OR (", $excludes) . "))"; } // Ensure we have a valid query to this point return isset($query) ? $query : ''; }
/** * Execute a search. * * @param string $query The search query * @param string $handler The Query Handler to use (null for default) * @param array $filter The fields and values to filter results on * @param string $start The record to start with * @param string $limit The amount of records to return * @param array $facet An array of faceting options * @param string $spell Phrase to spell check * @param string $dictionary Spell check dictionary to use * @param string $sort Field name to use for sorting * @param string $fields A list of fields to be returned * @param string $method Method to use for sending request (GET/POST) * @param bool $returnSolrError Fail outright on syntax error (false) or * treat it as an empty result set with an error key set (true)? * * @throws object PEAR Error * @return array An array of query results * @access public */ public function search($query, $handler = null, $filter = null, $start = 0, $limit = 20, $facet = null, $spell = '', $dictionary = null, $sort = null, $fields = null, $method = HTTP_REQUEST_METHOD_POST, $returnSolrError = false) { // Query String Parameters $options = array('q' => $query, 'rows' => $limit, 'start' => $start, 'indent' => 'yes'); // Force sort by title_sort for empty search sorted by score if ($limit > 0 && $query == '*:*' && (empty($sort) || $sort == 'score desc')) { $sort = 'title asc'; } // Add Sorting if ($sort && !empty($sort)) { // There may be multiple sort options (ranked, with tie-breakers); // process each individually, then assemble them back together again: $sortParts = explode(',', $sort); for ($x = 0; $x < count($sortParts); $x++) { $sortParts[$x] = $this->_normalizeSort($sortParts[$x]); } $options['sort'] = implode(',', $sortParts); } // Determine which handler to use if (!$this->isAdvanced($query)) { $ss = is_null($handler) ? null : $this->_getSearchSpecs($handler, $query); // Is this a Dismax search? if (isset($ss['DismaxFields'])) { // Specify the fields to do a Dismax search on: $options['qf'] = implode(' ', $ss['DismaxFields']); // Specify the default dismax search handler so we can use any // global settings defined by the user: $options['qt'] = 'dismax'; // Load any custom Dismax parameters from the YAML search spec file: if (isset($ss['DismaxParams']) && is_array($ss['DismaxParams'])) { foreach ($ss['DismaxParams'] as $current) { // The way we process the current parameter depends on // whether or not we have previously encountered it. If // we have multiple values for the same parameter, we need // to turn its entry in the $options array into a subarray; // otherwise, one-off parameters can be safely represented // as single values. if (isset($options[$current[0]])) { if (!is_array($options[$current[0]])) { $options[$current[0]] = array($options[$current[0]]); } $options[$current[0]][] = $current[1]; } else { $options[$current[0]] = $current[1]; } } } // Apply search-specific filters if necessary: if (isset($ss['FilterQuery'])) { if (is_array($filter)) { $filter[] = $ss['FilterQuery']; } else { $filter = array($ss['FilterQuery']); } } } else { // Not DisMax... but if we have a handler set, we may still need // to build a query using a setting in the YAML search specs or a // simple field name: if (!empty($handler)) { $options['q'] = $this->_buildQueryComponent($handler, $query); } } } else { // Force boolean operators to uppercase if we are in a case-insensitive // mode: if (!$this->_caseSensitiveBooleans) { $query = VuFindSolrUtils::capitalizeBooleans($query); } // Adjust range operators if we are in a case-insensitive mode: if (!$this->_caseSensitiveRanges) { $query = VuFindSolrUtils::capitalizeRanges($query); } // Process advanced search -- if a handler was specified, let's see // if we can adapt the search to work with the appropriate fields. if (!empty($handler)) { $options['q'] = $this->_buildAdvancedQuery($handler, $query); // If highlighting is enabled, we only want to use the inner query // for highlighting; anything added outside of this is a boost and // should be ignored for highlighting purposes! if ($this->_highlight) { $options['hl.q'] = $this->_buildAdvancedInnerQuery($handler, $query); } } } // Limit Fields if ($fields) { $options['fl'] = $fields; } else { // This should be an explicit list $options['fl'] = '*,score'; } // Build Facet Options if ($facet && !empty($facet['field'])) { $options['facet'] = 'true'; $options['facet.mincount'] = 1; $options['facet.limit'] = isset($facet['limit']) ? $facet['limit'] : null; unset($facet['limit']); $options['facet.field'] = isset($facet['field']) ? $facet['field'] : null; unset($facet['field']); if (isset($facet['prefix'])) { if (is_array($facet['prefix'])) { foreach ($facet['prefix'] as $name => $prefix) { $options["f.{$name}.facet.prefix"] = $prefix; // TODO: This is a kludge, maybe something better is // needed to indicate when we want more than the default // limit for hierarchical facets. $options["f.{$name}.facet.limit"] = 1000; } } else { $options['facet.prefix'] = $facet['prefix']; } } unset($facet['prefix']); $options['facet.sort'] = isset($facet['sort']) ? $facet['sort'] : 'count'; unset($facet['sort']); if (isset($facet['offset'])) { $options['facet.offset'] = $facet['offset']; unset($facet['offset']); } if (isset($facet['query'])) { $options['facet.query'] = $facet['query']; unset($facet['query']); } foreach ($facet as $param => $value) { $options[$param] = $value; } } // Don't use the filters for an id query if ($handler != 'ids') { if ($this->_mergedRecords) { // Filter out merged children by default if (!isset($filter)) { $filter = array(); } $filter[] = '-merged_child_boolean:TRUE'; } elseif ($this->_mergedRecords !== null) { // Filter out merged records by default if (!isset($filter)) { $filter = array(); } $filter[] = '-merged_boolean:TRUE'; } if ($this->_hideComponentParts) { // Filter out component parts by default if (!isset($filter)) { $filter = array(); } $filter[] = '-hidden_component_boolean:TRUE'; } } // Build Filter Query $this->_mergeBuildingPriority = array(); if (is_array($filter) && count($filter)) { // Change 'online_boolean' filter to 'online_str_mv' // if sources contain merged records (i.e. if deduplication is enabled). if (($pos = array_search('online_boolean:"1"', $filter)) !== false) { $searchSettings = getExtraConfigArray('searches'); if (isset($searchSettings['Records']['merged_records']) && $searchSettings['Records']['merged_records']) { if (isset($searchSettings['Records']['sources']) && ($sources = $searchSettings['Records']['sources']) !== '') { $tmp = array(); foreach (explode(',', $sources) as $source) { $tmp[] = "\"{$source}\""; } $filter[$pos] = 'online_str_mv:(' . implode(' OR ', $tmp) . ')'; } else { $filter[$pos] = 'online_str_mv:*'; } } } $options['fq'] = $filter; foreach ($filter as $f) { if (strncmp($f, 'building:', 9) == 0) { // Assume we have a facet hierarchy... $fullHierarchy = substr($f, 12, -1); foreach (explode('/', $fullHierarchy) as $part) { $this->_mergeBuildingPriority[] = $part; } } } } // Enable Spell Checking if ($spell != '') { $options['spellcheck'] = 'true'; $options['spellcheck.q'] = $spell; if ($dictionary != null) { $options['spellcheck.dictionary'] = $dictionary; } } // Enable highlighting if ($this->_highlight) { $options['hl'] = 'true'; $options['hl.fl'] = '*'; $options['hl.simple.pre'] = '{{{{START_HILITE}}}}'; $options['hl.simple.post'] = '{{{{END_HILITE}}}}'; } // Hack to make it possible to see the search debug information if (isset($_REQUEST['debugSolrQuery'])) { $options['debugQuery'] = 'true'; } if ($this->debug) { echo '<pre>Search options: ' . print_r($options, true) . "\n"; if ($filter) { echo "\nFilterQuery: "; foreach ($filter as $filterItem) { echo " {$filterItem}"; } } if ($sort) { echo "\nSort: " . $options['sort']; } echo "</pre>\n"; } $result = $this->_select($method, $options, $returnSolrError); if (PEAR::isError($result)) { PEAR::raiseError($result); } return $result; }