INFORMATION
テクノロジ

類義語検索と類義語ハイライト

阿部 慎一朗 著

はじめに

一般的な検索エンジンでは、類義語の検索や類義語のハイライトができます(類義語とは、旅館|ホテル、首相|内閣総理大臣、木村拓哉|キムタク、ロンウイットサブスクリプション|RCSS、などのように、商品名・人名などの略語や任意の語句の言い換え表現を指します)。一般的なRDBの場合、単純なSQL文であれば、文字列の部分一致で検索しにいきますので、文字列が違う類義語の検索はサポートできていません(ハイライトもできません)。したがって類義語が検索できる点は、検索エンジンの特徴のひとつと言えます。Solrでは、日本語の類義語検索や類義語ハイライトができますので、本記事では、使用方法とその注意点をご説明いたします。

Solrでは、類義語フィルタをフィールドタイプに追加して、類義語辞書を用意して、検索対象ドキュメントをインデクシングすれば、類義語検索や類義語ハイライトがすぐに実現できます。しかしながら、類義語フィルタ(=シノニムフィルタ、SynonymFilter)が持っている設定に注意しないと、日本語固有の問題で、トークンのポジションずれ・オフセットずれが発生し、最悪の場合類義語検索がヒットしない現象や、類義語のハイライトがずれて出力される、といった現象が発生します。類義語フィルタを導入することで、類義語を含むドキュメントをヒットさせてシステム出力を増やし、検索結果の再現率を高めようとしたのにもかかわらず、逆にシステム出力が減って再現率が低下してしまう、という本末転倒な事態を招きかねません。そこで類義語検索と類義語ハイライトの最善なアプローチをご説明したいと思います。

目次

SynonymFilterFactoryの2つの実装

Solrでは、schema.xmlで類義語フィルタを定義すると、Luceneが持っているSynonymFilterFactoryを呼びます。SynonymFilterFactoryのコンストラクタでは、luceneMatchVersion(=Solrのバージョン)が3.3以前ならば、SlowSynonymFilterFactoryが使用され、3.4以上ならば、FSTSynonymFilterFactoryが呼ばれることになっています(参考ソース)。

SlowSynonymFilterは最初に開発された類義語フィルタであり、従来から日本語検索でも利用されています。FSTSynonymFilterは後から新しく開発された類義語フィルタです。前者は類義語辞書をcharの配列をキーにして管理するCharArrayMapにロードするのに対し、後者は類義語辞書をFSTで管理するSynonymMapにロードします(FSTはSolr/LuceneのいくつかのAnalyzerやSpellCheckComponentなどでも利用されているオブジェクトで、lookupの動作が速くてメモリ節約の特徴をもった実装です。FSTSynonymFilterが処理が速いということなので、最初の類義語フィルタについて”Slow”SynonymFilterと名前を変更されています)。インデクシング時に、与えられた文章データを単語分割し、ついで類義語辞書を使ってシノニム展開させて転置索引を作成します。検索リクエストによって、類義語検索や類義語ハイライトが実現することができます。

現行のSolrでは、類義語フィルタを定義すると、デフォルトではFSTSynonymFilterで処理します。ところがFSTSynonymFilterで日本語を処理するとき、類義語ハイライトにずれが生じるケースがあります。一方、SlowSynonymFilterではこのハイライトずれは発生しません。FSTSynonymFilterとSlowSynonymFilterで、類義語トークンの出力が異なっており、結果としてハイライト挙動に違いがあります。この挙動について以下詳しく見ていきます。

