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