MongoDB指南---1四、特殊的索引和集合:固定集合、TTL索引、全文本索引

上一篇文章: MongoDB指南---1三、索引類型、索引管理
下一篇文章: MongoDB指南---1五、特殊的索引和集合:地理空間索引、使用GridFS存儲文件

本章介紹MongoDB中一些特殊的集合和索引類型,包括:git

  • 用於類隊列數據的固定集合(capped collection);
  • 用於緩存的TTL索引;
  • 用於簡單字符串搜索的全文本索引;
  • 用於二維平面和球體空間的地理空間索引;
  • 用於存儲大文件的GridFS。

 固定集合

MongoDB中的「普通」集合是動態建立的,並且能夠自動增加以容納更多的數據。MongoDB中還有另外一種不一樣類型的集合,叫作固定集合,固定集合須要事先建立好,並且它的大小是固定的(如圖6-1所示)。說到固定大小的集合,有一個頗有趣的問題:向一個已經滿了的固定集合中插入數據會怎麼樣?答案是,固定集合的行爲相似於循環隊列。若是已經沒有空間了,最老的文檔會被刪除以釋放空間,新插入的文檔會佔據這塊空間(如圖6-2所示)。也就是說,當固定集合被佔滿時,若是再插入新文檔,固定集合會自動將最老的文檔從集合中刪除。github

clipboard.png

圖6-1 新文檔被插入到隊列末尾正則表達式

clipboard.png

圖6-2 若是隊列已經被佔滿,那麼最老的文檔會被以後插入的新文檔覆蓋
固定集合的訪問模式與MongoDB中的大部分集合不一樣:數據被順序寫入磁盤上的固定空間。所以它們在碟式磁盤(spinning disk)上的寫入速度很是快,尤爲是集合擁有專用磁盤時(這樣就不會由於其餘集合的一些隨機性的寫操做而「中斷」)。shell

固定集合不能被分片。

固定集合能夠用於記錄日誌,儘管它們不夠靈活。雖然能夠在建立時指定集合大小,但沒法控制何時數據會被覆蓋。segmentfault

 建立固定集合

不一樣於普通集合,固定集合必須在使用以前先顯式建立。可使用create命令建立固定集合。在shell中,可使用createCollection函數:api

> db.createCollection("my_collection", {"capped" : true, "size" : 100000});
{ "ok" : true }

上面的命令建立了一個名爲my_collection大小爲100 000字節的固定集合。
除了大小,createCollection還可以指定固定集合中文檔的數量:數組

> db.createCollection("my_collection2",
... {"capped" : true, "size" : 100000, "max" : 100});
{ "ok" : true }

可使用這種方式來保存最新的10則新聞,或者是將每一個用戶的文檔數量限制爲1000。
固定集合建立以後,就不能改變了(若是須要修改固定集合的屬性,只能將它刪除以後再重建)。所以,在建立大的固定集合以前應該仔細想清楚它的大小。緩存

爲固定集合指定文檔數量限制時,必須同時指定固定集合的大小。無論先達到哪個限制,以後插入的新文檔就會把最老的文檔擠出集合:固定集合的文檔數量不能超過文檔數量限制,固定集合的大小也不能超過大小限制。

建立固定集合時還有另外一個選項,能夠將已有的某個常規集合轉換爲固定集合,可使用convertToCapped命令實現。下面的例子將test集合轉換爲一個大小爲10 000字節的固定集合:服務器

> db.runCommand({"convertToCapped" : "test", "size" : 10000});
{ "ok" : true }

沒法將固定集合轉換爲非固定集合(只能將其刪除)。app

 天然排序

對固定集合能夠進行一種特殊的排序,稱爲天然排序(natural sort)。天然排序返回結果集中文檔的順序就是文檔在磁盤上的順序(如圖6-3所示)。

clipboard.png

圖6-3 使用{"$natural" : 1}進行排序
對大多數集合來講,天然排序的意義不大,由於文檔的位置常常變更。可是,固定集合中的文檔是按照文檔被插入的順序保存的,天然順序就是文檔的插入順序。所以,天然排序獲得的文檔是從舊到新排列的。固然也能夠按照重新到舊的順序排列(如圖6-4所示)。

> db.my_collection.find().sort({"$natural" : -1})

clipboard.png

圖6-4 使用{"$natural" : -1}進行排序

循環遊標

