KWに"関係"した最新Qiita記事を返すAPIをつくりたい（Ver.0）

お恥ずかしながら、[Qiita API](https://qiita.com/api/v2/docs)の存在をこの夏に知った私ですが、
今回は[Qiitaの夏休み企画](https://qiita.com/official-events/6d31965c499a69377c0b)に乗っかって、検索キーワードに"関連"したQiita記事を抽出するWEB APIをつくってみました。

お断り）
初学者が短時間で、見様見真似で、設計・開発したAPIです。
ぜひ、温かい目で読んでいただきながら、よろしければアドバイスお願いします！

## モチベーション

大きなモチベーションは次の通りです。

- SlackでQiitaの新しい記事を確認したい
    - 忙しいときは自分からQiitaを開くのが億劫
    - Qiita側から”自身の専門に関連する”記事を推薦してほしい
- 既存のQiita APIでは、"キーワードを含む"で記事の検索が可能
    - つまり、キーワードの完全一致で検索できる
    - しかし「機械学習」で検索したとき、「教師あり学習」や「教師なし学習」が入った記事もほしい！

そこで、自然言語処理（NLP）のアプローチで
1. Qiitaの記事タイトルと検索KWを分散表現ベクトル（単語ベクトル）に変換
1. 検索KWベクトルの近傍 $k = 10$ 件の記事を返す

という、機能としてはシンプルなAPIを開発しました。

## 構成

![モデル.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2529987/096b7410-a741-4fe9-01a5-1a3fc25c3e1c.png)

今回作成したサービス（qiita4youと呼びます）は、ざっくりと次の3構成になっています。

- 埋め込み抽出：与えられた単語を埋め込みベクトルに変換して返します
- 記事ベクトル変換：Qiita APIを通して、記事の取得とその埋め込みベクトルを保存します
- 類似記事抽出：クライアントから与えられた単語について、類似記事10件を返します

今回はDockerコンテナを用いて構築しました。すべてのソースコードはGitHubリポジトリにあります。

https://github.com/KaiSugahara/qiita4you

ここからは、詳細を少しだけ説明していきたいと思います。

### 埋め込み抽出

```python: app-fasttext/index.py
# FLASK
from flask import Flask, jsonify, request
app = Flask(__name__)

# fastText
import fasttext
embed_model = fasttext.load_model('/data/jawiki_fasttext.bin')

@app.route("/", methods=["POST"])
def index():
    words = request.json
    word_embeddings = {w: embed_model.get_word_vector(w).tolist() for w in words}
    return jsonify(word_embeddings)

if __name__ == "__main__":
    app.run(host="0.0.0.0", port=80)
```

与えられた単語のリスト $words$ を埋め込みベクトルに変換して、返すだけのプライベートAPIです。
（たまたま私の手元にあった）Wikipediaコーパスで学習済みのfastTextモデル`jawiki_fasttext.bin`を使用します。

補足）
結果を見ていくと、上手く単語ベクトルが生成できていない気がします。
Qiitaで扱われているドメインに、Wikipediaの（広い）ドメインが上手くマッチしていない可能性があるので、これは今後の課題です。

### 記事ベクトル変換

ソースコードは長すぎるので割愛します...

https://github.com/KaiSugahara/qiita4you/blob/master/app-get-items/index.py

Qiita記事の取得 → 記事ベクトルの抽出 を（およそ）30分おきに定期実行するコンテナです。

- Qiita記事の取得
    - Qiita APIを叩いて、過去3日間の記事ID / タイトル / URLを保存
- 記事ベクトルの抽出
    - MeCabを用いて、記事タイトルから名詞のみ抽出
    - 名詞をベクトルに変換し、重心（平均）をとる → 記事ベクトルとする
    - 記事ベクトルを保存

補足）
単純に重心をとれば良いか、というのも微妙です。今後検証する必要あり。

### 類似記事抽出

```python: app-search-items/index.py
...

@app.route("/", methods=["GET"])
def index():

    df_attribute = pd.read_csv("/data/data_attribute.csv", index_col=0)
    df_embedding = pd.read_csv("/data/data_embedding.csv", index_col=0)

    target_word = request.args.get("s", "")

    # Check
    if target_word == "":
        return jsonify({"message": "パラメータsが指定されていません。"}), 400

    target_embedding = requests.post('http://app-fasttext:80/', json=[target_word])
    target_embedding = np.array(list(target_embedding.json().values()))
    target_embedding = target_embedding.mean(axis=0).reshape(1, -1)

    cos_sim = cosine_similarity(target_embedding, df_embedding.to_numpy())[0]
    TopK = pd.Series(cos_sim).sort_values(ascending=False).head(10)

    return jsonify(list(df_attribute.iloc[TopK.index].T.to_dict().values())), 200

...
```

GETリクエストで受け取った $s$ をもとに、関連記事10件のタイトルとURLを返すパブリックAPIです。

処理としては
1. 過去3日間の記事ベクトルを読み込み（df_embedding）
1. キーワード $s$ を単語ベクトル target_embedding に変換
1. 記事とキーワードのコサイン類似度を計算
    - それほど記事数は多くないのでコサイン類似度を選定しました
1. 類似度上位10件の記事タイトル / URLを返す

だけです。

## 結果を見てみる

~~`qiita4you`（仮）という名前でAPIを公開しています。
（学生でも契約できる貧乏マシンに載せているので、あくまでデモ的な位置付けです）~~

2023/05/22 追記）お金の事情により、現在停止しています。

https://hogehoge/

まずキーワード「機械学習」で叩いてみます。（2022/09/08 20:49）
https://hogehoge/?s=機械学習

```
[
    {"title":"[論文解説] 強化学習による高頻度取引戦略の構築","url":"https://qiita.com/sugiyama404/items/1a852458c6c3333b6416"},
    {"title":"機械学習で人とゴリラを見分ける","url":"https://qiita.com/yamadanagamasa/items/ff40c45e8509191521a0"},
    {"title":"ソリューションアクセラレータ：DatabricksとOSMRによるスケーラブルなルート生成","url":"https://qiita.com/taka_yayoi/items/6b064914642f022b9cc5"},
    {"title":"Dezeroでセマンティックセグメンテーション","url":"https://qiita.com/shushin/items/fb213ad6477be878abd9"},
    {"title":"scikit-learn 教師なし学習の覚え書き","url":"https://qiita.com/t_hirao/items/bcaaf98b0cbea6939ab0"},
    {"title":"デバッグの極意〜初めてのペアプロから学ぶ仮説検証のPDCAサイクル〜","url":"https://qiita.com/yu--ma/items/6d9c165f016952673729"},
    {"title":"Node.jsの基礎（簡単な操作）","url":"https://qiita.com/hiro949/items/8f992bdafa8f90828b62"},
    {"title":"バッチ画像処理向け Python GUI フレームワークのメモ","url":"https://qiita.com/syoyo/items/b82da8e1d9e07e7d94ac"},
    {"title":"ノーコード開発ツールで未解決課題を解決できるか考えてみた","url":"https://qiita.com/ak-ishi/items/267c9b2e30f43316a593"},
    {"title":"【Java基礎】変数＆演算","url":"https://qiita.com/r_1204/items/ca65befe354ab7b14c69"}
]
```
「機械学習」というキーワードに対して、強化学習（1件目）やセグメンテーション（4件目）、教師なし学習（5件目）の記事がヒットしています。
もともとのQiita APIでは完全一致しないと抽出できないので、これは便利ですね。（自画自賛）

ただし、機械学習とは直接関係のないJavaScript、Javaの記事が入っているので、精度面ではまだまだ改良しがいがありそうです。

他にも、いくつか叩いてみました。
- https://hogehoge/?s=勉強
```
[
    {"title":"勉強になったことをメモ","url":"https://qiita.com/ShinyaMorita/items/0153bfdfb9d1d1d8d4df"},
    {"title":"ぐうたらな自分を変える教科書 やる気が出る脳 読書メモ","url":"https://qiita.com/tseno/items/db3612f2254559270174"},
    {"title":"書き方がよくわかんないからとりあえずテスト投稿","url":"https://qiita.com/hyasira/items/0f01e273da32380e07d6"},
    {"title":"30代女性が未経験からテックキャンプを受講した感想","url":"https://qiita.com/pipiyoyoyo/items/9ca6500a6b7c3066454e"},
    {"title":"【完全独学】SQL勉強法(Bigquery)","url":"https://qiita.com/ysk_510/items/2b3eeffe9168cdc15d21"},
    {"title":"【PHP基礎復習2】引き算","url":"https://qiita.com/koala_56/items/3a6448bb67dbc5571ba5"},
    {"title":"【PHP基礎復習4】引き算②","url":"https://qiita.com/koala_56/items/0a016877848a69113126"},
    {"title":"プログラマになりたい/なろうかなと思う君へ。君にはプログラマの仕事を体験してもらう","url":"https://qiita.com/debu-despot/items/33ccc62df6037054d9ac"},
    {"title":"「わかる！ドメイン駆動設計 ～もちこちゃんの大冒険～」読書メモ","url":"https://qiita.com/otkz44/items/52b5124837495f544c35"},
    {"title":"【PHP基礎復習1】割り算の余り","url":"https://qiita.com/koala_56/items/f8163e3dec660ea00d27"}
]
```

- https://hogehoge/?s=キャリア
```
[
    {"title":"個人開発２年間の軌跡","url":"https://qiita.com/gmasa/items/fb9a0487bb0a12b91219"},
    {"title":"データエンジニアの選択肢（株式会社ナウキャスト入社エントリ）","url":"https://qiita.com/numa5h0/items/49cb5cabe692db64a387"},
    {"title":"【開発奮闘記】機械音痴が挑戦！LINE Botでタイの魅力を発信！","url":"https://qiita.com/rsaaa269/items/34474da03c5299a66a68"},
    {"title":"Haskellに再入門して100万エッジのグラフネットワークでDijkstra探索するまで","url":"https://qiita.com/reki2000/items/2c0fbc9be298044f9a03"},
    {"title":"エリック・エヴァンスのドメイン駆動設計 1部 1章を読んでまとめてみた","url":"https://qiita.com/Hiyokokeko/items/02f4cc40dfad70dee6a6"},
    {"title":"外部設計書作成時に気をつけてほしいこと。（設計はプロ・ユーザー視点の評価を踏まえたもの）","url":"https://qiita.com/SFITB/items/976cec16dc2fbbd7fdb7"},
    {"title":"エリック・エヴァンスのドメイン駆動設計 3章を読んでまとめてみた","url":"https://qiita.com/Hiyokokeko/items/3b5c41e7536075d8be47"},
    {"title":"エリック・エヴァンスのドメイン駆動設計 2章を読んでまとめてみた","url":"https://qiita.com/Hiyokokeko/items/15b7f43f41a98c425c39"},
    {"title":"コミュニケーションをとりたい！OVER40の初心者が翻訳LINEbotの作成に1週間チャレンジ！","url":"https://qiita.com/koji-kikkawa/items/001d9813cdf28f732015"},
    {"title":"LazyVGridとかLazyVStackのカラムにアニメーションをつける時の注意点","url":"https://qiita.com/kamomeKUN/items/04f2f8198d09edd1df2f"}
]
```

- https://hogehoge/?s=統計学
```
[
    {"title":"ハミルトニアンでシグモイドを導出して分類問題を解きながら統計力学を復習","url":"https://qiita.com/h1day/items/8168ee8861cce11b12a6"},
    {"title":"numpyで統計②：単回帰分析で残差分散を求める","url":"https://qiita.com/lzpel/items/edc5832fabbe05b243b4"},
    {"title":"[論文解説] 強化学習による高頻度取引戦略の構築","url":"https://qiita.com/sugiyama404/items/1a852458c6c3333b6416"},
    {"title":"統計検定1級対策 各分布の特徴の覚え方2 離散分布編","url":"https://qiita.com/tsu_59/items/8c1878612bf667995fe5"},
    {"title":"MATLABで微分方程式を解いてみよう。その２","url":"https://qiita.com/arcadia13/items/8b59e4692300ea4b89ac"},
    {"title":"鋭角三角形「2022年 近畿大学・医学部(前期) 数学 ［１］」をsympyでやってみた。 ","url":"https://qiita.com/mrrclb48z/items/92d7028479095ae9ac9b"},
    {"title":"データ値の累積分布（可視化、JavaScript）","url":"https://qiita.com/kkdd/items/bb4de4e17137123c221a"},
    {"title":"IoU の計算方法の最終的解決と世界一親切な図説","url":"https://qiita.com/k-akiyama/items/89714d276871ea339aa9"},
    {"title":"マーケティングビジネス実務検定B級を受けてみた","url":"https://qiita.com/MRO/items/fb37fcb683c6ed227027"},
    {"title":"ニューラルネットワークを用いた手書き数字認識","url":"https://qiita.com/shota_seki/items/bfe74eaffd50ce65cc88"}
]
```

## これから改良したいこと

タイトルにVer.0と入れているのは、不完全だと感じている点が結構あるという感想からです。

- fastTextモデルの改良
    - コーパスの変更：よりエンジニア界隈ドメインに近いコーパスを使ってみたい
        - ex. Qiita記事から本文を抽出するとか
    - 逐次学習
        - 長期的に見たとき、新出単語に対応できるか（特にエンジニア界隈は新しい単語が多い...）
        - ex. 新しいQiita記事を使って、モデルを更新する
- 記事ベクトルの生成方法
    - タイトルに含まれる名詞から重心をとるだけ？
        - ex. 頻度について重み付き平均をとる、TF-IDF
    - 記事の本文も上手く使いたい

## おわりに

夏休み企画に乗っかって、見様見真似でAPIをつくって公開してみました。

爪が甘くて自分でもツッコミどころが多いのですが、ぜひ改良点がありましたらコメントにて指摘いただけると幸いです...
またSlack等で使っていただいて、ぜひフィードバックもお願いしたいです！