deduplication について
複数ソースから集めたデータを重複排除する処理スキルです。意味的に似た内容をグループ化し、ソースの評判やレコードの完全性などに基づいて代表データを選びます。
主な機能
- 01ID による重複処理
- 02意味的な内容のグループ化
- 03代表レコードの選択
- 04ソースの評判スコアリング
- 05重複排除の削減指標
活用シーン
- 01複数ソースのフィードから重複項目を除去
- 02同じニュース記事の代表版を選択
- 03ID が衝突した際に最も完全なレコードを保持
インストール
コマンドをコピーしてターミナルで実行します。
$
npx skills add https:/ / github.com/ dadbodgeoff/ drift --skill deduplicationインストール後、Claude Code で/deduplicationを入力して読み込まれたか確認します。
詳細
イベントの重複排除
複数ソースのデータを、評価スコアとハッシュベースのグループ化によって正規版に選定します。
このスキルを使う場面
- 複数のソース(ニュース、イベント、商品など)のデータを集約する場合
- 異なる媒体やソースから同じコンテンツが配信されている場合
- 重複の中から「最適な」バージョンを選ぶ必要がある場合
- 最適化のために重複排除の指標を追跡する場合
主な概念
単純な URL の重複排除だけでは不十分です。本番環境では、次のことが必要です。
- 意味的類似性によるグループ化(同じ話題を扱う異なる媒体の記事)
- 正規版の選定(「最適な」バージョンを選ぶ)
- 評価スコアの付与(信頼性の高いソースを優先する)
- ID ベースとコンテンツベースの両方の重複排除
モードは 2 つあります。
- ID ベース:ソースに一意の ID がある場合、ID が重複したときに「最適な」バージョンを残します
- コンテンツベース:意味的類似性でグループ化し、各グループから正規版を選びます
実装
TypeScript
import { createHash } from 'crypto';
interface DeduplicationResult<T> {
items: T[];
originalCount: number;
dedupedCount: number;
reductionPercent: number;
duplicateGroups?: number;
}
// ============================================
// ID-Based Deduplication
// ============================================
function deduplicateById<T extends { id: string }>(
items: T[],
preferFn: (existing: T, candidate: T) => T
): DeduplicationResult<T> {
const seen = new Map<string, T>();
for (const item of items) {
const existing = seen.get(item.id);
if (existing) {
seen.set(item.id, preferFn(existing, item));
} else {
seen.set(item.id, item);
}
}
const dedupedItems = Array.from(seen.values());
const reductionPercent = items.length > 0
? Math.round((1 - dedupedItems.length / items.length) * 100)
: 0;
return {
items: dedupedItems,
originalCount: items.length,
dedupedCount: dedupedItems.length,
reductionPercent,
};
}
// ============================================
// Content-Based Deduplication
// ============================================
interface Article {
title: string;
url: string;
domain: string;
publishedAt: string;
tone?: number;
}
/**
* Generate deduplication key from content
* Groups by: normalized title + source country + date
*/
function generateDedupKey(article: Article): string {
const normalizedTitle = article.title
.toLowerCase()
.replace(/[^\w\s]/g, '')
.trim()
.slice(0, 50);
const dateStr = article.publishedAt?.slice(0, 10).replace(/-/g, '') || 'unknown';
return `${normalizedTitle}|${dateStr}`;
}
/**
* Generate unique ID from URL
*/
function generateEventId(url: string): string {
return createHash('md5').update(url).digest('hex').slice(0, 12);
}
/**
* Source reputation scoring
*/
function getReputationScore(domain: string): number {
// Tier 1: Wire services and major international
const tier1 = ['reuters.com', 'apnews.com', 'bbc.com', 'bbc.co.uk',
'aljazeera.com', 'france24.com', 'dw.com'];
if (tier1.some(r => domain.includes(r))) return 100;
// Tier 2: Major newspapers
const tier2 = ['nytimes.com', 'washingtonpost.com', 'theguardian.com',
'ft.com', 'economist.com', 'wsj.com'];
if (tier2.some(r => domain.includes(r))) return 75;
// Tier 3: Regional/national
const tier3 = ['cnn.com', 'foxnews.com', 'nbcnews.com', 'abcnews.go.com'];
if (tier3.some(r => domain.includes(r))) return 50;
return 10;
}
/**
* Select canonical article from duplicate group
*/
function selectCanonical<T extends Article>(
group: { item: T; source: string }[]
): { item: T; source: string } {
return group.reduce((best, current) => {
const bestScore = getReputationScore(best.item.domain) +
Math.abs(best.item.tone || 0);
const currentScore = getReputationScore(current.item.domain) +
Math.abs(current.item.tone || 0);
return currentScore > bestScore ? current : best;
});
}
/**
* Deduplicate articles from multiple sources
*/
function deduplicateArticles<T extends Article>(
sourceResults: { sourceName: string; articles: T[] }[]
): DeduplicationResult<T & { source: string }> {
const groups = new Map<string, { item: T; source: string }[]>();
let totalArticles = 0;
// Group articles by dedup key
for (const { sourceName, articles } of sourceResults) {
for (const article of articles) {
totalArticles++;
const key = generateDedupKey(article);
if (!groups.has(key)) {
groups.set(key, []);
}
groups.get(key)!.push({ item: article, source: sourceName });
}
}
// Select canonical article from each group
const items: (T & { source: string })[] = [];
for (const group of groups.values()) {
const canonical = selectCanonical(group);
items.push({ ...canonical.item, source: canonical.source });
}
const reductionPercent = totalArticles > 0
? Math.round((1 - items.length / totalArticles) * 100)
: 0;
console.log(`[Dedup] ${totalArticles} → ${items.length} (${reductionPercent}% reduction)`);
return {
items,
originalCount: totalArticles,
dedupedCount: items.length,
reductionPercent,
duplicateGroups: groups.size,
};
}
使用例
ID ベースの重複排除
const events = await fetchEvents();
const result = deduplicateById(events, (existing, candidate) => {
// Prefer events with coordinates
if (!existing.lat && candidate.lat) return candidate;
// Prefer higher sentiment magnitude
if (Math.abs(candidate.sentiment) > Math.abs(existing.sentiment)) {
return candidate;
}
return existing;
});
console.log(`Reduced ${result.reductionPercent}% duplicates`);
複数ソースの集約
const results = await Promise.all([
fetchFromSourceA(),
fetchFromSourceB(),
fetchFromSourceC(),
]);
const { items, reductionPercent } = deduplicateArticles([
{ sourceName: 'source-a', articles: results[0] },
{ sourceName: 'source-b', articles: results[1] },
{ sourceName: 'source-c', articles: results[2] },
]);
// items now contains canonical articles with source attribution
ベストプラクティス
- 意味的グループ化 - URL だけでなく、正規化したコンテンツに基づいてグループ化します
- 評価スコアの付与 - 信頼性の高いソースを正規版として優先します
- 最適なバージョンの選定 - ID が重複したときは、最も多くのデータを含むバージョンを残します
- 削減量の追跡 - 重複排除による削減効果を記録します
- ソースの帰属情報 - 正規版の提供元を追跡します
よくある間違い
- 単純な URL の重複排除(異なる媒体の同じ話題を見逃します)
- 重複の中から無作為に選ぶ(品質を示す情報が失われます)
- 正規化しない(大文字・小文字や句読点の違いによって、重複を見逃します)
- 削減指標を追跡しない(最適化できません)
- ソースリストをハードコードする(設定可能にしてください)
関連パターン
- batch-processing - 重複排除した項目を効率的に処理します
- validation-quarantine - 重複排除の前に検証します
- checkpoint-resume - 重複排除済みのファイルを追跡します
コメント
Markdown 対応