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

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

CTRを正しく計測するためにBOTによる表示カウントを除外する方法

こんにちは、超ニッチ&尖りまくりの、にょろりんこの備忘録的技術ブログです。

前回の記事では、動画の人気ランキングにとって非常に重要な クリック数(selected)のカウント において、Botアクセスを除外する方法を紹介しました。

JavaScript上で生成した署名付きトークンを用いることで、curlスクリプトによる不正な加算を防ぎ、「人間のクリックだけがカウントされる」設計を実現しています。

www.n-rinko.com

今回はその続編として、CTR(クリック率)を正しく計測するための「表示回数(appear)」の信頼性確保について解説します。

CTRは「クリック数 ÷ 表示数」で計算される以上、分母となる表示回数が荒らされてしまえば、どんなにクリックされても正しい評価はできません。とくにcurlBotが大量にページを取得するような状況では、見られていないのに表示されたことになるという矛盾が簡単に発生します。

ティアラ。コーヒーブレイク

SaePornsでは、こうした事態を防ぐために、表示ログにも署名トークンによるBot排除の仕組みを導入しています。本記事では、その技術的な仕組みと実装例について詳しく紹介していきます。

そもそもなぜ表示回数をカウントする必要があるのか?

CTR(Click Through Rate/クリック率)は、クリック数 ÷ 表示回数で計算されます。
つまり、クリックされた回数だけでなく、「何回その動画が表示されたか」という情報がなければ、CTRは正しく出せません。

例えば、次の2つの動画があるとします:

  • 動画A:10回表示されて3回クリック(CTR = 30%)
  • 動画B:100回表示されて3回クリック(CTR = 3%)

どちらもクリック数は同じですが、表示された回数が違えば、ユーザーからの関心度はまったく異なることがわかります。

表示されてもクリックされない動画は「見向きもされていない」可能性が高く、逆に少ない表示で多くクリックされていれば、「強い興味を引くタイトル」かもしれません。

このように、表示回数はコンテンツの相対的な魅力注目度を測るための「母数」になります。表示数を正しくカウントできなければ、CTRを指標として使う意味がなくなり、ランキングやレコメンドの精度にも大きな影響を与えます。

そのため、「表示されたこと」自体をどう定義し、どう信頼できる形で記録するかが、CTR運用において非常に重要なのです。

BOTに表示されても、それは「見られた」ことにはならない

表示回数をカウントする理由は、「ユーザーに見られたという事実」を記録し、CTRの分母として使うためです。

しかし、ここでひとつ、重要な落とし穴があります。それは、Botによるアクセスも「表示された」とカウントされてしまうという問題です。

たとえば、検索エンジンクローラーや、curlPythonスクリプトのようなBotが一覧ページを取得したとき、通常の実装では、その動画たちは「ユーザーに表示された」ことになってしまいます。

でも、それは本当に見られたのでしょうか?スクリーンにも表示されていない、人間の目にも触れていない、クリックされる可能性もゼロ、そんなアクセスを「1回表示された」と数えてしまえば、CTRは一瞬で崩壊します。

Botによる大量アクセスの例
  • 本来:表示10回 → クリック3回(CTR = 30%)
  • Botが一覧を50回取得:表示60回 → クリック3回(CTR = 5%)

クリック数は同じでも、Botのせいで「人気がないように見えてしまう」のです。

CTRを信頼できる指標にするには、「表示された」の中身をちゃんと選別する必要があります。つまり「人間のブラウザから、JSを通じて描画された場合のみ」を「信頼できる表示」として記録すべきなのです。

そのためにSaePornsでは、単なるアクセスではなく、JavaScriptで生成された署名付きトークンを持ったアクセスだけを、appear(表示回数)として記録する設計にしています。

具体的にコードを見ていきましょう。

// video-list.js

// トークン付き appear +1 加算
if (word_a !== '') {
  const timestamp = Math.floor(Date.now() / 1000);
  const raw = `${ud.ID}:${word_a}:${timestamp}:${navigator.userAgent}`;
  const token = CryptoJS.SHA256(raw).toString();

  memberList.push({
    ID: ud.ID,
    priority: 1,
    timestamp: timestamp,
    token: token
  });
}

ここでは、「人間のブラウザのみで生成できる署名トークン」生成処理しています。Botcurl, Puppeteerなど)はCryptoJSもnavigatorも無いので生成することができません。

