Apache Solr設定メモ

#はじめに
DB（Postgresql）の全文検索する必要が出てきそうだったので調査した際の内容のメモです。（Elasticsearchに押されている感がある気はしますが今回はこちら…）
全体的に[こちら](https://ameblo.jp/itboy/theme-10069131532.html)のブログを参考にさせていただきました。（説明されているバージョンが古いですが…）
英語が堪能であれば[リファレンス](https://www.apache.org/dyn/closer.lua/lucene/solr/ref-guide/)を参照するのが一番でしょうけれど、私はそうではないので…（ところどころ参照しますが）

#環境
 - Apache Solr 7.3.1
 - PostgreSQL 10.4

#事前準備
以下2点については終了している前提で説明を行います。

 - Solorのインストール
 - PostgreSQLのインストール

※Solrのコアは「test」で作成しました。


#PostgreSQLとの接続設定

 - Solrの動作設定ファイル（solrconfig.xml）

```xml:参照するjarファイルの追加
  <lib dir="${solr.install.dir:../../../..}/server/lib/" regex="postgresql-\d.*\.jar" />
  <lib dir="${solr.install.dir:../../../..}/dist/" regex="solr-dataimporthandler-\d.*\.jar" />
  <lib dir="${solr.install.dir:../../../..}/contrib/dataimporthandler-extras/lib/" regex=".*\.jar" />
```
dirでパスを指定すると、指定したディレクトリ以下のすべてのjarファイルが追加されます。また、regexに正規表現でファイル名を指定することで、追加の対象を指定した正規表現に一致するものに限定することができます。
今回はPostgreSQL接続用のjarファイルとDataImport時に使用するjarファイルを追加しています。

```xml:requestHandlerの設定追加
    <requestHandler name="/dataimport" class="org.apache.solr.handler.dataimport.DataImportHandler">
        <lst name="defaults">
          <str name="config">data-config.xml</str>
        </lst>
    </requestHandler>
```
ここではDataImportのための基本設定を記載します。詳細な設定内容はDataConfigファイルに記載します。
使用するDataConfigファイルは`<str name="config"></str>`で設定します。今回は`data-config.xml`という名称のファイルにしてあります。（DataConfigファイルについては後述します。）
また、PostgareSQL用のJDBCは事前に適切なバージョンのものを[ダウンロード](https://jdbc.postgresql.org/download.html)しておきます。

```xml:schemaFactoryの追加
    <schemaFactory class="ClassicIndexSchemaFactory"/>
```
今回は、`schema.xml`を使用してスキーマの定義を行うので、上記設定が必要になります。

```xml:updateProcessorの変更（以下を削除）
  <updateProcessor class="solr.AddSchemaFieldsUpdateProcessorFactory" name="add-schema-fields">
    <lst name="typeMapping">
      <str name="valueClass">java.lang.String</str>
      <str name="fieldType">text_general</str>
      <lst name="copyField">
        <str name="dest">*_str</str>
        <int name="maxChars">256</int>
      </lst>
      <!-- Use as default mapping instead of defaultFieldType -->
      <bool name="default">true</bool>
    </lst>

　　　　　　　　～（略）～

    <lst name="typeMapping">
      <str name="valueClass">java.lang.Number</str>
      <str name="fieldType">pdoubles</str>
    </lst>
  </updateProcessor>
```
デフォルトでは上記の設定がされているはずですが、`schema.xml`を使用する場合には必要ないため削除します。（コメントアウトする場合は<!-- -->が入れ子にならないように注意してください。※エラーになります）

```xml:updateRequestProcessorChainの変更
  <updateRequestProcessorChain name="add-unknown-fields-to-the-schema" default="${update.autoCreateFields:true}"
           processor="uuid,remove-blank,field-name-mutating,parse-boolean,parse-long,parse-double,parse-date,add-schema-fields">
    <processor class="solr.LogUpdateProcessorFactory"/>
    <processor class="solr.DistributedUpdateProcessorFactory"/>
    <processor class="solr.RunUpdateProcessorFactory"/>
  </updateRequestProcessorChain>
```
上記の`processor`の設定から`,add-schema-fields`を削除します。


 - データ検索のスキーマ定義ファイル（schema.xml）

```xml:field_nameの追加
  <field name="name" type="text_en" termPositions="true" termVectors="true" indexed="true" stored="true"/>
  <field name="body" type="text_en" termPositions="true" termVectors="true" indexed="true" stored="true"/>
  <field name="updated" type="pdate" termPositions="true" termVectors="true" indexed="true" stored="true"/>
```
データインポート先のテーブルに存在するカラムを追加します。
`name`：フィールド名
`type`：fieldTypeで設定してある型
`termPositions`：検索キーワードの含まれる位置を返すかどうか
`termVectors`：検索キーワードが含まれる数やキーワードの開始、終了位置などを結果に含めるかどうか
`indexed`：インデックスを付加するかどうか
`stored`：フィールドを検索結果に求めるかどうか

 - DBを検索対象にする場合のデータ構造定義ファイル

```xml:data-config.xml
<dataConfig>
  <dataSource driver="org.postgresql.Driver" url="jdbc:postgresql://localhost:5432/postgres" user="postgres" />
  <document>
      <entity name="test" query="select * from test"
              deltaQuery="select id from test where updated >= '${dataimporter.last_index_time}'"
              deltaImportQuery="select * from test where id=${dataimporter.delta.id}">
        <field column="id" name="id" />
        <field column="name" name="name" />
        <field column="body" name="body" />
        <field column="update" name="updated" />
      </entity>
  </document>
</dataConfig>
```
`dataSource`：データソースの設定
　`driver`：使用するドライバーの名称を設定します。今回はPostgreSQLに接続するため、`org.postgresql.Driver`を設定します。
　`url`：接続先のURLを設定します。今回は事前に作成したPostgreSQLのDBを指定しています。
　`user`：接続先に接続する際のユーザー名を設定します。
　`password`：接続先に接続する際のパスワードを設定します。今回はPostgreSQL側のパスワード設定をオフにしたので設定していません。
`document`
　`entity`：取得してくるデータの設定
　　`name`：エンティティの名称
　　`query`：全データを取得するSQL
　　`deltaQuery`：DB全データとSolr取得済み分データの差分を取得するSQL
　　`deltaImportQuery`：データをインポートする対象になるSQL。今回はdeltaQueryで取得したIDのデータのみを取得するSQL
　　`field`：データフィールドの設定
　　　`column`：DBのカラム名
　　　`name`：取得後のフィールド名


※余談
　Wikipediaからデータを取得する

```xml:dataConfigメモ
<dataConfig>
  <dataSource driver="org.postgresql.Driver"
              url="jdbc:postgresql://localhost:5432/wikipedia"
              user="wikipedia"
              password="secret" />
  <document>
    <entity name="page" query="SELECT page_id, page_title from page">
      <field column="page_id" name="id" />
      <field column="page_title" name="name" />
      <entity name="revision" query="select rev_id from revision where rev_page=${page.page_id}">
        <entity name="pagecontent" query="select old_text from pagecontent where old_id=${revision.rev_id}">
          <field column="old_text" name="text" />
        </entity>
      </entity>
    </entity>
  </document>
</dataConfig>
```
古い記事ですが、[Apache Solrのschema.xmlを読み解く](https://ameblo.jp/itboy/entry-11531282069.html?frm=theme)で詳細な説明がされていますのでご参照ください。

#検索設定
[Solrの形態素解析で使えるフィルタあれこれ](https://ameblo.jp/itboy/entry-11803122464.html?frm=theme)

####charFilter
charFilterは文字列の解析前にデータの加工を行うものです。（検索対象にするデータで不必要なものを排除したり、置き換えたりします）

 - マッピング用の定義ファイルを使用

```xml:schema.xml
<charFilter class="solr.MappingCharFilterFactory" mapping="[定義ファイルパス]"/>
```

```text:定義ファイルの記述
 "０" => "0"
 "１" => "1"
 "２" => "2"
 "３" => "3"
 "４" => "4"
 "５" => "5"
 "６" => "6"
 "７" => "7"
 "８" => "8"
 "９" => "9"
```
上記のように設定を行うと、全角数字が半角数字に変換することができます。


####tokenizer
検索ワードの分割のための解析方法についての設定を行います。

以下2つがよく使用されるようです。

 - 形態素解析による文字列分割

言語的に意味のある単位で単語を分割します。

```xml:schema.xml
<tokenizer class="solr.JapaneseTokenizerFactory" mode=[検索モード] userDictionary=[ユーザ固有設定の単語登録ファイル]  userDictionaryEncoding=[userDictionaryのエンコード]  discardPunctuation=true />
```
`mode`
　`nomal`：標準の単語分割
　`search`：検索に有効な単語分割
　`extended`：`search`＋ユニグラムの単語分割
`userDictionary`：ユーザー設定
　設定ファイルのサンプルは\example\example-DIH\solr\solr\conf\lang以下にある「userdict_ja.txt」を参照してください。

```text:userdict_ja.txt
#
# This is a sample user dictionary for Kuromoji (JapaneseTokenizer)
#
# Add entries to this file in order to override the statistical model in terms
# of segmentation, readings and part-of-speech tags.  Notice that entries do
# not have weights since they are always used when found.  This is by-design
# in order to maximize ease-of-use.
#
# Entries are defined using the following CSV format:
#  <text>,<token 1> ... <token n>,<reading 1> ... <reading n>,<part-of-speech tag>
#
# Notice that a single half-width space separates tokens and readings, and
# that the number tokens and readings must match exactly.
#
# Also notice that multiple entries with the same <text> is undefined.
#
# Whitespace only lines are ignored.  Comments are not allowed on entry lines.
#

# Custom segmentation for kanji compounds
日本経済新聞,日本 経済 新聞,ニホン ケイザイ シンブン,カスタム名詞
関西国際空港,関西 国際 空港,カンサイ コクサイ クウコウ,カスタム名詞

# Custom segmentation for compound katakana
トートバッグ,トート バッグ,トート バッグ,かずカナ名詞
ショルダーバッグ,ショルダー バッグ,ショルダー バッグ,かずカナ名詞

# Custom reading for former sumo wrestler
朝青龍,朝青龍,アサショウリュウ,カスタム人名
```
`discardPunctuation`：falseを設定すると句読点を保持します。trueを設定すると破棄します。（デフォルトはtrue）



 - N-gram トークナイザ

ある指定された文字数区切りで単語を分割します。よく用いられるのは2文字ずつに分割するBigramです。

```xml:schema.xml
<tokenizer class="solr.NgramTokenizerFactory" minGramSize="" maxGramSize="" />
```
`minGramSize`：分割の最小文字数（0より大きい）
`maxGramSize`：分割の最大文字数（minGramSize以上）

上記二つを併用する方法での検索を行う方法については記事にまとめてくださっている方がいたので、以下をご参照ください。
[形態素解析とNgramを併用したハイブリッド検索をSolrで実現する方法](https://tech.starttoday-tech.com/entry/solr_hybrid_search)

####filter
Solrにおける文字列検索時のフィルターの設定です。上記のトークナイザの設定により使用できるものが変わります。

 - 類語のパターンを登録して検索対象にする

```xml:schema.xml
<filter class="solr.SynonymGraphFilterFactory" synonyms="[類語のパターンファイルパス]"/>
```
`synonyms`：類語のパターンを記載したファイルへのパスを記載します。

```xml:例
couch,sofa,divan
teh => the
huge,ginormous,humungous => large
small => tiny,teeny,weeny
```
 - ','区切り
　','で並べている単語をすべて類語として扱います。
 - '=>'
　'=>'の左側に記載された単語は、右側に記載された単語として扱われます。（左側の単語自体は右の変換対象に含めない限り検索対象になりません。）

`ignoreCase`：trueであれば、英文字(アルファベット)の大文字と小文字を区別しないで類語を判別します。(デフォルトはfalse)
`expand`：trueであれば、同義語を全件抽出します。falseであれば、最初に見つかった同義語のみ抽出します。(デフォルトはtrue)と
`format`：類語の変換コントロール。
`tokenizerFactory`：同義語ファイルを変換するときに使用するtokenizerFactoryの名称。
`analyzer`：同義語ファイルを変換するときに使用するアナライザークラスの名称。

 - 動詞を基本形にする

```xml:schema.xml
<filter class="solr.JapaneseBaseFormFilterFactory"/>
```

 - 品詞を除外する

```xml:schema.xml
<filter class="solr.JapanesePartOfSpeechStopFilterFactory"  tags="lang/stoptags_ja.txt" enablePositionIncrements="true"/>
```
`tags`：除外対象のリストファイルパスを設定します。リストファイルのサンプルがサンプルフォルダのconf/lang/stoptags_ja.txtにあります。トークンを除外した際に
`enablePositionIncrements`：これがfalseかつバージョンが4.3以前であれば、フィルタで除外された位置を保持しない？らしいです。5.0以降では修正済みとのこと。

 - 全角⇔半角の処理を行う

```xml:schema.xml
<filter class="solr.CJKWidthFilterFactory"/>
```
全角英字を半角、半角カタカナを全角にします。全角数字は対象外。（MappingCharFilterFactoryを先に通す等の対応が必要）

 - カタカナの長音を取り除く
「コンピューター」と「コンピュータ」など、同じ意味でも長音をつけたりつけなかったりする単語を統一するために設定します。

```xml:schema.xml
<filter class="solr.JapaneseKatakanaStemFilterFactory" minimumLength="4"/>
```
`minimumLength`：設定値より文字数が多い場合除外の対象となります。設定値はデフォルトが4、最小値が2です。

 - 英字を小文字に変換する
英字をすべて小文字に変換することで表記ゆれを統一します。

```xml:schema.xml
<filter class="solr.LowerCaseFilterFactory"/>
```


 - インデックスさせない単語を登録
「て」「に」「を」「は」や、「によって」「に関して」など検索時に必要ない単語を除外します。

```xml:schema.xml
<filter class="solr.StopFilterFactory" words="lang/stopwords_ja.txt"  ignoreCase="true"/>
```
`words`：インデックスさせない単語リストパスを設定します。
`format`
`ignoreCase`：trueであれば、英文字(アルファベット)の大文字と小文字を区別しないで類語を判別します。(デフォルトはfalse)
`enablePositionIncrements`

#サジェスト機能設定
サジェスト機能を有効にすることで、検索時に入力した内容の候補を表示できるようになります。（[おいしい健康](http://oishi-kenko.hatenablog.com/entry/2017/11/07/181446)様のブログにて詳細が書かれていました。）

####候補語として表示するデータについて
詳細は参考にさせていただいたブログを参照していただきたいのですが、「検索対象に対する形態素解析」もしくは「検索ログ」を使用する方法があるそうです。
どちらを使用するかは、メリット・デメリットからシステムに合わせる形で選択してください。

####solrconfig.xmlの設定
 - searchComponentの追加

とりあえず、`solr.SpellCheckComponent`クラスを使用して設定を追加します。（Solrのものでなくてもいいのだと思います。）

```xml:searchComponent
<searchComponent class="solr.SpellCheckComponent" name="[searchComponentの名称]">
    <lst name="[名称]">
      <str name="name">[searchComponentの名称]</str>
      <str name="classname">org.apache.solr.spelling.suggest.Suggester</str>
      <str name="lookupImpl">org.apache.solr.spelling.suggest.fst.AnalyzingLookupFactory</str>
      <str name="buildOnCommit">true</str>
      <str name="comparatorClass">score</str>
      <str name="field">[検索対象のフィールド名]</str>
      <str name="suggestAnalyzerFieldType">[解析に使用するfieldType名]</str>
      <bool name="exactMatchFirst">true</bool>
    </lst>
    <str name="queryAnalyzerFieldType">[解析に使用するfieldType名]</str>
</searchComponent>
```


 - requestHandlerの追加

上記で定義したsearchComponentを指定したパスで受けられるように、requestHandlerを定義します。

```xml:requestHandler
<requestHandler name="[パス]" class="org.apache.solr.handler.component.SearchHandler">
    <lst name="defaults">
      <str name="spellcheck">true</str>
      <str name="spellcheck.dictionary">[コンポーネント名]</str>
      <str name="spellcheck.collate">true</str>
      <str name="spellcheck.count">100</str>
      <str name="spellcheck.onlyMorePopular">true</str>
    </lst>
    <arr name="components">
      <str>[コンポーネント名]</str>
    </arr>
</requestHandler>
```

#コマンド
コンソールからのSolrの起動・終了などを行うためのコマンドで、今回使ったものは以下になります。

```console:起動
　solr start (-d [サーバディレクトリ名])
```
```console:終了
　solr stop -all
```
```console:コア追加
　solr create -c [コア名]
```
補足ですが、私は環境変数のPathに\solr-7.3.1\binを登録しておきました。
（毎回cdしたりパス入れるのは面倒ですし）


#おわりに
検索エンジンについて調べたのは初めてなのですが、検索用の変換を行ったりするフィルタがたくさんあるので、いろいろ対応できそうだと思う反面、適用順序などを気をつけないと意図しないフィルタのかかり方がされてしまうようですし、なかなか難しいところがあると思いました。
また、Apache Solr以外（Elasticsearchがわりとメジャーなんですかね？）の検索エンジンもいろいろあるようですし、いろいろ比較してみて使いやすいものを見つけたほうがよいのかもしれません。

公式ドキュメントが英語だったため、訳がおかしなところがあるかもしれませんが、その場合、ご指摘いただければ幸いです。