2018年9月2日

ElasticsearchでWikipediaの全文検索にチャレンジ (4)

前置き


前回の記事の続きです。
間に記事を挟んだことから類推可能な通り、今回はPythonを採用して検索しております。
動作させるための利用環境は該当記事を参考にしてください。
なお、Elasticsearchについては、これまでの記事(123)の手順に沿ってWikipediaのデータを投入した環境という前提となります。

取得コード


今回は環境構築の記事だけでお腹いっぱいになったので、内容は余り触れずコードと結果をみてみたいと思います。余り大したことしてないですし。

# ライブラリ登録
import MeCab, json, codecs, re, inspect
import pandas as pd
from datetime import datetime
from elasticsearch import Elasticsearch

# 初期設定
ESServer = ["http://localhost:9200"]
es = Elasticsearch(ESServer)
mt = MeCab.Tagger("-Ochasen")
mt.parse('') # MeCab初期化 (不具合回避)

# 関数定義

# 形態素解析し頻度表を作成
# 解析対象文章をリストで渡す。
def get_freq(doclist) : 
    tl = [] # 辞書初期化
    for doc in doclist : # 文書リストを順次処理
        node = mt.parseToNode( doc["text"] )
        while node : # 解析したTermごとに順次処理
            # print(node.surface + " => " + node.feature)
            arr = node.feature.split(",") # Mecabフォーマットを配列(リスト)化
            if arr[0] != "BOS/EOS" : # 開始、終了識別は除外
                if len(arr) > 7 : # 振り仮名が出力されない単語もある
                    Phonetic = arr[7]
                else :
                    Phonetic = "*"
                record = {"Term": node.surface, "Info1": arr[0], 
                    "Info2": arr[1], "Phonetic": Phonetic, "Freq": 1 }
                tl.append(record) # 重複ありで辞書へ投入

            node = node.next

    # 集計処理用に一旦DataFrameに変換
    df = pd.io.json.json_normalize(tl) # 辞書をDataFrameへ変換

    # 以下のパターンはどちらがよいか判断しかねたので両方作成。関数に引数指定させてもいいが、シンプルに一方だけにしたい。
    Morptype = "typeB" # 集計方法のパターン指定
    if Morptype == "typeA" : # 検出時の品詞、かなをそのまま利用
        # Groupby で頻度集計
        grp = df.groupby(['Term','Info1','Info2','Phonetic'], as_index=False) # 列はindex化させない
        res = grp.count()
        # print(res) # for Debug

        # 辞書型に再変換
        tl = res.to_dict(orient='records')
    elif Morptype == "typeB" : # 検出時のsurfaceを再問合わせして品詞を設定
        # Groupby で頻度集計
        grp = df[["Term", "Freq"]].groupby(['Term'], as_index=False) # 列はindex化させない
        res = grp.count()
        # print(res) # for Debug

        # 辞書型に再変換
        tl = res.to_dict(orient='records')
        # MeCabに品詞を再問合わせ
        for row in tl :
            node = mt.parseToNode( row["Term"] )
            while node : # 解析したTermごとに順次処理
                # print(node.surface + " => " + node.feature)
                arr = node.feature.split(",") # Mecabフォーマットを配列(リスト)化
                if arr[0] != "BOS/EOS" : # 開始、終了識別は除外
                    if len(arr) > 7 : # 振り仮名が出力されない単語もある
                        Phonetic = arr[7]
                    else :
                        Phonetic = "*"
                    row["Info1"] = arr[0]
                    row["Info2"] = arr[1]
                    row["Phonetic"] = Phonetic

                node = node.next

    return tl
    # end get-freq()

# メイン処理開始 #################################
key = "ブルータス"
inx = "wikipedia"
body = {"query": {"match": {"text": key}}, "size": 200}
doclist = [] # 辞書初期化(ドキュメントのテーブルとして利用)
res = es.search(index=inx, body=body) # Elasticsearchへクエリーを投入

for doc in res['hits']['hits']:
    row = { "id":doc['_id'] ,"title" :doc["_source"]["title"], 
        "text" :doc["_source"]["text"], "timestamp":doc["_source"]["timestamp"] } 
    doclist.append( row ) # id, title, text, timestamp を1行としてリストに格納

# ElasticsearchにWiki記法ごと入れているため暫定的に処理している。本来は投入前に整形すべき。
for doc in doclist : # 文書リストを順次処理
    doc["text"] = re.sub(r"[\[\]\{\}\*\"\'\|]+", "", doc["text"]) # 本文からWiki記法削除処理

# 解析対象データをファイルに保存
with codecs.open("doclist.json", "w", "utf-8") as f:
    json.dump(doclist, f, indent=4, ensure_ascii=False) 

tl = get_freq(doclist) # 形態素解析を行い頻度表を作成

# 解析後のデータをファイルに保存
with codecs.open("termlist.json", "w", "utf-8") as f:
    json.dump(tl, f, indent=4, ensure_ascii=False)

df = pd.io.json.json_normalize(tl) # 辞書をDataFrameへ変換

