kintone
投稿日:

“重複応募”を洗い出す!kintoneに「重複チェック」機能を実装(プラグイン付)

“重複応募”を洗い出す!kintoneに「重複チェック」機能を実装する方法

登録時のチェックだけでは防げない“重複してしまうレコード”

皆さんこんにちは。kintoneアプリエンジニアのtomiokaです。今回もお客様から相談をよく受ける、「現場あるある」系のお話ですが、データが重複してしまう問題について書いてみます。

予約受付やイベント受付といった、「受付をする」という現場では、「応募者が同じ人なのに別レコードで登録されている」という問題がほぼ必ずと言っていいほど発生します。重複して受け付けても問題ないような企画ならよいのですが、ほとんどのケースは重複はなんとか防ぎたいですよね。

そこで登録の段階で、登録済みの人は弾いてしまおうということになるんですが、例えばkintoneのフィールド設定でメールアドレスをユニーク設定にしていても、別のメールアドレスを使って登録してしまえば防げません。さらに、他のシステムで受け付けた内容をCSVでインポートしたり、紙での応募を受け付けていたりなど、複数の経路から登録されることもあり、気づけば同一人物が複数件。。。ということになりやすいんです。
結果、来場者の管理や予約を捌いていく時に目視で確認しなければならなくなり、とても使いづらく、ミスが出やすいデータ管理になってしまいます。

この記事では、そうした「登録時では防げない重複」について、実際の案件で利用した日次運用で検出する実践的な方法を紹介します。

そもそも「登録時」は「重複を防ぐ」タイミングではない。

kintoneの標準機能では、特定フィールドに「ユニーク」を設定することで重複した値を登録できないようにできます。
しかし、次のようなケースでは防ぎ切れません。

ケース結果
別メールアドレスtanaka@example.comtanaka.work@gmail.com同一人物が別レコード化
CSVインポート別担当が同じ候補者を取り込むキーになる項目が違ったりなかったりで二重登録
紙起票の転記手入力で表記ゆれ・“山田太郎”と“山田 太郎”が別人扱い
・住所の番地を”1丁目2番3号”と書くのか、”1-2-3″と書くか

つまり、「登録時に防ぐ」よりも「登録後に洗い出す」方が現実的なのです。

重複チェックを“業務習慣”として運用に組み込む

イベント受付や予約受付では、「応募者情報が複数の経路で入ってくる」ことが前提です。

  • 別メディア経由の応募と直接応募で、同じ候補者が別レコード化
  • 紙の応募票を後日CSVでインポート
  • 同じ人が複数のイベントに分けて登録される

こうした構造的重複は、どんな制御ルールを設けても完全には防げません。
だからこそ重要なのは「重複登録を防ぐこと」ではなく、日次の運用で“重複を検出する”フローを作ることです。

弊社の過去のプロジェクトでは、そんな重複登録を検出するための、「重複チェック」ボタンを提案いたしました。機能は、担当者が毎朝クリックするだけで重複候補が一覧化されます。重複チェックが日常業務に自然に溶け込む設計が狙いの機能となります。

実装の流れとコードサンプル

*コード全体を含むプラグインは、本記事末尾からダウンロードいただけます。

アプリのフィールド構成例

例えば以下のような受付管理アプリがあったとして、重複チェック機能を考えていきましょう。

フィールド名フィールドコード種類
氏名name文字列(1行)
フリガナname_kana文字列(1行)
メールアドレスemail文字列(1行)
電話番号tel文字列(1行)
生年月日dob日付

“データのゆれ”に対応した重複チェック

実際に登録されるデータを考えると、こういう“ゆれ”にも対応していかないといけません。

ゆれ問題
複数メールアドレスtanaka@icloud.comtanaka.work@gmail.com同一人物が別登録
名前の揺れ「山田 太郎」「山田太郎」「ヤマダタロウ」表記差で検出漏れ
電話フォーマット差090-1234-567809012345678不一致判定
誤変換・旧字体「一郎」「一朗」同一人物を別扱い

これらを何の基準を持って重複していると考えるかは、実際にはプロジェクトそれぞれにはなりますが、ここではよくある基準として、次の3つを使ってみたいと思います。

  1. 厳密(=絶対に重複してます):email 完全一致
  2. 準一致(=たぶん重複していると思います):name_kana + dob(ふりがな+生年月日)
  3. あいまい(=重複している、、かな):bigramSimilarity(name) ≥ 0.90 AND 電話末尾4桁一致

