Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
98.55% covered (success)
98.55%
68 / 69
91.67% covered (success)
91.67%
11 / 12
CRAP
0.00% covered (danger)
0.00%
0 / 1
FetchPhaseConfigBuilder
98.55% covered (success)
98.55%
68 / 69
91.67% covered (success)
91.67%
11 / 12
23
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 newHighlightField
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
5
 addNewRegexHLField
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 supportsRegexFields
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 newRegexField
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 addHLField
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 getHLField
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 buildHLConfig
100.00% covered (success)
100.00%
14 / 14
100.00% covered (success)
100.00%
1 / 1
4
 withConfig
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getHLFieldsPerTargetAndPriority
100.00% covered (success)
100.00%
12 / 12
100.00% covered (success)
100.00%
1 / 1
2
 configureDefaultFullTextFields
100.00% covered (success)
100.00%
14 / 14
100.00% covered (success)
100.00%
1 / 1
1
 clearSkipIfLastMatched
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
1<?php
2
3namespace CirrusSearch\Search\Fetch;
4
5use CirrusSearch\SearchConfig;
6use CirrusSearch\Searcher;
7use Elastica\Query\AbstractQuery;
8use Wikimedia\Assert\Assert;
9
10/**
11 * Class holding the building state of the fetch phase elements of
12 * an elasticsearch query.
13 * Currently only supports the highlight section but can be extended to support
14 * source filtering and stored field.
15 */
16class FetchPhaseConfigBuilder implements HighlightFieldGenerator {
17
18    /** @var HighlightedField[] */
19    private $highlightedFields = [];
20
21    /** @var SearchConfig */
22    private $config;
23
24    /**
25     * @var string
26     */
27    private $factoryGroup;
28
29    /**
30     * @var bool
31     */
32    private $provideAllSnippets;
33
34    /**
35     * @param SearchConfig $config
36     * @param string|null $factoryGroup
37     * @param bool $provideAllSnippets
38     */
39    public function __construct(
40        SearchConfig $config,
41        $factoryGroup = null,
42        bool $provideAllSnippets = false
43    ) {
44        $this->config = $config;
45        $this->factoryGroup = $factoryGroup;
46        $this->provideAllSnippets = $provideAllSnippets;
47    }
48
49    /**
50     * @inheritDoc
51     */
52    public function newHighlightField(
53        $name,
54        $target,
55        $priority = HighlightedField::DEFAULT_TARGET_PRIORITY
56    ): BaseHighlightedField {
57        $useExp = $this->config->get( 'CirrusSearchUseExperimentalHighlighter' );
58        if ( $useExp ) {
59            $factories = ExperimentalHighlightedFieldBuilder::getFactories();
60        } else {
61            $factories = BaseHighlightedField::getFactories();
62        }
63        if ( $this->factoryGroup !== null && isset( $factories[$this->factoryGroup][$name] ) ) {
64            return ( $factories[$this->factoryGroup][$name] )( $this->config, $name, $target, $priority );
65        }
66        if ( $useExp ) {
67            return new ExperimentalHighlightedFieldBuilder( $name, $target, $priority );
68        } else {
69            return new BaseHighlightedField( $name, BaseHighlightedField::FVH_HL_TYPE, $target, $priority );
70        }
71    }
72
73    /**
74     * @param string $name
75     * @param string $target
76     * @param string $pattern
77     * @param bool $caseInsensitive
78     * @param int $priority
79     */
80    public function addNewRegexHLField(
81        $name,
82        $target,
83        $pattern,
84        $caseInsensitive,
85        $priority = HighlightedField::COSTLY_EXPERT_SYNTAX_PRIORITY
86    ) {
87        if ( !$this->supportsRegexFields() ) {
88            return;
89        }
90        $this->addHLField( $this->newRegexField( $name, $target, $pattern, $caseInsensitive, $priority ) );
91    }
92
93    /**
94     * Whether this builder can generate regex fields
95     * @return bool
96     */
97    public function supportsRegexFields() {
98        return (bool)$this->config->get( 'CirrusSearchUseExperimentalHighlighter' );
99    }
100
101    /**
102     * @inheritDoc
103     */
104    public function newRegexField(
105        $name,
106        $target,
107        $pattern,
108        $caseInsensitive,
109        $priority = HighlightedField::COSTLY_EXPERT_SYNTAX_PRIORITY
110    ): BaseHighlightedField {
111        Assert::precondition( $this->supportsRegexFields(), 'Regex fields not supported' );
112        return ExperimentalHighlightedFieldBuilder::newRegexField(
113            $this->config, $name, $target, $pattern, $caseInsensitive, $priority );
114    }
115
116    /**
117     * @param HighlightedField $field
118     */
119    public function addHLField( HighlightedField $field ) {
120        $prev = $this->highlightedFields[$field->getFieldName()] ?? null;
121        if ( $prev === null ) {
122            $this->highlightedFields[$field->getFieldName()] = $field;
123        } else {
124            $this->highlightedFields[$field->getFieldName()] = $prev->merge( $field );
125        }
126    }
127
128    /**
129     * @param string $field
130     * @return HighlightedField|null
131     */
132    public function getHLField( $field ) {
133        return $this->highlightedFields[$field] ?? null;
134    }
135
136    /**
137     * @param AbstractQuery|null $mainHLQuery
138     * @return array
139     */
140    public function buildHLConfig( AbstractQuery $mainHLQuery = null ): array {
141        $fields = [];
142        foreach ( $this->highlightedFields as $field ) {
143            $arr = $field->toArray();
144            if ( $this->provideAllSnippets ) {
145                $arr = $this->clearSkipIfLastMatched( $arr );
146            }
147            $fields[$field->getFieldName()] = $arr;
148        }
149        $config = [
150            'pre_tags' => [ Searcher::HIGHLIGHT_PRE_MARKER ],
151            'post_tags' => [ Searcher::HIGHLIGHT_POST_MARKER ],
152            'fields' => $fields,
153        ];
154
155        if ( $mainHLQuery !== null ) {
156            $config['highlight_query'] = $mainHLQuery->toArray();
157        }
158
159        return $config;
160    }
161
162    /**
163     * @param SearchConfig $config
164     * @return FetchPhaseConfigBuilder
165     */
166    public function withConfig( SearchConfig $config ): self {
167        return new self( $config, $this->factoryGroup );
168    }
169
170    /**
171     * Return the list of highlighted fields indexed per target
172     * and ordered by priority (reverse natural order)
173     * @return HighlightedField[][]
174     */
175    public function getHLFieldsPerTargetAndPriority(): array {
176        $fields = [];
177        foreach ( $this->highlightedFields as $f ) {
178            $fields[$f->getTarget()][] = $f;
179        }
180        return array_map(
181            static function ( array $v ) {
182                usort( $v, static function ( HighlightedField $g1, HighlightedField $g2 ) {
183                    return $g2->getPriority() <=> $g1->getPriority();
184                } );
185                return $v;
186            },
187            $fields
188        );
189    }
190
191    public function configureDefaultFullTextFields() {
192        // TODO: find a better place for this
193        // Title/redir/category/template
194        $field = $this->newHighlightField( 'title', HighlightedField::TARGET_TITLE_SNIPPET );
195        $this->addHLField( $field );
196        $field = $this->newHighlightField( 'redirect.title', HighlightedField::TARGET_REDIRECT_SNIPPET );
197        $this->addHLField( $field->skipIfLastMatched() );
198        $field = $this->newHighlightField( 'category', HighlightedField::TARGET_CATEGORY_SNIPPET );
199        $this->addHLField( $field->skipIfLastMatched() );
200
201        $field = $this->newHighlightField( 'heading', HighlightedField::TARGET_SECTION_SNIPPET );
202        $this->addHLField( $field->skipIfLastMatched() );
203
204        // content
205        $field = $this->newHighlightField( 'text', HighlightedField::TARGET_MAIN_SNIPPET );
206        $this->addHLField( $field );
207
208        $field = $this->newHighlightField( 'auxiliary_text', HighlightedField::TARGET_MAIN_SNIPPET );
209        $this->addHLField( $field->skipIfLastMatched() );
210
211        $field = $this->newHighlightField( 'file_text', HighlightedField::TARGET_MAIN_SNIPPET );
212        $this->addHLField( $field->skipIfLastMatched() );
213    }
214
215    private function clearSkipIfLastMatched( array $arr ) {
216        unset( $arr['options']['skip_if_last_matched'] );
217        if ( empty( $arr['options'] ) ) {
218            unset( $arr['options'] );
219        }
220        return $arr;
221    }
222}