ES7學習筆記(十三)GEO位置搜索

ES的基本內容介紹的已經差不多了,最後我們再來看看GEO位置搜索,現在大部分APP都有基於位置搜索的功能,比如:我們點外賣,可以按照離我們的距離進行排序,這樣可以節省我們的配送費和送餐的時間;還有找工作時,也可以按照離自己家的距離進行排序,誰都想找個離家近的工作,對吧。這些功能都是基於GEO搜索實現的,目前支持GEO搜索功能的中間件有很多,像MySQL、Redis、ES等。我們看看在ES當中怎麼實現GEO位置搜索。

GEO字段的創建

GEO類型的字段是不能使用動態映射自動生成的,我們需要在創建索引時指定字段的類型爲geo_pointgeo_point類型的字段存儲的經緯度,我們看看經緯度是怎麼定義的,

英文 簡寫 正數 負數
維度 latitude lat 北緯 南緯
經度 longitude lon或lng 東經 西經

經度的簡寫有2個,一般常用的是lon,lng則在第三方地圖的開放平臺中使用比較多。下面我們先創建一個帶有geo_point類型字段的索引,如下:

PUT /my_geo
{
    "settings":{
        "analysis":{
            "analyzer":{
                "default":{
                    "type":"ik_max_word"
                }
            }
        }
    },
    "mappings":{
        "dynamic_date_formats":[
            "MM/dd/yyyy",
            "yyyy/MM/dd HH:mm:ss",
            "yyyy-MM-dd",
            "yyyy-MM-dd HH:mm:ss"
        ],
        "properties":{
            "location":{
                "type":"geo_point"
            }
        }
    }
}

創建了一個my_geo索引,在索引中有一些基礎的配置,默認IK分詞器,動態映射的時間格式。重點是最後我們添加了一個字段location,它的類型是geo_point

索引創建完了,我們添加兩條數據吧,假設,路人甲在北京站,路人乙在朝陽公園。那麼我們怎麼“北京站”和“朝陽公園”的經緯度呢?我們在做項目時,前端都會接地圖控件,經緯度的信息可以調用地圖控件的API獲取。在咱們的示例中,也不接地圖控件了,太麻煩了,直接在網上找到“北京站”和“朝陽公園”的座標吧。

我們查到“北京站”的座標如下:

然後添加一條數據:

POST /my_geo/_doc
{
    "name":"路人甲",
    "location":{
        "lat": 39.90279998006104,
        "lon": 116.42703999493406
    }
}

再查“朝陽公園”的座標

再添加“路人乙”的信息

POST /my_geo/_doc
{
    "name":"路人乙",
    "location":{
        "lat": 39.93367367974064,
        "lon": 116.47845257733152
    }
}

我們再用elasticsearch-head插件看一下索引中的數據:

GEO查詢

“路人甲”和“路人乙”的信息都有了,但是沒有location字段的信息,因爲location是特性類型的字段,在這裏是展示不出來的。我們搜索一下吧,看看怎麼用geo搜索,假設“我”的位置在“工體”,我們先要查到“工體”的座標,

然後再查詢5km範圍內都有誰,發送請求如下:

POST /my_geo/_search
{
    "query":{
        "bool":{
            "filter":{
                "geo_distance":{
                    "distance":"5km",
                    "location":{
                        "lat":39.93031708627304,
                        "lon":116.4470385453491
                    }
                }
            }
        }
    }
}

在查詢的時候用的是filter查詢,再filter查詢裏再使用geo_distance查詢,我們定義距離distance爲5km,再指定geo類型的字段location,當前的座標爲:39.93031708627304N,116.4470385453491E。查詢一下,看看結果:

{
    ……
    "hits":[
        {
            "_index":"my_geo",
            "_type":"_doc",
            "_id":"AtgtXnIBOZNtuLQtIVdD",
            "_score":0,
            "_source":{
                "name":"路人甲",
                "location":{
                    "lat": 39.90279998006104,
        			"lon": 116.42703999493406
                }
            }
        },
        {
            "_index":"my_geo",
            "_type":"_doc",
            "_id":"ZdguXnIBOZNtuLQtMVfA",
            "_score":0,
            "_source":{
                "name":"路人乙",
                "location":{
                    "lat": 39.93367367974064,
        			"lon": 116.47845257733152
                }
            }
        }
    ]
}

