INFORMATION
テクノロジ

Apache Solr の日本語シノニム検索とハイライト 最新事情(2022年版)

長山 英司 著

はじめに

Apache Solr における日本語のシノニム検索とシノニムを考慮したハイライトの動作について、ManagedSynonymGraphFilterを使用した場合の検証を行ったので報告する。ManagedSynonymGraphFilterは、シノニム辞書(リソース)をREST API(WEB API)でメンテするSynonymGraphFilterの別バージョンである。当記事内容は、SynonymGraphFilterに関して記述した前回記事にならい、SynonymGraphFilterをManagedSynonymGraphFilterに置き換えて検証を行った結果をもとにしている。また、検証に用いたSolrのバージョンは 8.11.1 である。

本来、ManagedSynonymGraphFilterとSynonymGraphFilterには動作の違いはないはずであるが、検証の結果これら2つのGraphFilterには以下の違いが存在することが分かった。

  • ManagedSynonymGraphFilterの方がハイライトの再現性が高い
    SynonymGraphFilterでハイライトNGとなっていた検索結果のうち、ManagedSynonymGraphFilterではハイライトOKとなるものが数個見つかった。これについての詳細は、後述する(ステップ2)正しいハイライトがなされるフィールド型の選出の中の【注意】参照。
  • ManagedSynonymGraphFilterを使用した場合にはN-gram系のハイライトも行われる
    SynonymGraphFilterでは全く行われなかったN-gram系のハイライトが、ManagedSynonymGraphFilterでは行われることが分かった。
その他、正常にハイライトされる場合でもGraphFilterの差によって細かくハイライトされる場合(例:<em>内閣</em><em>総理</em><em>大臣</em>)、全体がハイライトされる場合(例:<em>内閣総理大臣</em>)などの違いが見つかったが見た目はどちらも内閣総理大臣となり、正常であるので大きな問題ではない。 やはり一番大きい問題は、ManagedSynonymGraphFilterの場合はN-gram系のハイライトも動作するということなので、特別な理由がない限り今後はManagedSynonymGraphFilterの使用を推奨したい。

前準備

試行錯誤を経て、検証は以下の2ステップに分けて行うことにした。

  • (ステップ1)正しい検索結果を返すフィールド型の選出
    後述する様々なフィールド型においてシノニム検索を行い正しい検索結果を返すものを確認する。
  • (ステップ2)正しいハイライトがなされるフィールド型の選出
    同様に様々なフィールド型においてシノニムを考慮したハイライトが正しく行われるかを確認する。

Apache Solrは、検索に使用するフィールドとハイライト対象にするフィールドを分けることができるが、事前調査の結果、ステップ2で得られた「正しくハイライトが行われるフィールド型」が、ステップ1で得られた「検索で正しい結果を返すフィールド型」のサブセットとなることが判明した。そこで本稿では、ステップ1で得られたフィールド型のうち、さらにステップ2でハイライトが正しくなされるフィールド型は何か、という表現になっているが、あらかじめご了承いただきたい。

テストのために用意するフィールド型

日本語でシノニム検索とハイライトを考えるとき、以下に示すようにApache Solrでは多くのパラメーター設定があり、その組み合わせは多岐にわたる。本検証ではシノニム検索がうまくいかないと予想できる場合においても漏れがないように網羅的にテストを行うこととした。

以下の場合について網羅的に組み合わせたフィールド型を用意する。

  • 形態素区切りかN-gram区切りか
    N-gramの代表として今回の検証では2-gramを用いる。
      形態素区切り:フィールド型名のtext_の後ろがjaのもの(例:text_ja_e_q_sgf_managed)
      N-gram区切り:フィールド型名のtext_の後ろが2gのもの(例:text_2g_e_i_sgffgf_managed)
  • シノニムの適用方法をnormlizeにするかexpandにするか
    normalizeはシノニムをある一つの代表表記に正規化してシノニム検索を可能にする方法である。
    expandはシノニム全てを展開してシノニム検索を可能にする方法である。
      normalize:フィールド型名のtext_*_の後ろがnのもの(例:text_ja_n_q_sgf_managed)
      expand:フィールド型名のtext_*_の後ろがeのもの(例:text_ja_e_i_sgffgf_managed)
      ※フィールド型名の後ろがnnや、en等2桁になっている場合は、1桁目が索引時、2桁目が検索時という意味である)
  • シノニムの適用を索引時にするか検索時にするかあるいは両方にするか
    Solrは、索引時にもシノニム適用が可能であり索引時にexpandでシノニムを適用した場合には全てのシノニムについて展開が行われ索引付けが行われる。
      索引時:フィールド型名のtext_*_*_の後ろがiのもの(例:text_ja_e_i_sgffgf_managed)
      検索時:フィールド型名のtext_*_*_の後ろがqのもの(例:text_ja_e_q_sgf_managed)
      両方:フィールド型名のtext_*_*_の後ろがiqのもの(例:text_ja_ne_iq_sgffgfsgf_managed。なお、索引時はnormalize、検索時はexpand)
