はじめに
オプチャグラフは、LINE OpenChatの統計情報を収集・分析・可視化するWebサービスです。
前回までの記事:
- ①データパイプライン - 25万件/毎時のクロール〜静的ファイル生成
- ②差分検出 - 25万件の更新を99%削減する仕組み
- ③バッチ設計 - 冪等性・再開可能性・障害耐性
- ④2025年の技術的チャレンジ振り返り - 多言語対応の実装・キーワードスパム対策等
タグ付けシステム
オプチャグラフでオープンチャットを見ていると、「ポケカ」「あんスタ」「転スラ」といったタグが付いているのに気づいたことはありませんか?
実はあのタグ、各オープンチャットに1つだけしか付きません。
「え、複数のタグを付けたほうが便利じゃない?」「複数のテーマのオープンチャットもあるよね?」と思うかもしれません。でも、1つに絞ることには理由があるのです。
今回は、25万件以上のオープンチャットに自動でタグを付けるシステムの仕組みと、開発中の試行錯誤についてお話しします。
1. なぜ「1つだけ」なのか
オープンチャットの「キーワード羅列文化」
LINEオープンチャットには独特の文化があります。説明文にキーワードを大量に詰め込んで、検索でヒットしやすくするのです。
【グループの説明】
ポケモン好きが集まるグループです!
ポケモン ポケットモンスター ポケモンGO ポケモンカード ポケカ
ポケモンスリープ ポケモンユナイト ポケモンSV スカーレット
バイオレット ポケモンマスターズ ポケモンカフェ ピカチュウ...
気持ちはわかります。検索に引っかかりやすくしたいですよね。
でも、もしこれら全部をタグにしたらどうなるか?
複数タグの問題
仮に複数タグを許可すると:
- 「ポケモン」で検索 → 数万件ヒット(多すぎて探せない)
- 「ポケカ」タグと「ポケモン」タグが両方付く → 重複だらけ
- タグの意味が薄れる(全部に全部のタグが付くから)
1つに絞ることで、「このオープンチャットが一番強く関連するジャンル」がわかるようになります。
メリットとデメリット
| 1タグ制 | 複数タグ制 | |
|---|---|---|
| ✅ メリット | タグの意味が明確 / 分類が被らない / 表示がシンプル | 複数ジャンルに対応できる |
| ❌ デメリット | 複合ジャンルが表現しにくい | タグが氾濫する / 管理が複雑 |
オプチャグラフでは「探しやすさ」を優先して、1タグ制を採用しています。
2. 「早い者勝ち」の仕組み
先に付いたタグが勝つ
1つしかタグが付かないということは、どのタグを付けるかを決める必要があります。
オプチャグラフでは「先に処理されたタグが優先」というシンプルなルールを採用しています。
DB::execute(
"INSERT INTO recommend
SELECT oc.id, '{$tag}'
FROM (
SELECT oc.*
FROM open_chat AS oc
LEFT JOIN recommend AS t ON t.id = oc.id
WHERE t.id IS NULL -- ★ここがポイント!
) AS oc
WHERE {$search}"
);
WHERE t.id IS NULL — つまり「まだタグが付いていないオープンチャットだけ」が対象になります。
一度タグが付いたら、後続の処理では無視される。だから処理の順番が超重要なのです。
3. タグの優先順位
処理される順番
DB::transaction(function () {
$this->deleteRecommendTags('recommend'); // 1. まず全削除
$this->updateStrongestTags(); // 2. 最優先タグ
$this->updateBeforeCategory(); // 3. カテゴリ別優先タグ
$this->updateName(); // 4. 名前からタグ付け
$this->updateDescription('oc.name'); // 5. 説明文からタグ付け
$this->updateDescription(); // 6. 説明文からタグ付け(続き)
$this->modifyRecommendTags(); // 7. 手動修正を反映
});
上から順に処理されるので、上にあるほど優先度が高い。
なぜこの順番なのか
| 優先度 | 処理 | 説明 | 例 |
|---|---|---|---|
| 1 | 最優先タグ | 特殊なケースを最優先で処理 | — |
| 2 | カテゴリ別優先タグ | カテゴリごとに「これが来たら優先」を定義 | ゲームカテゴリで「荒野行動」「スプラトゥーン」 |
| 3 | 名前からのタグ | オープンチャット名に含まれるキーワードでタグ付け | 「原神」「あんスタ」「プロセカ」 |
| 4 | 説明文からのタグ | 説明文に含まれるキーワードでタグ付け | 名前に書いてなくても説明で判定 |
名前より説明文が後なのは、名前のほうが信頼できるから。
説明文はキーワード羅列で「盛る」ことができますが、オープンチャットの名前は実際の内容を反映していることが多いのです。
4. 具体例で見る優先度の仕組み
例1:ポケモン系オープンチャット
名前:「ポケカ交換&対戦グループ」
処理の流れ:
- 最優先タグ → 該当なし
- カテゴリ別 → 該当なし
- 名前から → 「ポケカ」にマッチ → ✅ 「ポケモンカード(ポケカ)」タグが付く
- 説明文から → (もう付いてるのでスキップ)
例2:複合的なオープンチャット
名前:「原神&崩壊スターレイル雑談」
処理の流れ:
- 最優先タグ → 該当なし
- カテゴリ別 → 該当なし
-
名前から → 「原神」にマッチ → ✅ 「原神」タグが付く
- (「崩壊スターレイル」もマッチするが、「原神」が先に定義されているので原神が勝つ)
タグ定義の並び順も重要なのです。
5. タグ定義という「全部手書き」の世界
345行、200以上のタグを手動管理
function getNameStrongTags(): array
{
return [
"御朱印",
"神社",
["IVE", ["IVE_AND_シリアル", "IVE_AND_波", "IVE_AND_当選"]],
["ひとり旅", ["一人旅", "ひとり旅", "1人旅"]],
["東京リベンジャーズ(東リベ)", ["東京リベンジャーズ_OR_東リベ_OR_東リべ"]],
["プロジェクトセカイ(プロセカ)", ["プロセカ"]],
["あんさんぶるスターズ!(あんスタ)", ["あんスタ_OR_あんさんぶるスターズ_OR_enst"]],
// ... 200個以上続く ...
];
}
これ、全部手書きです。
独自の検索構文
SQLのLIKE句を意識した独自構文を使っています:
| 書き方 | 意味 | 例 |
|---|---|---|
A_OR_B |
AまたはBを含む | 東京リベンジャーズ_OR_東リベ |
A_AND_B |
AとB両方を含む | 原神_AND_なりきり |
utfbin_X |
大文字小文字・絵文字を厳密に区別 | utfbin_Ado |
// これが...
["あんスタなりきり", ["あんスタ_AND_なりきり", "あんスタ_AND_也"]]
// こう変換される
// WHERE (name LIKE '%あんスタ%' AND name LIKE '%なりきり%')
// OR (name LIKE '%あんスタ%' AND name LIKE '%也%')
6. なぜこの方法を選んだのか
他の技術との比較
タグ付けの自動化には、いくつかの選択肢がありました。
| 方法 | メリット | デメリット |
|---|---|---|
| 形態素解析(MeCab等) | 日本語を正確に分割できる | 辞書のメンテナンスが大変 / 新語に弱い |
| 全文検索エンジン(Elasticsearch等) | 高速で柔軟な検索 | インフラ構築・運用コストが高い |
| SQLのLIKE句(採用) | シンプル / 追加インフラ不要 | 複雑なマッチングは苦手 |
オプチャグラフでは、シンプルさと運用コストの低さを優先してSQLのLIKE句を採用しました。
手書き定義のメリット
-
なぜそのタグが付いたか説明できる
- 「この検索条件にマッチしたから」と明確
- 問題があったときの調査が簡単
-
間違いをすぐ直せる
- タグ定義を1行変えるだけで修正完了
- デプロイすれば即反映
-
順番を細かくコントロールできる
- 1タグ制では順番が命
- 手書きだから並び替えも自由自在
-
新しいコンテンツにすぐ対応できる
- 新作ゲームが出たら1行追加するだけ
- 流行りの変化に即座に追従可能
7. 開発中の試行錯誤
ポケポケ事件:30分で4回の修正
2025年2月2日、「ポケポケ」(Pokémon TCG Pocket)のタグ定義でこんなことが起きました:
18:01 - タグを変更
18:01 - また変更(同じ時間に2回!)
18:11 - 「やっぱり違う」とRevert(巻き戻し)
18:28 - 改めて修正
30分で4回も変更しています。
何が難しかったのか
「ポケモン」関連のオープンチャットは種類が多い:
- ポケモン(本家ゲーム)
- ポケモンGO
- ポケモンカード(ポケカ)
- ポケモンスリープ
- ポケポケ(Pokémon TCG Pocket)
- ポケモンユナイト
- ポケモンチャンピオンズ
検索条件を少し間違えると、全部同じタグになってしまう。
例えば「ポケモン」で検索すると全部マッチする。「ポケカ」と「ポケポケ」は名前が似てるから混同しやすい。
// 最終的な定義(それぞれ別々に、順番を考慮して定義)
["ポケポケ(Pokémon TCG Pocket)", ["ポケポケ"]],
["ポケモンカード(ポケカ)", ["ポケモンカード_OR_ポケカ_OR_ダイキ様"]],
["ポケモンGO", ["ポケモンGO"]],
["ポケモンチャンピオンズ", ["ポケモンチャンピオンズ"]],
["ポケットモンスター(ポケモン)", ["ポケモン大好きチャット"]],
なりきり界隈の複雑さ
「なりきり」(キャラクターになりきって会話する文化)は、作品ごとにタグを分けています:
["原神なりきり", ["原神_AND_なりきり", "原神_AND_也", "原神_AND_nrkr"]],
["プロセカなりきり", ["プロセカ_AND_なりきり", "プロセカ_AND_也"]],
["あんスタなりきり", ["あんスタ_AND_なりきり", "あんスタ_AND_也"]],
["ハイキューなりきり", ["ハイキュー_AND_なりきり", "HQ_AND_也"]],
["なりきり", ["なりきり_OR_ぜんゆる_OR_nrkr_OR_緩也_OR_夢也"]], // 汎用
ポイント:
- 作品別なりきりタグを先に定義
- 汎用の「なりきり」タグは後に定義
- こうすることで「原神なりきり」は「原神なりきり」タグが付き、どの作品かわからないものだけ「なりきり」タグが付く
8. まとめ:シンプルな仕組みで複雑な問題を解く
全体の流れ
オープンチャット登録/更新
↓
タグを一度削除
↓
優先度順にタグ処理
↓
すでにタグが付いていたらスキップ
↓
最終的に1つだけタグが付く
この設計の特徴
| 要素 | 内容 |
|---|---|
| タグ数 | 1オープンチャット = 1タグ |
| 優先度 | 処理順で決まる(早い者勝ち) |
| 定義方法 | 345行の手書きPHP |
| 検索 | SQLのLIKE句を動的生成 |
これがオプチャグラフの「力技」タグ付けシステムです。
15万件のオープンチャットを、200以上の手書きタグ定義と、処理順という単純なルールで分類しています。
シリーズ記事
- オプチャグラフ開発記1:LINE OpenChatの分析サービスを作ったら50万PVになった話
- オプチャグラフ開発記2:お一人様フレームワークを作った理由
- オプチャグラフ開発記3:未経験から始めた開発で自作PHPフレームワークに辿り着くまで
- オプチャグラフ開発記4:15万オープンチャットを収集するクローリングシステム

