基于Solr的空间搜索(2)

标签: java基础 jvm和java底层 Lucene Solr | 发表时间:2013-01-09 14:18 | 作者:hongzhen
出处:http://rdc.taobao.com/team/jm

本文将继续围绕Solr+Lucene使用Cartesian Tiers 笛卡尔层和GeoHash的构建索引和查询的细节进行介绍

在Solr中其实支持很多默认距离函数,但是基于坐标构建索引和查询的主要会基于2种方案:

(1)GeoHash

(2)Cartesian Tiers+GeoHash

而这块的源码实现都在lucene-spatial.jar中可以找到。接下来我将根据这2种方案展开关于构建索引和查询细节进行阐述,都是代码分析,感兴趣的看官可以继续往下看。GeoHash  

构建索引阶段

定义geohash域,在schema.xml中定义:

<fieldtype name= “geohash” class= “solr.GeoHashField”/>

接下来再构建索引的时候使用到lucene-spatial.jar的GeoHashUtils类:

String geoHash = GeoHashUtils. encode(latitude, longitude);//通过geoHash算法将经纬度变成base32的编码document.addField(“geohash”, geoHash); //将经纬度对应的bash32编码存入索引。

查询阶段

在solrconfig.xml中配置好QP,该QP将对用户的请求Query进行QParser,

查询语法规范是{!spatial  sfield=geofield pt= latitude, longitude d=xx, sphere_radius=xx }

sfield:geohash对应的域名

pt:经纬度字符串

d=球面距离

sphere_radius:圆周半径

接下来看看QP是如何解析上述查询语句,然后生成基于GeoHash的Query的,见如下代码,代码来源SpatialFilterQParser的parse()方法:

//GeohashType一定是继承SpatialQueryable的
if (type instanceof SpatialQueryable) {
        double radius = localParams.getDouble(SpatialParams.SPHERE_RADIUS, DistanceUtils.EARTH_MEAN_RADIUS_KM); //圆周半径
//pointStr=经纬度串,dist=距离,DistanceUnits.KILOMETERS 距离单位
        SpatialOptions opts = new SpatialOptions(pointStr, dist, sf, measStr, radius, DistanceUnits.KILOMETERS);
        opts.bbox = bbox;
//通过GeoHashField 创建查询Query
        result = ((SpatialQueryable)type).createSpatialQuery(this, opts);
      }
其中最核心的方法便是GeoHashField的createSpatialQuery(),该方法负责生成基于geoHash的查询Query,展开看该方法:
public Query createSpatialQuery(QParser parser, SpatialOptions options) {
    double [] point = new double[0];
try {
//解析经纬度
      point = DistanceUtils.parsePointDouble(null, options.pointStr, 2);
    } catch (InvalidGeoException e) {
      throw new SolrException(SolrException.ErrorCode.BAD_REQUEST, e);
}
//将经纬度编码成bash32,对如何编码请看本文geohash算法解析篇幅
    String geohash = GeoHashUtils.encode(point[0], point[1]);
    //TODO: optimize this
    return new SolrConstantScoreQuery(new ValueSourceRangeFilter(new GeohashHaversineFunction(getValueSource(options.field, parser),
            new LiteralValueSource(geohash), options.radius), “0″, String.valueOf(options.distance), true, true));
  }

     从源码中可以看到代码作者有标示TODO:optimize this,笔者从源码中看到这块的实现,也觉得确实有疑惑,整个大体实现流程是基于Lucene的Filter的方式来过滤命中docId,但是其过滤的范围让笔者看起来觉得性能会出现问题,可能也是源码中有TODO:optimize this的缘故吧。

