Elasticsearch 在地理信息空间索引的探索和演进问题分析

目录
  • 一、业务背景
  • 二、背景知识
  • 三、方案演进
    • 3.1 史前时代
    • 3.2 Elasticsearch 2.0 版本
    • 3.3 Elasticsearch 2.2 版本
    • 3.4 Elasticsearch 5.0 版本
    • 3.5 后续发展

本文梳理了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个问题。

1. 如何精确定位一个地址?

由经度、纬度和相对高度组成的地理坐标系,能够明确标示出地球上的任何一个位置。地球上的经度范围[-180, 180],纬度范围[-90,90]。通常以本初子午线(经度为0)、赤道(纬度为0)为分界线。对于大多数业务场景,由经纬度组成的二维坐标已经足以应对业务问题,可能重庆山城会有些例外。

2. 如何计算两个地址距离?

对于平面坐标系,由勾股定理可以方便计算出两个点的距离。但是由于地球是一个不完美球体,且不同位置有不同海拔高度,所以精确计算两个距离位置是一个非常复杂的问题。在不考虑高度的情况下,二维坐标距离通常使用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 h2 = 1 - cos(x1 - x2);
   double h3 = 1 - cos((lon1 - lon2) * TO_RADIANS);
   double h = h2 + cos(x1) * cos(x2) * h3;
   // 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));
 }

3. 如何方便在互联网分享经纬度坐标?

Geohash是2008-02-26由Gustavo Niemeyer在自己的个人博客上公布的算法服务。其初衷在于通过对经纬度的编码对外提供简短的URL标识地图位置,方便在电子邮件、论坛和网站中使用。

实际上Geohash的价值不仅仅是提供简短的URL,它更大的价值在于:

Geohash给地图上每个坐标提供了独一无二的ID,这个唯一ID就相当于给每个地理位置提供了一个身份证。唯一ID在数据库中应用场景非常丰富。

在数据库中给坐标点提供了另一种存储方式,将二维的坐标点转化成为一维的字符串,对于一维数据就可以借助B树等索引来加速查询。

Geohash是一种前缀编码,位置相近的坐标点前缀相同。通过前缀提供了高性能的邻近位置POI查询,而邻近位置POI查询是LBS服务的核心能力。

关于Geohash的编码规则,这里不展开。这里最关键的点在于:

Geohash是一种前缀编码,位置相近的坐标点前缀相同。Geohash编码长度不同,所覆盖区域范围不同。

在前面知识的铺垫下,最简单的求一个坐标点指定半径范围内的坐标集合的方案就出炉了。

  • 暴力算法

中心坐标点依次跟集合中每个坐标点计算距离,筛选出符合半径条件的坐标点。

这个算法大家太熟悉了,就是最常见的暴力(Brute Force)算法。这个算法在海量数据背景下是没法满足毫秒级响应时间要求的,所以多用于离线计算。对于毫秒级响应的业务诉求,这个算法可以基于geohash进行改造。

  • 二次筛选

基于坐标中心点计算出geohash, 基于半径确定geohash前缀。

通过Geohash前缀初筛出大致符合要求的坐标点(需要将中心点所在Geohash块周围8个Geohash块纳入初筛范围)。

对于初筛结果使用Haversine公式进行二次筛选。

除了上述方案,Elasticsearch在地理信息处理上有哪些奇思妙想呢?

三、方案演进

Elasticsearch从2.0版本开始支持geo_distance查询,到当前已更新到7.14版本。

从2015年至今已经经历了6年的发展, 建设了如下的能力:

技术迭代大致可以分为3个阶段:

发展的成效显著,从性能测试的结果可以略窥一二:

总的来说,资源消耗降低的前提下搜索和写入数据效率都有大幅度提升。下面就详细介绍Elasticsearch对地理信息索引的思路。

3.1 史前时代

Elasticsearch是基于Lucene构建的搜索引擎。Lucene最开始的设想是一个全文检索工具箱,即支持字符串检索,并没有考虑数值类型的处理。其核心思想非常简单,将文档分词后,为每个词构建一个term => array[docIds]的映射。

这样用户输入关键词只需要三步就可以获得想要的结果:

第一步: 通过关键词找到对应的倒排表。这一步简单来说就是查词典。例如:TermQuery.TermWeight 获取该term的倒排表,读取docId+freq信息。

