Apache Solr で全文検索

第4回 スキーマ定義例

2013.04.24

今回は実際にスキーマを定義し検索できるところまでを説明したいと思います。

4.1 使用するデータ

使用するデータには青空文庫 で公開されているものを使用します。

ご存知の通り、青空文庫では著作権の切れた作品がテキストデータとして公開されています。また、登録されている作品一覧がCSVデータとして公開されています。

これらのデータを使い青空文庫用の検索スキーマを定義していきたいと思います。

4.2 青空文庫コアの用意

スキーマ定義の前に、青空文庫用のコアを用意します。

基本的な設定はサンプルのcollection1をコピーして使います。

$ cd ${solr.solr.home}
$ cp -r collection1 aozora

aozoraコアを作成したら ${solr.solr.home}/solr.xml にコアを追加します。ついでに、デフォルトのコアをcollection1からaozoraにします。

<solr persistent="true">
<cores adminPath="/admin/cores"
       defaultCoreName="aozora"
       host="${host:}"
       hostPort="${jetty.port:}"
       hostContext="${hostContext:}"
       zkClientTimeout="${zkClientTimeout:15000}">
  <core name="aozora" instanceDir="aozora" />
  <core name="collection1" instanceDir="collection1" />
</cores>
</solr>

4.3 スキーマの設計

青空文庫の作品一覧のページにはいくつかのCSVファイルが用意されているのですが、このうちデータ項目が充実している「公開中 作家別作品一覧拡充版:全て(CSV形式、UTF-8、zip圧縮)」を使用します。

ただ、作品一覧拡充版の全項目を使うのは大変なので、今回は以下の項目を使います。

  • 作品ID
  • 作品名
  • 副題
  • 原題
  • 作品著作権フラグ
  • 図書カードURL
  • 人物ID
  • 底本名1
  • 底本出版社名1
  • テキストファイルURL
  • XHTML/HTMLファイルURL

これらの項目からフィールドをどのようにするか決めていきます。

今回は 表4.1 のようにしました。

表4.1: 青空文庫スキーマ

作品一覧の項目名 フィールド名 indexed stored その他
  id string true true ユニークキー
作品ID bid string true true  
作品名 title text_ja true true  
副題 subtitle string false true  
原題 orgtitle text_ja false true  
著作権フラグ copyright boolean false true  
図書カードURL card_url string false true  
人物ID aid string true true  
family_name string false true  
first_name string false true  
底本名1 orgbook text_ja false true  
底本出版社名1 publisher text_ja true true  
テキストファイルURL text_url string false true  
HTMLファイルURL html_url string false true  
  author text_ja true false multiValued=”true”
  content string false true  
  text text_ja true false multiValued=”true”

ユニークキーであるidフィールドは、作品ID と 人物ID を “-” で繋げたものとしました。

著者名で検索するためのauthorフィールドを追加しました。authorフィールドはfamily_name、first_nameフィールドをコピーして作ります。

作品データをcontentフィールドに登録します。highlight検索に使いたいので、stored=”true”にしました。

textフィールドはドキュメントを包括的に検索するためのフィールドです。デフォルトで検索したいフィールドをすべてこのフィールドにコピーします。

schema.xml ファイルは以下のようになります。

