到这里Elasticsearch的整个系列分享就基本上结束了,当然后续还是会针对某一点进行讲解。为何要在实践篇中讲解"查找附近的人"呢?说实话,想了很久,最终才确定下来,总体希望这个实践对今后的工作过程中有帮助。
1. 需求描述
随着移动设备的普及,很多移动APP都提供了LBS(Location Based Service)。其实LBS并不是什么新东西,但它也带来了不一样的改变,一方面可以提高以前做不到的事情,一方面提升用户体验。下面我们将基于位置实现这一个类似的功能:比如微信中附近的人。
2. 实现思路
要实现这样一个需求,可用的技术非常多。我是这么考虑的:
1. 评估实现它的复杂度(技术难度,也可以认为是风险点)
2. 大数据量下的实际情况,技术的成熟度(业界都是怎么玩的)
当然,基于es实现它,es有一个对应的数据类型Geo-point。
2.1 geohash
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。同理,将编码转换成经纬度的解码算法与之相反。
3. 代码实现
//建立模型 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<String, Object> 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"}
到这里就结束了,示例不是目的,它能否带来抛砖引玉的作用取决于您。
————————————————
原文链接:https://blog.csdn.net/alex_xfboy/article/details/86099746