接下来继续讲下核心处理流程,Lucene的查询规则是Query->Weight->Scorer,而主要负责查询遍历结果集合的就是Scorer,该例子也不例外,同样是SolrConstantScoreQueryà ConstantWeightà ConstantScorer,通过Query生成Weight,Weight生成Scorer,熟悉Lucene的读者应该很清楚了,这里不再累述,其中ConstantScorer的通过docIdSetIterator遍历获取满足条件的docId。而docIdSetIterator便是前面源码中的ValueSourceRangeFilter,该Filter将会过滤掉不在一个指定球面距离范围内的数据,而ValueSourceRangeFilter并不是实际工作的类,它又将过滤交给了GeohashHaversineFunction,见ValueSourceRangeFilter如下代码:

 public DocIdSet getDocIdSet(final Map context, final IndexReader reader) throws IOException {
     return new DocIdSet() {
////lowerVal=0,upperVal=distance,includeLower=true,includeupper=true
       @Override
      public DocIdSetIterator iterator() throws IOException {
////valueSource= GeohashHaversineFunction,也是实际进行DocList过滤的类
         return valueSource.getValues(context, reader).getRangeScorer(reader, lowerVal, upperVal, includeLower, includeUpper);
       }
     };
  }

那么继续看GeohashHaversineFunction,首先看其  getRangeScorer()方法,最核心的部分为:

    if (includeLower && includeUpper) {
      return new ValueSourceScorer(reader, this) {
        @Override
        public boolean matchesValue(int doc) {
//计算docId对应的经纬度和查询传入的经纬度的距离
          float docVal = floatVal(doc);
//如果返回的docVal(目标坐标和查询坐标的球面距离)在给定的distance之内则返回true
//也就是说目标地址为待查询的周边范围内
          return docVal >= l && docVal <= u;
        }
      };
    }
所以再看看计算球面距离的GeohashHaversineFunction.floatVal()方法,可以从该方法最终调用的是distance()方法,如下所示:
protected double distance(int doc, DocValues gh1DV, DocValues gh2DV) {
    double result = 0;
    String h1 = gh1DV.strVal(doc); //docId对应的经纬度的base32编码
    String h2 = gh2DV.strVal(doc); //查询的经纬度的base32编码
    if (h1 != null && h2 != null && h1.equals(h2) == false){
      //TODO: If one of the hashes is a literal value source, seems like we could cache it
      //and avoid decoding every time
      double[] h1Pair = GeoHashUtils.decode(h1); //base32解码
      double[] h2Pair = GeoHashUtils.decode(h2);
//计算2个经度纬度之间的球面距离
      result = DistanceUtils.haversine(Math.toRadians(h1Pair[0]), Math.toRadians(h1Pair[1]),
              Math.toRadians(h2Pair[0]), Math.toRadians(h2Pair[1]), radius);
    } else if (h1 == null || h2 == null){
      result = Double.MAX_VALUE;
}
//返回2个经纬度之间球面距离
    return result;
  }

所以整个查询流程是将索引中的所有docId从第一个docId =0开始,对应的经度纬度和查询经纬度的球面距离是否在查询给定的distance之内,满足着将该docId返回,不满足则过滤。

大家可能看到是所有docId,这也是笔者觉得该过滤范围实现不靠谱的地方,也许是作者说需要进一步优化的地方。大家如果对怎么是所有docId进行过滤有疑惑,可以查看ValueSourceScorer的nextDoc() advance()方法,相信看过之后就明白了。到此Solr基于GeoHash的查询实现介绍完毕了。

相关 [solr 空间 搜索] 推荐:

基于Solr的空间搜索(3)

- - 淘宝网综合业务平台团队博客
接上文,本文将继续介绍基于Solr的地理位置搜索的第二种实现方案. 从基于Solr的地理位置搜索(2)文章中可以看到完全基于GeoHash的查询过滤,将完全遍历整个docment文档,从效率上来看并不太合适,所以结合笛卡尔层后,能有效缩减少过滤范围,从性能上能很大程度的提高.       int tier = START_TIER;//开始构建索引的层数.

基于Solr的空间搜索(2)

- - 淘宝网综合业务平台团队博客
本文将继续围绕Solr+Lucene使用Cartesian Tiers 笛卡尔层和GeoHash的构建索引和查询的细节进行介绍. 在Solr中其实支持很多默认距离函数,但是基于坐标构建索引和查询的主要会基于2种方案:. 而这块的源码实现都在lucene-spatial.jar中可以找到. 接下来我将根据这2种方案展开关于构建索引和查询细节进行阐述,都是代码分析,感兴趣的看官可以继续往下看.

