Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
75.68% covered (warning)
75.68%
56 / 74
55.56% covered (warning)
55.56%
5 / 9
CRAP
0.00% covered (danger)
0.00%
0 / 1
ExperimentalHighlightedFieldBuilder
75.68% covered (warning)
75.68%
56 / 74
55.56% covered (warning)
55.56%
5 / 9
18.24
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 entireValue
71.43% covered (warning)
71.43%
5 / 7
0.00% covered (danger)
0.00%
0 / 1
1.02
 redirectAndHeadings
77.78% covered (warning)
77.78%
7 / 9
0.00% covered (danger)
0.00%
0 / 1
1.01
 text
100.00% covered (success)
100.00%
19 / 19
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
 newRegexField
100.00% covered (success)
100.00%
10 / 10
100.00% covered (success)
100.00%
1 / 1
2
 merge
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
6
 skipIfLastMatched
100.00% covered (success)
100.00%
2 / 2
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
1<?php
2
3namespace CirrusSearch\Search\Fetch;
4
5use CirrusSearch\Search\SearchQuery;
6use CirrusSearch\SearchConfig;
7
8class ExperimentalHighlightedFieldBuilder extends BaseHighlightedField {
9    public const EXPERIMENTAL_HL_TYPE = 'experimental';
10
11    /**
12     * @param string $fieldName
13     * @param string $target
14     * @param int $priority
15     */
16    public function __construct( $fieldName, $target, $priority = self::DEFAULT_TARGET_PRIORITY ) {
17        parent::__construct( $fieldName, self::EXPERIMENTAL_HL_TYPE, $target, $priority );
18    }
19
20    public static function entireValue(): callable {
21        return static function ( SearchConfig $config, $fieldName, $target, $priority ) {
22            $self = new self( $fieldName, $target, $priority );
23            $self->matchPlainFields();
24            $self->setFragmenter( 'none' );
25            $self->setNumberOfFragments( 1 );
26            return $self;
27        };
28    }
29
30    public static function redirectAndHeadings(): callable {
31        return static function ( SearchConfig $config, $fieldName, $target, $priority ) {
32            $self = new self( $fieldName, $target, $priority );
33            $self->matchPlainFields();
34            $self->addOption( 'skip_if_last_matched', true );
35            $self->setFragmenter( 'none' );
36            $self->setOrder( 'score' );
37            $self->setNumberOfFragments( 1 );
38            return $self;
39        };
40    }
41
42    public static function text(): callable {
43        return static function ( SearchConfig $config, $fieldName, $target, $priority ) {
44            $self = new self( $fieldName, $target, $priority );
45            $self->matchPlainFields();
46            $self->addOption( 'skip_if_last_matched', true );
47            $self->setFragmenter( 'scan' );
48            $self->setNumberOfFragments( 1 );
49            $self->setFragmentSize( $config->get( 'CirrusSearchFragmentSize' ) );
50            $self->setOptions( [
51                'top_scoring' => true,
52                'boost_before' => [
53                    // Note these values are super arbitrary right now.
54                    '20' => 2,
55                    '50' => 1.8,
56                    '200' => 1.5,
57                    '1000' => 1.2,
58                ],
59                // We should set a limit on the number of fragments we try because if we
60                // don't then we'll hit really crazy documents, say 10MB of "d d".  This'll
61                // keep us from scanning more then the first couple thousand of them.
62                // Setting this too low (like 50) can bury good snippets if the search
63                // contains common words.
64                'max_fragments_scored' => 5000,
65            ] );
66            return $self;
67        };
68    }
69
70    protected static function mainText(): callable {
71        return function ( SearchConfig $config, $fieldName, $target, $priority ) {
72            $self = ( self::text() )( $config, $fieldName, $target, $priority );
73            /** @var BaseHighlightedField $self */
74            $self->setNoMatchSize( $config->get( 'CirrusSearchFragmentSize' ) );
75            return $self;
76        };
77    }
78
79    /**
80     * @param SearchConfig $config
81     * @param string $name
82     * @param string $target
83     * @param string $pattern
84     * @param bool $caseInsensitive
85     * @param int $priority
86     * @param string $regexFlavor
87     * @return self
88     */
89    public static function newRegexField(
90        SearchConfig $config,
91        $name,
92        $target,
93        $pattern,
94        $caseInsensitive,
95        $priority,
96        $regexFlavor = 'lucene'
97    ): self {
98        // TODO: verify that we actually need to have all the text() options when running a regex
99        /** @var self $self */
100        $self = ( self::text() )( $config, $name, $target, $priority );
101        $self->addOption( 'regex', [ $pattern ] );
102        $self->addOption( 'locale', $config->get( 'LanguageCode' ) );
103        $self->addOption( 'regex_flavor', $regexFlavor );
104        $self->addOption( 'skip_query', true );
105        $self->addOption( 'regex_case_insensitive', $caseInsensitive );
106        $self->addOption( 'max_determinized_states', $config->get( 'CirrusSearchRegexMaxDeterminizedStates' ) );
107
108        if ( $name == 'source_text.plain' ) {
109            $self->setNoMatchSize( $config->get( 'CirrusSearchFragmentSize' ) );
110        }
111        return $self;
112    }
113
114    /**
115     * @inheritDoc
116     */
117    public function merge( HighlightedField $other ): HighlightedField {
118        if ( isset( $this->options['regex'] ) &&
119             $other instanceof self &&
120             isset( $other->options['regex'] ) &&
121             $this->getFieldName() === $other->getFieldName()
122        ) {
123            $this->options['regex'] = array_merge( $this->options['regex'], $other->options['regex'] );
124            $mergedInsensitivity = $this->options['regex_case_insensitive'] || $other->options['regex_case_insensitive'];
125            $this->options['regex_case_insensitive'] = $mergedInsensitivity;
126            return $this;
127        } else {
128            return parent::merge( $other );
129        }
130    }
131
132    /**
133     * @return $this
134     */
135    public function skipIfLastMatched(): BaseHighlightedField {
136        $this->addOption( 'skip_if_last_matched', true );
137        return $this;
138    }
139
140    /**
141     * @return array
142     */
143    public static function getFactories() {
144        return [
145            SearchQuery::SEARCH_TEXT => [
146                'title' => self::entireValue(),
147                'redirect.title' => self::redirectAndHeadings(),
148                'category' => self::redirectAndHeadings(),
149                'heading' => self::redirectAndHeadings(),
150                'text' => self::mainText(),
151                'source_text.plain' => self::mainText(),
152                'auxiliary_text' => self::text(),
153                'file_text' => self::text(),
154            ]
155        ];
156    }
157}