/** * Find query hits for model in index. * * @param Model $model * @return array|\ZendSearch\Lucene\Search\QueryHit */ private function findHits(Model $model) { // Build query for finding of model's hits. $query = new MultiTerm(); // Add model's class UID. list($name, $value) = $this->config->primaryKeyPair($model); $query->addTerm(new Term($value, $name), true); // Add class uid for identification of model's class. list($name, $value) = $this->config->classUidPair($model); $query->addTerm(new Term($value, $name), true); return $this->index()->find($query); }
/** * Re-write query into primitive queries in the context of specified index * * @param \ZendSearch\Lucene\SearchIndexInterface $index * @return \ZendSearch\Lucene\Search\Query\AbstractQuery */ public function rewrite(Lucene\SearchIndexInterface $index) { if ($this->_term->field != null) { return $this; } else { $query = new MultiTerm(); $query->setBoost($this->getBoost()); foreach ($index->getFieldNames(true) as $fieldName) { $term = new Index\Term($this->_term->text, $fieldName); $query->addTerm($term); } return $query->rewrite($index); } }
public function testDelete() { $this->connection->shouldReceive('getIndex')->andReturn($luceneIndex = m::mock()); $luceneIndex->shouldReceive('find')->with(m::on(function ($arg) { $term = new MultiTerm(); $term->addTerm(new Term(1, 'primary_key'), true); $term->addTerm(new Term('12345', 'class_uid'), true); $this->assertEquals($term, $arg); return true; }))->andReturnUsing(function () { $hitMock = m::mock(); $hitMock->id = 10; return [$hitMock]; }); $luceneIndex->shouldReceive('delete')->with(10)->once(); $index = $this->createIndex(); $index->delete($this->model); }
/** * Lists all Post models. * @return mixed */ public function actionIndex() { $searchModel = new PostSearch(); $dataProvider = $searchModel->search(Yii::$app->request->post()); //setlocale(LC_ALL, 'en_US.UTF-8'); setlocale(LC_CTYPE, 'ru_RU.UTF-8'); //Lucene\Lucene::setDefaultSearchField('contents'); Lucene\Search\QueryParser::setDefaultEncoding('UTF-8'); Lucene\Analysis\Analyzer\Analyzer::setDefault(new Lucene\Analysis\Analyzer\Common\Utf8\CaseInsensitive()); Lucene\Lucene::setResultSetLimit(10); // create blog posts index located in /data/posts_index ,make sure the folder is writable $index = Lucene\Lucene::create('data/posts_index'); $posts = Post::find()->all(); //var_dump($posts);die(); // iterate through posts and build the index foreach ($posts as $p) { $doc = new Lucene\Document(); $doc->addField(Lucene\Document\Field::UnIndexed('entry_id', $p->id)); $doc->addField(Lucene\Document\Field::Keyword('title', $p->title)); $doc->addField(Lucene\Document\Field::text('contents', $p->content)); $index->addDocument($doc); } // commit the index $index->commit(); //Lucene\Analysis\Analyzer\Analyzer::setDefault(new Lucene\Analysis\Analyzer\Common\Utf8\CaseInsensitive()); // explode the search query to individual words $words = explode(' ', urldecode(Yii::$app->getRequest()->getQueryParam('q'))); // start a search query and add a term for each word to it $query = new Lucene\Search\Query\MultiTerm(); foreach ($words as $w) { $query->addTerm(new Lucene\Index\Term($w)); } // open and query the index $index = Lucene\Lucene::open('data/posts_index'); $results = $index->find($query); // the search results //var_dump($results); return $this->render('index', ['searchModel' => $searchModel, 'dataProvider' => $dataProvider, 'search' => $results, 'query' => $query]); }
/** * Optimize query in the context of specified index * * @param \ZendSearch\Lucene\SearchIndexInterface $index * @return \ZendSearch\Lucene\Search\Query\AbstractQuery */ public function optimize(Lucene\SearchIndexInterface $index) { $terms = $this->_terms; $signs = $this->_signs; foreach ($terms as $id => $term) { if (!$index->hasTerm($term)) { if ($signs === null || $signs[$id] === true) { // Term is required return new EmptyResult(); } else { // Term is optional or prohibited // Remove it from terms and signs list unset($terms[$id]); unset($signs[$id]); } } } // Check if all presented terms are prohibited $allProhibited = true; if ($signs === null) { $allProhibited = false; } else { foreach ($signs as $sign) { if ($sign !== false) { $allProhibited = false; break; } } } if ($allProhibited) { return new EmptyResult(); } /** * @todo make an optimization for repeated terms * (they may have different signs) */ if (count($terms) == 1) { // It's already checked, that it's not a prohibited term // It's one term query with one required or optional element $optimizedQuery = new Term(reset($terms)); $optimizedQuery->setBoost($this->getBoost()); return $optimizedQuery; } if (count($terms) == 0) { return new EmptyResult(); } $optimizedQuery = new MultiTerm($terms, $signs); $optimizedQuery->setBoost($this->getBoost()); return $optimizedQuery; }
/** * Re-write query into primitive queries in the context of specified index * * @param \ZendSearch\Lucene\SearchIndexInterface $index * @throws \ZendSearch\Lucene\Exception\RuntimeException * @throws \ZendSearch\Lucene\Exception\OutOfBoundsException * @return \ZendSearch\Lucene\Search\Query\AbstractQuery */ public function rewrite(Lucene\SearchIndexInterface $index) { $this->_matches = array(); if ($this->_pattern->field === null) { // Search through all fields $fields = $index->getFieldNames(true); } else { $fields = array($this->_pattern->field); } $prefix = self::_getPrefix($this->_pattern->text); $prefixLength = strlen($prefix); $matchExpression = '/^' . str_replace(array('\\?', '\\*'), array('.', '.*'), preg_quote($this->_pattern->text, '/')) . '$/'; if ($prefixLength < self::$_minPrefixLength) { throw new RuntimeException('At least ' . self::$_minPrefixLength . ' non-wildcard characters are required at the beginning of pattern.'); } /** * @todo check for PCRE unicode support may be performed through Zend_Environment in some future */ ErrorHandler::start(E_WARNING); $result = preg_match('/\\pL/u', 'a'); ErrorHandler::stop(); if ($result == 1) { // PCRE unicode support is turned on // add Unicode modifier to the match expression $matchExpression .= 'u'; } $maxTerms = Lucene\Lucene::getTermsPerQueryLimit(); foreach ($fields as $field) { $index->resetTermsStream(); if ($prefix != '') { $index->skipTo(new Index\Term($prefix, $field)); while ($index->currentTerm() !== null && $index->currentTerm()->field == $field && substr($index->currentTerm()->text, 0, $prefixLength) == $prefix) { if (preg_match($matchExpression, $index->currentTerm()->text) === 1) { $this->_matches[] = $index->currentTerm(); if ($maxTerms != 0 && count($this->_matches) > $maxTerms) { throw new OutOfBoundsException('Terms per query limit is reached.'); } } $index->nextTerm(); } } else { $index->skipTo(new Index\Term('', $field)); while ($index->currentTerm() !== null && $index->currentTerm()->field == $field) { if (preg_match($matchExpression, $index->currentTerm()->text) === 1) { $this->_matches[] = $index->currentTerm(); if ($maxTerms != 0 && count($this->_matches) > $maxTerms) { throw new OutOfBoundsException('Terms per query limit is reached.'); } } $index->nextTerm(); } } $index->closeTermsStream(); } if (count($this->_matches) == 0) { return new EmptyResult(); } elseif (count($this->_matches) == 1) { return new Term(reset($this->_matches)); } else { $rewrittenQuery = new MultiTerm(); foreach ($this->_matches as $matchedTerm) { $rewrittenQuery->addTerm($matchedTerm); } return $rewrittenQuery; } }
/** * Re-write query into primitive queries in the context of specified index * * @param \ZendSearch\Lucene\SearchIndexInterface $index * @throws \ZendSearch\Lucene\Exception\OutOfBoundsException * @return \ZendSearch\Lucene\Search\Query\AbstractQuery */ public function rewrite(Lucene\SearchIndexInterface $index) { $this->_matches = array(); if ($this->_field === null) { // Search through all fields $fields = $index->getFieldNames(true); } else { $fields = array($this->_field); } $maxTerms = Lucene\Lucene::getTermsPerQueryLimit(); foreach ($fields as $field) { $index->resetTermsStream(); if ($this->_lowerTerm !== null) { $lowerTerm = new Index\Term($this->_lowerTerm->text, $field); $index->skipTo($lowerTerm); if (!$this->_inclusive && $index->currentTerm() == $lowerTerm) { // Skip lower term $index->nextTerm(); } } else { $index->skipTo(new Index\Term('', $field)); } if ($this->_upperTerm !== null) { // Walk up to the upper term $upperTerm = new Index\Term($this->_upperTerm->text, $field); while ($index->currentTerm() !== null && $index->currentTerm()->field == $field && $index->currentTerm()->text < $upperTerm->text) { $this->_matches[] = $index->currentTerm(); if ($maxTerms != 0 && count($this->_matches) > $maxTerms) { throw new OutOfBoundsException('Terms per query limit is reached.'); } $index->nextTerm(); } if ($this->_inclusive && $index->currentTerm() == $upperTerm) { // Include upper term into result $this->_matches[] = $upperTerm; } } else { // Walk up to the end of field data while ($index->currentTerm() !== null && $index->currentTerm()->field == $field) { $this->_matches[] = $index->currentTerm(); if ($maxTerms != 0 && count($this->_matches) > $maxTerms) { throw new OutOfBoundsException('Terms per query limit is reached.'); } $index->nextTerm(); } } $index->closeTermsStream(); } if (count($this->_matches) == 0) { return new EmptyResult(); } elseif (count($this->_matches) == 1) { return new Term(reset($this->_matches)); } else { $rewrittenQuery = new MultiTerm(); foreach ($this->_matches as $matchedTerm) { $rewrittenQuery->addTerm($matchedTerm); } return $rewrittenQuery; } }
/** * Удалить из индекса информацию об объекте ISearchable * * @param ISearchable $item */ public function deleteIndex(ISearchable $item) { // получаем доступ к индексу $index = $this->connection; // формируем запрос на удаление существующей записи из индекса $query = new MultiTerm(); $query->addTerm(new Term($item->getId(), 'pk'), true); $query->addTerm(new Term(get_class($item), 'model'), true); // собственно удаляем $hits = $index->find($query); foreach ($hits as $hit) { $index->delete($hit->id); } }
/** * Parses a query string * * @param string $strQuery * @param string $encoding * @throws \ZendSearch\Lucene\Search\Exception\QueryParserException * @throws \ZendSearch\Lucene\Exception\RuntimeException * @return \ZendSearch\Lucene\Search\Query\AbstractQuery */ public static function parse($strQuery, $encoding = null) { self::_getInstance(); // Reset FSM if previous parse operation didn't return it into a correct state self::$_instance->reset(); try { self::$_instance->_encoding = $encoding !== null ? $encoding : self::$_instance->_defaultEncoding; self::$_instance->_lastToken = null; self::$_instance->_context = new QueryParserContext(self::$_instance->_encoding); self::$_instance->_contextStack = array(); self::$_instance->_tokens = self::$_instance->_lexer->tokenize($strQuery, self::$_instance->_encoding); // Empty query if (count(self::$_instance->_tokens) == 0) { return new Query\Insignificant(); } foreach (self::$_instance->_tokens as $token) { try { self::$_instance->_currentToken = $token; self::$_instance->process($token->type); self::$_instance->_lastToken = $token; } catch (\Exception $e) { if (strpos($e->getMessage(), 'There is no any rule for') !== false) { throw new QueryParserException('Syntax error at char position ' . $token->position . '.', 0, $e); } throw new RuntimeException($e->getMessage(), $e->getCode(), $e); } } if (count(self::$_instance->_contextStack) != 0) { throw new QueryParserException('Syntax Error: mismatched parentheses, every opening must have closing.'); } return self::$_instance->_context->getQuery(); } catch (QueryParserException $e) { if (self::$_instance->_suppressQueryParsingExceptions) { $queryTokens = Analyzer\Analyzer::getDefault()->tokenize($strQuery, self::$_instance->_encoding); $query = new Query\MultiTerm(); $termsSign = self::$_instance->_defaultOperator == self::B_AND ? true : null; foreach ($queryTokens as $token) { $query->addTerm(new Index\Term($token->getTermText()), $termsSign); } return $query; } else { throw new RuntimeException($e->getMessage(), $e->getCode(), $e); } } }
/** * Re-write query into primitive queries in the context of specified index * * @param \ZendSearch\Lucene\SearchIndexInterface $index * @throws \ZendSearch\Lucene\Search\Exception\QueryParserException * @return \ZendSearch\Lucene\Search\Query\AbstractQuery */ public function rewrite(Lucene\SearchIndexInterface $index) { if ($this->_field === null) { $query = new Query\MultiTerm(); $query->setBoost($this->getBoost()); $hasInsignificantSubqueries = false; if (Lucene\Lucene::getDefaultSearchField() === null) { $searchFields = $index->getFieldNames(true); } else { $searchFields = array(Lucene\Lucene::getDefaultSearchField()); } foreach ($searchFields as $fieldName) { $subquery = new Term($this->_word, $this->_encoding, $fieldName); $rewrittenSubquery = $subquery->rewrite($index); foreach ($rewrittenSubquery->getQueryTerms() as $term) { $query->addTerm($term); } if ($rewrittenSubquery instanceof Query\Insignificant) { $hasInsignificantSubqueries = true; } } if (count($query->getTerms()) == 0) { $this->_matches = array(); if ($hasInsignificantSubqueries) { return new Query\Insignificant(); } else { return new Query\EmptyResult(); } } $this->_matches = $query->getQueryTerms(); return $query; } // ------------------------------------- // Recognize exact term matching (it corresponds to Keyword fields stored in the index) // encoding is not used since we expect binary matching $term = new Index\Term($this->_word, $this->_field); if ($index->hasTerm($term)) { $query = new Query\Term($term); $query->setBoost($this->getBoost()); $this->_matches = $query->getQueryTerms(); return $query; } // ------------------------------------- // Recognize wildcard queries /** * @todo check for PCRE unicode support may be performed through Zend_Environment in some future */ ErrorHandler::start(E_WARNING); $result = preg_match('/\\pL/u', 'a'); ErrorHandler::stop(); if ($result == 1) { $word = iconv($this->_encoding, 'UTF-8', $this->_word); $wildcardsPattern = '/[*?]/u'; $subPatternsEncoding = 'UTF-8'; } else { $word = $this->_word; $wildcardsPattern = '/[*?]/'; $subPatternsEncoding = $this->_encoding; } $subPatterns = preg_split($wildcardsPattern, $word, -1, PREG_SPLIT_OFFSET_CAPTURE); if (count($subPatterns) > 1) { // Wildcard query is recognized $pattern = ''; foreach ($subPatterns as $id => $subPattern) { // Append corresponding wildcard character to the pattern before each sub-pattern (except first) if ($id != 0) { $pattern .= $word[$subPattern[1] - 1]; } // Check if each subputtern is a single word in terms of current analyzer $tokens = Analyzer\Analyzer::getDefault()->tokenize($subPattern[0], $subPatternsEncoding); if (count($tokens) > 1) { throw new QueryParserException('Wildcard search is supported only for non-multiple word terms'); } foreach ($tokens as $token) { $pattern .= $token->getTermText(); } } $term = new Index\Term($pattern, $this->_field); $query = new Query\Wildcard($term); $query->setBoost($this->getBoost()); // Get rewritten query. Important! It also fills terms matching container. $rewrittenQuery = $query->rewrite($index); $this->_matches = $query->getQueryTerms(); return $rewrittenQuery; } // ------------------------------------- // Recognize one-term multi-term and "insignificant" queries $tokens = Analyzer\Analyzer::getDefault()->tokenize($this->_word, $this->_encoding); if (count($tokens) == 0) { $this->_matches = array(); return new Query\Insignificant(); } if (count($tokens) == 1) { $term = new Index\Term($tokens[0]->getTermText(), $this->_field); $query = new Query\Term($term); $query->setBoost($this->getBoost()); $this->_matches = $query->getQueryTerms(); return $query; } //It's not insignificant or one term query $query = new Query\MultiTerm(); /** * @todo Process $token->getPositionIncrement() to support stemming, synonyms and other * analizer design features */ foreach ($tokens as $token) { $term = new Index\Term($token->getTermText(), $this->_field); $query->addTerm($term, true); // all subterms are required } $query->setBoost($this->getBoost()); $this->_matches = $query->getQueryTerms(); return $query; }
/** * Optimize query in the context of specified index * * @param \ZendSearch\Lucene\SearchIndexInterface $index * @return \ZendSearch\Lucene\Search\Query\AbstractQuery */ public function optimize(Lucene\SearchIndexInterface $index) { $subqueries = array(); $signs = array(); // Optimize all subqueries foreach ($this->_subqueries as $id => $subquery) { $subqueries[] = $subquery->optimize($index); $signs[] = $this->_signs === null ? true : $this->_signs[$id]; } // Remove insignificant subqueries foreach ($subqueries as $id => $subquery) { if ($subquery instanceof Insignificant) { // Insignificant subquery has to be removed anyway unset($subqueries[$id]); unset($signs[$id]); } } if (count($subqueries) == 0) { // Boolean query doesn't has non-insignificant subqueries return new Insignificant(); } // Check if all non-insignificant subqueries are prohibited $allProhibited = true; foreach ($signs as $sign) { if ($sign !== false) { $allProhibited = false; break; } } if ($allProhibited) { return new Insignificant(); } // Check for empty subqueries foreach ($subqueries as $id => $subquery) { if ($subquery instanceof EmptyResult) { if ($signs[$id] === true) { // Matching is required, but is actually empty return new EmptyResult(); } else { // Matching is optional or prohibited, but is empty // Remove it from subqueries and signs list unset($subqueries[$id]); unset($signs[$id]); } } } // Check, if reduced subqueries list is empty if (count($subqueries) == 0) { return new EmptyResult(); } // Check if all non-empty subqueries are prohibited $allProhibited = true; foreach ($signs as $sign) { if ($sign !== false) { $allProhibited = false; break; } } if ($allProhibited) { return new EmptyResult(); } // Check, if reduced subqueries list has only one entry if (count($subqueries) == 1) { // It's a query with only one required or optional clause // (it's already checked, that it's not a prohibited clause) if ($this->getBoost() == 1) { return reset($subqueries); } $optimizedQuery = clone reset($subqueries); $optimizedQuery->setBoost($optimizedQuery->getBoost() * $this->getBoost()); return $optimizedQuery; } // Prepare first candidate for optimized query $optimizedQuery = new self($subqueries, $signs); $optimizedQuery->setBoost($this->getBoost()); $terms = array(); $tsigns = array(); $boostFactors = array(); // Try to decompose term and multi-term subqueries foreach ($subqueries as $id => $subquery) { if ($subquery instanceof Term) { $terms[] = $subquery->getTerm(); $tsigns[] = $signs[$id]; $boostFactors[] = $subquery->getBoost(); // remove subquery from a subqueries list unset($subqueries[$id]); unset($signs[$id]); } elseif ($subquery instanceof MultiTerm) { $subTerms = $subquery->getTerms(); $subSigns = $subquery->getSigns(); if ($signs[$id] === true) { // It's a required multi-term subquery. // Something like '... +(+term1 -term2 term3 ...) ...' // Multi-term required subquery can be decomposed only if it contains // required terms and doesn't contain prohibited terms: // ... +(+term1 term2 ...) ... => ... +term1 term2 ... // // Check this $hasRequired = false; $hasProhibited = false; if ($subSigns === null) { // All subterms are required $hasRequired = true; } else { foreach ($subSigns as $sign) { if ($sign === true) { $hasRequired = true; } elseif ($sign === false) { $hasProhibited = true; break; } } } // Continue if subquery has prohibited terms or doesn't have required terms if ($hasProhibited || !$hasRequired) { continue; } foreach ($subTerms as $termId => $term) { $terms[] = $term; $tsigns[] = $subSigns === null ? true : $subSigns[$termId]; $boostFactors[] = $subquery->getBoost(); } // remove subquery from a subqueries list unset($subqueries[$id]); unset($signs[$id]); } else { // $signs[$id] === null || $signs[$id] === false // It's an optional or prohibited multi-term subquery. // Something like '... (+term1 -term2 term3 ...) ...' // or // something like '... -(+term1 -term2 term3 ...) ...' // Multi-term optional and required subqueries can be decomposed // only if all terms are optional. // // Check if all terms are optional. $onlyOptional = true; if ($subSigns === null) { // All subterms are required $onlyOptional = false; } else { foreach ($subSigns as $sign) { if ($sign !== null) { $onlyOptional = false; break; } } } // Continue if non-optional terms are presented in this multi-term subquery if (!$onlyOptional) { continue; } foreach ($subTerms as $termId => $term) { $terms[] = $term; $tsigns[] = $signs[$id] === null ? null : false; $boostFactors[] = $subquery->getBoost(); } // remove subquery from a subqueries list unset($subqueries[$id]); unset($signs[$id]); } } } // Check, if there are no decomposed subqueries if (count($terms) == 0) { // return prepared candidate return $optimizedQuery; } // Check, if all subqueries have been decomposed and all terms has the same boost factor if (count($subqueries) == 0 && count(array_unique($boostFactors)) == 1) { $optimizedQuery = new MultiTerm($terms, $tsigns); $optimizedQuery->setBoost(reset($boostFactors) * $this->getBoost()); return $optimizedQuery; } // This boolean query can't be transformed to Term/MultiTerm query and still contains // several subqueries // Separate prohibited terms $prohibitedTerms = array(); foreach ($terms as $id => $term) { if ($tsigns[$id] === false) { $prohibitedTerms[] = $term; unset($terms[$id]); unset($tsigns[$id]); unset($boostFactors[$id]); } } if (count($terms) == 1) { $clause = new Term(reset($terms)); $clause->setBoost(reset($boostFactors)); $subqueries[] = $clause; $signs[] = reset($tsigns); // Clear terms list $terms = array(); } elseif (count($terms) > 1 && count(array_unique($boostFactors)) == 1) { $clause = new MultiTerm($terms, $tsigns); $clause->setBoost(reset($boostFactors)); $subqueries[] = $clause; // Clause sign is 'required' if clause contains required terms. 'Optional' otherwise. $signs[] = in_array(true, $tsigns) ? true : null; // Clear terms list $terms = array(); } if (count($prohibitedTerms) == 1) { // (boost factors are not significant for prohibited clauses) $subqueries[] = new Term(reset($prohibitedTerms)); $signs[] = false; // Clear prohibited terms list $prohibitedTerms = array(); } elseif (count($prohibitedTerms) > 1) { // prepare signs array $prohibitedSigns = array(); foreach ($prohibitedTerms as $id => $term) { // all prohibited term are grouped as optional into multi-term query $prohibitedSigns[$id] = null; } // (boost factors are not significant for prohibited clauses) $subqueries[] = new MultiTerm($prohibitedTerms, $prohibitedSigns); // Clause sign is 'prohibited' $signs[] = false; // Clear terms list $prohibitedTerms = array(); } /** @todo Group terms with the same boost factors together */ // Check, that all terms are processed // Replace candidate for optimized query if (count($terms) == 0 && count($prohibitedTerms) == 0) { $optimizedQuery = new self($subqueries, $signs); $optimizedQuery->setBoost($this->getBoost()); } return $optimizedQuery; }
/** * Search page for the term in the index. * * @param $term * @param array $fields * @return array|\ZendSearch\Lucene\Search\QueryHit */ public function search($term, $fields = []) { Wildcard::setMinPrefixLength($this->minPrefixLength); Lucene::setResultSetLimit($this->resultsLimit); $query = new MultiTerm(); if (count($fields)) { foreach ($fields as $field => $value) { $query->addTerm(new IndexTerm($value, $field), true); } } $subTerm = explode(' ', $term); foreach ($subTerm as $value) { $query->addTerm(new IndexTerm($value), true); } return $this->luceneIndex->find($query); }