MongoDb 物理位置应用实现

1代码实现

官方驱动2.7版本

1.1范围查找

        /// <summary>
        /// 半径范围查找位置信息
        /// </summary>
        /// <param name="x">经度</param>
        /// <param name="y">纬度</param>
        /// <param name="radius">半径</param>
        /// <returns></returns>
        public async Task<List<T>> GeoWithinCenterSphere(double x,double y,double radius)
        {
            var filter = Builders<T>.Filter.GeoWithinCenterSphere("Location", x, y, radius / 6378.1);
            return await collection.Find(filter).ToListAsync();
        }    
6378.1貌似是公里

1.2范围查找并返回距离 官方文档
/*附加距离 result里的dis为距离*/
db.runCommand(
   {
     geoNear: "Event", /*Collecti名*/
     near: { type: "Point", coordinates: [ 114.24791, 30.598503 ] },
     spherical: true,
     maxDistance:5000 /* 单位为米 */
   }
)

  

2数据库准备

2.1创建索引 详情官方文档

db.Event.ensureIndex({'Location':'2dsphere'})

  2.2数据查找 官方文档

db.Event.find( { Location: { $geoWithin: { $centerSphere: [ [ 114.24791, 30.598503 ] ,10/6378.1  ] } } } )

10/6378.1=10公里 有个  

2.3附带距离的查找

/*附加距离 result里的dis为距离*/
db.runCommand(
   {
     geoNear: "Event", /*Collecti名*/
     near: { type: "Point", coordinates: [ 114.24791, 30.598503 ] },
     spherical: true,
     maxDistance:5000 /* 单位为米 */
   }
)

 

3Mongodb索引介绍

转自 http://shift-alt-ctrl.iteye.com/blog/2250101

地理空间索引(Geospatial):为了支持高效的地址位置数据查询,mongodb提供了2种特殊的索引,2d索引(坐标平面)和2sphere索引(经纬球面),存储数据包括经纬度信息(-180 ~ 180)以及数据表示的几何类型;在目前LBS应用中,通过使用Geo索引,可以实现类似于“附近的人、消息”等功能,我们可以利用现有的地图服务获取位置的经纬度信息,同时配合mongodb Geo索引来完成我们的部分业务应用。不过,mongodb并不是专业的LBS数据库。“2d”和2dsphere两种索引要求的document数据结构是不同的,所以开发者需要注意,我们稍后介绍。

    2dsphere:经纬球面,如果数据是球面的位置(球面几何),可以使用此索引,2dsphere索引可以与其他多个字段组合。location的数据结构可以为GeoJSON对象或者为传统的坐标对。【参见GeoJSON】mongodb支持如下几种GeoJSON对象:

    1)Point:点

    2)LineString:线段

    3)Polygon:多边形

    4)MultiLineString:多条线段

    5)MultiPoint:多点

    6)MultiPolygon:多个多变形

    7)GeometryCollection

 

    2d:平面坐标,在平面几何(欧几里得几何)中计算距离,我们可以存储一些传统的坐标对,这时就可以使用2d索引,2d索引最多只能与一个其他的字段组合,且2d字段必须做前缀,这与2dsphere是有区别的。不过2d索引是mongodb的旧版特性,在2.6之后,我们可以统一使用2dsphere即可,2d索引可以通过转换成GeoJSON中Point类型。

    我们使用Geo索引对坐标数据进行如下类型的查询:

    1)包含:可以查询包含(inclusion)在指定的多边形区域内的locations(所对应的documents,或者indexes),使用$geoWithin操作符。2d和2dsphere都支持包含查询。

    2)相交(intersection):可以查询与指定的几何体相交的locations,仅支持球面数据,即2dsphere索引,使用$geoIntersects操作。

    3)临近(proximity):可以查询与指定的point最近的其他points,使用$near操作,2d和2dsphere都支持。

    

    Geo两种索引都不能作为shard key(sharding cluster模式中),当然可以存储在sharded collection中,只是不能作为shard key。不过在shared collection中,$near和$nearSphere将不支持,而是使用$geoNear替代。(稍后专门介绍这些指令)

 

    6、文本索引(Text):支持对collection中的string内容进行搜索操作,即text search;mongodb内置支持几种语言的分词,不过这种文本搜索的能力与专业的搜索引擎相比,还相当简陋;此外mongodb的文本索引,不支持中文,关于此索引,我们将不再赘言。

 

    7、哈希索引(Hash):主要使用场景为“hash sharding”,即在“Sharding”架构模式中,对sharding key使用hash索引,以便数据的sharding和路由操作。Hash索引只支持值的“相等比较”,不支持range查询。当然如果你确定application的所有query均为某个field的“相等”匹配,也可以为此filed建立hash索引。hash索引只支持一个字段,它不能和其他字段组合,同时也不能是“unique”,使用db.<collection>.createIndex({a : "hashed"})来创建hash索引。

 

