public function __construct() { parent::__construct(); $this->init(); foreach ($this->getClasses() as $class => $options) { SearchVariant::with($class, $options['include_children'])->call('alterDefinition', $class, $this); } $this->buildDependancyList(); }
/** * Fulltextsearch module doesn't yet support facets very well, so I've just copied this function here so * we have access to the results. I'd prefer to modify it minimally so we can eventually get rid of it * once they add faceting or hooks to get directly at the returned response. * * @param SearchQuery $query * @param integer $offset * @param integer $limit * @param Array $params Extra request parameters passed through to Solr * @param array $facetSpec - Added for ShopSearch so we can process the facets * @return ArrayData Map with the following keys: * - 'Matches': ArrayList of the matched object instances */ public function search(SearchQuery $query, $offset = -1, $limit = -1, $params = array(), $facetSpec = array()) { $service = $this->getService(); SearchVariant::with(count($query->classes) == 1 ? $query->classes[0]['class'] : null)->call('alterQuery', $query, $this); $q = array(); $fq = array(); // Build the search itself foreach ($query->search as $search) { $text = $search['text']; preg_match_all('/"[^"]*"|\\S+/', $text, $parts); $fuzzy = $search['fuzzy'] ? '~' : ''; foreach ($parts[0] as $part) { $fields = isset($search['fields']) ? $search['fields'] : array(); if (isset($search['boost'])) { $fields = array_merge($fields, array_keys($search['boost'])); } if ($fields) { $searchq = array(); foreach ($fields as $field) { $boost = isset($search['boost'][$field]) ? '^' . $search['boost'][$field] : ''; $searchq[] = "{$field}:" . $part . $fuzzy . $boost; } $q[] = '+(' . implode(' OR ', $searchq) . ')'; } else { $q[] = '+' . $part . $fuzzy; } } } // Filter by class if requested $classq = array(); foreach ($query->classes as $class) { if (!empty($class['includeSubclasses'])) { $classq[] = 'ClassHierarchy:' . $class['class']; } else { $classq[] = 'ClassName:' . $class['class']; } } if ($classq) { $fq[] = '+(' . implode(' ', $classq) . ')'; } // Filter by filters foreach ($query->require as $field => $values) { $requireq = array(); foreach ($values as $value) { if ($value === SearchQuery::$missing) { $requireq[] = "(*:* -{$field}:[* TO *])"; } elseif ($value === SearchQuery::$present) { $requireq[] = "{$field}:[* TO *]"; } elseif ($value instanceof SearchQuery_Range) { $start = $value->start; if ($start === null) { $start = '*'; } $end = $value->end; if ($end === null) { $end = '*'; } $requireq[] = "{$field}:[{$start} TO {$end}]"; } else { $requireq[] = $field . ':"' . $value . '"'; } } $fq[] = '+(' . implode(' ', $requireq) . ')'; } foreach ($query->exclude as $field => $values) { $excludeq = array(); $missing = false; foreach ($values as $value) { if ($value === SearchQuery::$missing) { $missing = true; } elseif ($value === SearchQuery::$present) { $excludeq[] = "{$field}:[* TO *]"; } elseif ($value instanceof SearchQuery_Range) { $start = $value->start; if ($start === null) { $start = '*'; } $end = $value->end; if ($end === null) { $end = '*'; } $excludeq[] = "{$field}:[{$start} TO {$end}]"; } else { $excludeq[] = $field . ':"' . $value . '"'; } } $fq[] = ($missing ? "+{$field}:[* TO *] " : '') . '-(' . implode(' ', $excludeq) . ')'; } // if(!headers_sent()) { // if ($q) header('X-Query: '.implode(' ', $q)); // if ($fq) header('X-Filters: "'.implode('", "', $fq).'"'); // } if ($offset == -1) { $offset = $query->start; } if ($limit == -1) { $limit = $query->limit; } if ($limit == -1) { $limit = SearchQuery::$default_page_size; } $params = array_merge($params, array('fq' => implode(' ', $fq))); $res = $service->search($q ? implode(' ', $q) : '*:*', $offset, $limit, $params, Apache_Solr_Service::METHOD_POST); //Debug::dump($res); $results = new ArrayList(); if ($res->getHttpStatus() >= 200 && $res->getHttpStatus() < 300) { foreach ($res->response->docs as $doc) { $result = DataObject::get_by_id($doc->ClassName, $doc->ID); if ($result) { $results->push($result); // Add highlighting (optional) $docId = $doc->_documentid; if ($res->highlighting && $res->highlighting->{$docId}) { // TODO Create decorator class for search results rather than adding arbitrary object properties // TODO Allow specifying highlighted field, and lazy loading // in case the search API needs another query (similar to SphinxSearchable->buildExcerpt()). $combinedHighlights = array(); foreach ($res->highlighting->{$docId} as $field => $highlights) { $combinedHighlights = array_merge($combinedHighlights, $highlights); } // Remove entity-encoded U+FFFD replacement character. It signifies non-displayable characters, // and shows up as an encoding error in browsers. $result->Excerpt = DBField::create_field('HTMLText', str_replace('�', '', implode(' ... ', $combinedHighlights))); } } } $numFound = $res->response->numFound; } else { $numFound = 0; } $ret = array(); $ret['Matches'] = new PaginatedList($results); $ret['Matches']->setLimitItems(false); // Tell PaginatedList how many results there are $ret['Matches']->setTotalItems($numFound); // Results for current page start at $offset $ret['Matches']->setPageStart($offset); // Results per page $ret['Matches']->setPageLength($limit); // Facets //Debug::dump($res); if (isset($res->facet_counts->facet_fields)) { $ret['Facets'] = $this->buildFacetResults($res->facet_counts->facet_fields, $facetSpec); } // Suggestions (requires custom setup, assumes spellcheck.collate=true) if (isset($res->spellcheck->suggestions->collation)) { $ret['Suggestion'] = $res->spellcheck->suggestions->collation; } return new ArrayData($ret); }
/** * @param SearchQuery $query * @param integer $offset * @param integer $limit * @param array $params Extra request parameters passed through to Solr * @return ArrayData Map with the following keys: * - 'Matches': ArrayList of the matched object instances */ public function search(SearchQuery $query, $offset = -1, $limit = -1, $params = array()) { $service = $this->getService(); $searchClass = count($query->classes) == 1 ? $query->classes[0]['class'] : null; SearchVariant::with($searchClass)->call('alterQuery', $query, $this); $q = array(); // Query $fq = array(); // Filter query $qf = array(); // Query fields $hlq = array(); // Highlight query // Build the search itself $q = $this->getQueryComponent($query, $hlq); // If using boosting, set the clean term separately for highlighting. // See https://issues.apache.org/jira/browse/SOLR-2632 if (array_key_exists('hl', $params) && !array_key_exists('hl.q', $params)) { $params['hl.q'] = implode(' ', $hlq); } // Filter by class if requested $classq = array(); foreach ($query->classes as $class) { if (!empty($class['includeSubclasses'])) { $classq[] = 'ClassHierarchy:' . $class['class']; } else { $classq[] = 'ClassName:' . $class['class']; } } if ($classq) { $fq[] = '+(' . implode(' ', $classq) . ')'; } // Filter by filters $fq = array_merge($fq, $this->getFiltersComponent($query)); // Prepare query fields unless specified explicitly if (isset($params['qf'])) { $qf = $params['qf']; } else { $qf = $this->getQueryFields(); } if (is_array($qf)) { $qf = implode(' ', $qf); } if ($qf) { $params['qf'] = $qf; } if (!headers_sent() && !Director::isLive()) { if ($q) { header('X-Query: ' . implode(' ', $q)); } if ($fq) { header('X-Filters: "' . implode('", "', $fq) . '"'); } if ($qf) { header('X-QueryFields: ' . $qf); } } if ($offset == -1) { $offset = $query->start; } if ($limit == -1) { $limit = $query->limit; } if ($limit == -1) { $limit = SearchQuery::$default_page_size; } $params = array_merge($params, array('fq' => implode(' ', $fq))); $res = $service->search($q ? implode(' ', $q) : '*:*', $offset, $limit, $params, Apache_Solr_Service::METHOD_POST); $results = new ArrayList(); if ($res->getHttpStatus() >= 200 && $res->getHttpStatus() < 300) { foreach ($res->response->docs as $doc) { $result = DataObject::get_by_id($doc->ClassName, $doc->ID); if ($result) { $results->push($result); // Add highlighting (optional) $docId = $doc->_documentid; if ($res->highlighting && $res->highlighting->{$docId}) { // TODO Create decorator class for search results rather than adding arbitrary object properties // TODO Allow specifying highlighted field, and lazy loading // in case the search API needs another query (similar to SphinxSearchable->buildExcerpt()). $combinedHighlights = array(); foreach ($res->highlighting->{$docId} as $field => $highlights) { $combinedHighlights = array_merge($combinedHighlights, $highlights); } // Remove entity-encoded U+FFFD replacement character. It signifies non-displayable characters, // and shows up as an encoding error in browsers. $result->Excerpt = DBField::create_field('HTMLText', str_replace('�', '', implode(' ... ', $combinedHighlights))); } } } $numFound = $res->response->numFound; } else { $numFound = 0; } $ret = array(); $ret['Matches'] = new PaginatedList($results); $ret['Matches']->setLimitItems(false); // Tell PaginatedList how many results there are $ret['Matches']->setTotalItems($numFound); // Results for current page start at $offset $ret['Matches']->setPageStart($offset); // Results per page $ret['Matches']->setPageLength($limit); // Include spellcheck and suggestion data. Requires spellcheck=true in $params if (isset($res->spellcheck)) { // Expose all spellcheck data, for custom handling. $ret['Spellcheck'] = $res->spellcheck; // Suggestions. Requires spellcheck.collate=true in $params if (isset($res->spellcheck->suggestions->collation)) { // Extract string suggestion $suggestion = $this->getCollatedSuggestion($res->spellcheck->suggestions->collation); // The collation, including advanced query params (e.g. +), suitable for making another query programmatically. $ret['Suggestion'] = $suggestion; // A human friendly version of the suggestion, suitable for 'Did you mean $SuggestionNice?' display. $ret['SuggestionNice'] = $this->getNiceSuggestion($suggestion); // A string suitable for appending to an href as a query string. // For example <a href="http://example.com/search?q=$SuggestionQueryString">$SuggestionNice</a> $ret['SuggestionQueryString'] = $this->getSuggestionQueryString($suggestion); } } return new ArrayData($ret); }
/** * Clear all records of the given class in the current state ONLY. * * Optionally delete from a given group (where the group is defined as the ID % total groups) * * @param SolrIndex $indexInstance Index instance * @param string $class Class name * @param int $groups Number of groups, if clearing from a striped group * @param int $group Group number, if clearing from a striped group */ protected function clearRecords(SolrIndex $indexInstance, $class, $groups = null, $group = null) { // Clear by classname $conditions = array("+(ClassHierarchy:{$class})"); // If grouping, delete from this group only if ($groups) { $conditions[] = "+_query_:\"{!frange l={$group} u={$group}}mod(ID, {$groups})\""; } // Also filter by state (suffix on document ID) $query = new SearchQuery(); SearchVariant::with($class)->call('alterQuery', $query, $indexInstance); if ($query->isfiltered()) { $conditions = array_merge($conditions, $indexInstance->getFiltersComponent($query)); } // Invoke delete on index $deleteQuery = implode(' ', $conditions); $indexInstance->getService()->deleteByQuery($deleteQuery); }