Amazon国別サイト横断検索のために - Apache Solr (その2)

前回で Solr が使用可能になったので、今回は検索で取得したデータを登録してみる。

対象データ

以下の条件で取得したデータ(2013-08-15時点で25件)を利用する。日本語も入れとかないとなあ、ということで Artist=カステラ としてみた。

http://ecs.amazonaws.jp/onca/xml
?Service=AWSECommerceService
&Version=2011-08-01
&Operation=ItemSearch
&AssociateTag=kahnn-22
&SearchIndex=Music
&Artist=カステラ
&Sort=titlerank
&Availability=Available
&ResponseGroup=ItemAttributes,Tracks
&ItemPage=1

スキーマ設計

Solrの方は、example ディレクトリを利用し、example/solr/collection1/conf/schema.xml を変更したもので試す。
まずは商品データにあったスキーマ設計が必要となる。最初から全部の要素を扱うのは大変なので、幾つか特徴的な要素を選んでみたのが以下の表になる。

Amazon商品データスキーマ
項目 フィールド名 indexed stored その他
ID(post時にASINの値をそのまま利用) id long true true required="true", uniqueKey
VERSION _version_ long true true
Timestamp timestamp date true true default="NOW"
ASIN asin string true true required="true"
EAN ean string true true
DetailPageURL(商品詳細ページのURL) detail_url string false true
Title title text_ja true true
Artist artist text_ja true true multiValued="true"
Label label text_ja true true
ReleaseDate release_date date true true
ListPrice/FormattedPrice(国別に整形された値段表示) formatted_price string false true
ListPrice/Amount(値段の数字(単位無視)) price long true true

この設計に従って schema.xml を以下のように記述した。fieldType についてはサンプルの通りで、比較用に text_cjk type の CopyField である text を用意している以外は、ほとんど上記設計のままとなっている。

<?xml version="1.0" encoding="UTF-8" ?>
<schema name="amazon_product_sample" version="1.5">

 <types>
  <fieldType name="string" class="solr.StrField"
             sortMissingLast="true" />
  <fieldType name="long" class="solr.TrieLongField"
             precisionStep="0" positionIncrementGap="0"/>
  <fieldType name="date" class="solr.TrieDateField"
             precisionStep="0" positionIncrementGap="0"/>
  <fieldType name="text_ja" class="solr.TextField"
             positionIncrementGap="100" autoGeneratePhraseQueries="false">
    <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" ignoreCase="true" words="lang/stopwords_ja.txt" />
      <filter class="solr.JapaneseKatakanaStemFilterFactory" minimumLength="4"/>
      <filter class="solr.LowerCaseFilterFactory"/>
    </analyzer>
  </fieldType>
  <fieldType name="text_cjk" class="solr.TextField" positionIncrementGap="100">
    <analyzer>
      <tokenizer class="solr.StandardTokenizerFactory"/>
      <filter class="solr.CJKWidthFilterFactory"/>
      <filter class="solr.LowerCaseFilterFactory"/>
      <filter class="solr.CJKBigramFilterFactory"/>
    </analyzer>
  </fieldType>

 </types>

 <fields>
   <field name="id" type="string" indexed="true" stored="true" required="true" />
   <field name="_version_" type="long" indexed="true" stored="true" />
   <field name="timestamp" type="date" indexed="true" stored="true" default="NOW" />
   <field name="asin" type="string" indexed="true" stored="true" required="true" />
   <field name="ean" type="string" indexed="true" stored="true" />
   <field name="detail_url" type="string" indexed="false" stored="true" />
   <field name="title" type="text_ja" indexed="true" stored="true" />
   <field name="artist" type="text_ja" indexed="true" stored="true" />
   <field name="label" type="text_ja" indexed="true" stored="true" />
   <field name="release_date" type="date" indexed="true" stored="true" />
   <field name="formatted_price" type="string" indexed="false" stored="true" />
   <field name="price" type="long" indexed="true" stored="true" />
   <field name="text" type="text_cjk" indexed="true" stored="false" multiValued="true" />
 </fields>

 <uniqueKey>id</uniqueKey>

 <copyField source="title" dest="text" />
 <copyField source="artist" dest="text" />
 <copyField source="label" dest="text" />

</schema>
余談1
Solr4.4の schema.xml を見てると多言語対応周りが強化されていて 3.6より前の情報と随分異なる。これからさわる人にとっては随分と楽になっているという訳で、先人の貢献に感謝。
余談2
複数の CopyField としてフィールド名 "text" を使用しているが、この "text" フィールドが無いと、起動時にエラーが出る。example/solr/collection1/conf/solrconfig.xml では、"text" フィールドが有ることを前提にして各種設定が書かれており、これらを修正するくらいならば、schema で用意した方が楽。(まあ、本格的に使用する場合は、ちゃんと吟味すべきでしょうけど)

データ加工と登録

次に商品データを登録する必要があるが、post.jar を 使って登録する場合、Solr で追加する際の型式に合わせてやる必要がある。今回は xslt で所定の xml 型式に変換する事にした。
完成イメージは以下のようになる。文法については UpdateXmlMessages を参照。

<add>
  <doc>
    <field name="id">XXXXXXXXX</field>
    <field name="asin">XXXXXXXXX</field>
    <field name="detail_url">http://......</field>
    <field name="ean">XXXXXXXXXX</field>
    <field name="title">世界の娯楽</field>
    <field name="artist">カステラ</field>
    <field name="label">SME</field>
    <field name="release_date">1991-11-12</field>
    <field name="formatted_price">¥1,000</field>
    <field name="price">1000</field>
  </doc>
</add>

