にほんごのれんしゅう

日本語として伝えるための訓練を兼ねたテクログ

botはツイートを圧縮するとサイズが小さくなることを利用して、botの検出

今やっていること

真面目に相性を考慮した企業推薦アプリやマッチングアプリを作りたい
企業への就職や出会いを求める場など、現在はITが進んでいますが、まだ最適な状態に至っていいないだろうと思われます。そんな課題を解決するために、人の行動ログ(ここではSNSでの発信ログ等)を利用して、真面目なマッチングエンジンを作ろうとしていました。

具体的な多くの人の行動ログを取得可能なサービスを所有していないので、Twitter社のデータを用いて、マッチングエンジンを作ろうとして現在、技術検証や精度の改善などをしています。

日本語のテキストを書くユーザ 2400万人 分の直近500 ~ 1000ツイート程度をサンプリングしており、さまざまな観点を検証しています。

安西先生...、botが邪魔です...!
狭い課題として、botと呼ばれるプログラムでの自動運用されたアカウントが少なくない数存在し、botは特定のキーワードを何度もつぶやくので、個性や特性を重要視したマッチングエンジンを作成した際に悪影響を及ぼす可能性が強くあります。

そのため、狭いスコープの課題ですが、botをデータと機械学習でうまく検出し、ブロックする仕組みを構築しましたので、ご参考にしていただけますと幸いです。

先行研究

奈良先端科学技術大学院大学が数年前に出した軽めの(?)論文に、認知症者􏰀発言􏰁圧縮すると小さなサイズになるというものがあります。

認知症の人は、認知症ではない人に比べて、発話した情報が、圧縮アルゴリズムにて圧縮すると、小さくなるというものでした。

ハフマン符号化をかけることで、頻出するパターンをより短い符号に置き換えでデータの圧縮サイズをあげようというものになります。

pythonで処理するには同系統アルゴリズムであるbzip2が便利

bzip2は、明確に ブロックソート法ハフマン符号化 を行っており、かつ、zipという圧縮方式より結果が綺麗に出たので、採用しました。

具体的には、以下のようなプロセスで元のテキストファイルサイズと、圧縮済みのファイルサイズを比較します。

import bz2
import os
import random
from pathlib import Path

salt = f"{random.random():0.12f}"
pid = f"{os.getpid()}_{salt}"

all_text # ある特定のユーザのツイートを1つにjoinしたもの

# そのままのテキスト情報を書き込んだときのサイズを取得
with open(f"/tmp/{pid}", "w") as fp:
   fp.write(all_text)
original_size = Path(f"/tmp/{pid}").stat().st_size

# bz2で圧縮した時のサイズを取得
with bz2.open(f"/tmp/{pid}", "wt") as fp:
    fp.write(all_text)
bz2_size = Path(f"/tmp/{pid}").stat().st_size

# clean up
Path(f"/tmp/{pid}").unlink()

圧縮アルゴリズムで作った特徴量と、特徴量エンジニアリングで作った特徴量を組み合わせて学習

ラベルの定義

ユーザネームの末尾が _bot になっているものをbotと定義

botはかなりの数存在し、検索を汚染するような形でよく出現します。twitterのユーザーネームで末尾に_botをつけているアカウントが存在し、これはほぼbotだろうという前提で処理して良いものであろうと理解できます。ユーザーネームの末尾が _bot ならば、botと定義しました。

縦軸に 圧縮したファイルサイズ/オリジナルファイルサイズ , 横軸に 機械的な周期性 を取ると、上記のような散布図を得ることができます。

ノイズもままある
_bot と末尾のユーザーネームにつけているのにbotでない人、それなりにいるんです。Twitterをやっていると理解できる事柄ですが、自身のアイデンティティなどを機械などと近い存在であると思っている人は _bot とかつけることがあるようです。
手動でクリーニング仕切れない量程度には、人なのに、 _botサフィックスをつけている人がいるのですが、機械学習でうまくやることで解決していきます

