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などをいじっています

この記事をシェア

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

関連する記事

2022年4月5日
アーキテクチャとディレクトリ構造
mazrean icon mazrean
2021年5月16日
CPCTFを支えたインフラ
mazrean icon mazrean
2022年3月27日
ReactでToDoリストを作る(後編)
mehm8128 icon mehm8128
2022年3月19日
ReactでToDoリストを作る(中編)
mehm8128 icon mehm8128
2022年3月18日
ReactでToDoリストを作る(前編)
mehm8128 icon mehm8128
2022年3月26日
GoでToDoリストを作ろう ! (Day-4)
irori icon irori
記事一覧 タグ一覧 Google アナリティクスについて