A Day in the Life

静的サイトジェネレータ向けに類似ドキュメントを出力する cli を作った

先日この secon.dev で使う用に、いわゆる関連エントリーを出力する cli を書いたのだけど、世の中に有る静的サイトジェネレータの殆どは、どこかのファイルパスに .md, .html 等の markdown や html で書いた記事データを元に html を生成するものが大半なことに気づいたので、ひょっとしたら利用する奇特な人が現れるかも、と類似ドキュメントを出力する cli として公開した。

この cli の引数に、関連エントリーとして類推して欲しいファイルをガッと渡すと、json 形式でファイルごとに関連度が高いファイルを表示する。このサイト(secon.dev)の記事ファイルは公開していないため、例としてr7kamura さんが公開している r7kamura.comのソースコード に含まれる記事の markdown で関連エントリーを類推してみる。

$ time similar-documents --debug -k 3 -t japanese ~/src/github.com/r7kamura/r7kamura.com/articles/*.md > r7kamura_com_similar_articles.json
files to texts 951 documents
calc tfidf...
calc similarity...
assign similarity score
similar-documents --debug -k 3 -t japanese  >   8.03s user 3.98s system 383% cpu 3.131 total

951記事の記事類推に3.1秒(Ryzen 3900X環境)ほど。この json には私の環境のファイルパスが入ってしまっているので、ちょっと整形する。Hash の key が記事のパスで、entry に含まれる Array にスコアが高い順に関連記事が入っている。

cat r7kamura_com_similar_articles.json | jq . | sd '/home/yu1/src/github.com/r7kamura/' 'https://' |sd '.md"' '"' > converted.json
cat converted.json

この json の中からいくつか抜粋してみる。ダクトレールの記事には高いスコアで他のダクトレール系の記事が関連記事になっている。

  "https://r7kamura.com/articles/2021-02-05-switchbot-hub-mini-on-rails": [
    [
      "https://r7kamura.com/articles/2020-12-19-google-home-mini-on-rails",
      0.6502251932677562
    ],
    [
      "https://r7kamura.com/articles/2021-01-18-nature-remo-on-rails",
      0.6088665752039284
    ],
    [
      "https://r7kamura.com/articles/2016-12-12-h",
      0.33070364498269256
    ]
  ],

ゲーム、ライザのアトリエの記事には、別のライザの記事や FF13 の記事などがヒットしている。スコアを見ると圧倒的にライザの別記事が高い。

  "https://r7kamura.com/articles/2021-02-13-atelier-ryza": [
    [
      "https://r7kamura.com/articles/2020-01-19-atelier-ryza",
      0.4632359977711961
    ],
    [
      "https://r7kamura.com/articles/2020-12-31-games-2020",
      0.17984491640184092
    ],
    [
      "https://r7kamura.com/articles/2021-01-30-final-fantasy-13",
      0.15056225381780178
    ]
  ],

浴槽掃除の記事には、浴槽洗剤や排水溝掃除の記事が類推されている。

  "https://r7kamura.com/articles/2021-02-19-laundry-cleaning": [
    [
      "https://r7kamura.com/articles/2020-11-02-lookplus",
      0.39103024934082137
    ],
    [
      "https://r7kamura.com/articles/2020-10-12-ember-restored",
      0.3759286934329018
    ],
    [
      "https://r7kamura.com/articles/2014-08-31-h",
      0.33743028929351304
    ]
  ],

こんな感じでコマンド一発で関連エントリーを類推した json を出力できるので、あとは静的サイトビルド時に利用すれば、関連エントリー機能を静的サイトジェネレータでも組み込みやすくなる、と思っている。

永続的に利用できる計算機リソースがある環境では、Elasticsearch の More like this などを利用したほうが、より的確に関連エントリーを出せるのだけど、静的サイトジェネレータの利用ではコマンド一発で気軽に出せるほうが、状態を気にしなくてよいのでメリットも有るだろう。

技術的なこと

難しいことは行っておらず、機械学習の初学用テキストにほぼ出てくるような文章類推の手法、文字カウントをとってtf-idfを求め、コサイン類似度から似ているドキュメントを探しているだけ。tokenizer には日本語ならMeCab(の使いやすく辞書もインストールしやすいpythonラッパーの fugashi)を使い、tf-idf・コサイン類似度の計算は全部scikit-learn任せだ。古典的な手法なのだけど、実際に使ってみると割と良い感じに類推される。

また今の所 .md.html は各種フォーマットからパーサ経由でテキストに変換し、他のファイルはすべてテキストとして扱っている。tf-idf のアルゴリズム上、たくさんの文章に出る単語はスコアが下がるので、すべてのファイルが特定フォーマットのファイルなら、そのフォーマットで使われる単語はスコアに影響しにくい(もちろんテキストに変換されたほうが望ましいが)ので、割とうまく類推できる気がする。

記事の一覧 >

関連するかもエントリー

このウェブサイトの実装 2020年版
r7kamura さんや kzys さん に倣って、このウェブサイトの実装を紹介してみる。ホスティングGoogle Firebase Hosting を使って静的ファイルを配信してる。一部動的な実装に関しては、Cloud Functions for Firebase を使っている...
r7kamura さんや kzys さん に倣って、このウェブサイトの実装を紹介してみる。ホスティングGoogle Firebase Hos...