検証環境:schema.xmlの設定とサンプルデータ

  1. Solr 4.7.2(Java 6で動作できる最後のバージョンを採用します。ちなみに4.8以上はJava 7以上が必要です。)
  2. schema.xmlに検証用フィールドタイプを設定
  3. 各種フィールドタイプ

    text_ja_synonymJapaneseTokenizer + FSTSynonymFilterを使用
    text_ja_synonym33JapaneseTokenizer + SlowSynonymFilterを使用
    text_2g_synonym2gramのTokenizer + FSTSynonymFilterを使用
    text_2g_synonym332gramのTokenizer + SlowSynonymFilterを使用
    text_1g_synonym1gramのTokenizer + FSTSynonymFilterを使用
    text_1g_synonym331gramのTokenizer + SlowSynonymFilterを使用

    • autoGeneratePhraseQueries=trueにして自動フレーズクエリ生成して検索し、ゴミデータをヒットさせないようにしています。
    • JapaneseTokenizerはnormalモードにしています。searchモードでは複合語の細かい単語分割と同時に複合語のオリジナルトークンをシノニムとして自動生成する機能を持っているので、SynonymFilterと併用利用ができません。
    • SynonymFilterはインデクシング時にシノニム展開(expand=true)させて、クエリ側ではシノニムフィルタを扱いません。クエリ側でシノニム展開すると、MultiPhraseQuery(複雑なクエリ検索式)が生成されて、本来ヒットすべきものが最悪ヒットできないという問題が起きるからです。
    • SynonymFilterのluceneMatchVersion属性で3.3を指定すると、SlowSynonymFilterを採用できます。指定しなければFSTSynonymFilterが採用されます。
        <fieldType name="text_ja_synonym" class="solr.TextField" positionIncrementGap="100" autoGeneratePhraseQueries="true">
          <analyzer type="index">
            <tokenizer class="solr.JapaneseTokenizerFactory" mode="normal"/>
            <filter class="solr.SynonymFilterFactory" synonyms="synonyms-index.txt"
                    tokenizerFactory="solr.JapaneseTokenizerFactory"
                    tokenizerFactory.mode="normal"
                    ignoreCase="true" expand="true"/>
          </analyzer>
          <analyzer type="query">
            <tokenizer class="solr.JapaneseTokenizerFactory" mode="normal"/>
          </analyzer>
        </fieldType>
    
        <fieldType name="text_ja_synonym33" class="solr.TextField" positionIncrementGap="100" autoGeneratePhraseQueries="true">
          <analyzer type="index">
            <tokenizer class="solr.JapaneseTokenizerFactory" mode="normal"/>
            <filter class="solr.SynonymFilterFactory" synonyms="synonyms-index.txt"
                    tokenizerFactory="solr.JapaneseTokenizerFactory"
                    tokenizerFactory.mode="normal"
                    luceneMatchVersion="3.3"
                    ignoreCase="true" expand="true"/>
          </analyzer>
          <analyzer type="query">
            <tokenizer class="solr.JapaneseTokenizerFactory" mode="normal"/>
          </analyzer>
        </fieldType>
    
        <fieldType name="text_2g_synonym" class="solr.TextField" positionIncrementGap="100" autoGeneratePhraseQueries="true">
          <analyzer type="index">
            <tokenizer class="solr.NGramTokenizerFactory" minGramSize="2" maxGramSize="2"/>
            <filter class="solr.SynonymFilterFactory" synonyms="synonyms-index.txt"
                    tokenizerFactory="solr.NGramTokenizerFactory"
                    tokenizerFactory.minGramSize="2"
                    tokenizerFactory.maxGramSize="2"
                    ignoreCase="true" expand="true"/>
          </analyzer>
          <analyzer type="query">
            <tokenizer class="solr.NGramTokenizerFactory" minGramSize="2" maxGramSize="2"/>
          </analyzer>
        </fieldType>
      
        <fieldType name="text_2g_synonym33" class="solr.TextField" positionIncrementGap="100" autoGeneratePhraseQueries="true">
          <analyzer type="index">
            <tokenizer class="solr.NGramTokenizerFactory" minGramSize="2" maxGramSize="2"/>
            <filter class="solr.SynonymFilterFactory" synonyms="synonyms-index.txt"
                    tokenizerFactory="solr.NGramTokenizerFactory"
                    tokenizerFactory.minGramSize="2"
                    tokenizerFactory.maxGramSize="2"
                    luceneMatchVersion="3.3"
                    ignoreCase="true" expand="true"/>
          </analyzer>
          <analyzer type="query">
            <tokenizer class="solr.NGramTokenizerFactory" minGramSize="2" maxGramSize="2"/>
          </analyzer>
        </fieldType>
    
        <fieldType name="text_1g_synonym" class="solr.TextField" positionIncrementGap="100" autoGeneratePhraseQueries="true">
          <analyzer type="index">
            <tokenizer class="solr.NGramTokenizerFactory" minGramSize="1" maxGramSize="1"/>
            <filter class="solr.SynonymFilterFactory" synonyms="synonyms-index.txt"
                    tokenizerFactory="solr.NGramTokenizerFactory"
                    tokenizerFactory.minGramSize="1"
                    tokenizerFactory.maxGramSize="1"
                    ignoreCase="true" expand="true"/>
          </analyzer>
          <analyzer type="query">
            <tokenizer class="solr.NGramTokenizerFactory" minGramSize="1" maxGramSize="1"/>
          </analyzer>
        </fieldType>
    
        <fieldType name="text_1g_synonym33" class="solr.TextField" positionIncrementGap="100" autoGeneratePhraseQueries="true">
          <analyzer type="index">
            <tokenizer class="solr.NGramTokenizerFactory" minGramSize="1" maxGramSize="1"/>
            <filter class="solr.SynonymFilterFactory" synonyms="synonyms-index.txt"
                    tokenizerFactory="solr.NGramTokenizerFactory"
                    tokenizerFactory.minGramSize="1"
                    tokenizerFactory.maxGramSize="1"
                    luceneMatchVersion="3.3"
                    ignoreCase="true" expand="true"/>
          </analyzer>
          <analyzer type="query">
            <tokenizer class="solr.NGramTokenizerFactory" minGramSize="1" maxGramSize="1"/>
          </analyzer>
        </fieldType>
    
  4. schema.xmlにダイナミックフィールドとコピーフィールドを設定
    • FastVectorHighlighter(略称:FVH)を使用するために、3つのterm*属性をtrueとして追加設定しています。ハイライトでは、Ngramの単語分割でもハイライトができ、検索リクエストのqパラメータで定義した単語についてフレーズまとまり単位でハイライトができ、しかも高速にハイライトできるFastVectorHighlighterを採用します。デフォルトSolrハイライタは、FVHのこれらの特徴を持っていないので使用しません。
       <dynamicField name="*_ja_synonym" type="text_ja_synonym" indexed="true" stored="true" multiValued="true" termVectors="true" termOffsets="true" termPositions="true"/>
       <dynamicField name="*_ja_synonym33" type="text_ja_synonym33" indexed="true" stored="true" multiValued="true" termVectors="true" termOffsets="true" termPositions="true"/>
    
       <dynamicField name="*_2g_synonym" type="text_2g_synonym" indexed="true" stored="true" multiValued="true" termVectors="true" termOffsets="true" termPositions="true"/>
       <dynamicField name="*_2g_synonym33" type="text_2g_synonym33" indexed="true" stored="true" multiValued="true" termVectors="true" termOffsets="true" termPositions="true"/>  
    
       <dynamicField name="*_1g_synonym" type="text_1g_synonym" indexed="true" stored="true" multiValued="true" termVectors="true" termOffsets="true" termPositions="true"/>
       <dynamicField name="*_1g_synonym33" type="text_1g_synonym33" indexed="true" stored="true" multiValued="true" termVectors="true" termOffsets="true" termPositions="true"/>
    
       <copyField source="content" dest="content_ja_synonym"/>
       <copyField source="content" dest="content_ja_synonym33"/>
    
       <copyField source="content" dest="content_2g_synonym"/>
       <copyField source="content" dest="content_2g_synonym33"/>
    
       <copyField source="content" dest="content_1g_synonym"/>
       <copyField source="content" dest="content_1g_synonym33"/>
    
  5. 類義語辞書
    • synonyms-index.txtを用意して次の類義語を登録します。
    • 1行で複数の単語の類義語を定義できるカンマ区切り形式を採用します。片方向形式は採用しません。理由は、前述でインデクシング時にシノニム展開するようにするのがセオリーとしているからで、もし片方向形式を採用すると、インデクシング側だけでなくクエリ側にも類義語の正規化を行うために定義が必要になるからです。
    内閣総理大臣,首相
    
  6. インデクシングデータ
    • Solrのexample/exampledocsディレクトリにあるpost.shでSolrにポストします。
    • idとcontentフィールドだけ用意して、あとはcopyFieldとdynamicFieldを使って、各フィールドタイプの転置索引を作るようにしています。
    <add>
    <doc>
      <field name="id">1</field>
      <field name="content">首相でございます</field>
    </doc>
    <doc>
      <field name="id">2</field>
      <field name="content">内閣総理大臣でございます</field>
    </doc>
    </add>
    
  7. 検証のための検索リクエスト
  8. http://localhost:8983/solr/select?q=検索キーワード
    &fl=id
    &df=任意のフィールド名
    &hl=on
    &hl.useFastVectorHighlighter=true
    &hl.tag.pre=<b>&hl.tag.post=</b>
    
    • ブラウザのURL窓に直接打ってリクエストします。
    • qパラメータの検索キーワードは、「首相」や「内閣総理大臣」などの単語を入れて検索させます。
    • df(デフォルトサーチフィールド)で各フィールドタイプの転置索引を検索するようにフィールド指定します。ちなみにdfフィールドがそのままハイライトフィールドになるので、hl.flは指定しません。
    • FVHを発動させ、検索リクエストを投げると、返却されるXMLのhighlightingセクションに強調の<b>タグを付けられたcontentフィールドが返却されます。