例えば、上記の図の左側でまるで囲まれた _bot であると自称しているユーザは、実際は典型的な人間のツイートをしており、botではありません。

_bot が末尾のものだけでは、precision によりすぎている

真の課題は、_botサフィックス等がつかなくても人間のユーザのフリをするbotアカウントを検出することであります。このたぐいのアカウントは大量にあり、自動運用されている宣伝・企業アカウントなどはまずbotであります。

そのため、 _botサフィックスでの判定は例外があるものの、機械学習によるprecisionによりすぎた判別をrecall側に倒す作業とも捉えることが可能になります。

モデルの閾値を調整し、現実的なリコールになるようにする

特徴量を選定する

今回のスコープとしてrecallを上がるように倒したいので、モデルの複雑度を上げすぎないことと、ノイズとなるbotでないのにbotを自称している人を判別してはいけないので、うまく丸めるため特徴量を多くしすぎないことがポイントとなります。

以下の特徴量を選択しました。

  • compression_ratio: 圧縮率
  • is_near_rate: 前のツイートから10分の倍率でツイートした率
  • uniq_ratio: ユニークツイート数

モデルの作成

LightGBMで、特徴量を3つ用いて、構築する木の複雑さを3に抑えて、AUCをメトリックとして学習を行いました。 Holdoutで25%をvalidationとして、AUCが悪化しない範囲で学習を継続します。
特徴量単体だと、AUCが 0.90程度までしかでませんが、 0.922 まで上げることが可能になりました。

作成したモデルのしきい値探索

どの程度、botと判定していいのかを定義する

_bot がつくアカウントがTwitterのアカウントの全体の 0.05% 程度でした。定性的な感想ですが、2%程度は完全にbotに近いアカウントであり、学習したLightGBMのモデルの閾値を0.05まで緩めることで、2%程度のリコールを得ることができました。

0.05のしきい値で推論した結果

素の _bot だけの散布図より、オレンジの面積が大きく広がっていることがわかります。

モデルの結果の定性評価

リコールを広げた結果、具体的にどのようなアカウントがボットと判定されたのか見ていきます。

著作権法32条に基づき、技術検証のため、公開情報を引用しています)

例1: もう使用していないアカウントでアプリ登録をしたアカウントをボットとして判定

連携したアプリからの宣伝のみなのでbot判定でOK

例2: 出会い系の誘導の業者アカウントをボットとして判定

実態は偽装した業者アカウントbotなので見抜けており、期待した挙動である

Twitter APIでまとめてbotをブロックする

このbotの検出は価値がある作業で、Twitterの検索結果をbotや業者が激しく汚染する、という経験を体験している方は多いかと存じます。
検出されたbotTwitter APIとそれをpythonで簡単に使えるようにしたtweepyをインストールすることで、簡単に特定のユーザをblockすることができます。

書捨てのコードだと以下のようにして実行することができます。

入力となるcsvデータは末尾のDropboxのリンクに付属します。

import time
import tweepy
import os
import pandas as pd
from tqdm import tqdm

auth = tweepy.OAuthHandler(os.environ["API_KEY"], os.environ["API_SECRET_KEY"])
auth.set_access_token(os.environ["ACCESS_TOKEN"], os.environ["ACCESS_TOKEN_SECRET"])

api = tweepy.API(auth)

df = pd.read_csv("./tmp/result.csv")
df.sort_values(by=["yhat"], ascending=False, inplace=True)

for username, yhat in tqdm(zip(df.username, df.yhat), desc="blocking...", total=len(df)):
    try:
        api.create_block(username)
        # time.sleep(0.1) # you may need this
    except Exception as exc:
        print(exc)
        continue

データとコード

  • GitHub: 再現をしたい場合、何をやったかが確認できます
  • Dropbox: 最終的な、推論したスコアを付与したデータ
  • Dropbox: Githubにあるコードを再現するためのデータ(オリジナルのTweet情報は含みません)

