mcpField
D
deduplication

deduplication

github.com/dadbodgeoffデータ・データベース79248 回インストール10 週間前に更新
リポジトリ

deduplication について

複数ソースから集めたデータを重複排除する処理スキルです。意味的に似た内容をグループ化し、ソースの評判やレコードの完全性などに基づいて代表データを選びます。

主な機能

  1. 01ID による重複処理
  2. 02意味的な内容のグループ化
  3. 03代表レコードの選択
  4. 04ソースの評判スコアリング
  5. 05重複排除の削減指標

活用シーン

  1. 01複数ソースのフィードから重複項目を除去
  2. 02同じニュース記事の代表版を選択
  3. 03ID が衝突した際に最も完全なレコードを保持

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/dadbodgeoff/drift --skill deduplication

インストール後、Claude Code で/deduplicationを入力して読み込まれたか確認します。

詳細

イベントの重複排除

複数ソースのデータを、評価スコアとハッシュベースのグループ化によって正規版に選定します。

このスキルを使う場面

  • 複数のソース(ニュース、イベント、商品など)のデータを集約する場合
  • 異なる媒体やソースから同じコンテンツが配信されている場合
  • 重複の中から「最適な」バージョンを選ぶ必要がある場合
  • 最適化のために重複排除の指標を追跡する場合

主な概念

単純な URL の重複排除だけでは不十分です。本番環境では、次のことが必要です。

  • 意味的類似性によるグループ化(同じ話題を扱う異なる媒体の記事)
  • 正規版の選定(「最適な」バージョンを選ぶ)
  • 評価スコアの付与(信頼性の高いソースを優先する)
  • ID ベースとコンテンツベースの両方の重複排除

モードは 2 つあります。

  1. ID ベース:ソースに一意の ID がある場合、ID が重複したときに「最適な」バージョンを残します
  2. コンテンツベース:意味的類似性でグループ化し、各グループから正規版を選びます

実装

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

ベストプラクティス

  1. 意味的グループ化 - URL だけでなく、正規化したコンテンツに基づいてグループ化します
  2. 評価スコアの付与 - 信頼性の高いソースを正規版として優先します
  3. 最適なバージョンの選定 - ID が重複したときは、最も多くのデータを含むバージョンを残します
  4. 削減量の追跡 - 重複排除による削減効果を記録します
  5. ソースの帰属情報 - 正規版の提供元を追跡します

よくある間違い

  • 単純な URL の重複排除(異なる媒体の同じ話題を見逃します)
  • 重複の中から無作為に選ぶ(品質を示す情報が失われます)
  • 正規化しない(大文字・小文字や句読点の違いによって、重複を見逃します)
  • 削減指標を追跡しない(最適化できません)
  • ソースリストをハードコードする(設定可能にしてください)

関連パターン

  • batch-processing - 重複排除した項目を効率的に処理します
  • validation-quarantine - 重複排除の前に検証します
  • checkpoint-resume - 重複排除済みのファイルを追跡します

コメント