第二步: 根据倒排表得到的docId和词频信息对文档进行打分,返回给用户分值最高的TopN结果。例如:TopScoreDocCollector -- collect()方法,基于小顶堆,保留分数最大的TopN文档。

第三步: 基于docId查询正排表获取文档字段明细信息。

这三步看起来简单,但简直是数据结构应用最佳战场,它需要综合考虑磁盘、内存、IO、数据结构时间复杂度,非常具有挑战性。

例如:查词典可以用很多数据结构实现,比如跳跃表,平衡树、HashMap等,而Lucene的核心工程师Mike McCandless实现了一个只有他自己能懂的FST, 是综合了有限自动机和前缀树的一种数据结构,用来平衡查询复杂度和存储空间,比HashMap慢,但是空间消耗低。文档打分通常用小顶堆来维护分值最高的N个结果,如果有新的文档打分超过堆顶,则替换堆顶元素即可。

**问题:**对于真实业务场景而言,只有字符串匹配查询是不够的,字符串和数值是应用最广泛的两种数据类型。如果需要进行区间查询怎么办呢?这是一个数据库产品非常基础的能力。

Lucene提供了一种适配方案RangeQuery。就是用枚举来模拟数值查询。简单来说:RangeQuery=BooleanQuery+TermQuery,所以限制查询是整数且区间最大不能超过1024。这种实现是可以说是非常鸡肋的,好在Lucene 2.9.0版本真正支持数值查询。

LUCENE-1470,LUCENE-1582,LUCENE-1602,LUCENE-1673,LUCENE-1701, LUCENE-1712

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)

这个实现很强大,支持了int/long/float/double/short/byte,也不限制查询区间了。它的核心思路是将数值字节数组化,然后利用前缀分层管理区间。

如下图所示:

本质上还是RangeQuery=BooleanQuery+TermQuery,只不过在前面做了一层转换:通过前缀树管理一个区间实现了匹配词数量的缩减,而这个缩减是非常有效的。所以这里就有一个专家参数:precisionStep。就是用来控制每个数值字段在分词是生成term的数量,生成term数量越多,区间控制粒度越细,占用磁盘空间越大,查询效率通常越高。

例如:如果precisionStep=8,则意味前缀树叶子节点的上层控制着255个叶子。那么,当查询范围在1511时,由于跨了相邻的2个非叶子节点,所以需要遍历511个term。但是假如查询范围在0512,又只需遍历2个term即可。这样的实现用起来真的有过山车的感觉。

综上,Elasticsearch核心的Lucene倒排索引是一种经典的以不变应万变:字符串和数值索引核心都是查倒排表。理解这个核心,对于后面理解地理位置数据存储和查询非常关键。接下来我们以geo_distance的实现思路为探索主线条,探索一下ES各个版本的实现思路。

3.2 Elasticsearch 2.0 版本

这个版本实现geo_distance查询的思路非常朴素,是建立在数值区间查询(NumericRangeQuery)的基础上。它的geo_point类型字段其实是一个复合字段,或者说是一个结构体。在底层实现时分别用两个独立字段索引来避免暴力扫描。即Elasticsearch的geo_point字段在实现上是lat,lon,加上编码成的geohash综合提供检索聚合功能。

字段定义如下所示:

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);
 }

**第二步:**两个查询通过BooleanQuery组合成一个取交集的复合查询,以实现初筛出在经纬度所示矩形区域内的docId集合。

核心代码如下图所示:

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());
}
}

**第三步:**利用FieldData缓存(正向信息)根据docId获取矩形区域中每个坐标点的经纬度,然后利用前面的Haversine公式计算跟中心坐标点的距离,进行精确筛选,得到符合条件的文档集合。

核心代码如下所示:

// 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);
         }
     };
 }

这是一种非常简单且直观的思路实现了中心点指定半径范围POI的搜索能力。

简单总结一下要点:

利用中心点坐标和半径确定矩形区域边界。

利用Bool查询综合两个NumericRangeQuery查询,实现矩形区域初筛。

利用Haversine公式计算中心点和矩形区域内每个坐标点距离,进行第二阶段过滤操作,筛选出最终符合条件的docId集合。

方案虽然简单,但是毕竟实现了geo_distance的能力。又不是不能用,对吧?那么该方案有什么问题呢?