# 品詞を名詞に絞り、集計順に並べる
# ※キーワード以外でキーワードよりも極端に多い単語は処理誤り等の可能性が高い
#  係数を3倍に設定し、除外するものとする
#  ただし、固有名詞と判定されたものは取り除かない。
coef = 3
df = df[df['Info1'] == '名詞'] # 名詞だけに絞り込む
KeyFreq = df[df["Term"]==key]['Freq'].iat[0] # 検索キーワードの件数を取得
# print(KeyFreq) # for Debug
# 大量キーワードの除外処理
df = df[ ~( (df['Freq'] > (KeyFreq * coef)) & ~(df['Info2'] == '固有名詞') ) ] 

df = df.sort_values(by='Freq', ascending=False) # 降順ソート

tl = df.to_dict(orient='records') # 辞書型に再変換

with codecs.open("filtertermlist.json", "w", "utf-8") as f:
    json.dump(tl, f, indent=4, ensure_ascii=False)

print( "end : " + datetime.now().strftime("%Y/%m/%d %H:%M:%S") )

120行ちょっとのコードなので適当にデバッグコードを挟みながら動作を確認いただけるといいかなと思います。
頻度表の作成についてはまだ方式を決めかねたため、2パターンのコードが併存しているいけてないコードです。
※公式によるとMeCabは未知の品詞を自動判定させるようなので。

最終的にはきれいにしたものを正式公開したいですね。
その際、ライセンスについてはこのブログについてに書いてある通りですが、Apache2.0でgithub公開にしてもいいかもですね。夢が広がります。

結果


先のコードは指定したキーワード「ブルータス」で検索を掛けて、形態素解析にて単語に分解し、Mecabで品詞を特定した上で、ブルータスに関連しそうな名詞を拾い上げるという処理です。
保存されるファイルの内容は以下の通りです。

  • doclist.json:Elasticsearchから取得した200件の記事内容
  • termlist.json:MeCabで形態素解析を行った結果(頻度表)
  • filtertermlist.json:頻度表から不要と思われる情報を除外した結果

それぞれの情報は各々で実行してご確認ください。以下は結果の一部抜粋です。

    {
        "Freq": 267,
        "Info1": "名詞",
        "Info2": "固有名詞",
        "Phonetic": "ローマ",
        "Term": "ローマ"
    },
    {
        "Freq": 248,
        "Info1": "名詞",
        "Info2": "固有名詞",
        "Phonetic": "*",
        "Term": "ブルータス"
    },
    {
        "Freq": 72,
        "Info1": "名詞",
        "Info2": "固有名詞",
        "Phonetic": "*",
        "Term": "ブルトゥス"
    },
    {
        "Freq": 196,
        "Info1": "名詞",
        "Info2": "固有名詞",
        "Phonetic": "シェイクスピア",
        "Term": "シェイクスピア"
    },
    {
        "Freq": 158,
        "Info1": "名詞",
        "Info2": "固有名詞",
        "Phonetic": "*",
        "Term": "ジュリアス・シーザー"
    },
    {
        "Freq": 84,
        "Info1": "名詞",
        "Info2": "固有名詞",
        "Phonetic": "*",
        "Term": "カエサル"
    },
    {
        "Freq": 25,
        "Info1": "名詞",
        "Info2": "固有名詞",
        "Phonetic": "*",
        "Term": "ガイウス・ユリウス・カエサル"
    },

キーワード「ブルータス」を基準に確認をしていきましょう。

ローマの人なのでローマのカウント率が高いですね。
英語読みのブルータスから、ラテン語のブルトゥスを拾うことができています。
シェイクスピアの戯曲で有名なので、シェイクスピアが上位に来ていますね。
戯曲名はジュリアス・シーザーでこれも上位に来ています。
というか、カエサルの英語読みなので、上位に来て当然ですが。
ブルータスはカエサルの暗殺に関わり、カエサルから「ブルータスよ、お前もか」と言われた人物として有名なので、カエサルは上位にランクインします。

実際にWikipediaの本人のページを検索するとカエサルが46件ヒットします。

どうやら、それなりに関連性の高い単語を拾えていそうです。

なお、77行目で指定しているキーワードを「カエサル」に変えて同じことをしてみましたが、ブルータス、ブルトゥスともに比率は低めでした。クレオパトラのほうがまだ高かったですね。
あとは戦争、戦記、内戦など、戦いに明け暮れた人なのかと思わせるキーワードが上位でした。
エンジニア的にはシーザー暗号から「暗号」というキーワードに期待したのですが、2件でした。先頭から200件の記事しか参照していないので仕方がないかも知れませんが。

この関わりの深い単語を拾う行為を共起というそうです。せっかくなので次回はこの記事にある他の方式を試したい。 https://qiita.com/nishina555/items/c650dd06b283996210ac

0 件のコメント:

コメントを投稿

TIPS:VSCodeで日本語化がうまくいかないとき

前置き Visual Studio Codeで拡張機能「 Japanese Language Pack for Visual Studio Code 」を入れたら日本語になりますよね。 でも、「 Remote Development 」で色々な環境を日本語化してると、偶に...