Webアプリにできないでしょうか?

  • 常に新鮮なコーパスはあり、集計はできる
  • TwitterIDでログインすると、一括でブロックできるアプリは作ることができ、また、世の中に必要な気がします。

技術書典8の製作物を無料公開します

お久しぶりです。 私生活で反省することが多く、心身ともに疲弊し、しばらくTwitterやブログなど対外的なアウトプットをお休みしていました。

技術書典8で復活を遂げようと思っていたのですが、コロナのために開催がなくなってしまいました。

いろいろな人を頼り、助けてもらい、多くの人の力で作ったコンテンツであったので、死蔵させいても仕方がないと思い、公開します。

サークル名は Practical Data Science and Data Engineering で、いかにも、、、な名前にしました。あまりひねったり、ギャグに走るのは性分でないためです。

ここのGitHubからダイレクトにPDFをダウンロードすることもできますが、もし私にお茶でも奢っていいと言う優しい方がいらしたら、BOOTHのコンテンツを販売しているサイトから購入していただけると幸いです。

表紙などは、商用利用OKのphotostockからPowerPoint()で加工したもので、Nate SilverさんのSignal and Noiseをきっかけに業界に入ったという背景もあって、デザインを真似ています。

内容としましては、いろんな試行錯誤をしてきたScrapingのテクニックをまとめた全集としました。

また、著作権は主張したいのですが、GitHubからダウンロードしたコンテンツを教育目的や学術目的でシェアする分には構いませんので自由に使っていただけると幸いです。

GitHub版(無料)

BOOTHでの販売

私の印刷所に持ち込むPDFの作り方が全てmarkdown + OSSだけで完結したのでその方法についてもご紹介します。

メジャーなOSSだけで日光企画さんのレビューをクリアできた

最初はAdobeの製品であったり、一部の使う機会が少ないソフトを学ばなくてはいけないのか、、、と学習コストを心配していたのですが、pandocとgoogle chrome + nodejsだけでなんとか日光企画さんのレビューをクリアする程度には品質を確保できたのでその方法をご紹介します。 MacOSで作成しましたが、Linuxでもいけるはずです。

Markdownで資料をつくる

GitHubVS CodeインタラクティブMarkdownを編集できるモードで、コンテンツを作成します。MarkdownGithubのREADMEなどを作成するときに使用できるフォーマットで、プログラムを作成する機会のある人なら大体書けると思います。

PandocでMarkdownをデザイン付きHTMLにコンパイルする

Pandocを製本のユースケースでは、あまり国内で使用している人はいないようでしたが、Markdown形式をHTML形式に変換する際にたいへん便利です。また、この変換にはCSSを指定することができ、GitHubのウェブサイトのようにコードに対してシンタックスハイライトなどを行うことも可能です。

 $ pandoc \
      -s ${インプットファイル.md} \
      -f markdown \
      --metadata title="Practical Data Science & Engineering Vol.1" \
      -c ${シンタックスハイライト.css} \
      -o ${出力ファイル名.html}

このようなコマンドで簡単にMarkdownシンタックスハイライト付きのHTML化することができます。

HTMLをPDFに変換する

最もめんどくさく、かつ、辛いのがPDF化の作業なのですが、海外のサイトなどを見ているとhtmlでフォーマットされたコンテンツをPDF化するにはGoogle ChromeのPDF生成機能を用いるのがよいとされていることが多いようです。

Google Chromeデバッグモードで起動する

 $ /Applications/Google\ Chrome.app/Contents/MacOS/Google\ Chrome --headless --remote-debugging-port=9222

実際にGUIで操作するスタイルでは無くCUIで操作するため、Google Chromeデバッグモードで起動する必要があります。

Debug Modeで起動したGoogle Chromeにhtmlをpdfに変換する命令を行う

NodeJSをインストールしたのち、NodeJSからGoogle Chromeを操作するモジュールである、 chrome-remote-interfaceをインストールします。