Filterの適用についての注意

シノニムを適用する場合、索引時と検索時では以下のようにFilterを適用することとする。

  • 索引時
    SynonymGraphFilterとFlattenGraphFilterを使用する。
    ※FlattenGraphFilterは、インデクシング時のみSynonymGraphFilterの直後に配置することが強く推奨されている。
    フィールド型名の_managedの前が、sgffgfである。
        <analyzer type="index">
          <tokenizer class="solr.JapaneseTokenizerFactory" mode="normal"/>
          <filter class="solr.ManagedSynonymGraphFilterFactory" managed="synonym-managed-xx-x" />
          <filter class="solr.FlattenGraphFilterFactory"/>
        </analyzer>
        <analyzer type="query">
          <tokenizer class="solr.JapaneseTokenizerFactory" mode="normal"/>
        </analyzer>
    
  • 検索時
    フィールド型名の_managedの前が、sgfである。
        <analyzer type="index">
          <tokenizer class="solr.JapaneseTokenizerFactory" mode="normal"/>
        </analyzer>
        <analyzer type="query">
          <tokenizer class="solr.JapaneseTokenizerFactory" mode="normal"/>
          <filter class="solr.ManagedSynonymGraphFilterFactory" managed="synonym-managed-xx-x" />
        </analyzer>
    
  • 索引時及び検索時
    フィールド型名の_managedの前が、sgffgfsgfである。
        <analyzer type="index">
          <tokenizer class="solr.JapaneseTokenizerFactory" mode="normal"/>
          <filter class="solr.ManagedSynonymGraphFilterFactory" managed="synonym-managed-xx-x" />
          <filter class="solr.FlattenGraphFilterFactory"/>
        </analyzer>
        <analyzer type="query">
          <tokenizer class="solr.JapaneseTokenizerFactory" mode="normal"/>
          <filter class="solr.ManagedSynonymGraphFilterFactory" managed="synonym-managed-xx-x" />
        </analyzer>
    
フィールド名

フィールド名はわかりやすいようにフィールド型名の先頭textをcontentに置き換えたものにする。
また、「termVectors=”true” termOffsets=”true” termPositions=”true”」をつけるようにしているが、これはハイライト(fastVector)をする際に必要な指定のためである。

