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    /**
51     * @param \Elastica\Result $result
52     * @return CirrusSearchResult
53     */
54    public function build( \Elastica\Result $result ): CirrusSearchResult {
55        $title = $this->getTitleHelper()->makeTitle( $result );
56        $fields = $result->getFields();
57        $builder = $this->newBuilder( $title, $result->getId() )
58            ->wordCount( $fields['text.word_count'][0] ?? 0 )
59            ->byteSize( $result->text_bytes ?? 0 )
60            ->timestamp( new MWTimestamp( $result->timestamp ) )
61            ->score( $result->getScore() )
62            ->explanation( $result->getExplanation() );
63
64        if ( isset( $result->namespace_text ) ) {
65            $builder->interwikiNamespaceText( $result->namespace_text );
66        }
67
68        $highlights = $result->getHighlights();
69        $this->doTitleSnippet( $title, $result, $highlights );
70        $this->doMainSnippet( $highlights );
71        $this->doHeadings( $title, $highlights );
72        $this->doCategory( $highlights );
73        $source = $result->getData();
74
75        foreach ( $this->extraFields as $field ) {
76            if ( isset( $source[$field] ) ) {
77                $builder->addExtraField( $field, $source[$field] );
78            }
79        }
80
81        return $builder->build();
82    }
83
84    /**
85     * @return TitleHelper
86     */
87    protected function getTitleHelper(): TitleHelper {
88        return $this->titleHelper;
89    }
90
91    private function doTitleSnippet( Title $title, \Elastica\Result $result, array $highlights ) {
92        $matched = false;
93        foreach ( $this->highligtedFields[ArrayCirrusSearchResult::TITLE_SNIPPET] as $hlField ) {
94            if ( isset( $highlights[$hlField->getFieldName()] ) ) {
95                $nstext = $title->getNamespace() === 0 ? '' : $this->titleHelper->getNamespaceText( $title ) . ':';
96                $snippet = $nstext . $this->escapeHighlightedText( $highlights[ $hlField->getFieldName() ][ 0 ] );
97                $this->builder
98                    ->titleSnippet( $snippet )
99                    ->titleSnippetField( $hlField->getFieldName() );
100                $matched = true;
101                break;
102            }
103        }
104        if ( !$matched && $title->isExternal() ) {
105            // Interwiki searches are weird. They won't have title highlights by design, but
106            // if we don't return a title snippet we'll get weird display results.
107            $this->builder->titleSnippet( $title->getText() );
108        }
109
110        if ( !$matched && isset( $this->highligtedFields[ArrayCirrusSearchResult::REDIRECT_SNIPPET] ) ) {
111            foreach ( $this->highligtedFields[ArrayCirrusSearchResult::REDIRECT_SNIPPET] as $hlField ) {
112                // Make sure to find the redirect title before escaping because escaping breaks it....
113                if ( !isset( $highlights[$hlField->getFieldName()][0] ) ) {
114                    continue;
115                }
116                $redirTitle = $this->findRedirectTitle( $result, $highlights[$hlField->getFieldName()][0] );
117                if ( $redirTitle !== null ) {
118                    $this->builder
119                        ->redirectTitle( $redirTitle )
120                        ->redirectSnippet( $this->escapeHighlightedText( $highlights[$hlField->getFieldName()][0] ) )
121                        ->redirectSnippetField( $hlField->getFieldName() );
122                    break;
123                }
124            }
125        }
126    }
127
128    private function doMainSnippet( $highlights ) {
129        $hasTextSnippet = false;
130        foreach ( $this->highligtedFields[ArrayCirrusSearchResult::TEXT_SNIPPET] as $hlField ) {
131            if ( isset( $highlights[$hlField->getFieldName()][0] ) ) {
132                $snippet = $highlights[$hlField->getFieldName()][0];
133                if ( $this->containsMatches( $snippet ) ) {
134                    $this->builder
135                        ->textSnippet( $this->escapeHighlightedText( $snippet ) )
136                        ->textSnippetField( $hlField->getFieldName() )
137                        ->fileMatch( $hlField->getFieldName() === 'file_text' );
138                    $hasTextSnippet = true;
139                    break;
140                }
141            }
142        }
143
144        // Hardcode the fallback to the "text" highlight, it generally contains the beginning of the
145        // text content if nothing has matched.
146        if ( !$hasTextSnippet && isset( $highlights['text'][0] ) ) {
147            $this->builder
148                ->textSnippet( $this->escapeHighlightedText( $highlights['text'][0] ) )
149                ->textSnippetField( 'text' );
150        }
151    }
152
153    private function doHeadings( Title $title, $highlights ) {
154        if ( !isset( $this->highligtedFields[ArrayCirrusSearchResult::SECTION_SNIPPET] ) ) {
155            return;
156        }
157        foreach ( $this->highligtedFields[ArrayCirrusSearchResult::SECTION_SNIPPET] as $hlField ) {
158            if ( isset( $highlights[$hlField->getFieldName()] ) ) {
159                $this->builder
160                    ->sectionSnippet( $this->escapeHighlightedText( $highlights[$hlField->getFieldName()][0] ) )
161                    ->sectionSnippetField( $hlField->getFieldName() )
162                    ->sectionTitle( $this->findSectionTitle( $highlights[$hlField->getFieldName()][0], $title ) );
163                break;
164            }
165        }
166    }
167
168    private function doCategory( $highlights ) {
169        if ( !isset( $this->highligtedFields[ArrayCirrusSearchResult::CATEGORY_SNIPPET] ) ) {
170            return;
171        }
172        foreach ( $this->highligtedFields[ArrayCirrusSearchResult::CATEGORY_SNIPPET] as $hlField ) {
173            if ( isset( $highlights[$hlField->getFieldName()] ) ) {
174                $this->builder
175                    ->categorySnippet( $this->escapeHighlightedText( $highlights[$hlField->getFieldName()][0] ) )
176                    ->categorySnippetField( $hlField->getFieldName() );
177            }
178            break;
179        }
180    }
181}