Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
96.39% covered (success)
96.39%
80 / 83
62.50% covered (warning)
62.50%
5 / 8
CRAP
0.00% covered (danger)
0.00%
0 / 1
FullTextCirrusSearchResultBuilder
96.39% covered (success)
96.39%
80 / 83
62.50% covered (warning)
62.50%
5 / 8
33
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 newBuilder
75.00% covered (warning)
75.00%
3 / 4
0.00% covered (danger)
0.00%
0 / 1
2.06
 build
100.00% covered (success)
100.00%
20 / 20
100.00% covered (success)
100.00%
1 / 1
4
 getTitleHelper
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 doTitleSnippet
100.00% covered (success)
100.00%
23 / 23
100.00% covered (success)
100.00%
1 / 1
11
 doMainSnippet
100.00% covered (success)
100.00%
15 / 15
100.00% covered (success)
100.00%
1 / 1
6
 doHeadings
88.89% covered (warning)
88.89%
8 / 9
0.00% covered (danger)
0.00%
0 / 1
4.02
 doCategory
87.50% covered (warning)
87.50%
7 / 8
0.00% covered (danger)
0.00%
0 / 1
4.03
1<?php
2
3namespace CirrusSearch\Search;
4
5use CirrusSearch\Search\Fetch\HighlightedField;
6use CirrusSearch\Search\Fetch\HighlightingTrait;
7use MediaWiki\Title\Title;
8use MediaWiki\Utils\MWTimestamp;
9
10class FullTextCirrusSearchResultBuilder {
11    use HighlightingTrait;
12
13    /** @var CirrusSearchResultBuilder|null */
14    private $builder;
15
16    /** @var TitleHelper */
17    private $titleHelper;
18
19    /** @var HighlightedField[][] indexed per target and ordered by priority */
20    private $highligtedFields;
21
22    /** @var string[] */
23    private $extraFields;
24
25    /**
26     * @param TitleHelper $titleHelper
27     * @param HighlightedField[][] $hlFieldsPerTarget list of highlighted field indexed per target and sorted by priority
28     * @param string[] $extraFields list of extra fields to extract from the source doc
29     */
30    public function __construct( TitleHelper $titleHelper, array $hlFieldsPerTarget, array $extraFields = [] ) {
31        $this->titleHelper = $titleHelper;
32        $this->highligtedFields = $hlFieldsPerTarget;
33        $this->extraFields = $extraFields;
34    }
35
36    /**
37     * @param Title $title
38     * @param string $docId
39     * @return CirrusSearchResultBuilder
40     */
41    private function newBuilder( Title $title, $docId ): CirrusSearchResultBuilder {
42        if ( $this->builder === null ) {
43            $this->builder = new CirrusSearchResultBuilder( $title, $docId );
44        } else {
45            $this->builder->reset( $title, $docId );
46        }
47        return $this->builder;
48    }
49
50    public function build( \Elastica\Result $result ): CirrusSearchResult {
51        $title = $this->getTitleHelper()->makeTitle( $result );
52        $fields = $result->getFields();
53        $builder = $this->newBuilder( $title, $result->getId() )
54            ->wordCount( $fields['text.word_count'][0] ?? 0 )
55            ->byteSize( $result->text_bytes ?? 0 )
56            ->timestamp( new MWTimestamp( $result->timestamp ) )
57            ->score( $result->getScore() )
58            ->explanation( $result->getExplanation() );
59
60        if ( isset( $result->namespace_text ) ) {
61            $builder->interwikiNamespaceText( $result->namespace_text );
62        }
63
64        $highlights = $result->getHighlights();
65        $this->doTitleSnippet( $title, $result, $highlights );
66        $this->doMainSnippet( $highlights );
67        $this->doHeadings( $title, $highlights );
68        $this->doCategory( $highlights );
69        $source = $result->getData();
70
71        foreach ( $this->extraFields as $field ) {
72            if ( isset( $source[$field] ) ) {
73                $builder->addExtraField( $field, $source[$field] );
74            }
75        }
76
77        return $builder->build();
78    }
79
80    protected function getTitleHelper(): TitleHelper {
81        return $this->titleHelper;
82    }
83
84    private function doTitleSnippet( Title $title, \Elastica\Result $result, array $highlights ) {
85        $matched = false;
86        foreach ( $this->highligtedFields[ArrayCirrusSearchResult::TITLE_SNIPPET] as $hlField ) {
87            if ( isset( $highlights[$hlField->getFieldName()] ) ) {
88                $nstext = $title->getNamespace() === 0 ? '' : $this->titleHelper->getNamespaceText( $title ) . ':';
89                $snippet = $nstext . $this->escapeHighlightedText( $highlights[ $hlField->getFieldName() ][ 0 ] );
90                $this->builder
91                    ->titleSnippet( $snippet )
92                    ->titleSnippetField( $hlField->getFieldName() );
93                $matched = true;
94                break;
95            }
96        }
97        if ( !$matched && $title->isExternal() ) {
98            // Interwiki searches are weird. They won't have title highlights by design, but
99            // if we don't return a title snippet we'll get weird display results.
100            $this->builder->titleSnippet( $title->getText() );
101        }
102
103        if ( !$matched && isset( $this->highligtedFields[ArrayCirrusSearchResult::REDIRECT_SNIPPET] ) ) {
104            foreach ( $this->highligtedFields[ArrayCirrusSearchResult::REDIRECT_SNIPPET] as $hlField ) {
105                // Make sure to find the redirect title before escaping because escaping breaks it....
106                if ( !isset( $highlights[$hlField->getFieldName()][0] ) ) {
107                    continue;
108                }
109                $redirTitle = $this->findRedirectTitle( $result, $highlights[$hlField->getFieldName()][0] );
110                if ( $redirTitle !== null ) {
111                    $this->builder
112                        ->redirectTitle( $redirTitle )
113                        ->redirectSnippet( $this->escapeHighlightedText( $highlights[$hlField->getFieldName()][0] ) )
114                        ->redirectSnippetField( $hlField->getFieldName() );
115                    break;
116                }
117            }
118        }
119    }
120
121    private function doMainSnippet( array $highlights ) {
122        $hasTextSnippet = false;
123        foreach ( $this->highligtedFields[ArrayCirrusSearchResult::TEXT_SNIPPET] as $hlField ) {
124            if ( isset( $highlights[$hlField->getFieldName()][0] ) ) {
125                $snippet = $highlights[$hlField->getFieldName()][0];
126                if ( $this->containsMatches( $snippet ) ) {
127                    $this->builder
128                        ->textSnippet( $this->escapeHighlightedText( $snippet ) )
129                        ->textSnippetField( $hlField->getFieldName() )
130                        ->fileMatch( $hlField->getFieldName() === 'file_text' );
131                    $hasTextSnippet = true;
132                    break;
133                }
134            }
135        }
136
137        // Hardcode the fallback to the "text" highlight, it generally contains the beginning of the
138        // text content if nothing has matched.
139        if ( !$hasTextSnippet && isset( $highlights['text'][0] ) ) {
140            $this->builder
141                ->textSnippet( $this->escapeHighlightedText( $highlights['text'][0] ) )
142                ->textSnippetField( 'text' );
143        }
144    }
145
146    private function doHeadings( Title $title, array $highlights ) {
147        if ( !isset( $this->highligtedFields[ArrayCirrusSearchResult::SECTION_SNIPPET] ) ) {
148            return;
149        }
150        foreach ( $this->highligtedFields[ArrayCirrusSearchResult::SECTION_SNIPPET] as $hlField ) {
151            if ( isset( $highlights[$hlField->getFieldName()] ) ) {
152                $this->builder
153                    ->sectionSnippet( $this->escapeHighlightedText( $highlights[$hlField->getFieldName()][0] ) )
154                    ->sectionSnippetField( $hlField->getFieldName() )
155                    ->sectionTitle( $this->findSectionTitle( $highlights[$hlField->getFieldName()][0], $title ) );
156                break;
157            }
158        }
159    }
160
161    private function doCategory( array $highlights ) {
162        if ( !isset( $this->highligtedFields[ArrayCirrusSearchResult::CATEGORY_SNIPPET] ) ) {
163            return;
164        }
165        foreach ( $this->highligtedFields[ArrayCirrusSearchResult::CATEGORY_SNIPPET] as $hlField ) {
166            if ( isset( $highlights[$hlField->getFieldName()] ) ) {
167                $this->builder
168                    ->categorySnippet( $this->escapeHighlightedText( $highlights[$hlField->getFieldName()][0] ) )
169                    ->categorySnippetField( $hlField->getFieldName() );
170            }
171            break;
172        }
173    }
174}