chrome-remote-interface経由で以下のようなスクリプトを介してPDFに変換すると、PDFの各ページの余白やヘッダーやフッターの幅を調整したり、ページ番号を入れたりすることができます。

(印刷会社の日光企画さんに持ち込んで色々ヒアリングしたところ、ページナンバーを入れるのは必須であると言うことでした。)

    #!/usr/bin/env node
    const homedir = require('os').homedir();
    const CDP = require(homedir+'/.config/yarn/global/node_modules/chrome-remote-interface/');
    const fs = require('fs');
    
    const port = process.argv[2];
    const htmlFilePath = process.argv[3];
    const pdfFilePath = process.argv[4];
    
    (async function() {
            const protocol = await CDP({port: port});
            // Extract the DevTools protocol domains we need and enable them.
            // See API docs: https://chromedevtools.github.io/devtools-protocol/
            const {Page} = protocol;
            await Page.enable();
    
            Page.loadEventFired(function () {
                    console.log("Waiting 100ms just to be sure.")
                    setTimeout(function () {
                            //https://chromedevtools.github.io/devtools-protocol/tot/Page/#method-printToPDF
                            console.log("Printing...")
                            Page.printToPDF({
                                    marginTop: 0.8,
                                    marginBottom: 0.2,
                                    displayHeaderFooter: true,
                                    headerTemplate: '<div></div>',
                                    footerTemplate: '<div class="text center"><span class="pageNumber"></span></div>',
                            }).then((base64EncodedPdf) => {
                                    fs.writeFileSync(pdfFilePath, Buffer.from(base64EncodedPdf.data, 'base64'), 'utf8');
                                    console.log("Done")
                                    protocol.close();
                            });
                    }, 100);
            });
    
            Page.navigate({url: 'file://'+htmlFilePath});
    })();

このJavaScritpをprint-via-chrome.jsとして、実行権限を付与すると、実行できるようになるので、このように引数にchromeデバッグポート、インプットのhtml、出力ファイル名を引数にして実行するとpdfを得ることができます。

 $ print-via-chrome.js 9222 ${HTML_FILE} ${PDF_FILE}

一連のコード

DocCompile.py というファイルを作成して、pandocでカバーしきれないHTML, CSSの編集と、Google Chromeに印刷させる一連のフローをPython Scriptにラップアップしました。 自分の環境に書き換えて用いていただけると幸いです。

参考

自作レコメンドで最適な読書体験をしたい

最適な読書体験をしたい

 アマゾンなどでレコメンドされる本を上から見ていても読書体験がそんなに良くありません。
 本の売り上げランキングなどは、大衆に受ける本がほとんどであり、少々独特なセンスを持つ人たちにはそんなに受けが良くないです。
 結果として現状の解決策がSNSや人づてに聞き及ぶぐらいしかないのとジャケ買いなどがせいぜいです
 どうあるべきかを考えるとき、仮に他人の本棚を知ることができれば、集合知機械学習を用いて自分に向いているだろう本をレコメンドさせることができます

会社の技術共有会の小話で話した話

Matrix Factorization

 2000年台のNetflix Prizeからある伝統的な手法で、シンプルで動作が早く、ユーザが多くアイテムの数がとても多いときに有効な手法です。

DeepLearningでも実装できるし、sklearnなどでも関数が用意されています。

コード

自分のクエリとなる特徴量

 自分のAmazon Fionaという特定のURLにアクセスると自分の今までKindleで買ってきた本がAjaxレンダリングされます。
 Ajaxにより描画されていて、かつ、とても描画が遅いので普通の方法では自動取得できなく、google-chrome-headlessブラウザ等を利用してJSを実行しながら内容を取得できるようにします。
- 購入した本の一覧が見えるページ: https://amazon.co.jp/gp/digital/fiona/manage

実行コマンド

$ cd DataCollection
$ EMAIL=*****@gmail.com PASSWORD=***** python3 A001_from_kindle.py 
$ python3 B001_scan_local_html.py
fionaのURLをアクセスするとAjaxでこのように描画される

