jenkins-bot has submitted this change and it was merged.
Change subject: Add list of highlighted words and total term frequency to API
output
......................................................................
Add list of highlighted words and total term frequency to API output
Bug: T104443
Change-Id: I56593181890c79afbdb69f85e83c9a3a6c5c9a14
---
M includes/Api/ApiFlowSearch.php
M includes/Search/SearchEngine.php
M includes/Search/Searcher.php
3 files changed, 163 insertions(+), 16 deletions(-)
Approvals:
Matthias Mullie: Looks good to me, approved
jenkins-bot: Verified
diff --git a/includes/Api/ApiFlowSearch.php b/includes/Api/ApiFlowSearch.php
index bdb2002..f3896b6 100644
--- a/includes/Api/ApiFlowSearch.php
+++ b/includes/Api/ApiFlowSearch.php
@@ -10,6 +10,7 @@
use Flow\Model\UUID;
use Flow\Search\Connection;
use Flow\Search\SearchEngine;
+use Flow\Search\Searcher;
use Flow\TalkpageManager;
use MWNamespace;
use Status;
@@ -28,7 +29,10 @@
public function execute() {
$params = $this->extractRequestParams();
- $this->searchEngine->setType( $params['type'] );
+ if ( $params['type'] ) {
+ $this->searchEngine->setType( $params['type'] );
+ }
+
$this->searchEngine->setLimitOffset( $params['limit'],
$params['offset'] );
$this->searchEngine->setSort( $params['sort'] );
@@ -55,10 +59,31 @@
throw new InvalidDataException( $status->getMessage(),
'fail-search' );
}
- /** @var \Elastica\ResultSet|null $result */
- $result = $status->getValue();
- // result can be null, if nothing was found
- $results = $result === null ? array() : $result->getResults();
+ /** @var \Elastica\ResultSet|null $resultSet */
+ $resultSet = $status->getValue();
+ // $resultSet can be null, if nothing was found
+ $results = $resultSet === null ? array() :
$resultSet->getResults();
+
+ // list of highlighted words
+ $highlights = array();
+ /** @var \Elastica\Result $result */
+ foreach ( $results as $result ) {
+ // there'll always be exactly 1 excerpt
+ // see Searcher.php, ...->setHighlight() config
+ $excerpt = $result->getHighlights();
+ $excerpt = $excerpt[Searcher::HIGHLIGHT_FIELD][0];
+
+ $pre = preg_quote( Searcher::HIGHLIGHT_PRE, '/' );
+ $post = preg_quote( Searcher::HIGHLIGHT_POST, '/' );
+ if ( preg_match_all( '/' . $pre . '(.*?)' . $post .
'/', $excerpt, $matches ) ) {
+ $highlights += array_flip( $matches[1] );
+ }
+ }
+ $highlights = array_keys( $highlights );
+
+ // total term frequency
+ $ttf = $resultSet->getAggregation( 'ttf' );
+ $ttf = $ttf['value'];
$topicIds = array();
foreach ( $results as $topic ) {
@@ -68,7 +93,9 @@
// output similar to view-topiclist
$results = $this->formatApi( $topicIds );
// search-specific output
- $results['total'] = $result->getTotalHits();
+ $results['total'] = $resultSet->getTotalHits();
+ $results['highlights'] = $highlights;
+ $results['ttf'] = $ttf;
$this->getResult()->addValue( null, $this->getModuleName(),
$results );
}
diff --git a/includes/Search/SearchEngine.php b/includes/Search/SearchEngine.php
index cdc3953..f238bf9 100644
--- a/includes/Search/SearchEngine.php
+++ b/includes/Search/SearchEngine.php
@@ -42,7 +42,7 @@
/**
* @param string $term text to search
- * @return Status
+ * @return \Status
*/
public function searchText( $term ) {
$term = trim( $term );
diff --git a/includes/Search/Searcher.php b/includes/Search/Searcher.php
index cf1c249..2930bde 100644
--- a/includes/Search/Searcher.php
+++ b/includes/Search/Searcher.php
@@ -5,10 +5,16 @@
use Elastica\Query;
use Elastica\Query\QueryString;
use Elastica\Exception\ExceptionInterface;
+use Elastica\Request;
+use Elastica\ResultSet;
use PoolCounterWorkViaCallback;
use Status;
class Searcher {
+ const HIGHLIGHT_FIELD = 'revisions.text';
+ const HIGHLIGHT_PRE = '<span class="searchmatch">';
+ const HIGHLIGHT_POST = '</span>';
+
/**
* @var string|false $type
*/
@@ -47,6 +53,26 @@
$queryString->setFields( array( 'revisions.text' ) );
$this->query->setQuery( $queryString );
+ // add aggregation to determine exact amount of matching search
terms
+ $terms = $this->getTerms( $term );
+ $this->query->addAggregation( $this->termsAggregation( $terms )
);
+
+ // @todo: abstract-away this config? (core/cirrus also has this
- share it somehow?)
+ $this->query->setHighlight( array(
+ 'fields' => array(
+ static::HIGHLIGHT_FIELD => array(
+ 'type' => 'plain',
+ 'order' => 'score',
+
+ // we want just 1 excerpt of result
text, which includes all highlights
+ 'number_of_fragments' => 1,
+ 'fragment_size' => 10000, // We want
the whole value but more than this is crazy
+ ),
+ ),
+ 'pre_tags' => array( static::HIGHLIGHT_PRE ),
+ 'post_tags' => array( static::HIGHLIGHT_POST ),
+ ) );
+
// @todo: support insource: queries (and perhaps others)
$searchable = Connection::getFlowIndex( $this->indexBaseName );
@@ -61,22 +87,116 @@
// Perform the search
$work = new PoolCounterWorkViaCallback( 'Flow-Search',
"_elasticsearch", array(
'doWork' => function() use ( $search ) {
- try {
- $result = $search->search();
- return Status::newGood( $result
);
- } catch ( ExceptionInterface $e ) {
- return Status::newFatal(
'flow-error-search' );
+ try {
+ $result = $search->search();
+ return Status::newGood( $result );
+ } catch ( ExceptionInterface $e ) {
+ if ( strpos( $e->getMessage(), 'dynamic
scripting for [groovy] disabled' ) ) {
+ // known issue with default ES
config, let's display a more helpful message
+ return Status::newFatal( new
\RawMessage(
+ "Couldn't complete
search: dynamic scripting needs to be enabled. " .
+ "Please add
'script.disable_dynamic: false' to your elasticsearch.yml"
+ ) );
}
- },
- 'error' => function( $status ) {
- $status = $status->getErrorsArray();
- wfLogWarning( 'Pool error searching
Elasticsearch: ' . $status[ 0 ][ 0 ] );
+
return Status::newFatal(
'flow-error-search' );
}
+ },
+ 'error' => function( Status $status ) {
+ $status = $status->getErrorsArray();
+ wfLogWarning( 'Pool error searching
Elasticsearch: ' . $status[0][0] );
+ return Status::newFatal( 'flow-error-search' );
+ }
) );
$result = $work->execute();
return $result;
}
+
+ /**
+ * We want to retrieve the total amount of search word hits
+ * (static::termsAggregation) but our search terms may not be how
+ * ElasticSearch stores the words in its index.
+ * Elastic will "analyze" text (perform stemming, etc) and store
+ * the terms in a normalized way.
+ * AFAICT, there is not really a way to get to that information
+ * from within a search query.
+ *
+ * Luckily, since 1.0, Elastic supports _termvector, which gives
+ * you statistics about the terms in your document.
+ * Since 1.4, Elastic supports feeding _termvector documents to
+ * analyze.
+ * We're going to (ab)use this by letting it respond with term
+ * information on a bogus document that contains only our current
+ * search terms.
+ * So we'll give it a document with just our keywords for the
+ * column that we're searching in (revisions.text) and Elastic will
+ * use that column's configuration to analyze the text we feed it.
+ * It will then respond with the normalized terms & their stats.
+ *
+ * @param string $terms
+ * @return array
+ */
+ protected function getTerms( $terms ) {
+ $terms = preg_split( '/\s+/', $terms );
+
+ // _termvectors only works on a type, but our types are
+ // configured exactly the same so it doesn't matter which
+ $types = Connection::getAllTypes();
+ $searchable = Connection::getFlowIndex( $this->indexBaseName );
+ $searchable = $searchable->getType( array_pop( $types ) );
+
+ $query = array(
+ // bogus document that contains the current search term
+ 'doc' => array(
+ 'revisions' => array(
+ 'text' => $terms,
+ ),
+ ),
+ "fields" => array( "revisions.text" ),
+ );
+
+ // Elastica has no abstraction over _termvector like it has
+ // for _query, so just do the request ourselves
+ $response = $searchable->request(
+ '_termvector',
+ Request::POST,
+ $query,
+ array()
+ );
+
+ $data = $response->getData();
+ return array_keys(
$data['term_vectors']['revisions.text']['terms'] );
+ }
+
+ /**
+ * We can only do this if dynamic scripting is enabled. In
elasticsearch.yml:
+ * script.disable_dynamic: false
+ * @see vendor/ruffin/elastica/test/bin/run_elasticsearch.sh
+ *
+ * @param array $terms
+ * @return \Elastica\Aggregation\Sum
+ */
+ protected function termsAggregation( array $terms ) {
+ $terms = str_replace( '"', '\\"', $terms );
+
+ $script = '
+keywords = ["' . implode( '","', $terms ) . '"]
+total = 0
+for (term in keywords) {
+ total += _index["revisions.text"][term].tf()
+}
+return total';
+ $script = new \Elastica\Script( $script, null, 'groovy' );
+
+ $aggregation = new \Elastica\Aggregation\Sum( 'ttf' );
+ // $aggregation->setScript() doesn't seem to properly set
'lang': 'groovy'
+ // see https://github.com/ruflin/Elastica/pull/748
+ // $aggregation->setScript( $script );
+ $aggregation->setParams( array( 'lang' => 'groovy' ) );
+ $aggregation->setParam( 'script', $script->getScript() );
+
+ return $aggregation;
+ }
}
--
To view, visit https://gerrit.wikimedia.org/r/184404
To unsubscribe, visit https://gerrit.wikimedia.org/r/settings
Gerrit-MessageType: merged
Gerrit-Change-Id: I56593181890c79afbdb69f85e83c9a3a6c5c9a14
Gerrit-PatchSet: 14
Gerrit-Project: mediawiki/extensions/Flow
Gerrit-Branch: master
Gerrit-Owner: Matthias Mullie <[email protected]>
Gerrit-Reviewer: EBernhardson <[email protected]>
Gerrit-Reviewer: Etonkovidova <[email protected]>
Gerrit-Reviewer: Legoktm <[email protected]>
Gerrit-Reviewer: Manybubbles <[email protected]>
Gerrit-Reviewer: Mattflaschen <[email protected]>
Gerrit-Reviewer: Matthias Mullie <[email protected]>
Gerrit-Reviewer: SG <[email protected]>
Gerrit-Reviewer: jenkins-bot <>
_______________________________________________
MediaWiki-commits mailing list
[email protected]
https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits