CLOVER🍀

That was when it all began.

Apache Solr 5.xのSuggesterを使って、サジェストとDid You Mean?

先ほど、こんなエントリを書きました。

Apache Solr 5.xで、サジェストを実装することを考える
http://d.hatena.ne.jp/Kazuhira/20150912/1442047726

これを書く時にSuggesterをいったん置いておいてEdgeNGramにチャレンジしましたが、そもそもこちらのルートも試すべきかな?と思い、実践してみることに。

合わせて、Did You Mean?にもトライしてみようかと。

設定は、まずはschema.xmlから。

    <fieldType name="text_suggest_ja" class="solr.TextField" positionIncrementGap="100" autoGeneratePhraseQueries="true">
      <analyzer>
        <tokenizer class="solr.JapaneseTokenizerFactory" mode="normal"/>
        <filter class="solr.JapaneseBaseFormFilterFactory"/>
        <filter class="solr.JapanesePartOfSpeechStopFilterFactory" tags="lang/stoptags_ja.txt" />
        <filter class="solr.CJKWidthFilterFactory"/>
        <filter class="solr.StopFilterFactory" ignoreCase="true" words="lang/stopwords_ja.txt" />
        <filter class="solr.JapaneseKatakanaStemFilterFactory" minimumLength="4"/>
        <filter class="solr.LowerCaseFilterFactory"/>
      </analyzer>
    </fieldType>

    <fieldType name="text_suggest_reading_ja" class="solr.TextField" positionIncrementGap="100" autoGeneratePhraseQueries="true">
      <analyzer>
        <tokenizer class="solr.JapaneseTokenizerFactory" mode="normal"/>
        <filter class="solr.JapaneseBaseFormFilterFactory"/>
        <filter class="solr.JapanesePartOfSpeechStopFilterFactory" tags="lang/stoptags_ja.txt" />
        <filter class="solr.CJKWidthFilterFactory"/>
        <filter class="solr.StopFilterFactory" ignoreCase="true" words="lang/stopwords_ja.txt" />
        <filter class="solr.JapaneseKatakanaStemFilterFactory" minimumLength="4"/>
        <filter class="solr.JapaneseReadingFormFilterFactory" useRomaji="false"/>
        <filter class="solr.ICUTransformFilterFactory" id="Katakana-Hiragana"/>
        <filter class="solr.LowerCaseFilterFactory"/>
      </analyzer>
    </fieldType>

リンク先の定義よりもっと簡素にして(EdgeNGram削除、インデックスとクエリのAnalyzer統合)、単語そのものと読みに対するfieldTypeを定義。ICU関係のものは使っているので、ライブラリのコピーは必要です。
※リンク先のエントリ参照

フィールド定義は、こちら。

    <field name="content" type="string" indexed="true" stored="true" required="true" multiValued="false" />
    <field name="content_ja" type="text_suggest_ja" indexed="true" stored="false" multiValued="false" />
    <field name="content_reading_ja" type="text_suggest_reading_ja" indexed="true" stored="false" multiValued="false" />

    <copyField source="content" dest="content_ja"/>
    <copyField source="content" dest="content_reading_ja"/>

    <uniqueKey>content</uniqueKey>

solrconfig.xmlに、サジェスト用とDid You Mean?用の設定を書いていきます。

まずは、サジェスト用(autocomplete_ja)。

  <searchComponent name="autocomplete_ja" class="solr.SpellCheckComponent">
    <str name="queryAnalyzerFieldType">text_suggest_ja</str>

    <lst name="spellchecker">
      <str name="name">autocomplete_ja</str>
      <str name="field">content_ja</str>
      <str name="classname">org.apache.solr.spelling.suggest.Suggester</str>
      <str name="lookupImpl">org.apache.solr.spelling.suggest.fst.AnalyzingLookupFactory</str>
      <str name="storeDir">autocomplete_ja</str>
      <str name="buildOnCommit">true</str>
      <int name="maxEdits">2</int>
      <str name="comparatorClass">freq</str>
      <str name="suggestAnalyzerFieldType">text_suggest_ja</str>
      <bool name="extraMatchFirst">true</bool>
    </lst>
  </searchComponent>

  <requestHandler name="/autocomplete_ja" class="solr.SearchHandler" startup="lazy">
    <lst name="defaults">
      <str name="spellcheck.dictionary">autocomplete_ja</str>
      <str name="spellcheck">on</str>
      <str name="spellcheck.count">10</str>
      <str name="spellcheck.collate">true</str>
      <str name="spellcheck.onlyMorePopular">true</str>
    </lst>
    <arr name="last-components">
      <str>autocomplete_ja</str>
    </arr>
  </requestHandler>

サジェスト用(読み)(autocomplete_reading_ja)

  <searchComponent name="autocomplete_reading_ja" class="solr.SpellCheckComponent">
    <str name="queryAnalyzerFieldType">text_suggest_reading_ja</str>

    <lst name="spellchecker">
      <str name="name">autocomplete_reading_ja</str>
      <str name="field">content_reading_ja</str>
      <str name="classname">org.apache.solr.spelling.suggest.Suggester</str>
      <str name="lookupImpl">org.apache.solr.spelling.suggest.fst.AnalyzingLookupFactory</str>
      <str name="storeDir">autocomplete_reading_ja</str>
      <str name="buildOnCommit">true</str>
      <int name="maxEdits">2</int>
      <str name="comparatorClass">freq</str>
      <str name="suggestAnalyzerFieldType">text_suggest_reading_ja</str>
      <bool name="extraMatchFirst">true</bool>
    </lst>
  </searchComponent>

  <requestHandler name="/autocomplete_reading_ja" class="solr.SearchHandler" startup="lazy">
    <lst name="defaults">
      <str name="spellcheck.dictionary">autocomplete_reading_ja</str>
      <str name="spellcheck">on</str>
      <str name="spellcheck.count">10</str>
      <str name="spellcheck.collate">true</str>
      <str name="spellcheck.onlyMorePopular">true</str>
    </lst>
    <arr name="last-components">
      <str>autocomplete_reading_ja</str>
    </arr>
  </requestHandler>

Did You Mean?(didyoumean_ja)

  <searchComponent name="didyoumean_ja" class="solr.SpellCheckComponent">
    <str name="queryAnalyzerFieldType">text_suggest_ja</str>

    <lst name="spellchecker">
      <str name="name">didyoumean_ja</str>
      <str name="field">content</str>
      <str name="classname">org.apache.solr.spelling.suggest.Suggester</str>
      <str name="lookupImpl">org.apache.solr.spelling.suggest.fst.FuzzyLookupFactory</str>
      <str name="storeDir">didyoumean_ja</str>
      <str name="buildOnCommit">true</str>
      <int name="maxEdits">2</int>
      <str name="comparatorClass">score</str>
      <str name="suggestAnalyzerFieldType">text_suggest_ja</str>
    </lst>
  </searchComponent>

  <requestHandler name="/didyoumean_ja" class="solr.SearchHandler" startup="lazy">
    <lst name="defaults">
      <str name="spellcheck.dictionary">didyoumean_ja</str>
      <str name="spellcheck">on</str>
      <str name="spellcheck.count">1</str>
      <str name="spellcheck.collate">true</str>
      <str name="spellcheck.onlyMorePopular">true</str>
    </lst>
    <arr name="last-components">
      <str>didyoumean_ja</str>
    </arr>
  </requestHandler>

では、データを用意して

data.json 
[
  {
    "content": "吾輩は猫である"
  },
  {
    "content": "我が名は青春のエッセイドラゴン"
  },
  {
    "content": "下町ロケット"
  },
  {
    "content": "北斗の拳"
  },
  {
    "content": "進撃の巨人"
  }
]

登録。

$ curl 'http://localhost:8983/solr/mycore/update?commit=true' --data-binary @data.json

動作確認してみます。

サジェスト(autocomplete_ja)から。

## 「下」でサジェスト
$ curl 'http://localhost:8983/solr/mycore/autocomplete_ja?wt=json&indent=true&spellcheck.collate=false' -d '{ "query": "content_ja:下" }'
{
  "responseHeader":{
    "status":0,
    "QTime":1},
  "response":{"numFound":0,"start":0,"docs":[]
  },
  "spellcheck":{
    "suggestions":[
      "下",{
        "numFound":1,
        "startOffset":11,
        "endOffset":12,
        "suggestion":["下町"]}]}}

## 「進」でサジェスト
$ curl 'http://localhost:8983/solr/mycore/autocomplete_ja?wt=json&indent=true&spellcheck.collate=false' -d '{ "query": "content_ja:進" }'
{
  "responseHeader":{
    "status":0,
    "QTime":2},
  "response":{"numFound":0,"start":0,"docs":[]
  },
  "spellcheck":{
    "suggestions":[
      "進",{
        "numFound":1,
        "startOffset":11,
        "endOffset":12,
        "suggestion":["進撃"]}]}}

動いてそうです。

続いて、読み(autocomplete_reading_ja)の方。