上記より、用意したフィールドは以下のようになる。id、contentの2つのフィールドと、その他のcontent_*という16種類の検証用に使用するフィールドを持っている。content_*はcontentフィールドをもとにしたコピーフィールドとなっている。

  <field name="id"      type="string" indexed="true" stored="true" required="true" multiValued="false" />
  <field name="content" type="string" indexed="true" stored="true" required="true" multiValued="true" />
  <dynamicField name="*_ja_n_q_sgf_managed"         type="text_ja_n_q_sgf_managed"         indexed="true" stored="true" required="false" multiValued="true" termVectors="true" termOffsets="true" termPositions="true" />
  <dynamicField name="*_ja_n_i_sgffgf_managed"      type="text_ja_n_i_sgffgf_managed"      indexed="true" stored="true" required="false" multiValued="true" termVectors="true" termOffsets="true" termPositions="true" />
  <dynamicField name="*_ja_nn_iq_sgffgfsgf_managed" type="text_ja_nn_iq_sgffgfsgf_managed" indexed="true" stored="true" required="false" multiValued="true" termVectors="true" termOffsets="true" termPositions="true" />
  <dynamicField name="*_ja_ne_iq_sgffgfsgf_managed" type="text_ja_ne_iq_sgffgfsgf_managed" indexed="true" stored="true" required="false" multiValued="true" termVectors="true" termOffsets="true" termPositions="true" />
  <dynamicField name="*_ja_e_q_sgf_managed"         type="text_ja_e_q_sgf_managed"         indexed="true" stored="true" required="false" multiValued="true" termVectors="true" termOffsets="true" termPositions="true" />
  <dynamicField name="*_ja_e_i_sgffgf_managed"      type="text_ja_e_i_sgffgf_managed"      indexed="true" stored="true" required="false" multiValued="true" termVectors="true" termOffsets="true" termPositions="true" />
  <dynamicField name="*_ja_ee_iq_sgffgfsgf_managed" type="text_ja_ee_iq_sgffgfsgf_managed" indexed="true" stored="true" required="false" multiValued="true" termVectors="true" termOffsets="true" termPositions="true" />
  <dynamicField name="*_ja_en_iq_sgffgfsgf_managed" type="text_ja_en_iq_sgffgfsgf_managed" indexed="true" stored="true" required="false" multiValued="true" termVectors="true" termOffsets="true" termPositions="true" />
  <dynamicField name="*_2g_n_q_sgf_managed"         type="text_2g_n_q_sgf_managed"         indexed="true" stored="true" required="false" multiValued="true" termVectors="true" termOffsets="true" termPositions="true" />
  <dynamicField name="*_2g_n_i_sgffgf_managed"      type="text_2g_n_i_sgffgf_managed"      indexed="true" stored="true" required="false" multiValued="true" termVectors="true" termOffsets="true" termPositions="true" />
  <dynamicField name="*_2g_nn_iq_sgffgfsgf_managed" type="text_2g_nn_iq_sgffgfsgf_managed" indexed="true" stored="true" required="false" multiValued="true" termVectors="true" termOffsets="true" termPositions="true" />
  <dynamicField name="*_2g_ne_iq_sgffgfsgf_managed" type="text_2g_ne_iq_sgffgfsgf_managed" indexed="true" stored="true" required="false" multiValued="true" termVectors="true" termOffsets="true" termPositions="true" />
  <dynamicField name="*_2g_e_q_sgf_managed"         type="text_2g_e_q_sgf_managed"         indexed="true" stored="true" required="false" multiValued="true" termVectors="true" termOffsets="true" termPositions="true" />
  <dynamicField name="*_2g_e_i_sgffgf_managed"      type="text_2g_e_i_sgffgf_managed"      indexed="true" stored="true" required="false" multiValued="true" termVectors="true" termOffsets="true" termPositions="true" />
  <dynamicField name="*_2g_ee_iq_sgffgfsgf_managed" type="text_2g_ee_iq_sgffgfsgf_managed" indexed="true" stored="true" required="false" multiValued="true" termVectors="true" termOffsets="true" termPositions="true" />
  <dynamicField name="*_2g_en_iq_sgffgfsgf_managed" type="text_2g_en_iq_sgffgfsgf_managed" indexed="true" stored="true" required="false" multiValued="true" termVectors="true" termOffsets="true" termPositions="true" />
  <copyField source="content" dest="content_ja_n_q_sgf_managed"         />
  <copyField source="content" dest="content_ja_n_i_sgffgf_managed"      />
  <copyField source="content" dest="content_ja_nn_iq_sgffgfsgf_managed" />
  <copyField source="content" dest="content_ja_ne_iq_sgffgfsgf_managed" />
  <copyField source="content" dest="content_ja_e_q_sgf_managed"         />
  <copyField source="content" dest="content_ja_e_i_sgffgf_managed"      />
  <copyField source="content" dest="content_ja_ee_iq_sgffgfsgf_managed" />
  <copyField source="content" dest="content_ja_en_iq_sgffgfsgf_managed" />
  <copyField source="content" dest="content_2g_n_q_sgf_managed"         />
  <copyField source="content" dest="content_2g_n_i_sgffgf_managed"      />
  <copyField source="content" dest="content_2g_nn_iq_sgffgfsgf_managed" />
  <copyField source="content" dest="content_2g_ne_iq_sgffgfsgf_managed" />
  <copyField source="content" dest="content_2g_e_q_sgf_managed"         />
  <copyField source="content" dest="content_2g_e_i_sgffgf_managed"      />
  <copyField source="content" dest="content_2g_ee_iq_sgffgfsgf_managed" />
  <copyField source="content" dest="content_2g_en_iq_sgffgfsgf_managed" />
シノニム辞書(リソース)