3.3 Elasticsearch 2.2 版本

ES2.0版本的实现有个问题, 就是没有很好解决二维组合条件查询的数据筛选。它是分别获取符合纬度范围条件的文档集合和符合经度范围条件的文档集合然后进行交集,初筛了太多无效的文档集合。

它的处理思路用一张图表示如下:

即选择了那么多的记录,最终只有经纬度范围交汇的红色区域是初筛的范围。

针对上面的问题,ES 2.2版本引入特性:基于四叉树(Quadtree)的地理位置查询(Lucene 5.3版本实现)。

Quadtree并非什么复杂高深的数据结构,相比二叉树,多了两个子节点。

作为一种基础的数据结构,Quadtree应用场景非常广泛,在图像处理、空间索引、碰撞检测、人生游戏模拟、分形图像分析等领域都可以看到它的身影。

在Elasticsearch地理位置空间索引问题上,Quadtree用来表示区间,可以视为前缀树的一种。

  • 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的区间划分过程。

ES 2.2是如何使用Quadtree来实现geo_distance查询呢?

通常我们使用一种数据结构,是先基于该数据结构存储数据,然后查询这个数据结构。ES这里使用Quadtree的做法非常巧妙:存储的时候没有感觉用到Quadtree,查询时却用其查询方式。

**morton编码:**在理解ES的处理思路前,需要科普一个知识点,那就是morton编码。关于morton编码,跟geohash类似,是一种将二维数据按二进制位交叉编码成一维数据的一种网格编码,其用法和特点跟geohash也是类似的。对于64位的morton码,其经纬度定位精度范围控制到了厘米级别,对于地理位置场景而言,是非常非常高的精度了。

**数据存储:**ES2.2版本之前一个经纬度坐标需要三个字段存储:lat,lon,geohash。有了Quadtree后,只需要一个字段存储就可以了。具体的实现思路如下:将lat,lon坐标进行映射,使得经纬度的取值范围从[-180,180]/[-90,90]映射到[0,2147483520](整数好处理), 然后处理成一维的mortonHash数值。对于数值字段的处理思路,就又回到了前缀(trie)的思路,就又回到了熟悉的专家参数precisionStep。在这里的前缀该如何理解?对于一维数据,每个前缀管理一段区间,对于二维数据每个前缀管理一个二维网格区域。例如一个坐标点利用precisionStep=9来划分前缀,其可视化矩形区域如下:

(取shift=27,36)

(取shift=36,45)

**数据查询:**在查询时,首先将查询中心点坐标转换成一个矩形。这个处理思路我们延续了ES 2.0的做法,不陌生了。

例如:对于坐标为(116.433322,39.900255),半径为1km的点,生成的矩形如下所示:

double centerLon = 116.433322;
double centerLat = 39.900255;
double radiusMeters = 1000.0;
GeoRect geoRect = GeoUtils.circleToBBox(centerLon, centerLat, radiusMeters);
System.out.println( geoRect );

用高德API生成对应的可视化图形如下:

有了这个矩形后,后面的做法就跟ES 2.0有些不同了。ES 2.2版本的思路是利用Quadtree对整个世界地图进行网格化。具体的流程如下:

  • Quadtree处理流程

第一步: 以经纬度(0,0)为起始中心点,将整个世界切分成4个区块。并判断参数生成的矩形在哪个区块。

第二步: 对于矩形区域不在的区域,略过。对于矩形区域所在的区块,继续四分,切成4个区块。

第三步: 当满足如下任一条件时,将相关的文档集合收集起来,作为第一批粗筛的结果。

  • 条件一:切分到正好跟前缀的precisionStep契合,并且quad-cell在矩形内部时。
  • 条件二:切分到最小层级(level=13)时且quad-cell跟矩形区域有交集时。

第四步: 利用lucene的doc_values缓存机制,获取每个docId对应的经纬度,利用距离公式计算是否在半径范围内,得到最终的结果。(这个操作也是常规思路了)

另外ES在处理时进行了版本兼容。

例如: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的核心工程师 Michael McCandless受到论文《Bkd-Tree: A Dynamic Scalable kd-Tree》启发,基于BKD tree再次升级了地理位置数据索引建模和查询功能。