いろいろな人達の本棚の特徴量

 レコメンドを行うには大量のデータが必要になります。
 他人の本棚が必要になりますが、https://booklog.jp/ が本棚SNSになっているのでこれを利用します。
(すいません、スクレイピングしないと学習できないので、集めます)
実行コマンド

$ cd DataCollection
$ python3 A001_scrape.py

 現在120万ユーザが登録しているらしく、全体の一割程度でいいのでユーザの本棚をサンプルして、本棚に登録されている本を1として、登録されていない本を0とすると、巨大な疎行列を作ることができます。scipyのlil_matrixという疎行列ライブラリを利用して構築すると、400Mbyte程度に収めることができます。

実行コマンド

$ cd MakeBookReadMatrix
$ python3 A001.py
$ python3 B001.py
$ python3 C001.py

学習

一応、Matrix Factorizationにも過学習という概念があるので、2%をtestとして切り出して、ホールドアウトで、レコメンドしたときのMatrixとのMean Square Errorを小さくします。
実行コマンド

$ cd MakeBookReadMatrix
$ python3 D001.py --fit
fit non-negative matrix factorization
(1757, 1133108)
test mse = 0.000107 # <- 今回のデータ・セットではこのくらい

推論

Kindle Fionaから得られた本を、1*BOOK_NUMのMatrixに変形して、学習で作ったモデルに入力すると、各アイテム毎のレコメンドを行った際のウェイトを知ることができます。

実行コマンド

$ python3 D001.py

scores_00.csv というファイルができ、その中にタイトルとウェイトが記されている.

自分の結果

過去に漫画を大量に買っていたのでおおよそ納得の結果

別の絵本が多いユーザでもやってみましたが、絵本が多く上位に出るので想定通りできていることが確認できました。

依存(Ubuntuを想定)

$ wget https://dl.google.com/linux/direct/google-chrome-stable_current_amd64.deb
$ sudo apt install ./google-chrome-stable_current_amd64.deb
$ wget https://chromedriver.storage.googleapis.com/75.0.3770.140/chromedriver_linux64.zip # google-chromeのversionに応じたものを使ってください
$ unzip chromedriver_linux64.zip
$ sudo mv chromedriver /usr/local/bin/
  • requirements.txt
$ pip install -r requirements.txt

再現できないときいは

 よく指摘されるので、Linuxの用意や再現が難しい場合は、私のデータで学習したときのデータからモデルまでの動作が確認できたときのスナップショットがあるので、参考にしてみてください。

まとめ

 自分の知識や体験の幅を広げるには、レコメンドでウェイトが付いているが、リコールを高めに見たときに低いウェイトの方に来ている本を読むと世界や価値観の広がりを高めることができているように思います。
商業的にはおそらく高いウェイトの作品をレコメンドするとよいのでしょうが、自分に近すぎるコンテンツということもあり食傷気味であり、Amazonででる本などは興味を惹かれなかったのですが、自分でこのレコメンドエンジンを使う分にはこの制約がなくて良さそうです。

Pythonで100万ドキュメントオーダーのサーチエンジンを作る(PageRank, tfidf, 転置インデックスとか)

検索エンジンを何故作ってみたかったか

  • もともとこのブログのコンセプトのNLP的なことで、情報を整理してなにか便利にしたかった(Googleと同じモチベーションの世界の情報を整理する)
  • 4年前にほぼ同じシステムを作ろうとしたとき、500万を超える大量のインデックスを検索するシステムは、数学的な理解度が十分でない+エンジニアリング力が伴わないなどでギブアップした背景があり、今回再チャレンジしたくなった
  • ほぼすべての機能をpure python(+いくつかの例外はある)で実装して、世の中の ソフトウェアを使うだけ検索エンジンをやってみたなどではなく、実際に理解して組んでみることを目的としたかった

依存パッケージと依存ソフトウェア

GitHubのコードを参照してください

