日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 编程资源 > 编程问答 >内容正文

编程问答

基于Solr的空间搜索学习笔记

發布時間:2025/7/25 编程问答 23 豆豆
生活随笔 收集整理的這篇文章主要介紹了 基于Solr的空间搜索学习笔记 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.
基于Solr的空間搜索學習筆記
在Solr中基于空間地址查詢主要圍繞2個概念實現:
(1) Cartesian Tiers 笛卡爾層
Cartesian Tiers是通過將一個平面地圖的根據設定的層次數,將每層的分解成若干個網格,如下圖所示:



?
??? 每層以2的評方遞增,所以第一層為4個網格,第二層為16 個,所以整個地圖的經緯度將在每層的網格中體現:


?

?

笛卡爾層在Lucene中對空間地理位置查詢最大的用處在查找周邊地址的時候有效的減少查詢量,即將查詢量可以控制在分層后最小的網格中的若干docId。
那么如何構建這樣的索引結構呢,其實很簡單,只需要對應笛卡爾層的層數來構建域即可。

也即是tiers0->field_0,tiers1->field_1,tiers2-field_2,……,tiers19->field_19。(一般20層即可)。每個對應笛卡爾層次的域將根據當前這條記錄的經緯度通過笛卡爾算法計算出歸屬于當前層的網格,然后將gridId(網格唯一標示)以term的方式存入索引。這樣每條記錄關于笛卡爾0-19的域將都會有一個gridId對應起來。但是查詢的時候一般是需要查周邊的地址,那么可能周邊的范圍超過一個網格的范圍,那么實際操作過程是根據經緯度和一個距離確定出需要涉及查詢的從19-0(從高往低查,留給讀者思考)若干層對應的若干網格的數據(關于代碼實現在后面的文章內容闡述)。那么一個經緯度周邊地址的查詢只需要如下圖圓圈內的數據:




所以通過這樣的數據過濾,將極大的減少計算量。
(2) GeoHash算法
在Lucene索引中將經緯度的二維坐標通過geohash,變成一個一維的字符串base32的坐標,例如,經緯度對應一個base32的坐標為DRT2Y,那這個base32的字符串什么意思呢,
其實編碼中每個字符都是代表一個區域,并且前面的字符是后面字符的父區域,即R是D區域內的子區域,T又為D區域的子區域,大家可以從如下圖片獲得base32的層級關系(以下圖片均來自互聯網):



?

進入D區域,則看到又分為若干區域,而R為其子區域:



?

繼續進入R區域,可以繼續看到有子區域T區域:



?

而2Y也是基于以上的關系類推,所以一個base32的編碼是標示一個區域,而編碼過程中會根據經緯度的精度來確定這個區域大小。從上面的解釋大家肯定會想到編碼的前綴是表示更大的區域。例如wx4g0ec1,它的前綴wx4g0e表示包含編碼wx4g0ec1在內的更大區域。所以根據這個特點,利用模糊查詢是可以達到一種附近地點的查詢。
geohash算法實現其實非常簡單,網上有很多例子,在這里借用下這些例子再加上比較詳細的說明。基本算法流程是基于多輪的收斂,以達到滿足精度要求為止。具體流程以(39.92324 緯度, 116.3906 經度)為例,首先將緯度的范圍(-90, 90)平分成兩個區間(-90, 0)、(0, 90),如果目標緯度位在(-90,0),則編碼為0,在(0,90)則編碼為1。由于上面的例子中維度39.92324是屬于(0, 90),所以第一輪獲得的編碼位取1。接下來再將(0, 90)分成 (0, 45), (45, 90)兩個區間,而39.92324位于(0, 45),所以編碼為0。以此類推,直到精度符合要求為止,如下圖所示:



?

所以通過16輪的計算后得到經度39.92324的編碼為:1011 1000 1100 0111 1001
經度也用同樣的算法,對(-180, 180)多輪的依次細分計算:

?

得到經度116.3906的編碼為1101 0010 1100 0100 0100
經緯度的編碼都計算完畢后,接下來就需要合并經緯度的編碼,規則是以經度開始,依次每次取一位合并成5位的新編碼,如上圖紅色字標示順序所示:



?

完成合并編碼后就需要將該編碼和base32編碼表對應起來,做法是每5位為一個十進制數,以11100為例,它的十進制數是28,所以對應的base32編碼表示W,如下圖所示:



?

其他的五位編碼依次從表中找到對應位置后,(39.92324 緯度, 116.3906 經度)的base32編碼為:wx4g0ec1
解碼算法與編碼算法相反,先進行base32解碼,然后分離出經緯度,最后根據二進制編碼對經緯度范圍進行細分即可,這里不再贅述。不過由于geohash表示的是區間,編碼越長越精確,但不可能解碼出完全一致的地址

關于Solr+Lucene使用Cartesian Tiers 笛卡爾層和GeoHash的構建索引和查詢的細節介紹將在新的Blog中闡述。

在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()方法:

Java代碼 ?
  • //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,展開看該方法:

    ?

    ?

    Java代碼 ?
  • 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如下代碼:

    ?

    Java代碼 ?
  • ?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()方法,最核心的部分為:

    Java代碼 ?
  • ????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()方法,如下所示:

    Java代碼 ?
  • 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的空间搜索学习笔记的全部內容,希望文章能夠幫你解決所遇到的問題。

    如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。