Wikipediaのコンテンツは Creative Commons Licenseおよび GNU Free Documentation Licenseの下にライセンスされておりWikipedia財団は再配布や再利用のために惜しげもなくこの貴重なデータベースのダンプファイル(XMLファイル)を一般提供している。全文検索の検証で大量のデータが必要なときこのWikipediaのような生きたデータを使えるのは非常に有りがたい。これはこのWikipediaデータベースダンプ(日本語)を元にAzure Searchインデックスを生成してみましょうというお話。

利用するWikipedia XMLファイルとその定義

最新版日本語レポジトリには複数のXMLファイルが用意されているがここでは全ページのタイトル、ディスクリプションといった要約データを集約しているファイルjawiki-latest-abstract.xmlを利用する。XMLのフォーマットは次のとおり。この中からtitle, url, abstractを抽出してAzure Searchに投入する。

<doc>
<title>Wikipedia: 自然言語</title>
<url>http://ja.wikipedia.org/wiki/%E8%87%AA%E7%84%B6%E8%A8%80%E8%AA%9E</url>
<abstract>自然言語(しぜんげんご、)とは、人間によって日常の意思疎通のために用いられる、文化的背景を持って自然に発展してきた記号体系である。大別すると音声に>よる話し言葉と文字や記号として書かれる書き言葉がある。</abstract>
<links>
<sublink linktype="nav"><anchor>概要</anchor><link>http://ja.wikipedia.org/wiki/%E8%87%AA%E7%84%B6%E8%A8%80%E8%AA%9E#.E6.A6.82.E8.A6.81</link></sublink>
<sublink linktype="nav"><anchor>関連項目</anchor><link>http://ja.wikipedia.org/wiki/%E8%87%AA%E7%84%B6%E8%A8%80%E8%AA%9E#.E9.96.A2.E9.80.A3.E9.A0.85.E7.9B.AE</link></sublink>
</links>
</doc>

尚、実際にダウンロードしてみるとわかると思うがこのファイルはサイズが比較的大きく集約されているドキュメント数も実に多い。カウントしてみたところ現時点(2015/06/09)で969541 件あった。Azure Searchの料金プランのうちFreeプランは最大ドキュメント数が10,000であることからここで利用する料金プランはFreeではなく標準プランを選択する必要がある。

Index Schema

インデックス名はwikipedia、フィールドはキーフィールドのためのitemidフィールドと上記wikipedia XMLファイルのtitle, url, abstractを格納するための3フィールドを定義。

{
    "name": "wikipedia",
    "fields": [
        { "name":"itemid", "type":"Edm.String", "key": true, "searchable": false },
        { "name":"title", "type":"Edm.String", "filterable":false, "sortable":false, "facetable":false},
        { "name":"abstract", "type":"Edm.String", "filterable":false, "sortable":false, "facetable":false, "analyzer":"ja.lucene" },
        { "name":"url", "type":"Edm.String", "sortable":false, "facetable":false }
    ]
}

Azure Search投入用JSONデータの生成

Wikipedia XMLファイルからAzure Search投入用のJSONデータを生成するスクリプト(xml2json.pl)を作ってみた。巨大ファイルのDOM構造を一度にオンメモリ展開することは物理的に不可能であるため、XML::Twigモジュールを使ってドキュメント毎にコールバック関数からフィールドデータを抽出するように工夫を凝らしている。

#!/usr/bin/perl -w
use strict;
use XML::Twig;
use Getopt::Std;
use Encode;
use JSON;

my $UPLOAD_THRESHOLD=100;
my %opts=();
getopts('c⭕', \%opts) or die "Wrong Options!\n";

my $inputfile=$opts{'c'};
my $outputpath=$opts{'o'};
usage() if ( $inputfile eq '' || $outputpath eq '' );

my($tmpcounter,$itemcounter, $filecounter) = (0,0,0);
my $itemsarr = [];

my $twig = new XML::Twig(
            twig_handlers => { doc => \&doc }
        );
$twig->parsefile($inputfile);
if ($tmpcounter) {
    flush2json($itemsarr, "items-$filecounter.json");
}

sub doc {
    my($twig, $doc)= @_;
    my $item = {
            itemid => "$itemcounter",
            title=> utf8string($doc->first_child('title')->text),
            abstract=> utf8string($doc->first_child('abstract')->text),
            url =>$doc->first_child('url')->text
        };
    push($itemsarr,$item);
    $tmpcounter++; $itemcounter++;
    if ($tmpcounter % $UPLOAD_THRESHOLD == 0 ){
        flush2json($itemsarr, "items-$filecounter.json");
        $tmpcounter=0;
        $filecounter++;
        $itemsarr=[];
    }
    $twig->purge;
}

sub flush2json {
    my ($iarr, $f)=@_;
    my $outarr->{'value'}=$iarr;
    my $json = JSON->new();
    my $js = $json->encode($outarr);
    open(P, "> $outputpath/$f") or die "can't open: $outputpath/$f \n";
    print P "$js";
    close P;
}

sub usage {
    print STDERR "Usage: $0 -c <inputxml> -o <outputpath>\n";
    exit(1);
}

sub utf8string {
    my $s= shift;
    if (utf8::is_utf8($s)) {
        return encode('utf-8', $s);
    }
    return $s;
}

__END__

次のように-cパラメータでインプットファイルを、-oパラメータでJSON出力のためのパスを指定する。実行すると-oパラメータで指定したパスにitems-N.json (N:0以上の整数)なファイルが大量に生成される。現時点でのデータから生成されたJSONファイルを数えてみたところitems-0.json …items-9692.jsonで合計9693個だった。

$ xml2json.pl -c ./jawiki-latest-abstract.xml -o <path to output>

Azure SearchにJSONデータを投入

上記ステップで生成されたJSONファイルからAzure SearchにPOSTするための簡易スクリプト(AzureSearch-AddItemsFromFile.sh)を作る。

#!/bin/sh

if [ $# -ne 1 ]
then
    echo "Usage: $0 <json.file>"
    exit 1;
fi

SERVICE_NAME='<Azure Search Service Name>'
API_VER='2015-02-28-Preview'
ADMIN_KEY='<API KEY>'
CONTENT_TYPE='application/json'
INDEX_NAME='wikipedia'
URL="https://$SERVICE_NAME.search.windows.net/indexes/$INDEX_NAME/docs/index?api-version=$API_VER"
JSON=`cat $1`

curl \
 -H "Content-Type: $CONTENT_TYPE"\
 -H "api-key: $ADMIN_KEY"\
 -XPOST $URL -d "$JSON"

最後に大量に生成されたJSONファイル群をファイル名を1つずつこのスクリプトの第一引数に指定して実行していく。1万近いファイルなので全て投入が完了するまでに少々時間はかかります。

for i in `ls -1tr *.json`;do echo $i; ./AzureSearch-AddItemsFromFile.sh $i;done