到这里Elasticsearch的整个系列分享就基本上结束了,当然后续还是会针对某一点进行讲解。为何要在实践篇中讲解"查找附近的人"呢?说实话,想了很久,最终才确定下来,总体希望这个实践对今后的工作过程中有帮助。
随着移动设备的普及,很多移动APP都提供了LBS(Location Based Service)。其实LBS并不是什么新东西,但它也带来了不一样的改变,一方面可以提高以前做不到的事情,一方面提升用户体验。下面我们将基于位置实现这一个类似的功能:比如微信中附近的人。
要实现这样一个需求,可用的技术非常多。我是这么考虑的:
1. 评估实现它的复杂度(技术难度,也可以认为是风险点)
2. 大数据量下的实际情况,技术的成熟度(业界都是怎么玩的)
当然,基于es实现它,es有一个对应的数据类型Geo-point。
geohash基本原理是将地球理解为一个二维平面,将平面递归分解成更小的子块,每个子块在一定经纬度范围内拥有相同的编码,这种方式简单粗暴,可以满足对小规模的数据进行经纬度的检索。它的详细介绍在wiki上,花上5分钟就可以看完。
以经纬度值:(116.389550, 39.928167)进行算法说明,对纬度39.928167进行逼近编码 (地球纬度区间是[-90,90])
1.区间[-90,90]进行二分为[-90,0),[0,90],称为左右区间,可以确定39.928167属于右区间[0,90],给标记为1
2.接着将区间[0,90]进行二分为 [0,45),[45,90],可以确定39.928167属于左区间 [0,45),给标记为0
3.递归上述过程39.928167总是属于某个区间[a,b]。随着每次迭代区间[a,b]总在缩小,并越来越逼近39.928167
4.如果给定的纬度x(39.928167)属于左区间,则记录0,如果属于右区间则记录1,序列的长度跟给定的区间划分次数有关,如下图5.同理,地球经度区间是[-180,180],可以对经度116.389550进行编码。通过上述计算,纬度产生的编码为1 1 0 1 0 0 1 0 1 1 0 0 0 1 0,经度产生的编码为1 0 1 1 1 0 0 0 1 1 0 0 0 1 1。合并:偶数位放经度,奇数位放纬度,把2串编码组合生成新串:11100 11101 00100 01111 0000 01101。将11100 11101 00100 01111 0000 01101转成十进制,对应着28、29、4、15,0,13 十进制对应的base32编码就是wx4g0e。同理,将编码转换成经纬度的解码算法与之相反。
第一步,建立一个简易的模型
//建立模型
public class WxUser {
private String uid;
private String nickName;
private String sex = "女";
private int age;
/**
* 它支持4方式,这里用数组
*/
private double[] location;
public String getUid() {
return uid;
}
public void setUid(String uid) {
this.uid = uid;
}
public String getNickName() {
return nickName;
}
public void setNickName(String nickName) {
this.nickName = nickName;
}
public String getSex() {
return sex;
}
public void setSex(String sex) {
this.sex = sex;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public double[] getLocation() {
return location;
}
public void setLocation(double[] location) {
this.location = location;
}
}
第二步,坐标查询要借助个在线工具
//准备es数据,以上海人民广场为中心点
public class EsInit_test {
private Random random = new Random();
private String[] firstNames = {
"赵", "钱", "孙", "李", "周", "吴", "郑", "王", "冯", "陈",
"楮", "卫", "蒋", "沈", "韩", "杨", "朱", "秦", "尤", "许",
"何", "吕", "施", "张", "孔", "曹", "严", "华", "金", "魏",
"陶", "姜", "戚", "谢", "邹", "喻", "柏", "水", "窦", "章",
"云", "苏", "潘", "葛", "奚", "范", "彭", "郎", "鲁", "韦",
"昌", "马", "苗", "凤", "花", "方", "俞", "任", "袁", "柳",
"酆", "鲍", "史", "唐", "费", "廉", "岑", "薛", "雷", "贺",
"倪", "汤", "滕", "殷", "罗", "毕", "郝", "邬", "安", "常",
"乐", "于", "时", "傅", "皮", "卞", "齐", "康", "伍", "余",
"元", "卜", "顾", "孟", "平", "黄", "和", "穆", "萧", "尹",
"姚", "邵", "湛", "汪", "祁", "毛", "禹", "狄", "米", "贝",
"明", "臧", "计", "伏", "成", "戴", "谈", "宋", "茅", "庞",
"熊", "纪", "舒", "屈", "项", "祝", "董", "梁", "杜", "阮",
"蓝", "闽", "席", "季", "麻", "强", "贾", "路", "娄", "危",
"江", "童", "颜", "郭", "梅", "盛", "林", "刁", "锺", "徐",
"丘", "骆", "高", "夏", "蔡", "田", "樊", "胡", "凌", "霍",
"虞", "万", "支", "柯", "昝", "管", "卢", "莫", "经", "房",
"裘", "缪", "干", "解", "应", "宗", "丁", "宣", "贲", "邓",
"郁", "单", "杭", "洪", "包", "诸", "左", "石", "崔", "吉",
"钮", "龚", "程", "嵇", "邢", "滑", "裴", "陆", "荣", "翁",
"荀", "羊", "於", "惠", "甄", "麹", "家", "封", "芮", "羿",
"储", "靳", "汲", "邴", "糜", "松", "井", "段", "富", "巫",
"乌", "焦", "巴", "弓", "牧", "隗", "山", "谷", "车", "侯",
"宓", "蓬", "全", "郗", "班", "仰", "秋", "仲", "伊", "宫",
"宁", "仇", "栾", "暴", "甘", "斜", "厉", "戎", "祖", "武",
"符", "刘", "景", "詹", "束", "龙", "叶", "幸", "司", "韶",
"郜", "黎", "蓟", "薄", "印", "宿", "白", "怀", "蒲", "邰",
"从", "鄂", "索", "咸", "籍", "赖", "卓", "蔺", "屠", "蒙",
"池", "乔", "阴", "郁", "胥", "能", "苍", "双", "闻", "莘",
"党", "翟", "谭", "贡", "劳", "逄", "姬", "申", "扶", "堵",
"冉", "宰", "郦", "雍", "郤", "璩", "桑", "桂", "濮", "牛",
"寿", "通", "边", "扈", "燕", "冀", "郏", "浦", "尚", "农",
"温", "别", "庄", "晏", "柴", "瞿", "阎", "充", "慕", "连",
"茹", "习", "宦", "艾", "鱼", "容", "向", "古", "易", "慎",
"戈", "廖", "庾", "终", "暨", "居", "衡", "步", "都", "耿",
"满", "弘", "匡", "国", "文", "寇", "广", "禄", "阙", "东",
"欧", "殳", "沃", "利", "蔚", "越", "夔", "隆", "师", "巩",
"厍", "聂", "晁", "勾", "敖", "融", "冷", "訾", "辛", "阚",
"那", "简", "饶", "空", "曾", "毋", "沙", "乜", "养", "鞠",
"须", "丰", "巢", "关", "蒯", "相", "查", "后", "荆", "红",
"游", "竺", "权", "逑", "盖", "益", "桓", "公", "仉", "督",
"晋", "楚", "阎", "法", "汝", "鄢", "涂", "钦", "归", "海",
"岳", "帅", "缑", "亢", "况", "后", "有", "琴", "商", "牟",
"佘", "佴", "伯", "赏", "墨", "哈", "谯", "笪", "年", "爱",
"阳", "佟",
"万俟", "司马", "上官", "欧阳", "夏侯",
"诸葛", "闻人", "东方", "赫连", "皇甫",
"尉迟", "公羊", "澹台", "公冶", "宗政",
"濮阳", "淳于", "单于", "太叔", "申屠",
"公孙", "仲孙", "轩辕", "令狐", "锺离",
"宇文", "长孙", "慕容", "鲜于", "闾丘",
"司徒", "司空", "丌官", "司寇", "南宫",
"子车", "颛孙", "端木", "巫马", "公西",
"漆雕", "乐正", "壤驷", "公良", "拓拔",
"夹谷", "宰父", "谷梁", "段干", "百里",
"东郭", "南门", "呼延", "羊舌", "微生",
"梁丘", "左丘", "东门", "西门"
};
private static final String indexName = "weixin";
private static final String typeName = "user";
private TransportClient client;
@Before
public void setUp() throws UnknownHostException {
if (client == null) {
// 连接集群的设置
Settings settings = Settings.builder()
.put("client.transport.ignore_cluster_name", true)
.build();
client = new PreBuiltTransportClient(settings)
.addTransportAddress(new TransportAddress(InetAddress.getByName("127.0.0.1"), 9300));
}
}
@Test
public void createIndex() throws IOException {
client.admin().indices().prepareCreate(indexName)
.setSettings(Settings.builder().put("index.number_of_shards", 2).put("index.number_of_replicas", 1))
.addMapping(typeName, XContentFactory.jsonBuilder().startObject()
.startObject("properties")
.startObject("uid").field("type", "keyword").endObject()
.startObject("nickName").field("type", "text").endObject()
.startObject("sex").field("type", "keyword").endObject()
.startObject("age").field("type", "integer").endObject()
.startObject("location").field("type", "geo_point").endObject()
.endObject()
.endObject()).get();
System.out.println("创建完成!");
}
//上海人民广场
private double lat = 31.228725;
private double lon = 121.475186;
private int nearDistance = 50;
@Test
public void initData() throws ExecutionException, InterruptedException {
WxUser user = new WxUser();
for (int i = 0; i < 200; i++) {
user.setLocation(randomPoint(lat, lon));
String id = String.format("wx_%s", UUID.randomUUID().toString().substring(24));
user.setUid(id);
String nickName = String.format("%s女士", firstNames[random.nextInt(firstNames.length)]);
user.setNickName(nickName);
user.setAge(random.nextInt(35));
String json = JSON.toJSONString(user);
IndexResponse response = client.prepareIndex(indexName, typeName).setSource(json, XContentType.JSON).get();
System.out.println(response.getId());
}
}
/**
* @param lat 纬度
* @param lon 经度
* @return
*/
private double[] randomPoint(double lat, double lon) {
double min = 0.000001;//最小1米
double max = 0.00002;//最大1000米
double randomNum = random.nextDouble() % (max - min + 1) + max;
DecimalFormat numFormat = new DecimalFormat("########.000000");
String slat = numFormat.format(randomNum + lat);
String sLon = numFormat.format(randomNum + lon);
double dLat = Double.valueOf(slat);
double dLon = Double.valueOf(sLon);
return new double[]{dLon, dLat};//TODO:es存储是经度在前,维度在后
}
}
第三步,使用es的api执行搜索
//搜索附近
@Test
public void searchNear() throws ExecutionException, InterruptedException {
SearchRequest searchRequest = new SearchRequest(indexName);
searchRequest.types(typeName);
SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();
sourceBuilder.from(0);
sourceBuilder.size(15);
sourceBuilder.timeout(new TimeValue(60, TimeUnit.SECONDS));
QueryBuilder geoQuery = new GeoDistanceQueryBuilder("location")
.point(lat, lon)
.distance(nearDistance, DistanceUnit.KILOMETERS) ////指定位置为中心的圆的半径,100km
.geoDistance(GeoDistance.PLANE); //按平面计算距离,平面(更快,但在长距离和靠近极点的地方是不准确的)而立方(default)
sourceBuilder.query(geoQuery);
GeoDistanceSortBuilder geoSort = SortBuilders.geoDistanceSort("location", lat, lon)
.order(SortOrder.ASC) //最近的排在最前面
.unit(DistanceUnit.KILOMETERS);
sourceBuilder.sort(geoSort);
searchRequest.source(sourceBuilder);
SearchResponse searchResponse = client.search(searchRequest).get();
SearchHits hits = searchResponse.getHits();
SearchHit[] searchHits = hits.getHits();
System.out.println("小明,您当前位置为:[" + lon + "," + lat + "],开始搜索附近 " + nearDistance + "KM 以内的朋友...");
System.out.println("检索完成!总耗时:" + searchResponse.getTook().getMillis() + "毫秒,符合条件的有 " + searchHits.length + " 个!");
for (SearchHit hit : searchHits) {
String sourceAsString = hit.getSourceAsString();
BigDecimal geoDistance = new BigDecimal((double) hit.getSortValues()[0])
.setScale(0, BigDecimal.ROUND_HALF_DOWN);//四舍五入
Map sourceAsMap = hit.getSourceAsMap();
System.out.println(sourceAsMap.get("nickName") + " 距您 " + geoDistance + "KM,source:" + sourceAsString);
}
}
执行结果:
小明,您当前位置为:[121.475186,31.228725],开始搜索附近 50KM 以内的朋友...
检索完成!总耗时:4毫秒,符合条件的有 10 个!
邹女士 距您 5KM,source:{"age":2,"location":[121.505997,31.259536],"nickName":"邹女士","sex":"女","uid":"wx_0863b4f97cc1"}
巫马女士 距您 16KM,source:{"age":29,"location":[121.582163,31.335702],"nickName":"巫马女士","sex":"女","uid":"wx_de170132e6c2"}
那女士 距您 16KM,source:{"age":5,"location":[121.582977,31.336516],"nickName":"那女士","sex":"女","uid":"wx_571b83ca7ea1"}
汪女士 距您 17KM,source:{"age":26,"location":[121.58967,31.343209],"nickName":"汪女士","sex":"女","uid":"wx_b3a5e216eb7b"}
祖女士 距您 19KM,source:{"age":33,"location":[121.606634,31.360173],"nickName":"祖女士","sex":"女","uid":"wx_5cdfd226cf79"}
倪女士 距您 23KM,source:{"age":26,"location":[121.631181,31.38472],"nickName":"倪女士","sex":"女","uid":"wx_ed0eeafd07cf"}
柏女士 距您 34KM,source:{"age":30,"location":[121.705156,31.458695],"nickName":"柏女士","sex":"女","uid":"wx_13873bdd086d"}
余女士 距您 36KM,source:{"age":0,"location":[121.723644,31.477183],"nickName":"余女士","sex":"女","uid":"wx_d3996a3c78de"}
墨女士 距您 40KM,source:{"age":3,"location":[121.745568,31.499107],"nickName":"墨女士","sex":"女","uid":"wx_d15e247ad23c"}
桂女士 距您 44KM,source:{"age":31,"location":[121.774825,31.528364],"nickName":"桂女士","sex":"女","uid":"wx_828e1ef300e5"}
到这里就结束了,示例不是目的,它能否带来抛砖引玉的作用取决于您。