AI に記事やレポートを毎日作らせている 1 人目のデータ担当の方向けに、AI 生成の重複チェックを「台帳との照合」で作った記録を紹介します。読み終わると、人が中身を読まなくても、同じ内容の成果物を二度作らない仕組みを小さく組めるようになります。
題材は、筆者がこのサイトの運用で試した記録です。AI が毎日記事を書く仕組みに、公開済みの記事と重なる企画を機械で止める工程を足しました。その途中で、照合が「止めすぎる」穴と「見逃す」穴の 2 つにつまずきました。
ここで扱うのは、AI が書く前の企画を公開済みの記事の台帳と照らし合わせる仕組みで、書き終えた文章の類似度やコピペを調べるツールではありません。
この記事で分かること
- AI の成果物の重複を、公開済みの成果物の 台帳(一覧表のこと。ここでは記事ごとの主キーワードなどを 1 行ずつ並べたもの)との照合で止める構成
- 照合の 鍵(2 つの成果物が「同じ」かを判定するために比べる値)を、主キーワードと検索意図にした理由
- 空の値どうしが一致して止まる穴と、全角と半角の違いで一致しない穴の直し方
- Unicode の正規化 4 形式で、表記違い 10 例がどこまで同じ語になるかを試した結果
何が起きていたか:企画の工程がタイトルしか知らなかった
このサイトでは、AI が企画・調査・執筆を分担して記事を作っています。最初の工程である企画の担当は、既存記事のタイトルしか知りませんでした。そのため、同じ検索の問いに答える記事が重なるおそれがありました。
ペースは 1 日 6 本です。同じ問いに答える記事が重なると、検索結果で互いの順位を奪い合うおそれがあります。この状態を カニバリゼーション(同じサイトの記事どうしが、同じ検索語で競合してしまうこと)と呼びます。
タイトルは書き方の幅が広く、同じ問いでも言い回しが変わります。そこで、記事ごとに「主キーワード・検索意図・答える問い」を書き残し、新しい企画をそれと照らし合わせることにしました。
構成図:公開済みの成果物から台帳を作り、企画と照合する
全体の流れは図 1 のとおりです。公開済みの記事の frontmatter(記事ファイルの先頭に書く、タイトルや日付などの設定の欄)から台帳を作り、企画の担当に渡し、企画の後に機械で照合します。
台帳に入れる項目と、その役割を表にまとめます。
| 項目 | 役割 |
|---|---|
| slug | 記事を見分ける名前 |
| タイトル | 古い記事の照合の手がかり |
| 主キーワード | 照合の鍵の 1 つ目 |
| 検索意図 | 照合の鍵の 2 つ目 |
| 答える問い | 企画の担当が読む説明 |
台帳を集める処理は、下書き(draft: true)の記事を入れません。台帳に載るのは、公開済みの記事だけです。
frontmatter の読み取りには、サイト本体と同じ YAML の読み込み部品である js-yaml を使いました。最初は 1 行ずつ正規表現で読んでいましたが、字下げした値や、引用符の中の「#」を読み違えることがレビューで分かったためです。
止めた企画は捨てずに overlaps(重なった企画の記録)に残し、既存記事を更新する候補として報告します。ただし、既存記事の更新までは自動で行いません。
照合の鍵をどう決めたか:主キーワードと検索意図
照合の鍵には「主キーワードの語の集合」と「検索意図の 4 分類」の両方が同じことを選びました。4 分類は「情報収集」「比較検討」「購入・申込」「特定サイト」です。
鍵を広げて「主キーワードだけ」にすると、同じ語を使っていても別の問いに答える記事まで止まります。たとえば同じ語でも、やり方を知りたい記事と、製品を比べたい記事は別の読者の役に立ちます。重複チェックの鍵は、同じ読者の同じ問いに答えるかを表す値の組にすると、止めすぎを防げます。
では、鍵の決め方で止まる件数はどう変わるのでしょうか。筆者が 2026-11-13 時点の台帳(公開済み 81 本)で、記事どうしの組のうち鍵が一致したものを数えました。
図 2 の値を表にも載せます。
| 鍵 | 空の鍵を外す | 外さない |
|---|---|---|
| タイトルだけ | 0 組 | 0 組 |
| 主キーワードだけ | 0 組 | 2,701 組 |
| 主キーワードと検索意図 | 0 組 | 2,701 組 |
空の鍵を外すと、3 通りのどれでも 0 組でした。台帳の 81 本のうち主キーワードを持つ記事は 7 本だけで、筆者の計算では 8.6% です。主キーワードを持つ 7 本どうしはどれも語が違うため、今の台帳では鍵の違いによる差が出ませんでした。
そのため、主キーワードと検索意図を選んだ根拠は、件数の差ではありません。「広げると別の問いの記事まで止まる」という設計上の理由です。記事が増えて主キーワード付きの行が増えると、差が出てくると考えられます。
つまずき 1:空の値どうしが一致して止まる
レビューで最初に見つかった穴は、空白だけの主キーワードどうしが「一致」と判定されることでした。検索意図が 4 分類のどれにも当たらない「分類なし」どうしも、同じように一致していました。
図 2 の「外さない」の列が、この穴を台帳で再現した数です。主キーワードを持たない古い記事は 74 本あり、空どうしがすべて一致すると 74 × 73 ÷ 2 = 2,701 組になります。これは台帳の中の組を筆者が数えた模擬計算で、実際に止めた企画の数ではありません。
もし直さずに動かしていたら、主キーワードが空白だけで、検索意図も分類なしの企画は、主キーワードを持たない古い記事 74 本のどれとも「重なる」と判定されたと考えられます。空の値は「同じ値」ではなく「値が無い」ことを表すので、比べる対象から外す必要がありました。
直した後の判定の順番は図 3 のとおりです。先に鍵を作り、鍵が空か、検索意図が 4 分類に当たらない企画は照合せずに通します。
鍵を作る前に空かどうかを調べると、空白だけの主キーワードは「空ではない」と判定されてしまいます。空白だけの文字列は、空白で分けて空の要素を除いた後に、はじめて空になるためです。空の値の判定は、表記をそろえて鍵を作った後に行うと、空白だけの値も取りこぼしません。
つまずき 2:全角と半角の違いで一致しない
2 つ目の穴は逆向きで、同じ主キーワードなのに一致しないことでした。全角の「GA4」と半角の「GA4」が、別の語として扱われていたためです。
直し方は、比べる前に Unicode の 正規化(見た目や意味が同じなのに符号が違う文字を、決まった形にそろえること)をかけることです。Unicode の仕様書(UAX #15)は、文字列を正規化形式の 1 つに変換してから比べれば等価かを判定できると説明しています(Unicode Standard Annex #15)。
正規化の形式のうち NFKC は、互換等価な文字の違い(全角と半角のように、多くの場面で区別しなくてよい違い)をならします。仕様書は、半角と全角のカタカナが同じ文字列に正規化される例を挙げています(UAX #15)。
今の鍵の作り方は、次の 1 行です。NFKC で正規化し、小文字にし、空白で分け、空の要素と重複を除き、並べ替えてから空白でつなぎます。
const keywordKey = (text) => [...new Set(String(text || '').normalize('NFKC').toLowerCase().split(/\s+/).filter(Boolean))].sort().join(' ')
穴を直した後は、テストで次の 2 つを固定しました。空白(半角・全角)だけの主キーワード 2 件と、検索意図が 4 分類に当たらない 1 件は、どれも止まりません。全角の「GA4 料金」と半角の「GA4 料金」は、4 分類のどれでも 1 件止まります。
| 入力 | 直す前 | 直した後 |
|---|---|---|
| 空白だけの主キーワード | 一致して止まる | 照合しない |
| 検索意図が分類なし | 一致して止まる | 照合しない |
| 全角と半角の GA4 料金 | 一致しない | 一致して止まる |
「直す前」の列は、レビューの指摘と修正の記録に書いた挙動と、上の 2,701 組の模擬計算によるものです。直す前のコードでテストを動かした結果ではありません。
正規化 4 形式で表記違い 10 例を試した結果
NFKC だけで足りるのかを確かめるため、筆者が Node.js v20.20.2 で表記違い 10 例を試しました。JavaScript の normalize() は NFC・NFD・NFKC・NFKD の 4 形式を受け取り、省略すると NFC を使います(MDN: String.prototype.normalize())。
4 形式の違いを先に整理します。MDN によると、NFC と NFD は 正準等価性(見た目も意味も完全に同じ文字の違い)で、NFKC と NFKD は 互換等価性(全角と半角のように、意味は同じで見た目が違う文字の違い)で分解します。
| 形式 | 分解の基準 | 最後に合成するか |
|---|---|---|
| NFC | 正準等価 | する |
| NFD | 正準等価 | しない |
| NFKC | 互換等価 | する |
| NFKD | 互換等価 | しない |
比べ方は、基準の語と表記違いの語の両方に同じ形式をかけ、結果が完全に同じかを見るだけです。
const same = (a, b, form) => a.normalize(form) === b.normalize(form)
same('GA4 レポート', 'GA4 レポート', 'NFKC') // true
same('GA4 レポート', 'GA4 レポート', 'NFC') // false
基準の語は「GA4 レポート」で、カナの 2 例だけは「ガイド」を基準にしました。結果を図 4 にまとめます。
10 例それぞれの結果は次の表のとおりです。「○」が一致、「×」が不一致で、右端の列は前の節の keywordKey の結果です。
| 表記違い | NFC | NFD | NFKC | NFKD | 鍵 |
|---|---|---|---|---|---|
| 全角英数字 | × | × | ○ | ○ | ○ |
| 全角の空白 | × | × | ○ | ○ | ○ |
| 前後の空白 | × | × | × | × | ○ |
| 連続した空白 | × | × | × | × | ○ |
| 小文字 | × | × | × | × | ○ |
| 半角カナ | × | × | ○ | ○ | ○ |
| 濁点の分解 | ○ | ○ | ○ | ○ | ○ |
| 語の順番 | × | × | × | × | ○ |
| 全角アンダーバー | × | × | × | × | × |
| 丸数字 | × | × | ○ | ○ | ○ |
表の中の例は、文字にすると見分けにくいものがあるので補足します。
- 全角の空白: 「GA4」と「レポート」の間が全角の空白(U+3000)
- 前後の空白: 語の前と後ろに半角の空白が 2 つずつ
- 濁点の分解: 「ガ」を「カ」と結合用の濁点(U+3099)の 2 文字で書いたもの
- 全角アンダーバー: 「GA4_レポート」のように、空白の代わりに「_」を挟んだもの
- 丸数字: 「GA④ レポート」
筆者が表から読み取ったことは 3 つです。1 つ目に、NFKC と NFKD は NFC より 4 例多く吸収しました(全角英数字・全角の空白・半角カナ・丸数字)。
2 つ目に、濁点の分解は NFC でも一致しました。濁点の分解は正準等価の違いなので、NFKC だけの効果ではありません。
3 つ目に、正規化だけでは、前後の空白・連続した空白・小文字・語の順番の 4 例を吸収できませんでした。表記の違いを吸収するには、NFKC に加えて小文字化・空白での分割・並べ替えまで重ねて鍵を作る必要があります。
データパイプラインの重複排除との対応
ここまでの考え方は、データパイプラインで重複した行を取り除く 重複排除(同じ内容の行を 1 つにまとめる処理)と同じ形をしています。筆者の対応づけを表にまとめます。
| 台帳の照合 | 重複排除での考え方 |
|---|---|
| 主キーワードと検索意図 | 重複を判定する鍵を決める |
| NFKC・小文字・分割 | 比べる前に表記をそろえる |
| 空の鍵を照合しない | 空の値を同じ値と扱わない |
| overlaps に残す | 落とした行を捨てずに記録する |
| タイトルだけの古い記事 | 鍵の無い行は別に扱う |
たとえば顧客データの名寄せでも、メールアドレスの大文字と小文字や、前後の空白をそろえてから比べます。段階ごとの考え方は、メールアドレスの名寄せを 4 段階に分けて整理した記事で詳しく扱っています。
同じ処理を何度動かしても結果が変わらないようにする考え方は、データパイプラインの冪等性でつまずいた構築ログも参考になります。重複チェックは「同じものを二度作らない」ための仕組みなので、冪等性と組み合わせて考えると抜けを見つけやすくなります。
つまずきやすい点
ここでは、作ってみて分かった注意点と、まだ残っている穴をまとめます。
全角アンダーバーは今の鍵でも一致しません
NFKC で全角の「_」は半角の「_」になりますが、空白ではないので語が分かれません。そのため「GA4_レポート」と「GA4 レポート」は、どの形式でも、今の鍵でも別の語になりました。
古い記事はタイトルでしか照合できません
台帳の 3 項目は省略できる欄にしたので、項目の無い古い記事もそのまま表示されます。古い記事は title だけが台帳に入り、企画の担当(AI)がタイトルから判断します。
機械の照合は完全一致だけです
文章の 類似度(2 つの文章がどれだけ似ているかを数値にしたもの)を測って止める方法や、AI が書いた文章かを見分ける生成AIチェッカーは、この記事では扱っていません。言い回しの違う同じ問いは、企画の担当が台帳を読んで避ける前提です。
実際に止めた記録はまだありません
この仕組みを入れた後の毎日の実行で、台帳が企画を止めた実績は、執筆の時点で確認できていません。テストでは止まることを確かめていますが、本番で止まった件数は今後の記録を待つ必要があります。
品質の検査を機械で行う考え方は、AI 生成記事の品質ゲートを CI で検証した記事でも扱っています。重複チェックも品質ゲートの 1 つとして、ほかの検査と同じ場所に並べると管理しやすくなります。
まとめ
- AI の成果物の重複は、公開済みの成果物から作った台帳と、主キーワードと検索意図の組で照合すると、人が読まずに止められます。
- 空の値は「同じ値」ではないので、表記をそろえて鍵を作った後に空かどうかを調べ、空なら照合から外します。
- 表記の違いは NFKC だけでは 10 例中 5 例しか吸収できず、小文字化・分割・並べ替えまで重ねた鍵で 9 例になりました。
自分の環境で AI の成果物の重複チェックや台帳の作りを相談したい方は、お問い合わせからご連絡ください。重複排除や名寄せの SQL の確認に不安がある場合は、BigQuery SQL レビューパックも用意しています。
参考にした一次情報
いずれも 2026-11-13 時点で内容を確認しました。
- Unicode Standard Annex #15: Unicode Normalization Forms(正規化形式に変換してから比べる考え方、NFKC が互換等価な文字の違いをならすこと)
- MDN Web Docs: String.prototype.normalize()(4 形式の指定と、省略時に NFC を使うこと)
- nodeca/js-yaml(frontmatter の読み取りに使った YAML の読み込み部品の配布元)