Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
96.39% |
80 / 83 |
|
62.50% |
5 / 8 |
CRAP | |
0.00% |
0 / 1 |
FullTextCirrusSearchResultBuilder | |
96.39% |
80 / 83 |
|
62.50% |
5 / 8 |
33 | |
0.00% |
0 / 1 |
__construct | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
1 | |||
newBuilder | |
75.00% |
3 / 4 |
|
0.00% |
0 / 1 |
2.06 | |||
build | |
100.00% |
20 / 20 |
|
100.00% |
1 / 1 |
4 | |||
getTitleHelper | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
doTitleSnippet | |
100.00% |
23 / 23 |
|
100.00% |
1 / 1 |
11 | |||
doMainSnippet | |
100.00% |
15 / 15 |
|
100.00% |
1 / 1 |
6 | |||
doHeadings | |
88.89% |
8 / 9 |
|
0.00% |
0 / 1 |
4.02 | |||
doCategory | |
87.50% |
7 / 8 |
|
0.00% |
0 / 1 |
4.03 |
1 | <?php |
2 | |
3 | namespace CirrusSearch\Search; |
4 | |
5 | use CirrusSearch\Search\Fetch\HighlightedField; |
6 | use CirrusSearch\Search\Fetch\HighlightingTrait; |
7 | use MediaWiki\Title\Title; |
8 | use MWTimestamp; |
9 | |
10 | class 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 | } |