循環遊標(tailable cursor)是一種特殊的遊標,當循環遊標的結果集被取光後,遊標不會被關閉。循環遊標的靈感來自tail -f命令(循環遊標跟這個命令有點兒類似),會盡量久地持續提取輸出結果。因爲循環遊標在結果集取光以後不會被關閉,所以,當有新文檔插入到集合中時,循環遊標會繼續取到結果。因爲普通集合並不維護文檔的插入順序,因此循環遊標只能用在固定集合上。
循環遊標一般用於當文檔被插入到「工做隊列」(其實就是個固定集合)時對新插入的文檔進行處理。若是超過10分鐘沒有新的結果,循環遊標就會被釋放,所以,當遊標被關閉時自動從新執行查詢是很是重要的。下面是一個在PHP中使用循環遊標的例子(不能在mongo shell中使用循環遊標):

$cursor = $collection->find()->tailable();

while (true) {
    if (!$cursor->hasNext()) {
        if ($cursor->dead()) {
            break;
        }
        sleep(1);
    }
    else {
        while ($cursor->hasNext()) {
            do_stuff($cursor->getNext());
        }
    }
}

這個遊標會不斷對查詢結果進行處理,或者是等待新的查詢結果,直到遊標被關閉(超過10分鐘沒有新的結果或者人爲停止查詢操做)。

沒有_id索引的集合

默認狀況下,每一個集合都有一個"_id"索引。可是,若是在調用createCollection建立集合時指定autoIndexId選項爲false,建立集合時就不會自動在"_id"上建立索引。實踐中不建議這麼使用,可是對於只有插入操做的集合來講,這確實能夠帶來速度的稍許提高。

若是建立了一個沒有"_id"索引的集合,那就永遠都不能複製它所在的mongod了。複製操做要求每一個集合上都要有"_id"索引(對於複製操做,可以惟一標識集合中的每個文檔是很是重要的)。

在2.2版本以前,固定集合默認是沒有"_id"索引的,除非顯式地將autoIndexId置爲true。若是正在使用舊版的固定集合,要確保你的應用程序可以填充"_id"字段(大多數驅動程序會自動填充"_id"字段),而後使用ensureIndex命令建立"_id"索引。
記住,"_id"索引必須是惟一索引。不一樣於其餘索引,"_id"索引一經建立就沒法刪除了,所以在生產環境中建立索引以前先本身實踐一下是很是重要的。因此建立"_id"索引必須一次成功!若是建立的"_id"索引不合規範,就只能刪除集合再重建了。

 TTL索引

上一節已經講過,對於固定集合中的內容什麼時候被覆蓋,你只擁有很是有限的控制權限。若是須要更加靈活的老化移出系統(age-out system),可使用TTL索引(time-to-live index,具備生命週期的索引),這種索引容許爲每個文檔設置一個超時時間。一個文檔到達預設置的老化程度以後就會被刪除。這種類型的索引對於緩存問題(好比會話的保存)很是有用。
在ensureIndex中指定expireAfterSecs選項就能夠建立一個TTL索引:

> // 超時時間爲24小時
> db.foo.ensureIndex({"lastUpdated" : 1}, {"expireAfterSecs" : 60*60*24})

這樣就在"lastUpdated"字段上創建了一個TTL索引。若是一個文檔的"lastUpdated"字段存在而且它的值是日期類型,當服務器時間比文檔的"lastUpdated"字段的時間晚expireAfterSecs秒時,文檔就會被刪除。
爲了防止活躍的會話被刪除,能夠在會話上有活動發生時將"lastUpdated"字段的值更新爲當前時間。只要"lastUpdated"的時間距離當前時間達到24小時,相應的文檔就會被刪除。
MongoDB每分鐘對TTL索引進行一次清理,因此不該該依賴以秒爲單位的時間保證索引的存活狀態。可使用collMod命令修改expireAfterSecs的值:

> db.runCommand({"collMod" : "someapp.cache", "expireAfterSecs" : 3600})

在一個給定的集合上能夠有多個TTL索引。TTL索引不能是複合索引,可是能夠像「普通」索引同樣用來優化排序和查詢。

全文本索引

MongoDB有一個特殊類型的索引用於在文檔中搜索文本。前面幾章都是使用精確匹配和正則表達式來查詢字符串,可是這些技術有一些限制。使用正則表達式搜索大塊文本的速度很是慢,並且沒法處理語言的理解問題(好比entry與entries應該算是匹配的)。使用全文本索引能夠很是快地進行文本搜索,就如同內置了多種語言分詞機制的支持同樣。

建立任何一種索引的開銷都比較大,而建立全文本索引的成本更高。在一個操做頻繁的集合上建立全文本索引可能會致使MongoDB過載,因此應該是離線狀態下建立全文本索引,或者是在對性能沒要求時。建立全文本索引時要特別當心謹慎,內存可能會不夠用(除非你有SSD)。

