ぽっぺんしゃんにょろりんこ

匿名・非追跡型アダルト動画検索エンジンの設計ノート

Pythonで作る形態素解析API:SudachiPyとFlaskで最速分割サーバー構築②

こんにちは、にょろりんこの備忘録的技術ブログです。

今回は、Pythonで作る形態素解析API構築シリーズの第2回 をお届けします。

前回の記事では、SudachiPyをFlaskと組み合わせ、POSTでテキストを送信すると名詞や動詞、形容詞などの基本トークンを返す最小構成API を作成しました。

www.n-rinko.com

これだけでも、検索クエリ生成やタグ付け処理の基盤として十分活用できますが、実際の運用では以下のような機能が欲しくなってきます。

  • 名詞だけでなく、複合語(例:癒し系彼女)や文節単位のフレーズも抽出したい
  • 名詞同士や英単語同士のバイグラム(2単語連結)を生成したい
  • 動詞・形容詞は原形(辞書形)に統一しておきたい
  • 類義語(シノニム)展開もAPI側で済ませたい

そこで今回は、これらの要望をすべて盛り込んだ最速分割サーバー②完全版コードを紹介します。

次章から、この改良版APIコードを処理の流れとともに丁寧に解説していきます。

実際のコード

まずはエンドポイントの設定

# APIエンドポイント
@app.route('/extract', methods=['POST'])
def extract():
    data = request.get_json()
    if not data or 'title' not in data:
        return jsonify({"error": "title が指定されていません"}), 400

この部分では、/extract というPOST用APIエンドポイント(APIのアクセス先)を定義しています。リクエストからJSONデータを取得し、title キーがなければ400エラーを返して処理を終了します。

タイトル(文字列)のクリーニング

text = data['title']
cleaned = re.sub(r'[。、・「」『』【】()]', '', text)

この部分では、リクエスJSONから title のテキストを取り出し、re.sub() を使って句読点や括弧など不要な記号を削除しています。これにより、形態素解析で余計なトークンが生成されるのを防いでいます。

SudachiPy Tokenizerの生成と解析

#Tokenizerを都度生成

tokenizer_obj = dictionary.Dictionary(dict="full").create()
mode = tokenizer.Tokenizer.SplitMode.C
tokens = tokenizer_obj.tokenize(cleaned, mode)

この部分では、SudachiPyのTokenizerオブジェクトを都度生成し、解析モードを SplitMode.C(最も細かく分割するモード)に設定して、先ほどクリーニングしたテキストを形態素解析しています。都度生成することで、スレッドセーフにAPIを運用できます。

ここまでで、クリーニングされたタイトル文字列を形態素に分解することができました。

名詞トークンの抽出

word_tokens =
for t in tokens:
    if t.part_of_speech()[0] == "名詞":
        noun = normalize_noun(t.surface())
        if is_valid_token(noun, "名詞"):
            word_tokens.append(noun)

この部分では、形態素解析で得られたトークンの中から品詞が「名詞」のものだけを抽出しています。normalize_noun() で名詞を正規化(末尾助詞の除去など)し、is_valid_token() で妥当性をチェックしてから word_tokens リストに追加しています。

ここまでで、テキストから有効な名詞トークンのリストを作ることができました。

英単語の抽出と名詞トークンへの追加

english_tokens = re.findall(r'\b[a-zA-Z]{3,}\b', text)
word_tokens.extend(english_tokens)

この部分では、元のテキストから3文字以上の英単語を正規表現で抽出しています。そして、その英単語リストを先ほどの名詞トークンリスト word_tokens に追加しています。

ここまでで、日本語の名詞トークンだけでなく、英単語も検索語句として扱えるようになりました。

SaePornsでは英語表現はMythomaxで日本語化されているため、あまり使わないコードですが、一般的なニュース記事解析や技術文書解析など、英単語をそのままキーワードとして活用したい場合には役立つ処理です。

例えば、プログラミング言語名や技術用語、ブランド名などは無理に翻訳せず英語のまま検索対象にした方が精度が高まるため、この処理を残しています。

動詞・形容詞の原形変換と抽出

normalized_forms =
for t in tokens:
    pos_major = t.part_of_speech()[0]
    if pos_major in {"動詞", "形容詞"}:
        base = convert_base(t.normalized_form())
        if base != t.surface() and is_valid_token(base, pos_major):
            normalized_forms.append(base)

この部分では、トークンの品詞が「動詞」または「形容詞」の場合、normalized_form() で辞書形(原形)に変換しています。さらに、convert_base() を通して「為る」を「する」に変換するなどの正規化を行い、元の表層形と異なり、かつ有効な単語であれば normalized_forms リストに追加しています。

ここまでで、動詞や形容詞を検索やタグ付けに使いやすい形(原形)に統一することができました。

文節分割関数の定義

def split_bunsetsu(tokens) -> list[list]:
    bunsetsu, current = ,
    for t in tokens:
        if t.part_of_speech()[0] in {"名詞", "動詞", "形容詞", "形状詞"}:
            if current:
                bunsetsu.append(current)
            current = [t]
        else:
            current.append(t)
    if current:
        bunsetsu.append(current)
    return bunsetsu

この部分では、形態素解析で得られたトークン列を文節単位に分割する split_bunsetsu() 関数を定義しています。

処理内容としては、トークンの品詞が「名詞」「動詞」「形容詞」「形状詞」のいずれかの場合に文節の切れ目とみなし、それまでの current を bunsetsu に追加して新しい文節を開始します。それ以外の品詞は現在の文節に追加されます。

一般的には、文節は自立語(名詞・動詞・形容詞・形容動詞)で始まりますが、SudachiPyの環境では「形容動詞」が「形状詞」という名前で扱われているため、ここでも 形状詞 を指定しています。

ここまでで、形態素トークンのリストを文節単位で分割する準備が整いました。

文節フレーズの生成

bunsetsu_phrases =
for seg in split_bunsetsu(tokens):
    phrase = ''.join(t.surface() for t in seg)
    if len(phrase) > 1 and is_valid(phrase):
        bunsetsu_phrases.append(phrase)

この部分では、先ほど定義した split_bunsetsu() 関数を使ってトークン列を文節単位に分割し、それぞれの文節内のトークンを結合してフレーズを作成しています。

生成したフレーズは、2文字以上で is_valid() による妥当性チェックを通過したものだけを bunsetsu_phrases リストに追加しています。

ここまでで、解析結果から文節単位の自然なフレーズ候補を取得できるようになりました。具体的には、例えば「制服の少女が微笑んでいる」というタイトルの場合、「制服」「少女」「微笑んで」「いる」といった文節に区切られたフレーズが抽出できます。

オフィスでくつろぐティアラ
名詞+助詞+名詞の複合語生成

compound_phrases =
for i in range(len(tokens) - 2):
    t1, t2, t3 = tokens[i], tokens[i+1], tokens[i+2]
    if (t1.part_of_speech()[0] == "名詞" and
        t2.surface() in {"の", "な"} and
        t3.part_of_speech()[0] == "名詞"):
        phrase = t1.surface() + t2.surface() + t3.surface()
        if is_valid(phrase):
            compound_phrases.append(phrase)

この部分では、形態素解析結果から 「名詞 + の/な + 名詞」 のパターンを検出し、複合語として compound_phrases に追加しています。

例えば、「制服の女性」「素敵な彼女」「夜の誘惑」といった日本語特有の名詞連結表現を複合語として抽出できます。

ここまでで、単純な単語抽出だけでなく、文中で意味のある名詞複合語もタグ候補として取得できるようになりました。

名詞トークン同士のバイグラム生成

noun_bigrams =
for i in range(len(word_tokens)):
    for j in range(i + 1, len(word_tokens)):
        for a, b in [(word_tokens[i], word_tokens[j]), (word_tokens[j], word_tokens[i])]:
            big = f"{a} {b}"
            if is_valid(big):
                noun_bigrams.append(big)

この部分では、名詞トークンリスト word_tokens の中から 2つの名詞を組み合わせたバイグラム(2単語連結) を生成しています。

特徴として、トークンのペアを順番を変えて両方向(a b と b a)で生成しis_valid() による妥当性チェックを通過した組み合わせだけを noun_bigrams に追加するというフローで行っています。

例えば、名詞トークンに「女子校生」「誘惑」「放課後」が含まれていれば、

「女子校生 誘惑」

「誘惑 女子校生」

「女子校生 放課後」

「放課後 女子校生」

「誘惑 放課後」

「放課後 誘惑」

といったバイグラムが生成され、検索タグや関連語句として活用できます。

ここまでで、名詞同士を組み合わせた検索ワード候補を作成できるようになりました。

英単語同士のバイグラム生成

english_bigrams =
for i in range(len(english_tokens)):
    for j in range(i + 1, len(english_tokens)):
        big = f"{english_tokens[i]} {english_tokens[j]}"
        if is_valid(big):
            english_bigrams.append(big)

