Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
97.78% covered (success)
97.78%
44 / 45
90.00% covered (success)
90.00%
9 / 10
CRAP
0.00% covered (danger)
0.00%
0 / 1
WeightedTagsHooks
97.78% covered (success)
97.78%
44 / 45
90.00% covered (success)
90.00%
9 / 10
15
0.00% covered (danger)
0.00%
0 / 1
 create
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 __construct
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getConfig
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 onCirrusSearchSimilarityConfig
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
2
 onSearchIndexFields
90.91% covered (success)
90.91%
10 / 11
0.00% covered (danger)
0.00%
0 / 1
3.01
 onCirrusSearchAnalysisConfig
100.00% covered (success)
100.00%
15 / 15
100.00% covered (success)
100.00%
1 / 1
2
 onCirrusSearchAddQueryFeatures
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 canBuild
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 canUse
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 maxScore
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
1<?php
2
3namespace CirrusSearch\Search;
4
5use CirrusSearch\CirrusConfigNames;
6use CirrusSearch\CirrusSearch;
7use CirrusSearch\Hooks\CirrusSearchAddQueryFeaturesHook;
8use CirrusSearch\Hooks\CirrusSearchAnalysisConfigHook;
9use CirrusSearch\Hooks\CirrusSearchSimilarityConfigHook;
10use CirrusSearch\Maintenance\AnalysisConfigBuilder;
11use CirrusSearch\Query\ArticlePredictionKeyword;
12use CirrusSearch\Query\HasRecommendationFeature;
13use CirrusSearch\SearchConfig;
14use MediaWiki\Config\ConfigFactory;
15use MediaWiki\Search\Hook\SearchIndexFieldsHook;
16
17/**
18 * Functionality related to the weighted_tags search feature.
19 * @package CirrusSearch\Search
20 * @see ArticlePredictionKeyword
21 */
22class WeightedTagsHooks implements
23    SearchIndexFieldsHook,
24    CirrusSearchAddQueryFeaturesHook,
25    CirrusSearchAnalysisConfigHook,
26    CirrusSearchSimilarityConfigHook
27{
28    public const FIELD_NAME = 'weighted_tags';
29    public const FIELD_SIMILARITY = 'weighted_tags_similarity';
30    public const FIELD_INDEX_ANALYZER = 'weighted_tags';
31    public const FIELD_SEARCH_ANALYZER = 'keyword';
32    private SearchConfig $config;
33
34    public static function create( ConfigFactory $configFactory ): WeightedTagsHooks {
35        /** @var SearchConfig $searchConfig */
36        $searchConfig = $configFactory->makeConfig( CirrusSearch::NAME );
37        /** @phan-suppress-next-line PhanTypeMismatchArgumentSuperType $searchConfig is actually a SearchConfig */
38        return new self( $searchConfig );
39    }
40
41    public function __construct( SearchConfig $config ) {
42        $this->config = $config;
43    }
44
45    /**
46     * Visible for testing
47     * @return SearchConfig
48     */
49    public function getConfig(): SearchConfig {
50        return $this->config;
51    }
52
53    /**
54     * @inheritDoc
55     */
56    public function onCirrusSearchSimilarityConfig( array &$similarity ): void {
57        if ( !$this->canBuild() ) {
58            return;
59        }
60        $maxScore = $this->maxScore();
61        $similarity[self::FIELD_SIMILARITY] = [
62            'type' => 'scripted',
63            // no weight=>' script we do not want doc independent weighing
64            'script' => [
65                // apply boost close to docFreq to force int->float conversion
66                'source' => "return (doc.freq*query.boost)/$maxScore;",
67            ],
68        ];
69    }
70
71    /**
72     * @inheritDoc
73     */
74    public function onSearchIndexFields( &$fields, $engine ) {
75        if ( !( $engine instanceof CirrusSearch ) ) {
76            return;
77        }
78        if ( !$this->canBuild() ) {
79            return;
80        }
81
82        $fields[self::FIELD_NAME] = new WeightedTags(
83            self::FIELD_NAME,
84            self::FIELD_NAME,
85            self::FIELD_INDEX_ANALYZER,
86            self::FIELD_SEARCH_ANALYZER,
87            self::FIELD_SIMILARITY
88        );
89    }
90
91    /**
92     * @inheritDoc
93     */
94    public function onCirrusSearchAnalysisConfig( array &$analysisConfig, AnalysisConfigBuilder $analysisConfigBuilder ): void {
95        if ( !$this->canBuild() ) {
96            return;
97        }
98        $maxScore = $this->maxScore();
99        $analysisConfig['analyzer'][self::FIELD_INDEX_ANALYZER] = [
100            'type' => 'custom',
101            'tokenizer' => 'keyword',
102            'filter' => [
103                'weighted_tags_term_freq',
104            ],
105        ];
106        $analysisConfig['filter']['weighted_tags_term_freq'] = [
107            'type' => 'term_freq',
108            // must be a char that never appears in the topic names/ids
109            'split_char' => '|',
110            // max score (clamped), we assume that orig_score * 1000
111            'max_tf' => $maxScore,
112        ];
113    }
114
115    /**
116     * @inheritDoc
117     */
118    public function onCirrusSearchAddQueryFeatures( SearchConfig $config, array &$extraFeatures ): void {
119        if ( $this->canUse() ) {
120            // articletopic keyword, matches by ORES  scores
121            $extraFeatures[] = new ArticlePredictionKeyword();
122            // article recommendations filter
123            $extraFeatures[] = new HasRecommendationFeature( $this->maxScore() );
124        }
125    }
126
127    /**
128     * Check whether weighted_tags data should be processed.
129     * @return bool
130     */
131    private function canBuild(): bool {
132        return (bool)( $this->config->get( CirrusConfigNames::WeightedTags )['build'] ?? false );
133    }
134
135    /**
136     * Check whether weighted_tags data is available for searching.
137     * @return bool
138     */
139    private function canUse(): bool {
140        return (bool)( $this->config->get( CirrusConfigNames::WeightedTags )['use'] ?? false );
141    }
142
143    private function maxScore(): int {
144        return (int)( $this->config->get( CirrusConfigNames::WeightedTags )['max_score'] ?? 1000 );
145    }
146}