feature image

2021年4月2日 | ブログ記事

traQの検索機能が謎のエラーを吐いた話

こんにちは。19BのSysAd班のtokiです。

この記事はSysAdTechBlogの第4回目の記事です。
先日traQに検索機能を実装していたら、突然謎のエラーを吐いてしまった話をします。

traQの検索機能

traQにメッセージ検索機能を搭載する案は昔からありましたが、つい先日、2021年3月13日にこれを実装したクライアントv3.8.0とサーバーv3.6.0がリリースされました。

サーバー側の検索機能はElasticsearchを用いて実装されています。リリース当初は良い感じに動いているかのように見えたのですが、すぐに日本語検索が上手く機能しないことに気づきました。

日本語検索が上手くいかない例

上の例では、「かわいい」という単語が「か」「わ」「い」「い」に分割(tokenize)されて、「か & わ & い & い」で検索がかかってしまっています。Elasticsearchはデフォルトでは日本語テキストのtokenizeとanalyzeを上手くやってくれません。

ちなみに「"かわいい"」といった感じにdouble quote"で囲むと、完全一致での検索はしてくれます。
が、単語の活用や表記ゆれなどもあるので、流石にこれでは使い勝手が悪そうです。

そこで、日本語tokenizerであるSudachiと、そのElasticsearchプラグインを使用するように、以下のプルリクエストが出されました。

ESのtokenizerをsudachiに by FujishigeTemma · Pull Request #1106 · traPtitech/traQ

これでいい感じに日本語のメッセージを検索できるぞ!と、検証段階に入ったときに事件は起きました。

java.lang.StackOverflowError

本番環境に実際に存在する、約130万メッセージのコピーが手元にいい感じにあった[1]ので、これらを実際にElasticsearchに食わせてみることにしました。

が、メッセージを食わせている途中で突然Elasticsearchが以下のようなエラーを吐いて落ちてしまいました。

{"type": "server", "timestamp": "2021-03-18T06:08:03,325Z", "level": "ERROR", "component": "o.e.b.ElasticsearchUncaughtExceptionHandler", "cluster.name": "docker-cluster", "node.name": "cc43cd0817aa", "message": "fatal error in thread [elasticsearch[cc43cd0817aa][write][T#1]], exiting", "cluster.uuid": "ABZQm9QQQq2YQPyigw68_w", "node.id": "fl2YrnfBRHyo45FEFNej0w" , 
"stacktrace": ["java.lang.StackOverflowError: null",
"at java.util.regex.Pattern$Branch.match(Pattern.java:4800) ~[?:?]",
"at java.util.regex.Pattern$GroupHead.match(Pattern.java:4855) ~[?:?]",
"at java.util.regex.Pattern$Loop.match(Pattern.java:4964) ~[?:?]",
"at java.util.regex.Pattern$GroupTail.match(Pattern.java:4886) ~[?:?]",
"at java.util.regex.Pattern$BranchConn.match(Pattern.java:4763) ~[?:?]",
"at java.util.regex.Pattern$BmpCharProperty.match(Pattern.java:4020) ~[?:?]",
"at java.util.regex.Pattern$Branch.match(Pattern.java:4800) ~[?:?]",

Sudachiプラグインを入れる前は同じメッセージを食わせても何もエラーが出ていなかったことから、Sudachiプラグインの何かが悪いということはすぐに分かりました。
ですが、「StackOverflow」?「java.util.regex」...?
これらのエラーに、当初は全く心当たりがありませんでした。


  1. traQ本体に検索機能が実装される前に、簡易的な検索機能をBotにて実装していたため、メッセージのコピーがありました。 ↩︎

コーナーケース

調査を進めていくと、特定のメッセージを食わせると上のエラーを吐いて落ちることが分かりました。

そのメッセージが以下のものです。

お前か!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!

確かに、如何にもコーナーケースっぽいメッセージですね。その感嘆符「!」の数、驚愕の21,828個。
何かが「StackOverflow」したのは納得できそうです。でも何が?

ちょっと正規表現のお話

先程のエラーに「java.util.regex」とありましたが、これを元に更に調査を進めていくと、以下の部分が原因であることが分かりました。

WorksApplications/Sudachi
A Japanese Tokenizer for Business. Contribute to WorksApplications/Sudachi development by creating an account on GitHub.
原因箇所

どうやら、Javaの正規表現では(!|?)*といったalternationのgreedyなループは関数の再帰的な呼び出しでのマッチ処理に書き換わるようで、これが深すぎるとStackOverflowErrorを起こします。
より簡単なコードに還元すると、以下のようになります。

    // "!" x 10,000
    String s = String.join("", Collections.nCopies(10000, "!"));
    // Bad: causes java.lang.StackOverflowError
    Pattern p = Pattern.compile("(!|?)*");
    Matcher m = p.matcher(s);
    if (m.find()) {
        System.out.println("Match found!");
    }

今回は一文字のalternationだったので、素直にcharacter classを使うように書き換えました。
こうすることで、StackOverflowErrorは起こらなくなります。

    // "!" x 10,000
    String s = String.join("", Collections.nCopies(10000, "!"));
    // Good
    Pattern p = Pattern.compile("[!?]*");
    Matcher m = p.matcher(s);
    if (m.find()) {
        System.out.println("Match found!");
    }

ちなみに私が使っているエディタであるIntelliJ IDEAは、「一文字のalternationはcharacter classに書き換えろ」と怒ってくれます。偉いですね。

[!?]*とするべき

修正、そしてデプロイへ・・・

一文字のalternationをcharacter classを使うように書き換えたプルリクエストをSudachiに出しました。

Fix SentenceDetector causing StackOverflowError by motoki317 · Pull Request #148 · WorksApplications/Sudachi
Hello,This pull request fixes the issue that SentenceDetector was causing StackOverflowError when it was fed with particular strings, such as strings followed by a lot of punctuations (!, about 20...

比較的すぐにマージされたので嬉しいです。

2021年4月2日現在、この修正を含んだバージョンのリリースはまだされていないので、自分達で使う分は修正済みのソースをビルドして使っています。

近日、本番環境のtraQにもこの変更が入り、日本語検索ができるようになる予定です。お楽しみに!

toki icon
この記事を書いた人
toki

Java, Go, JS/TSなどをいじっています

この記事をシェア

このエントリーをはてなブックマークに追加
共有

関連する記事

2024年9月20日
2024年 1-Monthonを開催しました!!
Synori icon Synori
2023年7月15日
2023 春ハッカソン 06班 stamProlog
H1rono_K icon H1rono_K
2023年9月13日
ブログリレーを支えるリマインダー
H1rono_K icon H1rono_K
2022年4月5日
アーキテクチャとディレクトリ構造
mazrean icon mazrean
2021年5月16日
CPCTFを支えたインフラ
mazrean icon mazrean
2024年7月20日
部員の活動紹介サービス traPortfolio をリリースしました
mehm8128 icon mehm8128
記事一覧 タグ一覧 Google アナリティクスについて 特定商取引法に基づく表記