全文本索引也會致使比「普通」索引更嚴重的性能問題,由於全部字符串都須要被分解、分詞,而且保存到一些地方。所以,可能會發現擁有全文本索引的集合的寫入性能比其餘集合要差。全文本索引也會下降分片時的數據遷移速度:將數據遷移到其餘分片時,全部文本都須要從新進行索引。
寫做本書時,全文本索引仍然只是一個處於「試驗階段」的功能,因此須要專門啓用這個功能才能進行使用。啓動MongoDB時指定--setParameter textSearch Enabled=true選項,或者在運行時執行setParameter命令,均可以啓用全文本索引:

> db.adminCommand({"setParameter" : 1, "textSearchEnabled" : true})

假如咱們使用這個非官方的Hacker News JSON API(http://api.ihackernews.com)將最近的一些文章加載到了MongoDB中。
爲了進行文本搜索,首先須要建立一個"text"索引:

> db.hn.ensureIndex({"title" : "text"})

如今,必須經過text命令才能使用這個索引(寫做本書時,全文本索引還不能用在「普通」查詢中):

test> db.runCommand({"text" : "hn", "search" : "ask hn"})
{
    "queryDebugString" : "ask|hn||||||",
    "language" : "english",
    "results" : [
        {
            "score" : 2.25,
            "obj" : {
                "_id" : ObjectId("50dcab296803fa7e4f000011"),
                "title" : "Ask HN: Most valuable skills you have?",
                "url" : "/comments/4974230",
                "id" : 4974230,
                "commentCount" : 37,
                "points" : 31,
                "postedAgo" : "2 hours ago",
                "postedBy" : "bavidar"
            }
        },
        {
            "score" : 0.5625,
            "obj" : {
                "_id" : ObjectId("50dcab296803fa7e4f000001"),
                "title" : "Show HN: How I turned an old book...",
                "url" : "http://www.howacarworks.com/about",
                "id" : 4974055,
                "commentCount" : 44,
                "points" : 95,
                "postedAgo" : "2 hours ago",
                "postedBy" : "AlexMuir"
            }
        },
        {
            "score" : 0.5555555555555556,
            "obj" : {
                "_id" : ObjectId("50dcab296803fa7e4f000010"),
                "title" : "Show HN: ShotBlocker - iOS Screenshot detector...",
                "url" : "https://github.com/clayallsopp/ShotBlocker",
                "id" : 4973909,    
                "commentCount" : 10,
                "points" : 17,
                "postedAgo" : "3 hours ago",
                "postedBy" : "10char"
        }
    }
],
"stats" : {
    "nscanned" : 4,
    "nscannedObjects" : 0,
    "n" : 3,
    "timeMicros" : 89
},
"ok" : 1 }

匹配到的文檔是按照相關性降序排列的:"Ask HN"位於第一位,而後是兩個部分匹配的文檔。每一個對象前面的"score"字段描述了每一個結果與查詢的匹配程度。
如你所見,這個搜索是不區分大小寫不的,至少對於[a-zA-Z]這些字符是這樣。全文本索引會使用toLower將單詞變爲小寫,但這是與本地化相關的,因此某些語言的用戶可能會發現MongoDB會不可預測性地變得區分大小寫,這取決於toLower在不一樣字符集上的行爲。MongoDB一直在努力提升對不一樣字符集的支持。
全文本索引只會對字符串數據進行索引:其餘的數據類型會被忽略,不會包含在索引中。一個集合上最多隻能有一個全文本索引,可是全文本索引能夠包含多個字段:

> db.blobs.ensureIndex({"title" : "text", "desc" : "text", "author" : "text"})

與「普通」的多鍵索引不一樣,全文本索引中的字段順序不重要:每一個字段都被同等對待。能夠爲每一個字段指定不一樣的權重來控制不一樣字段的相對重要性:

> db.hn.ensureIndex({"title" : "text", "desc" : "text", "author" : "text"},
... {"weights" : {"title" : 3, "author" : 2}})

默認的權重是1,權重的範圍能夠是1~1 000 000 000。使用上面的代碼設置權重以後,"title"字段成爲其中最重要的字段,"author"其次,最後是"desc"(沒有指定,所以它的權重是默認值1)。
索引一經建立,就不能改變字段的權重了(除非刪除索引再重建),因此在生產環境中建立索引以前應該先在測試數據集上實際操做一下。
對於某些集合,咱們可能並不知道每一個文檔所包含的字段。可使用"$**"在文檔的全部字符串字段上建立全文本索引:這不只會對頂級的字符串字段創建索引,也會搜索嵌套文檔和數組中的字符串字段:

> db.blobs.ensureIndex({"$**" : "text"})

也能夠爲"$**"設置權重:

> db.hn.ensureIndex({"whatever" : "text"},
... {"weights" : {"title" : 3, "author" : 1, "$**" : 2}})

"whatever"能夠指代任何東西。在設置權重時指明瞭是對全部字段進行索引,所以MongoDB並不要求你明確給出字段列表。

 搜索語法

默認狀況下,MongoDB會使用OR鏈接查詢中的每一個詞:「ask OR hn」。這是執行全文本查詢最有效的方式,可是也能夠進行短語的精確匹配,以及使用NOT。爲了精確查詢「ask hn」這個短語,能夠用雙引號將查詢內容括起來:

> db.runCommand({text: "hn", search: "\"ask hn\""})
{
    "queryDebugString" : "ask|hn||||ask hn||",
    "language" : "english",
    "results" : [
        {
            "score" : 2.25,
            "obj" : {
                "_id" : ObjectId("50dcab296803fa7e4f000011"),
                "title" : "Ask HN: Most valuable skills you have?",
                "url" : "/comments/4974230",
                "id" : 4974230,
                "commentCount" : 37,
                "points" : 31,
                "postedAgo" : "2 hours ago",
                "postedBy" : "bavidar"
            }
        }
    ],
    "stats" : {
        "nscanned" : 4,
        "nscannedObjects" : 0,
        "n" : 1,
        "nfound" : 1,
        "timeMicros" : 20392
    },
    "ok" : 1
}

這比使用OR的匹配慢一些,由於MongoDB首先要執行一個OR匹配,而後再對匹配結果進行AND匹配。
能夠將查詢字符串的一部分指定爲字面量匹配,另外一部分仍然是普通匹配:

> db.runCommand({text: "hn", search: "\"ask hn\" ipod"})

這會精確搜索"ask hn"這個短語,也會可選地搜索"ipod"。
也可使用"-"字符指定特定的詞不要出如今搜索結果中:

> db.runCommand({text: "hn", search: "-startup vc"})

這樣就會返回匹配「vc」可是不包含「startup」這個詞的文檔。

 優化全文本搜索

有幾種方式能夠優化全文本搜索。若是可以使用某些查詢條件將搜索結果的範圍變小,能夠建立一個由其餘查詢條件前綴和全文本字段組成的複合索引:

> db.blog.ensureIndex({"date" : 1, "post" : "text"})

這就是局部的全文本索引,MongoDB會基於上面例子中的"date"先將搜索範圍分散爲多個比較小的樹。這樣,對於特定日期的文檔進行全文本查詢就會快不少了。
也可使用其餘查詢條件後綴,使索引可以覆蓋查詢。例如,若是要返回"author"和"post"字段,能夠基於這兩個字段建立一個複合索引:

> db.blog.ensureIndex({"post" : "text", "author" : 1})

前綴和後綴形式也能夠組合在一塊兒使用:

> db.blog.ensureIndex({"date" : 1, "post" : "text", "author" : 1})

這裏的前綴索引字段和後綴索引字段都不能夠是多鍵字段。
建立全文本索引會自動在集合上啓用usePowerOf2Sizes選項,這個選項能夠控制空間的分配方式。這個選項可以提升寫入速度,因此不要禁用它。

 在其餘語言中搜索

當一個文檔被插入以後(或者索引第一次被建立以後),MongoDB會查找索引字段,對字符串進行分詞,將其減少爲一個基本單元(essential unit)。而後,不一樣語言的分詞機制是不一樣的,因此必須指定索引或者文檔使用的語言。文本類型的索引容許指定"default_language"選項,它的默認值是"english",能夠被設置爲多種其餘語言(MongoDB的在線文檔提供了最新的支持語言列表)。
例如,要建立一個法語的索引,能夠這麼作:

> db.users.ensureIndex({"profil" : "text", "intérêts" : "text"},
... {"default_language" : "french"})

這樣,這個索引就會默認使用法語的分詞機制,除非指定了其餘的分詞機制。若是在插入文檔時指定"language"字段,就能夠爲每一個文檔分別指定分詞時使用的語言:

> db.users.insert({"username" : "swedishChef",
... "profile" : "Bork de bork", language : "swedish"})
上一篇文章: MongoDB指南---1三、索引類型、索引管理
下一篇文章: MongoDB指南---1五、特殊的索引和集合:地理空間索引、使用GridFS存儲文件
相關文章
相關標籤/搜索