二、索引属性(property)

    1、唯一索引(Unique):即索引字段的值是唯一的,全局无重复;如果尝试存入重复的值(write),操作将会被拒绝。可以通过db.<collection>.createIndex({ "userid" : 1}, {unique : true})创建唯一索引。我们可以对一个组合索引使用unique属性,表示它们的组合值不能重复,默认情况下unique为false。如果插入的document不包含某个索引字段,那么此字段的值保存为null,mongodb也会用unique约束这样的文档,即不包含某个字段的文档只能保存一条。比如唯一索引{"x" : 1, "y" : 1},只能插入一条{"x" : "content"}(等同于{"x" : "content","y" : null})。对于hash索引而言,则不能使用unique属性。

    unique通常与sparse一起使用。

  

    2、稀疏索引(Sparse):只有当document中包含索引字段时,才会为此文档建立索引,但是这些文档仍然能够保存成功·;因为mongodb的document结构是模式自由的,所以document中不包含某个Field也是“正常的”;稀疏索引将会忽略那些document中不包含索引字段的记录,因此稀疏索引并没有包含collection中的所有文档;非sparse索引,将会为那些缺失的字段存储为null,因此索引包含了全部文档。

    我们可以将“Sparse”和“Unique”选项同时约束一个index,以避免包含重复值的document,同时还忽略那些不包含索引字段的document(不建立索引,但是文档仍然会被保存)。

    对于2d和2dsphere索引,通常是sparse。

    对于sparse属性的组合索引,只有至少包含一个索引字段的document才会建立索引,如果所有的索引字段都缺失,将不会为此文档建立索引。

    当sparse与unique同时约束索引时,比如createIndex({"score" : 1}, {sparse : true, unique: true}),如果文档中不包含score字段,那么文档都可以保存成功,但是不会对这些文档建立索引,但是对于包含了score字段的文档,那么score值则不能重复。如果是组合索引,比如createIndex({useid : 1, score : 1},{sparse : true, unique: true}),对于缺失score的文档,只要useid不同均可以建立索引且保存成功,如果useid和score都缺失,文档可以保存但是不会建立索引。

    我们已经了解到,sparse索引中可能没有包含所有的文档,因为部分文档可能因缺失索引字段而未能建立索引,但是它们缺失保存在了collection中,那么对于某些需要从全部documents筛选数据的操作将无法使用索引,比如查询语句{x : {$exists : false}}(查询不包含x字段的所有文档),除非你使用hint()方法强制使用此索引。

 

    3、TTL索引:对“date”、“timestamp”类型的字段建立TTL索引,并在索引选项中指定索引保存的时长,那么mongodb将会在一定时间之后自动移除那些“过期”的索引和documents。通过TTL索引,可以实现一个清洗数据的“定时任务”。TTL索引只能支持一个字段(同hash索引,不能对组合索引使用此特性),不能和其他字段组合,即一个索引中只能有一个字段;比如document中created字段类型为date,表示文档插入的时间,我们希望文档在3600秒后过期删除,那么可以通过db.<collection>.createIndex({ "created" : 1},{expireAfterSeconds : 3600})创建索引,其中“expireAfterSeconds”为TTL索引的必须选项。如果document中不包含索引字段,文档将不会过期。

    mongodb将会创建一个后台线程,读取index值并删除那些过期的document;在createIndex时,mongodb也会在构建索引的同时检测过期,删除那些过期的数据。TTL索引和众多的缓存服务一样,不能保证数据过期则立即被删除(对用户操作不可见),mongodb扫描数据(从头到尾)是有时间间隔的(60秒),因此过期数据的删除可能存在延迟问题。对于“replica Set”,这个后台线程只在primary上运行,primary上的删除操作同步给secondaries。mongodb的存储引擎的特性决定,这种大量的随机删除会导致严重的存储碎片,索引如果collection中存在TTL索引时,将会开启“usePowerOf2Sizes”选项而且不能修改,“usePowerOf2Sizes”可以降低存储碎片的可能。

Java代码  收藏代码
  1. collection.createIndex(new Document("created",1),new IndexOptions().expireAfter(15L,TimeUnit.MILLISECONDS));  
  2. ##java代码  

 

    对于java构建索引,代码样例如上,索引的属性可以通过IndexOptions来指定。

 