## 「した」
$ curl 'http://localhost:8983/solr/mycore/autocomplete_reading_ja?wt=json&indent=true&spellcheck.collate=false' -d '{ "query": "content_reading_ja:した" }'
{
  "responseHeader":{
    "status":0,
    "QTime":1},
  "response":{"numFound":0,"start":0,"docs":[]
  }}

## 「わが」
$ curl 'http://localhost:8983/solr/mycore/autocomplete_reading_ja?wt=json&indent=true&spellcheck.collate=false' -d '{ "query": "content_reading_ja:わが" }'
{
  "responseHeader":{
    "status":0,
    "QTime":2},
  "response":{"numFound":1,"start":0,"docs":[
      {
        "content":"我が名は青春のエッセイドラゴン",
        "_version_":1512112578393800704}]
  },
  "spellcheck":{
    "suggestions":[
      "わが",{
        "numFound":1,
        "startOffset":19,
        "endOffset":21,
        "suggestion":["わがはい"]}]}}

なんか、微妙な結果に…。

本の通りに、ローマ字でやった方がよかったかな…。

それに、この設定だとローマ字で入力された場合はサジェストできないんですよねぇ。本当に、大変…。

最後、Did You Mean?(didyoumean_ja)…といきたかったのですが、ドキュメント数、単語数が不足してて表示できず…。ちょっと多めにドキュメント投入してから、また確認します。

追試

というわけで、Did You Mean?がうまくいかなかったので、以下のエントリのプログラムとインデックス定義を使って、それなりのドキュメント量で再度チャレンジ。

Apache Solr 5.x+crawler4jで、Webサイトをクロールしてインデックス化する
http://d.hatena.ne.jp/Kazuhira/20150912/1442056103

こちらの場合でのDid You Mean?の定義だけ、載せておきます。

  <searchComponent name="didyoumean_ja" class="solr.SpellCheckComponent">
    <str name="queryAnalyzerFieldType">text_ja</str>

    <lst name="spellchecker">
      <str name="name">didyoumean_ja</str>
      <str name="field">contents</str>
      <str name="classname">org.apache.solr.spelling.suggest.Suggester</str>
      <str name="lookupImpl">org.apache.solr.spelling.suggest.fst.FuzzyLookupFactory</str>
      <str name="buildOnCommit">true</str>
      <int name="maxEdits">2</int>
      <str name="comparatorClass">score</str>
      <str name="suggestAnalyzerFieldType">text_ja</str>
    </lst>
  </searchComponent>

  <requestHandler name="/didyoumean_ja" class="solr.SearchHandler" startup="lazy">
    <lst name="defaults">
      <str name="spellcheck.dictionary">didyoumean_ja</str>
      <str name="spellcheck">on</str>
      <str name="spellcheck.count">1</str>
      <str name="spellcheck.collate">true</str>
      <str name="spellcheck.onlyMorePopular">true</str>
    </lst>
    <arr name="last-components">
      <str>didyoumean_ja</str>
    </arr>
  </requestHandler>

検索。

$ curl 'http://localhost:8983/solr/mycore/didyoumean_ja?wt=json&indent=true&spellcheck.collate=true' -d '{ "query": "contents:メガネ" }'
{
  "responseHeader":{
    "status":0,
    "QTime":2},
  "response":{"numFound":0,"start":0,"docs":[]
  },
  "spellcheck":{
    "suggestions":[
      "メガネ",{
        "numFound":1,
        "startOffset":9,
        "endOffset":12,
        "suggestion":["メソッド"]}],
    "collations":[
      "collation","contents:メソッド"]}}

「メガネ」で「メソッド」が現れました。まあ、「メガネ」とかうちのエントリに滅多に入っていませんからね…。

URLに「spellcheck.collate=true」を入れておくと、次に実行すべきと思われるクエリを提示してくれます。

「Lucene」のスペルミスと組み合わせて。

$ curl 'http://localhost:8983/solr/mycore/didyoumean_ja?wt=json&indent=true&spellcheck.collate=true' -d '{ "query": "contents:lucne contents:漢語" }'
{
  "responseHeader":{
    "status":0,
    "QTime":10},
  "response":{"numFound":0,"start":0,"docs":[]
  },
  "spellcheck":{
    "suggestions":[
      "lucne",{
        "numFound":1,
        "startOffset":9,
        "endOffset":14,
        "suggestion":["lucene"]},
      "漢語",{
        "numFound":1,
        "startOffset":24,
        "endOffset":26,
        "suggestion":["新語"]}],
    "collations":[
      "collation","contents:lucene contents:新語"]}}

Luceneは正しく訂正されましたが、「漢語」が「新語」になりました…。日本語は、やっぱり挙動がわかりにくいですね。

とりあえず、動かせましたということで。