随着生活服务类应用最近一年的崛起和普及,基于地理位置的内容正在日益重要。LBS已是老生常谈,不过在PC、在移动互联网时代,LBS在导航之外都未出现第二个杀手级应用。在没有O2O之前,LBS所依重的POI(Point of Interest)并未真正成为用户的“Interest”,人们的兴趣还是只存在于网络的虚拟内容,游戏、新闻、文学、网页、多媒体,等等。
O2O大热之后,越来越多的杀手级应用开始出现:打车拼车租车等用车服务进去主界面是地图,陌陌这个声名鹊起的后来者基于位置做出了社交的差异化,团购上门分享所有热门O2O应用均离不开地图。线下实体、生活服务、身边内容的呈现形式都基于地图的“点”而不是基于时间的“线”,人们不需刷而是搜索、缩放、点选,乃至不做任何操作根据位置移动来与之交互。我将这类内容称之为LocationPoint,这将成为Timeline之后的又一种至关重要的内容形式。
7.1 地理坐标点
地理坐标点 是指地球表面可以用经纬度描述的一个点。 地理坐标点可以用来计算两个坐标间的距离,还可以判断一个坐标是否在一个区域中,或在聚合中。
地理坐标点不能被动态映射 (dynamic mapping)自动检测,而是需要显式声明对应字段类型为 geo-point
:
PUT /attractions
{
"mappings": {
"restaurant": {
"properties": {
"name": {
"type": "string"
},
"location": {
"type": "geo_point"
}
}
}
}
}
7.2 经纬度坐标格式
如上例,location
字段被声明为 geo_point
后,我们就可以索引包含了经纬度信息的文档了。 经纬度信息的形式可以是字符串、数组或者对象:
PUT /attractions/restaurant/1
{
"name": "Chipotle Mexican Grill",
"location": "40.715, -74.011"
}
PUT /attractions/restaurant/2
{
"name": "Pala Pizza",
"location": {
"lat": 40.722,
"lon": -73.989
}
}
PUT /attractions/restaurant/3
{
"name": "Mini Munchies Pizza",
"location": [ -73.983, 40.719 ]
}
1 | 字符串形式以半角逗号分割,如 "lat,lon" 。 |
---|---|
2 | 对象形式显式命名为 lat 和 lon 。 |
3 | 数组形式表示为 [lon,lat] |
大家可以上百度或者高德的地图开放平台了解相关的操作,以百度地图开放为例(http://lbsyun.baidu.com/jsdemo.htm),比如想获取地图上某个点的经纬度,如下图所示:
7.3 通过地理坐标点过滤
我们有时候,希望可以根据当前所在的位置,找到自己身边的符合条件的一些商店,酒店之类的。它主要支持两种类型的地理查询:
一种是地理点(geo_point),即经纬度查询,另一种是地理形状查询(geo_shape),即支持点、线、圈、多边形查询.
ES中有3中位置相关的过滤器,用于过滤位置信息:
- geo_distance: 查找距离某个中心点距离在一定范围内的位置
- geo_bounding_box: 查找某个长方形区域内的位置
- geo_polygon: 查找位于多边形内的地点。
7.4 geo_distance
地理距离过滤器( geo_distance
)以给定位置为圆心画一个圆,来找出那些地理坐标落在其中的文档.
广泛地应用在O2O,LBS领域,比如查找附近的酒店、包店、共享单车等。如下图所示,按距用户的距离查询周边酒店:
例如:查询以某个经纬度为中心周围500KM以内的城市
{
"query": {
"geo_distance": {
"location": [118.0, 24.46667],
"distance": 500000.0,
"distance_type": "arc",
"validation_method": "STRICT",
"ignore_unmapped": false,
"boost": 1.0
}
}
}
JAVA代码示例:com.javablog.elasticsearch.query.impl.GeoQueryImpl
/**
* 以某个经纬度为中心查询周围限定距离的文档
* @param indexName 索引
* @param typeName 类型
* @param lot 经度
* @param lon 纬度
* @param distance 距离
* @throws IOException
*/
public void geoDistanceQuery(String indexName, String typeName, double lot, double lon, int distance) throws IOException {
SearchRequest searchRequest = new SearchRequest(indexName);
searchRequest.types(typeName);
SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();
GeoDistanceQueryBuilder geoDistanceQueryBuilder = QueryBuilders.geoDistanceQuery("location")
.point(lot,lon).
distance(distance, DistanceUnit.KILOMETERS).
geoDistance(GeoDistance.ARC);
searchSourceBuilder.query(geoDistanceQueryBuilder);
searchRequest.source(searchSourceBuilder);
log.info("source:" + searchRequest.source());
SearchResponse searchResponse = restHighLevelClient.search(searchRequest, RequestOptions.DEFAULT);
SearchHits hits = searchResponse.getHits();
System.out.println(hits.totalHits);
SearchHit[] h = hits.getHits();
for (SearchHit hit : h) {
System.out.println(hit.getSourceAsMap());
}
}
演示用例:com.javablog.elasticsearch.test.document.GeoQueryTest
代码依次执行:
1、建立索引
2、初始化数据
3、执行testGeoDistanceQuery,搜索“距厦门500公里以内的城市”
package com.javablog.elasticsearch.test.document;
import com.javablog.elasticsearch.SearchServiceApplication;
import com.javablog.elasticsearch.document.DocService;
import com.javablog.elasticsearch.document.IndexService;
import com.javablog.elasticsearch.query.GeoQuery;
import org.elasticsearch.action.admin.indices.create.CreateIndexRequest;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.common.xcontent.XContentBuilder;
import org.elasticsearch.common.xcontent.json.JsonXContent;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.test.context.web.WebAppConfiguration;
import java.io.IOException;
@RunWith(SpringJUnit4ClassRunner.class)
@SpringBootTest(classes = SearchServiceApplication.class)
@WebAppConfiguration
public class GeoQueryTest {
private final static Logger log = LoggerFactory.getLogger(GeoQueryTest.class);
private String indexName = "cn_large_cities";
private String type = "city_type";
@Autowired
private IndexService indexService;
@Autowired
private DocService docService;
@Autowired
private GeoQuery geoQuery;
@Test
public void testCreateIndex() throws IOException {
CreateIndexRequest request = new CreateIndexRequest(indexName);
buildSetting(request);
buildIndexMapping(request, type);
indexService.createIndex(indexName,type,request);
}
@Test
public void testDelIndex() throws IOException {
indexService.deleteIndex(indexName);
}
//设置分片
private void buildSetting(CreateIndexRequest request) {
request.settings(Settings.builder().put("index.number_of_shards", 3)
.put("index.number_of_replicas", 2));
}
/**
* 生成地理信息表索引结构
*
* city 城市
* state 省
* location 位置
* @param request
* @param type
* @throws IOException
*/
private void buildIndexMapping(CreateIndexRequest request, String type) throws IOException {
XContentBuilder mappingBuilder = JsonXContent.contentBuilder()
.startObject()
.startObject("properties")
.startObject("city")
.field("type", "keyword")
.field("index", "true")
.endObject()
.startObject("state")
.field("type", "keyword")
.field("index", "true")
.endObject()
.startObject("location")
.field("type", "geo_point")
// .field("index", "true")
.endObject()
.endObject()
.endObject();
request.mapping(type, mappingBuilder);
}
@Test
public void testInitData() throws IOException {
String json1 ="{" +
"\"city\": \"北京\", " +
"\"state\": \"北京\"," +
"\"location\": {\"lat\": \"39.91667\", \"lon\": \"116.41667\"}"
+"}";
String json2 ="{" +
"\"city\": \"上海\", " +
"\"state\": \"上海\"," +
"\"location\": {\"lat\": \"34.50000\", \"lon\": \"121.43333\"}"
+"}";
String json3 ="{" +
"\"city\": \"厦门\", " +
"\"state\": \"福建\"," +
"\"location\": {\"lat\": \"24.46667\", \"lon\": \"118.10000\"}"
+"}";
String json4 ="{" +
"\"city\": \"福州\", " +
"\"state\": \"福建\"," +
"\"location\": {\"lat\": \"26.08333\", \"lon\": \"119.30000\"}"
+"}";
String json5 ="{" +
"\"city\": \"广州\", " +
"\"state\": \"广东\"," +
"\"location\": {\"lat\": \"23.16667\", \"lon\": \"113.23333\"}"
+"}";
docService.add(indexName, type, json1);
docService.add(indexName, type, json2);
docService.add(indexName, type, json3);
docService.add(indexName, type, json4);
docService.add(indexName, type, json5);
}
@Test
public void testGeoDistanceQuery() throws IOException {
//距厦门500公里以内的城市
geoQuery.geoDistanceQuery(indexName,type,24.46667,118.0000,500);
}
}
7.5 geo_bounding_box
查找某个长方形区域内的位置,以高德地图开放平台为例(https://lbs.amap.com/api/javascript-api/example/overlayers/rectangle-draw-and-edit),通过在地图上用矩形框选取一定范围来搜索。如下图所示:
这是目前为止最有效的地理坐标过滤器了,因为它计算起来非常简单。 你指定一个矩形的 顶部 , 底部 , 左边界 ,和 右边界 ,然后过滤器只需判断坐标的经度是否在左右边界之间,纬度是否在上下边界之间:
他可以指定一下几个属性:
top_left
: 指定最左边的经度和最上边的纬度
bottom_right
: 指定右边的经度和最下边的纬度
例如:查询某个矩形范围内的文档
{
"query": {
"geo_bounding_box": {
"location": {
"top_left": [-74.0, 40.8],
"bottom_right": [-73.0, 40.715]
},
"validation_method": "STRICT",
"type": "MEMORY",
"ignore_unmapped": false,
"boost": 1.0
}
}
}
JAVA代码示例:com.javablog.elasticsearch.query.impl.GeoQueryImpl
/**
* 搜索矩形范围内的文档
* @param indexName 索引
* @param typeName TYPE
* @param top 最上边的纬度
* @param left 最左边的经度
* @param bottom 最下边的纬度
* @param right 右边的经度
* @throws IOException
*/
public void geoBoundingBoxQuery(String indexName, String typeName, double top,double left,double bottom,double right) throws IOException {
SearchRequest searchRequest = new SearchRequest(indexName);
searchRequest.types(typeName);
SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();
GeoBoundingBoxQueryBuilder address = QueryBuilders.geoBoundingBoxQuery("location").setCorners(top, left, bottom, right);
searchSourceBuilder.query(address);
searchRequest.source(searchSourceBuilder);
log.info("source:" + searchRequest.source());
SearchResponse searchResponse = restHighLevelClient.search(searchRequest, RequestOptions.DEFAULT);
SearchHits hits = searchResponse.getHits();
System.out.println(hits.totalHits);
SearchHit[] h = hits.getHits();
for (SearchHit hit : h) {
System.out.println(hit.getSourceAsMap());
}
}
演示用例:com.javablog.elasticsearch.test.document.GeoQueryTest
@Test
public void testGeoBoundingBoxh() throws IOException {
geoQuery.geoBoundingBoxQuery(indexName,type,40.8,-74.0,40.715,-73.0);
}
7.6 geo_polygon
根据给定的多个点组成的多边形,查询范围内的点。以高德地图开放平台为例(https://lbs.amap.com/api/javascript-api/example/overlayers/polygon-draw-and-edit),通过多个点来确定一个面积,在些面积区域内搜索。
例如:搜索某个多边型区域的文档。
{
"query": {
"geo_polygon": {
"location": {
"points": [
[-72.0, 42.0],
[117.0, 39.0],
[117.0, 40.0],
[-72.0, 42.0]
]
},
"validation_method": "STRICT",
"ignore_unmapped": false,
"boost": 1.0
}
}
}
JAVA代码示例:com.javablog.elasticsearch.query.impl.GeoQueryImpl
public void geoPolygonQuery(String indexName, String typeName) throws IOException {
SearchRequest searchRequest = new SearchRequest(indexName);
searchRequest.types(typeName);
SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();
List<GeoPoint> points=new ArrayList<GeoPoint>();
points.add(new GeoPoint(42, -72));
points.add(new GeoPoint(39, 117));
points.add(new GeoPoint(40, 117));
GeoPolygonQueryBuilder geoPolygonQueryBuilder = QueryBuilders.geoPolygonQuery("location", points);
searchSourceBuilder.query(geoPolygonQueryBuilder);
searchRequest.source(searchSourceBuilder);
log.info("source:" + searchRequest.source());
SearchResponse searchResponse = restHighLevelClient.search(searchRequest, RequestOptions.DEFAULT);
SearchHits hits = searchResponse.getHits();
System.out.println(hits.totalHits);
SearchHit[] h = hits.getHits();
for (SearchHit hit : h) {
System.out.println(hit.getSourceAsMap());
}
}
演示用例:com.javablog.elasticsearch.test.document.GeoQueryTest
@Test
public void testPolygonQuery() throws IOException {
geoQuery.geoPolygonQuery(indexName,type);
}
完整代码:https://github.com/chutianmen/elasticsearch-examples