三、索引的设计目的就是提高query性能,稍后我们会介绍如何建立合适的索引,以及索引性能的分析。如果你了解过其他数据库的索引机制,可能还会提到“覆盖索引”(Covered query);如果查询条件和“projection”选项中只包含index字段,那么mongodb的查询结果只需要从索引条目中得到即可,无需scan任何document(磁盘操作、以及载入内存),这就是“Covered query”,性能很高。比如对字段score建立了索引,那么查询:collection.find({score:{"$lt" : 30}}, {score : 1,_id : -1})。

 

    索引交集(intersection):这种特性很多database也支持,即一个query使用多个indexes(通常为2个),取其交集作为result。比如一个query有多个查询字段作为过滤条件,一个index能够完成查询的一部分(过滤结果集),另一个index则完成另一部分,然后mongodb将这两个index使用交集来完成整个query;不过索引交集,是否能够提高查询性能,依赖于特殊的查询和系统本身,所以索引交集并非总是能提高性能,相反也会导致性能低下。

 

    对于replica set环境中,当primary上构建索引完成之后,将会在secondaries上构建,不过在secondaries上将采用background模式,因为background构建较大数据集合的索引非常耗时,我们可以首先将secondary离群,然后手动使用foreground方式构建,完成之后再次加入replica set集合,不过这里有个问题需要注意,primary将会把用户的所有write操作写入oplog中,这是一个capped collection,需要确保oplog collection的容量足够大,以避免secondary离群时间过长而导致无法增量的catch up,此时将会导致一个全量的initial sync(将会更加耗时)。

    对于sharded cluster而言,createIndex操作将会被发送到所有的shard-primary上。

 

四、Intersection

    上文已经简单提到了这个概念,对于很多数据库而言,也包含此特性,比如mysql。假如某个collection中有两个索引{qty : 1}和{item : 1},对于collection.find({item : "1234", qty : {$gt : 15}})这个查询语句将有可能使用上述两个索引的intersection来完成查询,不过具体是否真的会使用intersection(以及具体使用了那个索引)则需要explain()方法来检测,因为mongodb并不会总是使用intersection,因为它对性能的提高是不可预测的,有可能是降低性能的。

    在intersection中是否选择某个index,仍然遵循“最左前缀”原则。

 

五、Geo索引

    Geo索引可能会在基于LBS方面的应用上有所帮助,接下来我们专门介绍一下。

    1、2dsphere

    2dsphere数据格式可以为GeoJSON或者普通的坐标对,同时2dsphere是2d的兼容升级版,在新版本中我们直接使用2dsphere即可。2dsphere索引默认为sparse属性,索引可以与其他类型的字段形成组合索引,即一个组合索引中只能有一个字段为2dsphere。2dsphere(包括2d)字段不能作为sharding key,但是可以保存在sharding collection中。比如文档:

Java代码  收藏代码
  1. {  
  2.   loc : { type: "Point", coordinates: [ -73.97, 40.77 ] },  
  3.   name: "Central Park",  
  4.   category : "Parks"  
  5. }  

    loc字段存储了一个GeoJSON格式的,有type和coordinates两个字段,分别表示“几何”的类型和坐标位置。通过createIndex({loc : "2dsphere"})创建索引,前文已经提到2dsphere可以与其他类型的字段一起构建成组合索引,2dsphere字段可以位于组合索引的任何位置(不像2d的组合索引,其中2d字段必须为索引的首个字段),比如createIndex({loc : "2dphere", category : -1});需要注意的是2dsphere索引字段存储的必须是GeoJSON对象或者普通的坐标对,对于普通的坐标对,数据结构为:

Java代码  收藏代码
  1. {  
  2.     loc : [-20.33, 30,12],  
  3.     category : "AAA",  
  4.     name : "BBB"  
  5. }  

    2、2d

    2d索引数据能够存储普通的坐标对,这是mongodb较旧版本中支持的特性,它的使用场景类似与2dsphere中的Point数据类型;同时2d索引不支持GeoJSON数据对象,也不能使用2d索引查询collection中GeoJSON数据;此外2d索引和2dsphere一样不能作为sharding key,不过仍然可以在sharding collection中保存2d数据,这与2dsphere一样。不过2d字段参与组合索引时,只能作为第一个字段,这与2dsphere不同;比如document保存2d数据:

Java代码  收藏代码
  1. {  
  2.     loc : [-20.33, 30,12],  
  3.     category : "AAA",  
  4.     name : "BBB"  
  5. }  

    可以通过createIndex({loc : "2d"}),或者组合索引createIndex({loc : "2d", category : -1})。通过数据结构可以知道,2d数据只能保存Point(点)类型的数据,其中坐标对分别为经度、维度,经度为第一个值,经纬度的值范围为[ -180,180)。

 

    3、GeoJSON对象

    在2dsphere中,我们知道GeoJSON是它所支持的标准数据类型,用来存储多种基于球面的地理位置数据(spherical),GeoJSON数据格式为:

Java代码  收藏代码
  1. { type : <GeoJSON type>, coordinates : <coordinates>}  

    type和coordinates两个字段都必填,type表示location的几何类型,coordinates表示其经纬度(经度在先,维度在后),根据type的不同,coordinates的数据结构也有所不同。接下来我们依次介绍几种常用的GeoJSON对象。对于java Driver,需要在3.1.x以上版本才支持GeoJSON API。

    1)Point(点

    表示一个坐标点,数据结构样例如下:

Java代码  收藏代码
  1. { type : "Point", coordinates : [40,5]}  

    其中coordinates为一个数组,表示坐标对。我们可以使用java插入一条Point类型的数据:

Java代码  收藏代码
  1. collection.insertOne(new Document("location",new Point(new Position(20.0,30.1))));  

    2)LineString(线段)

    由2个Point组成,其中coordinates为Point数组

