け日記

最近はPythonでいろいろやってます

Solrでスキーマの定義とドキュメントの登録を行う

前回の投稿に引き続き、Solrに慣れ親しんでいきます。

ohke.hateblo.jp

今回の投稿では、スキーマの定義、および、JSONを使ったドキュメント登録を行います。引き続き、チュートリアルと↓の本を参考にしています。

[改訂第3版]Apache Solr入門――オープンソース全文検索エンジン (Software Design plus)

Solrサーバの起動とSolrCoreの作成

話を単純にするために、クラスタ (SolrCloud)ではなく、スタンドアロンで起動して、SolrCoreとして"kenikki"を作ります (前回の投稿も参考にしてみてください) 。

$ pwd
/tmp/solr/solr-7.5.0

$ ./bin/solr start
...
Waiting up to 180 seconds to see Solr running on port 8983 [/]
Started Solr server on port 8983 (pid=82558). Happy searching!

$ ./bin/solr create_core -c kenikki -d ./server/solr/configsets/_default
...
Created new core 'kenikki'

$ ls -l ./server/solr/
total 24
-rw-r--r--  1 onishik  wheel  3095  9 18 13:08 README.txt
drwxr-xr-x  4 onishik  wheel   128  9 18 13:08 configsets
drwxr-xr-x  5 onishik  wheel   160 12  1 11:34 kenikki
-rw-r--r--  1 onishik  wheel  2170  9 18 13:08 solr.xml
-rw-r--r--  1 onishik  wheel  1006  9 18 13:08 zoo.cfg

http://localhost:8983/ にアクセスすると、管理画面が表示されます。Core Adminに作成した"kenikki"が追加されていることも確認できます。

f:id:ohke:20181201114708p:plain

スキーマの定義

前回の投稿では、HTMLファイルを全く加工せずにPOSTしていましたので、一見不要そうなフィールドも多々有りました。今回は予めスキーマを定義します。

Solrのスキーマの属性は以下のようなものがあります (詳細はこちら) 。

  • name : フィールド名
  • type : 型 (Solr組み込みの型の一覧)
  • indexed : trueの場合、クエリで検索可能なフィールドになる (デフォルトはtrue)
  • stored : trueの場合、クエリの結果に値を含めることができる (デフォルトはtrue)
  • required : trueの場合、POST時の必須項目となる (デフォルトはfalse)
  • multiValued : trueの場合、複数の値を持つことができる (デフォルトはfalse)

今回は6つのフィールドを定義します。

  • published_datetimeは日時のため、pdate型で指定
  • tagは複数の値を持つ (ex. "Python","機械学習" など) ため、multiValuedをtrueにしてます
  • descriptionは、内容的にはcontentの一部なので検索のためにインデックスはさせないが、検索結果には表示することを想定して、storedはtrueにしてます
  • contentは、逆に検索に使うのでインデックスされている必要があるが、検索結果の表示には使わないことを想定して、storedはfalseにしてます
name type indexed stored required multiValued
url string -
published_datetime pdate -
title text_ja -
tag string -
description text_ja - - -
content text_ja - -

注意すべきは、string型とtext_ja型です。

  • stringは、solr.StrFieldクラスの実装で、完全一致やワイルドカード・正規表現などによる検索ができます
  • text_jaは、solr.TextFieldクラスの実装で、日本語に適したアナライザが設定されており、全角・半角などの表記揺れや、複合名詞による検索などにも対応できるようになります
    • アナライザでは文字フィルタ (charFilter) 、トークナイザ (tokenizer, 形態素解析器) 、トークンフィルタ(filter) を定義できます

いずれの型もmanaged-schemaに定義されてます。

...
  <fieldType name="string" class="solr.StrField" sortMissingLast="true" docValues="true"/>
...
  <fieldType name="text_ja" class="solr.TextField" autoGeneratePhraseQueries="false" positionIncrementGap="100">
    <analyzer>
      <tokenizer class="solr.JapaneseTokenizerFactory" mode="search"/>
      <filter class="solr.JapaneseBaseFormFilterFactory"/>
      <filter class="solr.JapanesePartOfSpeechStopFilterFactory" tags="lang/stoptags_ja.txt"/>
      <filter class="solr.CJKWidthFilterFactory"/>
      <filter class="solr.StopFilterFactory" words="lang/stopwords_ja.txt" ignoreCase="true"/>
      <filter class="solr.JapaneseKatakanaStemFilterFactory" minimumLength="4"/>
      <filter class="solr.LowerCaseFilterFactory"/>
    </analyzer>
  </fieldType>
...

スキーマは以下のように http://localhost:8983/solr/kenikki/schema にPOSTすることで定義できます。

$ curl -X POST -H 'Content-type:application/json' --data-binary '{
  "add-field": {
    "name": "url",
    "type": "string",
    "indexed": "true",
    "stored": "true",
    "required": "true",
    "multiValued": "false"
  },
  "add-field": {
    "name": "published_datetime",
    "type": "pdate",
    "indexed": "true",
    "stored": "true",
    "required": "true",
    "multiValued": "false"
  },
  "add-field": {
    "name": "title",
    "type": "text_ja",
    "indexed": "true",
    "stored": "true",
    "required": "true",
    "multiValued": "false"
  },
  "add-field": {
    "name": "tag",
    "type": "string",
    "indexed": "true",
    "stored": "true",
    "required": "false",
    "multiValued": "true"
  },
  "add-field": {
    "name": "description",
    "type": "text_ja",
    "indexed": "false",
    "stored": "true",
    "required": "false",
    "multiValued": "false"
  },
  "add-field": {
    "name": "content",
    "type": "text_ja",
    "indexed": "true",
    "stored": "false",
    "required": "true",
    "multiValued": "false"
  }
}', http://localhost:8983/solr/kenikki/schema

API経由の場合はmanaged-schemaファイルの方に書き込まれます。

...
  <field name="content" type="text_ja" multiValued="false" indexed="true" required="true" stored="false"/>
  <field name="description" type="text_ja" multiValued="false" indexed="false" required="false" stored="true"/>
  <field name="published_datetime" type="pdate" multiValued="false" indexed="true" required="true" stored="true"/>
  <field name="tag" type="string" multiValued="true" indexed="true" required="false" stored="true"/>
  <field name="title" type="text_ja" multiValued="false" indexed="true" required="true" stored="true"/>
  <field name="url" type="string" multiValued="false" indexed="true" required="true" stored="true"/>
...

またAPIへPOSTする以外に、schema.xmlファイル (ここではserver/solr/kenikki/conf/schema.xml) を作成・編集することでもスキーマを定義できます。

  • この場合は、SolrCoreのリフレッシュまたはSolr再起動が必要となります

ドキュメントの作成

上のスキーマに沿って各項目に値を投入するために、前回収集したけ日記のHTMLファイルから、JSONファイルを生成します。

  • JSON以外にも、XMLやCSVなどの形式でもOKです
  • 1つのHTMLファイルから、1つのJSONファイルを生成します
  • 各項目はBeautifulSoupで抽出してます
import os
import json
from bs4 import BeautifulSoup  # pip install beautifulsoup4

for file_name in os.listdir('/tmp/solr/documents/'):
    # HTMLファイル以外は除外
    if not file_name.endswith('.html'):
        continue
    
    # HTML文字列を取得
    with open('/tmp/solr/documents/' + file_name, 'r') as f:
        html = f.read()
    
    # BeautifulSoupで解析
    soup = BeautifulSoup(html, 'html.parser')
    
    # 各項目をHTMLから抽出
    entry_dict = {
        'url': soup.find('a', class_='entry-title-link bookmark')['href'],
        'published_datetime': soup.find('header', class_='entry-header').time['datetime'],
        'title': soup.title.string,
        'tag': [a.string for a in soup.find_all('a', class_='entry-category-link')],  # multiValueなのでリスト
        'description': soup.find('meta', property='og:description')['content'],
        'content': soup.find('div', class_='entry-content').text
    }
    
    # 1HTMLファイルにつき1JSONファイルでjsonディレクトリ以下に出力
    with open('/tmp/solr/documents/json/' + file_name.replace('.html', '.json'), 'w') as f:
        f.write(json.dumps(entry_dict, ensure_ascii=False))

ドキュメントの登録

生成されたJSONファイルをPOSTで登録します。前回と同じくpostスクリプトを使います。

$ ./bin/post -c kenikki /tmp/solr/documents/json/*
...
POSTing file 2018-11-17-230000.json (application/json) to [base]/json/docs
113 files indexed.
COMMITting Solr index changes to http://localhost:8983/solr/kenikki/update...
Time spent: 0:00:01.370

ドキュメントの検索

いくつかのパターンで検索してみます。

contentを"Pythonリスト"で検索すると、"Pythonリスト"そのものだけではなく、"Python"と"リスト"に分割してそれぞれ含むドキュメントがヒットします。text_jaで定義された形態素解析器によって、柔軟な検索が行われている例です。

http://localhost:8983/solr/kenikki/select?q=content:Pythonリスト

tagをワイルドカード検索するクエリです。tagに"Python"を含む記事がヒットします。

http://localhost:8983/solr/kenikki/select?q=tag:P?th*

published_datetimeで、2018年11月以降の記事を検索するクエリです。フィルタクエリfqを使うことで範囲検索ができます。

http://localhost:8983/solr/kenikki/select?fq=published_datetime:[2018-11-01T00:00:00Z TO NOW]&q=*:*

まとめ

今回はスキーマを定義して、JSONファイルでドキュメントを登録して、検索できるようにしました。

参考文献

[改訂第3版]Apache Solr入門――オープンソース全文検索エンジン (Software Design plus)

[改訂第3版]Apache Solr入門――オープンソース全文検索エンジン (Software Design plus)

  • 作者: 打田智子,大須賀稔,大杉直也,西潟一生,西本順平,平賀一昭,株式会社ロンウイット,株式会社リクルートテクノロジーズ
  • 出版社/メーカー: 技術評論社
  • 発売日: 2017/04/27
  • メディア: 大型本
  • この商品を含むブログを見る