本文梳理了Elasticsearch对于数值索引实现方案的升级和优化思考,从2015年至今数值索引的方案经历了多个版本的迭代,实现思路从最初的字符串模拟到KD-Tree,技术越来越复杂,能力越来越强大,应用场景也越来越丰富。从地理位置信息建模到多维坐标,数据检索到数据分析洞察都可以看到Elasticsearch的身影。
一、业务背景
LBS服务是当前互联网重要的一环,涉及餐饮、娱乐、打车、零售等场景。在这些场景中,有很重要的一项基础能力:搜索附近的POI。比如搜索附近的美食,搜索附近的电影院,搜索附近的专车,搜索附近的门店。例如:以某个坐标点为中心查询出1km半径范围的POI坐标,如下图所示:
Elasticsearch在地理位置信息检索上具备了毫秒级响应的能力,而毫秒级响应对于用户体验至关重要。上面的问题使用Elasticsearch,只需用到geo_distance查询就可以解决业务问题。使用Elasticsearch的查询语法如下:
GET /my_locations/_search { "query": { "bool": { "must": { "match_all": {} }, "filter": { "geo_distance": { "distance": "1km", "pin.location": { "lat": 40, "lon": 116 } } } } } }
工具的使用是一个非常简单的事情,更有意思在于工具解决问题背后的思想。理解了处理问题的思想,就可以超然于工具本身,做到举一反三。本文基于在海量数据背景下,如何实现毫秒级搜索附近的POI这个问题,探讨了Elasticsearch的实现方案,以及实现地理位置索引技术的演进过程。
二、背景知识
在介绍Elasticsearch的处理方案前,我们首先需要介绍一些背景知识,主要是3个问题。
- 如何精确定位一个地址?
由经度、纬度和相对高度组成的地理坐标系,能够明确标示出地球上的任何一个位置。地球上的经度范围\[-180, 180\],纬度范围\[-90,90\]。通常以本初子午线(经度为0)、赤道(纬度为0)为分界线。对于大多数业务场景,由经纬度组成的二维坐标已经足以应对业务问题,可能重庆山城会有些例外。
- 如何计算两个地址距离?
对于平面坐标系,由勾股定理可以方便计算出两个点的距离。但是由于地球是一个不完美球体,且不同位置有不同海拔高度,所以精确计算两个距离位置是一个非常复杂的问题。在不考虑高度的情况下,二维坐标距离通常使用Haversine公式。 这个公式非常简单,只需用到arcsin和cos两个高中数学公式。其中φ和λ表示两个点纬度和经度的弧度制度量。其中d即为所求两个点的距离,对应的数学公式如下(参考维基百科):
程序员更喜欢看代码,对照代码理解公式更简单。相应的代码如下:
// 代码摘自lucene-core-8.2.0, SloppyMath工具类 /** * Returns the Haversine distance in meters between two points * given the previous result from {@link #haversinSortKey(double, double, double, double)} * @return distance in meters. */ public static double haversinMeters(double sortKey) { return TO_METERS * 2 * asin(Math.min(1, Math.sqrt(sortKey * 0.5))); } /** * Returns a sort key for distance. This is less expensive to compute than * {@link #haversinMeters(double, double, double, double)}, but it always compares the same. * This can be converted into an actual distance with {@link #haversinMeters(double)}, which * effectively does the second half of the computation. */ public static double haversinSortKey(double lat1, double lon1, double lat2, double lon2) { double x1 = lat1 * TO_RADIANS; double x2 = lat2 * TO_RADIANS; double h1 = 1 - cos(x1 - x2); double h2 = 1 - cos((lon1 - lon2) * TO_RADIANS); double h = h1 + cos(x1) * cos(x2) * h2; // clobber crazy precision so subsequent rounding does not create ties. return Double.longBitsToDouble(Double.doubleToRawLongBits(h) & 0xFFFFFFFFFFFFFFF8L); } // haversin // TODO: remove these for java 9, they fixed Math.toDegrees()/toRadians() to work just like this. public static final double TO_RADIANS = Math.PI / 180D; public static final double TO_DEGREES = 180D / Math.PI; // Earth's mean radius, in meters and kilometers; see http://earth-info.nga.mil/GandG/publications/tr8350.2/wgs84fin.pdf private static final double TO_METERS = 6_371_008.7714D; // equatorial radius private static final double TO_KILOMETERS = 6_371.0087714D; // equatorial radius /** * Returns the Haversine distance in meters between two points * specified in decimal degrees (latitude/longitude). This works correctly * even if the dateline is between the two points. * <p> * Error is at most 4E-1 (40cm) from the actual haversine distance, but is typically * much smaller for reasonable distances: around 1E-5 (0.01mm) for distances less than * 1000km. * * @param lat1 Latitude of the first point. * @param lon1 Longitude of the first point. * @param lat2 Latitude of the second point. * @param lon2 Longitude of the second point. * @return distance in meters. */ public static double haversinMeters(double lat1, double lon1, double lat2, double lon2) { return haversinMeters(haversinSortKey(lat1, lon1, lat2, lon2)); }
- 如何方便在互联网分享经纬度坐标?
Geohash是2008-02-26由Gustavo Niemeyer在自己的个人博客上公布的算法服务。其初衷在于通过对经纬度的编码对外提供简短的URL标识地图位置,方便在电子邮件、论坛和网站中使用。 实际上Geohash的价值不仅仅是提供简短的URL,它更大的价值在于&#