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

基于Solr的空间搜索(3)

淘宝网综合业务平台团队博客 2013-08-15 13:40:09 浏览 4,101 次

   接上文,本文将继续介绍基于Solr的地理位置搜索的第二种实现方案

   CartesianTiers+GeoHash

   从基于Solr的地理位置搜索(2)文章中可以看到完全基于GeoHash的查询过滤,将完全遍历整个docment文档,从效率上来看并不太合适,所以结合笛卡尔层后,能有效缩减少过滤范围,从性能上能很大程度的提高。

   构建索引阶段:

String geoHash = GeoHashUtils.encode(latitude, longitude);
     docment.addField(“geohash”, geoHash);
     //Cartesian Tiers
     int tier = START_TIER;//开始构建索引的层数
     //Create a bunch of tiers, each deeper level has more precision
//将一条记录的经纬度对应全部笛卡尔层的tierBoxId作为域值构建索引
     for (CartesianTierPlotter plotter : plotters) {
       docment.addField(“tier_” + tier , plotter.getTierBoxId(latitude, longitude));
       tier++;
     }

   看到这里大家肯定明白了。越相近的经纬度在同层肯定会在同一个网格中,所以他们存储的tierBoxId就会是一样。那么查询的时候通过经纬度对应层的tierBoxId,也就能找到相同层域的docId,但是如果给定的的查询范围大,可能需要将若干层的所属网格的docId都查到。

      整个查询过程是先通过笛卡尔层将若干个网格涉及的DocList存入bitSet,如下代码所示:

public DocIdSet getDocIdSet(final IndexReader reader) throws IOException {
   final FixedBitSet bits = new FixedBitSet(reader.maxDoc());
final TermDocs termDocs = reader.termDocs();
//需要查询的若干层网格的boxIdList,当然至此已经过滤掉不需要查询层的boxIdList
   final List<Double> area = shape.getArea();
   int sz = area.size();
   final Term term = new Term(fieldName);//
   // iterate through each boxid
   for (int i =0; i< sz; i++) {
     double boxId = area.get(i).doubleValue();
termDocs.seek(term.createTerm(NumericUtils.doubleToPrefixCoded(boxId)));
     // iterate through all documents
     // which have this boxId
//遍历所有包含给定boxId的docList,并将其放入bitset
     while (termDocs.next()) {
       bits.set(termDocs.doc());
     }
   }
   return bits;
 }

   介绍完笛卡尔层的计算后,接下来介绍笛卡尔层过滤后返还的bitset如何和geoHash结合,从实现上讲其实很简单,就是将通过笛卡尔层过滤的数据结果集合 依次遍历计算其与查询给定的经纬度坐标的球面距离,同时将该计算距离和查询指定范围距离进行比较,如果大于给定距离,则将当前记录继续过滤掉,那么最终剩下的数据结果集合,将是满足查询条件的地理位置结果集合。具体实现流程见如下代码:

//将笛卡尔层的Filter作为Geohash的Filter参数传递进去,形成一个过滤链
filter = distanceFilter = new GeoHashDistanceFilter(cartesianFilter, lat, lng, miles, geoHashFieldPrefix);

   再看GeoHashDistanceFilter中最核心的方法getDocIdSet():

public DocIdSet getDocIdSet(IndexReader reader) throws IOException {
     //在这里使用到了Lucene的FieldCache来作为缓存,实际上缓存了一个以docId为下标,base32编码为值的数组
   final String[] geoHashValues = FieldCache.DEFAULT.getStrings(reader, geoHashField);
   final int docBase = nextDocBase;
   nextDocBase += reader.maxDoc();
   return new FilteredDocIdSet(startingFilter.getDocIdSet(reader)) {
     @Override
     public boolean match(int doc) {
       //通过笛卡尔层的过滤后的doc直接找到对应的base32编码
       String geoHash = geoHashValues[doc];
       //通过解码将base32还原成经纬度坐标
       double[] coords = GeoHashUtils.decode(geoHash);
       double x = coords[0];
       double y = coords[1];
       Double cachedDistance = distanceLookupCache.get(geoHash);
       double d;
       if (cachedDistance != null) {
         d = cachedDistance.doubleValue();
       } else {
          //计算2个经纬度坐标的距离
         d = DistanceUtils.getDistanceMi(lat, lng, x, y);
         distanceLookupCache.put(geoHash, d);
       }
      //小于给定查询距离的的docid放入缓存,以供下次使用,同时返回True代表当前docId是满足条件的记录
       if (d < distance){
         distances.put(doc+docBase, d);
         return true;
       } else {
         return false;
       }
     }
   };

     从上述分析中大家应该可以想到 采用笛卡尔层 Filter结合GoHash Filter的实现方案,在计算规模上会比单独使用GeoHash少了很多,而在查询性能也会有更优异的表现。

   最后附上一个本地Demo的查询实例,用geofilter查找给定经纬度500km内的数据:

   q=*:*&fq={!geofilt pt=30.15,-79.85 sfield=tier d=500}

   查询返回结果:

   

建议继续学习

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