シノニム辞書(リソース)の内容の説明をする前に、例を挙げてリソースについて簡単な説明を行う。 ManagedSynonymGraphFilterを使用する場合、シノニム辞書(リソース)はテキストファイルで作成するのではなく、REST API(WEB API)を用いて作成することとなっている。 また、このリソースがSynonymGraphFilterの単なるテキストファイル辞書と大きく異なるのは、このリソース自体にトークナイザなどのフィルタの適用方法が含まれているということである。

例えば、SynonymGraphFilterを使用する場合はその適用方法をmanaged-schemaのフィールド型に以下のように直接定義する。

      <filter class="solr.SynonymGraphFilterFactory"
              expand="false"
              ignoreCase="true"
              synonyms="synonyms.txt"
              tokenizerFactory="solr.NGramTokenizerFactory"
              tokenizerFactory.minGramSize="2" tokenizerFactory.maxGramSize="2" />

一方、シノニム辞書(リソース)の場合は、単にリソース名を指定するだけである。

      <filter class="solr.ManagedSynonymGraphFilterFactory" managed="synonym-managed-xx-x" />

トークナイザに、NGramTokenizerFactoryを使用するとか、GramSizeの指定をどのようにするかなどはリソースの定義に含まれ、これもREST APIを通じて行う。

・リソースの初期化

下記、REST API(WEB API)の例において、basicはコレクション名である。

以下はリソースの初期化のREST APIの例である。

# text_jaタイプに使用する場合のリソース
curl -X PUT -H 'Content-type:application/json' --data-binary \
'{"initArgs":{"ignoreCase":true,"tokenizerFactory":"solr.JapaneseTokenizerFactory",\
"tokenizerFactory.mode":"normal","format":"solr"}}' \
"http://localhost:8983/solr/basic/schema/analysis/synonyms/synonym-managed-ja"

# text_2gタイプに使用する場合のリソース
curl -X PUT -H 'Content-type:application/json' --data-binary \
'{"initArgs":{"ignoreCase":true,"tokenizerFactory":"solr.NGramTokenizerFactory",\
"tokenizerFactory.maxGramSize":"2","tokenizerFactory.minGramSize":"2",format":"solr"}}' \
"http://localhost:8983/solr/basic/schema/analysis/synonyms/synonym-managed-2g"

上記では、JapaneseTokenizerを使用するリソース名称をsynonym-managed-ja、NGramTokenizerを使用するリソース名称をsynonym-managed-2gとしている。

・シノニムの追加、削除、取得

リソースの初期化ではexpand属性は指定できないため、SynonymGraphFilterFactoryのexpand=true/falseによる類義語の登録と等価な登録を行うためには、それぞれの語を全ての類義語に展開したルール(expand=trueに該当)での登録を行ったり、代表表記に正規化したルールでの登録(expand=falseに該当)を行う必要がある。

# シノニムの追加(expand=trueに該当)
$ curl -X PUT -H 'Content-type:application/json' --data-binary \
'{"managedMap":{"首相":["首相","内閣総理大臣"],"内閣総理大臣":["首相","内閣総理大臣"]}}' \
"http://localhost:8983/solr/basic/schema/analysis/synonyms/synonym-managed-ja"

# 上記のシノニムを一旦削除
$ curl -X DELETE \
"http://localhost:8983/solr/basic/schema/analysis/synonyms/synonym-managed-ja/首相"
$ curl -X DELETE \
"http://localhost:8983/solr/basic/schema/analysis/synonyms/synonym-managed-ja/内閣総理大臣"

# シノニムの追加(expand=falseに該当)
$ curl -X PUT -H 'Content-type:application/json' --data-binary \
'{"managedMap":{"首相":["首相"],"内閣総理大臣":["首相"]}}' \ 
"http://localhost:8983/solr/basic/schema/analysis/synonyms/synonym-managed-ja"

# シノニムの取得
$ curl -X GET \
"http://localhost:8983/solr/basic/schema/analysis/synonyms/synonym-managed-ja"

・取得されたシノニム(リソース内容)

{
  "responseHeader":{
    "status":0,
    "QTime":1},
  "synonymMappings":{
    "initArgs":{
      "ignoreCase":true,
      "tokenizerFactory":"solr.JapaneseTokenizerFactory",
      "tokenizerFactory.mode":"normal",
      "format":"solr"},
    "initializedOn":"2019-06-11T06:01:53.172Z",
    "updatedSinceInit":"2019-06-11T06:05:31.915Z",
  "managedMap":{
    "内閣総理大臣":[
      "首相"],
    "首相":[
      "首相"]}}}