Java代码  收藏代码
  1. {   
  2.     "type" : "LineString",   
  3.     "coordinates" : [[20.0, 30.0], [35.0, 45.0]]   
  4.  }   

    插入一个LineString文档:

Java代码  收藏代码
  1. List<Position> points = new ArrayList<Position>();  
  2. points.add(new Position(20,30));  
  3. points.add(new Position(35,45));  
  4. collection.insertOne(new Document("location",new LineString(points)));  

    3)Polygon(多边形)

    有多个LineString构成的闭合多边形,数据结构示例:

Java代码  收藏代码
  1. {  
  2.   type: "Polygon",  
  3.   coordinates: [ [ [ 0 , 0 ] , [ 3 , 6 ] , [ 6 , 1 ] , [ 0 , 0  ] ] ]  
  4. }  

   最终将有上述几个点依次连接,构成一个闭合的多边形,不过需要注意coordinates是一个数组,且数组的每个元素也是一个数组。

Java代码  收藏代码
  1. List<Position> positions = new ArrayList<Position>();  
  2. positions.add(new Position(0,0));  
  3. positions.add(new Position(3,6));  
  4. positions.add(new Position(6,1));  
  5. positions.add(new Position(0,0));//至少三个边,且最终闭合  
  6. collection.insertOne(new Document("location",new Polygon(positions)));  

    在实际工作中,可能还存在多边形嵌套的情况,即图形有一个“外环”多边形和内环多边形组成,它们中间的区域为实际面积。这种嵌套关系,要求两个多边形的边不能有任何交集,且内环被外环完全包含

,数据结构如下:

Java代码  收藏代码
  1. {  
  2.   type : "Polygon",  
  3.   coordinates : [  
  4.      [ [ 0 , 0 ] , [ 3 , 6 ] , [ 6 , 1 ] , [ 0 , 0 ] ],  
  5.      [ [ 2 , 2 ] , [ 3 , 3 ] , [ 4 , 2 ] , [ 2 , 2 ] ]  
  6.   ]  
  7. }  

    我们使用java插入一个这种环形多边形:

Java代码  收藏代码
  1. List<Position> exterior = new ArrayList<Position>();  
  2. exterior.add(new Position(0,0));  
  3. exterior.add(new Position(3,6));  
  4. exterior.add(new Position(6,1));  
  5. exterior.add(new Position(0,0));//外环  
  6.   
  7. //[ 2 , 2 ] , [ 3 , 3 ] , [ 4 , 2 ] , [ 2 , 2 ]  
  8. List<Position> holes = new ArrayList<Position>();  
  9. exterior.add(new Position(2,2));  
  10. exterior.add(new Position(3,3));  
  11. exterior.add(new Position(4,2));  
  12. exterior.add(new Position(2,2));//中间的空洞  
  13. sphere.insertOne(new Document("location",new Polygon(new PolygonCoordinates(exterior,holes))));  

    4)GeometryCollection(平面集合)

    这是一个特殊的数据结构,它表示有多种类型的“图形”组成的集合,比如“Point”、“Polygon”等等。数据结构示例如下:

Java代码  收藏代码
  1. {  
  2.   type: "GeometryCollection",  
  3.   geometries: [  
  4.      {  
  5.        type: "MultiPoint",  
  6.        coordinates: [  
  7.           [ -73.9580, 40.8003 ],  
  8.           [ -73.9498, 40.7968 ],  
  9.           [ -73.9737, 40.7648 ],  
  10.           [ -73.9814, 40.7681 ]  
  11.        ]  
  12.      },  
  13.      {  
  14.        type: "MultiLineString",  
  15.        coordinates: [  
  16.           [ [ -73.96943, 40.78519 ], [ -73.96082, 40.78095 ] ],  
  17.           [ [ -73.96415, 40.79229 ], [ -73.95544, 40.78854 ] ],  
  18.           [ [ -73.97162, 40.78205 ], [ -73.96374, 40.77715 ] ],  
  19.           [ [ -73.97880, 40.77247 ], [ -73.97036, 40.76811 ] ]  
  20.        ]  
  21.      }  
  22.   ]  
  23. }  

    此例表示它由“MultiPoint”和“MultiLineString”两个图形构成。

    5)其他

    GeoJSON还有其他几种类型的对象,在此不再介绍,比如:MultiPoint表示多个点,MultiLineString表示多个线段,MultiPolygon表示多个多边形等等。

    4、Geo索引与查询指令

    Geo查询包含“临近”、“交集”、“包含”等等,我们通过使用如下几个专门的指令来完成相关查询。

    1)$geoWithin

    查询那些完全包含在指定形状(shape)内的地理空间数据的documents,允许有边界(border)重叠,$geoWithin通常指定Polygon或者MultiPolygon参与查询。我们指定的形状可以是一个Polygon类型的GeoJSON对象(单环,或者多环的),或者是一个MultiPolygon类型的GeoJSON。

    $geoWithin需要对查询字段建立geo索引,不过建立索引确实可以提高查询性能,2dsphere和2d索引均支持此操作。$geoWithin不会对结果数据进行排序(sort)。比如我们需要查询指定多边形内的所有地理空间数据(只查询GeoJSON类型的documents):