様々なサイトを巡回する必要があり、requestsが文字コードの推論を高確率で失敗するので、nkflinux環境で入れている必要があります。

$ sudo apt install nkf
$ which nkf
/usr/bin/nkf

Mecabも入れます

$ sudo apt install mecab libmecab-dev mecab-ipadic
$ sudo apt install mecab-ipadic-utf8
$ sudo apt install python-mecab
$ pip3 install mecab-python3
$ git clone --depth 1 https://github.com/neologd/mecab-ipadic-neologd.git
$ ./bin/install-mecab-ipadic-neologd -n

残りの依存をインストールします

$ pip3 install -r requirements.txt

再現

基本的にGitHubのコードをUbuntu等のLinuxでAから順に実行してもらえば、再現できます。

クローラ(スクレイパー)はやろうと思えば無限に取得してしまうので、適当にSEEDを決めて、適当な時間で終了することを前提としていています。

全体の処理の流れ

  1. クローリング
  2. クローリングしたHTMLを, title, description, body, hrefsをパースしデータを整形する
  3. IDF辞書の作成
  4. TFIDFのデータを作成
  5. 転置したurl, hrefの対応を作成(単純な被参照量の特徴量)
  6. 非参照数のカウントと、PageRankのための学習データの作成
  7. URLとtfidfのウェイトの転置インデックスを作成
  8. hash化されたURLと実際のURLの対応表の作成
  9. PageRankの学習
  10. 検索のインターフェース

プログラムの詳細

A. クローリング

特定のドメインによらず、網羅的にクローリングしていきます。 ブログサイトをシードとしてドメインを限定せずどこまでも深く潜っていきます。
多様なサイトをクロールするがとても重いので、自作した分散可能なKVSをバックエンドのDBと利用しています。SQLLiteなどだとファイルが壊れやすく、LevelDB等だとシングルアクセスしかできません。

B. HTMLのパースと整形

Aで取得したデータは大きすぎるので、Bのプロセスで、tfidfでの検索の主な特徴量となる、"title", "description", "body"を取り出します。
また、そのページが参照している外部のURLをすべてパースします。

soup = BeautifulSoup(html, features='lxml')
for script in soup(['script', 'style']):
    script.decompose()
title = soup.title.text
description = soup.find('head').find(
                'meta', {'name': 'description'})
if description is None:
    description = ''
else:
    description = description.get('content')
body = soup.find('body').get_text()
body = re.sub('\n', ' ', body)
body = re.sub(r'\s{1,}', ' ', body)

BeautifulSoupでシンプルに処理することができる.

C. IDF辞書の作成

頻出する単語の重要度を下げるために、各単語がどの程度のドキュメントで参照されているかをカウントします。

D. TDIDFの計算

B, Cのデータを利用して、TFIDFとして完成させます
title description bodyはそれぞれ重要度が異なっており、 title : description : body = 1 : 1 : 0.001
として処理しました。

# title desc weight = 1
text = arow.title + arow.description 
text = sanitize(text)
for term in m.parse(text).strip().split():
    if term_freq.get(term) is None:
        term_freq[term] = 0
    term_freq[term] += 1

# title body = 0.001 
text = arow.body
text = sanitize(text)
for term in m.parse(text).strip().split():
    if term_freq.get(term) is None:
        term_freq[term] = 0
    term_freq[term] += 0.001 # ここのweightを 0.001 のように小さい値を設定する

F. あるURLと、あるURLのHTMLがリンクしているURLの転置インデックスを作成

昔良くあった、URLのリンクを色んな所から与えるとSEOができるということを知っていたので、どの程度外部から被参照されているか知るため、このような処理を行います

G. 被参照数のカウントと、PageRankのための学習データの作成

Fで作成したデータをもとに、networkxというライブラリでPageRankのノードのウェイトを学習可能なので、学習用データを作成します

このようなデータセットが入力として望まれます(右のハッシュがリンク元、左のハッシュがリンク先)