※上記のリソース内容と同じものが、Solrシステムによって、confディレクトリの下に _schema_analysis_synonyms_[リソース名].json という名前で自動的に保存される。当該ファイルを確認することによってManagedSynonymGraphFilterFactoryの挙動やシノニムの登録状況を知る事もできる。また、このファイルを使用して、リソースの初期化と類義語登録を一度に行うこともできる。

ファイルを使用して、リソースの初期化&シノニムの追加を行う場合のREST API

$ curl -X PUT -H 'Content-type:application/json' \
-d @_schema_analysis_synonyms_synonym-managed-ja.json \
"http://localhost:8983/solr/basic/schema/analysis/synonyms/synonym-managed-ja"

【重要】実際には、シノニム辞書(リソース)が有効となるのは、コレクションをリロードしたタイミングである。

リソースについての簡単な説明が済んだところで実際に使用した辞書内容について説明する。

・expandでシノニムを適用した場合の辞書内容

「総理大臣」、「首相」、「総理」、「内閣総理大臣」どの語が来ても、それぞれの語が、「総理大臣」、「首相」、「総理」、「内閣総理大臣」という複数の語に展開され処理する方式である。

{
  "managedMap":{
    "総理大臣":[
      "総理大臣",
      "首相",
      "総理",
      "内閣総理大臣"
    ],
    "首相":[
      "総理大臣",
      "首相",
      "総理",
      "内閣総理大臣"
    ],
    "総理":[
      "総理大臣",
      "首相",
      "総理",
      "内閣総理大臣"
    ],
    "内閣総理大臣":[
      "総理大臣",
      "首相",
      "総理",
      "内閣総理大臣"
    ]
  }
}

・normalizeでシノニムを適用した場合の辞書内容

「総理大臣」、「首相」、「総理」、「内閣総理大臣」どの語が来ても、「総理大臣」という一つの語(代表表記)に置き換え処理する方式である。

{
  "managedMap":{
    "総理大臣":[
      "総理大臣"],
    "首相":[
      "総理大臣"],
    "総理":[
      "総理大臣"],
    "内閣総理大臣":[
      "総理大臣"]
  }
}

今回の検証はテストケースも多くスクリプトを作成し行ったが、初期化した2つのリソース(synonym-managed-ja、synonym-managed-2g)の語彙内容を入れ替えるということは行わず以下の4つのリソースをあらかじめ作成(及びコレクションのリロードを1回だけ)しておいてテストケースにより適宜4つのリソースを切り替えて使用するという方法を取った。

  • synonym-managed-ja-e
    トークナイザーにJapaneseTokenizerを使用し、それぞれの語を全ての類義語に展開した(expand=trueに該当)シノニム辞書(リソース)
  • synonym-managed-ja-n
    トークナイザーにJapaneseTokenizerを使用し、それぞれの語を1つの代表表記に正規化した(expand=falseに該当)シノニム辞書(リソース)
  • synonym-managed-2g-e
    トークナイザーにNGramTokenizerを使用し、それぞれの語を全ての類義語に展開した(expand=trueに該当)シノニム辞書(リソース)
  • synonym-managed-2g-n
    トークナイザーにNGramTokenizerを使用し、それぞれの語を1つの代表表記に正規化した(expand=falseに該当)シノニム辞書(リソース)
テストデータ

索引付けするテストデータは以下のcsvデータであり、第1フィールドがid、第2フィールドがcontentの内容である。

1,首相でございます
2,総理でございます
3,総理大臣でございます
4,内閣総理大臣でございます
5,内閣でございます
6,大臣でございます
7,内閣大臣でございます
検索結果の判定方法

16種類の検証用フィールドについて、「首相」、「総理」、「総理大臣」、「内閣総理大臣」を検索語としてそれぞれ検索した場合、シノニムを含む検索結果だけが得られるかを検証する。 「首相」、「総理」、「総理大臣」、「内閣総理大臣」のどの検索語で検索した場合でも、結果としてid1~4だけが得られるはずである。