JapaneseTokenizerとFSTSynonymFilter

  1. 検索リクエスト
  2. qパラメータに類義語の単語をセットして、類義語検索のヒットと類義語ハイライトを確認します。

    #ケース1 「首相」
    http://localhost:8983/solr/select?q=首相&fl=id&df=content_ja_synonym&hl=on&hl.useFastVectorHighlighter=true&hl.tag.pre=<b>&hl.tag.post=</b>
    #ケース2 q=内閣総理大臣 //他の条件は同じ
    #ケース3 q=総理 //他の条件は同じ
    #ケース4 q=首相で //他の条件は同じ
    
  3. 結果
  4. #ケース1 2件ヒットして、ハイライトができます。
    id:1首相でございます
    id:2内閣総理大臣でございます
    #ケース2 2件ヒットしますが、id:1がハイライトずれを起こします。
    id:1首相でございます
    id:2内閣総理大臣でございます
    #ケース3 2件ヒットしますが、id:1がハイライトずれを起こします。
    id:1首相ございます
    id:2内閣総理大臣でございます
    #ケース4 1件しかヒットできない問題が発生します。
    id:1首相でございます
    id:2ヒットしない。

  5. 解説
  6. FSTSynonymFilterでは以下のようにトークン(単語テキスト[ポジション:開始オフセット,終了オフセット])を生成します。

    #content:首相でございます
    |首相[1:0,2]|で [2:2,3]|ござい[3:3,6]|ます
    |内閣[1:0,2]|総理[2:2,3]|大臣 [3:3,6]|
    #content:内閣総理大臣でございます
    |首相[1:0,6]                    |で[4:6,7]|ござい|ます
    |内閣[1:0,2]|総理[2:2,4]|大臣[3:4,6]|
    

    ケース2の問題は、ヒットした「内閣総理大臣」のオフセットが0-6であり、この範囲をハイライトしようとして、「首相でござい」が0-6の部分にあたるので、ズレています。

    ケース3の問題は、ヒットした「総理」のオフセットが2-3であり、この範囲をハイライトしようとして、「で」が2-3の部分にあたるので、ズレています。

    ケース4の問題は、クエリ「首相で」がautoGeneratePhraseQueriesによって「”首相 で”」というフレーズクエリになります。フレーズ検索はダブルクォートした単語群のポジションが増分1でヒットさせようとするので、「首相」のポジションが1で、「で」のポジションが2でなければヒットできません。「で」が4になっているのでポジションがズレています。


    FSTSynonymFilterは、もともとシングルトークンを類義語定義したときに効果を発揮するものですが、日本語の場合は類義語で複数トークンに分割されるような複合語が多いので、使用には向いていません。

JapaneseTokenizerとSlowSynonymFilter

  1. 検索リクエスト
  2. qパラメータに類義語の単語をセットして、類義語検索のヒットと類義語ハイライトを確認します。

    #ケース1 「首相」
    http://localhost:8983/solr/select?q=首相&fl=id&df=content_ja_synonym33&hl=on&hl.useFastVectorHighlighter=true&hl.tag.pre=<b>&hl.tag.post=</b>
    #ケース2 q=内閣総理大臣 //他の条件は同じ
    #ケース3 q=総理 //他の条件は同じ
    #ケース4 q=首相で //他の条件は同じ
    
  3. 結果
  4. #ケース1 2件ヒットして、ハイライトができます。
    id:1首相でございます
    id:2内閣総理大臣でございます
    #ケース2 2件ヒットして、ハイライトができます。
    id:1首相でございます
    id:2内閣総理大臣でございます
    #ケース3 2件ヒットして、ハイライトができます。
    id:1首相でございます
    id:2内閣総理大臣でございます
    #ケース4 ヒットできない問題が発生します。
    id:1ヒットしない。
    id:2ヒットしない。

  5. 解説
  6. SlowSynonymFilterでは以下のようにトークン(単語テキスト[ポジション:開始オフセット,終了オフセット])を生成します。

    #content:首相でございます
    |首相[1:0,2]                     |で [4:2,3]|ござい|ます
    |内閣[1:0,2]|総理[2:0,2]|大臣 [3:0,2]|
    #content:内閣総理大臣でございます
    |首相[1:0,6]                    |で[4:6,7]|ござい|ます
    |内閣[1:0,6]|総理[2:0,6]|大臣[3:0,6]|
    

    ケース1と2は、類義語がすべて同一オフセットになるので、ハイライトがズレません。

    ケース3は、ヒットした「総理」のオフセットが0-2あるいは0-6であり、この範囲をハイライトしようするので、「首相」や「内閣総理大臣」をハイライトします。

    ケース4の問題は、クエリ「首相で」がautoGeneratePhraseQueriesによって「”首相 で”」というフレーズクエリになります。フレーズ検索はダブルクォートした単語群のポジションが増分1でヒットさせようとするので、「首相」のポジションが1で、「で」のポジションが2でなければヒットできません。2つのドキュメントとも、「で」が4になっているのでポジションが一致しません。


    SlowSynonymFilterは、類義語展開したとき、類義語のオフセットをすべて同一にしているので、ハイライトはFSTSynonymFilterに比べてズレがおきません。

