/** * Test parseRange functionality. * * @return void * @access public */ public function testParseRange() { // basic range test: $result = VuFindSolrUtils::parseRange("[1 TO 100]"); $this->assertEquals('1', $result['from']); $this->assertEquals('100', $result['to']); // test whitespace handling: $result = VuFindSolrUtils::parseRange("[1 TO 100]"); $this->assertEquals('1', $result['from']); $this->assertEquals('100', $result['to']); // test invalid ranges: $this->assertFalse(VuFindSolrUtils::parseRange('1 TO 100')); $this->assertFalse(VuFindSolrUtils::parseRange('[not a range to me]')); }
/** * Support method for getVisData() -- extract details from applied filters. * * @param array $filters Current filter list * * @return array * @access private */ private function _processDateFacets($filters) { $result = array(); foreach ($this->_dateFacets as $current) { $from = $to = ''; if (isset($filters[$current])) { foreach ($filters[$current] as $filter) { if ($range = VuFindSolrUtils::parseRange($filter)) { $from = $range['from'] == '*' ? '' : $range['from']; $to = $range['to'] == '*' ? '' : $range['to']; break; } } } $result[$current] = array($from, $to); } return $result; }
/** * Support method for getVisData() -- extract details from applied filters. * * @param array $filters Current filter list * * @return array * @access protected */ protected function processDateFacets($filters) { $result = array(); foreach ($this->dateFacets as $current) { $from = $to = ''; if (isset($filters[$current])) { foreach ($filters[$current] as $filter) { if ($range = VuFindSolrUtils::parseRange($filter)) { $from = $range['from'] == '*' ? '' : $range['from']; $to = $range['to'] == '*' ? '' : $range['to']; break; } } } $result[$current] = array($from, $to); $result[$current]['label'] = $this->searchObject->getFacetLabel($current); } return $result; }
/** * Get the current settings for the date range facet, if it is set: * * @param object $savedSearch Saved search object (false if none) * * @return array Date range: Key 0 = from, Key 1 = to. * @access private */ private function _getDateRangeSettings($savedSearch = false) { // Default to blank strings: $from = $to = ''; // Check to see if there is an existing range in the search object: if ($savedSearch) { $filters = $savedSearch->getFilters(); if (isset($filters['main_date_str'])) { foreach ($filters['main_date_str'] as $current) { if ($range = VuFindSolrUtils::parseRange($current)) { $from = $range['from'] == '*' ? '' : $range['from']; $to = $range['to'] == '*' ? '' : $range['to']; $savedSearch->removeFilter('main_date_str:' . $current); break; } } } } // Send back the settings: return array($from, $to); }
/** * Returns the stored list of facets for the last search * * @param array $filter Array of field => on-screen description listing * all of the desired facet fields; set to null to get all configured values. * @param bool $expandingLinks If true, we will include expanding URLs (i.e. * get all matches for a facet, not just a limit to the current search) in the * return array. * * @return array Facets data arrays * @access public */ public function getFacetList($filter = null, $expandingLinks = false) { // If there is no filter, we'll use all facets as the filter: if (is_null($filter)) { $filter = $this->facetConfig; } // Start building the facet list: $list = array(); // If we have no facets to process, give up now if (!isset($this->indexResult['facet_counts']['facet_fields']) && !isset($this->indexResult['facet_counts']['facet_queries']) || !is_array($this->indexResult['facet_counts']['facet_fields']) && !is_array($this->indexResult['facet_counts']['facet_queries'])) { return $list; } $translationPrefix = isset($this->facetTranslationPrefix) ? $this->facetTranslationPrefix : ''; // Loop through every field returned by the result set $validFields = array_keys($filter); foreach ($this->indexResult['facet_counts']['facet_fields'] as $field => $data) { // Skip filtered fields and empty arrays: if (!in_array($field, $validFields) || count($data) < 1) { continue; } // Initialize the settings for the current field $list[$field] = array(); // Add the on-screen label $list[$field]['label'] = $filter[$field]; // This tells us whether there is a selection made in the facet group // Useful so we will not have to loop through the list later on $list[$field]['isApplied'] = false; // Build our array of values for this field $list[$field]['list'] = array(); // Should we translate values for the current facet? $translate = in_array($field, $this->translatedFacets); // Hierarchical facets $hierarchical = $this->getFacetSetting('SpecialFacets', 'hierarchical'); // Loop through values: foreach ($data as $facet) { // Initialize the array of data about the current facet: $currentSettings = array(); if ($translate) { if (is_array($hierarchical) && in_array($field, $hierarchical)) { $facetValue = $facet[0]; // Remove trailing slash $facetValue = rtrim($facetValue, '/'); $translatedValue = translate(array('prefix' => $translationPrefix, 'text' => $facetValue)); if ($translatedValue == $facetValue) { // Didn't find a translation, so let's just clean up the display string a bit $translatedValue = end(explode('/', $facetValue)); } $currentSettings['value'] = $translatedValue; } else { $currentSettings['value'] = translate(array('prefix' => $translationPrefix, 'text' => $facet[0])); } } else { $currentSettings['value'] = $facet[0]; } $currentSettings['untranslated'] = $facet[0]; $currentSettings['count'] = $facet[1]; $currentSettings['isApplied'] = false; $currentSettings['url'] = $this->renderLinkWithFilter("{$field}:" . $facet[0]); // If we want to have expanding links (all values matching the // facet) in addition to limiting links (filter current search // with facet), do some extra work: if ($expandingLinks) { $currentSettings['expandUrl'] = $this->getExpandingFacetLink($field, $facet[0]); } // Is this field a current filter? // preg_replace removes the filter exclude if any $rawField = preg_replace('/{!ex=.+}/', '', $field); if (in_array($rawField, array_keys($this->filterList))) { // and is this value a selected filter? if (in_array($facet[0], $this->filterList[$rawField])) { $currentSettings['isApplied'] = true; $list[$field]['isApplied'] = true; } } // Store the collected values: $list[$field]['list'][] = $currentSettings; } } foreach ($this->indexResult['facet_counts']['facet_queries'] as $key => $count) { list($field, $query) = explode(':', $key, 2); if (!in_array($field, $validFields)) { continue; } // Initialize the settings for the current field if (!isset($list[$field])) { $list[$field] = array(); // Add the on-screen label $list[$field]['label'] = $this->pseudoFacets[$field]; // Build our array of values for this field $list[$field]['list'] = array(); } // Initialize the array of data about the current facet: $currentSettings = array(); $range = VuFindSolrUtils::parseRange($query); $currentSettings['value'] = $currentSettings['untranslated'] = $query; $currentSettings['count'] = $count; $currentSettings['isApplied'] = false; $filter = $this->buildDateRangeFilter($field, $range['from'], $range['to']); $currentSettings['url'] = $this->renderLinkWithFilter($filter); // If we want to have expanding links (all values matching the // facet) in addition to limiting links (filter current search // with facet), do some extra work: if ($expandingLinks) { $currentSettings['expandUrl'] = $this->getExpandingFacetLink($field, $facet[0]); } // Is this field a current filter? if (in_array($field, array_keys($this->filterList))) { // and is this value a selected filter? if (in_array($facet[0], $this->filterList[$field])) { $currentSettings['isApplied'] = true; } } // Store the collected values: $list[$field]['list'][] = $currentSettings; } // Sort configured facets alphabetically $alphaSorted = $this->getFacetSetting('Results_Settings', 'hierarchicalFacetSortOptions'); if (is_array($alphaSorted)) { foreach ($alphaSorted as $alphaFacet => $mode) { if (isset($list[$alphaFacet])) { if ($mode == 'all' || $mode == 'top' && isset($list[$alphaFacet]['list'][0]['untranslated'][0]) && $list[$alphaFacet]['list'][0]['untranslated'][0] == '0') { usort($list[$alphaFacet]['list'], function ($a, $b) { return strtolower($a['value']) > strtolower($b['value']); }); } } } } return $list; }
/** * 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 : ''; }
/** * Support method for getVisData() -- extract details from applied filters. * * @param array $filters Current filter list * * @return array * @access protected */ protected function processDateFacets($filters) { $result = array(); $from = $to = ''; if (isset($filters[$this->filterField])) { foreach ($filters[$this->filterField] as $filter) { if ($range = VuFindSolrUtils::parseSpatialDateRange($filter, $this->searchObject->getSpatialDateRangeFilterType())) { $startDate = new DateTime("@{$range['from']}"); $endDate = new DateTime("@{$range['to']}"); $from = $startDate->format('Y'); $to = $endDate->format('Y'); break; } } } $result[$this->filterField] = array($from, $to); $result[$this->filterField]['label'] = $this->searchObject->getFacetLabel($this->filterField); return $result; }
/** * Support function to get publication date range. Return string in the form * "YYYY-YYYY" * * @param string $field Name of filter field to check for date limits. * * @return string * @access protected */ protected function getPublishedDates($field) { // Try to extract range details from request parameters or SearchObject: if (isset($_REQUEST[$field . 'from']) && isset($_REQUEST[$field . 'to'])) { $range = array('from' => $_REQUEST[$field . 'from'], 'to' => $_REQUEST[$field . 'to']); } else { if (is_object($this->_searchObject)) { $currentFilters = $this->_searchObject->getFilters(); if (isset($currentFilters[$field][0])) { $range = VuFindSolrUtils::parseRange($currentFilters[$field][0]); } } } // Normalize range if we found one: if (isset($range)) { if (empty($range['from']) || $range['from'] == '*') { $range['from'] = 0; } if (empty($range['to']) || $range['to'] == '*') { $range['to'] = date('Y') + 1; } return $range['from'] . '-' . $range['to']; } // No range found? Return empty string: return ''; }
/** * 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; }
/** * Convert spatial date range filter to displayable string * * @param string $filter Spatial date range filter * * @return string Resulting display string */ protected function spatialDateRangeFilterToString($filter) { $range = VuFindSolrUtils::parseSpatialDateRange($filter, $this->spatialDateRangeFilterType); if ($range === false) { return $filter; } $startDate = new DateTime("@{$range['from']}"); $endDate = new DateTime("@{$range['to']}"); if ($startDate->format('m') == 1 && $startDate->format('d') == 1 && $endDate->format('m') == 12 && $endDate->format('d') == 31) { return $startDate->format('Y') . ' - ' . $endDate->format('Y'); } $date = new VuFindDate(); return $date->convertToDisplayDate('U', $range['from']) . ' - ' . $date->convertToDisplayDate('U', $range['to']); }
/** * Execute a search. * * @param array $query The search terms from the Search Object * @param array $filterList 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 string $sortBy The value to be used by for sorting * @param array $facets The facets to include (null for defaults) * @param bool $returnErr On fatal error, should we fail outright (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 query($query, $filterList = null, $start = 1, $limit = 20, $sortBy = null, $facets = null, $returnErr = false) { // Query String Parameters $options = array('s.q' => $this->_buildQuery($query)); // TODO: add configurable authentication mechanisms to identify authorized // users and switch this to "authenticated" when appropriate (VUFIND-475): $options['s.role'] = 'none'; // Which facets should we include in results? Set defaults if not provided. if (!$facets) { $facets = array_keys($this->_config['Facets']); } // Default to "holdings only" unless a different setting is found in the // filters: $options['s.ho'] = 'true'; // Which filters should be applied to our query? $options['s.fvf'] = array(); $options['s.rf'] = array(); if (!empty($filterList)) { // Loop through all filters and add appropriate values to request: foreach ($filterList as $filterArray) { foreach ($filterArray as $filter) { $safeValue = $this->_escapeParam($filter['value']); // Special case -- "holdings only" is a separate parameter from // other facets. if ($filter['field'] == 'holdingsOnly') { $options['s.ho'] = $safeValue; } else { if ($filter['field'] == 'excludeNewspapers') { // Special case -- support a checkbox for excluding // newspapers: $options['s.fvf'][] = "ContentType,Newspaper Article,true"; } else { if ($range = VuFindSolrUtils::parseRange($filter['value'])) { // Special case -- range query (translate [x TO y] syntax): $from = $this->_escapeParam($range['from']); $to = $this->_escapeParam($range['to']); $options['s.rf'][] = "{$filter['field']},{$from}:{$to}"; } else { // Standard case: $options['s.fvf'][] = "{$filter['field']},{$safeValue}"; } } } } } } // Special case -- if user filtered down to newspapers AND excluded them, // we can't possibly have any results: if (in_array('ContentType,Newspaper Article,true', $options['s.fvf']) && in_array('ContentType,Newspaper Article', $options['s.fvf'])) { return array('recordCount' => 0, 'documents' => array()); } if (is_array($facets)) { $options['s.ff'] = array(); foreach ($facets as $facet) { // See if parameters are included as part of the facet name; // if not, override them with defaults. $parts = explode(',', $facet); $facetName = $parts[0]; $facetMode = isset($parts[1]) ? $parts[1] : 'and'; $facetPage = isset($parts[2]) ? $parts[2] : 1; if (isset($parts[3])) { $facetLimit = $parts[3]; } else { $facetLimit = isset($this->_config['Facet_Settings']['facet_limit']) ? $this->_config['Facet_Settings']['facet_limit'] : 30; } $facetParams = "{$facetMode},{$facetPage},{$facetLimit}"; // Special case -- we can't actually facet on PublicationDate, // but we need it in the results to display range controls. If // we encounter this field, set a flag indicating that we need // to inject it into the results for proper display later: if ($facetName == 'PublicationDate') { $injectPubDate = true; } else { $options['s.ff'][] = "{$facetName},{$facetParams}"; } } } if (isset($sortBy)) { $options['s.sort'] = $sortBy; } $options['s.ps'] = $limit; $options['s.pn'] = $start; // Define Highlighting if ($this->_highlight) { $options['s.hl'] = 'true'; $options['s.hs'] = '{{{{START_HILITE}}}}'; $options['s.he'] = '{{{{END_HILITE}}}}'; } else { $options['s.hl'] = 'false'; $options['s.hs'] = $options['s.he'] = ''; } if ($this->debug) { echo '<pre>Query: '; print_r($options); echo "</pre>\n"; } $result = $this->_call($options); if (PEAR::isError($result)) { if ($returnErr) { return array('recordCount' => 0, 'documents' => array(), 'errors' => $result->getMessage()); } else { PEAR::raiseError($result); } } // Add a fake "PublicationDate" facet if flagged earlier; this is necessary // in order to display the date range facet control in the interface. if (isset($injectPubDate) && $injectPubDate) { $result['facetFields'][] = array('fieldName' => 'PublicationDate', 'displayName' => 'PublicationDate', 'counts' => array()); } return $result; }