具体的には以下を行い検証する。
  content_ja_n_q_sgf_managedを
    「首相」で検索した場合、id1~4だけが得られるか
    「総理」で検索した場合、id1~4だけが得られるか
    「総理大臣」で検索した場合、id1~4だけが得られるか
    「内閣総理大臣」で検索した場合、id1~4だけが得られるか

  content_ja_n_i_sgffgf_managedを
    「首相」で検索した場合、id1~4だけが得られるか
    「総理」で検索した場合、id1~4だけが得られるか
    「総理大臣」で検索した場合、id1~4だけが得られるか
    「内閣総理大臣」で検索した場合、id1~4だけが得られるか
        ・
        ・
        ・

(ステップ1)正しい検索結果を返すフィールド型の選出

管理画面によるテストのサンプル画面

16種類の検証用フィールドを用いてのテスト実施は複雑なので、実際にはスクリプトを作成し行ったが、Solr管理画面からでも次のように設定すれば特定のパラメーターの組み合わせを試すことができる。

en3-managed管理画面1 en3-managed管理画面2


edismaxを使用しての検索の説明は、「KandaSearch ではじめる Apache Solr 入門」を参照していただくとして、 ここでは、ハイライトに関するパラメータなどについて簡単に説明する。

hlチェックをするとハイライトを行う
hl.flハイライト対象のフィールド。スペースやカンマで区切ることで複数指定できる。
hl.simple.preハイライトされる文字列の前に出力するタグ(文字列) ※注1
hl.simple.postハイライトされる文字列の後に出力するタグ(文字列) ※注1
hl.requireFieldMatchtrueにすると、ハイライト対象のフィールドが複数存在するときにこのフィールドに関するクエリ検索語のみがハイライトされる。
例えばtitle、bodyともにハイライト対象になっており、bodyに「先生」も「学校」も存在し、「title:先生 OR body:学校」で検索を行ったとすると、bodyのハイライトは、このパラメータの値により次のようになる。 trueなら、bodyに関する検索語の「学校」しかハイライトされないが、falseならtitleに関する検索語の「先生」もハイライトされる。
hl.usePhraseHighlighterクエリにフレーズ検索が存在するときはフレーズだけをハイライトする。
なお、Solr管理画面のusePhraseHighlighterは、usePhraseHighLighter(Lが大文字)として解釈されてしまうようでうまく機能しないので注意されたい。
これを回避するためには、例えば以下のようにusePhraseHighLighterを修正したクエリ式をアドレスバーに入れる必要がある。
https://●●●●●●●●●●.i.kandasearch.com/solr/synonym/select?debugQuery=true&df=content_ja&hl.fl=content_ja&hl.usePhraseHighlighter=true&hl=true&indent=true&q.op=OR&q=XXXXXXXXXX&sow=on
hl.highlightMultiTermワイルドカード検索やあいまい検索等でもハイライトを行うようにする。
hl.methodハイライトの方式。unified,original,fastVectorの三つの方式がある。

unified最新のハイライター。多くのケースで最もよいと言われている。
original最も古くからあるハイライター。
fastVectororiginalが使用できないN-gramフィールドでハイライトをしたい場合に導入されたハイライター。
sowSplit On Whitespaceの略。sow=falseがデフォルト動作となっており、sow=trueを明示しないと、autoGeneratePhraseQueries=trueの設定が効かなくなってしまい、フレーズ展開されなくなるので、 日本語の検索で期待通りの動作をしなくなってしまう。したがって今回の検証は常にsow=trueとして行った。※注2

※注1 fastVectorの場合は検索キーワードごとにハイライトの色を変えることができるので、solrconfig.xmlに以下のように設定する。

  <fragmentsBuilder name="colored"
                    class="solr.highlight.ScoreOrderFragmentsBuilder">
    <lst name="defaults">
      <str name="hl.tag.pre"><![CDATA[
           <b style="background:yellow">,<b style="background:lawgreen">,
           <b style="background:aquamarine">,<b style="background:magenta">,
           <b style="background:palegreen">,<b style="background:coral">,
           <b style="background:wheat">,<b style="background:khaki">,
           <b style="background:lime">,<b style="background:deepskyblue">]]></str>
      <str name="hl.tag.post"><![CDATA[</b>]]></str>
    </lst>
  </fragmentsBuilder>

※注2 sowについてはdebugQueryをonにして、作成されるクエリを確認すると以下の様に違いがあることがわかる。

sow=trueの場合

"parsedquery":"+DisjunctionMaxQuery((content_ja_nn_iq_sgffgfsgf_managed:\"総理 大臣\"))"
"parsedquery_toString":"+(content_ja_nn_iq_sgffgfsgf_managed:\"総理 大臣\")"