这个数据结构不仅仅是用于解决地理位置查询问题,更是数值类数据索引建模的通用方案。它可以处理一维的数值,从byte到BigDecimal, IPv6地址等等;它也可以处理二维乃至于N维的数据检索问题。

  • 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.

在前面的版本中,对于数值区间查询的处理思路本质上都是term匹配,通过前缀实现了一个term管理一个区间,从而降低了区间查询需要遍历的term数量。而从ES 5.0版本开始,彻底优化了数值查询(从一维到N维),其底层是Lucene 6.0版本实现的BKD tree的独立索引。其实现不仅降低了内存开销,而且提升了检索和索引速度。

关于bkd-tree的原理,其大体思路如下。在面对数值查询区间查询的问题上,大体分为两个层次:

【优化内存查询】:BST(binary-search-tree) > Self-balanced BST > kd-tree。

【优化外存(硬盘)查询】:B-tree > K-D-B-tree > BKD tree。

kd-tree其实就是多维的BST。例如:

**【数据存储】:**BKD tree的核心思路是非常简单的,将N维点集合形成的矩形空间(southWest,northEast)递归分割成更小的矩形空间。跟常见的kd-tree不同,当分割到网格区域里面坐标点的数量小于一定数量(比如1024)就停止了。

例如:

通过区域的分割,确保每个区域POI的数量大致相等。

**【数据查询】:**搜索的时候,就不再是像Quadtree从整个世界开始定位,而是基于当前的点集合形成的空间来查找。例如以geo_distance查询为例。

其流程如下:

第一步: 中心点坐标+半径生成一个矩形(shape boundary)。这一步是常规操作了,前面的版本也都是这么做的。

**第二步:**该矩形跟BKD tree 叶子节点形成的矩形(cell)进行intersect运算,所谓intersect运算,就是计算两个矩形的位置关系:相交、内嵌还是不相关。query和bkd-tree形成的区域有三种关系。

对于CELL_CROSSES_QUERY,如果是叶子节点,则需要判断cell中的每个POI是否符合query的查询条件;否则查询子区间;对于CELL_OUTSIDE_QUERY,直接略过;对于CELL_INSIDE_QUERY,整个cell中的POI都满足查询条件。

核心代码:LatLonPoint/LatLonPointDistanceQuery

3.5 后续发展

Geo查询能力的迭代和变迁,其实也是Elasticsearch作为一个数据库对数值查询能力的升级和优化,扩展产品的适用场景,让使用者打破对Elasticsearch只能做全文检索的偏见。从全文检索数据库扩展到分析型数据库,Elasticsearch还有很长的路要走。

按照 Michael McCandless的设想,当前的多维数据只能是单个点,但是有些场景需要将形状作为一个维度进行索引。在这种需求下,需要通过一种更普适化的k-d tree ,即R-Tree来实现。

路漫漫其修远兮,ES从2.0版本支持geo-spatial开始经历6年的发展,已经走了很远,然而依然有很多值得探索的领域和场景。

参考:

