DocumentDBでマルチバイト検索を実現する場合の留意事項について
Tags: documentdb, japanese
Published: 2024-04-14 23:12:19

Amazon DocumentDBは使っていますか？

Amazon DocumentDBはMongoDBの互換のドキュメント指向データベースであり、サービスの概要がわかる[BlackBeltの資料](https://pages.awscloud.com/rs/112-TZM-766/images/20190625_AWS_BlackBelt_AmazonDocumentDB_rev.pdf)は2019年のものが最新となっている。

MongoDBの互換となっているものの[サポートされている MongoDB API、オペレーション、およびデータ型](https://docs.aws.amazon.com/ja_jp/documentdb/latest/developerguide/mongo-apis.html)を見ると様々な面で機能差異があることがわかる。

特にマルチバイトを取り扱い場合においてはcollationに対応していないなどの問題があるため、検索要件が一致しているかどうかを検討する必要がある。

私が検証した中で、ドキュメントに記載がされていない部分において、マルチバイト検索においてインデックス適切に利用されず検索性能が大幅に低下する問題に遭遇したので、回避方法ともに紹介する。

今回の検証は、DocumentDB 4.0で行っているが、5.0でも事象が変わらない。

key に対して単一キーのインデックスを事前に作成する
```
rs0 [direct: primary] db1> db.col.createIndex({scope:1, key: 1})
scope_1_key_1
```

その上で、半角文字で開始するコレクションを検索すると4133レコードの結果がある場合でも134 msと高速に応答する。 
```
rs0:PRIMARY> db.col.find({ scope:"xxxxx", key: /^A/ }).hint({scope:1,key:1}).explain("executionStats");
{
        "queryPlanner" : {
                "plannerVersion" : 1,
                "namespace" : "db1.col",
                "winningPlan" : {
                        "stage" : "SUBSCAN",
                        "inputStage" : {
                                "stage" : "SORT",
                                "sortPattern" : {
                                        "key" : 1,
                                        "scope" : 1
                                },
                                "inputStage" : {
                                        "stage" : "FETCH",
                                        "inputStage" : {
                                                "stage" : "IXOR",
                                                "inputStages" : [
                                                        {
                                                                "stage" : "IXSCAN",
                                                                "indexName" : "scope_1_key_1"
                                                        },
                                                        {
                                                                "stage" : "IXSCAN",
                                                                "indexName" : "scope_1_key_1"
                                                        }
                                                ]
                                        }
                                }
                        }
                }
        },
        "executionStats" : {
                "executionSuccess" : true,
                "executionTimeMillis" : "134.866",
                "planningTimeMillis" : "0.632",
                "executionStages" : {
                        "stage" : "SUBSCAN",
                        "nReturned" : "4133",
                        "executionTimeMillisEstimate" : "133.853",
                        "inputStage" : {
                                "stage" : "SORT",
                                "nReturned" : "4133",
                                "executionTimeMillisEstimate" : "132.697",
                                "sortPattern" : {
                                        "key" : 1,
                                        "scope" : 1
                                },
                                "inputStage" : {
                                        "stage" : "FETCH",
                                        "nReturned" : "4133",
                                        "executionTimeMillisEstimate" : "105.506",
                                        "inputStage" : {
                                                "stage" : "IXOR",
                                                "nReturned" : "0",
                                                "executionTimeMillisEstimate" : "34.238",
                                                "inputStages" : [
                                                        {
                                                                "stage" : "IXSCAN",
                                                                "nReturned" : "4133",
                                                                "executionTimeMillisEstimate" : "33.333",
                                                                "indexName" : "scope_1_key_1"
                                                        },
                                                        {
                                                                "stage" : "IXSCAN",
                                                                "nReturned" : "0",
                                                                "executionTimeMillisEstimate" : "0.902",
                                                                "indexName" : "scope_1_key_1"
                                                        }
                                                ]
                                        }
                                }
                        }
                }
        },
        "serverInfo" : {
                "host" : "xxxxxxx",
                "port" : 27017,
                "version" : "4.0.0"
        },
        "ok" : 1,
        "operationTime" : Timestamp(1703472746, 1)
}
```

しかしながら、全角文字で開始するコレクションの検索を行った場合、先ほどの条件に対して2.７２倍の11266レコードの結果がある場合に、応答時間が153倍の 20518 msもかかる。

```
rs0:PRIMARY> db.col.find({ scope:"xxxxx", key: /^あ/ }).hint({scope:1,key:1}).explain("executionStats");
{
        "queryPlanner" : {
                "plannerVersion" : 1,
                "namespace" : "db1.col",
                "winningPlan" : {
                        "stage" : "SUBSCAN",
                        "inputStage" : {
                                "stage" : "IXSCAN",
                                "indexName" : "scope_1_key_1",
                                "direction" : "forward"
                        }
                }
        },
        "executionStats" : {
                "executionSuccess" : true,
                "executionTimeMillis" : "20518.276",
                "planningTimeMillis" : "0.303",
                "executionStages" : {
                        "stage" : "SUBSCAN",
                        "nReturned" : "11266",
                        "executionTimeMillisEstimate" : "20517.382",
                        "inputStage" : {
                                "stage" : "IXSCAN",
                                "nReturned" : "11266",
                                "executionTimeMillisEstimate" : "20514.086",
                                "indexName" : "scope_1_key_1",
                                "direction" : "forward"
                        }
                }
        },
        "serverInfo" : {
                "host" : "xxxxxxx",
                "port" : 27017,
                "version" : "4.0.0"
        },
        "ok" : 1,
        "operationTime" : Timestamp(1703472782, 1)
}
```

対応方法としては、正規表現を利用せずに文字コード範囲の検索によって実現することで応答時間を早めることができる。

db.col.find({ scope:"xxxxx", key: {$gte: "あ", $lt: String.fromCharCode("あ".charCodeAt(0)+1) } })

```
rs0:PRIMARY> db.col.find({ scope:"xxxxx", key: {$gte: "あ", $lt: "ぃ"}}).hint({scope:1,key:1}).explain("executionStats");
{
        "queryPlanner" : {
                "plannerVersion" : 1,
                "namespace" : "db1.col",
                "winningPlan" : {
                        "stage" : "SUBSCAN",
                        "inputStage" : {
                                "stage" : "IXSCAN",
                                "indexName" : "scope_1_key_1",
                                "direction" : "forward"
                        }
                }
        },
        "executionStats" : {
                "executionSuccess" : true,
                "executionTimeMillis" : "367.233",
                "planningTimeMillis" : "0.499",
                "executionStages" : {
                        "stage" : "SUBSCAN",
                        "nReturned" : "11266",
                        "executionTimeMillisEstimate" : "366.099",
                        "inputStage" : {
                                "stage" : "IXSCAN",
                                "nReturned" : "11266",
                                "executionTimeMillisEstimate" : "362.641",
                                "indexName" : "scope_1_key_1",
                                "direction" : "forward"
                        }
                }
        },
        "serverInfo" : {
                "host" : "xxxxxxx",
                "port" : 27017,
                "version" : "4.0.0"
        },
        "ok" : 1,
        "operationTime" : Timestamp(1703472821, 1)
}
```