このクエリによる検索結果は以下のようになる。

id結果
1首相でございます
2総理でございます
3総理大臣でございます
4内閣総理大臣でございます
sow=falseの場合

"parsedquery":"+DisjunctionMaxQuery(((content_ja_nn_iq_sgffgfsgf_managed:総理 content_ja_nn_iq_sgffgfsgf_managed:大臣)))
"parsedquery_toString":"+((content_ja_nn_iq_sgffgfsgf_managed:総理 content_ja_nn_iq_sgffgfsgf_managed:大臣))"

このクエリによる検索結果は以下のようになる。id6、7がノイズとして検索されてしまっている。

id結果
1首相でございます
2総理でございます
3総理大臣でございます
4内閣総理大臣でございます
6大臣でございます
7内閣大臣でございます
(ステップ1)正しい検索結果を返すフィールド型の選出結果

以下のフィールド型による検索が正しい検索結果を返した。

text_ja_nn_iq_sgffgfsgf_managed
text_ja_ne_iq_sgffgfsgf_managed
text_ja_e_q_sgf_managed
text_ja_e_i_sgffgf_managed
text_ja_ee_iq_sgffgfsgf_managed
text_ja_en_iq_sgffgfsgf_managed
text_2g_nn_iq_sgffgfsgf_managed
text_2g_ne_iq_sgffgfsgf_managed
text_2g_e_q_sgf_managed
text_2g_e_i_sgffgf_managed
text_2g_ee_iq_sgffgfsgf_managed
text_2g_en_iq_sgffgfsgf_managed

但しexpandでシノニム適用する場合は、索引時か検索時の片方だけに使用すればよいはずなのでtext_ja_ne_iq_sgffgfsgf_managed、text_ja_ee_iq_sgffgfsgf_managed、text_ja_en_iq_sgffgfsgf_managed、text_2g_ne_iq_sgffgfsgf_managed、text_2g_ee_iq_sgffgfsgf_managed、text_2g_en_iq_sgffgfsgf_managedは無駄である。 したがって以下が「(ステップ2)正しいハイライトがなされるフィールド型の選出」候補として残る。

text_ja_nn_iq_sgffgfsgf_managed
text_ja_e_q_sgf_managed
text_ja_e_i_sgffgf_managed
text_2g_nn_iq_sgffgfsgf_managed
text_2g_e_q_sgf_managed
text_2g_e_i_sgffgf_managed

(ステップ2)正しいハイライトがなされるフィールド型の選出

正しい検索結果を返した上記6候補のフィールド型のうちハイライトについてテストを行う。

unified,original,fastVectorの3つの方式でテストを行った。 なお、fastVectorを使用するには、フィールドの定義で「termVectors=”true” termOffsets=”true” termPositions=”true” 」が必要になる。 これがないフィールドをfastVectorでハイライトしようとした場合は、Solrは自動的にoriginalでハイライトを行う。 fastVectorで検証する際には、お節介とも思われる仕様である。fastVectorのハイライトがうまくいったと思っていてもその実originalでハイライトされていたという事になりかねないからだ。 fastVectorがoriginalで動いたかどうかはログファイルもしくは管理画面のLoggingを見て確認する必要がある。

・ログファイル

2021-11-21 19:22:46.578 WARN  (qtp307488715-50) [   x:synonym] o.a.s.h.DefaultSolrHighlighter Solr will use the standard Highlighter instead of FastVectorHighlighter because the does not store TermVectors with TermPositions and TermOffsets. field content_ja_nn_iq_sgffgfsgf_managed_noterm

・Solr管理画面のLogging

勝手にoriginal方式で検索
(ステップ2)正しいハイライトがなされるフィールド型の選出結果

ほとんどの場合において正常にハイライトされた。

id結果
1首相でございます
2総理でございます
3総理大臣でございます
4内閣総理大臣でございます

正常にハイライトされてないのは、以下の場合であった。

①text_ja_e_q_sgf_managedのfastVectorによるハイライトが全くされない(結果は空文字である)

fastVectorはTermQueryとPhraseQueryとBooleanQueryのみサポートしている。クエリ時シノニム展開するとSpanQueryになってしまうSynonymGraphはサポートできない。
text_ja_e_q_sgf_managedの首相による検索時のクエリ式をdebugQuery=onとして見てみると以下のようになっている。

