mangoBD地理位置索引JAVA实战

来源:互联网 发布:unity3d 角色模型下载 编辑:程序博客网 时间:2024/05/17 03:35

在现在的移动互联网应用中,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 0
原创粉丝点击