基于Solr的空间搜索(1)

- - 淘宝网综合业务平台团队博客
在Solr中基于空间地址查询主要围绕2个概念实现:. Cartesian Tiers 笛卡尔层. Cartesian Tiers是通过将一个平面地图的根据设定的层次数,将每层的分解成若干个网格,如下图所示:.  每层以2的评方递增,所以第一层为4个网格,第二层为16 个,所以整个地图的经纬度将在每层的网格中体现:.

Solr平台化搜索实战必知场景

- - 淘宝网综合业务平台团队博客
这个page是个人汇总了maillist、自己在搜索平台化、通用化过程中遇到的种种需求,为了避开必要的“敬业竞争禁止等”,特地从外网搜罗并汇总代表性的需求. 构成基于solr搜索“策略”参考、搜索应用查询的方案参考,但是,性能问题特别是高级用法,在大数据量时,务必压测,做到心里有底. 这里面给出的方法绝大部分基于solr接口、配置.

Solr SpellCheck 应用

- - 开源软件 - ITeye博客
通过对各类型的SpellCheck组件学习,完成项目拼写检查功能. 本文使用基于拼写词典的实现方式,solr版本为5.3.0. SpellCheck 简述. 拼写检查是对用户错误输入,响应正确的检查建议. 比如输入:周杰轮,响应:你是不是想找 周杰伦. Solr的拼写检查大致可分为两类,基于词典与基于Solr索引.

Solr DocValues详解

- - 企业架构 - ITeye博客
什么是docValues. docValues是一种记录doc字段值的一种形式,在例如在结果排序和统计Facet查询时,需要通过docid取字段值的场景下是非常高效的. 为什么要使用docValues. 这种形式比老版本中利用fieldCache来实现正排查找更加高效,更加节省内存. 倒排索引将字段内存切分成一个term列表,每个term都对应着一个docid列表,这样一种结构使得查询能够非常快速,因为term对应的docid是现成就有的.

solr的使用

- - Web前端 - ITeye博客
solr的原理不和大家一一讲述,主要讲solr在使用过程中的注意事项.  首先是安装solr,安装步骤省略. (不要说我懒,安装步骤导出都是. 成功之后 需要在solr里面建立一个针对你的业务的服务,我想建立一个叫做discuz的服务. 然后你在你的solr目录 :solr-5.5.3/server/solr/  下看见了discuz   ,这是你刚刚创建的,针对某一业务的整个搜索配置都是在这个目录下配置的.

Solr调优参考

- - 淘宝网通用产品团队博客
共整理三部分,第一部分Solr常规处理,第二部分针对性性处理,前者比较通用,后者有局限性. 务必根据具体应用特性,具体调节参数,对比性能. 具体应用需要全面去把控,各个因素一起起作用. 第一部分. E文连接 http://wiki.apache.org/solr/SolrPerformanceFactors.

Solr之缓存篇

- - 淘宝网综合业务平台团队博客
Solr在Lucene之上开发了很多Cache功能,从目前提供的Cache类型有:. 而每种Cache针对具体的查询请求进行对应的Cache. 本文将从几个方面来阐述上述几种Cache在Solr的运用,具体如下:. (1)Cache的生命周期. (2)Cache的使用场景. (3)Cache的配置介绍.

Solr主从备份

- - 研发管理 - ITeye博客
SOLR复制模式,是一种在分布式环境下用于同步主从服务器的一种实现方式,因之前提到的基于rsync的SOLR不同方式部署成本过高,被SOLR1.4版本所替换,取而代之的就是基于HTTP协议的索引文件传输机制,该方式部署简单,只需配置一个文件即可. 以下讲解具体操作步骤: . 步骤分主服务器和从服务器,允许有多个从服务器,即从服务器的配置一样.