到这里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