「あいまい」な基準で使っている、90%というしきい値は、スペース・旧字体・軽微な誤変換を許容しつつ、同姓同名の別人を拾いにくいバランス値です。つまり人が目視した時と同じような、同じ名前ですよね、且つ 電話番号も同じですよね。という基準を表しています。

例)「あいまい」基準

比較類似度電話末尾判定
山田太郎 vs 山田 太郎1.001234/1234✅ 重複候補
田中一郎 vs 田中一朗0.925678/5678✅ 重複候補
サトウハナコ vs 佐藤花子0.881234/1234❌ 類似度不足

コードの動作はシンプルです。ボタンを押せば、kintoneのJavaScript APIを経由して対象アプリのレコードを読み込みます。その後「重複しているかどうか」のチェックを行い、重複レコードを表示するという流れです。

揺れがあってもチェックできるコードサンプル

*コード全体を含むプラグインは本記事末尾からダウンロードいただけます。

メインとなるのがこの関数です。3つのチェックを順番に実行してここの条件を変えれば、いろいろなプロジェクトに応用できますね。

  function buildDuplicateGroups(records) {
    const groups = [];
    const byEmail = new Map();
    const byKanaDob = new Map();

    // 1) 厳密一致: email
    for (const r of records) {
      const email = String(r.email?.value || '').trim().toLowerCase();
      if (!email) continue;
      if (!byEmail.has(email)) byEmail.set(email, []);
      byEmail.get(email).push(r);
    }
    for (const arr of byEmail.values()) if (arr.length > 1) groups.push({ rule: 'email', members: arr });

    // 2) 準一致: name_kana + dob
    for (const r of records) {
      const kana = normalizeKana(r.name_kana?.value || '');
      const dob  = String(r.dob?.value || '');
      if (!kana || !dob) continue;
      const key = `${kana}__${dob}`;
      if (!byKanaDob.has(key)) byKanaDob.set(key, []);
      byKanaDob.get(key).push(r);
    }
    for (const arr of byKanaDob.values()) if (arr.length > 1) groups.push({ rule: 'kana+dob', members: arr });

    // 3) あいまい: name 類似 >= 0.90 && tel 下4桁一致
    const seen = new Set();
    for (let i = 0; i < records.length; i++) {
      for (let j = i + 1; j < records.length; j++) {
        const a = records[i], b = records[j];
        const nameA = (a.name?.value || '').trim();
        const nameB = (b.name?.value || '').trim();
        if (!nameA || !nameB) continue;
        const sim = bigramSimilarity(nameA, nameB);
        if (sim >= 0.90) {
          const tA = normalizePhone(a.tel?.value || '');
          const tB = normalizePhone(b.tel?.value || '');
          const last4A = tA.slice(-4), last4B = tB.slice(-4);
          if (last4A && last4B && last4A === last4B) {
            const key = [a.$id.value, b.$id.value].sort().join('-');
            if (!seen.has(key)) {
              groups.push({ rule: 'fuzzy(name)+tel4', members: [a, b], score: sim });
              seen.add(key);
            }
          }
        }
      }
    }
    return groups;
  }

特に「ゆれ」に対応する部分は細かいコードが必要です。以下は3つ目のチェック基準になる、名前の表記揺れに対応したチェック用のコードサンプルです。本来はもっと詳細に書かないと全ての名前をカバーしたとは言えませんが、雰囲気はこういうコードになると思います。

  function bigramSimilarity(a = '', b = '') {
    // 類似文字の正規化マップ(旧字体、異体字、よくある表記揺れ)
    const similarChars = {
      // 「郎」「朗」系
      '朗': '郎', '廊': '郎',
      // 「沢」「澤」系
      '澤': '沢', '泽': '沢',
      // 「斉」「斎」「齋」系
      '斎': '斉', '齋': '斉', '齊': '斉',
      // 「辺」「邊」系
      '邊': '辺', '邉': '辺',
      // 「高」「髙」系
      '髙': '高', '﨑': '崎',
      // 数字
      '1': '1', '2': '2', '3': '3', '4': '4', '5': '5',
      '6': '6', '7': '7', '8': '8', '9': '9', '0': '0',
      // その他よくある揺れ
      '衞': '衛', '國': '国', '實': '実', '淸': '清',
      '濵': '浜', '濱': '浜', '渕': '淵', '嶋': '島'
    };

    // 前処理関数
    const normalize = str => {
      // 1. 空白・スペースを完全除去
      str = String(str).replace(/[\s ]/g, '');
      // 2. カタカナをひらがなに統一
      str = str.replace(/[\u30A1-\u30F6]/g, c => String.fromCharCode(c.charCodeAt(0) - 0x60));
      // 3. 類似文字を正規化
      str = str.split('').map(c => similarChars[c] || c).join('');
      // 4. 小文字化(英数字)
      str = str.toLowerCase();
      return str;
    };

    a = normalize(a);
    b = normalize(b);

    // 完全一致チェック
    if (a === b) return 1;

    // 長さチェック(正規化後に再度確認)
    if (a.length < 2 || b.length < 2) {
      // 1文字同士の比較は完全一致のみ
      return a === b ? 1 : 0;
    }

    const bigrams = str => new Map(
      Array.from({ length: str.length - 1 }, (_, i) => str.slice(i, i + 2))
        .reduce((m, bg) => m.set(bg, (m.get(bg) || 0) + 1), new Map())
    );
    const A = bigrams(a), B = bigrams(b);
    let overlap = 0;
    for (const [bg, cnt] of A) overlap += Math.min(cnt, B.get(bg) || 0);
    const sizeA = [...A.values()].reduce((s, v) => s + v, 0);
    const sizeB = [...B.values()].reduce((s, v) => s + v, 0);
    return (2 * overlap) / (sizeA + sizeB);
  }

実際に導入してみた

実際にこの「重複チェック」機能を導入した人材派遣会社の現場では、次のような成果が確認されました。

  • 初月で重複率が約30%減少
    以前は同一人物の二重登録が頻発していたが、日次チェックで早期発見できるようになった。
  • 担当者の心理的負担が軽減
    「重複を見逃していたらどうしよう」という不安がなくなり、チーム全体で“データを見る安心感”が生まれた。
  • 名寄せ作業の効率化
    従来はExcelで手動照合していたが、ボタン一つで候補を絞り込めるため、
    名寄せ工数が1/3に削減された。

何より大きかったのは、「登録時に神経質にならなくても後で拾える」という心理的余裕が生まれたこと。
これにより、現場の入力スピードと運用継続性が向上しました。

運用業務に取り込むためのポイント

運用を続ける中で、次の3つを意識すると運用に馴染みやすく、また継続しやすくなります。

① 権限設計を明確にする
重複チェックは、原則として「管理者ロール」または「品質管理担当者」が実行するようにしましょう。
全員が押せる状態にすると、確認責任の所在が曖昧になります。

② しきい値と条件は現場に合わせて調整する
名前の類似度(0.9)や電話番号条件は、データ傾向に応じて見直せます。
例えば応募者の年齢層や媒体によって、フリガナ誤記の頻度も異なるため、
一律ではなく現場データを見ながら最適値を探すのがポイントです。

③ 定期実行を“業務ルーチン化”する
「毎朝9時のチェック」や「週次レポート連携」など、
運用スケジュールに組み込むことで文化として定着します。
仕組みはシンプルでも、“使い方”の継続が品質を生みます。

ボタン1つで“現場が動くデータ品質管理”

実際に登録時にガードするより、運用で見つける文化の方が、「一旦全部受け付けてから」という流れになって、焦らずに仕事ができる、そういう心理的効果が大きいと思います。登録時に、となると、本当は登録を受け付けないといけない人の登録を断ってしまうのではないか、、、、という不安も生まれますよね。

毎朝のワンクリックで、前日の重複を洗い出すというワークフローです。実際に導入したプロジェクトでは最初の1週間で「意外と重複が多いな」と気づき、1ヶ月後には「もうボタンを押さないと落ち着かない」と言われるほど、“人が使いたくなるツール”になってくれました。

株式会社ファストコーディングではこういった現場視点のkintoneアプリ改善を多数手掛けております。kintoneで困ったことがあったら是非ご相談ください

重複チェックプラグイン ダウンロード

ご利用ルール

本プラグインは本記事に記載のコードを理解する目的で作成されたものです。本プラグインに係る著作権その他の知的財産権(以下、「著作権等」といいます。)は、当社または当該著作権等の権利を有する第三者に帰属します。
本プラグインの利用によってお客様及び第三者に生じた損害においては、弊社は一切の責任を負わないものとします。