背景: Mattermost Bleve検索
2021/06にリリースされたMattermost v5.24から、Bleveによるメッセージの日本語検索が利用できるようになった。
それ以前のバージョンでは、Mattermostセットアップ時にDB(MySQL, PostgreSQL)の設定に手を加える必要があったが、Bleveによる検索機能が公式にサポートされたことで、システムコンソール画面からいくつか設定を変更するだけで日本語検索が実現できるようになった。嬉しい。
問題: 意図せぬ投稿が検索される
Bleve検索自体はとても便利な機能だが、その検索結果に少し問題がある。
2文字以上の単語で構成される日本語クエリによるBleve検索をおこなった際、本来ならばその単語が出現する投稿のみが検索結果として表示して欲しいところだが、実際の検索結果には、その単語内で使用されている文字が全て現れる投稿を検索してしまう。
例えば、「メロスは激怒した。必ず、かの邪智暴虐の王を除かなければならぬと決意した。」という投稿があったとする。
「激怒」「邪智暴虐」などの単語で検索を行うと、想定通りこの投稿を検索することができる。しかし、「激暴」という単語で検索を行った場合も、この単語自体は対象の投稿に存在しないにも関わらず、単語を構成する「激」「暴」の両方の文字が投稿に含まれているため、対象の投稿が検索結果として表示されてしまう。
この問題を改善するため、調査を行った。
調査1: Bleveのcjk言語向けAnalyzer
まず、Mattermost側でBleveをどのように使用しているかを調べる。
投稿内容のインデックスは、以下で定義されている。
...
import (
...
"github.com/blevesearch/bleve/v2/analysis/analyzer/standard"
...
)
...
func init() {
...
standardMapping = bleve.NewTextFieldMapping()
standardMapping.Analyzer = standard.Name
...
}
...
func getPostIndexMapping() *mapping.IndexMappingImpl {
postMapping := bleve.NewDocumentMapping()
...
postMapping.AddFieldMappingsAt("Message", standardMapping)
...
}
Mattermostの投稿(Post
)の内容(Message
)は、Bleveのstandard.Name
で表されるAnalyzerによって解析されるようだ。Bleveについてあまり詳しくはないが、standard.Name
で表されるAnalyzerの内容を見ると、en.StopName
が使われており、英文を前提としたAnalyzerだと考えられるので、この部分が問題なのではないかと思う。
https://github.com/blevesearch/bleve/blob/master/analysis/analyzer/standard/standard.go
BleveのAnalyzerについて調べていると、cjk
向けのAnalyzerもあることが分かった。
https://github.com/blevesearch/bleve/tree/master/analysis/lang/cjk
単純にMattermostの投稿内容のインデックスをBleveのcjk
Analyzerを使って行うよう変更してみる。
@@ -14,7 +14,7 @@ import (
"github.com/blevesearch/bleve/v2"
"github.com/blevesearch/bleve/v2/analysis/analyzer/keyword"
- "github.com/blevesearch/bleve/v2/analysis/analyzer/standard"
+ "github.com/blevesearch/bleve/v2/analysis/lang/cjk"
"github.com/blevesearch/bleve/v2/mapping"
"github.com/mattermost/mattermost-server/v6/model"
@@ -49,7 +49,7 @@ func init() {
keywordMapping.Analyzer = keyword.Name
standardMapping = bleve.NewTextFieldMapping()
- standardMapping.Analyzer = standard.Name
+ standardMapping.Analyzer = cjk.AnalyzerName
この変更を有効にしてMattermostを再起動し、Bleveインデックスを破棄→再構築すると、「激暴」という単語で当該の投稿が検索されなくなった。
めでたしめでたし。
…とはならなかった。
上記の変更を加えたことで、今度は1語による検索ができなくなった。
「検索は2語以上で」という制限を設ければ問題は解決だが、この制限はあまり好ましくない。
調査2: Bleve cjk
Analyzerを使用した際に1語検索ができるようにする
Bleveのcjk
AnalyzerはTokenFilterとしてBigramを使用している。つまり、2語ずつインデックスを構築しているので、1語での検索ができないようだ。
...
const AnalyzerName = "cjk"
func AnalyzerConstructor(config map[string]interface{}, cache *registry.Cache) (*analysis.Analyzer, error) {
...
bigramFilter, err := cache.TokenFilterNamed(BigramName)
if err != nil {
return nil, err
}
rv := analysis.Analyzer{
Tokenizer: tokenizer,
TokenFilters: []analysis.TokenFilter{
widthFilter,
toLowerFilter,
bigramFilter,
},
}
...
}
...
もう一度、Bleveのcjk
パッケージを見てみるとoutput_unigram
という設定があることに気づく。名前から、この設定をOn
にしてインデックスを構築することで、1語のインデックスも出力されるようになるのではないかと予想する。(それによるインデックスサイズの増加についてはここでは考慮しないことにする)
...
func CJKBigramFilterConstructor(config map[string]interface{}, cache *registry.Cache) (analysis.TokenFilter, error) {
outputUnigram := false
outVal, ok := config["output_unigram"].(bool)
if ok {
outputUnigram = outVal
}
return NewCJKBigramFilter(outputUnigram), nil
}
...
output_unigram
の設定をOnにする方法について色々調べてみたが、どうやってもOnにする方法が見つからない(これがこの記事を書くきっかけだったりする)。
以下のItemNamed
関数内で実行しているbuild(name, nil, cache)
の第二引数が前述のCJKBigramFilterConstructor
関数の第一引数 (config
)になり、ここにoutput_unigram: true
が設定されていれば有効にできるらしいが、nil
でハードコードされているのでどうしようもない。
func (c *ConcurrentCache) ItemNamed(name string, cache *Cache, build CacheBuild) (interface{}, error) {
...
// try to build it
newItem, err := build(name, nil, cache)
...
}
自分が気づいていない方法があるのかも知れないが、調べていてもわからないので、output_unigram
をtrue
にしたAnalyzerを新たに登録し、それを使用してみる。
@@ -13,9 +13,13 @@ import (
"time"
"github.com/blevesearch/bleve/v2"
+ "github.com/blevesearch/bleve/v2/analysis"
"github.com/blevesearch/bleve/v2/analysis/analyzer/keyword"
- "github.com/blevesearch/bleve/v2/analysis/analyzer/standard"
+ "github.com/blevesearch/bleve/v2/analysis/lang/cjk"
+ "github.com/blevesearch/bleve/v2/analysis/token/lowercase"
+ "github.com/blevesearch/bleve/v2/analysis/tokenizer/unicode"
"github.com/blevesearch/bleve/v2/mapping"
+ "github.com/blevesearch/bleve/v2/registry"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/shared/mlog"
@@ -44,12 +48,50 @@ var keywordMapping *mapping.FieldMapping
var standardMapping *mapping.FieldMapping
var dateMapping *mapping.FieldMapping
+const customCJKAnalyzerName = "custom_cjk_analyzer"
+const customCJKTokenFilterName = "custom_cjk_filter"
+
+func CustomCJKAnalyzerConstructor(config map[string]interface{}, cache *registry.Cache) (*analysis.Analyzer, error) {
+ tokenizer, err := cache.TokenizerNamed(unicode.Name)
+ if err != nil {
+ return nil, err
+ }
+ widthFilter, err := cache.TokenFilterNamed(cjk.WidthName)
+ if err != nil {
+ return nil, err
+ }
+ toLowerFilter, err := cache.TokenFilterNamed(lowercase.Name)
+ if err != nil {
+ return nil, err
+ }
+ bigramFilter, err := cache.TokenFilterNamed(customCJKTokenFilterName)
+ if err != nil {
+ return nil, err
+ }
+ rv := analysis.Analyzer{
+ Tokenizer: tokenizer,
+ TokenFilters: []analysis.TokenFilter{
+ widthFilter,
+ toLowerFilter,
+ bigramFilter,
+ },
+ }
+ return &rv, nil
+}
+
+func CustomCJKBigramFilterConstructor(config map[string]interface{}, cache *registry.Cache) (analysis.TokenFilter, error) {
+ return cjk.NewCJKBigramFilter(true), nil
+}
+
func init() {
+ registry.RegisterTokenFilter(customCJKTokenFilterName, CustomCJKBigramFilterConstructor)
+ registry.RegisterAnalyzer(customCJKAnalyzerName, CustomCJKAnalyzerConstructor)
+
keywordMapping = bleve.NewTextFieldMapping()
keywordMapping.Analyzer = keyword.Name
standardMapping = bleve.NewTextFieldMapping()
- standardMapping.Analyzer = standard.Name
+ standardMapping.Analyzer = customCJKAnalyzerName
dateMapping = bleve.NewNumericFieldMapping()
}
冗長だ。
再びMattermostを再起動、Bleveインデックスの破棄→再構築を行い検索を実行すると、1語でも検索ができるようになった。
おわりに
というわけで、MattermostのBleveによる日本語検索の改善について調べてみた。
とりあえず小さなサンプルでは実現可能なことが分かったが、output_unigram
の設定のせいで修正量が多くなりそうなので、もう少し調査してからIssueで報告しようと思う。
調査内容としては以下のあたりかな。
- 検索内容の正当性
- 日本語検索のテストセットが欲しいな…
- 検索インデックスの容量増加
- 日本語の投稿がたくさんあるMattermostインスタンスのデータが欲しいな…
blevex
のkagomeを使用した検索blevex
のREADMEを見ると、Mattermost本体に組み込んでメンテし続けるのはちょっと怖い気がするな…
Mattermostで実装するときは、Bleveの設定画面でAnalyzer選ぶような感じになるのかな。CJK以外でAnalyzer変更が必要な言語が無かったりすると、ちょっと独自改造っぽい感じになるので受け入れられにくそうな感じもするな。