この部分では、抽出した英単語リスト english_tokens の中から 2つの英単語を組み合わせたバイグラム(2単語連結) を生成しています。

特徴として、トークンのペアを順方向(i → j)のみで生成is_valid() による妥当性チェックを通過した組み合わせだけを english_bigrams に追加するということをしています。

例えば、英単語リストに「sexy」「girl」「teacher」が含まれていれば、

「sexy girl」

「sexy teacher」

「girl teacher」

といった英語バイグラムが生成されます。

ここまでで、英単語同士を組み合わせた検索ワード候補も作成できるようになりました。

名詞トークンの類義語(シノニム)展開

synonyms = set()
for w in word_tokens:
    for syn in get_synonyms(w):
        synonyms.add(syn)

この部分では、名詞トークンリスト word_tokens に含まれる各単語について、get_synonyms() 関数を使って Word2Vecモデルから類義語(シノニム)候補を取得 しています。

特徴として、各単語に対して get_synonyms(w) を呼び出し、得られた類義語を synonyms セットに追加。セットを使うことで、重複を自動的に排除しています。

例えば、「女子大生」に対して「JD」といった類義語がWord2Vecモデルから取得されれば、これらも検索タグや関連語句として活用可能になります。

ここまでで、名詞トークンに類義語を加え、より幅広い検索語句候補を生成できるようになりました。

バイグラムの類義語置換展開

bigram_synonyms = set()
for bg in noun_bigrams + english_bigrams:
    parts = bg.split()
    mapped = [get_synonyms(p)[0] if get_synonyms(p) else p for p in parts]
    new_bg = ' '.join(mapped)
    if new_bg != bg and is_valid(new_bg):
        bigram_synonyms.add(new_bg)

この部分では、生成済みの 名詞バイグラム noun_bigrams と英単語バイグラム english_bigrams に対して、それぞれの単語を類義語に置き換えた新しいバイグラムを生成しています。

特徴として、各バイグラムをスペースで分割して2単語に分ける。各単語について get_synonyms() で類義語を取得し、あれば最初の類義語に置換。元のバイグラムと異なり、かつ is_valid() で妥当性がある場合だけ bigram_synonyms に追加ということをしています。

例えば、バイグラムが「女子大生 誘惑」だった場合、「女子校生」の類義語が「JD」、「誘惑」の類義語が「誘惑行為」であれば、「JD 誘惑」「女子大生 誘惑行為」などの新しいバイグラムが生成されます。

ここまでで、既存バイグラムに類義語置換を加えた、多様な検索語句候補を生成できるようになりました。

最終結果の統合とAPIレスポンス生成

final = set()
final.update(word_tokens)
final.update(normalized_forms)
final.update(bunsetsu_phrases)
final.update(compound_phrases)
final.update(noun_bigrams)
final.update(english_bigrams)
final.update(synonyms)
final.update(bigram_synonyms)
final.add(text)

output = sorted({ normalize(w) for w in final if is_valid(w) })

return jsonify({"words": output})

この部分では、これまでに生成してきた各種トークンやフレーズをすべて final というセットに統合しています

具体的には、「名詞トークン」「動詞・形容詞の原形トークン」「文節フレーズ」「名詞複合語」「名詞バイグラム」「英単語バイグラム」「類義語」「バイグラム類義語」「元テキスト」これらすべてをセットに追加し、重複を排除します。

その後、「normalize() 」で正規化、「is_valid()」 で最終的な妥当性チェックを行い、ソートして output リストを作成し、最終的には「{"words": [...]}」というJSON形式でレスポンスを返却しています。

まとめ

ここまでで、クライアントから送られたテキストに対し、形態素解析から類義語展開まで含めた検索タグや関連語句候補の完全セットを一括で返すAPIが完成しました。

検索エンジンの快適さは、いかに早く、そしてユーザーが求めるワードを網羅的に拾えるか、にかかっています。だから、SAEPORNSでは今回紹介したような形態素解析APIと、事前に生成したインデックスを組み合わせて、高速かつ関連性の高い検索を実現しています。

もちろん、ここの部分はまだまだ改善余地があるので、今後もアルゴリズムの精度向上やインデックス構造の最適化を進めていきます。

このコードが実際に使われている「あなたを追跡しないアダルト動画検索エンジンSaePorns」はこちら!※18歳未満の方はご利用いただけません。

sae-porns.org

それではみなさんよい開発ライフを