到此这篇关于Elasticsearch 在地理信息空间索引的探索和演进问题分析的文章就介绍到这了,更多相关Elasticsearch地理空间索引内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • elasticsearch数据信息索引操作action support示例分析

    目录 抽象类分析 doExecute方法 performOperation代码 master的相关操作 总结 抽象类分析 Action这一部分主要是数据(索引)的操作和部分集群信息操作. 所有的请求通过client转发到对应的action上然后再由对应的TransportAction来执行相关请求.如果请求能在本机上执行则在本机上执行,否则使用Transport进行转发到对应的节点.action support部分是对action的抽象,所有的具体action都继承了support action

  • ElasticSearch合理分配索引分片原理

    Elasticsearch 是一个非常通用的平台,支持各种用户实例,并为组织数据和复制策略提供了极大的灵活性.但是,这种灵活性有时会使我们很难在早期确定如何很好地将数据组织成索引和分片,尤其是不熟悉 Elastic Stack.虽然不一定会在首次启动时引起问题,但随着数据量的增长,它们可能会导致性能问题.群集拥有的数据越多,纠正问题也越困难,因为有时可能需要重新索引大量数据. 因此,当我们遇到性能问题时,往往可以追溯到索引方式以及集群中分片的数量.那么就会遇到问题,我们应该有多少分片以及我的分片

  • Elasticsearch 基础介绍及索引原理分析

    前言 最近在参与一个基于Elasticsearch作为底层数据框架提供大数据量(亿级)的实时统计查询的方案设计工作,花了些时间学习Elasticsearch的基础理论知识,整理了一下,希望能对Elasticsearch感兴趣/想了解的同学有所帮助. 同时也希望有发现内容不正确或者有疑问的地方,望指明,一起探讨,学习,进步. 介绍 Elasticsearch 是一个分布式可扩展的实时搜索和分析引擎,一个建立在全文搜索引擎 Apache Lucene(TM) 基础上的搜索引擎.当然 Elastics

  • elasticsearch分布式及数据的功能源码分析

    从功能上说,可以分为两部分,分布式功能和数据功能.分布式功能主要是节点集群及集群附属功能如restful借口.集群性能检测功能等,数据功能主要是索引和搜索.代码上这些功能并不是完全独立,而是由相互交叉部分.当然分布式功能是为数据功能服务,数据功能肯定也难以完全独立于分布式功能. 它的源码有以下几个特点: 模块化: 每个功能都以模块化的方式实现,最后以一个借口向外暴露,最终通过guice(google轻量级DI框架)进行管理.整个系统有30多个模块(version1.5). 接口解耦: es代码中

  • elasticsearch索引index之Translog数据功能分析

    目录 跟大多数分布式系统一样,es也通过临时写入写操作来保证数据安全.因为lucene索引过程中,数据会首先据缓存在内存中直到达到一个量(文档数或是占用空间大小)才会写入到磁盘.这就会带来一个风险,如果在写入磁盘前系统崩溃,那么这些缓存数据就会丢失.es通过translog解决了这个问题,每次写操作都会写入一个临时文件translog中,这样如果系统需要恢复数据可以从translog中读取.本篇就主要分析translog的结构及写入方式. 这一部分主要包括两部分translog和tanslogF

  • elasticsearch元数据构建metadata及routing类源码分析

    目录 metadata部分 元数据部分主要包括 shardRouting,继承关系 总结 metadata部分 虽然在刚开始源码概述时把代码分为分布式和数据两部分,但是它们的界限并不明显.之前这几篇可以说是这两部分的衔接.我们在快速接近数据(index)部分.本篇分析一下之前分析cluster遗留下的问题:Metadata与routing,虽然这两部分的代码在cluster中,但是却直接和index相关. metadata部分主要是和索引相关的一些元数据构建和操作. 元数据部分主要包括 别名元数

  • 使用elasticsearch定时删除索引数据

    1.有的时候我们在使用ES 由于资源有限或业务需求,我们只想保存最近一段时间的数据,所以有必要做定时删除数据. 2.编写脚本 vim del_es_by_day.sh #!/bin/bash #定时删除elasticsearch索引 #author menard 2019-3-25 date=`date -d "-7 days" "+%Y.%m.%d"` /usr/bin/curl -v --user elastic:password -XDELETE "

  • Elasticsearch 在地理信息空间索引的探索和演进问题分析

    目录 一.业务背景 二.背景知识 三.方案演进 3.1 史前时代 3.2 Elasticsearch 2.0 版本 3.3 Elasticsearch 2.2 版本 3.4 Elasticsearch 5.0 版本 3.5 后续发展 本文梳理了Elasticsearch对于数值索引实现方案的升级和优化思考,从2015年至今数值索引的方案经历了多个版本的迭代,实现思路从最初的字符串模拟到KD-Tree,技术越来越复杂,能力越来越强大,应用场景也越来越丰富.从地理位置信息建模到多维坐标,数据检索到数

  • elasticsearch构造Client实现java客户端调用接口示例分析

    目录 client的继承关系 方法实现上 以index方法为例 execute方法代码 总结: elasticsearch通过构造一个client对外提供了一套丰富的java调用接口.总体来说client分为两类cluster信息方面的client及数据(index)方面的client.这两个大类由可以分为通用操作和admin操作两类. client的继承关系 (1.5版本,其它版本可能不一样): 通过这个继承关系图可以很清楚的了解client的实现,及功能.总共有三类即client, indi

  • SpringBoot集成ElasticSearch的示例代码

    目录 一.Elasticseach介绍 1.简单介绍 2.对比关系: 3.详细说明: 4.查出数据的解释 二.SpringBoot集成Elasticseach 1.引入依赖 2.添加配置 3.创建pojo类与索引对应 4.SpringData封装了基础的增删改查,自定义增删改查 5.测试方法--增删改查 一.Elasticseach介绍 1.简单介绍 官网:开源搜索:Elasticsearch.ELK Stack 和 Kibana 的开发者 | Elastic https://www.elast

  • 使用Python操作Elasticsearch数据索引的教程

    Elasticsearch是一个分布式.Restful的搜索及分析服务器,Apache Solr一样,它也是基于Lucence的索引服务器,但我认为Elasticsearch对比Solr的优点在于: 轻量级:安装启动方便,下载文件之后一条命令就可以启动: Schema free:可以向服务器提交任意结构的JSON对象,Solr中使用schema.xml指定了索引结构: 多索引文件支持:使用不同的index参数就能创建另一个索引文件,Solr中需要另行配置: 分布式:Solr Cloud的配置比较

  • 详解SpringBoot+Dubbo集成ELK实战

    前言 一直以来,日志始终伴随着我们的开发和运维过程.当系统出现了Bug,往往就是通过Xshell连接到服务器,定位到日志文件,一点点排查问题来源. 随着互联网的快速发展,我们的系统越来越庞大.依赖肉眼分析日志文件来排查问题的方式渐渐凸显出一些问题: 分布式集群环境下,服务器数量可能达到成百上千,如何准确定位? 微服务架构中,如何根据异常信息,定位其他各服务的上下文信息? 随着日志文件的不断增大,可能面临在服务器上不能直接打开的尴尬. 文本搜索太慢.无法多维度查询等 面临这些问题,我们需要集中化的

  • 使用Java 压缩文件打包tar.gz 包的详细教程

    一.背景 最近,小哈主要在负责日志中台的开发工作, 等等,啥是日志中台? 俺只知道中台概念,这段时间的确很火,但是日志中台又是用来干啥的? 这里小哈尽量地通俗的说下日志中台的职责,再说日志中台之前,我们先扯点别的? 相信大家对集中式日志平台 ELK 都知道一些,生产环境中, 稍复杂的架构,服务一般都是集群部署,这样,日志就会分散在每台服务器上,一旦发生问题,想要查看日志就会非常繁琐,你需要登录每台服务器找日志,因为你不确定请求被打到哪个节点上.另外,任由开发人员登录服务器查看日志本身就存在安全隐

  • 用python的seaborn画数值箱型图

    目录 一.概念介绍 二.数据展示 三.数据导入 四.画图 总结 一.概念介绍 箱型图(box-plot),又称为箱线图,盒型图,盒须图.在数据探索阶段或者描述性分析过程中,我们常常用于展示多类连续型数据的数值分布情况,便于类间对比和快速识别异常值. 在一幅箱型图中,一个连续数值序列构成一个盒子,如下所示. 每一个盒子主要展示的是数据的上四分位数Q1(25%),中位数(50%),下四分位数Q3(75%).划分异常值的界限我们称为上下极限,其离Q1,Q3分别是1.5IQR(IQR=Q3-Q1,称作四

  • 这十大Python库你真应该知道

    目录 01.Pandas 02.NumPy 03.Scikit-learn 04.Gradio 05.TensorFlow 06.Keras 07.SciPy 08.Statsmodels 09.Plotly 10.Seaborn 总结 01.Pandas 在数据分析师的日常工作中,70%到80%都涉及到理解和清理数据,也就是数据探索和数据挖掘. Pandas主要用于数据分析,这是最常用的Python库之一.它为你提供了一些最有用的工具来对数据进行探索.清理和分析.使用Pandas,你可以加载.

  • 探索Java I/O 模型的演进

    相关概念 同步和异步 描述的是用户线程与内核的交互方式: 同步是指用户线程发起 I/O 请求后需要等待或者轮询内核 I/O 操作完成后才能继续执行: 异步是指用户线程发起 I/O 请求后仍继续执行,当内核 I/O 操作完成后会通知用户线程,或者调用用户线程注册的回调函数. 阻塞和非阻塞 描述的是用户线程调用内核 I/O 操作的方式: 阻塞是指 I/O 操作需要彻底完成后才返回到用户空间: 非阻塞是指 I/O 操作被调用后立即返回给用户一个状态值,无需等到 I/O 操作彻底完成. 一个 I/O 操

随机推荐