NGramTokenizer(2gram)とFSTSynonymFilter

  1. 検索リクエスト
  2. qパラメータに類義語の単語をセットして、類義語検索のヒットと類義語ハイライトを確認します。

    #ケース1 「首相」
    http://localhost:8983/solr/select?q=首相&fl=id&df=content_2g_synonym&&hl=on&hl.useFastVectorHighlighter=true&hl.tag.pre=<b>&hl.tag.post=</b>
    #ケース2 q=内閣総理大臣 //他の条件は同じ
    #ケース3 q=総理 //他の条件は同じ
    #ケース4 q=首相で //他の条件は同じ
    
  3. 結果
  4. #ケース1 2件ヒットして、ハイライトができます。
    id:1首相でございます
    id:2内閣総理大臣でございます
    #ケース2 2件ヒットしますが、id:1がハイライトずれを起こします。
    id:1首相でございます
    id:2内閣総理大臣でございます
    #ケース3 2件ヒットしますが、id:1がハイライトずれを起こします。
    id:1首相でございます
    id:2内閣総理大臣でございます
    #ケース4 1件しかヒットできない問題が発生します。
    id:1首相でございます
    id:2ヒットしない。

  5. 解説
  6. FSTSynonymFilterでは以下のようにトークン(単語テキスト[ポジション:開始オフセット,終了オフセット])を生成します。

    #content:首相でございます
    |首相[1:0,2]|相で[2:1,3]|でご[3:2,4]|ござ[4:3,5]|ざい[5:4,6]|いま[6:5,7]|ます[7:6,8]
    |内閣[1:0,2]|閣総[2:1,3]|総理[3:2,4]|理大[4:3,5]|大臣[5:4,6]|
    #content:内閣総理大臣でございます
    |首相[1:0,6]                                        |臣で[6:5,7]|でご[7:6,8]|ござ|ざい|いま|ます
    |内閣[1:0,2]|閣総[2:1,3]|総理[3:2,4]|理大[4:3,5]|大臣[5:4,6]|
    

    ケース2の問題は、ヒットした「内閣総理大臣」のオフセットが0-6であり、この範囲をハイライトしようとして、「首相でござい」が0-6の部分にあたるので、ズレています。

    ケース3の問題は、ヒットした「総理」のオフセットが2-4であり、この範囲をハイライトしようとして、「でご」が2-4の部分にあたるので、ズレています。

    ケース4の問題は、クエリ「首相で」がautoGeneratePhraseQueriesによって「”首相 相で”」という検索式で、フレーズクエリになります。フレーズ検索はダブルクォートした単語群のポジションが増分1でヒットさせようとするので、「首相」のポジションが1で、「相で」のポジションが2でなければヒットできません。「相で」がなく、「臣で」がありますが、6になっているのでポジションがズレています。


    NGramの場合はNGramの単語分割ベースで、ポジションとオフセットがふられ、かつFSTSynonymFilterで類義語もNGramで複数トークンに分割されて調整される結果、ハイライトずれとヒット漏れが発生する場合があります。

NGramTokenizer(2gram)とSlowSynonymFilter

  1. 検索リクエスト
  2. qパラメータに類義語の単語をセットして、類義語検索のヒットと類義語ハイライトを確認します。

    #ケース1 「首相」
    http://localhost:8983/solr/select?q=首相&fl=id&df=content_2g_synonym33&hl=on&hl.useFastVectorHighlighter=true&hl.tag.pre=<b>&hl.tag.post=</b>
    #ケース2 q=内閣総理大臣 //他の条件は同じ
    #ケース3 q=総理 //他の条件は同じ
    #ケース4 q=首相で //他の条件は同じ
    
  3. 結果
  4. #ケース1 2件ヒットして、ハイライトができます。
    id:1首相でございます
    id:2内閣総理大臣でございます
    #ケース2 2件ヒットして、ハイライトができます。
    id:1首相でございます
    id:2内閣総理大臣でございます
    #ケース3 2件ヒットして、ハイライトができます。
    id:1首相でございます
    id:2内閣総理大臣でございます
    #ケース4 ヒットできない問題が発生します。
    id:1ヒットしない。
    id:2ヒットしない。

  5. 解説
  6. SlowSynonymFilterでは以下のようにトークン(単語テキスト[ポジション:開始オフセット,終了オフセット])を生成します。

    #content:首相でございます
    |首相[1:0,2]                                        |相で[6:1,3]|でご[7:2,4]|ござ|ざい|いま|ます
    |内閣[1:0,2]|閣総[2:0,2]|総理[3:0,2]|理大[4:0,2]|大臣[5:0,2]|
    #content:内閣総理大臣でございます
    |首相[1:0,6]                                        |臣で[6:5,7]|でご[7:6,8]|ござ|ざい|いま|ます
    |内閣[1:0,6]|閣総[2:0,6]|総理[3:0,6]|理大[4:0,6]|大臣[5:0,6]|
    

    ケース1と2は、類義語がすべて同一オフセットになるので、ハイライトがズレません。

    ケース3は、ヒットした「総理」のオフセットが0-2あるいは0-6であり、この範囲をハイライトしようするので、「首相」や「内閣総理大臣」をハイライトします。

    ケース4の問題は、クエリ「首相で」がautoGeneratePhraseQueriesによって「”首相 相で”」というフレーズクエリになります。フレーズ検索はダブルクォートした単語群のポジションが増分1でヒットさせようとするので、「首相」のポジションが1で、「相で」のポジションが2でなければヒットできません。「相で」が6になっているのでポジションが一致しません。


    SlowSynonymFilterは、類義語展開したとき、類義語のオフセットをすべて同一にしているので、ハイライトはFSTSynonymFilterに比べてズレがおきません。

NGramTokenizer(1gram)とFSTSynonymFilter

  1. 検索リクエスト
  2. qパラメータに類義語の単語をセットして、類義語検索のヒットと類義語ハイライトを確認します。

    #ケース1 「首相」
    http://localhost:8983/solr/select?q=首相&fl=id&df=content_1g_synonym&&hl=on&hl.useFastVectorHighlighter=true&hl.tag.pre=<b>&hl.tag.post=</b>
    #ケース2 q=内閣総理大臣 //他の条件は同じ
    #ケース3 q=総理 //他の条件は同じ
    #ケース4 q=首相で //他の条件は同じ
    
  3. 結果
  4. #ケース1 2件ヒットして、id:2がハイライトずれを起こします。
    id:1首相でございます
    id:2内閣総理大臣でございます
    #ケース2 2件ヒットしますが、id:1がハイライトずれを起こします。
    id:1首相でございます
    id:2内閣総理大臣でございます
    #ケース3 2件ヒットしますが、id:1がハイライトずれを起こします。
    id:1首相でございます
    id:2内閣総理大臣でございます
    #ケース4 1件しかヒットできない問題が発生します。
    id:1首相でございます
    id:2ヒットしない。

  5. 解説
  6. FSTSynonymFilterでは以下のようにトークン(単語テキスト[ポジション:開始オフセット,終了オフセット])を生成します。

    #content:首相でございます
    |首[1:0,1]|相[2:1,2]|で[3:2,3]|ご[4:3,4]|ざ[5:4,5]|い[6:5,6]|ま[7:6,7]|す[8:7,8]
    |内[1:0,2]|閣[2:1,2]|総[3:2,3]|理[4:3,4]|大[5:4,5]|臣[6:6,7]
    #content:内閣総理大臣でございます
    |首[1:0,1]|相[2:1,2]                                  |で[7:6,7]|ご[8:7,8]|ざ[8:8,9]|い[10:9,10]|ま[11:10,11]|す[12:11,12]
    |内[1:0,1]|閣[2:1,2]|総[3:2,3]|理[4:3,4]|大[5:4,5]|臣[6:5,6]
    

    ケース1の問題は、ヒットした「首相」のオフセットが0-2であり、この範囲をハイライトしようとして、「内閣」が0-2の部分にあたるので、ズレています。

    ケース2の問題は、ヒットした「内閣総理大臣」のオフセットが0-6であり、この範囲をハイライトしようとして、「首相でござい」が0-6の部分にあたるので、ズレています。

    ケース3の問題は、ヒットした「総理」のオフセットが2-4であり、この範囲をハイライトしようとして、「でご」が2-4の部分にあたるので、ズレています。

    ケース4の問題は、クエリ「首相で」がautoGeneratePhraseQueriesによって「”首 相 で”」という検索式で、フレーズクエリになります。フレーズ検索はダブルクォートした単語群のポジションが増分1でヒットさせようとするので、「首」のポジションが1で、「相」のポジションが2で、「で」のポジションが3でなければヒットできません。「で」が7になっているのでポジションがズレています。


    NGramの場合はNGramの単語分割ベースで、ポジションとオフセットがふられ、かつFSTSynonymFilterで類義語もNGramで複数トークンに分割されて調整される結果、ハイライトずれとヒット漏れが発生する場合があります。

