Apache Solr で全文検索(4)

スキーマ定義例

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

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
記事公開日:2013年04月24日