サジェストの速度改善をしてみた
はじめに
こんにちは。ミスミグループ本社Gateway推進本部の高島です。
当部門で使用するデータ整備を担当しております。
新規プロダクトの試作モデルを開発しているなかで、DBのデータ増に伴いクエリ実行結果が格段に遅くなる事象が発生しました。
データ要件や機能が固まっていないため、取り急ぎの速度改善を実施しました。
本記事ではその方法をご紹介します。
改善前の構成
クライアントからの入力キーワードに基づき、生産間接材の型番候補を付帯情報とともに返却します。
企画段階のため、柔軟に対応でき簡易なプロトタイプとしてDocumentDBを選定し構成しています。
不具合と対応策
不具合
データを大量追加したところ、クライアントへの型番候補表示において以下の不具合が発生しました。
- レイテンシーが10倍劣化
- タイムアウトにより型番候補の表示無し
型番候補表示までのレイテンシー
入力キーワード | データ追加前 | データ追加後 | |
---|---|---|---|
LMH8S | 1,858ms | ➡ | 22,485ms |
WJ-75S | 2,153ms | ➡ | 26,040ms |
TGH-75 | 2,156ms | ➡ | 26,028ms |
KR30XH | 2,159ms | ➡ | 26,111ms |
P24-2GT-4-33F-AL-WA | データなし | ➡ | タイムアウト |
原因
データ量増により、型番候補の抽出で使用している前方一致クエリ*の返却時間が遅くなることが主な原因でした。
※ここでの前方一致クエリとは、文字列の先頭部分が特定のパターンと一致するデータを抽出することを意味します。
例)クライアントからの入力キーワードが'ABCDEF'の場合に、'ABCDEF'、'ABCDE'、'ABCD'と末尾文字を削っていき型番情報を抽出できるまでDBへ問い合わせをします。
DBデータ量の変化
データ量 | データ追加前 | データ追加後 | |
---|---|---|---|
テーブルサイズ | 30MB | ➡ | 634MB |
件数 | 137千件 | ➡ | 2,841千件 |
対応策
DocumentDBのスケールアップを試みましたが、クエリ返却時間には効果がありませんでした。
そこで、インメモリキャッシュを利用し、DocumentDBでの前方一致クエリを行わないように対応しました。
キャッシュからキーワード検索し、DocumentDBのObjectIdを取得します。取得したObjectIdをKEYに型番情報を抽出する方法に変更しました。
- KEY:DocumentDBのcode(型番)をN-gramで文字数パターン分*
- VALUE:DocumentDBの_id(ObjectId)
※N-gramは自然言語処理でよく使われる基本的な手法です。
構成された文章の特徴を定量的に調べるために、連続するn個の単語や文字を纏めます。
今回は以下のイメージ
'ABCDEF' => ['A', 'AB', 'ABC', 'ABCD', 'ABCDE', 'ABCDEF']
キャッシュDBの作成
Amazon ElastiCacheのダッシュボードの「キャッシュを作成」からRedis OSSを選択し、独自のキャッシュを設計で作成しました。
EC2との疎通設定のため、セキュリティグループのインバウンドルールを設定しました。
キャッシュの登録
DocumentDB内BBコレクションの型番をN-gram処理したリストとObjectIdをキャッシュ登録しています。
from pymongo import MongoClient
from rediscluster import RedisCluster
import sys
## BBコレクションから"_id"と"code"を取得
mongo = MongoClient("mongodb://●●●:27017/?replicaSet=rs0&readPreference=secondaryPreferred&retryWrites=false")
codes = mongo['AAA']['BB'].find(projection={"_id":True, "code":True})
## 最小・最大連続文字数
len_egrams_min = 3
len_egrams_max = 40
## Redisアクセスクラスの取得
redis = RedisCluster(startup_nodes=[{"host": "▲▲▲.amazonaws.com","port": "6379"}], decode_responses=True,skip_full_coverage_check=True)
## 型番をテキスト分解したリストからキャッシュ登録
for detail in codes:
# 型番の取得
code = detail['code']
# ObjectIdの取得
value = str(detail['_id'])
# 型番をテキスト分解したリストを取得
keys = make_egrams(code, len_egrams_min, len_egrams_max)
# キャッシュ登録
for key in keys:
redis.sadd(key, value)
指定された文字列について、文字数パターン分テキスト分解を行う関数です。
def make_egrams(code: str, pos_starts_at: int, pos_ends_at: int):
"""
【引数】
code:対象文字列
pos_starts_at:最小連続文字数
pos_ends_at:最大連続文字数
【返値】
テキスト分解したリスト
"""
# 連続文字数の取得
try:
starts_at = pos_starts_at if 0 < pos_starts_at else 0
ends_at = pos_ends_at+1 if pos_ends_at+1 < len(code)+1 else len(code)+1
if ends_at < starts_at:
raise ValueError("Start position must be less than end position.")
except Exception as e:
print(f"Error generating range: {e}")
return []
# 連続文字数パターンの文字列取得
egrams=[]
for i in range(starts_at, ends_at):
egrams.append(code[0:i])
return egrams
改善後の構成
キーワードの検索先はElastiCacheに変更(DocumentDBのObjectId取得)、その後DocumentDBから型番情報を取得します。
おわりに
今回は既存のシステム構成を大きく変更しない対応方法を用いました。
対応した結果、クライアントの型番候補表示が大幅に改善されましたが、課題もうまれました。
結果
型番候補表示までのレイテンシー
入力キーワード | 改善前(不具合) | 改善後 | |
---|---|---|---|
LMH8S | 22,485ms | ➡ | 117ms |
WJ-75S | 26,040ms | ➡ | 109ms |
TGH-75 | 26,028ms | ➡ | 120ms |
KR30XH | 26,111ms | ➡ | 103ms |
P24-2GT-4-33F-AL-WA | タイムアウト | ➡ | 83ms |
課題
キャッシュ登録しているN-gramの文字数パターンを絞っている(型番の一部文字列 3~40文字)ため、 型番が40文字以上の場合に入力キーワードとの完全一致ができない。
現在、試作モデルの磨き込みでアーキテクチャの見直しをしています、別の機会に紹介できればと思います。
Discussion