NGramTokenizer(1gram)とSlowSynonymFilter

  1. 検索リクエスト
  2. qパラメータに類義語の単語をセットして、類義語検索のヒットと類義語ハイライトを確認します。

    #ケース1 「首相」
    http://localhost:8983/solr/select?q=首相&fl=id&df=content_1g_synonym33&hl=on&hl.useFastVectorHighlighter=true&hl.tag.pre=<b>&hl.tag.post=</b>
    #ケース2 q=内閣総理大臣 //他の条件は同じ
    #ケース3 q=総理 //他の条件は同じ
    #ケース4 q=首相で //他の条件は同じ
    
  3. 結果
  4. #ケース1 2件ヒットして、ハイライトができます。
    id:1首相でございます
    id:2内閣総理大臣でございます
    #ケース2 2件ヒットして、ハイライトができます。
    id:1首相でございます
    id:2内閣総理大臣でございます
    #ケース3 2件ヒットして、ハイライトができます。
    id:1首相でございます
    id:2内閣総理大臣でございます
    #ケース4 ヒットできない問題が発生します。
    id:1ヒットしない。
    id:2ヒットしない。

  5. 解説
  6. SlowSynonymFilterでは以下のようにトークン(単語テキスト[ポジション:開始オフセット,終了オフセット])を生成します。

    #content:首相でございます
    |首[1:0,2]|相[2:0,2]                                    |で[7:2,3]|ご[8:3,4]|ざ[9:4,5]|い[10:5,6]|ま[11:6,7]|す[12:7,8]
    |内[1:0,2]|閣[2:0,2]|総[3:0,2]|理[4:0,2]|大[5:0,2]|臣[6:0,2]
    #content:内閣総理大臣でございます
    |首[1:0,6]|相[2:0,6]                                  |で[7:6,7]|ご[8:7,8]|ざ[8:8,9]|い[10:9,10]|ま[11:10,11]|す[12:11,12]
    |内[1:0,6]|閣[2:0,6]|総[3:0,6]|理[4:0,6]|大[5:0,6]|臣[6:0,6]
    

    ケース1と2は、類義語がすべて同一オフセットになるので、ハイライトがズレません。

    ケース3は、ヒットした「総理」のオフセットが0-2あるいは0-6であり、この範囲をハイライトしようするので、「首相」や「内閣総理大臣」をハイライトします。

    ケース4の問題は、クエリ「首相で」がautoGeneratePhraseQueriesによって「”首 相 で”」というフレーズクエリになります。フレーズ検索はダブルクォートした単語群のポジションが増分1でヒットさせようとするので、「首」のポジションが1で、「相」のポジションが2で、「で」のポジションが3でなければヒットできません。2つのドキュメントとも、「で」が7になっているのでポジションが一致しません。


    SlowSynonymFilterは、類義語展開したとき、類義語のオフセットをすべて同一にしているので、ハイライトはFSTSynonymFilterに比べてズレがおきません。

中間まとめ

これまでで確認できたことを2点まとめます。

  1. 類義語ハイライトは、FSTSynonymFilterを使用するとき、ハイライトずれが起きるので使えない。ハイライトずれが起きないSlowSynonymFilterを使用するのがよい。
  2. 類義語検索は、SlowSynonymFilterを使用するとき、ヒットしない場合がある。

2に関する対応策としては、次の2つがあります。

  1. クエリ時のautoGeneratePhraseQueriesをfalseにする。自動フレーズ検索させない。
  2. マイナス点は、「フレーズ検索」しないので「単語検索」になり、ゴミもヒットする点です。ゴミとは、クライアントが期待しない単語がヒットしているものを指しています。たとえば、「首相で」を単語検索すると、1gramの場合では「首 OR 相 OR で」という検索式になりますので、「首相官邸」や「報連相で」などの単語・文章もヒットして検索結果に含まれてしまいます。ゴミはヒットしますが、検索結果の1ページ目や2ページ目に出ないようにすればよいので、検索リクエストを調整することで対応することができます。次の章でご説明します。

  3. 弊社サブスクリプションのJaNBestTokenizerとNGramSynonymTokenizerを使用する。
  4. 弊社のSolrプラグインのトークナイザは、上記で述べてきたような類義語検索および類義語ハイライトの問題を解消するために、開発されています。トークナイザで文字列処理する時点で、単語分割すると同時に類義語展開し、ポジションずれやオフセットずれをしないよう調整してトークンを生成します。これを使用すればSolrでSynonymFilterを定義する必要がありません。また、autoGeneratePhraseQueries=trueにしてフレーズ検索できるので、1で述べたゴミが含まれる問題を回避できます。ゴミを含まない必要分のシステム出力が行われて類義語検索の再現率を適正に高めることができます。次の次の章でご説明します。

形態素解析フィールドとNgramフィールドの横断検索

類義語検索は、SlowSynonymFilterを使用するとき、ヒットしない場合がありますので、これを回避するために、検索リクエスト時にクエリパーサを切り替えます。デフォルトはLuceneクエリパーサですが、フィールド横断検索がシンプルに書けるDismaxクエリパーサを使います。

最初に、類義語検索でヒットしない問題を解消するために、1箇所、autoGeneratePhraseQueriesをtrueからfalseに変更します。

-  <fieldType name="text_1g_synonym33" class="solr.TextField" positionIncrementGap="100" autoGeneratePhraseQueries="true">
+  <fieldType name="text_1g_synonym33" class="solr.TextField" positionIncrementGap="100" autoGeneratePhraseQueries="false">

