🦉

Go言語でドキュメントDBを作ってみた

2022/07/25に公開


何かしら作ってみたいと思っていた際に、ドキュメントDBをGo言語で実装するという記事を見つけた。

https://notes.eatonphil.com/documentdb.html

読んでるうちに自分も作ってみたくなったので上記を参考にしながらGo言語でドキュメントDBを実装してみた。

作ったもの

最終的に作成したものは下記。

https://github.com/x-color/docdb-in-go

参考にさせていただいた記事の内容に加えて、インデックス付与処理周りやロギングなどに手を加えたものとなっている。
また、クエリのパース処理に関しては、もとの実装だと読みづらく感じたのでほぼ完全にオリジナルにした。

下記のような感じで利用できる。

$ 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に登録している。

https://github.com/x-color/docdb-in-go/blob/15d4f8ddb023c8c531e5d611bac0b97ed6e9bd7b/server/server.go#L31-L48

DB登録処理では下記2つのDBへ登録している。インデックス登録の処理の詳細は後述する。

  • ドキュメントデータを保管するDB
  • インデックス情報を保管するDB

GetDocumentHandler()はパスパラメータとして渡されたIDを用いてドキュメントをDBの中から検索する。
処理としては、IDを用いてDBからデータを読み込んでいるだけなため、詳細は割愛する。

https://github.com/x-color/docdb-in-go/blob/15d4f8ddb023c8c531e5d611bac0b97ed6e9bd7b/server/server.go#L74-L90

SearchDocumentsHandler()はクエリパラメータとして渡された「ドキュメント検索クエリ」を解析し、それを用いてドキュメントをDBの中から検索する。
クエリ解析や検索については後述する。

https://github.com/x-color/docdb-in-go/blob/15d4f8ddb023c8c531e5d611bac0b97ed6e9bd7b/server/server.go#L50-L72

クエリのパース処理

今回の実装では下記のような検索クエリを利用できる。

  • 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()に渡され解析されている。

https://github.com/x-color/docdb-in-go/blob/15d4f8ddb023c8c531e5d611bac0b97ed6e9bd7b/query/query.go#L260-L291

内部では下記を用いて解析している。

https://github.com/x-color/docdb-in-go/blob/15d4f8ddb023c8c531e5d611bac0b97ed6e9bd7b/query/query.go#L35-L48

ここでの処理の流れは下記のようになっている。

  1. 前から順にクエリ文字列を読み込む
  2. 特殊文字を読み込んだら、そこまでの文字列をトークンとする
    • クエリはKey→Operand→Valueの順になっているはずなため、直前の状況からトークンの種類を決定している
  3. クエリの末尾を読み込み終わるまで上記を繰り返す

2番目のトークンの解析処理は下記で実装されている。

https://github.com/x-color/docdb-in-go/blob/15d4f8ddb023c8c531e5d611bac0b97ed6e9bd7b/query/query.go#L50-L75

たとえばa.b:>2 c.d:helloを上記処理で解析すると下記のように分解して返してくる。

  1. type = key, value = a.b
  2. type = operand, value = >
  3. type = value, value = 2
  4. type = key, value = c.d
  5. type = operand, value = =
  6. type = value, value = hello

これらによりトークンの配列が手に入ったら、ParseQuery()はクエリをシステム用の形式に組み直す。

https://github.com/x-color/docdb-in-go/blob/15d4f8ddb023c8c531e5d611bac0b97ed6e9bd7b/query/query.go#L177-L181

これら処理を行うことで、たとえばa.b:>2 c.d:helloは下記クエリとして解釈される。

[
    {
        "Keys": ["a", "b"],
        "Op": ">",
        "Value": "10"
    },
    {
        "Keys": ["c", "d"],
        "Op": "=",
        "Value": "hello"
    }
]

このあとは、このクエリをドキュメント検索に利用する。

実装の詳細は下記ファイルを確認してみてほしい。テストもあるので挙動は把握しやすいはず。

https://github.com/x-color/docdb-in-go/blob/main/query/query.go

インデックスの登録

このシステムでは、ドキュメントを登録する際にインデックス情報も同時に登録している。

https://github.com/x-color/docdb-in-go/blob/15d4f8ddb023c8c531e5d611bac0b97ed6e9bd7b/docdb/docdb.go#L35-L36

たとえば、下記のようなドキュメントの場合。

{
    a: {
        b: 1,
        c: {
            d: 2
        }
    }
}

このとき作成されるインデックスは、2種類ある。
1つ目はドキュメントのキーをつなげて値と一緒にしたもの。もう片方は、ドキュメントのキーをつなげたのみのもの。

  • a.b=1
  • a.c.d=2
  • a.b
  • a.c.d

この処理を行っているのがgetPath()getPathValues()となっている。

https://github.com/x-color/docdb-in-go/blob/15d4f8ddb023c8c531e5d611bac0b97ed6e9bd7b/docdb/docdb.go#L145-L183

値まで入っているインデックスはa.b.:1のような値を一致を利用したクエリに利用する。
それに対して、キーのみのインデックスはa.b:>2のような、範囲検索するクエリに利用している。
参考にした記事では、キーのみのインデックスは実装されていなかった。これがないと範囲検索時には全件検索となってしまうため、今回追加している。

これにより作成されたインデックスを下記処理でDBに記録しておく。

https://github.com/x-color/docdb-in-go/blob/15d4f8ddb023c8c531e5d611bac0b97ed6e9bd7b/docdb/docdb.go#L105-L130

インデックスを利用した検索

記録ができたので、次はインデックスを利用して検索する処理について。
この処理を行う前に検索クエリは解析されているので、それを用いている。

まずは、クエリの種類(一致検索か範囲検索)を確認し、それぞれに応じたインデックス内容を検索する。
一致検索の場合は、クエリのKeysとValueを利用してインデックスと同様の形式(例 a.b=1)のキーを生成する。これを用いてインデックスDBを検索し、一致する物があれはそのIDを記録しておく。

https://github.com/x-color/docdb-in-go/blob/15d4f8ddb023c8c531e5d611bac0b97ed6e9bd7b/docdb/docdb.go#L62-L72

範囲検索の場合は、Keysのみを利用してキー(例 a.b)を生成する。
こちらも同様にインデックスDBを検索し、一致する物があれはIDを記録しておく。

https://github.com/x-color/docdb-in-go/blob/15d4f8ddb023c8c531e5d611bac0b97ed6e9bd7b/docdb/docdb.go#L74-L81

上記で記録されたIDの内、すべてのクエリにヒットしたIDのみを選択する。選ばれたIDを利用してDBからドキュメントを取得する。
これにより、効率的にクエリに一致したドキュメントを検索し取得できる。

https://github.com/x-color/docdb-in-go/blob/15d4f8ddb023c8c531e5d611bac0b97ed6e9bd7b/docdb/docdb.go#L85-L102

実装した感想

ドキュメントDBを初めて実装したので、このようなしくみで検索させるのかと勉強になった。
参考にした記事ではクエリ解析部が複雑(読みやすさより短さを優先した実装?)になっていたので、そこを読み解くのは少々苦戦した。今回は、なるべく読みやすい実装にできたはず。
全体的に実装意欲を満たしてくれる良い題材だった。

今回のものは必要最低限な実装になっているので、いずれはもう少々複雑なドキュメントDBを実装してみたい。

Discussion