署名付き memberList を送信して、appear +1 を申請する

表示された動画ごとに、IDやトークンを含んだオブジェクトを作成し、それらを配列にまとめたものが memberList です。この配列に署名トークンを付与することで、「人間のブラウザで表示された」ことを保証します。

実際の送信処理は以下のようになっています。

// video-list.js

$.ajax({
  url: '/api540-update.php',
  type: 'POST',
  data: {
    searchWord: word_a,
    memberList: JSON.stringify(memberList)
  }
})

ちなみに、memberList の中身は次のようなイメージです:

const memberList = [
  {
    ID: 12345,
    priority: 1,
    timestamp: 1718888888,
    token: "abcdefg123456..."
  },
  {
    ID: 67890,
    priority: 1,
    timestamp: 1718888890,
    token: "hijklmn789012..."
  }
];

 

memberListは、JavaScript上で署名を付与した動画オブジェクトの配列であり、それをJSON.stringify() によってJSON文字列としてサーバーに送信しています。

このように構造化されたデータにより、サーバー側では署名トークンの検証や時刻チェックを行い、正規ユーザーからのアクセスのみを appear +1 として記録できる仕組みになっています。

ちなみに変数名がmemberListなのは、ごく初期にSaePornsが従業員管理ツールとしてスタートした時の名残です。直さなくちゃとは思っているのですが、放置されていますね。

ここまでで、トークン付き memberList をJavaScript側で生成し送信する処理は完了しました。ここからは、そのトークンを受け取るサーバー側(api540-update.php)の処理内容を見ていきます。

サーバー側で署名トークンを検証し、Botを除外する

クライアント側から送られてきた memberList は、JavaScriptによって生成された署名付きの配列です。

サーバー側(api540-update.php)では、これを受け取り、正規の人間ユーザーからのアクセスであることを検証したうえで appear +1 を行う設計になっています。

検証には、主に以下の3つのチェックを行っています。

必須パラメータと priority のチェック

// api540-update.php

if (!$videoID || !$timestamp || !$token || (int)$member['priority'] !== 1) {
    continue;
}

パラメータが正しく存在しているか、そして priority: 1(JSで意図的に送られたもの)であるかをチェックしています。データの信頼性を担保するためのフィルタです。

タイムスタンプの妥当性チェック(±5分)

// api540-update.php

if (abs($now - $timestamp) > 300) {
    continue;
}

トークンの生成時刻と現在時刻の差が5分(300秒)以内かどうかを確認しています。これにより、古いトークンの使い回しやリプレイ攻撃を防止しています。

署名トークンの再計算と一致判定

// api540-update.php

$expected = hash('sha256', "$videoID:$searchWord:$timestamp:$userAgent");

if (!hash_equals($expected, $token)) {
    continue;
}

JavaScript側で作成されたトークンと同じ方法で、サーバー側でも再度ハッシュを計算します。このとき userAgent も含めているため、curlBot では一致するトークンを再現できません。また、hash_equals() を使用することで、タイミング攻撃への耐性も確保しています。

すべての条件をパスしたリクエストのみ appear +1

// api540-update.php

$stmt1 = $dbh->prepare(
    "UPDATE words SET appear = appear + 1, updated_at = NOW()
     WHERE video_id = :id AND word = :word"
);

このように、3段階の検証をすべてパスしたリクエストだけが、表示された(appear)というログとしてカウントされます。Botcurlではこの署名を通すことができないため、CTRの分母となる表示ログの信頼性を保つことができる仕組みになっています。

まとめ

今回は、CTR(クリック率)を正しく計測するために、表示回数(appear)のログにも署名トークンによるBot排除の仕組みを導入している話をご紹介しました。

CTRを正しく計測しようと思うと、単に「クリック数」を数えるだけでは足りません。その前提となる「表示された」という事実が、どれだけ信頼できるか?そこに向き合うことが、検索品質やランキングの精度を左右します。

SaePornsでは、「匿名性」と「精度」の両立を目指して、JSとPHPでできる軽量なBot排除ロジックを日々試行錯誤しています。技術的にはやや尖っているかもしれませんが、一人でも「この考え方、応用できそう」と感じていただけたら嬉しいです。

それではみなさん、よい開発ライフを。開発継続中の、あなたを追跡しないアダルト動画の検索エンジンSaePornsはこちら。※18歳未満の方はご利用いただけません。

sae-porns.org