Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
28 / 28
100.00% covered (success)
100.00%
7 / 7
CRAP
100.00% covered (success)
100.00%
1 / 1
CirrusNearTitleFilterFeature
100.00% covered (success)
100.00%
28 / 28
100.00% covered (success)
100.00%
7 / 7
10
100.00% covered (success)
100.00%
1 / 1
 __construct
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getKeywords
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 doApply
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
2
 expand
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 createQuery
100.00% covered (success)
100.00%
14 / 14
100.00% covered (success)
100.00%
1 / 1
2
 getFilterQuery
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 doGetFilterQuery
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
1<?php
2
3namespace GeoData\Search;
4
5use CirrusSearch\Parser\AST\KeywordFeatureNode;
6use CirrusSearch\Query\Builder\QueryBuildingContext;
7use CirrusSearch\Query\FilterQueryFeature;
8use CirrusSearch\Query\SimpleKeywordFeature;
9use CirrusSearch\Search\SearchContext;
10use CirrusSearch\SearchConfig;
11use CirrusSearch\WarningCollector;
12use Elastica\Query\AbstractQuery;
13use GeoData\Coord;
14use MediaWiki\Config\Config;
15
16/**
17 * Applies geo filtering to the query by providing a Title.
18 *
19 * Limits search results to a geographic area within the geographic area. All values
20 * can be prefixed with a radius in m or km to apply. If not specified this defaults
21 * to 5km.
22 *
23 * Examples:
24 *  neartitle:Shanghai
25 *  neartitle:50km,Seoul
26 */
27class CirrusNearTitleFilterFeature extends SimpleKeywordFeature implements FilterQueryFeature {
28    use CirrusGeoFeature;
29
30    /**
31     * @var Config
32     */
33    private $config;
34
35    /**
36     * @param Config $config
37     */
38    public function __construct( Config $config ) {
39        $this->config = $config;
40    }
41
42    /**
43     * @return string[]
44     */
45    protected function getKeywords() {
46        return [ 'neartitle' ];
47    }
48
49    /**
50     * @param SearchContext $context
51     * @param string $key The keyword
52     * @param string $value The value attached to the keyword with quotes stripped
53     * @param string $quotedValue The original value in the search string, including quotes if used
54     * @param bool $negated Is the search negated? Not used to generate the returned AbstractQuery,
55     *  that will be negated as necessary. Used for any other building/context necessary.
56     * @return array Two element array, first an AbstractQuery or null to apply to the
57     *  query. Second a boolean indicating if the quotedValue should be kept in the search
58     *  string.
59     */
60    protected function doApply( SearchContext $context, $key, $value, $quotedValue, $negated ) {
61        [ $coord, $radius, $excludedPageId ] = $this->parseGeoNearbyTitle( $context, $key, $value );
62        $filter = null;
63        if ( $coord !== null ) {
64            $filter = $this->doGetFilterQuery( $context->getConfig(), $coord, $radius, $excludedPageId );
65        }
66        return [ $filter, false ];
67    }
68
69    /**
70     * @param KeywordFeatureNode $node
71     * @param SearchConfig $config
72     * @param WarningCollector $warningCollector
73     * @return array
74     */
75    public function expand(
76        KeywordFeatureNode $node,
77        SearchConfig $config,
78        WarningCollector $warningCollector
79    ) {
80        return $this->parseGeoNearbyTitle( $warningCollector, $node->getKey(), $node->getValue() );
81    }
82
83    /**
84     * Create a filter for near: and neartitle: queries.
85     *
86     * @param Coord $coord
87     * @param int $radius Search radius in meters
88     * @param string $docIdToExclude Document id to exclude, or "" for no exclusions.
89     * @return AbstractQuery
90     */
91    public static function createQuery( Coord $coord, $radius, $docIdToExclude = '' ) {
92        $query = new \Elastica\Query\BoolQuery();
93        $query->addFilter( new \Elastica\Query\Term( [ 'coordinates.globe' => $coord->globe ] ) );
94        $query->addFilter( new \Elastica\Query\Term( [ 'coordinates.primary' => true ] ) );
95
96        $distanceFilter = new \Elastica\Query\GeoDistance(
97            'coordinates.coord',
98            [ 'lat' => $coord->lat, 'lon' => $coord->lon ],
99            $radius . 'm'
100        );
101        $query->addFilter( $distanceFilter );
102
103        if ( $docIdToExclude !== '' ) {
104            $query->addMustNot( new \Elastica\Query\Term( [ '_id' => $docIdToExclude ] ) );
105        }
106
107        $nested = new \Elastica\Query\Nested();
108        $nested->setPath( 'coordinates' )->setQuery( $query );
109
110        return $nested;
111    }
112
113    /**
114     * @param KeywordFeatureNode $node
115     * @param QueryBuildingContext $context
116     * @return AbstractQuery|null
117     */
118    public function getFilterQuery( KeywordFeatureNode $node, QueryBuildingContext $context ) {
119        [ $coord, $radius, $excludedPageId ] = $context->getKeywordExpandedData( $node );
120        if ( $coord === null ) {
121            return null;
122        }
123        return $this->doGetFilterQuery( $context->getSearchConfig(), $coord, $radius, $excludedPageId );
124    }
125
126    /**
127     * @param SearchConfig $config
128     * @param Coord $coord
129     * @param int $radius
130     * @param int $excludedPageId
131     * @return AbstractQuery
132     */
133    protected function doGetFilterQuery(
134        SearchConfig $config,
135        Coord $coord,
136        $radius,
137        $excludedPageId
138    ) {
139        $excludedDocId = $config->makeId( $excludedPageId );
140        return self::createQuery( $coord, $radius, $excludedDocId );
141    }
142}