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 8月 12 09:16 currency.xml -rw-rw-r-- 1 solr solr 1348 8月 12 09:16 elevate.xml drwxrwxr-x 2 solr solr 4096 9月 6 13:06 lang -rw-rw-r-- 1 solr solr 55614 8月 12 09:16 managed-schema -rw-rw-r-- 1 solr solr 308 8月 12 09:16 params.json -rw-rw-r-- 1 solr solr 873 8月 12 09:16 protwords.txt -rw-rw-r-- 1 solr solr 61532 8月 12 09:16 solrconfig.xml -rw-rw-r-- 1 solr solr 781 8月 12 09:16 stopwords.txt -rw-rw-r-- 1 solr solr 1119 8月 12 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に変更後、日本語検索をしてみるところまで、というところで。