これは何?
Elasticsearchで日本語のトークナイズに良く使われるkuromoji_tokenizerと、同義語辞書としてよく使われるSynonym token filterおよびSynonym graph token filterの挙動についてまとめたものです。
記事中ではSynonym graph token filterを使用していますが、Synonym token filterを利用しても同じ結果になるようです。
TL; DR(一言でまとめると)
ElasticsearchのSynonym graph token filterに複合語を含んだ同義語(synonyms)を設定する場合は、以下の2つの対処法のどちらかを取る。
- 同義語辞書に登録したキーワードとピッタリ一致したときだけ一致扱いにしたい場合は対処方法1
 - 一部の形態素だけ一致した場合も一致扱いにしたい場合は対処方法2
 - 対処方法3は面倒な割に対処方法1と結果が変わらないので対象外
 
| 対処方法 | 「東京大学」の解析結果 | 検索キーワード「大学」は「東京大学」の文書にマッチするか | 
|---|---|---|
| 1: mode=normal | 「東京大学」 | × | 
| 2: discard_compound_token=true | 「東京」「大学」 | ○ | 
| 3: 同義語を全てユーザー辞書に追加 | 「東京大学」 | × | 
前提
Elasticsearch 7.9.1での検証結果です。
現象
Synonym graph token filterに日本語の複合語とみなされる文字列(例: 東京大学)を渡すと、以下のようなエラーが出る。
下記のページで説明されている現象と同じ:
ElasticsearchのSynonym追加において一部の日本語の文字でillegal_argument_exceptionが出る問題 - Qiita
インデックス作成リクエスト
PUT test 
{
  "settings": {
    "analysis": {
      "char_filter": {
        "normalize": {
          "type": "icu_normalizer",
          "name": "nfkc",
          "mode": "compose"
        }
      },
      "tokenizer": {
        "ja_kuromoji_tokenizer": {
          "mode": "search",
          "type": "kuromoji_tokenizer"
        }
      },
      "filter": {
        "ja_search_synonym": {
          "type": "synonym_graph",
          "synonyms": ["米国, アメリカ", "東京大学, 東大"]
        }
      },
      "analyzer": {
        "ja_kuromoji_index_analyzer": {
          "type": "custom",
          "char_filter": ["normalize"],
          "tokenizer": "ja_kuromoji_tokenizer"
        },
        "ja_kuromoji_search_analyzer": {
          "type": "custom",
          "char_filter": ["normalize"],
          "tokenizer": "ja_kuromoji_tokenizer",
          "filter": [
            "ja_search_synonym"
          ]
        }
      }
    }
  },
  "mappings": {
    "properties": {
      "text": {
        "type": "text",
        "search_analyzer": "ja_kuromoji_search_analyzer",
        "analyzer": "ja_kuromoji_index_analyzer"
      }
    }
  }
}
上記のリクエストに対しては、以下のように400 Bad Requestが返ってくる。
{
  "error" : {
    "root_cause" : [
      {
        "type" : "illegal_argument_exception",
        "reason" : "failed to build synonyms"
      }
    ],
    "type" : "illegal_argument_exception",
    "reason" : "failed to build synonyms",
    "caused_by" : {
      "type" : "parse_exception",
      "reason" : "Invalid synonym rule at line 2",
      "caused_by" : {
        "type" : "illegal_argument_exception",
        "reason" : "term: 東京大学 analyzed to a token (東京大学) with position increment != 1 (got: 0)"
      }
    }
  },
  "status" : 400
}
考えられる原因
kuromoji_tokenizerのmodeをsearchに設定した場合、東京大学は以下のように「東京」「東京大学」「大学」と複合語と単語が混ざった状態で解析される。
{
  "tokens" : [
    {
      "token" : "東京",
      "start_offset" : 0,
      "end_offset" : 2,
      "type" : "word",
      "position" : 0
    },
    {
      "token" : "東京大学",
      "start_offset" : 0,
      "end_offset" : 4,
      "type" : "word",
      "position" : 0,
      "positionLength" : 2
    },
    {
      "token" : "大学",
      "start_offset" : 2,
      "end_offset" : 4,
      "type" : "word",
      "position" : 1
    }
  ]
}
エラーメッセージと下記の記事から推測すると、この「東京」と「東京大学」でpositionが同じ0になっているのが問題のようだ。
対応方法1: kuromoji_tokenizerのmodeをnormalにする
前述の2つのWeb上の記事でも紹介されている方法。
インデックス作成リクエスト
問題が起こるリクエストからja_kuromoji_tokenizerのmodeのみ変更。問題ないレスポンスが返ってくる(200 OK)
PUT test 
{
  "settings": {
    "analysis": {
      "char_filter": {
        "normalize": {
          "type": "icu_normalizer",
          "name": "nfkc",
          "mode": "compose"
        }
      },
      "tokenizer": {
        "ja_kuromoji_tokenizer": {
          "mode": "normal",
          "type": "kuromoji_tokenizer"
        }
      },
      "filter": {
        "ja_search_synonym": {
          "type": "synonym_graph",
          "synonyms": ["米国, アメリカ", "東京大学, 東大"]
        }
      },
      "analyzer": {
        "ja_kuromoji_index_analyzer": {
          "type": "custom",
          "char_filter": ["normalize"],
          "tokenizer": "ja_kuromoji_tokenizer"
        },
        "ja_kuromoji_search_analyzer": {
          "type": "custom",
          "char_filter": ["normalize"],
          "tokenizer": "ja_kuromoji_tokenizer",
          "filter": [
            "ja_search_synonym"
          ]
        }
      }
    }
  },
  "mappings": {
    "properties": {
      "text": {
        "type": "text",
        "search_analyzer": "ja_kuromoji_search_analyzer",
        "analyzer": "ja_kuromoji_index_analyzer"
      }
    }
  }
}
複合語の解析結果
東京大学は「東大」と「東京大学」に解析される。「東京」とか「大学」は出てこない。
GET test/_analyze
{
  "analyzer" : "ja_kuromoji_search_analyzer",
  "text" : "東京大学"
}
# 結果
{
  "tokens" : [
    {
      "token" : "東大",
      "start_offset" : 0,
      "end_offset" : 4,
      "type" : "SYNONYM",
      "position" : 0
    },
    {
      "token" : "東京大学",
      "start_offset" : 0,
      "end_offset" : 4,
      "type" : "word",
      "position" : 0
    }
  ]
}
検索時の挙動
同義語辞書が効いているため、「東大」という検索キーワードに対して「東京大学」を含む文書がヒットするようになる(検索例2)。一方で、「大学」や「青山学院大学」(内部的には青山/学院/大学と解析されている)にはヒットしなくなる(検索例1, 3)
# 文書登録
POST _bulk
{"index": {"_index": "test", "_id": 1}}
{"text": "東京大学は本郷と駒場にキャンパスがあります。"}
# 検索例1
GET /test/_search
{
  "query": {
    "match": {
      "text": "日本の有名な大学"
    }
  }
}
# 検索結果1(ヒットなし)
{
  "took" : 423,
  "timed_out" : false,
  "_shards" : {
    "total" : 1,
    "successful" : 1,
    "skipped" : 0,
    "failed" : 0
  },
  "hits" : {
    "total" : {
      "value" : 0,
      "relation" : "eq"
    },
    "max_score" : null,
    "hits" : [ ]
  }
}
# 検索例2
GET /test/_search
{
  "query": {
    "match": {
      "text": "東大に行きたい"
    }
  }
}
# 検索結果2(上記の文書がヒット)
{
  "took" : 1,
  "timed_out" : false,
  "_shards" : {
    "total" : 1,
    "successful" : 1,
    "skipped" : 0,
    "failed" : 0
  },
  "hits" : {
    "total" : {
      "value" : 1,
      "relation" : "eq"
    },
    "max_score" : 0.5753642,
    "hits" : [
      {
        "_index" : "test",
        "_type" : "_doc",
        "_id" : "1",
        "_score" : 0.5753642,
        "_source" : {
          "text" : "東京大学は本郷と駒場にキャンパスがあります。"
        }
      }
    ]
  }
}
# 検索例3
GET /test/_search
{
  "query": {
    "match": {
      "text": "青山学院大学"
    }
  }
}
# 検索結果3(ヒットなし)
{
  "took" : 1,
  "timed_out" : false,
  "_shards" : {
    "total" : 1,
    "successful" : 1,
    "skipped" : 0,
    "failed" : 0
  },
  "hits" : {
    "total" : {
      "value" : 0,
      "relation" : "eq"
    },
    "max_score" : null,
    "hits" : [ ]
  }
}
対応方法2: kuromoji_tokenizerのdiscard_compound_tokenをtrueにセットする
下記の記事での対応方法。
How to implement Japanese full-text search in Elasticsearch
discard_compound_tokenをtrueにすると、searchモードのときに複合語が出力されなくなる。デフォルトはfalse。
kuromoji_tokenizerの説明。
インデックス作成リクエスト
問題が起こるリクエストのja_kuromoji_tokenizerにdiscard_compound_token: trueを追加。このオプションはデフォルトではfalseになっている。問題ないレスポンスが返ってくる(200 OK)
PUT test 
{
  "settings": {
    "analysis": {
      "char_filter": {
        "normalize": {
          "type": "icu_normalizer",
          "name": "nfkc",
          "mode": "compose"
        }
      },
      "tokenizer": {
        "ja_kuromoji_tokenizer": {
          "mode": "search",
          "discard_compound_token": true,
          "type": "kuromoji_tokenizer"
        }
      },
      "filter": {
        "ja_search_synonym": {
          "type": "synonym_graph",
          "synonyms": ["米国, アメリカ", "東京大学, 東大"]
        }
      },
      "analyzer": {
        "ja_kuromoji_index_analyzer": {
          "type": "custom",
          "char_filter": ["normalize"],
          "tokenizer": "ja_kuromoji_tokenizer"
        },
        "ja_kuromoji_search_analyzer": {
          "type": "custom",
          "char_filter": ["normalize"],
          "tokenizer": "ja_kuromoji_tokenizer",
          "filter": [
            "ja_search_synonym"
          ]
        }
      }
    }
  },
  "mappings": {
    "properties": {
      "text": {
        "type": "text",
        "search_analyzer": "ja_kuromoji_search_analyzer",
        "analyzer": "ja_kuromoji_index_analyzer"
      }
    }
  }
}
複合語の解析結果
「東京大学」は「東大」「東京」「大学」と解析される。「東京大学」は含まれない。
GET test/_analyze
{
  "analyzer" : "ja_kuromoji_search_analyzer",
  "text" : "東京大学"
}
# 結果
{
  "tokens" : [
    {
      "token" : "東大",
      "start_offset" : 0,
      "end_offset" : 4,
      "type" : "SYNONYM",
      "position" : 0,
      "positionLength" : 2
    },
    {
      "token" : "東京",
      "start_offset" : 0,
      "end_offset" : 2,
      "type" : "word",
      "position" : 0
    },
    {
      "token" : "大学",
      "start_offset" : 2,
      "end_offset" : 4,
      "type" : "word",
      "position" : 1
    }
  ]
}
検索時の挙動
「東京大学」を含む文書は、「東京大学」という検索キーワードにも(検索例2)、「大学」という検索キーワードにもヒットするようになる(検索例1, 3)。
# 文書登録
POST _bulk
{"index": {"_index": "test", "_id": 1}}
{"text": "東京大学は本郷と駒場にキャンパスがあります。"}
# 検索例1
GET /test/_search
{
  "query": {
    "match": {
      "text": "日本の有名な大学"
    }
  }
}
# 検索結果1(ヒット)
{
  "took" : 1,
  "timed_out" : false,
  "_shards" : {
    "total" : 1,
    "successful" : 1,
    "skipped" : 0,
    "failed" : 0
  },
  "hits" : {
    "total" : {
      "value" : 1,
      "relation" : "eq"
    },
    "max_score" : 0.2876821,
    "hits" : [
      {
        "_index" : "test",
        "_type" : "_doc",
        "_id" : "1",
        "_score" : 0.2876821,
        "_source" : {
          "text" : "東京大学は本郷と駒場にキャンパスがあります。"
        }
      }
    ]
  }
}
# 検索例2
GET /test/_search
{
  "query": {
    "match": {
      "text": "東大に行きたい"
    }
  }
}
# 検索結果2(ヒット)
{
  "took" : 1,
  "timed_out" : false,
  "_shards" : {
    "total" : 1,
    "successful" : 1,
    "skipped" : 0,
    "failed" : 0
  },
  "hits" : {
    "total" : {
      "value" : 1,
      "relation" : "eq"
    },
    "max_score" : 0.8630463,
    "hits" : [
      {
        "_index" : "test",
        "_type" : "_doc",
        "_id" : "1",
        "_score" : 0.8630463,
        "_source" : {
          "text" : "東京大学は本郷と駒場にキャンパスがあります。"
        }
      }
    ]
  }
}
# 検索例3
GET /test/_search
{
  "query": {
    "match": {
      "text": "青山学院大学"
    }
  }
}
# 検索結果3(ヒットなし)
{
  "took" : 0,
  "timed_out" : false,
  "_shards" : {
    "total" : 1,
    "successful" : 1,
    "skipped" : 0,
    "failed" : 0
  },
  "hits" : {
    "total" : {
      "value" : 1,
      "relation" : "eq"
    },
    "max_score" : 0.2876821,
    "hits" : [
      {
        "_index" : "test",
        "_type" : "_doc",
        "_id" : "1",
        "_score" : 0.2876821,
        "_source" : {
          "text" : "東京大学は本郷と駒場にキャンパスがあります。"
        }
      }
    ]
  }
}
対応方法3: 同義語辞書エントリを全部ユーザー辞書にセットする
同義語辞書エントリをユーザー辞書にセットすることで上述の同義語辞書のエラーを防ぐこともできる。
インデックス作成リクエスト
エラーが発生するリクエストのkuromoji_tokenizerにuser_dictionary_rulesを追加。問題ないレスポンスが返ってくる(200 OK)
PUT test 
{
  "settings": {
    "analysis": {
      "char_filter": {
        "normalize": {
          "type": "icu_normalizer",
          "name": "nfkc",
          "mode": "compose"
        }
      },
      "tokenizer": {
        "ja_kuromoji_tokenizer": {
          "mode": "search",
          "type": "kuromoji_tokenizer",
          "user_dictionary_rules": [
            "東京大学,東京大学,トウキョウダイガク,カスタム名詞"
          ]
        }
      },
      "filter": {
        "ja_search_synonym": {
          "type": "synonym_graph",
          "synonyms": ["米国, アメリカ", "東京大学, 東大"]
        }
      },
      "analyzer": {
        "ja_kuromoji_index_analyzer": {
          "type": "custom",
          "char_filter": ["normalize"],
          "tokenizer": "ja_kuromoji_tokenizer"
        },
        "ja_kuromoji_search_analyzer": {
          "type": "custom",
          "char_filter": ["normalize"],
          "tokenizer": "ja_kuromoji_tokenizer",
          "filter": [
            "ja_search_synonym"
          ]
        }
      }
    }
  },
  "mappings": {
    "properties": {
      "text": {
        "type": "text",
        "search_analyzer": "ja_kuromoji_search_analyzer",
        "analyzer": "ja_kuromoji_index_analyzer"
      }
    }
  }
}
複合語の解析結果
「東京大学」は「東大」「東京大学」と解析される。対処方法1でmode=normalに設定したときと同じ挙動。
GET test/_analyze
{
  "analyzer" : "ja_kuromoji_search_analyzer",
  "text" : "東京大学"
}
# 結果
{
  "tokens" : [
    {
      "token" : "東大",
      "start_offset" : 0,
      "end_offset" : 4,
      "type" : "SYNONYM",
      "position" : 0
    },
    {
      "token" : "東京大学",
      "start_offset" : 0,
      "end_offset" : 4,
      "type" : "word",
      "position" : 0
    }
  ]
}
検索時の挙動
対処方法1と同じ。「東京大学」を含む文書は、「東大」という検索キーワードにはヒットするが(検索例2)、「大学」という検索キーワードにはヒットしない(検索例1, 3)
# 文書登録
POST _bulk
{"index": {"_index": "test", "_id": 1}}
{"text": "東京大学は本郷と駒場にキャンパスがあります。"}
# 検索例1
GET /test/_search
{
  "query": {
    "match": {
      "text": "日本の有名な大学"
    }
  }
}
# 検索結果1(ヒットなし)
{
  "took" : 423,
  "timed_out" : false,
  "_shards" : {
    "total" : 1,
    "successful" : 1,
    "skipped" : 0,
    "failed" : 0
  },
  "hits" : {
    "total" : {
      "value" : 0,
      "relation" : "eq"
    },
    "max_score" : null,
    "hits" : [ ]
  }
}
# 検索例2
GET /test/_search
{
  "query": {
    "match": {
      "text": "東大に行きたい"
    }
  }
}
# 検索結果2(上記の文書がヒット)
{
  "took" : 1,
  "timed_out" : false,
  "_shards" : {
    "total" : 1,
    "successful" : 1,
    "skipped" : 0,
    "failed" : 0
  },
  "hits" : {
    "total" : {
      "value" : 1,
      "relation" : "eq"
    },
    "max_score" : 0.5753642,
    "hits" : [
      {
        "_index" : "test",
        "_type" : "_doc",
        "_id" : "1",
        "_score" : 0.5753642,
        "_source" : {
          "text" : "東京大学は本郷と駒場にキャンパスがあります。"
        }
      }
    ]
  }
}
# 検索例3
GET /test/_search
{
  "query": {
    "match": {
      "text": "青山学院大学"
    }
  }
}
# 検索結果3(ヒットなし)
{
  "took" : 1,
  "timed_out" : false,
  "_shards" : {
    "total" : 1,
    "successful" : 1,
    "skipped" : 0,
    "failed" : 0
  },
  "hits" : {
    "total" : {
      "value" : 0,
      "relation" : "eq"
    },
    "max_score" : null,
    "hits" : [ ]
  }
}
まとめ
対処方法3は同義語エントリが多くなると面倒。どれが複合語扱いされるかひと目ではわからないので、おそらくすべての同義語エントリをユーザー辞書登録する必要がある。
そう考えると、完全一致重視(Precision重視)にするなら対処方法1、部分一致重視(Recall重視)にするなら対処方法2、と言えそう。
なお、下記の記事ではさらにn-gram analyzerを利用して文字バイグラムもインデックスに追加することで、Recallを増す方法が紹介されています。