<?xml version="1.0" encoding="UTF-8" ?>
<schema name="aozora" version="1.5">
  <fields>
      <field name="id" type="string" indexed="true" stored="true" required="true" />
      <field name="bid" type="string" indexed="true" stored="true" required="true" />
      <field name="title" type="text_ja" indexed="true" stored="true" required="true" />
      <field name="subtitle" type="string" indexed="true" stored="true" />
      <field name="orgtitle" type="text_ja" indexed="true" stored="true" />
      <field name="copyright" type="boolean" indexed="true" stored="true" />
      <field name="card_url" type="string" indexed="false" stored="true" required="ture"/>
      <field name="aid" type="string" indexed="true" stored="true" required="true" />
      <field name="family_name" type="string" indexed="false" stored="true" />
      <field name="first_name" type="string" indexed="false" stored="true" />
      <field name="orgbook" type="text_ja" indexed="true" stored="true" />
      <field name="publisher" type="text_ja" indexed="true" stored="true" />
      <field name="text_url" type="string" indexed="false" stored="true" />
      <field name="html_url" type="string" indexed="false" stored="true" />
      <field name="author" type="text_ja" indexed="true" stored="false" multiValued="true" />
      <field name="content" type="string" indexed="false" stored="true" />
      <field name="text" type="text_ja" indexed="true" stored="false" multiValued="true"/>
      <field name="_version_" type="long" indexed="true" stored="true"/>
  </fields>

  <uniqueKey>id</uniqueKey>

  <copyField source="first_name" dest="author" />
  <copyField source="family_name" dest="author" />
  <copyField source="author" dest="text" />
  <copyField source="title" dest="text" />
  <copyField source="orgtitle" dest="text" />
  <copyField source="publisher" dest="text" />
  <copyField source="content" dest="text" />

  <!-- types はデフォルトのため割愛します -->
</schema>

4.4 データの登録

スキーマを定義したら、作品一覧、およびテキストファイルからデータを登録します。

作品一覧から項目を取り出したり、テキストファイルの修正(文字コードの修正)を行うのにスクリプトを用意しました。

用意したスクリプトは以下の通りです。

  • aozora2solr.rb
    • 公開一覧CSVファイルからSolr用の項目を抜き出し、結果(CSV形式)を標準出力に書き出す。
  • download_textfile.rb
    • テキストファイルURLからファイルをダウンロードする。入力にはaozora2solr.rbが出力したCSVファイルを使用する。
  • make_update_doc.sh
    • ダウンロードしたファイルから登録用のXMLファイルを出力する。

リスト4.1: aozora2solr.rb

#!/usr/bin/env ruby
# -*- coding: utf-8 -*-

require 'csv'

rows = CSV.read(ARGV[0], { :headers => true, :return_headers => false} )

header = [
  "id",
  "bid",
  "title",
  "aid",
  "family_name",
  "first_name",
  "subtitle",
  "orgtitle",
  "orgbook",
  "publisher",
  "card_url",
  "text_url",
  "html_url",
  "copyright"
]

CSV { |csv_out|
  csv_out << header
  rows.each do |row|
      newrow = []
      newrow << "#{row[0]}-#{row[14]}"
      newrow << row[0]
      newrow << row[1]
      newrow << row[14]
      newrow << row[15]
      newrow << row[16]
      newrow << row[4]
      newrow << row[6]
      newrow << row[27]
      newrow << row[28]
      newrow << row[13]
      newrow << row[45]
      newrow << row[50]
      newrow << ((row[10] == "あり") ? "true" : "false")
      csv_out << newrow
  end
}

リスト4.2: download_textfile.rb

#!/usr/bin/env ruby
# -*- coding: utf-8 -*-

require 'csv'
require 'uri'

rows = CSV.read(ARGV[0], { :headers => true, :return_headers => false})
rows.each do |row|
  next if row["text_url"].empty?
  next if row["copyright"] == "true"
  id = row["id"]
  url = URI.parse(row["text_url"])
  system("curl -s -o #{id}.zip #{url}") if url.host =~ /aozora\.gr\.jp\Z/
end

リスト4.3: make_update_doc.sh

#!/bin/sh

ERROR=error.txt
PROGNAME=$(basename $0)

for i in "$@"
do
  if [ ! -e "$i" ]; then
      echo "$i: No such file" >&2
      continue
  fi
  ID=${i%%.zip}
  OUT="${ID}.xml"
  TMP=$(mktemp -t "${PROGNAME}") || exit 1

  cat /dev/null   >  ${OUT}
  cat << HERE     >> ${OUT}
<add>
  <doc>
      <field name="id">$ID</field>
      <field name="content" update="set"><![CDATA[
HERE

  unzip -q -c ${i} | iconv -f "SJIS" -t "UTF-8" >> ${TMP} 2>> ${ERROR}
  if [ $? -ne 0 ]; then
      echo "failed to make ${OUT}." >> ${ERROR}
      rm -f ${OUT} ${TMP}
      continue
  fi
  cat ${TMP} | tr -d '\r' >> ${OUT}

  cat << HERE     >> ${OUT}
      ]]></field>
  </doc>
