上篇我们学习了MongoDB中的索引机制以及常见的索引管理,除此之外,MongoDB还支持一些针对特殊业务的集合或者索引,例如支持空间存储索引,支持固定大小集合,支持搜索和TTL索引,除此之外,MongoDB还支持GridFS存储,本篇我们就来学习一下这些高级应用特性
固定大小集合
在MongoDB中的普通集合都是动态增长的,而且可以自动增长以容纳更多的数据。MongoDB中还有另一种不同类型的集合,叫做固定集合,固定集合需要事先创建好,而且它的大小是固定的。这种集合我们可以看成是一个固定的环,每一个数据都是环上的一部分。使用固定大小的集合也会引出一个新的思考,如果集合满了,我们再次插入会如何?答案是最先插入的数据会被移除,可以理解为集合像环形一样,每次都会插入都会换到下一个位置,当集合存满以后,集合的位置再次来到起点的位置,这个时候会把原来的数据溢出,新的文档将会占据这个位置。
也因为固定集合的特殊性,导致和普通的集合有很大的不同,比如固定集合由于大小固定,因此使用的是固定空间,数据可以顺序写入,因此在磁盘上上的写入速度非常快,除此之外,固定集合也不能被分片。
创建·固定集合
MongoDB中创建固定集合可以设置两种大小,第一种就是设置集合内存大小-即字节数,例如我们创建一个限制10000字节大小的固定集合:
db.createCollection("appedLogCollection",{capped:true,size:10000})
而另外一种,则是限制存入集合的文档数量,如下:
db.createCollection("appedLogCollection2",{capped:true,size:10000,max:10})
其中max字段则指定了文档的数量,可以使用这种方式,就限制了存入集合中的文档永远只有最新的十条。需要注意的是,一旦创建了固定集合以后,其大小和数量限制都无法再去修改了,如果真的要改变,只能选择删除当前集合,再去重新创建,因此在创建固定集合之前建议考虑清楚需要设置的限制大小,如果是同事设置了文档数量以及集合大小的情况下,会按照优先触发的为主进行限制,而不是需要等待两个限制都触发。
如果在开发过程中,需要将一个普通的集合转换为固定集合,我们可以使用convertToCapped
命令实现,如下:
db.runCommand({"convertToCapped":"test1",size:10000})
这里的convertToCapped
指定的是需要转换为固定集合的集合名称,而当前db下有哪些集合,我们可以通过show collections
命令查看,但是需要注意的是,普通集合可以转为固定集合,而固定集合则无法转换回普通集合,因此在进行集合转换的时候,需要慎重考虑,除此之外,转换固定集合有可能出现错误,例如原集合中本来就有1000字节以上的大小,但是转换为固定集合的时候指定了大小小于1000字节,或者该集合已经被转换为固定集合了以后,我们选择再次进行转换,就会转换失败,因此为了防止转换失败,我们可以在转换之前先去判断一下该集合是否为固定集合,如下:
db.appedLogCollection.isCapped();//true
如果这里结果为true,则表示当前集合已经是固定集合,无需在进行转换了。
自然排序
对固定集合进行一种特殊的排序,称之为自然排序,而所谓自然排序返回的顺序和文档在磁盘上的顺序是一致的。由于普通的集合来说,磁盘空间不固定,大小不固定,因此自然排序是没有任何意义的,但是对于固定集合来说,本身在磁盘上就是固定的内存,文档是顺序写入的,因此读取出来的文档就是按照从旧到新的方式排列的,如果我们需要从新到旧排序,将最新的数据放在最前面的话,可以使用固定集合的{"$natural" : -1}
进行排序:
db.appedLogCollection.find().sort({"$natural":1});//按照自然排序-从旧到新排序
db.appedLogCollection.find().sort({"$natural":-1});//自然排序反向-从最新到最旧
TTL索引
我们知道,对于集合,尤其是固定集合中的文档何时覆盖,我们对其控制的很有限,并且无法手动删除部分数据,如果我们想要更加灵活的操作文档,将文档移除,可以选择在插入文档的时候,给每一个文档设置一个TTL时间,即过期时间,到了这个时间以后,这个文档就会默认被删除。
我们来给集合中创建一个ttl索引:
db.ttlCollection.ensureIndex({"create_time":1},{expireAfterSeconds:180})
其中expirAfterSeconds
字段指定了过期时间为180s,即create_time字段的时间在180s以前的都会被认为过期删除,但是这里我们需要注意的一点是,想要设置TTL索引,必须满足以下几个条件:
1).TTL只能针对单个索引字段有效,即混合字段是无法设置TTL索引的
2).想要设置为TTL的索引字段,必须是date格式,例如我们这里插入一条数据为:
db.ttlCollection.insert({"code":"11","name":"dc","age":18,"create_time":new Date('Nov 12, 2020 16:08:00')})
除此之外,文档是否过期的扫描,并不是实时的,在MongoDB中进行删除操作是独立的线程执行的,默认60S会去扫描一次,即使扫描到了文档过期,由于是独立线程的原因,也不一定会立刻删除成功,因此文档的过期可能会出现延迟的情况
如果我们不想删除经常使用的文档,可以考虑在每次使用文档的时候,都将TTL索引的字段值进行修改,改为当前的时间,这样就可以保证文档一直在TTL范围内不被移除。同样,也因为TTL默认60S,因此我们不应该依赖以秒为单位的时间保证索引的活跃状态。当然在创建完TTL索引以后,如果想要修改TTL的值,可以使用collMod
选项来重置expirAfterSeconds
的值,如下:
db.runCommand({collMod: 'ttlCollection', index: {keyPattern:{create_time:1}, expireAfterSeconds:800}})
这里的collMod字段指定的是需要修改的集合,keyPattern则是需要修改的TTL索引字段信息,修改完毕以后我们再次查看当前集合的索引信息:
db.ttlCollection.getIndexes();
//输出
[
{
"v" : 2,
"key" : {
"_id" : 1
},
"name" : "_id_",
"ns" : "set.ttlCollection"
},
{
"v" : 2,
"key" : {
"create_time" : 1
},
"name" : "create_time_1",
"ns" : "set.ttlCollection",
"expireAfterSeconds" : NumberLong(800)
}
]
可以清晰看到name为create_time_1的索引的expireAfterSeconds已经变为了800
设置删除时间
除了前面给某一个date字段设置到期的时间以外,我们有可能遇到以下场景,如需要清理在2020年11月13日之前的过期数据,并且每隔一段时间都会更新具体过期的时间,而不是插入了多久以后算过期,这个时候我们设置ttl为正数已经不合适了,但是我们依然可以利用TTL的特点来完成这个操作,如下:
//1.将集合每个文档中添加一个clear_time字段指定删除时间(并且保证每次插入的文档都有一个clear_time字段)
db.getCollection("ttlCollection").update({},{"$set":{"clear_time":new Date("Nov 13, 2020 00:00:00")}});
//2.删除原来的ttl,重新设置ttl时间为0,字段为clear_time
db.ttlCollection.dropIndex("create_time_1");
db.ttlCollection.ensureIndex({"clear_time":1},{expireAfterSeconds:0});
接着等待一段时间,再去查询,发现之前插入的11月12日的数据已经全部被删除了
全文检索
MonoDB从2.4版本开始,加入了一个特殊的索引类型,用于在文档中搜索文本,虽然前面学过可以根据正则等来进行搜索,但是在大文本中用正则搜索,会导致非常慢,并且很多场景下因为技术受限无法进行匹配搜索。但是使用全文索引不一样,其内部也内置和支持了多种分词机制。需要注意的是,创建全文检索索引的成本要高于其他的普通索引,存入的字符串会被分解分词后进行保存,因此创建全文检索的时候建议是在后台创建或者离线创建,并且在写入的时候因为拆分词组的原因,会比普通集合慢的多,如果做了分片的话,也会导致分片的迁移速度变慢,并且如果迁移到了其他分片,所有的文本还需要重新构建索引,这样可能会带来一段时间的内存和效率的开销。
支持的语言
截止到4.0版本,mongodb的全文索引支持的全文索引的语言达到了15种,如下:
- danish
- dutch
- english
- finnish
- french
- german
- hungarian
- italian
- norwegian
- portuguese
- romanian
- russian
- spanish
- swedish
- turkish
开启全文索引
mongoDB在2.6版本及以后默认是开启全文索引的,不需要额外去手动开启,如果是2.6以前的版本,可以使用setParameter
命令完成全文索引的开启:
db.adminCommand({setParameter:true,textSearchEnabled:true})
//或者在启动mongod的时候指定
mongod --setParameter textSearchEnabled=true
开启全文索引以后,我们来给一个集合中插入一些文档数据,需要注意的是这里的文本不建议包含中文,格式如下:
{
"p_text": "enjoy the mongodb articles on Runoob",
"tags": [
"mongodb",
"runoob"
]
}
接着我们可以给p_text字段设置类型为text的索引,即文本索引:
db.postText.ensureIndex({p_text:"text"})
使用全文索引
如果我们想要针对加了全文索引的字段进行查询,我们可以使用$text
进行查询,例如我们来查询刚刚文本中的mongodb字符串:
db.postText.find({$text:{$search:"mongodb"}});
//输出
{ "_id" : ObjectId("5fb297175efc1f0332d33f15"), "p_text" : "enjoy the mongodb articles on Runoob", "tags" : [ "mongodb", "runoob" ]
如果是使用的3.X版本以下的mongo,需要使用runCommand
命令来进行查询:
db.postText.runCommand("text",{search:"mongodb"})
除了普通的全文索引以外,我们还可能遇到多个字段联合创建多文本索引的情况,这个时候就有可能遇到多个字段上的数据都满足查询的,这个时候我们为了防止或者减少这种情况出现,可以选择在创建全文索引的时候,给每个字段指定不同的权重,权重的范围是从1~1 000 000 000,值越小,即权重越低,如下:
db.postText.ensureIndex({p_text:"text",tags:"text"},{weights:{p_text:100,tags:2}})
这样创建全文索引以后,默认情况下p_text字段的权重是最高的,tags的权重最低
删除索引
mongoDB规定一个集合中只能存在一个全文索引,并且由于全文索引的调整复杂,因此当我们创建完毕以后,就无法修改全文索引,此时当我们出现文档结构的调整,或者部分字段不需要进行文本索引的时候,只能选择删除当前的文本索引以后,重新建立索引,与普通的索引规则类似,全文索引的命名是按照 字段1_text_字段2_text... 的规则创建的,当然我们也可以根据getIndexes
函数来查询当前集合的索引:
db.postText.getIndexes();
//输出
[
{
"v" : 2,
"key" : {
"_id" : 1
},
"name" : "_id_",
"ns" : "postText.postText"
},
{
"v" : 2,
"key" : {
"_fts" : "text",
"_ftsx" : 1
},
"name" : "p_text_text_tags_text",
"ns" : "postText.postText",
"weights" : {
"p_text" : 1,
"tags" : 100
},
"default_language" : "english",
"language_override" : "language",
"textIndexVersion" : 3
}
]
可以看到当前的多字段全文索引的名称为:p_text_text_tags_text,可以和普通索引一样使用dropIndex
进行删除
db.postText.dropIndex("p_text_text_tags_text")
指定语言分词器的索引
我们在创建文本索引的时候,可以指定切换的语言分词,默认情况下mongoDB的default_language
选择的是english,我们可以指定:
db.postText.ensureIndex({p_text:"text",tags:"text"},{weights:{p_text:100,tags:2},{default_language:"french"}})
这样就创建了一个法语的文本索引,需要注意的是mongoDB在3.4版本开始是支持了中文索引的,但是此功能仅限在企业版的mongo中才有,而且经过测试,mongo的中文分词做的并不好,很多词汇无法区分,如果真的想要在项目中使用文本索引的方式,可以考虑将数据同步到es中,使用分词器进行检索,当然也可以在插入文档的时候,指定当前文档的语言,这样不同的文档之间使用的分词器也会不一样
地理坐标检索
mongoDB中支持基于地理位置的索引,可以帮助我们结合地理空间形状以及点集上进行快速高效的空间索引,而地理空间索引既可以使用平面几何也可以使用基于球面的几何,因此主要分为两种,一种为2dsphere
索引,这种索引仅仅支持球面几何索引,而另一种为2d
索引,这种索引同时支持平面和球面几何,但是2dsphere
索引在使用球面几何的查询上会更高效和精确,因此如果我们要查询的是基于球面地理的坐标,建议使用2dsphere
索引进行查询。
我们先创建一些测试数据:
db.map.insert({loc : [10, 10]});
db.map.insert({loc : [11, 10]});
db.map.insert({loc : [10, 11]});
db.map.insert({loc : [12, 15]});
db.map.insert({loc : [16, 17]});
db.map.insert({loc : [90, 90]});
db.map.insert({loc : [150, 160]});
创建地理索引
需要注意的是文档的结构是不限制的,但是我们设置为地理索引的字段的文档格式是固定的,这里我们给loc字段设置为2d地理索引:
db.map.createIndex({ loc: "2d" })
查询附近的点
假设当前我们知道了一个坐标,希望能获取这个坐标附近范围的其他点的位置,可以使用$near
命令查看:
db.map.find({loc:{$near:[11,11]}});
此时可以看到返回的结果为:
{ "_id" : ObjectId("5fb2453d555025767cb3028a"), "loc" : [ 10, 11 ] }
{ "_id" : ObjectId("5fb2454a555025767cb30290"), "loc" : [ 11, 10 ] }
{ "_id" : ObjectId("5fb2454b555025767cb30291"), "loc" : [ 10, 11 ] }
{ "_id" : ObjectId("5fb2453d555025767cb30289"), "loc" : [ 11, 10 ] }
{ "_id" : ObjectId("5fb2453d555025767cb30288"), "loc" : [ 10, 10 ] }
{ "_id" : ObjectId("5fb2454a555025767cb3028f"), "loc" : [ 10, 10 ] }
{ "_id" : ObjectId("5fb2454b555025767cb30292"), "loc" : [ 12, 15 ] }
{ "_id" : ObjectId("5fb2453d555025767cb3028b"), "loc" : [ 12, 15 ] }
{ "_id" : ObjectId("5fb2453d555025767cb3028c"), "loc" : [ 16, 17 ] }
{ "_id" : ObjectId("5fb2454b555025767cb30293"), "loc" : [ 16, 17 ] }
{ "_id" : ObjectId("5fb2453e555025767cb3028d"), "loc" : [ 90, 90 ] }
{ "_id" : ObjectId("5fb2454b555025767cb30294"), "loc" : [ 90, 90 ] }
{ "_id" : ObjectId("5fb2454b555025767cb30295"), "loc" : [ 150, 160 ] }
我们发现居然把数据全部返回了,当然我们可以在查询的时候设置附近的范围,单位(欧氏距离),使用$maxDistance
命令:
db.map.find({loc:{$near:[11,11],$maxDistance:3}})
可以看到返回的内容都是在3单位距离内的点坐标:
{ "_id" : ObjectId("5fb2453d555025767cb3028a"), "loc" : [ 10, 11 ] }
{ "_id" : ObjectId("5fb2454a555025767cb30290"), "loc" : [ 11, 10 ] }
{ "_id" : ObjectId("5fb2454b555025767cb30291"), "loc" : [ 10, 11 ] }
{ "_id" : ObjectId("5fb2453d555025767cb30289"), "loc" : [ 11, 10 ] }
{ "_id" : ObjectId("5fb2454a555025767cb3028f"), "loc" : [ 10, 10 ] }
{ "_id" : ObjectId("5fb2453d555025767cb30288"), "loc" : [ 10, 10 ] }
查询范围图形区域内的点
在2d索引中,是支持限制查询的最大距离范围的,不过不支持最小距离,设置查询范围的话需要使用$geoWithin
命令指定范围和图形,常见的图形范围如下:
//矩形范围,左边界的坐标和右边界的坐标
{"$box" : [[x1, y1], [x2, y2]]}
//圆形范围,r代表半径长度
{"$center" : [[x1, y1], r]}
//多边形,每个数组代表一个坐标点
{"$polygon" : [[x1, y1], [x2, y2], [x3, y3],...]}
查询矩形范围内的如下:
db.map.find({loc : {"$geoWithin" : {"$box" : [[9, 9], [11, 11]]}}})
查询圆形范围如下:
db.map.find({loc : {"$geoWithin" : {"$center" : [[10, 10], 2]}}})
查询多边形范围如下:
db.map.find({loc : {"$geoWithin" : {"$polygon" : [[10, 10],[10, 12],[11, 12],[12, 12]]}}})
2d和2dsphere索引集合的差异
除了上面简单的索引操作以外,mongodb针对这两种索引机制,提供了很多其他的操作命令,如下:
查询类型 | 几何类型 | 注释 |
---|---|---|
$near (GeoJSON点,2dsphere索引) |
球面 | |
$near (传统坐标,2d索引) |
平面 | |
$nearSphere (GeoJSON点,2dsphere索引) |
球面 | |
$nearSphere (传统坐标,2d索引) |
球面 | 使用GeoJSON点替换 |
$geoWithin:{$geometry:...} |
球面 | |
$geoWithin:{$box:...} |
平面 | |
$geoWithin:{$polygon:...} |
平面 | |
$geoWithin:{$center:...} |
平面 | |
$geoWithin:{$centerSphere:...} |
球面 | |
$geoIntersects |
球面 |
除此之外,两个索引数据之间需要的格式也是有差异的,上面我们使用的2d索引只需要索引字段的内容符合[x,y]坐标格式即可,而如果我们需要使用2dsphere
索引的话,我们需要维护GeoJSON格式的坐标信息,格式大概为:
{ type: 'GeoJSON type' , coordinates: 'coordinates'}
其中type指的是类型,常见的如Point、LineString、Polygon等,我们用得地图索引使用的type就是Point类型,coordinates 则是一个坐标数组,我们来插入一条数据,如下:
db.map2.insert({name:"A",sp:{type:"Point",coordinates:[105.754484701156,41.689607057699]}})
其中sp字段就是我们需要的GeoJSON格式的数据
GridFS存储文件
GridFS是MongoDB的一种存储机制,用来存储大型二进制文件,其优点如下:
1.使用GridFS能够简化你的栈。如果已经在使用MongoDB,那么可以使用GridFS来代替独立的文件存储工具
2.GridFS会自动平衡已有的复制或者设置为自动分片,所以对文件存储的故障转移、备份或者横向扩展会更加容易
3.当用户上传文件的时候,GridFS可以避免一些文件系统的问题,比如某个目录下无法存储大量文件或者大量的大内存文件
4.在GridFS中,文件存储的集中度比较高,因为在MongoDB中,一个数据文件是2GB的方式分割的
除此之外,GridFS也有一些缺陷,这也是目前主流的文件系统中几乎很少考虑GridFS的原因:
1.GridFS的性能比较低,比起直接搭建文件存储系统,进行访问文件的速度要慢上一些
2.GridFS不支持直接进行文件的修改,如果我们需要修改某个文件,必须先删除以后,再重新保存才可以
3.GridFS无法支持同时保存多个文件,因此在并发情况下,无法同时对多个文件进行加锁
使用GridFS
想要使用GridFS比较简单的方式就是mongofiles工具了,这个一般在mongoDB的发行版里面都有,可以直接使用,除此之外也可以根据mongoDB的驱动实现各语言的GridFS文件上传、下载
我们可以通过mongofiles --help
命令来查看相关的文件操作的命令,如下:
mongofiles --help
可以看到,我们操作mongofiles的命令如下:
mongofiles
而在操作中,我们只需要指定options,command以及文件名即可
上传
我们先在/up_files目录下创建一个文件:
vim up_test.txt
//上传
mongofiles put up_test.txt
//响应
2020-12-22T01:08:03.596+0800 connected to: mongodb://localhost/
2020-12-22T01:08:03.713+0800 added gridFile: up_test.txt
可以看到上传成功了,接着我们来查看一下当前的文件列表有木有这个文件,查看列表使用list命令:
mongofiles list
//响应
2020-12-22T01:08:45.289+0800 connected to: mongodb://localhost/
up_test.txt 31
可以看到我们上传的up_test.txt文件已经上传了,并且显示出了文件大小,31字节,现在我们把本地的文件删除,尝试从mongo中下载下来(下载命令是get):
rm -rf up_test.txt
ls -l
//可以看到当前目录下什么文件都没有了,开始下载
mongofiles get up_test.txt
//再次查看目录
ls -l
-rw-r--r--. 1 root root 31 12�� 22 01:22 up_test.txt
除此之外,我们还可以在GridFS中进行文件搜索以及删除远端的文件,分别是search
命令和delete
命令,如下:
mongofiles search up
//结果
2020-12-22T01:37:25.954+0800 connected to: mongodb://localhost/
up_test.txt 31
接着我们把搜索到的文件列表进行删除
mongofiles delete up_test.txt
//删除全部up开头的文件
2020-12-22T01:38:25.017+0800 connected to: mongodb://localhost/
2020-12-22T01:38:25.019+0800 successfully deleted all instances of 'up_test.txt' from GridFS
GridFS解惑
GridFS是一种轻量级的文件存储规范,用于存储MongoDB中的普通文档。基本上MongoDB不会对GridFS里面的文件做任何特殊处理,这些处理需要用户自己处理或者部分由MongoDB的驱动实现。
GridFS的原理是:**可以将大文件分割为多个比较大的chunk块,,一般为256k/个,每个chunk将作为MongoDB的一个文档(document)被存储在chunks集合中 **。因为MongoDB支持在文档中存储二进制,因此可以把存储块的成本降低很多。除了拆分的块以外,Mongo中还有个文档用于记录存储这些块文件的元信息(默认是fs.files
集合 )。而这些块会存储在一个特殊的集合中,默认使用的是fs.chunks
,也可以配置修改为其他的集合,集合中记录块的信息比较简单,fs.files
集合的文档内容如下:
{
"filename": "up_test.txt",
"chunkSize": NumberInt(31),
"uploadDate": ISODate("2020-12-21T16:32:33.557Z"),
"md5": "7b762939321e146569b07f72c62cca4f",
"length": NumberInt(31)
}
可以看到,这里定义了文件的名称,拆分的chunk块大小,以及文件本身的大小,上传的时间以及文件的md5,用于过滤重复的文件,除此之外,我们可以查看fs.chunks
集合的文档,如下:
{
"_id" : ObjectId("534a75f45b6c7fe66b"),
"files_id": ObjectId("534a75d19f54bfec8a2fe44b"),
"n": NumberInt(0),
"data": "Mongo Binary Data"
}
可以看到这里定义了几个属性,大概信息如下:
files_id:块所属文件的元信息
n:块在文件中的相对位置
data:块中包含的二进制数据