そしてSolrを再起動して、検索します。

  1. 検索リクエスト
  2. qパラメータに類義語の単語をセットして、類義語検索のヒットと類義語ハイライトを確認します。

    #ケース1 「首相」
    http://localhost:8983/solr/select?q=首相&fl=id&defType=edismax&qf=content_ja_synonym33^2 content_1g_synonym33&hl=on&hl.useFastVectorHighlighter=true&hl.requireFieldMatch=true&hl.fl=content_1g_synonym33&hl.tag.pre=<b>&hl.tag.post=</b>
    #ケース2 q=内閣総理大臣 //他の条件は同じ
    #ケース3 q=総理 //他の条件は同じ
    #ケース4 q=首相で //他の条件は同じ
    
  3. 結果
  4. #ケース1 2件ヒットして、ハイライトができます。
    id:1首相でございます
    id:2内閣総理大臣でございます
    #ケース2 2件ヒットして、ハイライトができます。
    id:1首相でございます
    id:2内閣総理大臣でございます
    #ケース3 2件ヒットして、ハイライトができます。
    id:1首相でございます
    id:2内閣総理大臣でございます
    #ケース4 2件ヒットして、ハイライトができます。
    id:1首相ございます
    id:2内閣総理大臣ございます

  5. 解説
  6. 上記の検索ではdismaxクエリを使って、形態素解析フィールド(content_ja_synonym33)と1gramフィールド(text_1g_synonym33)を横断検索します。前述で1gramフィールド側はフレーズ検索しないように設定を変えましたので、ケース4の場合、類義語をヒットさせることができました。ヒット漏れを回避してドキュメントを返すことができたので、ハイライトスニペットが生成でき、結果として類義語ハイライトもうまくできました。1gramフィールドはフレーズ検索していないのでゴミもヒットしますが、クエリ内容と完全一致していれば当然スコアは大きくなりますし、dismaxクエリのqfパラメータで形態素解析フィールドの方を重みをつけている関係で、1gramフィールドでヒットしたゴミドキュメントはスコアが小さくなりますので、検索結果の先頭付近のページには登場しなくなるはずです。

    ただ、フレーズ検索しないようにしているので、ヒットした場合に、ゴミ単語の”ハイライト”が行われますので注意です。たとえば「首相でございます。できれば増税で。」というドキュメントがあったとき、「q=首相で」でリクエストするとヒットしますが、ハイライトスニペットは「<b>首相</b><b>で</b>ございます。<b>で</b>きれば増税<b>で</b>。」となります。「で」が存在したら全部ハイライトしてしまいます。

    さらに注意事項ですが、上記例では、1gramフィールドでautoGeneratePhraseQueries=falseにして類義語をヒットさせています。2gramフィールドでもいいと思いますが、2gramフィールドで、autoGeneratePhraseQueries=falseにした場合、制限事項があります。それは、類義語検索はうまくいきますが、ハイライトがうまくいかないケースがあるということです。前述のように「q=首相で」でリクエストしたとき、「<b>首相</b>でございます」とハイライトされてしまいます。「で」がハイライトできません。これは、FVHがハイライトスニペットを作るとき、2gramフィールドでautoGeneratePhraseQueries=falseの場合に、奇数文字の場合(2gram単語をまたぐ場合)にきれいにハイライトできないという制約があるからです。クエリが「首相 OR 相で」と2gram分割して解釈されて、FVHが2gramフィールドに強調タグをつけるとき、最初に「首相」を見つけてハイライトすることができるのですが、次の「相で」を処理するとき、すでに「相」が強調タグ内に含まれている関係でハイライト済みと判断するためです。ちなみに偶数文字の場合はきれいにハイライトできます。「q=首相でご」でリクエストしたとき、「<b>首相</b><b>でご</b>ざいます」とハイライトされます。


    以上をまとめますと、autoGeneratePhraseQueriesをfalseにすれば、類義語検索のヒット漏れ問題は解決できます。しかしながら、autoGeneratePhraseQueriesをfalseにすることで、類義語のハイライトおよび通常単語のハイライトで、ドキュメントの内容によっては強調タグがたくさんついてしまう問題が発生します。

JaNBestTokenizer

JaNBestTokenizer(Javadoc)は、ipadic/juman/unidicのOSS辞書が利用できる形態素解析トークナイザで、日本語の単語分割における多義性に対応しておりNBest解(通常の形態素解析では1パスの単語分割ですが、Nパスの単語分割ができる)が出力でき、OSS辞書から自動で類義語を探して類義語展開もできつつ、ユーザ定義の類義語もサポートするというものです。弊社サブスクリプションのNLP4Lで導入されています。デモサイト(弊社サイト内リンク)を用意しています。

  1. 準備
  2. # 弊社のサブスクリプションのダウンロードページからNLP4Lのtar.gzファイルを取得して解凍する
    $ tar xvzf NLP4L-0.6.0.tar.gz
    
    # NLP4LのjarファイルをSolrに配置する
    $ cd NLP4L-0.6.0
    $ mkdir -p /home/solr/solr-4.7.2/example/solr/collection1/lib
    $ cp -rp NLP4L-0.6.0.jar !$
    $ cp -rp lib/RONDHUIT-COMMONS-0.9.0.jar !$
    
    $ cd NLP4L-0.6.0/projects/morphol
    # OSS辞書を指定する。デフォルトはipadic。
    $ cp -rp conf.properties.sample conf.properties
    
    $ vi morphol-ipadic.properties
    # 以下の属性の右辺に、ユーザ定義の類義語辞書を指定する
    # nlp4l.morphol.syn.file=/home/solr/solr-4.7.2/example/solr/collection1/conf/synonyms-index.txt
    
    # 辞書コンパイルする。
    $ chmod +x makedic.sh
    $ ./makedic.sh
    
  3. schema.xmlに検証用フィールドタイプを設定
  4.     <fieldType name="text_ja_nbest" class="solr.TextField" positionIncrementGap="100" autoGeneratePhraseQueries="true">
          <analyzer type="index">
            <tokenizer class="com.rondhuit.nlp.lucene.JaNBestTokenizerFactory"
                       dir="${dicDir}/ipadic" type="ipadic" n="2" useSynonyms="true" synonyms="synonyms-index.txt"/>
          </analyzer>
          <analyzer type="query">
            <tokenizer class="com.rondhuit.nlp.lucene.JaNBestTokenizerFactory"
                       dir="${dicDir}/ipadic" type="ipadic" n="1" useSynonyms="false"/>
          </analyzer>
        </fieldType>
    
    • n属性は、NBest解の数値です。
    • useSynonyms属性をtrueにして類義語サポートをonにし、synonyms属性でユーザ類義語辞書を指定します。
    • dir属性は前述でコンパイルした辞書の場所を指定します(プレースホルダを使ってSolr起動時に指定します)。
  5. schema.xmlにダイナミックフィールドとコピーフィールドを設定
  6.    <dynamicField name="*_ja_nbest" type="text_ja_nbest" indexed="true" stored="true" multiValued="true" termVectors="true" termOffsets="true" termPositions="true"/>
    
       <copyField source="content" dest="content_ja_nbest"/>
    
  7. Solrを再起動します。
  8. $ cd solr-4.7.2/example
    $ java -DdicDir=/home/solr/NLP4L-0.6.0/projects/morphol/out -jar start.jar
    
  9. データをインデクシングします。