そのための xslt スクリプト(amazon-to-solr.xsl)は以下の通り。

<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet 
  xmlns:xsl="http://www.w3.org/1999/XSL/Transform" 
  xmlns:aws="http://webservices.amazon.com/AWSECommerceService/2011-08-01"
  version="1.0">
<xsl:output method="xml" encoding="UTF-8"/>

<!-- Root Node -->
<xsl:template match="/">
  <add>
    <xsl:apply-templates select="aws:ItemSearchResponse/aws:Items/aws:Item"/>
  </add>
</xsl:template>

<xsl:template match="aws:ItemSearchResponse/aws:Items/aws:Item">
  <doc>
    <field name="id"><xsl:value-of select="aws:ASIN" /></field>
    <field name="asin"><xsl:value-of select="aws:ASIN" /></field>
    <field name="detail_url"><xsl:value-of select="aws:DetailPageURL" /></field>
    <field name="ean"><xsl:value-of select="aws:ItemAttributes/aws:EAN" /></field>
    <field name="title"><xsl:value-of select="aws:ItemAttributes/aws:Title" /></field>
    <field name="artist"><xsl:value-of select="aws:ItemAttributes/aws:Artist" /></field>
    <field name="label"><xsl:value-of select="aws:ItemAttributes/aws:Label" /></field>
    <field name="release_date"><xsl:value-of select="aws:ItemAttributes/aws:ReleaseDate" />T00:00:00Z</field>
    <xsl:apply-templates select="aws:ItemAttributes/aws:ListPrice" />
  </doc>
</xsl:template>

<xsl:template match="aws:ItemAttributes/aws:ListPrice">
    <xsl:element name="field">
      <xsl:attribute name="name">formatted_price</xsl:attribute>
      <xsl:value-of select="aws:FormattedPrice" />
    </xsl:element>
    <xsl:element name="field">
      <xsl:attribute name="name">price</xsl:attribute>
      <xsl:value-of select="aws:Amount" />
    </xsl:element>
</xsl:template>

</xsl:stylesheet>

Macには xsltproc が入っているので、以下のようにして変換可能。なお、amazon-data-1.xmlamazonから取得した商品xmlデータ。

$ xsltproc amazon-to-solr.xsl amazon-data-1.xml > solr-data-1.xml

後は post すればよい。

$ cd example/exampledocs/
$ java -jar post.jar ..../solr-data-*.xml

collection1 の Overviewで見ると、ドキュメントが25件登録された。これで準備OK。

フィールドの設定と解析状況については、Analysisから色々とキーワードを入力して試すことが出来る。text_cjk, text_ja の違いなどもわかりやすい。
実際の検索結果については、Queryから検索キーを入力して試すことが出来る。

検索による商品の確認

例えば、'artist:カステラ AND title:新世界' で検索したとする。なお、scoreとリリース日付でソートして、json型式で取得する。

$ curl 'http://localhost:8983/solr/collection1/select/' -d 'indent=on' \
       -d 'fl=score,asin,title,artist,price,label,release_date' -d 'wt=json' \
       -d 'sort=score+desc,release_date+desc' --data-urlencode 'q=artist:カステラ AND title:新世界'

{
  "responseHeader":{
    "status":0,
    "QTime":29,
    "params":{
      "sort":"score desc,release_date desc",
      "fl":"score,asin,title,artist,price,label,release_date",
      "indent":"on",
      "q":"artist:カステラ AND title:新世界",
      "wt":"json"}},
  "response":{"numFound":4,"start":0,"maxScore":2.792567,"docs":[
      {
        "asin":"B00005G4UJ",
        "title":"新世界",
        "artist":"カステラ",
        "label":"エピックレコードジャパン",
        "release_date":"1998-03-21T00:00:00Z",
        "price":2039,
        "score":2.792567},
      {
        "asin":"B000064QIH",
        "title":"新世界",
        "artist":"カステラ",
        "label":"ソニーレコード",
        "release_date":"1993-11-21T00:00:00Z",
        "price":2854,
        "score":2.792567},
      {
        "asin":"B00005G4UF",
        "title":"世界の娯楽",
        "artist":"カステラ",
        "label":"エピックレコードジャパン",
        "release_date":"1998-03-21T00:00:00Z",
        "price":2039,
        "score":1.0660849},
      {
        "asin":"B000UUPTKG",
        "title":"世界の娯楽",
        "artist":"カステラ",
        "label":"株式会社ソニー・ミュージックレコーズ",
        "release_date":"1989-09-01T00:00:00Z",
        "score":1.0660849}]
  }}

これで4つヒットしたわけだけど、タイトルとしては2つ。ここまで取得できれば、title と artist でマージするのは簡単だろう。その上で、「新世界」については asin=B00005G4UJ の 2039円 の方をお薦めするとかができそうである。同様に、「世界の娯楽」については price が設定されている*1 asin= B00005G4UF を薦めれば良い。
他にも色々と試してみたが、Amazonからの検索データをそのまま使うのではなく、キャッシュも兼ねた Solr に一度格納してから最適化するというのは結構使えそうに感じた。

参考

Solrはドキュメントも結構そろっているのだけど、やりたいことに辿り着くのが意外と難しくて、最初は慣れないこともあって苦労した。この書籍は対象が1.4と古いものの、概要や基本的な考え方を抑えるのに便利だと思う。正直、最初にこれを読んでから取り組めば良かった。(買って読んだのは設定をいじりだしてから)

4.4 では色々と変更も多いので、新版がでると良いんですけどね。

*1:設定があるかどうかだけなら、検索条件に "AND +price:*" を追加しても良い