Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
100.00% |
28 / 28 |
|
100.00% |
7 / 7 |
CRAP | |
100.00% |
1 / 1 |
CirrusNearTitleFilterFeature | |
100.00% |
28 / 28 |
|
100.00% |
7 / 7 |
10 | |
100.00% |
1 / 1 |
__construct | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getKeywords | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
doApply | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
2 | |||
expand | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
createQuery | |
100.00% |
14 / 14 |
|
100.00% |
1 / 1 |
2 | |||
getFilterQuery | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
2 | |||
doGetFilterQuery | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 |
1 | <?php |
2 | |
3 | namespace GeoData\Search; |
4 | |
5 | use CirrusSearch\Parser\AST\KeywordFeatureNode; |
6 | use CirrusSearch\Query\Builder\QueryBuildingContext; |
7 | use CirrusSearch\Query\FilterQueryFeature; |
8 | use CirrusSearch\Query\SimpleKeywordFeature; |
9 | use CirrusSearch\Search\SearchContext; |
10 | use CirrusSearch\SearchConfig; |
11 | use CirrusSearch\WarningCollector; |
12 | use Elastica\Query\AbstractQuery; |
13 | use GeoData\Coord; |
14 | use 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 | */ |
27 | class 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 | } |