Go言語でドキュメントDBを作ってみた
何かしら作ってみたいと思っていた際に、ドキュメントDBをGo言語で実装するという記事を見つけた。
読んでるうちに自分も作ってみたくなったので上記を参考にしながらGo言語でドキュメントDBを実装してみた。
作ったもの
最終的に作成したものは下記。
参考にさせていただいた記事の内容に加えて、インデックス付与処理周りやロギングなどに手を加えたものとなっている。
また、クエリのパース処理に関しては、もとの実装だと読みづらく感じたのでほぼ完全にオリジナルにした。
下記のような感じで利用できる。
$ go run main.go
$ curl -X POST \
-H 'Content-Type: application/json' \
-d '{"id": "1", "name": "bookA", "detail": {"price": 100,"description": "this is sample book"}}' \
http://localhost:8080/docs
{"id":"c759b15f-131e-41d6-af3c-5680c8f1ea11"}
$ curl -X POST \
-H 'Content-Type: application/json' \
-d '{"id": "2", "name": "bookB", "detail": {"price": 200,"description": "this is sample book"}}' \
http://localhost:8080/docs
{"id":"23a96578-e900-424f-a73f-808ff15d0823"}
$ curl -s http://localhost:8080/docs/23a96578-e900-424f-a73f-808ff15d0823 | jq
{
"detail": {
"description": "this is sample book",
"price": 200
},
"id": "2",
"name": "bookB"
}
$ curl --get -s http://localhost:8080/docs --data-urlencode 'q=name:"bookA"' | jq
{
"count": 1,
"documents": [
{
"document": {
"detail": {
"description": "this is sample book",
"price": 100
},
"id": "1",
"name": "bookA"
},
"id": "c759b15f-131e-41d6-af3c-5680c8f1ea11"
}
]
}
$ curl --get -s http://localhost:8080/docs --data-urlencode 'q=detail.price:>150' | jq
{
"count": 1,
"documents": [
{
"document": {
"detail": {
"description": "this is sample book",
"price": 200
},
"id": "2",
"name": "bookB"
},
"id": "23a96578-e900-424f-a73f-808ff15d0823"
}
]
}
$ curl --get -s http://localhost:8080/docs --data-urlencode 'q=detail.description:"this is sample book"' | jq
{
"count": 2,
"documents": [
{
"document": {
"detail": {
"description": "this is sample book",
"price": 100
},
"id": "1",
"name": "bookA"
},
"id": "c759b15f-131e-41d6-af3c-5680c8f1ea11"
},
{
"document": {
"detail": {
"description": "this is sample book",
"price": 200
},
"id": "2",
"name": "bookB"
},
"id": "23a96578-e900-424f-a73f-808ff15d0823"
}
]
}
どのような実装になっているか
リクエストハンドラ
ハンドラは、下記3つを実装している。
AddDocumentHandle()
GetDocumentHandler()
SearchDocumentsHandler()
AddDocumentHandle()
はJSON形式で受け取ったドキュメント情報をDBに登録する。
受け取ったドキュメントデータをデコードし、DBに登録している。
DB登録処理では下記2つのDBへ登録している。インデックス登録の処理の詳細は後述する。
- ドキュメントデータを保管するDB
- インデックス情報を保管するDB
GetDocumentHandler()
はパスパラメータとして渡されたIDを用いてドキュメントをDBの中から検索する。
処理としては、IDを用いてDBからデータを読み込んでいるだけなため、詳細は割愛する。
SearchDocumentsHandler()
はクエリパラメータとして渡された「ドキュメント検索クエリ」を解析し、それを用いてドキュメントをDBの中から検索する。
クエリ解析や検索については後述する。
クエリのパース処理
今回の実装では下記のような検索クエリを利用できる。
-
a.b:1
:a.b
にセットされている値が1
である -
a.b:>2 c.d:hello
:a.b
にセットされている値が2
より大きい、かつ、c.d
にセットされている値がhello
である -
a.b:"hello world"
:a.b
にセットされている値がhello world
である
これらクエリはParseQuery()
に渡され解析されている。
内部では下記を用いて解析している。
ここでの処理の流れは下記のようになっている。
- 前から順にクエリ文字列を読み込む
- 特殊文字を読み込んだら、そこまでの文字列をトークンとする
- クエリはKey→Operand→Valueの順になっているはずなため、直前の状況からトークンの種類を決定している
- クエリの末尾を読み込み終わるまで上記を繰り返す
2番目のトークンの解析処理は下記で実装されている。
たとえばa.b:>2 c.d:hello
を上記処理で解析すると下記のように分解して返してくる。
- type =
key
, value =a.b
- type =
operand
, value =>
- type =
value
, value =2
- type =
key
, value =c.d
- type =
operand
, value ==
- type =
value
, value =hello
これらによりトークンの配列が手に入ったら、ParseQuery()
はクエリをシステム用の形式に組み直す。
これら処理を行うことで、たとえばa.b:>2 c.d:hello
は下記クエリとして解釈される。
[
{
"Keys": ["a", "b"],
"Op": ">",
"Value": "10"
},
{
"Keys": ["c", "d"],
"Op": "=",
"Value": "hello"
}
]
このあとは、このクエリをドキュメント検索に利用する。
実装の詳細は下記ファイルを確認してみてほしい。テストもあるので挙動は把握しやすいはず。
インデックスの登録
このシステムでは、ドキュメントを登録する際にインデックス情報も同時に登録している。
たとえば、下記のようなドキュメントの場合。
{
a: {
b: 1,
c: {
d: 2
}
}
}
このとき作成されるインデックスは、2種類ある。
1つ目はドキュメントのキーをつなげて値と一緒にしたもの。もう片方は、ドキュメントのキーをつなげたのみのもの。
- a.b=1
- a.c.d=2
- a.b
- a.c.d
この処理を行っているのがgetPath()
とgetPathValues()
となっている。
値まで入っているインデックスはa.b.:1
のような値を一致を利用したクエリに利用する。
それに対して、キーのみのインデックスはa.b:>2
のような、範囲検索するクエリに利用している。
参考にした記事では、キーのみのインデックスは実装されていなかった。これがないと範囲検索時には全件検索となってしまうため、今回追加している。
これにより作成されたインデックスを下記処理でDBに記録しておく。
インデックスを利用した検索
記録ができたので、次はインデックスを利用して検索する処理について。
この処理を行う前に検索クエリは解析されているので、それを用いている。
まずは、クエリの種類(一致検索か範囲検索)を確認し、それぞれに応じたインデックス内容を検索する。
一致検索の場合は、クエリのKeysとValueを利用してインデックスと同様の形式(例 a.b=1
)のキーを生成する。これを用いてインデックスDBを検索し、一致する物があれはそのIDを記録しておく。
範囲検索の場合は、Keysのみを利用してキー(例 a.b
)を生成する。
こちらも同様にインデックスDBを検索し、一致する物があれはIDを記録しておく。
上記で記録されたIDの内、すべてのクエリにヒットしたIDのみを選択する。選ばれたIDを利用してDBからドキュメントを取得する。
これにより、効率的にクエリに一致したドキュメントを検索し取得できる。
実装した感想
ドキュメントDBを初めて実装したので、このようなしくみで検索させるのかと勉強になった。
参考にした記事ではクエリ解析部が複雑(読みやすさより短さを優先した実装?)になっていたので、そこを読み解くのは少々苦戦した。今回は、なるべく読みやすい実装にできたはず。
全体的に実装意欲を満たしてくれる良い題材だった。
今回のものは必要最低限な実装になっているので、いずれはもう少々複雑なドキュメントDBを実装してみたい。
Discussion