Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
84.80% |
106 / 125 |
|
82.76% |
24 / 29 |
CRAP | |
0.00% |
0 / 1 |
BaseHighlightedField | |
84.80% |
106 / 125 |
|
82.76% |
24 / 29 |
62.86 | |
0.00% |
0 / 1 |
__construct | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
addOption | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
addMatchedField | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
setOrder | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
setNumberOfFragments | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
setFragmenter | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
setFragmentSize | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
setNoMatchSize | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
setHighlightQuery | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
getHighlightQuery | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
merge | |
100.00% |
17 / 17 |
|
100.00% |
1 / 1 |
7 | |||
canMerge | |
100.00% |
17 / 17 |
|
100.00% |
1 / 1 |
10 | |||
setOptions | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getOptions | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getNumberOfFragments | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getHighlighterType | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getFragmenter | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getFragmentSize | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getNoMatchSize | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getMatchedFields | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getOrder | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
toArray | |
95.00% |
19 / 20 |
|
0.00% |
0 / 1 |
9 | |||
entireValue | |
71.43% |
5 / 7 |
|
0.00% |
0 / 1 |
1.02 | |||
redirectAndHeadings | |
75.00% |
6 / 8 |
|
0.00% |
0 / 1 |
1.02 | |||
text | |
100.00% |
8 / 8 |
|
100.00% |
1 / 1 |
1 | |||
mainText | |
60.00% |
3 / 5 |
|
0.00% |
0 / 1 |
1.06 | |||
skipIfLastMatched | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getFactories | |
0.00% |
0 / 12 |
|
0.00% |
0 / 1 |
2 | |||
matchPlainFields | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
2 |
1 | <?php |
2 | |
3 | namespace CirrusSearch\Search\Fetch; |
4 | |
5 | use CirrusSearch\Search\SearchQuery; |
6 | use CirrusSearch\SearchConfig; |
7 | use Elastica\Query\AbstractQuery; |
8 | use Elastica\Query\BoolQuery; |
9 | use Wikimedia\Assert\Assert; |
10 | |
11 | class BaseHighlightedField extends HighlightedField { |
12 | public const TYPE = 'highlighting'; |
13 | |
14 | public const FVH_HL_TYPE = 'fvh'; |
15 | |
16 | /** @var int|null */ |
17 | private $numberOfFragments; |
18 | |
19 | /** @var string */ |
20 | private $highlighterType; |
21 | |
22 | /** @var string|null */ |
23 | private $fragmenter; |
24 | |
25 | /** @var int|null fragmentSize */ |
26 | private $fragmentSize; |
27 | |
28 | /** @var int|null */ |
29 | private $noMatchSize; |
30 | |
31 | /** @var string[] */ |
32 | private $matchedFields = []; |
33 | |
34 | /** @var array */ |
35 | protected $options = []; |
36 | |
37 | /** @var AbstractQuery|null */ |
38 | private $highlightQuery; |
39 | |
40 | /** |
41 | * @var string|null |
42 | */ |
43 | private $order; |
44 | |
45 | /** |
46 | * @param string $fieldName |
47 | * @param string $highlighterType |
48 | * @param string $target |
49 | * @param int $priority |
50 | */ |
51 | public function __construct( $fieldName, $highlighterType, $target, $priority = self::DEFAULT_TARGET_PRIORITY ) { |
52 | parent::__construct( self::TYPE, $fieldName, $target, $priority ); |
53 | $this->highlighterType = $highlighterType; |
54 | } |
55 | |
56 | /** |
57 | * @param string $option |
58 | * @param mixed $value (json serialization value) |
59 | * @return self |
60 | */ |
61 | public function addOption( $option, $value ): self { |
62 | $this->options[$option] = $value; |
63 | return $this; |
64 | } |
65 | |
66 | /** |
67 | * @param string $field |
68 | * @return self |
69 | */ |
70 | public function addMatchedField( $field ): self { |
71 | $this->matchedFields[] = $field; |
72 | return $this; |
73 | } |
74 | |
75 | /** |
76 | * @param string $order |
77 | * @return self |
78 | */ |
79 | public function setOrder( $order ): self { |
80 | $this->order = $order; |
81 | return $this; |
82 | } |
83 | |
84 | /** |
85 | * @param int|null $numberOfFragments |
86 | * @return self |
87 | */ |
88 | public function setNumberOfFragments( $numberOfFragments ): self { |
89 | $this->numberOfFragments = $numberOfFragments; |
90 | |
91 | return $this; |
92 | } |
93 | |
94 | /** |
95 | * @param string|null $fragmenter |
96 | * @return self |
97 | */ |
98 | public function setFragmenter( $fragmenter ): self { |
99 | $this->fragmenter = $fragmenter; |
100 | |
101 | return $this; |
102 | } |
103 | |
104 | /** |
105 | * @param int|null $fragmentSize |
106 | * @return self |
107 | */ |
108 | public function setFragmentSize( $fragmentSize ): self { |
109 | $this->fragmentSize = $fragmentSize; |
110 | |
111 | return $this; |
112 | } |
113 | |
114 | /** |
115 | * @param int|null $noMatchSize |
116 | * @return self |
117 | */ |
118 | public function setNoMatchSize( $noMatchSize ): self { |
119 | $this->noMatchSize = $noMatchSize; |
120 | return $this; |
121 | } |
122 | |
123 | /** |
124 | * @param AbstractQuery $highlightQuery |
125 | * @return self |
126 | */ |
127 | public function setHighlightQuery( AbstractQuery $highlightQuery ): self { |
128 | $this->highlightQuery = $highlightQuery; |
129 | |
130 | return $this; |
131 | } |
132 | |
133 | /** |
134 | * @return AbstractQuery|null |
135 | */ |
136 | public function getHighlightQuery() { |
137 | return $this->highlightQuery; |
138 | } |
139 | |
140 | /** |
141 | * @inheritDoc |
142 | */ |
143 | public function merge( HighlightedField $other ): HighlightedField { |
144 | if ( $this->getFieldName() !== $other->getFieldName() ) { |
145 | throw new \InvalidArgumentException( |
146 | "Rejecting nonsense merge: Refusing to merge two HighlightFields with different field names: " . |
147 | "[{$other->getFieldName()}] != [{$this->getFieldName()}]" ); |
148 | } |
149 | if ( $other instanceof BaseHighlightedField && $this->canMerge( $other ) ) { |
150 | if ( $this->highlightQuery instanceof BoolQuery ) { |
151 | $this->highlightQuery->addShould( $other->highlightQuery ); |
152 | } else { |
153 | $thisQuery = $this->highlightQuery; |
154 | $otherQuery = $other->highlightQuery; |
155 | Assert::precondition( $thisQuery !== null && $otherQuery !== null, 'highlightQuery not null' ); |
156 | $this->highlightQuery = new BoolQuery(); |
157 | $this->highlightQuery->addShould( $thisQuery ); |
158 | $this->highlightQuery->addShould( $otherQuery ); |
159 | } |
160 | return $this; |
161 | } elseif ( $this->getPriority() >= $other->getPriority() ) { |
162 | return $this; |
163 | } else { |
164 | return $other; |
165 | } |
166 | } |
167 | |
168 | /** |
169 | * @param BaseHighlightedField $other |
170 | * @return bool |
171 | */ |
172 | private function canMerge( BaseHighlightedField $other ) { |
173 | if ( $this->highlighterType !== $other->highlighterType ) { |
174 | return false; |
175 | } |
176 | if ( $this->getTarget() !== $other->getTarget() ) { |
177 | return false; |
178 | } |
179 | if ( $this->highlightQuery === null || $other->highlightQuery === null ) { |
180 | return false; |
181 | } |
182 | if ( $this->matchedFields !== $other->matchedFields ) { |
183 | return false; |
184 | } |
185 | if ( $this->getFragmenter() !== $other->getFragmenter() ) { |
186 | return false; |
187 | } |
188 | if ( $this->getNumberOfFragments() !== $other->getNumberOfFragments() ) { |
189 | return false; |
190 | } |
191 | if ( $this->getNoMatchSize() !== $other->getNoMatchSize() ) { |
192 | return false; |
193 | } |
194 | if ( $this->options !== $other->options ) { |
195 | return false; |
196 | } |
197 | return true; |
198 | } |
199 | |
200 | public function setOptions( array $options ) { |
201 | $this->options = $options; |
202 | } |
203 | |
204 | /** |
205 | * @return array |
206 | */ |
207 | public function getOptions(): array { |
208 | return $this->options; |
209 | } |
210 | |
211 | /** |
212 | * @return int|null |
213 | */ |
214 | public function getNumberOfFragments() { |
215 | return $this->numberOfFragments; |
216 | } |
217 | |
218 | /** |
219 | * @return string |
220 | */ |
221 | public function getHighlighterType() { |
222 | return $this->highlighterType; |
223 | } |
224 | |
225 | /** |
226 | * @return string|null |
227 | */ |
228 | public function getFragmenter() { |
229 | return $this->fragmenter; |
230 | } |
231 | |
232 | /** |
233 | * @return int|null |
234 | */ |
235 | public function getFragmentSize() { |
236 | return $this->fragmentSize; |
237 | } |
238 | |
239 | /** |
240 | * @return int|null |
241 | */ |
242 | public function getNoMatchSize() { |
243 | return $this->noMatchSize; |
244 | } |
245 | |
246 | /** |
247 | * @return string[] |
248 | */ |
249 | public function getMatchedFields(): array { |
250 | return $this->matchedFields; |
251 | } |
252 | |
253 | /** |
254 | * @return string|null |
255 | */ |
256 | public function getOrder() { |
257 | return $this->order; |
258 | } |
259 | |
260 | /** |
261 | * @return array |
262 | */ |
263 | public function toArray() { |
264 | $output = [ |
265 | 'type' => $this->highlighterType |
266 | ]; |
267 | |
268 | if ( $this->numberOfFragments !== null ) { |
269 | $output['number_of_fragments'] = $this->numberOfFragments; |
270 | } |
271 | |
272 | if ( $this->fragmenter !== null ) { |
273 | $output['fragmenter'] = $this->fragmenter; |
274 | } |
275 | |
276 | if ( $this->highlightQuery !== null ) { |
277 | $output['highlight_query'] = $this->highlightQuery->toArray(); |
278 | } |
279 | if ( $this->order !== null ) { |
280 | $output['order'] = $this->order; |
281 | } |
282 | |
283 | if ( $this->fragmentSize !== null ) { |
284 | $output['fragment_size'] = $this->fragmentSize; |
285 | } |
286 | |
287 | if ( $this->noMatchSize ) { |
288 | $output['no_match_size'] = $this->noMatchSize; |
289 | } |
290 | |
291 | if ( $this->options !== [] ) { |
292 | $output['options'] = $this->options; |
293 | } |
294 | |
295 | if ( $this->matchedFields !== [] ) { |
296 | $output['matched_fields'] = $this->matchedFields; |
297 | } |
298 | |
299 | return $output; |
300 | } |
301 | |
302 | /** |
303 | * @return callable |
304 | */ |
305 | protected static function entireValue(): callable { |
306 | return static function ( SearchConfig $config, $fieldName, $target, $priority = self::DEFAULT_TARGET_PRIORITY ) { |
307 | $self = new self( $fieldName, self::FVH_HL_TYPE, $target, $priority ); |
308 | $self->setNumberOfFragments( 0 ); |
309 | $self->setOrder( 'score' ); |
310 | $self->matchPlainFields(); |
311 | return $self; |
312 | }; |
313 | } |
314 | |
315 | /** |
316 | * @return callable |
317 | */ |
318 | protected static function redirectAndHeadings(): callable { |
319 | return static function ( SearchConfig $config, $fieldName, $target, $priority = self::DEFAULT_TARGET_PRIORITY ) { |
320 | $self = new self( $fieldName, self::FVH_HL_TYPE, $target, $priority ); |
321 | $self->setNumberOfFragments( 1 ); |
322 | $self->matchPlainFields(); |
323 | $self->setFragmentSize( 10000 ); // We want the whole value but more than this is crazy |
324 | $self->setOrder( 'score' ); |
325 | return $self; |
326 | }; |
327 | } |
328 | |
329 | /** |
330 | * @return callable |
331 | */ |
332 | protected static function text(): callable { |
333 | return static function ( SearchConfig $config, $fieldName, $target, $priority ) { |
334 | $self = new self( $fieldName, self::FVH_HL_TYPE, $target, $priority ); |
335 | $self->setNumberOfFragments( 1 ); |
336 | $self->matchPlainFields(); |
337 | $self->setOrder( 'score' ); |
338 | $self->setFragmentSize( $config->get( 'CirrusSearchFragmentSize' ) ); |
339 | return $self; |
340 | }; |
341 | } |
342 | |
343 | /** |
344 | * @return callable |
345 | */ |
346 | protected static function mainText(): callable { |
347 | return function ( SearchConfig $config, $fieldName, $target, $priority ) { |
348 | $self = ( self::text() )( $config, $fieldName, $target, $priority ); |
349 | /** @var BaseHighlightedField $self */ |
350 | $self->setNoMatchSize( $config->get( 'CirrusSearchFragmentSize' ) ); |
351 | return $self; |
352 | }; |
353 | } |
354 | |
355 | /** |
356 | * Skip this field if the previous matched |
357 | * Optimization available only on the experimental highlighter. |
358 | * @return self |
359 | */ |
360 | public function skipIfLastMatched(): self { |
361 | return $this; |
362 | } |
363 | |
364 | public static function getFactories() { |
365 | return [ |
366 | SearchQuery::SEARCH_TEXT => [ |
367 | 'title' => self::entireValue(), |
368 | 'redirect.title' => self::redirectAndHeadings(), |
369 | 'category' => self::redirectAndHeadings(), |
370 | 'heading' => self::redirectAndHeadings(), |
371 | 'text' => self::mainText(), |
372 | 'source_text.plain' => self::mainText(), |
373 | 'auxiliary_text' => self::text(), |
374 | 'file_text' => self::text(), |
375 | ] |
376 | ]; |
377 | } |
378 | |
379 | /** |
380 | * Helper function to populate the matchedFields array with the additional .plain field. |
381 | * This only works if the getFieldName() denotes the actual elasticsearch field to highlight |
382 | * and is not already a plain field. |
383 | */ |
384 | protected function matchPlainFields() { |
385 | if ( substr_compare( $this->getFieldName(), '.plain', -strlen( '.plain' ) ) !== 0 ) { |
386 | $this->matchedFields = [ $this->getFieldName(), $this->getFieldName() . '.plain' ]; |
387 | } |
388 | } |
389 | } |