本文將繼續圍繞Solr+Lucene使用Cartesian Tiers 笛卡爾層和GeoHash的構建索引和查詢的細節進行介紹 git
在Solr中其實支持不少默認距離函數,可是基於座標構建索引和查詢的主要會基於2種方案: 算法
(1)GeoHash bash
(2)Cartesian Tiers+GeoHash ide
而這塊的源碼實現都在lucene-spatial.jar中能夠找到。接下來我將根據這2種方案展開關於構建索引和查詢細節進行闡述,都是代碼分析,感興趣的看官能夠繼續往下看。GeoHash 函數
構建索引階段 性能
定義geohash域,在schema.xml中定義: 優化
<fieldtype name=「geohash」 class=「solr.GeoHashField」/> this
接下來再構建索引的時候使用到lucene-spatial.jar的GeoHashUtils類: 編碼
String geoHash = GeoHashUtils.encode(latitude, longitude);//經過geoHash算法將經緯度變成base32的編碼document.addField(「geohash」, geoHash); //將經緯度對應的bash32編碼存入索引。 spa
查詢階段
在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的查詢實現介紹完畢了。