Java代码  收藏代码
  1. db.places.find(  
  2.    {  
  3.      loc: {  
  4.        $geoWithin: {  
  5.           $geometry: {  
  6.              type : "Polygon" ,  
  7.              coordinates: [ [ [ 0, 0 ], [ 3, 6 ], [ 6, 1 ], [ 0, 0 ] ] ]  
  8.           }  
  9.        }  
  10.      }  
  11.    }  
  12. )  

    JAVA代码示例如下:

Java代码  收藏代码
  1. List<Position> positions = new ArrayList<Position>();  
  2. positions.add(new Position(0,0));  
  3. positions.add(new Position(3,6));  
  4. positions.add(new Position(6,1));  
  5. positions.add(new Position(0,0));//外环  
  6.   
  7. collection.find(Filters.geoWithin("location", new Polygon(positions)));  

    2)$geoIntersets

    查询与指定的GeoJSON对象表示的图形有“交集”(相交,区域重叠)的地理空间数据,包括仅仅有边界相交的图形,可以指定任意类型的GeoJSON类型参与查询。仅对2dsphere索引数据有效,仅查询类型为GeoJSON类型的documents。

Java代码  收藏代码
  1. db.places.find(  
  2.    {  
  3.      loc: {  
  4.        $geoIntersects: {  
  5.           $geometry: {  
  6.              type: "Polygon" ,  
  7.              coordinates: [  
  8.                [ [ 0, 0 ], [ 3, 6 ], [ 6, 1 ], [ 0, 0 ] ]  
  9.              ]  
  10.           }  
  11.        }  
  12.      }  
  13.    }  
  14. )  

    JAVA代码示例为:

Java代码  收藏代码
  1. List<Position> positions = new ArrayList<Position>();  
  2. positions.add(new Position(0,0));  
  3. positions.add(new Position(3,6));  
  4. positions.add(new Position(6,1));  
  5. positions.add(new Position(0,0));//外环  
  6.   
  7. collection.find(Filters.geoIntersects("location", new Polygon(positions)));  

 

    3)$geometry

    配合$geoWithin、$geoIntersects、$near、$nearSphere使用,占位符,通过$geometry指定GeoJSON类型数据参与查询,即如果参与查询的不是GeoJSON,则不需要使用$geometry。我们在上述1)、2)中已经看到了对$geometry的使用例子。

    4)$near、$nearSphere

    这两个指令几乎一样,在查询条件中指定一个Point用来根据距离由近到远,获取相应的documents。这两个指令均可以查询GeoJSON对象,其中$near更倾向于对普通的坐标对查询(基于2d索引),$nearSphere倾向于对GeoJSON对象查询(2dsphere索引),不过$nearSphere也可以支持普通的坐标对。它们的查询样例为:

Java代码  收藏代码
  1. {  
  2.   $near: [ <x>, <y> ],  
  3.   $maxDistance: <distance in radians>  
  4. }  

 

Java代码  收藏代码
  1. {  
  2.   $nearSphere: {  
  3.      $geometry: {  
  4.         type : "Point",  
  5.         coordinates : [ <longitude>, <latitude> ]  
  6.      },  
  7.      $minDistance: <distance in meters>,  
  8.      $maxDistance: <distance in meters>  
  9.   }  
  10. }  
Java代码  收藏代码
  1. {  
  2.   $nearSphere: [ <x>, <y> ],  
  3.   $minDistance: <distance in radians>,  
  4.   $maxDistance: <distance in radians>  
  5. }  

 

    JAVA代码样例为:

Java代码  收藏代码
  1. collection.find(Filters.near("location",new Point(new Position(20.1,30,0)),50d,20d));  

    在查询条件中我们可以指定$minDistance和$maxDistance,最终处于2者距离之间的document将会由近到远的顺序作为result返回,其中$minDistance表示离指定的point至少多远,此指令仅仅当查询字段为2dsphere索引时才支持,即在2d索引下不能设置此值;$maxDistance表示最远的距离;对于$near,distance值为radians(弧度);对于$nearSphere,distance距离的单位是米,以指定的Point为圆心,distance为半径的圆所覆盖的地理位置数据(GeoJSON documents);如果$nearSphere应用在传统的坐标对上(2d数据),distance则为radians。 mongodb内部会有distance(米)与radians的转换计算。(参见下文)

 

    5)$center

    配合$geoWithin查询语句,使用$center指定圆心和半径,返回此区域内的数据,$center仅适用于传统的坐标对(2d),不查询GeoJSON对象。需要对查询字段建立2d索引,且不支持2dsphere索引。

