您的位置:首页 > 编程语言 > Java开发

mangoBD地理位置索引JAVA实战

2016-04-14 15:47 309 查看
在现在的移动互联网应用中,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需求的,搜索效率也是非常高。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: