Pythonで簡単な検索システムを自作してみた~Phase1~


作ったもの

ユーザーが入力した文章を形態素解析し、データの中からユーザーの興味に合ったものを検索するシステムをPythonで実装しました。

用意するもの

ユーザーが入力した文章の例をtextという変数に格納し、検索するデータをdata_listという二次元配列に格納します。

text = "どうして空は青いのかがが気になりました"
text1 = "光の三原色について知りたいです"
text2 = "葉っぱが好き‼‼"
data_list = [["空の色が青い理由", "空の色が青い理由を解き明かします‼‼"],
             ["曇り空を見てみよう", "いろいろな形をした雲を紹介します"],
             ["赤・青・緑 - 光の三原色", "光の三原色とはなんでしょう"],
             ["葉っぱが緑色なのはなぜ?", "空が青い理由や葉っぱが緑色の理由、リンゴが赤く見える理由などを解説します"],
             ["なぜ空は青いのか?", "青い光は波長が短く、赤い光は波長が長いので、青い光ほど散乱される量が多いのです"],
             ["帰り道の空が気になる豆知識", "毎日の帰り道の空が気になる豆知識を紹介します。"]]

形態素解析

textをjanomeという形態素解析器を使ってtextから重要な情報だけを抜き出すため、名詞と形容詞を抽出します。

from janome.tokenizer import Tokenizer
from janome.analyzer import Analyzer
from janome.charfilter import *
from janome.tokenfilter import *

# あとで使うモジュール
import itertools
import numpy as np

# 要素を形態素解析し、名詞と形容詞をリストにまとめる
word_list = []

text = "どうして空は青いのかがが気になりました"
text1 = "光の三原色について知りたいです"
text2 = "葉っぱが好き‼‼"

char_filters = [UnicodeNormalizeCharFilter()]
tokenizer = Tokenizer()
token_filters = [POSKeepFilter(['名詞', '形容詞'])]
analyzer = Analyzer(char_filters=char_filters,
                    tokenizer=tokenizer, token_filters=token_filters)

for token in analyzer.analyze(text):
    word_list.append(token.surface)

print(word_list)

これを実行すると、次のような結果が得られます。

['空', '青い', 'の', '気']

textから名詞と形容詞を抜き出せました‼‼
text1とtext2に関しても同じような結果を得ることが出来ます。

検索システム

次に、先ほど抜き出したtext内の単語とdata_list内のデータを突き合わせることで、より良く一致するデータを抽出して検索結果として表示します。

そのためのアルゴリズム(?)的なものを以下にまとめました。

data_listが二次元配列になっていて面倒くさいので、リスト内のリストのn番目の要素を取り出すことで二つに分割します。今回の場合は

["空の色が青い理由", "曇り空を見てみよう", "赤・青・緑 - 光の三原色", "葉っぱが緑色なのはなぜ?", "なぜ空は青いのか?", "帰り道の空が気になる豆知識"]

["空の色が青い理由を解き明かします‼‼", "いろいろな形をした雲を紹介します", "光の三原色とはなんでしょう", "空が青い理由や葉っぱが緑色の理由、リンゴが赤く見える理由などを解説します", "青い光は波長が短く、赤い光は波長が長いので、青い光ほど散乱される量が多いのです", "毎日の帰り道の空が気になる豆知識を紹介します。"]

の二つに分割するということです。そして②~⑦のコードをfor文で2回繰り返し、上のデータをそれぞれぶち込むことで、タイトルとキャプションの両方から一致するデータを選び取ることが出来ます。

以下がそのコードです。

for i in range(2):
    # data_listのi番目の要素を加工したリスト化する
    process_data_list = [data[i] for data in data_list]

次にprocess_data_list内の文字列からword_list内の単語が含まれるものを抜き出します。

# word_list内の単語とdata_listの文字列を突き合わせて、共通するものを単語ごとに書きだす
for i in range(2):
    # data_listのi番目の要素を加工したリスト化する
    process_data_list = [data[i] for data in data_list]
    match_list = []
    for word in word_list:
        match_list.append([s for s in process_data_list if word in s])
    print(match_list)

これを実行すると

[['空の色が青い理由', '曇り空を見てみよう', 'なぜ空は青いのか?', '帰り道の空が気になる豆知識'], ['空の色が青い理由', 'なぜ空は青いのか?'], ['空の色が青い理由', '赤・青・緑 - 光の三原色', '葉っぱが緑色なのはなぜ?', 'なぜ空は青いのか?', '帰り道の空が気になる豆知識'], ['帰り道の空が気になる豆知識']]
[['空の色が青い理由を解き明かします‼‼', '空が青い理由や葉っぱが緑色の理由、リンゴが赤く見える理由などを解説します', '毎日の帰り道の空が気になる豆知識を紹介します。'], ['空の色が青い理由を解き明かします‼‼', '空が青い理由や葉っぱが緑色の理由、リンゴが赤く見える理由などを解説します', '青い光は波長が短く、赤い光は波長が長いので、青い光ほど散乱される量が多いのです'], ['空の色が青い理由を解き明かします‼‼', '光の三原色とはなんでしょう', '空が青い理由や葉っぱが緑色の理由、リンゴが赤く見える理由などを解説します', '青い光は波長が短く、赤い光は波長が長いので、青い光ほど散乱される量が多いのです', '毎日の帰り道の空が気になる豆知識を紹介します。'], ['毎日の帰り道の空が気になる豆知識を紹介します。']]

タイトルとキャプションのデータそれぞれにおいてtextから抽出した単語と一致するものが抜き出せました。

そして、より多くの単語に一致している文字列を探すため、単語に紐づいて抽出された配列を総当たりで比べ、共通する要素があればfrequencyリストに格納します。

# word_list内の単語とdata_listの文字列を突き合わせて、共通するものを単語ごとに書きだす
for i in range(2):
    # data_listのi番目の要素を加工したリスト化する
    process_data_list = [data[i] for data in data_list]
    match_list = []
    for word in word_list:
        match_list.append([s for s in process_data_list if word in s])
    # print(match_list)

# 単語ごとに紐づけられた文字列を4C2で総当たりで比べ、共通する単語がある場合はそれをfrequencyに格納する
    frequency = []
    for el in itertools.combinations(match_list, 2):
        common = set(el[0]) & set(el[1])
        if common:
            frequency.append(list(common))
    print(frequency)

これを実行すると

[['空の色が青い理由', 'なぜ空は青いのか?'], ['帰り道の空が気になる豆知識', '空の色が青い理由', 'なぜ空は青いのか?'], ['帰り道の空が気になる豆知識'], ['空の色が青い理由', 'なぜ空は青いのか?'], ['帰り道の空が気になる豆知識']]
[['空の色が青い理由を解き明かします‼‼', '空が青い理由や葉っぱが緑色の理由、リンゴが赤く見える理由などを解説します'], ['空の色が青い理由を解き明かします‼‼', '毎日の帰り道の空が気になる豆知識を紹介します。', '空が青い理由や葉っぱが緑色の理由、リンゴが赤く見える理由などを解説します'], ['毎日の帰り道の空が気になる豆知識を紹介します。'], ['空の色が青い理由を解き明かします‼‼', '空が青い理由や葉っぱが緑色の理由、リンゴが赤く見える理由などを解説します', '青い光は波長が短く、赤い光は波長が長いので、青い光ほど散乱される量が多いのです'], ['毎日の帰り道の空が気になる豆知識を紹介します。']]

textから抜き出した要素と良く一致する文字列が数多く抜き出されました‼

いよいよ大詰めです。frequencyリストを一次元配列に直し、process_data_list内の要素がいくつ含まれているのかを数えます。そのために「for文の外で」要素の出現頻度を記録する二次元配列を定義します。

counter = [[], []]

二つのprocess_data_list内の、要素がいくつ入っているかを記録したcounterが二つできたら、それを足し合わせて一つの配列にします。この配列の数値がdata_list内のデータとtextの一致度と捉えることが出来ます。

# word_list内の単語とdata_listの文字列を突き合わせて、共通するものを単語ごとに書きだす
for i in range(2):
    # data_listのi番目の要素を加工したリスト化する
    process_data_list = [data[i] for data in data_list]
    match_list = []
    for word in word_list:
        match_list.append([s for s in process_data_list if word in s])
    # print(match_list)

# 単語ごとに紐づけられた文字列を4C2で総当たりで比べ、共通する単語がある場合はそれをfrequencyに格納する
    frequency = []
    for el in itertools.combinations(match_list, 2):
        common = set(el[0]) & set(el[1])
        if common:
            frequency.append(list(common))
    # print(frequency)

# frequencyの中にどの文字列がどれくらい出てきたのかを数えて、counterに格納する。
    for count in process_data_list:
        counter[i].append(
            list(itertools.chain.from_iterable(frequency)).count(count))
# タイトルと説明文の一致点数をかける
sum_count = list(map(lambda x, y: x + y, counter[0], counter[1]))
print(sum_count)

これを実行すると

[6, 0, 0, 3, 4, 6]

最後にこの配列の第三四分位数と第二四分位数を求め、第三四分位数以上のデータを検索結果、第二四分位数以上のデータを関連情報として表示します。

for i in range(2):
    # data_listのi番目の要素を加工したリスト化する
    process_data_list = [data[i] for data in data_list]
    match_list = []
    for word in word_list:
        match_list.append([s for s in process_data_list if word in s])
    print(match_list)

# 単語ごとに紐づけられた文字列を4C2で総当たりで比べ、共通する単語がある場合はそれをfrequencyに格納する
    frequency = []
    for el in itertools.combinations(match_list, 2):
        common = set(el[0]) & set(el[1])
        if common:
            frequency.append(list(common))
    # print(frequency)

# frequencyの中にどの文字列がどれくらい出てきたのかを数えて、counterに格納する。
    for count in process_data_list:
        counter[i].append(
            list(itertools.chain.from_iterable(frequency)).count(count))
# タイトルと説明文の一致点数をかける
sum_count = list(map(lambda x, y: x + y, counter[0], counter[1]))
# print(sum_count)


# counterの上位25%を取り出す
criterion = np.percentile(sum_count, 75)
relation = np.percentile(sum_count, 50)
# print(criterion)
# print(relation)


# data_list内の単語の出現回数が上位25%の場合出力する
process_data_list = [data[0] for data in data_list]
for i, results in enumerate(process_data_list):
    if sum_count[i] > criterion:
        print("検索結果:" + results)
    elif sum_count[i] > relation:
        print("関連情報:" + results)

実行結果は...

検索結果:空の色が青い理由
関連情報:なぜ空は青いのか?
検索結果:帰り道の空が気になる豆知識

でした‼‼‼‼

全コード

from janome.tokenizer import Tokenizer
from janome.analyzer import Analyzer
from janome.charfilter import *
from janome.tokenfilter import *
import itertools
import numpy as np

# 要素を形態素解析し、名詞と形容詞をリストにまとめる
word_list = []
text = "どうして空は青いのかがが気になりました"
text1 = "光の三原色について知りたいです"
text2 = "葉っぱが好き‼‼"
char_filters = [UnicodeNormalizeCharFilter()]
tokenizer = Tokenizer()
token_filters = [POSKeepFilter(['名詞', '形容詞'])]
analyzer = Analyzer(char_filters=char_filters,
                    tokenizer=tokenizer, token_filters=token_filters)
for token in analyzer.analyze(text):
    word_list.append(token.surface)

data_list = [["空の色が青い理由", "空の色が青い理由を解き明かします‼‼"],
             ["曇り空を見てみよう", "いろいろな形をした雲を紹介します"],
             ["赤・青・緑 - 光の三原色", "光の三原色とはなんでしょう"],
             ["葉っぱが緑色なのはなぜ?", "空が青い理由や葉っぱが緑色の理由、リンゴが赤く見える理由などを解説します"],
             ["なぜ空は青いのか?", "青い光は波長が短く、赤い光は波長が長いので、青い光ほど散乱される量が多いのです"],
             ["帰り道の空が気になる豆知識", "毎日の帰り道の空が気になる豆知識を紹介します。"]]

counter = [[], []]

# word_list内の単語とdata_listの文字列を突き合わせて、共通するものを単語ごとに書きだす
for i in range(2):
    # data_listのi番目の要素を加工したリスト化する
    process_data_list = [data[i] for data in data_list]
    match_list = []
    for word in word_list:
        match_list.append([s for s in process_data_list if word in s])
    print(match_list)

# 単語ごとに紐づけられた文字列を4C2で総当たりで比べ、共通する単語がある場合はそれをfrequencyに格納する
    frequency = []
    for el in itertools.combinations(match_list, 2):
        common = set(el[0]) & set(el[1])
        if common:
            frequency.append(list(common))
    # print(frequency)

# frequencyの中にどの文字列がどれくらい出てきたのかを数えて、counterに格納する。
    for count in process_data_list:
        counter[i].append(
            list(itertools.chain.from_iterable(frequency)).count(count))
# タイトルと説明文の一致点数をかける
sum_count = list(map(lambda x, y: x + y, counter[0], counter[1]))
# print(sum_count)


# counterの上位25%を取り出す
criterion = np.percentile(sum_count, 75)
relation = np.percentile(sum_count, 50)
# print(criterion)
# print(relation)


# data_list内の単語の出現回数が上位25%の場合出力する
process_data_list = [data[0] for data in data_list]
for i, results in enumerate(process_data_list):
    if sum_count[i] > criterion:
        print("検索結果:" + results)
    elif sum_count[i] > relation:
        print("関連情報:" + results)