MongoDB指南---15、特殊的索引和集合:地理空間索引、使用GridFS存儲文件

上一篇文章:MongoDB指南---14、特殊的索引和集合:固定集合、TTL索引、全文本索引
下一篇文章:

地理空間索引

MongoDB支持幾種類型的地理空間索引。其中最常用的是2dsphere索引(用於地球表面類型的地圖)和2d索引(用於平面地圖和時間連續的數據)。
2dsphere允許使用GeoJSON格式(http://www.geojson.org)指定點、線和多邊形。點可以用形如[longitude, latitude]([經度,緯度])的兩個元素的數組表示:

{
    "name" : "New York City",
    "loc" : {
        "type" : "Point",
        "coordinates" : [50, 2] 
    }
}

線可以用一個由點組成的數組來表示:

{
    "name" : "Hudson River",
    "loc" : {
        "type" : "Line",
        "coordinates" : [[0,1], [0,2], [1,2]]
    }
}

多邊形的表示方式與線一樣(都是一個由點組成的數組),但是"type"不同:

{
    "name" : "New England",
    "loc" : {
        "type" : "Polygon",
        "coordinates" : [[0,1], [0,2], [1,2]]
    }
}

"loc"字段的名字可以是任意的,但是其中的子對象是由GeoJSON指定的,不能改變。
在ensureIndex中使用"2dsphere"選項就可以創建一個地理空間索引:

> db.world.ensureIndex({"loc" : "2dsphere"}) 

地理空間查詢的類型

可以使用多種不同類型的地理空間查詢:交集(intersection)、包含(within)以及接近(nearness)。查詢時,需要將希望查找的內容指定爲形如{"$geometry" : geoJsonDesc}的GeoJSON對象。
例如,可以使用"$geoIntersects"操作符找出與查詢位置相交的文檔:

> var eastVillage = {
... "type" : "Polygon",
... "coordinates" : [
... [-73.9917900, 40.7264100],
... [-73.9917900, 40.7321400],
... [-73.9829300, 40.7321400],
... [-73.9829300, 40.7264100]
... ]}
> db.open.street.map.find(
... {"loc" : {"$geoIntersects" : {"$geometry" : eastVillage}}})

這樣就會找到所有與East Village區域有交集的文檔。
可以使用"$within"查詢完全包含在某個區域的文檔,例如:“East Village有哪些餐館?”

> db.open.street.map.find({"loc" : {"$within" : {"$geometry" : eastVillage}}})

與第一個查詢不同,這次不會返回那些只是經過East Village(比如街道)或者部分重疊(比如用於表示曼哈頓的多邊形)的文檔。
最後,可以使用"$near"查詢附近的位置:

> db.open.street.map.find({"loc" : {"$near" : {"$geometry" : eastVillage}}})

注意,"$near"是唯一一個會對查詢結果進行自動排序的地理空間操作符:"$near"的返回結果是按照距離由近及遠排序的。
地理位置查詢有一點非常有趣:不需要地理空間索引就可以使用"$geoIntersects"或者"$within"("$near"需要使用索引)。但是,建議在用於表示地理位置的字段上建立地理空間索引,這樣可以顯著提高查詢速度。

 複合地理空間索引

如果有其他類型的索引,可以將地理空間索引與其他字段組合在一起使用,以便對更復雜的查詢進行優化。上面提到過一種可能的查詢:“East Village有哪些餐館?”。如果僅僅使用地理空間索引,我們只能查找到East Village內的所有東西,但是如果要將“restaurants”或者是“pizza”單獨查詢出來,就需要使用其他索引中的字段了:

> db.open.street.map.ensureIndex({"tags" : 1, "location" : "2dsphere"})

然後就能夠很快地找到East Village內的披薩店了:

> db.open.street.map.find({"loc" : {"$within" : {"$geometry" : eastVillage}},
... "tags" : "pizza"})

其他索引字段可以放在"2dsphere"字段前面也可以放在後面,這取決於我們希望首先使用其他索引的字段進行過濾還是首先使用位置進行過濾。應該將那個能夠過濾掉儘可能多的結果的字段放在前面。

 2D索引

對於非球面地圖(遊戲地圖、時間連續的數據等),可以使用"2d"索引代替"2dsphere":

> db.hyrule.ensureIndex({"tile" : "2d"})

"2d"索引用於扁平表面,而不是球體表面。"2d"索引不應該用在球體表面上,否則極點附近會出現大量的扭曲變形。
文檔中應該使用包含兩個元素的數組表示2d索引字段(寫作本書時,這個字段還不是GeoJSON文檔)。示例如下:

{
    "name" : "Water Temple",
    "tile" : [ 32, 22 ]
}

"2d"索引只能對點進行索引。可以保存一個由點組成的數組,但是它只會被保存爲由點組成的數組,不會被當成線。特別是對於"$within"查詢來說,這是一項重要的區別。如果將街道保存爲由點組成的數組,那麼如果其中的某個點位於給定的形狀之內,這個文檔就會與$within相匹配。但是,由這些點組成的線並不一定完全包含在這個形狀之內。
默認情況下,地理空間索引是假設你的值都介於-180~180。可以根據需要在ensureIndex中設置更大或者更小的索引邊界值:

> db.star.trek.ensureIndex({"light-years" : "2d"}, {"min" : -1000, "max" : 1000})

這會創建一個2000×2000大小的空間索引。
使用"2d"索引進行查詢比使用"2dsphere"要簡單許多。可以直接使用"$near"或者"$within",而不必帶有"$geometry"子對象。可以直接指定座標:

> db.hyrule.find({"tile" : {"$near" : [20, 21]}})

這樣會返回hyrule集合內的全部文檔,按照距離(20,21)這個點的距離排序。如果沒有指定文檔數量限制,默認最多返回100個文檔。如果不需要這麼多結果,應該根據需要設置返回文檔的數量以節省服務器資源。例如,下面的代碼只會返回距離(20,21)最近的10個文檔:

> db.hyrule.find({"tile" : {"$near" : [20, 21]}}).limit(10)

"$within"可以查詢出某個形狀(矩形、圓形或者是多邊形)範圍內的所有文檔。如果要使用矩形,可以指定"$box"選項:

> db.hyrule.find({"tile" : {"$within" : {"$box" : [[10, 20], [15, 30]]}}})

"$box"接受一個兩元素的數組:第一個元素指定左下角的座標,第二個元素指定右上角的座標。
類似地,可以使用"$center"選項返回圓形範圍內的所有文檔,這個選項也是接受一個兩元素數組作爲參數:第一個元素是一個點,用於指定圓心;第二個參數用於指定半徑:

> db.hyrule.find({"tile" : {"$within" : {"$center" : [[12, 25], 5]}}})

還可以使用多個點組成的數組來指定多邊形:

> db.hyrule.find(
... {"tile" : {"$within" : {"$polygon" : [[0, 20], [10, 0], [-10, 0]]}}})

這個例子會查詢出包含給定三角形內的點的所有文檔。列表中的最後一個點會被連接到第一個點,以便組成多邊形。

使用GridFS存儲文件

GridFS是MongoDB的一種存儲機制,用來存儲大型二進制文件。下面列出了使用GridFS作爲文件存儲的理由。

  • 使用GridFS能夠簡化你的棧。如果已經在使用MongoDB,那麼可以使用GridFS來代替獨立的文件存儲工具。
  • GridFS會自動平衡已有的複製或者爲MongoDB設置的自動分片,所以對文件存儲做故障轉移或者橫向擴展會更容易。
  • 當用於存儲用戶上傳的文件時,GridFS可以比較從容地解決其他一些文件系統可能會遇到的問題。例如,在GridFS文件系統中,如果在同一個目錄下存儲大量的文件,沒有任何問題。
  • 在GridFS中,文件存儲的集中度會比較高,因爲MongoDB是以2 GB爲單位來分配數據文件的。

GridFS也有一些缺點。

  • GridFS的性能比較低:從MongoDB中訪問文件,不如直接從文件系統中訪問文件速度快。
  • 如果要修改GridFS上的文檔,只能先將已有文檔刪除,然後再將整個文檔重新保存。MongoDB將文件作爲多個文檔進行存儲,所以它無法在同一時間對文件中的所有塊加鎖。

通常來說,如果你有一些不常改變但是經常需要連續訪問的大文件,那麼使用GridFS再合適不過了。

 GridFS入門

使用GridFS最簡單的方式是使用mongofiles工具。所有的MongoDB發行版中都包含了mongofiles,可以用它在GridFS中上傳文件、下載文件、查看文件列表、搜索文件,以及刪除文件。
與其他的命令行工具一樣,運行mongofiles --help就可以查看它的可用選項了。
在下面這個會話中,首先用mongofiles從文件系統中上傳一個文件到GridFS,然後列出GridFS中的所有文件,最後再將之前上傳過的文件從GridFS中下載下來:

$ echo "Hello, world" > foo.txt
$ ./mongofiles put foo.txt
connected to: 127.0.0.1
added file: { _id: ObjectId('4c0d2a6c3052c25545139b88'),
                filename: "foo.txt", length: 13, chunkSize: 262144,
                uploadDate: new Date(1275931244818),
                md5: "a7966bf58e23583c9a5a4059383ff850" }
done!
$ ./mongofiles list
connected to: 127.0.0.1
foo.txt 13
$ rm foo.txt
$ ./mongofiles get foo.txt
connected to: 127.0.0.1
done write to: foo.txt
$ cat foo.txt
Hello,world

在上面的例子中,使用mongofiles執行了三種基本操作:put、list和get。put操作可以將文件系統中選定的文件上傳到GridFS;list操作可以列出GridFS中的文件;get操作與put相反,用於將GridFS中的文件下載到文件系統中。mongofiles還支持另外兩種操作:用於在GridFS中搜索文件的search操作和用於從GridFS中刪除文件的delete操作。

 在MongoDB驅動程序中使用GridFS

所有客戶端驅動程序都提供了GridFS API。例如,可以用PyMongo(MongoDB的Python驅動程序)執行與上面直接使用mongofiles一樣的操作:

>>> from pymongo import Connection
>>> import gridfs
>>> db = Connection().test
>>> fs = gridfs.GridFS(db)
>>> file_id = fs.put("Hello, world", filename="foo.txt")
>>> fs.list()
[u'foo.txt']
>>> fs.get(file_id).read()
'Hello, world'

PyMongo中用於操作GridFS的API與mongofiles非常像:可以很方便地執行put、get和list操作。幾乎所有MongoDB驅動程序都遵循這種基本模式對GridFS進行操作,當然通常也會提供一些更高級的功能。關於特定驅動程序對GridFS的操作,可以查詢相關驅動程序的文件。

 揭開GridFS的面紗

GridFS是一種輕量級的文件存儲規範,用於存儲MongoDB中的普通文檔。MongoDB服務器幾乎不會對GridFS請求做“特殊”處理,所有處理都由客戶端的驅動程序和工具負責。
GridFS背後的理念是:可以將大文件分割爲多個比較大的塊,將每個塊作爲獨立的文檔進行存儲。由於MongoDB支持在文檔中存儲二進制數據,所以可以將塊存儲的開銷降到非常低。除了將文件的每一個塊單獨存儲之外,還有一個文檔用於將這些塊組織在一起並存儲該文件的元信息。
GridFS中的塊會被存儲到專用的集合中。塊默認使用的集合是fs.chunks,不過可以修改爲其他集合。在塊集合內部,各個文檔的結構非常簡單:

{
    "_id" : ObjectId("..."),
    "n" : 0,
    "data" : BinData("..."),
    "files_id" : ObjectId("...")
}

與其他的MongoDB文檔一樣,塊也都擁有一個唯一的"_id"。另外,還有如下幾個鍵。

  • "files_id"

塊所屬文件的元信息。

  • "n"

塊在文件中的相對位置。

  • "data"

塊所包含的二進制數據。

每個文件的元信息被保存在一個單獨的集合中,默認情況下這個集合是fs.files。這個文件集合中的每一個文檔表示GridFS中的一個文件,文檔中可以包含與這個文件相關的任意用戶自定義元信息。除用戶自定義的鍵之外,還有幾個鍵是GridFS規範規定必須要有的。

  • "_id"

文件的唯一id,這個值就是文件的每個塊文檔中"files_id"的值。

  • "length"

文件所包含的字節數。

  • "chunkSize"

組成文件的每個塊的大小,單位是字節。這個值默認是256 KB,可以在需要時進行調整。

  • "uploadDate"

文件被上傳到GridFS的日期。

  • "md5"

文件內容的md5校驗值,這個值由服務器端計算得到。

這些必須字段中最有意思(或者說能夠見名知意)的一個可能是"md5"。"md5"字段的值是由MongoDB服務器使用filemd5命令得到的,這個命令可以用來計算上傳到GridFS的塊的md5校驗值。這意味着,用戶可以通過檢查文件的md5校驗值來確保文件上傳正確。
如上面所說,在fs.files中,除了這些必須字段外,可以使用任何自定義的字段來保存必需的文件元信息。可能你希望在文件元信息中保存文件的下載次數、MIME類型或者用戶評分。
只要理解了GridFS底層的規範,自己就可以很容易地實現一些驅動程序沒有提供的輔助功能。例如,可以使用distinct命令得到GridFS中保存文件的文件名集合(集合中的每個文件名都是唯一的)。

> db.fs.files.distinct("filename")
[ "foo.txt" , "bar.txt" , "baz.txt" ]

這樣,在加載或者收集文件相關信息時,應用程序可以擁有非常大的靈活性.

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章