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        $regexFlavor = 'lucene'
111    ): BaseHighlightedField {
112        Assert::precondition( $this->supportsRegexFields(), 'Regex fields not supported' );
113        return ExperimentalHighlightedFieldBuilder::newRegexField(
114            $this->config, $name, $target, $pattern, $caseInsensitive, $priority, $regexFlavor );
115    }
116
117    public function addHLField( HighlightedField $field ) {
118        $prev = $this->highlightedFields[$field->getFieldName()] ?? null;
119        if ( $prev === null ) {
120            $this->highlightedFields[$field->getFieldName()] = $field;
121        } else {
122            $this->highlightedFields[$field->getFieldName()] = $prev->merge( $field );
123        }
124    }
125
126    /**
127     * @param string $field
128     * @return HighlightedField|null
129     */
130    public function getHLField( $field ) {
131        return $this->highlightedFields[$field] ?? null;
132    }
133
134    /**
135     * @param AbstractQuery|null $mainHLQuery
136     * @return array
137     */
138    public function buildHLConfig( ?AbstractQuery $mainHLQuery = null ): array {
139        $fields = [];
140        foreach ( $this->highlightedFields as $field ) {
141            $arr = $field->toArray();
142            if ( $this->provideAllSnippets ) {
143                $arr = $this->clearSkipIfLastMatched( $arr );
144            }
145            $fields[$field->getFieldName()] = $arr;
146        }
147        $config = [
148            'pre_tags' => [ Searcher::HIGHLIGHT_PRE_MARKER ],
149            'post_tags' => [ Searcher::HIGHLIGHT_POST_MARKER ],
150            'fields' => $fields,
151        ];
152
153        if ( $mainHLQuery !== null ) {
154            $config['highlight_query'] = $mainHLQuery->toArray();
155        }
156
157        return $config;
158    }
159
160    public function withConfig( SearchConfig $config ): self {
161        return new self( $config, $this->factoryGroup );
162    }
163
164    /**
165     * Return the list of highlighted fields indexed per target
166     * and ordered by priority (reverse natural order)
167     * @return HighlightedField[][]
168     */
169    public function getHLFieldsPerTargetAndPriority(): array {
170        $fields = [];
171        foreach ( $this->highlightedFields as $f ) {
172            $fields[$f->getTarget()][] = $f;
173        }
174        return array_map(
175            static function ( array $v ) {
176                usort( $v, static function ( HighlightedField $g1, HighlightedField $g2 ) {
177                    return $g2->getPriority() <=> $g1->getPriority();
178                } );
179                return $v;
180            },
181            $fields
182        );
183    }
184
185    public function configureDefaultFullTextFields() {
186        // TODO: find a better place for this
187        // Title/redir/category/template
188        $field = $this->newHighlightField( 'title', HighlightedField::TARGET_TITLE_SNIPPET );
189        $this->addHLField( $field );
190        $field = $this->newHighlightField( 'redirect.title', HighlightedField::TARGET_REDIRECT_SNIPPET );
191        $this->addHLField( $field->skipIfLastMatched() );
192        $field = $this->newHighlightField( 'category', HighlightedField::TARGET_CATEGORY_SNIPPET );
193        $this->addHLField( $field->skipIfLastMatched() );
194
195        $field = $this->newHighlightField( 'heading', HighlightedField::TARGET_SECTION_SNIPPET );
196        $this->addHLField( $field->skipIfLastMatched() );
197
198        // content
199        $field = $this->newHighlightField( 'text', HighlightedField::TARGET_MAIN_SNIPPET );
200        $this->addHLField( $field );
201
202        $field = $this->newHighlightField( 'auxiliary_text', HighlightedField::TARGET_MAIN_SNIPPET );
203        $this->addHLField( $field->skipIfLastMatched() );
204
205        $field = $this->newHighlightField( 'file_text', HighlightedField::TARGET_MAIN_SNIPPET );
206        $this->addHLField( $field->skipIfLastMatched() );
207    }
208
209    private function clearSkipIfLastMatched( array $arr ): array {
210        unset( $arr['options']['skip_if_last_matched'] );
211        if ( empty( $arr['options'] ) ) {
212            unset( $arr['options'] );
213        }
214        return $arr;
215    }
216}