Java代码  收藏代码
  1. {  
  2.    <location field>: {  
  3.       $geoWithin: { $center: [ [ <x>, <y> ] , <radius> ] }  
  4.    }  
  5. }  

    radius(半径)的数值要根据你的坐标系的测量单位而定。如果你的坐标对,只是简单的平面坐标(自定义坐标系),那么radius就根据你自己的坐标系单位而定;如果是经纬度坐标对,那么radius就需要用实际的距离(米)与地球半径距离做一些换算。

    sharding collection中不支持$near和$nearSphere,请使用$geoNear替代,使用方式与它们相同。

    6)$centerSphere

    配合$geoWithin查询语句,基本原理同$center,只不过它适用于GeoJSON表示的地理位置数据(sphere,球面),半径以radians计量。此指令支持2d和2dsphere索引。

Java代码  收藏代码
  1. {  
  2.    <location field>: {  
  3.       $geoWithin: { $centerSphere: [ [ <x>, <y> ], <radius> ] }  
  4.    }  
  5. }  

    比如查询经纬度为88W、30N的点,10英里范围内的空间数据,则可以使用如下查询语句(3963.2为地球半径的近似英里数):

Java代码  收藏代码
  1. db.places.find( {  
  2.   loc: { $geoWithin: { $centerSphere: [ [ -88, 30 ], 10/3963.2 ] } }  
  3. } )  

    关于distance与radians的换算方式,【参见此文】

    7)$box

    配合$geoWithin使用,指定一个方形,查询此区域内的地理位置数据,像$center一样只支持普通的坐标对,不支持对GeoJSON对象的查询;$box需要指定“左下角”和“右上角”的坐标位置。只有2d索引支持$box,2dsphere不支持。

Java代码  收藏代码
{  
  <location field>: {  
     $geoWithin: {  
        $box: [  
          [ <bottom left coordinates> ],  
          [ <upper right coordinates> ]  
        ]  
     }  
  }  
}  

  

    8)$polygon

    同$box,语法如下:

Java代码  收藏代码
  1. {  
  2.    <location field>: {  
  3.       $geoWithin: {  
  4.          $polygon: [ [ <x1> , <y1> ], [ <x2> , <y2> ], [ <x3> , <y3> ], ... ]  
  5.       }  
  6.    }  
  7. }  

    9)其他

    尽管2d索引支持对球面距离的查询,不过我们尽可能考虑使用2dsphere索引来替代,如果你的数据是原始的“经纬度”。(如果是自定义坐标系,那么则可以不考虑使用2dsphere)。2d索引支持在欧几里得平面上计算距离查询数据,2d索引也支持如下几个操作命令使用球面(spherical)几何的距离查询,不过通常我们认为2dsphere是查询球面数据的首要选择:$nearSphere、$centerSphere、$near、以及使用{spherical : true}选项的geoNear指令。上述三个查询使用radians来表示距离,其他的查询则不是,对于球面(spherical)查询正常工作,需要将distance转换成radians,或者使用自己的距离单位将radians转换成距离。

    距离转换成radians:将距离除以球面的半径(比如地球),使用相同的距离测量单位。

    radians转换成距离:radians乘以球面半径(比如地球)。地球的半径近似为3963.2英里或者6378.1千米。

    比如如下查询collection中以[-74,40.74]为圆心半径为100英里的区域内的数据:

Java代码  收藏代码
  1. db.places.find( { loc: { $geoWithin: { $centerSphere: [ [ -74, 40.74 ] ,100 / 3963.2 ] } } } )  

 

    $geoNear是在sharding collection中使用,它不是一个普通的操作,而是属于pipleline聚合函数。我们稍后再介绍,请参见【geoNear】    

 

