Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
84.80% covered (warning)
84.80%
106 / 125
82.76% covered (warning)
82.76%
24 / 29
CRAP
0.00% covered (danger)
0.00%
0 / 1
BaseHighlightedField
84.80% covered (warning)
84.80%
106 / 125
82.76% covered (warning)
82.76%
24 / 29
62.86
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 addOption
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 addMatchedField
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 setOrder
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 setNumberOfFragments
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 setFragmenter
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 setFragmentSize
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 setNoMatchSize
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 setHighlightQuery
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 getHighlightQuery
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 merge
100.00% covered (success)
100.00%
17 / 17
100.00% covered (success)
100.00%
1 / 1
7
 canMerge
100.00% covered (success)
100.00%
17 / 17
100.00% covered (success)
100.00%
1 / 1
10
 setOptions
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getOptions
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getNumberOfFragments
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getHighlighterType
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getFragmenter
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getFragmentSize
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getNoMatchSize
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getMatchedFields
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getOrder
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 toArray
95.00% covered (success)
95.00%
19 / 20
0.00% covered (danger)
0.00%
0 / 1
9
 entireValue
71.43% covered (warning)
71.43%
5 / 7
0.00% covered (danger)
0.00%
0 / 1
1.02
 redirectAndHeadings
75.00% covered (warning)
75.00%
6 / 8
0.00% covered (danger)
0.00%
0 / 1
1.02
 text
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
1
 mainText
60.00% covered (warning)
60.00%
3 / 5
0.00% covered (danger)
0.00%
0 / 1
1.06
 skipIfLastMatched
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getFactories
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 1
2
 matchPlainFields
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
2
1<?php
2
3namespace CirrusSearch\Search\Fetch;
4
5use CirrusSearch\Search\SearchQuery;
6use CirrusSearch\SearchConfig;
7use Elastica\Query\AbstractQuery;
8use Elastica\Query\BoolQuery;
9use Wikimedia\Assert\Assert;
10
11class 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}