Wikipediaのデータをスクロールスキャンする

WikipediaをElasticsearchに取り込むの続き。
いろいろと試していくためには、Elasticsearchに取り込んだWikipediaのデータにアクセスできるようになる必要がある。Elasticsearchで全データにアクセスするためには、ScrollScanでアクセスする。
というわけで、今回はPythonで全データにアクセスしてみる。
事前にpip install ElasticsearchなどでPythonモジュールを利用可能な状況にしておく必要があるが、以下のような.pyを作成する。

# coding: utf-8
from optparse import OptionParser
import sys
from elasticsearch import Elasticsearch
import json
from elasticsearch.exceptions import NotFoundError
def main(argv=sys.argv):
    parser = OptionParser(prog="main.py")
    parser.add_option("--elasticsearch", dest="elasticsearch",
                  help="Elasticsearch", metavar="ES",
                  default="localhost:9200")
    parser.add_option("--index", dest="index",
                  help="Index", metavar="INDEX",
                  default="_all")
    parser.add_option("--source", dest="source",
                  help="Source", metavar="SOURCE",
                  default="{\"query\":{\"match_all\":{}}}")
    (options, args) = parser.parse_args()
    source = json.loads(options.source)
    es = Elasticsearch(hosts=options.elasticsearch)
    response = es.search(index=options.index,
                         scroll='1m',
                         search_type='scan',
                         size=100,
                         body=source)
    scroll_id = response['_scroll_id']
    counter = 0
    while (True):
        try:
            response = es.scroll(scroll_id=scroll_id, scroll='1m')
            if len(response['hits']['hits']) == 0:
                break
            for hit in response['hits']['hits']:
                counter = counter + 1
                if "_source" in hit:
                    if "title" in hit['_source']:
                        print hit['_source']['title']
        except NotFoundError:
            print u"Finished ({0}) documents.".format(counter)
            break
        except:
            print u"Unexpected error: {0}".format(sys.exc_info()[0])
            break
    return 0
if __name__ == "__main__":
    sys.exit(main())

上記の例ではスクロールで_sourceのtitleデータを出力している。
NotFoundErrorのエラーで止めるのも微妙だけど、何か良い方法があるのかしら…。
実行は

$ python title.py --index jawiki-pages-articles

という感じで実行すれば、タイトルが出力されていく。
というわけで、そのあたりのコードをいじればいろいろできる感じかな。

WikipediaをElasticsearchに取り込む

どこまで継続できるかというのはあるけど、Elasticsearchを使って機械学習的なことをちょっとまとめていこうかと。
まず、いろいろとやるためには適用するデータが必要になるけど、ここではWikipediaのデータを使ってあれこれしてみようかな。
MovieLensとか、IRISとか、定番データは世の中に存在していると思うけど、あれはあれできれいな感じだからHelloWorld的なことをしたければ、それらを使ってみるのが良いかと思う。
Elasticsearchをここからダウンロードして、展開した後にkuromojiをインストールしておく。

$ wget https://download.elasticsearch.org/elasticsearch/elasticsearch/elasticsearch-1.4.2.tar.gz
$ tar zxvf elasticsearch-1.4.2.tar.gz
$ cd elasticsearch-1.4.2/
$ bin/plugin -install elasticsearch/elasticsearch-analysis-kuromoji/2.4.1

今回は、WikipediaのデータをElasticsearchに入れるところまでを試してみる。
Wikipediaのデータを投入するのには、WikipediaのRiverを利用する。
WikipediaのRiverは

$ bin/plugin -install elasticsearch/elasticsearch-river-wikipedia/2.4.1

でElasticsearch 1.4系のものをインストールできる。
インストール後、Elasticsearchを起動する。

$ ./bin/elasticsearch &

次にWikipediaのデータをアーカイブされているところから取得する(ダウンロードしたものを置くところはどこでも良いです)。

$ mkdir wikipedia
$ cd wikipedia/
$ wget http://dumps.wikimedia.org/jawiki/latest/jawiki-latest-pages-articles.xml.bz2

ここまで準備できれば、あとはRiverを実行するだけだが、事前に登録先のインデックスを作成しておく。