parsedquery_toString":"+(spanOr([spanNear([content_ja_e_q_sgf_managed:内閣, content_ja_e_q_sgf_managed:総理, content_ja_e_q_sgf_managed:大臣], 0, true), content_ja_e_q_sgf_managed:総理, spanNear([content_ja_e_q_sgf_managed:総理, content_ja_e_q_sgf_managed:大臣], 0, true), content_ja_e_q_sgf_managed:首相]))


【注意】text_ja_e_i_sgffgf_managedを「総理」で検索した場合に、unified、fastVectorによるハイライトは正常であった。これは、SynonymGraphFilterを用いた同様の検索においてNGとなっていたものである。


なお、content_ja_e_q_sgf_managedの正常動作では、「総理大臣」、「内閣総理大臣」のハイライトは以下のようにトークン単位で行われるが、これは仕様である。

id結果
3<em>総理</em><em>大臣</em>でございます
4<em>内閣</em><em>総理</em><em>大臣</em>でございます

結論

シノニムを適用しハイライトを行う場合には、以下が良いように思える。

  • text_ja_nn_iq_sgffgfsgf_managedのunified、original、fastVectorによるハイライト
  • text_ja_e_q_sgf_managedのunified、originalによるハイライト
  • text_ja_e_i_sgffgf_managedのunified、original、fastVectorによるハイライト
  • text_2g_nn_iq_sgffgfsgf_managedのunified、original、fastVectorによるハイライト
  • text_2g_e_q_sgf_managedのunified、originalによるハイライト
  • text_2g_e_i_sgffgf_managedのunified、original、fastVectorによるハイライト

但し、正常にハイライトを行うという以外にも、以下①②のような注意が必要である。

①索引時のシノニム適用には以下のような特徴がある。
・シノニム辞書に変更があった場合に再インデックスが必要である。
・expandでシノニムを適用する場合には索引の肥大に繋がる。また文書長が長くなることでスコアが不当に低減する

②fastVectorに必要な「termVectors=”true” termOffsets=”true” termPositions=”true” 」も索引の肥大に繋がるので可能ならfastVector方式でのハイライトは避ける。
※unified又はoriginal方式で正確にハイライトができるのであれば、fastVectorによるハイライトは出来なくてもよい。

①②をかんがみると、シノニム展開を適用した場合の検索・ハイライト表示には、以下のフィールド型を使用するのがもっともよいように考えられる。

フィールド型text_ja_e_q_sgf_managedのunified又はoriginal方式によるハイライト

  <fieldType name="text_ja_e_q_sgf_managed" class="solr.TextField" autoGeneratePhraseQueries="true" positionIncrementGap="100" >
    <analyzer type="index">
      <tokenizer class="solr.JapaneseTokenizerFactory" mode="normal"/>
    </analyzer>
    <analyzer type="query">
      <tokenizer class="solr.JapaneseTokenizerFactory" mode="normal"/>
      <filter class="solr.ManagedSynonymGraphFilterFactory" managed="synonym-managed-ja-e" />
    </analyzer>
  </fieldType>

フィールド型text_2g_e_q_sgf_managedのunified又はoriginal方式によるハイライト

  <fieldType name="text_2g_e_q_sgf_managed" class="solr.TextField" autoGeneratePhraseQueries="true" positionIncrementGap="100" >
    <analyzer type="index">
      <tokenizer class="solr.NGramTokenizerFactory" minGramSize="2" maxGramSize="2" />
    </analyzer>
    <analyzer type="query">
      <tokenizer class="solr.NGramTokenizerFactory" minGramSize="2" maxGramSize="2" />
      <filter class="solr.ManagedSynonymGraphFilterFactory" managed="synonym-managed-2g-e"/>
    </analyzer>
  </fieldType>

以上、Apache Solrでシノニムを考慮した日本語の検索とハイライトにおけるベストプラクティスを得ることができた。



本記事がお客様の業務に少しでもお役に立てば幸いです。



著者略歴


某精密機器メーカーにてNLP業務に従事し、形態素解析器、全文検索エンジン(転置索引)、強調表示器作成を担当する。その後、音声入力による質問応答タスクの研究や多言語翻訳システムの構築等を経験。


トレーニングコース

ロンウイットのトレーニングは、Lucene/Solrの経験豊富なコミッターの
監修のもと開発されたハンズオン(実習)形式のコースです。

セミナー

情報検索の基礎からクラウド型企業向け検索エンジンの導入手順、ユーザー事例まで解説!