そしてSolrで、検索します。

  1. 検索リクエスト
  2. qパラメータに類義語の単語をセットして、類義語検索のヒットと類義語ハイライトを確認します。

    #ケース1 「首相」
    http://localhost:8983/solr/select?q=首相&fl=id&df=content_ja_nbest&hl=on&hl.useFastVectorHighlighter=true&hl.tag.pre=<b>&hl.tag.post=</b>
    #ケース2 q=内閣総理大臣 //他の条件は同じ
    #ケース3 q=総理 //他の条件は同じ
    #ケース4 q=首相で //他の条件は同じ
    
  3. 結果
  4. #ケース1 2件ヒットして、ハイライトができます。
    id:1首相でございます
    id:2内閣総理大臣でございます
    #ケース2 2件ヒットして、ハイライトができます。
    id:1首相でございます
    id:2内閣総理大臣でございます
    #ケース3 ヒットしません。
    id:1ヒットしない。
    id:2ヒットしない。
    #ケース4 2件ヒットして、ハイライトができます。
    id:1首相でございます
    id:2内閣総理大臣でございます

  5. 解説
  6. JaNBestTokenizerでは以下のようにトークン(単語テキスト[ポジション:開始オフセット,終了オフセット])を生成します。

    #content:首相でございます
    |首相    [1:0,2]|で[2:2,3]|ござい[3:3,6]|ます
    |内閣総理大臣[1:0,2]|
    #content:内閣総理大臣でございます
    |首相    [1:0,6]|で[2:6,7]|ござい[3:7,10]|ます
    |内閣総理大臣[1:0,6]|
    

    ケース1と2は、類義語がすべて同一オフセットになるので、ハイライトがズレません。JaNBestTokenizerの特徴は、類義語は同一オフセット、同一ポジションにする点になります。類義語の文字列は形態素解析の単語分割を行いません。

    ケース3は、類義語辞書に入っている単語は、その単語自体を類義語展開しますので、それ以上単語分割しない仕様になっています。「総理」という単語は転置索引にないのでヒットしません。

    ケース4は、クエリ「首相で」がautoGeneratePhraseQueriesによって「”首相 で”」というフレーズクエリになります。フレーズ検索はダブルクォートした単語群のポジションが増分1でヒットさせようとするので、「首相」のポジションが1で、「で」のポジションが2なので、ヒットします。


    JaNBestTokenizerは、類義語展開したとき、類義語のオフセットをすべて同一にしているので、ハイライトはズレがおきません。また、類義語のポジションもすべて同一にしているので、フレーズ検索が効き、ヒット漏れが発生しません。


    ただし、ケース3の「q=総理」がヒットもしないしハイライトもされないというのは問題と思いますので、次の対応をとります。

    • 対応方法1:「形態素解析フィールドとNgramフィールドの横断検索」の章で説明したdismaxクエリを発行する。
    • http://localhost:8983/solr/select?q=総理&fl=id&defType=edismax&qf=content_ja_nbest^2 content_1g_synonym33&hl=on&hl.useFastVectorHighlighter=true&hl.requireFieldMatch=true&hl.fl=content_1g_synonym33&hl.tag.pre=<b>&hl.tag.post=</b>
      

      マイナス点は、上記章で説明したとおり、ゴミ単語の”ハイライト”が起きてしまう点です。

    • 対応方法2:類義語辞書に複合語を登録するときに、最小単位の単語も類義語として登録する。
    • synonyms-index.txtに「総理」を追加します。想定しうる類義語候補を全部登録する対応となります。

      内閣総理大臣,首相,総理
      

      類義語辞書を修正したら辞書コンパイルをやりなおしして、Solrの再起動および再インデクシングが必要です。

      データをインデクシングすると、JaNBestTokenizerでは以下のようにトークン(単語テキスト[ポジション:開始オフセット,終了オフセット])を生成します。

      #content:首相でございます
      |首相    [1:0,2]|で[2:2,3]|ござい[3:3,6]|ます
      |内閣総理大臣[1:0,2]|
      |総理    [1:0,2]|
      #content:内閣総理大臣でございます
      |首相    [1:0,6]|で[2:6,7]|ござい[3:7,10]|ます
      |内閣総理大臣[1:0,6]|
      |総理    [1:0,2]|
      

      これでケース3の「q=総理」を検索すると、結果は次のようになり、問題は解消します。

      #ケース3 2件ヒットして、ハイライトができます。
      id:1首相でございます
      id:2内閣総理大臣でございます

      マイナス点は、想定しうる類義語を全部書く必要が出てくる点です。

NGramSynonymTokenizer

NGramSynonymTokenizerは、N-Gramで単語分割しながら、類義語辞書に入っている単語は、N-Gram分割せずに、定義されている単語でそのまま出力するトークナイザです。弊社サブスクリプションのRCSS 1.3.0から導入されています。Solr/Luceneで提案中(Jiraリンク:LUCENE-5252)でして、こちらにその提案/実装内容があります(SlideShareリンク:NGramSynonymTokenizer)。

  1. 準備
  2. # 弊社のサブスクリプションのダウンロードページからRCSSのtar.gzファイルを取得して解凍する
    $ tar xvzf RCSS-basic-1.4.0.tar.gz
    
    # SolrプラグインのjarファイルをSolrに配置する
    $ cd RCSS-basic-1.4.0
    $ mkdir -p /home/solr/solr-4.7.2/example/solr/collection1/lib
    $ cp -rp plugin/RONDHUIT-solr-plugin-1.4.0.jar !$
    # 前章でRONDHUIT-COMMONSをコピーしていなかったら、コピーします。
    $ cp -rp plugin/RONDHUIT-COMMONS-0.4.0.jar !$
    
  3. schema.xmlに検証用フィールドタイプを設定
  4.     <fieldType name="text_2gramsynonym" class="solr.TextField" positionIncrementGap="100" autoGeneratePhraseQueries="true">
          <analyzer type="index">
            <tokenizer class="com.rondhuit.solr.analysis.NGramSynonymTokenizerFactory" n="2" expand="true" synonyms="synonyms-index.txt"/>
          </analyzer>
          <analyzer type="query">
            <tokenizer class="com.rondhuit.solr.analysis.NGramSynonymTokenizerFactory" n="2" expand="false" synonyms="synonyms-index.txt"/>
          </analyzer>
        </fieldType>
    
    • n属性は、Gramの数値です(minGramSize=maxGramSize)。
    • expand属性をインデクシング時にtrueにして、synonyms属性でユーザ類義語辞書を指定します。
  5. schema.xmlにダイナミックフィールドとコピーフィールドを設定
  6.    <dynamicField name="*_2gramsynonym" type="text_2gramsynonym" indexed="true" stored="true" multiValued="true" termVectors="true" termOffsets="true" termPositions="true"/>  
    
       <copyField source="content" dest="content_2gramsynonym"/>
    
  7. Solrを再起動します。
  8. データをインデクシングします。

