🗂️

Docker+ElasticsearchにWikipediaの情報をインデクシングする

2022/12/01に公開

はじめに

この記事は ZOZO #2 Advent Calendar 2022 1日目の記事になります。

Elasticsearchに大量のデータを投入して、様々な検索処理を実験したいと思いました。
このような場合、よくWikipediaのデータが利用されますが、巷のWikipediaのデータをインデクシングする記事が古くて、最新の環境だと難航したので、今回まとめることにしました。

やること3行

  • Dockerで環境構築
  • Wikipediaのデータをダウンロード&整形
  • 整形したデータをElasticsearchにインデクシング

環境

  • M1 Mac
  • Docker & Compose v2
    • イメージ:Python 3.10

準備

Elasticsearchにインデクシングするためのデータを用意したり整形したりといった準備について以下では説明しています。

最終的なファイル構成

最終的なファイル構成は下記のようになっている。

$ tree .
.
|-- Dockerfile
|-- compose.yml
|-- jawiki-latest-pages-articles.xml.bz2
|-- mapping.json
|-- output
|   `-- AA
|       |-- wiki_00
|       |-- wiki_00_new.ndjson
|       |-- wiki_01
|       |-- wiki_01_new.ndjson
|       |-- ...
|       |-- wiki_42_new.ndjson
|       |-- wiki_43
|       `-- wiki_43_new.ndjson
`-- reformat_to_ndjson.py

Wikipediaのデータを用意

下記サイトから、Wikipediaの本文データをダウンロードします。
https://dumps.wikimedia.org/jawiki/

今回はwgetコマンドでダウンロードします

$ wget https://dumps.wikimedia.org/jawiki/latest/jawiki-latest-pages-articles.xml.bz2

環境構築

Dockerを使ってPython環境とWikipediaを整形するための環境を作ります。
なお今回は、Compose v2を使用しています。
https://matsuand.github.io/docs.docker.jp.onthefly/compose/cli-command/

FROM python:3.10

WORKDIR /app
RUN git clone --depth 1 https://github.com/zaemyung/wikiextractor.git
COPY jawiki-latest-pages-articles.xml.bz2 .
COPY reformat_to_ndjson.py .

Wikipediaのデータを整形するために、下記リポジトリをクローンしています。
https://github.com/zaemyung/wikiextractor

このリポジトリは下記リポジトリ(本家)をフォークしたものになります。
https://github.com/attardi/wikiextractor

本家を使わない理由としては、実行時エラーが大量発生するため、これらを解決したフォーク先のリポジトリを今回は使用しています。

compose.yml
services:
  tool:
    build: .
    volumes:
      - ./output:/app/output

コンテナを構築します。

$ docker compose build

以降は、構築したコンテナに入り作業を行うので下記コマンドを実行します。

# 構築したコンテナの中に入る
$ docker compose run --rm tool bash

データ整形

インデクシングできるようにダウンロードしたデータを整形していきます。

json形式に整形

Wikipediaの情報をjson形式に変換します。

$ python wikiextractor/WikiExtractor.py -o output -b 80M jawiki-latest-pages-articles.xml.bz2 --json

このあと、変換したデータを ElasticsearchのBulk API を使ってインデキシングしますが、このときBulk APIに渡せるデータ容量が最大で100MiBなので、小分けにする必要があります。

そのため、 -bオプションで80MiBを指定しています。
また、-oオプジョンは出力先フォルダを指定して、小分けにされたデータがoutputに出力されるようにしています。

コマンド実行後、完了まで数分~10分ほど待ちます。

処理が終わると、output/AA配下にwiki_xxのようなファイルが複数出力されていると思います。

{"id": "3577", "url": "https://ja.wikipedia.org/wiki?curid=3577", "title": "新京成電鉄新京成線", "text": "新京成電鉄新..."}
{"id": "3588", "url": "https://ja.wikipedia.org/wiki?curid=3588", "title": "総武本線", "text": "総武本..."}
.
.
.

Bulk API用にデータを整形

次に、このようなjson形式のデータをBulk APIに渡せるように下記のような形式に整形します。

{"index":{}}
{"id": "3577", "url": "https://ja.wikipedia.org/wiki?curid=3577", "title": "新京成電鉄新京成線", "text": "新京成電鉄新..."}
{"index":{}}
{"id": "3588", "url": "https://ja.wikipedia.org/wiki?curid=3588", "title": "総武本線", "text": "総武本..."}
.
.
.

スクリプトファイルとしてbashから実行できるように実行権限を渡します。

$ chmod +x reformat_to_ndjson.py

並列で整形処理を実行します。

$ ls ./output/AA/* -d | xargs -L 1 -P 10 bash -c './reformat_to_ndjson.py $0'

reformat_to_ndjson.pyでは、wiki_xxを読み込んで各行毎に{"index":{}}を追加する処理を書いています。

reformat_to_ndjson.py
#!/usr/bin/env python
# -*- coding: utf-8 -*-

import sys
import datetime

# 第一引数に処理するjsonファイルのパスを指定すること
filepath = sys.argv[1]
# ndjsonに変換したデータを入れる用の配列
ndjson = []

start_datetime = datetime.datetime.now()
print(f"{start_datetime.strftime('%Y/%m/%d %H:%M:%S')}: start converting {filepath}.")

# ファイルの各行を読み込んでBulk API用のactionを追加する
with open(filepath, encoding="utf-8") as f:
    for line in f:
        # 空行はスキップ
        if(line == '\n'):
            ndjson.append('\n')
            continue

        ndjson.append('{"index":{}}\n')
        ndjson.append(line)

# 作成したndjsonを新規ファイルに書き込む
new_filepath = filepath + "_new.ndjson"
with open(new_filepath, mode='w', encoding="utf-8") as f:
    f.writelines(ndjson)

finish_datetime = datetime.datetime.now()
processing_time = finish_datetime - start_datetime
print(f"{finish_datetime.strftime('%Y/%m/%d %H:%M:%S')}: {filepath} is converted to {new_filepath}({processing_time.seconds} sec).")

コマンド実行後、wiki_xx_new.ndjson./output/AA配下に保存されているはずです。

インデクシング

Mappingの作成

indexのMappingを作成します
index名はwikiとしています。

$ curl -H 'Content-Type: application/json' -X PUT 'http://127.0.0.1:9200/wiki?pretty' -d @mapping.json

Mapping情報はmapping.jsonに記載されたものを指定しています。

mapping.json
{
  "settings": {
    "number_of_shards": 1,
    "number_of_replicas": 0,
    "analysis": {
      "analyzer": {
        "ngram": {
          "filter": [
            "cjk_width",
            "lowercase"
          ],
          "char_filter": [
            "html_strip"
          ],
          "type": "custom",
          "tokenizer": "ngram"
        }
      },
      "tokenizer": {
        "ngram": {
          "token_chars": [
            "letter",
            "digit"
          ],
          "min_gram": "1",
          "type": "nGram",
          "max_gram": "2"
        }
      }
    }
  },
  "mappings": {
    "properties": {
      "text": {
        "analyzer": "ngram",
        "type": "text"
      },
      "url": {
        "type": "keyword"
      },
      "id": {
        "type": "keyword"
      },
      "title": {
        "analyzer": "ngram",
        "type": "text"
      }
    }
  }
}

インデクシング

下記コマンドで作成したindexに対してインデクシングします。

$ ls ./output/AA/*_new.ndjson -d | xargs -L 1 -P 3 bash -c 'echo $0 ; cat $0 | curl -s -X POST -H '\''Content-Type: application/x-ndjson'\'' '\''http://127.0.0.1:9200/wiki/_bulk?pretty'\'' --data-binary @-;'

結果

下記コマンドを叩くと、登録されたドキュメント数とサイズが返ってきます。

$ curl -X GET 'http://127.0.0.1:9200/wiki/_stats?pretty' -s | jq '{ count: .indices.wiki.primaries.docs.count, size: .indices.wiki.primaries.store.size_in_bytes }'
# 結果
{
  "count": 1352779,
  "size": 7142651721
}

検索

実際に検索してみると結果が返って来ることが確認できました。

# 検索
$ curl -s -XGET 'http://127.0.0.1:9200/wiki/_search?pretty' -d '{"query":{"match":{"text": "hello world"}},"size":1}' -H 'Content-Type: application/json'

# 結果
{
  "took" : 54,
  "timed_out" : false,
  "_shards" : {
    "total" : 1,
    "successful" : 1,
    "skipped" : 0,
    "failed" : 0
  },
  "hits" : {
    "total" : {
      "value" : 10000,
      "relation" : "gte"
    },
    "max_score" : 64.4458,
    "hits" : [
      {
        "_index" : "wiki",
        "_type" : "_doc",
        "_id" : "5d1veoQBbbruqLk5VZIW",
        "_score" : 64.4458,
        "_source" : {
          "id" : "1346091",
          "url" : "https://ja.wikipedia.org/wiki?curid=1346091",
          "title" : "CherryPy",
          "text" : "CherryPy\n\nCherryPy は、...中略...Hello World は ...中略...\n\n\n"
        }
      }
    ]
  }
}

おわりに

ElasticsearchにWikipediaのデータを投入する方法をまとめました。
参考になれば幸いです。

参考サイト

株式会社ZOZO

Discussion