六、索引策略

    1、按需创建索引

    索引可以提高query的查询效率,当然也会增加write时带来的开支,增加内存、磁盘的使用。

    1)如果所有的(大多数)查询都使用了同一个字段,那么可以给此字段建立单一字段索引。

    2)如果有些query只使用了一个key,而其他的query则是此key与其他key的组合,那么创建一个组合索引则比创建一个或者多个单一索引更加高效,索引的“最左前缀”不仅可以支持单一key的查询,而且还能兼顾它们的组合查询,且还在一定程度上了减少了因多个索引带来的性能开销。比如索引{x: 1, y : 1}开支持 {x : 1} 和 {x : 1, y : 1}两个key的查询。

    2、为了提高查询效率,我们尽可能确保所有的索引数据均可以保存在内存中,这样可以避免索引数据从磁盘索引文件中读取。可以使用db.collection.totalIndexSize()来查看此collection中所有索引的大小。如果你有多个collection,那么索引的总尺寸应该为所有collection的尺寸之和。

    当然索引不可能总是被全部载入内存,毕竟索引的尺寸终究有可能远大于内存尺寸,比如当数据量巨大或者系统中创建了多个index。mongodb只会将最近使用的indexes或者“right-most”保存在内存中,这可以更加高效的使用索引。

    3、选择性(Selectivity)是判定一个索引筛选数据的能力,越高效的索引其选择性越高,即可以排除掉更多的不匹配数据,更少的数据遍历。比如文档中有一个字段status,这个status有2个合法值“new”、“processed”,如果在status字段上建立索引,那么此索引的选择性就比较低,因为它只能筛选掉一半数据,这也意味着即使索引生效,也需要遍历整个collection中一半的数据才能得到最终结果,事实上对status建立索引是没有必要的。

    4、对于sort操作,如果能利用索引的排序直接获去结果,这是性能最好的;不过如果query不能通过index获得sort的结果,那么mongodb只能自己在内存中重新排序,在没有使用索引情况下排序操作所占用内存不得超过32M,否则将中断排序。

    对于单字段索引,索引的顺序对sort操作并无影响,应为无论是正序还是倒序,mongodb都可以很简单的翻转,这不会对性能有任何影响。

    对于多字段的组合索引中的sort操作,排序字段必须和它们在index中声明的顺序保持一致,比如索引{a : 1, b : 1},将不支持sort( { b : 1, a : 1}),且排序的方向必须和index声明的保持一致,比如上述索引支持{a : 1, b : 1} 和 { a : -1, b : -1},但无法支持{a : 1, b : -1} 和 { a : -1, b : 1}。

    如果参与排序的字段(和顺序)和索引字段一样或者是索引的前缀字段,且查询条件中也包含索引的前缀字段,那么mongodb则可以使用索引进行排序,这是一种比较高效的做法。比如索引{ a : 1, b : 1, c : 1},如下查询均可以利用索引排序:

Java代码  收藏代码
  1. db.data.find().sort( { a: 1 } )  
  2. db.data.find().sort( { a: -1 } )  
  3. db.data.find().sort( { a: 1, b: 1 } )  
  4. db.data.find().sort( { a: -1, b: -1 } )  
  5. db.data.find().sort( { a: 1, b: 1, c: 1 } )  
  6. db.data.find( { a: { $gt: 4 } } ).sort( { a: 1, b: 1 } )  

    如果参与排序的字段不是索引的前缀字段,具体是否能够利用索引排序,就要求查询条件的字段需要为索引的最左前缀字段,同时查询条件的字段和sort字段的也应该构成组合索引的左前缀,比如索引 { a : 1, b : 1, c: 1},那么此索引将支持如下几种排序方式:

Java代码  收藏代码
  1. db.data.find( { a: 5 } ).sort( { b: 1, c: 1 } )  
  2. db.data.find( { b: 3, a: 4 } ).sort( { c: 1 } )  
  3. db.data.find( { a: 5, b: { $lt: 3} } ).sort( { b: 1 } )  

    如果查询字段和sort字段不能构成组合索引的左前缀,那么将不能高效的利用索引排序,或者有可能不会使用此索引,比如: 

Java代码  收藏代码
  1. db.data.find( { a: { $gt: 2 } } ).sort( { c: 1 } )  
  2. db.data.find( { c: 5 } ).sort( { c: 1 } )  

 

七、geoNear实例

    如果collection中geo数据很大,我们需要考虑使用sharding架构设计,在sharding collection中,因为不支持$near、$nearSphere指令,事实上这两个指令反而是应用中最常用的操作,那么我们只能使用$geoNear来替代,用来查询“离指定Point一定距离内的geo数据”;$geoNear是一个聚合方法(Aggregation),不过它比较特殊,聚合方法的pipeline中通常只需要$geoNear一个stage即可,$geoNear这个stage中已经集成了“$match”、“$sort”、“$limit”三个功能,即不再需要在pipeline中额外声明$match、$sort、$limit三个stage。(详细内容,请参考【Aggregation】

    $geoNear操作接受一个查询document,此document中支持如下Filed:

    1)spherical:是否为“球面”,默认为false。如果使用2dsphere索引,那么它必须为true,则距离计算使用“米”(meters),参与查询的Point为GeoJSON类型,通常为地理位置的经纬度;如果使用2d索引,那么它为false,参与查询的Point为普通的坐标对,那么距离计算则使用radians,上文中已经提到radians与“米”之间的换算方式。

    2)limit或者num:这两个字段含义一致,表示返回数据的总条数。作用同stage $limit。

    3)maxDistance:可选值,表示离指定Point最大的距离,对于GeoJSON而言,distance的值为“米”,如果是普通坐标对,则为radians。

    4)minDistance:可选值,指离指定Point最小的距离。(3.2+版本)

    5)query:可选值,筛选文档的查询条件,同stage $match。

    6)distanceField:必填值,在输出的结果文档中增加一个字段,用来表示计算的距离,即location与指定的Point之间的距离。

    7)distanceMultiplier:可选值,数字类型,此值将会与“计算的距离”相乘,通常用来将距离转换,在2d数据、传统坐标对中使用(即spherical : false)。比如在2d数据中,计算的距离为radians,可以指定distanceMultiplier为地球的半径,那么最终返回的“distanceFiled”值为radians * distanceMultiplier,此时得到的是值为“米”(或者千米,取决于distanceMultiplier的单位);这样结果就和2dsphere(即spherical : true)查询的结果一致了。

    8)near:必填值,格式为GeoJSON(2dsphere)或者普通的坐标对(2d),指定参与比较的Point。

 

    当使用$geoNear时需要考虑到:

    1)它必须为pipeline的首个stage。

    2)必须包含distanceField,此字段将会在结果中输出。

    3)我们发现,$geoNear没有指定那个字段和“near”比较,所以这硬性要求collection中有且只有一个字段是2dsphere或者2d索引。这样$geoNear就直接使用索引字段参与查询。

    

    如下为$geoNear的shell查询方式语法:

Java代码  收藏代码
  1. db.places.aggregate([  
  2.    {  
  3.      $geoNear: {  
  4.         near: { type: "Point", coordinates: [ -73.99279 , 40.719296 ] },  
  5.         distanceField: "dist.calculated",  
  6.         maxDistance: 2,  
  7.         query: { type: "public" },  
  8.         includeLocs: "dist.location",  
  9.         num: 5,  
  10.         spherical: true  
  11.      }  
  12.    }  
  13. ])  

 

    如下为一个完整的JAVA示例:

    1)创建索引

Java代码  收藏代码
  1. MongoClient mongoClient = new MongoClient("127.0.0.1",27017); //mongos  
  2. MongoDatabase db = mongoClient.getDatabase("test");  
  3. MongoCollection<Document> collection = db.getCollection("geo_sample");  
  4. collection.createIndex(new Document("location","2dsphere"));  

 

    2)插入geo数据

Java代码  收藏代码
  1. //insert some test datas;  
  2. List<Document> locations = new ArrayList<Document>();  
  3. locations.add(new Document("type","public")  
  4.             .append("location",new Point(new Position(-73.99279,40.719296))));  
  5. locations.add(new Document("type","public")  
  6.             .append("location",new Point(new Position(-73.99378,40.719396))));  
  7. locations.add(new Document("type","public")  
  8.             .append("location",new Point(new Position(-73.99379,40.719395))));  
  9. collection.insertMany(locations);  

 

   3)聚合方法查询

Java代码  收藏代码
  1. //查询  
  2. List<Document> pipeline = new ArrayList<Document>();  
  3. Document geoNear = new Document();  
  4. Document content = new Document();  
  5. content.append("near",new Point(new Position(-73.99279,40.719296)));  
  6. content.append("distanceField","distance");  
  7. content.append("maxDistance",100);  
  8. content.append("query",new Document("type","public"));  
  9. content.append("spherical",true);  
  10. content.append("limit",20);  
  11. geoNear.append("$geoNear", content);  
  12.   
  13. pipeline.add(geoNear);  
  14. MongoCursor<Document> cursor =  collection.aggregate(pipeline).allowDiskUse(true)  
  15.             .batchSize(128)  
  16.             .maxTime(60,TimeUnit.SECONDS)  
  17.             .useCursor(true).iterator();  
  18. while (cursor.hasNext()) {  
  19.    Document document = cursor.next();  
  20.    System.out.println(document.toJson());  
  21. }  
  22. cursor.close();  

 

    4)查询结果样例

Java代码  收藏代码
  1. {  
  2.     "_id": {  
  3.         "$oid": "563ad23af4f64b0d42eadae4"  
  4.     },   
  5.     "type": "public",   
  6.     "location": {  
  7.         "type": "Point",   
  8.         "coordinates": [  
  9.             -73.99279,   
  10.             40.719296  
  11.         ]  
  12.     },   
  13.     "distance": 0  
  14. }  
  15. {  
  16.     "_id": {  
  17.         "$oid": "563ad28df4f64b0d45c9780a"  
  18.     },   
  19.     "type": "public",   
  20.     "location": {  
  21.         "type": "Point",   
  22.         "coordinates": [  
  23.             -73.99378,   
  24.             40.719396  
  25.         ]  
  26.     },   
  27.     "distance": 84.26495157209176  
  28. }  
  29. {  
  30.     "_id": {  
  31.         "$oid": "563ad28df4f64b0d45c9780b"  
  32.     },   
  33.     "type": "public",   
  34.     "location": {  
  35.         "type": "Point",   
  36.         "coordinates": [  
  37.             -73.99379,   
  38.             40.719395  
  39.         ]  
  40.     },   
  41.     "distance": 85.08684215614285  
  42. }  

  

参考:https://my.oschina.net/manmao/blog/741628

posted @ 2018-10-17 23:04  老皮肉  阅读(628)  评论(0编辑  收藏  举报