そしてSolrで、検索します。

  1. 検索リクエスト
  2. qパラメータに類義語の単語をセットして、類義語検索のヒットと類義語ハイライトを確認します。

    #ケース1 「首相」
    http://localhost:8983/solr/select?q=首相&fl=id&df=content_2gramsynonym&hl=on&hl.useFastVectorHighlighter=true&hl.tag.pre=<b>&hl.tag.post=</b>
    #ケース2 q=内閣総理大臣 //他の条件は同じ
    #ケース3 q=総理 //他の条件は同じ
    #ケース4 q=首相で //他の条件は同じ
    
  3. 結果
  4. #ケース1 2件ヒットして、ハイライトができます。
    id:1首相でございます
    id:2内閣総理大臣でございます
    #ケース2 2件ヒットして、ハイライトができます。
    id:1首相でございます
    id:2内閣総理大臣でございます
    #ケース3 ヒットしません。
    id:1ヒットしない。
    id:2ヒットしない。
    #ケース4 2件ヒットして、ハイライトができます。
    id:1首相でございます
    id:2内閣総理大臣でございます

  5. 解説
  6. NGramSynonymTokenizerでは以下のようにトークン(単語テキスト[ポジション:開始オフセット,終了オフセット])を生成します。

    #content:首相でございます
    |首相    [1:0,2]|で [2:2,3]|ござ[3:5,3]|ざい[4:6,4]|いま|ます
    |内閣総理大臣[1:0,2]|でご[2:2,4]|
    #content:内閣総理大臣でございます
    |首相    [1:0,6]|で [2:6,7]|ござ[3:7,9]|ざい[4:8,10]|いま|ます
    |内閣総理大臣[1:0,6]|でご[2:6,6]|
    

    ケース1と2は、類義語がすべて同一オフセットになるので、ハイライトがズレません。NGramSynonymTokenizerの特徴は、類義語は同一オフセット、同一ポジションにする点になります。類義語の文字列はN-Gram分割を行いません。

    ケース3は、類義語辞書に入っている単語は、その単語自体を類義語展開しますので、それ以上単語分割しない仕様になっています。「総理」という単語は転置索引にないのでヒットしません。

    ケース4は、クエリ「首相で」がautoGeneratePhraseQueriesによって「”首相 で”」というフレーズクエリになります。フレーズ検索はダブルクォートした単語群のポジションが増分1でヒットさせようとするので、「首相」のポジションが1で、「で」のポジションが2なので、ヒットします。「”首相 相で”」とはならないところが、NGramSynonymTokenizerの特徴です。類義語辞書登録された文字列の前後は、ヒット漏れを回避するために1文字トークンを追加で切りだしてポジション調整します。


    NGramSynonymTokenizerは、類義語展開したとき、類義語のオフセットをすべて同一にしているので、ハイライトはズレがおきません。また、類義語のポジションもすべて同一にしているので、フレーズ検索が効き、ヒット漏れが発生しません。


    ただし、ケース3の「q=総理」がヒットもしないしハイライトもされないというのは問題と思いますので、次の対応をとります。

    • 対応方法1:「形態素解析フィールドとNgramフィールドの横断検索」の章で説明したdismaxクエリを発行する。
    • http://localhost:8983/solr/select?q=総理&fl=id&defType=edismax&qf=content_2gramsynonym^2 content_1g_synonym33&hl=on&hl.useFastVectorHighlighter=true&hl.requireFieldMatch=true&hl.fl=content_1g_synonym33&hl.tag.pre=<b>&hl.tag.post=</b>
      

      マイナス点は、上記章で説明したとおり、ゴミ単語の”ハイライト”が起きてしまう点です。

    • 対応方法2:類義語辞書に複合語を登録するときに、最小単位の単語も類義語として登録する。
    • synonyms-index.txtに「総理」を追加します。想定しうる類義語候補を全部登録する対応となります。

      内閣総理大臣,首相,総理
      

      類義語辞書を修正したら、Solrの再起動および再インデクシングが必要です。

      データをインデクシングすると、JaNBestTokenizerでは以下のようにトークン(単語テキスト[ポジション:開始オフセット,終了オフセット])を生成します。

      #content:首相でございます
      |首相    [1:0,2]|で [2:2,3]|ござ[3:5,3]|ざい[4:6,4]|いま|ます
      |内閣総理大臣[1:0,2]|でご[2:2,4]|
      |総理    [1:0,2]|
      #content:内閣総理大臣でございます
      |首相    [1:0,6]|で [2:6,7]|ござ[3:7,9]|ざい[4:8,10]|いま|ます
      |内閣総理大臣[1:0,6]|でご[2:6,6]|
      |総理    [1:0,6]|
      

      これでケース3の「q=総理」を検索すると、結果は次のようになり、問題は解消します。

      #ケース3 2件ヒットして、ハイライトができます。
      id:1首相でございます
      id:2内閣総理大臣でございます

      マイナス点は、想定しうる類義語を全部書く必要が出てくる点です。

最終まとめ

類義語検索と類義語ハイライトを実現するためには、

  • ハイライトずれを回避するために、SlowSynonymFilterを使う。
  • ヒット漏れを回避するために、形態素解析フィールドとNgramフィールドの横断検索を行う。
  • JaNBestTokenizer/NGramSynonymTokenizerを使って、類義語検索と類義語ハイライトを最適化する。

全体的な評価をしますと、以下の様な表が書けます。

使用パターン類義語検索類義語ハイライト
JapaneseTokenizerとFSTSynonymFilter
JapaneseTokenizerとSlowSynonymFilter
NGramTokenizerとFSTSynonymFilter
NGramTokenizerとSlowSynonymFilter
形態素解析フィールドとNgramフィールドの横断検索
JaNBestTokenizer○ ※
NGramSynonymTokenizer○ ※

※ 類義語が複合語の場合の最小単位の単語検索はできないので、最小単位の単語も類義語辞書に含めれば類義語検索のヒット漏れが防げるので○としています。


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




トレーニングコース

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

セミナー

ロンウイットのApache Software Foundationコミッターが、情報検索の基礎、自然言語処理、そして、ユーザにとっての効果についてご説明させていただきます。