Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
95.77% covered (success)
95.77%
68 / 71
62.50% covered (warning)
62.50%
5 / 8
CRAP
0.00% covered (danger)
0.00%
0 / 1
FullTextCirrusSearchResultBuilder
95.77% covered (success)
95.77%
68 / 71
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%
19 / 19
100.00% covered (success)
100.00%
1 / 1
11
 doMainSnippet
100.00% covered (success)
100.00%
11 / 11
100.00% covered (success)
100.00%
1 / 1
6
 doHeadings
85.71% covered (warning)
85.71%
6 / 7
0.00% covered (danger)
0.00%
0 / 1
4.05
 doCategory
83.33% covered (warning)
83.33%
5 / 6
0.00% covered (danger)
0.00%
0 / 1
4.07
1<?php
2
3namespace CirrusSearch\Search;
4
5use CirrusSearch\Search\Fetch\HighlightedField;
6use CirrusSearch\Search\Fetch\HighlightingTrait;
7use MWTimestamp;
8use Title;
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 ? '' :
96                    $this->titleHelper->getNamespaceText( $title ) . ':';
97                $this->builder->titleSnippet( $nstext . $this->escapeHighlightedText( $highlights[ $hlField->getFieldName() ][ 0 ] ) );
98                $matched = true;
99                break;
100            }
101        }
102        if ( !$matched && $title->isExternal() ) {
103            // Interwiki searches are weird. They won't have title highlights by design, but
104            // if we don't return a title snippet we'll get weird display results.
105            $this->builder->titleSnippet( $title->getText() );
106        }
107
108        if ( !$matched && isset( $this->highligtedFields[ArrayCirrusSearchResult::REDIRECT_SNIPPET] ) ) {
109            foreach ( $this->highligtedFields[ArrayCirrusSearchResult::REDIRECT_SNIPPET] as $hlField ) {
110                // Make sure to find the redirect title before escaping because escaping breaks it....
111                if ( !isset( $highlights[$hlField->getFieldName()][0] ) ) {
112                    continue;
113                }
114                $redirTitle = $this->findRedirectTitle( $result, $highlights[$hlField->getFieldName()][0] );
115                if ( $redirTitle !== null ) {
116                    $this->builder->redirectTitle( $redirTitle )
117                        ->redirectSnippet( $this->escapeHighlightedText( $highlights[$hlField->getFieldName()][0] ) );
118                    break;
119                }
120            }
121        }
122    }
123
124    private function doMainSnippet( $highlights ) {
125        $hasTextSnippet = false;
126        foreach ( $this->highligtedFields[ArrayCirrusSearchResult::TEXT_SNIPPET] as $hlField ) {
127            if ( isset( $highlights[$hlField->getFieldName()][0] ) ) {
128                $snippet = $highlights[$hlField->getFieldName()][0];
129                if ( $this->containsMatches( $snippet ) ) {
130                    $this->builder->textSnippet( $this->escapeHighlightedText( $snippet ) )->
131                        fileMatch( $hlField->getFieldName() === 'file_text' );
132                    $hasTextSnippet = true;
133                    break;
134                }
135            }
136        }
137
138        // Hardcode the fallback to the "text" highlight, it generally contains the beginning of the
139        // text content if nothing has matched.
140        if ( !$hasTextSnippet && isset( $highlights['text'][0] ) ) {
141            $this->builder->textSnippet( $this->escapeHighlightedText( $highlights['text'][0] ) );
142        }
143    }
144
145    private function doHeadings( Title $title, $highlights ) {
146        if ( !isset( $this->highligtedFields[ArrayCirrusSearchResult::SECTION_SNIPPET] ) ) {
147            return;
148        }
149        foreach ( $this->highligtedFields[ArrayCirrusSearchResult::SECTION_SNIPPET] as $hlField ) {
150            if ( isset( $highlights[$hlField->getFieldName()] ) ) {
151                $this->builder->sectionSnippet( $this->escapeHighlightedText( $highlights[$hlField->getFieldName()][0] ) )
152                    ->sectionTitle( $this->findSectionTitle( $highlights[$hlField->getFieldName()][0], $title ) );
153                break;
154            }
155        }
156    }
157
158    private function doCategory( $highlights ) {
159        if ( !isset( $this->highligtedFields[ArrayCirrusSearchResult::CATEGORY_SNIPPET] ) ) {
160            return;
161        }
162        foreach ( $this->highligtedFields[ArrayCirrusSearchResult::CATEGORY_SNIPPET] as $hlField ) {
163            if ( isset( $highlights[$hlField->getFieldName()] ) ) {
164                $this->builder->categorySnippet( $this->escapeHighlightedText( $highlights[$hlField->getFieldName()][0] ) );
165            }
166            break;
167        }
168    }
169}