</add>
HERE

  rm -f ${TMP}
done

最終的に出来上がったXMLファイルをSolr付属のpost.shを使って登録します。

$ post.sh *.xml

変換から登録までの手順をまとめるとこうなります。

$ aozora2solr.rb list_person_all_extended_utf8.csv > aozora.csv
$ download_textfile.rb aozora.csv
$ make_update_doc.sh *.zip
$ curl "http://localhost:8080/solr/aozora/update?commit=true" -H 'Content-Type: application/csv' --data-binary @aozora.csv
$ post.sh *.xml

4.5 検索

データを登録したら、検索して値がとれることを確認します。なお、テストのためlist_person_all_extended_utf8.csv全件ではなく、先頭1000件しか登録していません。

フィールド指定無しの”スリーピー”で検索した結果。

$ curl "http://localhost:8080/solr/select?q=%E3%82%B9%E3%83%AA%E3%83%BC%E3%83%94%E3%83%BC&wt=xml&indent=true"
<?xml version="1.0" encoding="UTF-8"?>
<response>

<lst name="responseHeader">
<int name="status">0</int>
<int name="QTime">1</int>
<lst name="params">
  <str name="indent">true</str>
  <str name="q">スリーピー</str>
  <str name="wt">xml</str>
</lst>
</lst>
<result name="response" numFound="1" start="0">
<doc>
  <str name="id">046658-001257</str>
  <str name="bid">046658</str>
  <str name="title">スリーピー・ホローの伝説</str>
  <str name="aid">001257</str>
  <str name="family_name">アーヴィング</str>
  <str name="first_name">ワシントン</str>
  <str name="subtitle">故ディードリッヒ・ニッカボッカーの遺稿より</str>
  <str name="orgtitle">THE LEGEND OF SLEEPY HOLLOW</str>
  <str name="orgbook">スケッチ・ブック</str>
  <str name="publisher">新潮文庫、新潮社</str>
  <str name="card_url">http://www.aozora.gr.jp/cards/001257/card46658.html</str>
  <str name="text_url">http://www.aozora.gr.jp/cards/001257/files/46658_ruby_44679.zip</str>
  <str name="html_url">http://www.aozora.gr.jp/cards/001257/files/46658_44767.html</str>
  <bool name="copyright">false</bool>
  <str name="content">
--(snip)--
      </str>
  <long name="_version_">1432312155267399680</long></doc>
</result>
</response>

author:”芥川” で検索した結果。

$ curl "http://localhost:8080/solr/select?q=author%3A%E8%8A%A5%E5%B7%9D&rows=5&fl=bid%2Ctitle&wt=xml&indent=true"
<?xml version="1.0" encoding="UTF-8"?>
<response>

<lst name="responseHeader">
<int name="status">0</int>
<int name="QTime">1</int>
<lst name="params">
  <str name="fl">bid,title</str>
  <str name="indent">true</str>
  <str name="q">author:芥川</str>
  <str name="wt">xml</str>
  <str name="rows">5</str>
</lst>
</lst>
<result name="response" numFound="368" start="0">
<doc>
  <str name="bid">000013</str>
  <str name="title">十本の針</str></doc>
<doc>
  <str name="bid">000014</str>
  <str name="title">あばばばば</str></doc>
<doc>
  <str name="bid">000015</str>
  <str name="title">アグニの神</str></doc>
<doc>
  <str name="bid">000016</str>
  <str name="title">秋</str></doc>
<doc>
  <str name="bid">000017</str>
  <str name="title">あの頃の自分の事</str></doc>
</result>
</response>

以上です。

次回は、このデータをもとにクライアントサイトを構築してみたいと思います。

著者プロフィール

toza

ミドルウェアから上の層を色々とやってます。長春系八極拳使い。

記事一覧Index