设为首页 收藏本站
查看: 1107|回复: 0

[经验分享] 基于Solr的地理位置搜索(2)

[复制链接]

尚未签到

发表于 2016-12-16 09:32:10 | 显示全部楼层 |阅读模式
  

    本文将继续围绕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、欢迎大家加入本站运维交流群:群②:261659950 群⑤:202807635 群⑦870801961 群⑧679858003
2、本站所有主题由该帖子作者发表,该帖子作者与运维网享有帖子相关版权
3、所有作品的著作权均归原作者享有,请您和我们一样尊重他人的著作权等合法权益。如果您对作品感到满意,请购买正版
4、禁止制作、复制、发布和传播具有反动、淫秽、色情、暴力、凶杀等内容的信息,一经发现立即删除。若您因此触犯法律,一切后果自负,我们对此不承担任何责任
5、所有资源均系网友上传或者通过网络收集,我们仅提供一个展示、介绍、观摩学习的平台,我们不对其内容的准确性、可靠性、正当性、安全性、合法性等负责,亦不承担任何法律责任
6、所有作品仅供您个人学习、研究或欣赏,不得用于商业或者其他用途,否则,一切后果均由您自己承担,我们对此不承担任何法律责任
7、如涉及侵犯版权等问题,请您及时通知我们,我们将立即采取措施予以解决
8、联系人Email:admin@iyunv.com 网址:www.yunweiku.com

所有资源均系网友上传或者通过网络收集,我们仅提供一个展示、介绍、观摩学习的平台,我们不对其承担任何法律责任,如涉及侵犯版权等问题,请您及时通知我们,我们将立即处理,联系人Email:kefu@iyunv.com,QQ:1061981298 本贴地址:https://www.yunweiku.com/thread-314995-1-1.html 上篇帖子: 利用solr构建企业搜索平台 (九) 下篇帖子: solr 搜索短语搜索不出来的解决方案
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

扫码加入运维网微信交流群X

扫码加入运维网微信交流群

扫描二维码加入运维网微信交流群,最新一手资源尽在官方微信交流群!快快加入我们吧...

扫描微信二维码查看详情

客服E-mail:kefu@iyunv.com 客服QQ:1061981298


QQ群⑦:运维网交流群⑦ QQ群⑧:运维网交流群⑧ k8s群:运维网kubernetes交流群


提醒:禁止发布任何违反国家法律、法规的言论与图片等内容;本站内容均来自个人观点与网络等信息,非本站认同之观点.


本站大部分资源是网友从网上搜集分享而来,其版权均归原作者及其网站所有,我们尊重他人的合法权益,如有内容侵犯您的合法权益,请及时与我们联系进行核实删除!



合作伙伴: 青云cloud

快速回复 返回顶部 返回列表