$ curl -XPUT 'http://localhost:9200/jawiki-pages-articles/' -d '
{
  "settings": {
    "index":{
      "refresh_interval": "60s",
      "number_of_replicas":"0",
      "number_of_shards":"5"
    },
    "analysis":{
      "tokenizer":{
        "ja_tokenizer":{
          "type":"kuromoji_tokenizer",
          "mode":"normal",
          "discard_punctuation":"false",
          "user_dictionary":"userdict_ja.txt"
        }
      },
      "analyzer":{
        "ja_analyzer":{
          "type":"custom",
          "tokenizer":"ja_tokenizer"
        }
      }
    }
  },
  "mappings":{
    "page":{
      "_all": {
        "enabled": false
      },
      "properties":{
        "category":{
          "type":"string",
          "index":"not_analyzed"
        },
        "disambiguation":{
          "type":"boolean"
        },
        "link":{
          "type":"string",
          "index":"not_analyzed"
        },
        "redirect":{
          "type":"boolean"
        },
        "redirect_page":{
          "type":"string",
          "index":"not_analyzed",
          "copy_to":"redirect_page_text"
        },
        "redirect_page_text":{
          "type":"string",
          "analyzer":"ja_analyzer"
        },
        "special":{
          "type":"boolean"
        },
        "stub":{
          "type":"boolean"
        },
        "text":{
          "type":"string",
          "analyzer":"ja_analyzer"
        },
        "title":{
          "type":"string",
          "index":"not_analyzed",
          "copy_to":"title_text"
        },
        "title_text":{
          "type":"string",
          "analyzer":"ja_analyzer"
        }
      }
    }
  }
}'

$ES_HOME/config/userdict_ja.txtは空ではだめなので、kuromojiの辞書を適当に作成してください。
kuromojiの設定は後日変えるかもしれませんが、とりあえず、そんな感じにしておきます。
titleプロパティとかはanalyzeしない方が良さそうな場合もありそうなので、形態素解析用にはcopy_toで別プロパティを作成しておくことにします。
あとは、

$ curl -XPUT localhost:9200/_river/jawiki-pages-articles/_meta -d '
{
    "type":"wikipedia",
    "wikipedia":{
        "url":"file:$ES_HOME/wikipedia/jawiki-latest-pages-articles.xml.bz2"
    },
    "index":{
        "bulk_size":1000
    }
}'

を実行してしばらく待つとデータができあがりです($ES_HOMEを適当に置き換えてください)。
latestを持ってきているので、実行したときによって違うかと思いますが、190万ドキュメントで7Gくらいの量のようです。
次回以降でこれをつかっていろいろと試せれば良いかな。

elasticsearch-pyを試す

ElasticsearchのPython用クライアントであるelasticsearch-pyを試してみる。
pipが利用できる環境であれば、

$ sudo pip install elasticsearch

とすれば利用できるようになる。
あとは、以下のような感じでes.pyとか適当に作って、

# -*- coding: utf-8 -*-
from datetime import datetime
from elasticsearch import Elasticsearch
es = Elasticsearch("localhost:9200")
# データの登録
res1 = es.index(index="sample", doc_type="data",
                  body={"msg":"Hello", "timestamp": datetime.now()}, id="1")
print(res1)
# 検索
res2 = es.search(index="sample", doc_type="data",
                 body={"query":{"match_all":{}}})
print(res2)
# インデックスの削除
res3 = es.indices.delete(index="sample")
print(res3)

これを実行すれば、アクセスすることができます。
結構、簡単に使えますね。

Synonymプラグイン

Elasticsearch Analysis SynonymLUCENE-5252で上がっているNGramSynonymTokenizerをElasticsearchで利用できるようにしたプラグインです。詳しいことは、LUCENE-5252をググって確認してもらうのが良いですが、無邪気にSynonymTokenFilterを使っていると予期しない問題に遭遇することもあるかと思いますが(特に日本語とかで)、その辺の対応ができるかと思います。このプラグインとしては、NGramSynonymTokenizerをngram_synonymとして登録してあるので、インデックスの設定でanalyzerとして利用設定すれば使うことができます。というわけで、日本語検索では利用してみるとよいと思います。

Reindexingプラグイン

ReindexingプラグインはElasticsearchでドキュメントを追加したときにJSONデータを保存しているsourceフィールドを利用して、スクロールスキャンで既存のデータをマルっと別のインデックスへ再登録することができるプラグインです。
これにより、インデックスをコピーすることができます。たとえば、

$ curl -XPOST localhost:9200/sample/_reindex/newsample/

とすることで、sampleインデックスをnewsampleインデックスへコピーが簡単にできます。
さらに、urlパラメータを利用することで、別のElasticsearchクラスタへもデータを送ることができます。

$ curl -XPOST localhost:9200/sample/_reindex/newsample/?url=http%3A%2F%2Flocalhost%3A9200%2F

という感じで、登録先を指定することもできます。
あとは、コピー先のインデックスを用意しておかなければ、作成時にインデックスが作られることになりますが、既存と異なるインデックスセッティングやマッピングでインデックスを作成しておいてから再インデキシングを実行すると、既存と異なる設定のインデックスに置き換えることもできます。これをすることで、Analayzerの比較検証とかもやりやすくなるかと思います。