expr = new Search_Expr_Or([]); } else { $this->expr = new Search_Expr_And([]); } if ($query) { $this->filterContent($query); } } public function __clone() { $this->expr = clone $this->expr; } public function setIdentifierFields(array $fields) { $this->identifierFields = $fields; } public function addObject($type, $objectId) { if (is_null($this->objectList)) { $this->objectList = new Search_Expr_Or([]); $this->expr->addPart($this->objectList); } $type = new Search_Expr_Token($type, 'identifier', 'object_type'); $objectId = new Search_Expr_Token($objectId, 'identifier', 'object_id'); $this->objectList->addPart(new Search_Expr_And([$type, $objectId])); } public function filterContent($query, $field = 'contents') { global $prefs; if ($prefs['unified_search_default_operator'] == 1 && strpos($query, '*') !== false) { // Wildcard queries with spaces need to be OR otherwise "*foo bar*" won't match "foo bar" if set to AND. $query = preg_replace('/\s+/', '* *', trim($query)); $query = str_replace(['*AND*', '*OR*', '**'], ['', 'OR', '*'], $query); } $this->addPart($query, 'plaintext', $field); } public function filterIdentifier($query, $field) { $this->addPart(new Search_Expr_Token($query), 'identifier', $field); } public function filterType($types) { if (is_array($types)) { foreach ($types as $type) { if ($type) { $tokens[] = new Search_Expr_Token($type); } } if (isset($tokens)) { $or = new Search_Expr_Or($tokens); $this->addPart($or, 'identifier', 'object_type'); } } elseif ($types) { $token = new Search_Expr_Token($types); $this->addPart($token, 'identifier', 'object_type'); } } public function filterMultivalue($query, $field) { $this->addPart($query, 'multivalue', $field); } public function filterContributors($query) { $this->filterMultivalue($query, 'contributors'); } public function filterCategory($query, $deep = false) { $this->filterMultivalue($query, $deep ? 'deep_categories' : 'categories'); } public function filterTags($query) { $this->filterMultivalue($query, 'freetags'); } public function filterLanguage($query) { $this->addPart($query, 'identifier', 'language'); } public function filterPermissions(array $groups, $user = null) { $tokens = []; foreach ($groups as $group) { $tokens[] = new Search_Expr_Token($group); } $or = new Search_Expr_Or($tokens); if ($user) { $sub = $this->getSubQuery('permissions'); $sub->filterMultivalue($or, 'allowed_groups'); $sub->filterMultivalue(new Search_Expr_Token($user), 'allowed_users'); } else { $this->addPart($or, 'multivalue', 'allowed_groups'); } $this->applyTransform(new Search_Formatter_Transform_FieldPermissionEnforcer()); } /** * Sets up Laminas search term for a date range * * @param string $from date - a unix timestamp or most date strings such as 'now', '2011-11-21', 'last week' etc * @param string $to date as with $from (other examples: '-42 days', 'last tuesday') * @param string $field to search in such as 'tracker_field_42'. default: modification_date * @link http://www.php.net/manual/en/datetime.formats.php * @return void */ public function filterRange($from, $to, $field = 'modification_date') { if (! is_numeric($from) && $from !== "") { $from2 = strtotime($from); if ($from2) { $from = $from2; } else { Feedback::error(tra('filterRange: "from" value not parsed')); } } if (! is_numeric($to)) { $to2 = strtotime($to); if ($to2) { $to = $to2; } else { Feedback::error(tra('filterRange: "to" value not parsed')); } } /* make the range filter work regardless of ordering - if from > to, swap */ if (is_numeric($from) && is_numeric($to) && $to < $from) { $temp = $to; $to = $from; $from = $temp; } $this->addPart(new Search_Expr_Range($from, $to), 'timestamp', $field); } public function filterTextRange($from, $to, $field = 'title') { /* make the range filter work regardless of ordering - if from > to, swap */ if (strcmp($from, $to) > 0) { $temp = $to; $to = $from; $from = $temp; } $this->addPart(new Search_Expr_Range($from, $to), 'plaintext', $field); } public function filterNumericRange($from, $to, $field) { /* make the range filter work regardless of ordering - if from > to, swap */ if ($to < $from) { $temp = $to; $to = $from; $from = $temp; } $this->addPart(new Search_Expr_Range($from, $to), 'numeric', $field); } public function filterInitial($initial, $field = 'title') { $this->addPart(new Search_Expr_Initial($initial), 'plaintext', $field); } public function filterNotInitial($initial, $field = 'title') { $this->addPart(new Search_Expr_Not(new Search_Expr_Initial($initial)), 'plaintext', $field); } public function filterRelation($query, array $invertable = []) { $query = $this->parse($query); $replacer = new Search_Query_RelationReplacer($invertable); $query = $query->walk([$replacer, 'visit']); $this->addPart($query, 'multivalue', 'relations'); } public function filterSimilar($type, $object, $field = 'contents') { $part = new Search_Expr_And( [ new Search_Expr_Not( new Search_Expr_And( [ new Search_Expr_Token($type, 'identifier', 'object_type'), new Search_Expr_Token($object, 'identifier', 'object_id'), ] ) ), $mlt = new Search_Expr_MoreLikeThis($type, $object), ] ); $mlt->setField($field); $this->expr->addPart($part); } public function filterSimilarToThese($objects, $content, $field = 'contents') { $excluded = []; foreach ($objects as $object) { $excluded[] = new Search_Expr_And( [ new Search_Expr_Token($object['object_type'], 'identifier', 'object_type'), new Search_Expr_Token($object['object_id'], 'identifier', 'object_id'), ] ); } $mlt = new Search_Expr_MoreLikeThis($content); $mlt->setField($field); $part = new Search_Expr_And( [ $mlt, new Search_Expr_Not(new Search_Expr_Or($excluded)), ] ); $this->expr->addPart($part); } public function filterDistance($distance, $lat, $lon, $field = 'geo_point') { $this->addPart(new Search_Expr_Distance($distance, $lat, $lon), 'geo_distance', $field); } private function addPart($query, $type, $field) { if (is_string($field)) { $field = explode(',', $field); } $parts = []; foreach ((array) $field as $f) { $part = $this->parse($query); $part->setType($type); $part->setField($f); $parts[] = $part; } if (count($parts) === 1) { $this->expr->addPart($parts[0]); } else { $this->expr->addPart(new Search_Expr_Or($parts)); } } public function setOrder($order) { if (is_string($order)) { $this->sortOrder = Search_Query_Order::parse($order); } else { $this->sortOrder = $order; } } public function setRange($start, $count = null) { $this->start = (int) $start; if ($count) { $this->count = (int) $count; } } public function setCount($count = null) { if ($count) { $this->count = (int) $count; } } /** * Affects the range from a numeric value * @param $pageNumber int Page number from 1 to n */ public function setPage($pageNumber) { $pageNumber = max(1, (int) $pageNumber); $this->setRange(($pageNumber - 1) * $this->count); } public function setWeightCalculator(Search_Query_WeightCalculator_Interface $calculator) { $this->weightCalculator = $calculator; } public function getSortOrder() { if ($this->sortOrder) { return $this->sortOrder; } else { return Search_Query_Order::getDefault(); } } /** * @param Search_Index_Interface $index * @param string $multisearchId : When provided, it means that the current query is to be added to an * Elasticsearch Multisearch query, rather than executed as a single query search. Triggering of the Multisearch * query is done though the Index object. * @param Search_Elastic_ResultSet $resultFromMultisearch : When provided, it means that this is one of the sub-results from an * Elasticsearch Multisearch query, and so it just has to be processed as if a result had come back in the case * of a single query. * @return Search_ResultSet */ public function search(Search_Index_Interface $index, $multisearchId = '', $resultFromMultisearch = '') { $this->finalize(); try { $resultset = $index->find($this, $this->start, $this->count, $multisearchId, $resultFromMultisearch); } catch (Search_Elastic_SortException $e) { //on sort exception, try again without the sort field $this->sortOrder = null; $resultset = $index->find($this, $this->start, $this->count); } catch (Exception $e) { if (empty($e->suppress_feedback)) { Feedback::error(tr('Malformed search query:') . ' ' . $e->getMessage()); trigger_error($e->getMessage(), E_USER_WARNING); } return Search_ResultSet::create([]); } if ($multisearchId > '') { // This individual query would already be added to a Multisearch in the Index object and there would be // no results to deal with until the Multisearch is triggered later return; } $resultset->applyTransform(function ($entry) { if (! isset($entry['_index']) || ! isset($this->foreignQueries[$entry['_index']])) { foreach ($this->transformations as $trans) { if (is_callable($trans)) { $entry = $trans($entry); } } } return $entry; }); foreach ($this->foreignQueries as $indexName => $query) { $resultset->applyTransform(function ($entry) use ($query, $indexName) { if (isset($entry['_index']) && $entry['_index'] == $indexName) { foreach ($query->transformations as $trans) { if (is_callable($trans)) { $entry = $trans($entry); } } } return $entry; }); } $resultset = $this->processReturnOnlyResultsFromList($resultset); return $resultset; } public function scroll($index) { $this->finalize(); try { $res = $index->scroll($this); foreach ($res as $row) { foreach ($this->transformations as $trans) { if (is_callable($trans)) { $row = $trans($row); } } yield $row; } } catch (Exception $e) { Feedback::error(tra("Malformed search query")); trigger_error($e->getMessage(), E_USER_WARNING); } } public function applyTransform(callable $transform) { $this->transformations[] = $transform; } public function store($name, $index) { if ($index instanceof Search_Index_QueryRepository) { $this->finalize(); $index->store($name, $this->expr); return true; } return false; } private function finalize() { if ($this->weightCalculator) { $this->expr->walk([$this->weightCalculator, 'calculate']); if ($this->postFilter) { $this->postFilter->expr->walk([$this->weightCalculator, 'calculate']); } foreach ($this->foreignQueries as $query) { $query->expr->walk([$this->weightCalculator, 'calculate']); } } if ($this->identifierFields) { $fields = $this->identifierFields; $this->expr->walk( function (Search_Expr_Interface $expr) use ($fields) { if (method_exists($expr, 'getField') && in_array($expr->getField(), $fields)) { $expr->setType('identifier'); } } ); if ($this->postFilter) { $this->postFilter->expr->walk( function (Search_Expr_Interface $expr) use ($fields) { if (method_exists($expr, 'getField') && in_array($expr->getField(), $fields)) { $expr->setType('identifier'); } } ); } foreach ($this->foreignQueries as $query) { $query->expr->walk( function (Search_Expr_Interface $expr) use ($fields) { if (method_exists($expr, 'getField') && in_array($expr->getField(), $fields)) { $expr->setType('identifier'); } } ); } } } public function getExpr() { return $this->expr; } private function parse($query) { if (is_string($query)) { $parser = new Search_Expr_Parser(); $query = $parser->parse($query); } elseif ($query instanceof Search_Expr_Interface) { $query = clone $query; } return $query; } public function getTerms() { $terms = []; $extractor = new Search_Type_Factory_Direct(); $this->expr->walk( function ($expr) use (&$terms, $extractor) { if ($expr instanceof Search_Expr_Token && $expr->getField() == 'contents') { $terms[] = $expr->getValue($extractor)->getValue(); } } ); return $terms; } public function getSubQuery($name) { if (empty($name)) { return $this; } if (! isset($this->subQueries[$name])) { $subquery = new self(); $subquery->expr = new Search_Expr_Or([]); $this->expr->addPart($subquery->expr); $this->subQueries[$name] = $subquery; } return $this->subQueries[$name]; } public function getPostFilter() { if (! $this->postFilter) { $subquery = new self(); $this->postFilter = $subquery; $subquery->postFilter = $subquery; } return $this->postFilter; } public function requestFacet(Search_Query_Facet_Interface $facet) { $this->facets[] = $facet; } public function getFacets() { return $this->facets; } public function includeForeign($indexName, Search_Query $query) { $this->foreignQueries[$indexName] = $query; } public function getForeignQueries() { return $this->foreignQueries; } /** * Set list of results to return * * @param array $returnOnlyResultList * @return void */ public function setReturnOnlyResultList($returnOnlyResultList) { $this->returnOnlyResultList = $returnOnlyResultList; } /** * Get list of results to return * * @return array */ public function getReturnOnlyResultList() { return $this->returnOnlyResultList; } /** * Filter the results of the query * * @param Search_ResultSet $resultSet * @return Search_ResultSet */ protected function processReturnOnlyResultsFromList($resultSet) { if (! empty($this->getReturnOnlyResultList())) { $tmpResults = []; foreach ($this->getReturnOnlyResultList() as $resultPosition) { $arrayKey = $resultPosition - 1; if (isset($resultSet[$arrayKey])) { $tmpResults[] = $resultSet[$arrayKey]; } } $resultSet = Search_ResultSet::create($tmpResults); } return $resultSet; } }