Elasticsearch 在地理資訊空間索引的探索和演進
vivo 網際網路伺服器團隊- Shuai Guangying
一、業務背景
GET /my_locations/_search{ "query": { "bool": { "must": { "match_all": {} }, "filter": { "geo_distance": { "distance": "1km", "pin.location": { "lat": 40, "lon": 116 } } } } }}
二、背景知識
-
1. 如何精確定位一個地址?
-
2. 如何計算兩個地址距離?
這個公式非常簡單,只需用到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 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)); }
-
3. 如何方便在網際網路分享經緯度座標?
-
Geohash給地圖上每個座標提供了獨一無二的ID,這個唯一ID就相當於給每個地理位置提供了一個身份證。唯一ID在資料庫中應用場景非常豐富。
-
在資料庫中給座標點提供了另一種儲存方式,將二維的座標點轉化成為一維的字串,對於一維資料就可以藉助B樹等索引來加速查詢。
-
Geohash是一種字首編碼,位置相近的座標點字首相同。透過字首提供了高效能的鄰近位置POI查詢,而鄰近位置POI查詢是LBS服務的核心能力。 關於Geohash的編碼規則,這裡不展開。這裡最關鍵的點在於:
Geohash是一種字首編碼,位置相近的座標點字首相同。Geohash編碼長度不同,所覆蓋區域範圍不同。
-
暴力演算法
中心座標點依次跟集合中每個座標點計算距離,篩選出符合半徑條件的座標點。
-
二次篩選
-
基於座標中心點計算出geohash, 基於半徑確定geohash字首。
-
透過Geohash字首初篩出大致符合要求的座標點(需要將中心點所在Geohash塊周圍8個Geohash塊納入初篩範圍)。
-
對於初篩結果使用Haversine公式進行二次篩選。
三、方案演進
從2015年至今已經經歷了6年的發展, 建設了如下的能力:
技術迭代大致可以分為3個階段:
發展的成效顯著,從效能測試的結果可以略窺一二:
總的來說,資源消耗降低的前提下搜尋和寫入資料效率都有大幅度提升。下面就詳細介紹Elasticsearch對地理資訊索引的思路。
3.1 史前時代
第一步: 透過關鍵詞找到對應的倒排表。這一步簡單來說就是查詞典。例如:TermQuery.TermWeight 獲取該term的倒排表,讀取docId+freq資訊。 第二步: 根據倒排表得到的docId和詞頻資訊對文件進行打分,返回給使用者分值最高的TopN結果。例如:TopScoreDocCollector -- collect()方法,基於小頂堆,保留分數最大的TopN文件。 第三步: 基於docId查詢正排表獲取文件欄位明細資訊。
例如:查詞典可以用很多資料結構實現,比如跳躍表,平衡樹、HashMap等,而Lucene的核心工程師Mike McCandless實現了一個只有他自己能懂的FST, 是綜合了有限自動機和字首樹的一種資料結構,用來平衡查詢複雜度和儲存空間,比HashMap慢,但是空間消耗低。文件打分通常用小頂堆來維護分值最高的N個結果,如果有新的文件打分超過堆頂,則替換堆頂元素即可。
Added NumericRangeQuery and NumericRangeFilter, a fast alternative to RangeQuery/RangeFilter for numeric searches. They depend on a specific structure of terms in the index that can be created by indexing using the new NumericField or NumericTokenStream classes. NumericField can only be used for indexing and optionally stores the values as string representation in the doc store. Documents returned from IndexReader/IndexSearcher will return only the String value using the standard Fieldable interface. NumericFields can be sorted on and loaded into the FieldCache. (Uwe Schindler, Yonik Seeley, Mike McCandless)
如下圖所示:
例如:如果precisionStep=8,則意味字首樹葉子節點的上層控制著255個葉子。那麼,當查詢範圍在1~511時,由於跨了相鄰的2個非葉子節點,所以需要遍歷511個term。但是假如查詢範圍在0~512,又只需遍歷2個term即可。這樣的實現用起來真的有雲霄飛車的感覺。
3.2 Elasticsearch 2.0 版本
public static final class GeoPointFieldType extends MappedFieldType { private MappedFieldType geohashFieldType; private int geohashPrecision; private boolean geohashPrefixEnabled; private MappedFieldType latFieldType; private MappedFieldType lonFieldType; public GeoPointFieldType() {}}
// 計算經緯度座標+距離得到的矩形區域// GeoDistance類public static DistanceBoundingCheck distanceBoundingCheck(double sourceLatitude, double sourceLongitude, double distance, DistanceUnit unit) { // angular distance in radians on a great circle // assume worst-case: use the minor axis double radDist = unit.toMeters(distance) / GeoUtils.EARTH_SEMI_MINOR_AXIS; double radLat = Math.toRadians(sourceLatitude); double radLon = Math.toRadians(sourceLongitude); double minLat = radLat - radDist; double maxLat = radLat + radDist; double minLon, maxLon; if (minLat > MIN_LAT && maxLat < MAX_LAT) { double deltaLon = Math.asin(Math.sin(radDist) / Math.cos(radLat)); minLon = radLon - deltaLon; if (minLon < MIN_LON) minLon += 2d * Math.PI; maxLon = radLon + deltaLon; if (maxLon > MAX_LON) maxLon -= 2d * Math.PI; } else { // a pole is within the distance minLat = Math.max(minLat, MIN_LAT); maxLat = Math.min(maxLat, MAX_LAT); minLon = MIN_LON; maxLon = MAX_LON; } GeoPoint topLeft = new GeoPoint(Math.toDegrees(maxLat), Math.toDegrees(minLon)); GeoPoint bottomRight = new GeoPoint(Math.toDegrees(minLat), Math.toDegrees(maxLon)); if (minLon > maxLon) { return new Meridian180DistanceBoundingCheck(topLeft, bottomRight); } return new SimpleDistanceBoundingCheck(topLeft, bottomRight); }
public class IndexedGeoBoundingBoxQuery {public static Query create(GeoPoint topLeft, GeoPoint bottomRight, GeoPointFieldMapper.GeoPointFieldType fieldType) { if (!fieldType.isLatLonEnabled()) { throw new IllegalArgumentException("lat/lon is not enabled (indexed) for field [" + fieldType.names().fullName() + "], can't use indexed filter on it"); } //checks to see if bounding box crosses 180 degrees if (topLeft.lon() > bottomRight.lon()) { return westGeoBoundingBoxFilter(topLeft, bottomRight, fieldType); } else { return eastGeoBoundingBoxFilter(topLeft, bottomRight, fieldType); }}private static Query westGeoBoundingBoxFilter(GeoPoint topLeft, GeoPoint bottomRight, GeoPointFieldMapper.GeoPointFieldType fieldType) { BooleanQuery.Builder filter = new BooleanQuery.Builder(); filter.setMinimumNumberShouldMatch(1); filter.add(fieldType.lonFieldType().rangeQuery(null, bottomRight.lon(), true, true), Occur.SHOULD); filter.add(fieldType.lonFieldType().rangeQuery(topLeft.lon(), null, true, true), Occur.SHOULD); filter.add(fieldType.latFieldType().rangeQuery(bottomRight.lat(), topLeft.lat(), true, true), Occur.MUST); return new ConstantScoreQuery(filter.build());}private static Query eastGeoBoundingBoxFilter(GeoPoint topLeft, GeoPoint bottomRight, GeoPointFieldMapper.GeoPointFieldType fieldType) { BooleanQuery.Builder filter = new BooleanQuery.Builder(); filter.add(fieldType.lonFieldType().rangeQuery(topLeft.lon(), bottomRight.lon(), true, true), Occur.MUST); filter.add(fieldType.latFieldType().rangeQuery(bottomRight.lat(), topLeft.lat(), true, true), Occur.MUST); return new ConstantScoreQuery(filter.build());}}
// GeoDistanceRangeQuery類的實現 @Override public Weight createWeight(IndexSearcher searcher, boolean needsScores) throws IOException { final Weight boundingBoxWeight; if (boundingBoxFilter != null) { boundingBoxWeight = searcher.createNormalizedWeight(boundingBoxFilter, false); } else { boundingBoxWeight = null; } return new ConstantScoreWeight(this) { @Override public Scorer scorer(LeafReaderContext context) throws IOException { final DocIdSetIterator approximation; if (boundingBoxWeight != null) { approximation = boundingBoxWeight.scorer(context); } else { approximation = DocIdSetIterator.all(context.reader().maxDoc()); } if (approximation == null) { // if the approximation does not match anything, we're done return null; } final MultiGeoPointValues values = indexFieldData.load(context).getGeoPointValues(); final TwoPhaseIterator twoPhaseIterator = new TwoPhaseIterator(approximation) { @Override public boolean matches() throws IOException { final int doc = approximation.docID(); values.setDocument(doc); final int length = values.count(); for (int i = 0; i < length; i++) { GeoPoint point = values.valueAt(i); if (distanceBoundingCheck.isWithin(point.lat(), point.lon())) { double d = fixedSourceDistance.calculate(point.lat(), point.lon()); if (d >= inclusiveLowerPoint && d <= inclusiveUpperPoint) { return true; } } } return false; } }; return new ConstantScoreScorer(this, score(), twoPhaseIterator); } }; }
-
利用中心點座標和半徑確定矩形區域邊界。
-
利用Bool查詢綜合兩個NumericRangeQuery查詢,實現矩形區域初篩。
-
利用Haversine公式計算中心點和矩形區域內每個座標點距離,進行第二階段過濾操作,篩選出最終符合條件的docId集合。
3.3 Elasticsearch 2.2 版本
它的處理思路用一張圖表示如下:
-
Region quadtree
The region quadtree represents a partition of space in two dimensions by decomposing the region into four equal quadrants, subquadrants, and so on with each leaf node containing data corresponding to a specific subregion. Each node in the tree either has exactly four children, or has no children (a leaf node). The height of quadtrees that follow this decomposition strategy (i.e. subdividing subquadrants as long as there is interesting data in the subquadrant for which more refinement is desired) is sensitive to and dependent on the spatial distribution of interesting areas in the space being decomposed. The region quadtree is a type of trie.
在區間劃分上,Quadtree跟geohash的處理思路有些相似。在一維世界,二分可以無限迭代。同理,在二維世界,四分也可以無限迭代。下面這個圖可以非常形象展示Quadtree的區間劃分過程。
(取shift=27,36)
(取shift=36,45)
double centerLon = 116.433322; double centerLat = 39.900255; double radiusMeters = 1000.0; GeoRect geoRect = GeoUtils.circleToBBox(centerLon, centerLat, radiusMeters); System.out.println( geoRect );
-
Quadtree處理流程
第一步: 以經緯度(0,0)為起始中心點,將整個世界切分成4個區塊。並判斷引數生成的矩形在哪個區塊。 第二步: 對於矩形區域不在的區域,略過。對於矩形區域所在的區塊,繼續四分,切成4個區塊。 第三步: 當滿足如下任一條件時,將相關的文件集合收集起來,作為第一批粗篩的結果。
-
條件一:切分到正好跟字首的precisionStep契合,並且quad-cell在矩形內部時。
-
條件二:切分到最小層級(level=13)時且quad-cell跟矩形區域有交集時。
第四步: 利用lucene的doc_values快取機制,獲取每個docId對應的經緯度,利用距離公式計算是否在半徑範圍內,得到最終的結果。(這個操作也是常規思路了)
例如:ES 2.2版本對於geo_distance的實現關鍵點,判斷索引版本是否是V_2_2_0版本以後建立,如果是則直接用Lucene的GeoPointDistanceQuery查詢類,否則沿用ES 2.0版本的GeoDistanceRangeQuery。
IndexGeoPointFieldData indexFieldData = parseContext.getForField(fieldType);final Query query;if (parseContext.indexVersionCreated().before(Version.V_2_2_0)) { query = new GeoDistanceRangeQuery(point, null, distance, true, false, geoDistance, geoFieldType, indexFieldData, optimizeBbox);} else { distance = GeoUtils.maxRadialDistance(point, distance); query = new GeoPointDistanceQuery(indexFieldData.getFieldNames().indexName(), point.lon(), point.lat(), distance);} if (queryName != null) { parseContext.addNamedQuery(queryName, query);}
核心程式碼參考: GeoPointDistanceQuery、GeoPointRadiusTermsEnum
3.4 Elasticsearch 5.0 版本
-
LUCENE-6825
This can be used for very fast 1D range filtering for numerics, removing the 8 byte (long/double) limit we have today, so e.g. we could efficiently support BigInteger, BigDecimal, IPv6 addresses, etc. It can also be used for > 1D use cases, like 2D (lat/lon) and 3D (x/y/z with geo3d) geo shape intersection searches. ... It should give sizable performance gains (smaller index, faster searching) over what we have today, and even over what auto-prefix with efficient numeric terms would do.
【最佳化記憶體查詢】:BST(binary-search-tree) > Self-balanced BST > kd-tree。 【最佳化外存(硬碟)查詢】:B-tree > K-D-B-tree > BKD tree。
kd-tree其實就是多維的BST。例如:
第二步: 該矩形跟BKD tree 葉子節點形成的矩形(cell)進行intersect運算,所謂intersect運算,就是計算兩個矩形的位置關係:相交、內嵌還是不相關。query和bkd-tree形成的區域有三種關係。
核心程式碼:LatLonPoint/LatLonPointDistanceQuery
3.5 後續發展
來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/69912579/viewspace-2902492/,如需轉載,請註明出處,否則將追究法律責任。
相關文章
- 地理空間資料Geometry在MySQL中使用MySql
- 軟體SaaS化大勢所趨 資訊化演進背景下潛在市場空間巨大
- 地理空間資料分析與視覺化:洞察地理現象的智慧之眼視覺化
- ElasticSearch 獲取es資訊以及索引操作Elasticsearch索引
- 使用Elasticsearch的動態索引和索引優化Elasticsearch索引優化
- 移動分割槽表和分割槽索引的表空間索引
- 空間統計(一)度量地理分佈
- Redis 地理空間(geospatial)介紹及應用Redis
- 使用openlayers在網頁中展示地理資訊網頁
- 高效管理 Elasticsearch 中基於時間的索引Elasticsearch索引
- Oracle中表空間、表、索引的遷移Oracle索引
- 地理資訊科學在考古學中的應用:GIS與遙感技術的時空穿梭之旅
- Space Capital:地理空間情報手冊報告API
- 地理資訊科學教育的新趨勢:探索未來教育的經緯度
- 【Elasticsearch】Elasticsearch 索引模板Elasticsearch索引
- YashanDB在地理資訊系統(GIS)領域的關鍵功能和技術優勢
- 資料湖從前世到今身的演進與選型探索
- MySQL共享表空間各個版本之間的演變圖MySql
- mysql儲存地理資訊的方法MySql
- 使用ELASTICSEARCH進行近實時索引 - bozhoElasticsearch索引
- 6_redis十大關係之地理空間GEORedis
- 提升地理空間分析效率,火山引擎ByteHouse上線GIS能力
- 表空間和資料檔案的管理
- ElasticSearch 索引 VS MySQL 索引Elasticsearch索引MySql
- 帶你走進神一樣的Elasticsearch索引機制Elasticsearch索引
- Elasticsearch和Kibana在許可證方面進行了重大的更改Elasticsearch
- 高德深度資訊接入的平臺化演進
- 剖析 Elasticsearch 的索引原理Elasticsearch索引
- 在Spring Data Elasticsearch 4中使用地理距離排序 - sothawoSpringElasticsearch排序
- 融合通訊技術趨勢和演進方向
- elasticsearch索引原理Elasticsearch索引
- elasticsearch: 指定索引資料的儲存目錄Elasticsearch索引
- oracle建立臨時表空間和資料表空間以及刪除Oracle
- 坑系列 — 時間和空間的平衡
- PostgreSQL在不同的表空間移動資料檔案SQL
- 風險洞察之事件匯流排的探索與演進事件
- 探索餓了麼移動APP的架構演進之路APP架構
- SQLServer行版本資訊吃資料庫tempdb空間SQLServer資料庫