%PDF- %PDF-
| Direktori : /www/varak.net/wiki.varak.net/extensions/CirrusSearch/includes/Query/ |
| Current File : /www/varak.net/wiki.varak.net/extensions/CirrusSearch/includes/Query/GeoFeature.php |
<?php
namespace CirrusSearch\Query;
use CirrusSearch\Search\SearchContext;
use CirrusSearch\SearchConfig;
use Elastica\Query\AbstractQuery;
use GeoData\GeoData;
use GeoData\Coord;
use GeoData\Globe;
use Title;
/**
* Applies geo based features to the query.
*
* Two forms of geo based querying are provided: a filter that limits search
* results to a geographic area and a boost that increases the score of
* results within the geographic area. Supports specifying geo coordinates
* either by providing a latitude and longitude, or a page title to source the
* latitude and longitude from. All values can be prefixed with a radius in m
* or km to apply. If not specified this defaults to 5km.
*
* Examples:
* neartitle:Shanghai
* neartitle:50km,Seoul
* nearcoord:1.2345,-5.4321
* nearcoord:17km,54.321,-12.345
* boost-neartitle:"San Francisco"
* boost-neartitle:50km,Kampala
* boost-nearcoord:-12.345,87.654
* boost-nearcoord:77km,34.567,76.543
*/
class GeoFeature extends SimpleKeywordFeature {
// Default radius, in meters
const DEFAULT_RADIUS = 5000;
// Default globe
const DEFAULT_GLOBE = 'earth';
/**
* @return string
*/
protected function getKeywordRegex() {
return '(boost-)?near(coord|title)';
}
/**
* @param SearchContext $context
* @param string $key The keyword
* @param string $value The value attached to the keyword with quotes stripped
* @param string $quotedValue The original value in the search string, including quotes if used
* @param bool $negated Is the search negated? Not used to generate the returned AbstractQuery,
* that will be negated as necessary. Used for any other building/context necessary.
* @return array Two element array, first an AbstractQuery or null to apply to the
* query. Second a boolean indicating if the quotedValue should be kept in the search
* string.
*/
protected function doApply( SearchContext $context, $key, $value, $quotedValue, $negated ) {
if ( !class_exists( GeoData::class ) ) {
return [ null, false ];
}
if ( substr( $key, -5 ) === 'title' ) {
list( $coord, $radius, $excludeDocId ) = $this->parseGeoNearbyTitle(
$context->getConfig(),
$value
);
} else {
list( $coord, $radius ) = $this->parseGeoNearby( $value );
$excludeDocId = '';
}
$filter = null;
if ( $coord ) {
$context->setSearchType( 'geo_' . $context->getSearchType() );
if ( substr( $key, 0, 6 ) === 'boost-' ) {
$context->addGeoBoost( $coord, $radius, $negated ? 0.1 : 1 );
} else {
$filter = self::createQuery( $coord, $radius, $excludeDocId );
}
}
return [ $filter, false ];
}
/**
* radius, if provided, must have either m or km suffix. Valid formats:
* <title>
* <radius>,<title>
*
* @param SearchConfig $config the Cirrus config object
* @param string $text user input to parse
* @return array Three member array with Coordinate object, integer radius
* in meters, and page id to exclude from results.. When invalid the
* Coordinate returned will be null.
*/
public function parseGeoNearbyTitle( SearchConfig $config, $text ) {
$title = Title::newFromText( $text );
if ( $title && $title->exists() ) {
// Default radius if not provided: 5km
$radius = self::DEFAULT_RADIUS;
} else {
// If the provided value is not a title try to extract a radius prefix
// from the beginning. If $text has a valid radius prefix see if the
// remaining text is a valid title to use.
$pieces = explode( ',', $text, 2 );
if ( count( $pieces ) !== 2 ) {
return [ null, 0, '' ];
}
$radius = $this->parseDistance( $pieces[0] );
if ( $radius === null ) {
return [ null, 0, '' ];
}
$title = Title::newFromText( $pieces[1] );
if ( !$title || !$title->exists() ) {
return [ null, 0, '' ];
}
}
$coord = GeoData::getPageCoordinates( $title );
if ( !$coord ) {
return [ null, 0, '' ];
}
return [ $coord, $radius, $config->makeId( $title->getArticleID() ) ];
}
/**
* radius, if provided, must have either m or km suffix. Latitude and longitude
* must be floats in the domain of [-90:90] for latitude and [-180,180] for
* longitude. Valid formats:
* <lat>,<lon>
* <radius>,<lat>,<lon>
*
* @param string $text
* @return array Two member array with Coordinate object, and integer radius
* in meters. When invalid the Coordinate returned will be null.
*/
public function parseGeoNearby( $text ) {
$pieces = explode( ',', $text, 3 );
// Default radius if not provided: 5km
$radius = self::DEFAULT_RADIUS;
if ( count( $pieces ) === 3 ) {
$radius = $this->parseDistance( $pieces[0] );
if ( $radius === null ) {
return [ null, 0 ];
}
$lat = $pieces[1];
$lon = $pieces[2];
} elseif ( count( $pieces ) === 2 ) {
$lat = $pieces[0];
$lon = $pieces[1];
} else {
return [ null, 0 ];
}
$globe = new Globe( self::DEFAULT_GLOBE );
if ( !$globe->coordinatesAreValid( $lat, $lon ) ) {
return [ null, 0 ];
}
return [
new Coord( floatval( $lat ), floatval( $lon ), $globe->getName() ),
$radius,
];
}
/**
* @param string $distance
* @param int $default
* @return int|null Parsed distance in meters, or null if unparsable
*/
public function parseDistance( $distance ) {
if ( !preg_match( '/^(\d+)(m|km|mi|ft|yd)$/', $distance, $matches ) ) {
return null;
}
$scale = [
'm' => 1,
'km' => 1000,
// Supported non-SI units, and their conversions, sourced from
// https://en.wikipedia.org/wiki/Unit_of_length#Imperial.2FUS
'mi' => 1609.344,
'ft' => 0.3048,
'yd' => 0.9144,
];
return max( 10, (int) round( $matches[1] * $scale[$matches[2]] ) );
}
/**
* Create a filter for near: and neartitle: queries.
*
* @param Coord $coord
* @param int $radius Search radius in meters
* @param string $docIdToExclude Document id to exclude, or "" for no exclusions.
* @return AbstractQuery
*/
public static function createQuery( Coord $coord, $radius, $docIdToExclude = '' ) {
$query = new \Elastica\Query\BoolQuery();
$query->addFilter( new \Elastica\Query\Term( [ 'coordinates.globe' => $coord->globe ] ) );
$query->addFilter( new \Elastica\Query\Term( [ 'coordinates.primary' => 1 ] ) );
$distanceFilter = new \Elastica\Query\GeoDistance(
'coordinates.coord',
[ 'lat' => $coord->lat, 'lon' => $coord->lon ],
$radius . 'm'
);
$distanceFilter->setOptimizeBbox( 'indexed' );
$query->addFilter( $distanceFilter );
if ( $docIdToExclude !== '' ) {
$query->addMustNot( new \Elastica\Query\Term( [ '_id' => $docIdToExclude ] ) );
}
$nested = new \Elastica\Query\Nested();
$nested->setPath( 'coordinates' )->setQuery( $query );
return $nested;
}
}