IT技术博客大学习 共学习 共进步

基于Solr的空间搜索(2)

淘宝网综合业务平台团队博客 2013-08-15 13:39:11 浏览 3,121 次

   本文将继续围绕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的查询实现介绍完毕了。

建议继续学习

  1. 怎样用好Google进行搜索 (阅读 15,662)
  2. 淘宝搜索:定向抓取网页技术漫谈 (阅读 9,361)
  3. 简析搜索引擎中网络爬虫的搜索策略 (阅读 7,280)
  4. 几种常见的基于Lucene的开源搜索解决方案对比 (阅读 5,981)
  5. 基于用户行为分析的搜索引擎自动性能评价 (阅读 5,602)
  6. 百度搜索URL参数解析 (阅读 5,582)
  7. 用Sphinx快速搭建站内搜索功能 (阅读 5,561)
  8. Xapian搜索体系结构 (阅读 5,161)
  9. 附近地点搜索初探 (阅读 5,140)
  10. 互联网网站的反爬虫策略浅析 (阅读 5,040)