CLOVER🍀

That was when it all began.

Apache Solr 5.xで、Managed Schema DefinitionからClassicIndexSchema(schema.xml)に変更しつつ、日本語検索したい

Apache Solrを使って、日本語検索をするまでの初歩的な?話について。

まず、「bin/solr create」でコアを作成した時、日本語系の設定はないのだろうなぁと思っていたのですが、意外とそうでもありませんでした。

スタンドアロンなSolrを使っている場合、「bin/solr create」で作成されるのはコアになります(SolrCloudの場合はコレクション)。

「bin/solr create -help」を行ってみると、そんな感じのことが書かれています。

$ bin/solr create -help

Usage: solr create [-c name] [-d confdir] [-n configName] [-shards #] [-replicationFactor #] [-p port]

  Create a core or collection depending on whether Solr is running in standalone (core) or SolrCloud
  mode (collection). In other words, this action detects which mode Solr is running in, and then takes
  the appropriate action (either create_core or create_collection). For detailed usage instructions, do:

    bin/solr create_core -help

       or

    bin/solr create_collection -help

で、オプションの説明を見るには、スタンドアロンの場合は「create_core」を見ればよさそうですね。

というわけで、見てみます。

$ bin/solr create_core -help

Usage: solr create_core [-c core] [-d confdir] [-p port]

  -c <core>     Name of core to create

  -d <confdir>  Configuration directory to copy when creating the new core, built-in options are:

      basic_configs: Minimal Solr configuration
      data_driven_schema_configs: Managed schema with field-guessing support enabled
      sample_techproducts_configs: Example configuration with many optional features enabled to
         demonstrate the full power of Solr

      If not specified, default is: data_driven_schema_configs

      Alternatively, you can pass the path to your own configuration directory instead of using
      one of the built-in configurations, such as: bin/solr create_core -c mycore -d /tmp/myconfig

  -p <port>     Port of a local Solr instance where you want to create the new core
                  If not specified, the script will search the local system for a running
                  Solr instance and will use the port of the first server it finds.

ここで、「-d」オプションで指定する内容で、生成される設定が変わります。

ばくっと読むと、こんな感じ?

  • basic_configs … ミニマムなSolrの設定。schema.xmlが定義され、REST APIでは管理不可
  • data_driven_schema_configs … REST APIで管理可能な設定のSolr(デフォルト)
  • sample_techproducts_configs … 多くのオプションを設定した、Solrの全機能を試すデモンストレーション用

で、実際

$ bin/solr create -c mycore

とやってコアを作成すると、このような形で設定ファイルが生成され、
※ここでのSolrのデータや設定の保存ディレクトリは、「/var/solr」とします

$ ls -l /var/solr/data/mycore/conf/
合計 148
-rw-rw-r-- 1 solr solr  3974  812 09:16 currency.xml
-rw-rw-r-- 1 solr solr  1348  812 09:16 elevate.xml
drwxrwxr-x 2 solr solr  4096  96 13:06 lang
-rw-rw-r-- 1 solr solr 55614  812 09:16 managed-schema
-rw-rw-r-- 1 solr solr   308  812 09:16 params.json
-rw-rw-r-- 1 solr solr   873  812 09:16 protwords.txt
-rw-rw-r-- 1 solr solr 61532  812 09:16 solrconfig.xml
-rw-rw-r-- 1 solr solr   781  812 09:16 stopwords.txt
-rw-rw-r-- 1 solr solr  1119  812 09:16 synonyms.txt

「managed-schema」というファイルにはKuromojiを使ったフィールドタイプの定義がしてあります。

    <dynamicField name="*_txt_ja" type="text_ja"  indexed="true"  stored="true"/>
    <fieldType name="text_ja" class="solr.TextField" positionIncrementGap="100" autoGeneratePhraseQueries="false">
      <analyzer>
        <tokenizer class="solr.JapaneseTokenizerFactory" mode="search"/>
        <!--<tokenizer class="solr.JapaneseTokenizerFactory" mode="search" userDictionary="lang/userdict_ja.txt"/>-->
        <!-- Reduces inflected verbs and adjectives to their base/dictionary forms (辞書形) -->
        <filter class="solr.JapaneseBaseFormFilterFactory"/>
        <!-- Removes tokens with certain part-of-speech tags -->
        <filter class="solr.JapanesePartOfSpeechStopFilterFactory" tags="lang/stoptags_ja.txt" />
        <!-- Normalizes full-width romaji to half-width and half-width kana to full-width (Unicode NFKC subset) -->
        <filter class="solr.CJKWidthFilterFactory"/>
        <!-- Removes common tokens typically not useful for search, but have a negative effect on ranking -->
        <filter class="solr.StopFilterFactory" ignoreCase="true" words="lang/stopwords_ja.txt" />
        <!-- Normalizes common katakana spelling variations by removing any last long sound character (U+30FC) -->
        <filter class="solr.JapaneseKatakanaStemFilterFactory" minimumLength="4"/>
        <!-- Lower-cases romaji characters -->
        <filter class="solr.LowerCaseFilterFactory"/>
      </analyzer>
    </fieldType>

ですので、「text_ja」フィールドタイプを指定する、もしくは「*_txt_ja」としてダイナミックフィールドを使用すると、Kuromojiを使った形態素解析が可能です。

今回は、こちらをベースにしてもう少し設定を縮めて日本語検索を設定してみたいと思います。また、REST APIスキーマ定義をしてもいいのですが、設定内容の多さを考えると、「schema.xmlを書く、でいいんじゃないのかなー」と思いまして、REST APIは外して考えることにします。

…と、このように書くと、「basic_configs」でschema.xmlを設定すればいいのでは?と思うかもですが、この場合はKuromojiの設定が丸ごと落ちてしまうので、今回は「data_driven_schema_configs」をベースにすることにします。

まず、solrconfig.xmlを編集。

$ vim /var/solr/data/mycore/conf/solrconfig.xml

schemaFactoryの設定を、ここから

  <schemaFactory class="ManagedIndexSchemaFactory">
    <bool name="mutable">true</bool>
    <str name="managedSchemaResourceName">managed-schema</str>
  </schemaFactory>

このように変更。

  <schemaFactory class="ClassicIndexSchemaFactory"/>

また、AddSchemaFieldsUpdateProcessorFactoryの設定を削除しておきます。

    <!--
    <processor class="solr.AddSchemaFieldsUpdateProcessorFactory">
      <str name="defaultFieldType">strings</str>
      <lst name="typeMapping">
        <str name="valueClass">java.lang.Boolean</str>
        <str name="fieldType">booleans</str>
      </lst>
      <lst name="typeMapping">
        <str name="valueClass">java.util.Date</str>
        <str name="fieldType">tdates</str>
      </lst>
      <lst name="typeMapping">
        <str name="valueClass">java.lang.Long</str>
        <str name="valueClass">java.lang.Integer</str>
        <str name="fieldType">tlongs</str>
      </lst>
      <lst name="typeMapping">
        <str name="valueClass">java.lang.Number</str>
        <str name="fieldType">tdoubles</str>
      </lst>
    </processor>
    -->

これで、schema.xmlを使うことができるようになります。

managed-schemaファイルを元に、schema.xmlを作成。というか、mv。

$ mv /var/solr/data/mycore/conf/managed-schema /var/solr/data/mycore/conf/schema.xml

中身は一気にシュリンクして、こんな感じをベースに。

<?xml version="1.0" encoding="UTF-8" ?>
<schema name="mycore-schema" version="1.5">
    <field name="id" type="string" indexed="true" stored="true" required="true" multiValued="false" />
    <field name="_version_" type="long" indexed="true" stored="true"/>
    <field name="_root_" type="string" indexed="true" stored="false"/>
    <field name="_text_" type="text_general" indexed="true" stored="false" multiValued="true"/>

    <dynamicField name="*_i"  type="int"    indexed="true"  stored="true"/>
    <dynamicField name="*_is" type="ints"    indexed="true"  stored="true"/>
    <dynamicField name="*_s"  type="string"  indexed="true"  stored="true" />
    <dynamicField name="*_ss" type="strings"  indexed="true"  stored="true"/>
    <dynamicField name="*_l"  type="long"   indexed="true"  stored="true"/>
    <dynamicField name="*_ls" type="longs"   indexed="true"  stored="true"/>
    <dynamicField name="*_t"   type="text_general" indexed="true" stored="true"/>
    <dynamicField name="*_txt" type="text_general" indexed="true" stored="true"/>
    <dynamicField name="*_b"  type="boolean" indexed="true" stored="true"/>
    <dynamicField name="*_bs" type="booleans" indexed="true" stored="true"/>
    <dynamicField name="*_f"  type="float"  indexed="true"  stored="true"/>
    <dynamicField name="*_fs" type="floats"  indexed="true"  stored="true"/>
    <dynamicField name="*_d"  type="double" indexed="true"  stored="true"/>
    <dynamicField name="*_ds" type="doubles" indexed="true"  stored="true"/>

    <dynamicField name="*_coordinate"  type="tdouble" indexed="true"  stored="false" />

    <dynamicField name="*_dt"  type="date"    indexed="true"  stored="true"/>
    <dynamicField name="*_dts" type="date"    indexed="true"  stored="true" multiValued="true"/>
    <dynamicField name="*_p"  type="location" indexed="true" stored="true"/>
    <dynamicField name="*_srpt"  type="location_rpt" indexed="true" stored="true"/>

    <dynamicField name="*_ti" type="tint"    indexed="true"  stored="true"/>
    <dynamicField name="*_tis" type="tints"    indexed="true"  stored="true"/>
    <dynamicField name="*_tl" type="tlong"   indexed="true"  stored="true"/>
    <dynamicField name="*_tls" type="tlongs"   indexed="true"  stored="true"/>
    <dynamicField name="*_tf" type="tfloat"  indexed="true"  stored="true"/>
    <dynamicField name="*_tfs" type="tfloats"  indexed="true"  stored="true"/>
    <dynamicField name="*_td" type="tdouble" indexed="true"  stored="true"/>
    <dynamicField name="*_tds" type="tdoubles" indexed="true"  stored="true"/>
    <dynamicField name="*_tdt" type="tdate"  indexed="true"  stored="true"/>
    <dynamicField name="*_tdts" type="tdates"  indexed="true"  stored="true"/>

    <dynamicField name="*_c"   type="currency" indexed="true"  stored="true"/>

    <dynamicField name="ignored_*" type="ignored" multiValued="true"/>
    <dynamicField name="attr_*" type="text_general" indexed="true" stored="true" multiValued="true"/>

    <dynamicField name="random_*" type="random" />

  <!-- Field to use to determine and enforce document uniqueness. 
      Unless this field is marked with required="false", it will be a required field
   -->
  <uniqueKey>id</uniqueKey>

    <fieldType name="string" class="solr.StrField" sortMissingLast="true" />
    <fieldType name="strings" class="solr.StrField" sortMissingLast="true" multiValued="true"/>

    <fieldType name="boolean" class="solr.BoolField" sortMissingLast="true"/>

    <fieldType name="booleans" class="solr.BoolField" sortMissingLast="true" multiValued="true"/>

    <fieldType name="int" class="solr.TrieIntField" precisionStep="0" positionIncrementGap="0"/>
    <fieldType name="float" class="solr.TrieFloatField" precisionStep="0" positionIncrementGap="0"/>
    <fieldType name="long" class="solr.TrieLongField" precisionStep="0" positionIncrementGap="0"/>
    <fieldType name="double" class="solr.TrieDoubleField" precisionStep="0" positionIncrementGap="0"/>

    <fieldType name="ints" class="solr.TrieIntField" precisionStep="0" positionIncrementGap="0" multiValued="true"/>
    <fieldType name="floats" class="solr.TrieFloatField" precisionStep="0" positionIncrementGap="0" multiValued="true"/>
    <fieldType name="longs" class="solr.TrieLongField" precisionStep="0" positionIncrementGap="0" multiValued="true"/>
    <fieldType name="doubles" class="solr.TrieDoubleField" precisionStep="0" positionIncrementGap="0" multiValued="true"/>

    <fieldType name="tint" class="solr.TrieIntField" precisionStep="8" positionIncrementGap="0"/>
    <fieldType name="tfloat" class="solr.TrieFloatField" precisionStep="8" positionIncrementGap="0"/>
    <fieldType name="tlong" class="solr.TrieLongField" precisionStep="8" positionIncrementGap="0"/>
    <fieldType name="tdouble" class="solr.TrieDoubleField" precisionStep="8" positionIncrementGap="0"/>
    
    <fieldType name="tints" class="solr.TrieIntField" precisionStep="8" positionIncrementGap="0" multiValued="true"/>
    <fieldType name="tfloats" class="solr.TrieFloatField" precisionStep="8" positionIncrementGap="0" multiValued="true"/>
    <fieldType name="tlongs" class="solr.TrieLongField" precisionStep="8" positionIncrementGap="0" multiValued="true"/>
    <fieldType name="tdoubles" class="solr.TrieDoubleField" precisionStep="8" positionIncrementGap="0" multiValued="true"/>

    <fieldType name="date" class="solr.TrieDateField" precisionStep="0" positionIncrementGap="0"/>
    <fieldType name="dates" class="solr.TrieDateField" precisionStep="0" positionIncrementGap="0" multiValued="true"/>

    <fieldType name="tdate" class="solr.TrieDateField" precisionStep="6" positionIncrementGap="0"/>
    <fieldType name="tdates" class="solr.TrieDateField" precisionStep="6" positionIncrementGap="0" multiValued="true"/>


    <fieldType name="binary" class="solr.BinaryField"/>

    <fieldType name="random" class="solr.RandomSortField" indexed="true" />

  <dynamicField name="*_ws" type="text_ws"  indexed="true"  stored="true"/>

    <fieldType name="text_ws" class="solr.TextField" positionIncrementGap="100">
      <analyzer>
        <tokenizer class="solr.WhitespaceTokenizerFactory"/>
      </analyzer>
    </fieldType>

    <fieldType name="text_general" class="solr.TextField" positionIncrementGap="100" multiValued="true">
      <analyzer type="index">
        <tokenizer class="solr.StandardTokenizerFactory"/>
        <filter class="solr.StopFilterFactory" ignoreCase="true" words="stopwords.txt" />
        <!-- in this example, we will only use synonyms at query time
        <filter class="solr.SynonymFilterFactory" synonyms="index_synonyms.txt" ignoreCase="true" expand="false"/>
        -->
        <filter class="solr.LowerCaseFilterFactory"/>
      </analyzer>
      <analyzer type="query">
        <tokenizer class="solr.StandardTokenizerFactory"/>
        <filter class="solr.StopFilterFactory" ignoreCase="true" words="stopwords.txt" />
        <filter class="solr.SynonymFilterFactory" synonyms="synonyms.txt" ignoreCase="true" expand="true"/>
        <filter class="solr.LowerCaseFilterFactory"/>
      </analyzer>
    </fieldType>

    <fieldType name="ignored" stored="false" indexed="false" multiValued="true" class="solr.StrField" />
    <dynamicField name="*_point" type="point"  indexed="true"  stored="true"/>
    <fieldType name="point" class="solr.PointType" dimension="2" subFieldSuffix="_d"/>
    <fieldType name="location" class="solr.LatLonType" subFieldSuffix="_coordinate"/>
    <fieldType name="location_rpt" class="solr.SpatialRecursivePrefixTreeFieldType"
               geo="true" distErrPct="0.025" maxDistErr="0.001" distanceUnits="kilometers" />
    <fieldType name="currency" class="solr.CurrencyField" precisionStep="8" defaultCurrency="USD" currencyConfig="currency.xml" />
             

    <!-- CJK bigram (see text_ja for a Japanese configuration using morphological analysis) -->
    <dynamicField name="*_txt_cjk" type="text_cjk"  indexed="true"  stored="true"/>
    <fieldType name="text_cjk" class="solr.TextField" positionIncrementGap="100">
      <analyzer>
        <tokenizer class="solr.StandardTokenizerFactory"/>
        <!-- normalize width before bigram, as e.g. half-width dakuten combine  -->
        <filter class="solr.CJKWidthFilterFactory"/>
        <!-- for any non-CJK -->
        <filter class="solr.LowerCaseFilterFactory"/>
        <filter class="solr.CJKBigramFilterFactory"/>
      </analyzer>
    </fieldType>
    
    <!-- Japanese using morphological analysis (see text_cjk for a configuration using bigramming)

         NOTE: If you want to optimize search for precision, use default operator AND in your query
         parser config with <solrQueryParser defaultOperator="AND"/> further down in this file.  Use 
         OR if you would like to optimize for recall (default).
    -->
    <dynamicField name="*_txt_ja" type="text_ja"  indexed="true"  stored="true"/>
    <fieldType name="text_ja" class="solr.TextField" positionIncrementGap="100" autoGeneratePhraseQueries="false">
      <analyzer>
        <tokenizer class="solr.JapaneseTokenizerFactory" mode="search"/>
        <!--<tokenizer class="solr.JapaneseTokenizerFactory" mode="search" userDictionary="lang/userdict_ja.txt"/>-->
        <!-- Reduces inflected verbs and adjectives to their base/dictionary forms (辞書形) -->
        <filter class="solr.JapaneseBaseFormFilterFactory"/>
        <!-- Removes tokens with certain part-of-speech tags -->
        <filter class="solr.JapanesePartOfSpeechStopFilterFactory" tags="lang/stoptags_ja.txt" />
        <!-- Normalizes full-width romaji to half-width and half-width kana to full-width (Unicode NFKC subset) -->
        <filter class="solr.CJKWidthFilterFactory"/>
        <!-- Removes common tokens typically not useful for search, but have a negative effect on ranking -->
        <filter class="solr.StopFilterFactory" ignoreCase="true" words="lang/stopwords_ja.txt" />
        <!-- Normalizes common katakana spelling variations by removing any last long sound character (U+30FC) -->
        <filter class="solr.JapaneseKatakanaStemFilterFactory" minimumLength="4"/>
        <!-- Lower-cases romaji characters -->
        <filter class="solr.LowerCaseFilterFactory"/>
      </analyzer>
    </fieldType>
    
  <!-- Similarity is the scoring routine for each document vs. a query.
       A custom Similarity or SimilarityFactory may be specified here, but 
       the default is fine for most applications.  
       For more info: http://wiki.apache.org/solr/SchemaXml#Similarity
    -->
  <!--
     <similarity class="com.example.solr.CustomSimilarityFactory">
       <str name="paramkey">param value</str>
     </similarity>
    -->
</schema>

あとは、KuromojiとCJK系のAnalyzerの比較のためにこんなフィールドを加えてみます。

    <field name="title_ja" type="text_ja" indexed="true" stored="true"/>
    <field name="title_cjk" type="text_cjk" indexed="true" stored="true"/>

ここまでやったら、Solrを再起動。

# service solr restart

インデックスに登録するデータを用意。
data.json

[
  {
    "id": "1",
    "title_ja": "[Kuromoji]今から、東京都へ向かいます。",
    "title_cjk": "ダミー"
  },
  {
    "id": "2",
    "title_ja": "ダミー",
    "title_cjk": "[CJK]今から、東京都へ向かいます。"
  }
]

登録。

$ curl -H 'Content-Type: application/json' 'http://localhost:8983/solr/mycore/update?commit=true' --data-binary @data.json
{"responseHeader":{"status":0,"QTime":388}}

検索してみます。

「title_ja」に「東京都」。

$ curl 'http://localhost:8983/solr/mycore/select?wt=json&indent=true' -d '{ "query": "title_ja:東京都" }'
{
  "responseHeader":{
    "status":0,
    "QTime":1,
    "params":{
      "indent":"true",
      "json":"{ \"query\": \"title_ja:東京都\" }",
      "wt":"json"}},
  "response":{"numFound":1,"start":0,"docs":[
      {
        "id":"1",
        "title_ja":"[Kuromoji]今から、東京都へ向かいます。",
        "title_cjk":"ダミー",
        "_version_":1511576419592830976}]
  }}

ヒットします。

「title_cjk」に「東京都」。

$ curl 'http://localhost:8983/solr/mycore/select?wt=json&indent=true' -d '{ "query": "title_cjk:東京都" }'
{
  "responseHeader":{
    "status":0,
    "QTime":3,
    "params":{
      "indent":"true",
      "json":"{ \"query\": \"title_cjk:東京都\" }",
      "wt":"json"}},
  "response":{"numFound":1,"start":0,"docs":[
      {
        "id":"2",
        "title_ja":"ダミー",
        "title_cjk":"[CJK]今から、東京都へ向かいます。",
        "_version_":1511576419594928128}]
  }}

ヒットします。

「title_ja」に「京都」。

$ curl 'http://localhost:8983/solr/mycore/select?wt=json&indent=true' -d '{ "query": "title_ja:京都" }'
{
  "responseHeader":{
    "status":0,
    "QTime":0,
    "params":{
      "indent":"true",
      "json":"{ \"query\": \"title_ja:京都\" }",
      "wt":"json"}},
  "response":{"numFound":0,"start":0,"docs":[]
  }}

「title_cjk」に「京都」。

$ curl 'http://localhost:8983/solr/mycore/select?wt=json&indent=true' -d '{ "query": "title_cjk:京都" }'
{
  "responseHeader":{
    "status":0,
    "QTime":1,
    "params":{
      "indent":"true",
      "json":"{ \"query\": \"title_cjk:京都\" }",
      "wt":"json"}},
  "response":{"numFound":1,"start":0,"docs":[
      {
        "id":"2",
        "title_ja":"ダミー",
        "title_cjk":"[CJK]今から、東京都へ向かいます。",
        "_version_":1511576419594928128}]
  }}

こちらは、bi-gramなのでヒットします。

動いてそうですね。

とりあえず、Managed Schema Definitionからschema.xmlに変更後、日本語検索をしてみるところまで、というところで。