看來,我們站在“工體”,“北京站”的路人甲和“朝陽公園”的路人乙都在5km的範圍內。把範圍縮短一點如何,改爲3km看看,搜索的請求不變,只是把distance改爲3km,看看結果吧,

{
    ……
    "hits":[
        {
            "_index":"my_geo",
            "_type":"_doc",
            "_id":"ZdguXnIBOZNtuLQtMVfA",
            "_score":0,
            "_source":{
                "name":"路人乙",
                "location":{
                    "lat": 39.93367367974064,
        			"lon": 116.47845257733152
                }
            }
        }
    ]
}

只有在“朝陽公園”的路人乙被搜索了出來。完全符合預期,我們再看看程序中怎麼使用GEO搜索。

JAVA 代碼

在定義實體類時,對應的GEO字段要使用特殊的類型,如下:

@Setter@Getter
public class MyGeo {

    private String name;
    private GeoPoint location;

}

location的類型是GeoPoint,添加數據的方法沒有變化,轉化成Json就可以了。再看看查詢怎麼用,

public void searchGeo() throws IOException {
    SearchRequest searchRequest = new SearchRequest("my_geo");
    SearchSourceBuilder ssb = new SearchSourceBuilder();

    //工體的座標
    GeoPoint geoPoint = new GeoPoint(39.93367367974064d,116.47845257733152d);
    //geo距離查詢  name=geo字段
    QueryBuilder qb = QueryBuilders.geoDistanceQuery("location")
        //距離 3KM
        .distance(3d, DistanceUnit.KILOMETERS)
        //座標工體
        .point(geoPoint);

    ssb.query(qb);
    searchRequest.source(ssb);
    SearchResponse response = client.search(searchRequest, RequestOptions.DEFAULT);

    for (SearchHit hit : response.getHits().getHits()) {
        System.out.println(hit.getSourceAsString());
    }


}
  • SearchRequest指定索引my_geo
  • 創建工體的座標點GeoPoint
  • 創建geo距離查詢,指定geo字段location,距離3km,座標點工體
  • 其他的地方沒有變化

運行一下,看看結果,

{"name":"路人乙","location":{"lat":39.93360786576342,"lon":116.47853840802}}

只有在“朝陽公園”的路人乙被查詢了出來,符合預期。

距離排序

有的小夥伴可能會有這樣的疑問,我不想按照距離去查詢,只想把查詢結果按照離“我”的距離排序,該怎麼做呢?再看一下,

public void searchGeoSort() throws IOException {
    SearchRequest searchRequest = new SearchRequest("my_geo");
    SearchSourceBuilder ssb = new SearchSourceBuilder();

    //工體的座標
    GeoPoint geoPoint = new GeoPoint(39.93367367974064d,116.47845257733152d);

    GeoDistanceSortBuilder sortBuilder = SortBuilders
        .geoDistanceSort("location", geoPoint)
        .order(SortOrder.ASC);

    ssb.sort(sortBuilder);
    searchRequest.source(ssb);
    SearchResponse response = client.search(searchRequest, RequestOptions.DEFAULT);

    for (SearchHit hit : response.getHits().getHits()) {
        System.out.println(hit.getSourceAsString());
    }
}

這次查詢並沒有設置查詢條件,而是創建了一個geo距離排序,同樣,先指定geo字段location,和當前的座標工體,再設置排序是升序。運行一下,看看結果,

{"name":"路人乙","location":{"lat":39.93360786576342,"lon":116.47853840802}}
{"name":"路人甲","location":{"lat":39.902799980059335,"lon":116.42721165631102}}

離“工體”比較近的“路人乙”排在了第一個,也是符合預期的。有問題大家評論區留言吧~

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章