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 */
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    public function setHighlightQuery( AbstractQuery $highlightQuery ): self {
124        $this->highlightQuery = $highlightQuery;
125
126        return $this;
127    }
128
129    /**
130     * @return AbstractQuery|null
131     */
132    public function getHighlightQuery() {
133        return $this->highlightQuery;
134    }
135
136    /**
137     * @inheritDoc
138     */
139    public function merge( HighlightedField $other ): HighlightedField {
140        if ( $this->getFieldName() !== $other->getFieldName() ) {
141            throw new \InvalidArgumentException(
142                "Rejecting nonsense merge: Refusing to merge two HighlightFields with different field names: " .
143            "[{$other->getFieldName()}] != [{$this->getFieldName()}]" );
144        }
145        if ( $other instanceof BaseHighlightedField && $this->canMerge( $other ) ) {
146            if ( $this->highlightQuery instanceof BoolQuery ) {
147                $this->highlightQuery->addShould( $other->highlightQuery );
148            } else {
149                $thisQuery = $this->highlightQuery;
150                $otherQuery = $other->highlightQuery;
151                Assert::precondition( $thisQuery !== null && $otherQuery !== null, 'highlightQuery not null' );
152                $this->highlightQuery = new BoolQuery();
153                $this->highlightQuery->addShould( $thisQuery );
154                $this->highlightQuery->addShould( $otherQuery );
155            }
156            return $this;
157        } elseif ( $this->getPriority() >= $other->getPriority() ) {
158            return $this;
159        } else {
160            return $other;
161        }
162    }
163
164    /**
165     * @param BaseHighlightedField $other
166     * @return bool
167     */
168    private function canMerge( BaseHighlightedField $other ) {
169        if ( $this->highlighterType !== $other->highlighterType ) {
170            return false;
171        }
172        if ( $this->getTarget() !== $other->getTarget() ) {
173            return false;
174        }
175        if ( $this->highlightQuery === null || $other->highlightQuery === null ) {
176            return false;
177        }
178        if ( $this->matchedFields !== $other->matchedFields ) {
179            return false;
180        }
181        if ( $this->getFragmenter() !== $other->getFragmenter() ) {
182            return false;
183        }
184        if ( $this->getNumberOfFragments() !== $other->getNumberOfFragments() ) {
185            return false;
186        }
187        if ( $this->getNoMatchSize() !== $other->getNoMatchSize() ) {
188            return false;
189        }
190        if ( $this->options !== $other->options ) {
191            return false;
192        }
193        return true;
194    }
195
196    public function setOptions( array $options ) {
197        $this->options = $options;
198    }
199
200    public function getOptions(): array {
201        return $this->options;
202    }
203
204    /**
205     * @return int|null
206     */
207    public function getNumberOfFragments() {
208        return $this->numberOfFragments;
209    }
210
211    /**
212     * @return string
213     */
214    public function getHighlighterType() {
215        return $this->highlighterType;
216    }
217
218    /**
219     * @return string|null
220     */
221    public function getFragmenter() {
222        return $this->fragmenter;
223    }
224
225    /**
226     * @return int|null
227     */
228    public function getFragmentSize() {
229        return $this->fragmentSize;
230    }
231
232    /**
233     * @return int|null
234     */
235    public function getNoMatchSize() {
236        return $this->noMatchSize;
237    }
238
239    /**
240     * @return string[]
241     */
242    public function getMatchedFields(): array {
243        return $this->matchedFields;
244    }
245
246    /**
247     * @return string|null
248     */
249    public function getOrder() {
250        return $this->order;
251    }
252
253    /**
254     * @return array
255     */
256    public function toArray() {
257        $output = [
258            'type' => $this->highlighterType
259        ];
260
261        if ( $this->numberOfFragments !== null ) {
262            $output['number_of_fragments'] = $this->numberOfFragments;
263        }
264
265        if ( $this->fragmenter !== null ) {
266            $output['fragmenter'] = $this->fragmenter;
267        }
268
269        if ( $this->highlightQuery !== null ) {
270            $output['highlight_query'] = $this->highlightQuery->toArray();
271        }
272        if ( $this->order !== null ) {
273            $output['order'] = $this->order;
274        }
275
276        if ( $this->fragmentSize !== null ) {
277            $output['fragment_size'] = $this->fragmentSize;
278        }
279
280        if ( $this->noMatchSize ) {
281            $output['no_match_size'] = $this->noMatchSize;
282        }
283
284        if ( $this->options !== [] ) {
285            $output['options'] = $this->options;
286        }
287
288        if ( $this->matchedFields !== [] ) {
289            $output['matched_fields'] = $this->matchedFields;
290        }
291
292        return $output;
293    }
294
295    protected static function entireValue(): callable {
296        return static function ( SearchConfig $config, $fieldName, $target, $priority = self::DEFAULT_TARGET_PRIORITY ) {
297            $self = new self( $fieldName, self::FVH_HL_TYPE, $target, $priority );
298            $self->setNumberOfFragments( 0 );
299            $self->setOrder( 'score' );
300            $self->matchPlainFields();
301            return $self;
302        };
303    }
304
305    protected static function redirectAndHeadings(): 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( 1 );
309            $self->matchPlainFields();
310            $self->setFragmentSize( 10000 ); // We want the whole value but more than this is crazy
311            $self->setOrder( 'score' );
312            return $self;
313        };
314    }
315
316    protected static function text(): callable {
317        return static function ( SearchConfig $config, $fieldName, $target, $priority ) {
318            $self = new self( $fieldName, self::FVH_HL_TYPE, $target, $priority );
319            $self->setNumberOfFragments( 1 );
320            $self->matchPlainFields();
321            $self->setOrder( 'score' );
322            $self->setFragmentSize( $config->get( 'CirrusSearchFragmentSize' ) );
323            return $self;
324        };
325    }
326
327    protected static function mainText(): callable {
328        return function ( SearchConfig $config, $fieldName, $target, $priority ) {
329            $self = ( self::text() )( $config, $fieldName, $target, $priority );
330            /** @var BaseHighlightedField $self */
331            $self->setNoMatchSize( $config->get( 'CirrusSearchFragmentSize' ) );
332            return $self;
333        };
334    }
335
336    /**
337     * Skip this field if the previous matched
338     * Optimization available only on the experimental highlighter.
339     */
340    public function skipIfLastMatched(): self {
341        return $this;
342    }
343
344    /**
345     * @return array
346     */
347    public static function getFactories() {
348        return [
349            SearchQuery::SEARCH_TEXT => [
350                'title' => self::entireValue(),
351                'redirect.title' => self::redirectAndHeadings(),
352                'category' => self::redirectAndHeadings(),
353                'heading' => self::redirectAndHeadings(),
354                'text' => self::mainText(),
355                'source_text.plain' => self::mainText(),
356                'auxiliary_text' => self::text(),
357                'file_text' => self::text(),
358            ]
359        ];
360    }
361
362    /**
363     * Helper function to populate the matchedFields array with the additional .plain field.
364     * This only works if the getFieldName() denotes the actual elasticsearch field to highlight
365     * and is not already a plain field.
366     */
367    protected function matchPlainFields() {
368        if ( !str_ends_with( $this->getFieldName(), '.plain' ) ) {
369            $this->matchedFields = [ $this->getFieldName(), $this->getFieldName() . '.plain' ];
370        }
371    }
372}