d2a88da0ca550a8b 37a3d49657247e61
d2a88da0ca550a8b 6552c5a8ff9b2470
d2a88da0ca550a8b 3bf8e875fc951502
d2a88da0ca550a8b 935b17a90f5fb652
7996001a6e079a31 aabef32c9c8c4c13
d2a88da0ca550a8b e710f0bdab0ac500
d2a88da0ca550a8b a4bcfc4597f138c7
4cd5e7e2c81108be 7de6859b50d1eed2

H. 単語から簡単にURLを逆引きできるように、転置インデックスの作成

最もシンプルな単語のみでの検索に対応できるように、単語からURLをすぐ検索できるindexを作ります
出力が、単語(のハッシュ値)ごとにテキストファイルが作成されて、 URLのハッシュ , weight(tfidf) , refnum(被参照数) のファイルが具体的な転置インデックスのファイルになります

0010c40c7ed2c240        0.000029752     4
000ca0244339eb34        0.000029773     0
0017a9b7d83f5d24        0.000029763     0
00163826057db7c3        0.000029773     0

I. URLとhash値の対応表の作成

URLはそのままメモリ上に持つとオーバーフローしてしまうので、sha256をつかって先頭の16文字だけを使った小さいhash値とみなすことで100万オーダーのドキュメントであってもある程度実用に耐えうる検索が可能になります。

J. PageRankの学習

Gで作成したデータを学習してURLにPageRankの値を学習します。

networkxを用いれば凄くシンプルなコードで学習する事ができます

import networkx as nx
import json
G = nx.read_edgelist('tmp/to_pagerank.txt', nodetype=str)
# ノード数とエッジ数を出力
print(nx.number_of_nodes(G))
print(nx.number_of_edges(G))
print('start calc pagerank')
pagerank = nx.pagerank(G)
print('finish calc pagerank')
json.dump(pagerank, fp=open('tmp/pagerank.json', 'w'), indent=2)

K. 検索のインターフェース

検索IFを提供

$ python3 K001_search_query.py
(ここで検索クエリを入力)

$ python3 K001_search_query.py
ふわふわ
                   hurl    weight  refnum  weight_norm                                                            url  pagerank  weight*refnum_score+pagerank
9276   36b736bccbbb95f2  0.000049       1     1.000000  https://bookwalker.jp/dea270c399-d1c5-470e-98bd-af9ba8d8464a/  0.000146                      1.009695
2783   108a6facdef1cf64  0.000037       0     0.758035     http://blog.livedoor.jp/usausa_life/archives/79482577.html  1.000000                      0.995498
32712  c3ed3d4afd05fc43  0.000045       1     0.931093          https://item.fril.jp/bc7ae485a59de01d6ad428ee19671dfa  0.000038                      0.940083
...

実際の使用例

"雷ちゃん"等で検索すると、ほしい情報がおおよそちゃんと上に来るようにチューニングすることができました。
Pixivについては明示的にクローリング先に設定していないが、Aのクローラがどんどんとリンクをたどりインデックスを作成した結果で、自動で獲得したものです。

"洒落怖"など、他のクエリでも望んだ結果が帰ってきています。

検索のスコアリングはどうあるべきか

手でいろいろ試してみて、良さそうなスコアはこのような感じなりました。(私が正解データです)

本来は、謎のヒューリスティックに依存した掛け算やlogを取るアプローチより、正解の並び順を与えて、機械学習で正しい並び順になるように学習するのが筋が良いです。

SEOという文化が登場してもう何十年にもなると思うのですが、高いweightを被参照リンクなどに与えるとひどいことになります。アフィリエイトブログやアドネットワークのようなものが大量に上に来てしまいます。

結局できた

様々なクエリを売ってみてそれなりの検索をすることができました。
少なくとも500万 ~ 1000万ドキュメントぐらいならば、すぐ検索できるので、適切なユースケース(図書館とか病院のカルテとか)を設定すればいろいろな応用ができそうです。