mangoBD地理位置索引JAVA实战

在现在的移动互联网应用中,LBS功能几乎是每个APP的标配。LBS功能的实现方式也有很多种,Mysql有相应的计算函数,但是Mysql实现此功能需要经过较多的计算,如果数量很大,对于查询性能是个极大的考验。不过对于一开始就使用Mysql的项目来说,需要增加LBS功能就能平和地过渡。
mongoDB有个重要的特性就是支持二维空间索引,利用mongoDB我们极其容易实现LBS功能。例如,我们有个需求,需要查找在我当前位置附近的学校,学校按距离从近到远的顺序排序,并能准确地获取当前位置到学校的距离。这样的需求,使用mongoDB的空间索引结合特殊的查询方法很容易实现。

MongoDB二维空间索引的数据存储及JAVA实现

建立MongDB二位空间索引就要在文档中有相应的存储坐标信息的字段。建立空间索引的key可以使用array或内嵌文档存储,但是前两个elements必须存储固定的一对空间位置数值。如
{ loc : [ 50 , 30 ] }
{ loc : { x : 50 , y : 30 } }
{ loc : { foo : 50 , y : 30 } }
{ loc : { lat : 40.739037, long: 73.992964 } }
我们采用第一种数组的方式。
先来看看我们插入文档数据的java代码:

/**
     * 根据索引名称获取索引
     * @param dbName 数据库名
     * @param collectionName 集合名
     * @param indexName 索引名字
     * @return
     */
    @Override
    public DBObject getIndexByName(String dbName, String collectionName,String indexName) {
        List<DBObject> indexList=this.getIndexInfos(dbName, collectionName);
        if(null!=indexList){
            for(DBObject o:indexList){
                String name=(String) o.get("name");
                if(StringUtils.isNotBlank(name)&&name.equals(indexName)){
                    return o;
                }
            }
        }
        return null;
    }

/**
     * 向指定的数据库中添加给定的keys和相应的values,并插入文档的地理位置信息
     * @param dbName 数据库名
     * @param collectionName 集合名
     * @param keys 集合中域的keys
     * @param values 集合中域中的值
     * @param index_name_2d 2d索引的名称
     * @param lon 经度
     * @param lat 纬度
     * @return
     */
    @Override
    public boolean insert(String dbName, String collectionName, String[] keys, Object[] values,String index_name_2d, double lon, double lat) {
        DB db=null;
        DBCollection dbCollection=null;
        WriteResult result=null;
        String resultString=null;
        if(null!=keys && null!=values){
            if(keys.length!=values.length){
                return false;
            }
            db=this.mongoClient.getDB(dbName);
            dbCollection=db.getCollection(collectionName);
            BasicDBObject insertObject=new BasicDBObject();
            for(int i=0;i<keys.length;i++){//构建添加条件 
                insertObject.put(keys[i], values[i]);
            }
             //设置地址位置信息索引字段 
            if(StringUtils.isNotBlank(index_name_2d)){
                BasicDBObject index_2d = new BasicDBObject();
                index_2d.put(index_name_2d, "2d");
                index_2d.put("background", true);
                //没有2d索引 则创建
                if(null==this.getIndexByName(dbName, collectionName, index_name_2d)){
                    dbCollection.ensureIndex(index_2d, index_name_2d, false);
                }
            }
             //地理位置信息
             insertObject.put( index_name_2d, new Double[]{lon,lat} );
            try {
                result=dbCollection.insert(insertObject);
                resultString=result.getError();
            } catch (Exception e) {
                e.printStackTrace();
            }finally {
                if(null!=db){
                    db.requestDone();//请求结束后关闭db
                }
            }
            return null==resultString?false:true;


        }
        return false;
    }

简单说明一下思路,上述代码主要的思路是在MongoDB中新建文档数据,而且文档中一个名为loc的字段专门存储位置信息即地理位置坐标;然后给集合中的loc字段建立2d地理空间索引。
因为后续我们需要使用mongoDB的GeoNear命令来做地理空间查询的功能,所以一个文档中只能有一个位置信息存储的字段,多于一个则会报错。
接下来,我们写测试类,并在mongoDB中添加一点数据:

@Test
    public void testInsert(){
        MongoDBDao dao= MongoDBDaoImpl.getInstance();
        System.out.println("-----------测试mongoDB的crdu操作------------");
        //新增
        System.out.println("-----------插入-----------");
        String[] keys=new String[]{"name","address","city","students"};
        Object[] values=new Object[]{"中山大学","广州地海珠区新港西路135号","广州",new Integer(101)};
        dao.insert("test", "school",keys, values,"loc",113.305314,23.102723);

        values=new Object[]{"华南农业大学","广州天河区五山街五山路483号华南农业大学三角市","广州",new Integer(32342433)};
        dao.insert("test", "school",keys, values,"loc",113.359105,23.161023);

        values=new Object[]{"四川大学","四川成都市人民南路三段17号","成都",new Integer(643432)};
        dao.insert("test", "school",keys, values,"loc",104.072946,30.647093);

        values=new Object[]{"重庆大学","重庆市沙坪坝区沙正街174号","重庆",new Integer(5673834)};
        dao.insert("test", "school",keys, values,"loc",106.474815,29.570351);

        values=new Object[]{"清华大学","北京市海淀区清华大学","北京",new Integer(938383)};
        dao.insert("test", "school",keys, values,"loc",116.332557,40.009417);

        System.out.println("#####插入数据后,集合中得数据:");
        ArrayList<DBObject> list=dao.find("test", "school", null, null, -1);
        for(DBObject o:list){
            System.out.println(o);
        }
    }
    @Test
    public void testGetIndexes(){
        MongoDBDao dao= MongoDBDaoImpl.getInstance();
        System.out.println("-----------测试mongoDB的crdu操作------------");
        List<DBObject> list=dao.getIndexInfos("test", "mapinfo");
        System.out.println(dao.getIndexInfos("test", "mapinfo"));
        for(DBObject o:list){
            String indexName=(String) o.get("name");
            System.out.println("indexName:"+indexName);
        }
        System.out.println("loc_2d index:"+dao.getIndexByName("test", "mapinfo", "loc_2d"));
    }

上面的测试代码显示,我们在mongDB中存储一些学校的信息,并记录了每个学校的坐标信息。
Junit执行之后的控制台的输出如下:

-----------测试mongoDB的crdu操作------------
-----------插入-----------
#####插入数据后,集合中得数据:
{ "_id" : { "$oid" : "570f54d609a80a3f4c8718b3"} , "name" : "中山大学" , "address" : "广州地海珠区新港西路135号" , "city" : "广州" , "students" : 101 , "loc" : [ 113.305314 , 23.102723]}
{ "_id" : { "$oid" : "570f54d609a80a3f4c8718b4"} , "name" : "华南农业大学" , "address" : "广州天河区五山街五山路483号华南农业大学三角市" , "city" : "广州" , "students" : 32342433 , "loc" : [ 113.359105 , 23.161023]}
{ "_id" : { "$oid" : "570f54d609a80a3f4c8718b5"} , "name" : "四川大学" , "address" : "四川成都市人民南路三段17号" , "city" : "成都" , "students" : 643432 , "loc" : [ 104.072946 , 30.647093]}
{ "_id" : { "$oid" : "570f54d609a80a3f4c8718b6"} , "name" : "重庆大学" , "address" : "重庆市沙坪坝区沙正街174号" , "city" : "重庆" , "students" : 5673834 , "loc" : [ 106.474815 , 29.570351]}
{ "_id" : { "$oid" : "570f54d609a80a3f4c8718b7"} , "name" : "清华大学" , "address" : "北京市海淀区清华大学" , "city" : "北京" , "students" : 938383 , "loc" : [ 116.332557 , 40.009417]}

可以看到,每个文档数据中有这样的位置信息字段”loc” : [ 116.332557 , 40.009417]

使用MongoDB的GeoNear命令来实现地理位置搜索功能

GeoNear命令,是基于db的command,而不是基于collection的find,也就是需要通过runcommand执行,具体语法如下

db.runCommand({ geoNear : “school” , near : [113.366498,23.127249], num : 10 } )

这个结果是根据距离排序而且有距离的记录,但是距离是经纬度的差值,MongoDB 1.8以后提供了Spherical Model,用distanceMultiplier指定地球半径来得到实际的公里或者米的距离,记得加上spherical:true,命令变为:

db.runCommand({ geoNear : “school” , near : [113.366498,23.127249], distanceMultiplier: 6378137, num : 10, spherical:true } )

那么查出的结果中距离的单位就是米!
这个num参数是取得记录的条数,适合做列表翻页用,但是地图上我们很难说只取多少个点,而是取多大范围,那么maxDistance参数正好适合,也就是多少范围的;我上面采用的地球半径是米(地球的半径是6378137米),那么这里查询我统一采用米来计算,命令变为:

db.runCommand({ geoNear : “school” , near : [113.366498,23.127249], distanceMultiplier: 6378137, maxDistance:2500/6378137 ,spherical:true} )

也就是查询2500米范围内的点;
其他参数还有个Query,用于联合查询,结果完整的命令如下:

db.runCommand({ geoNear : “school” , near : [113.366498,23.127249], distanceMultiplier: 6378137, maxDistance:2500/6378137,num : 10, spherical:true , query:{city:”广州”}} )

整条命令的含义:我要搜索在坐标(113.366498,23.127249)附近2500米范围内最近的学校,最多只能查出10所学校,而且学校所在的城市是在广州。
实现geoNear 搜索的java代码如下:

/**
     * 使用geoNear查询附近地理空间的数据
     * @param dbName 数据库名
     * @param collectionName 集合名
     * @param locationField 位置信息域的名字
     * @param centerLon 中心点的经度
     * @param centerLat 中心
     * @param keys 其他查询条件的keys
     * @param values 其他查询条件的values
     * @param limit 查询限制条数大小
     * @param maxDistance 最大距离
     * @return
     */
    @Override
    public CommandResult geoNear(String dbName, String collectionName, String locationField, double centerLon,
            double centerLat, String[] keys, Object[] values, int limit, Long maxDistance) {
        DB db=null;
        DBCursor dbCursor=null;
        try {
            db=this.mongoClient.getDB(dbName);
            //构建查询条件
            BasicDBObject queryObj=new BasicDBObject();
            if(null!=keys && null!=values && keys.length==values.length){
                for(int i=0;i<keys.length;i++){
                    queryObj.put(keys[i], values[i]);
                }
            }
            BasicDBObject myCmd = new BasicDBObject(); 
            myCmd.append("geoNear", collectionName);//集合名
            double[] loc = {centerLon,centerLat}; 
            myCmd.append("near", loc); 
            /**
             * geoNear默认结果是根据距离排序有距离的记录,但是距离是经纬度的差值,
             * MongoDB 1.8以后提供了Spherical Model,用distanceMultiplier指定地球半径来得到实际的公里或者米的距离,
             * 记得加上spherical:true
             */
            myCmd.append("spherical", true); 
            myCmd.append("distanceMultiplier", 6378137); //地球的半径,单位米
            myCmd.append("maxDistance", (double)maxDistance / 6378137 ); //指定maxDistance米范围内
            myCmd.append("query", queryObj);//非地理位置域的查询条件
            myCmd.append("limit", limit);
            CommandResult myResults = db.command(myCmd); 
            return myResults;
        } catch (Exception e) {
            e.printStackTrace();
        }finally {
            if(null!=dbCursor){
                dbCursor.close();
            }
            if(null!=db){
                db.requestDone();
            }
        }

        return null;
    }

接下来,就是我们的测试代码了

@Test
    public void testGeoNear(){
        MongoDBDao dao= MongoDBDaoImpl.getInstance();
        System.out.println("-----------测试mongoDB的crdu操作------------");
        String[] keys={"city"};
        String[] values={"广州"};
        CommandResult c=dao.geoNear("test", "school", "loc", 113.366498,23.127249, keys, values, 10, 1000000l);
        System.out.println("mongodb geoNear命令执行的结果:"+c);
        if(c.ok()){//查询成功
            //获取数据
            Collection<DBObject> resultList=(Collection<DBObject>) c.get("results");
            for(DBObject o:resultList){
                System.out.println("------------------------------------------");
                DBObject school=(DBObject) o.get("obj");
                System.out.println("学校名称:"+school.get("name"));
                System.out.println("城市:"+school.get("city"));
                System.out.println("地址:"+school.get("address"));
                System.out.println("学校人数:"+school.get("students"));
                System.out.println("距离:"+o.get("dis")+"米");
            }
        }

    }

执行测试测结果如下:

-----------测试mongoDB的crdu操作------------
mongodb geoNear命令执行的结果:{ "serverUsed" : "192.168.244.100:27017" , "waitedMS" : 0 , "results" : [ { "dis" : 3835.107409650452 , "obj" : { "_id" : { "$oid" : "570f54d609a80a3f4c8718b4"} , "name" : "华南农业大学" , "address" : "广州天河区五山街五山路483号华南农业大学三角市" , "city" : "广州" , "students" : 32342433 , "loc" : [ 113.359105 , 23.161023]}} , { "dis" : 6833.3044097593565 , "obj" : { "_id" : { "$oid" : "570f54d609a80a3f4c8718b3"} , "name" : "中山大学" , "address" : "广州地海珠区新港西路135号" , "city" : "广州" , "students" : 101 , "loc" : [ 113.305314 , 23.102723]}}] , "stats" : { "nscanned" : 20 , "objectsLoaded" : 8 , "avgDistance" : 5334.205909704904 , "maxDistance" : 6833.3044097593565 , "time" : 1} , "ok" : 1.0}
------------------------------------------
学校名称:华南农业大学
城市:广州
地址:广州天河区五山街五山路483号华南农业大学三角市
学校人数:32342433
距离:3835.107409650452------------------------------------------
学校名称:中山大学
城市:广州
地址:广州地海珠区新港西路135号
学校人数:101
距离:6833.3044097593565

测试结果显示,学校按与当前位置的距离由近及远的排序,而且能计算出到学校的距离,完美地实现了我们的需求。
由此可见,使用mongoDB的二位空间索引的功能,是很容易实现最常规又最实用的LBS需求的,搜索效率也是非常高。

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 7
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 7
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值