1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

文字列の正規化は同一レコードの判定ではない:検索で衝突を残す設計

1
Posted at

検索フォームで大文字・小文字や句読点の違いを吸収すると、入力は楽になります。ただし、同じ検索キーになった文字列を、同じレコードとして保存してよいとは限りません。

開発している Krillion Answers の回答チェック処理では、正規化した文字列を検索に使い、元の回答レコードは残しています。ここでは、その実装から検索時の衝突処理を取り出して説明します。日替わりデータ、商品名、用語集などでも使える考え方です。

まず検索する範囲を決める

このアプリで確認したいのは、ある単語が一般的に珍しいかどうかではありません。「特定の日付・問題・保存済みデータに、その回答と得点の記録があるか」です。

そのため、回答を探す前に次を確定します。

  • 対象日と問題 ID
  • 保存されたスナップショットのバージョン
  • その問題の回答プールが完全に読み出せているか

実装では、要求されたバージョンが保存データと違えば stale、読み出した件数が予定件数と違えば unavailable にします。入力文字列だけをキーにして、別の日や別の問題から得点を持ってくる処理にはしません。

正規化は照合用のキーを作る処理

実際に使っている正規化関数です。

function normalizeAnswer(answer) {
  return typeof answer === 'string'
    ? answer.trim().normalize('NFC').toLowerCase()
        .replace(/[\p{P}\p{Z}\s]/gu, '')
    : '';
}

NFC、大文字・小文字の統一、句読点・区切り文字・空白の除去を行います。これはこの検索用途の方針であって、あらゆる名前に適した正規化ではありません。元の綴りは表示用に保持します。

たとえば、合成済みの é と e\u0301 は同じキーになります。一方、NFC は NFKC ではないので、全角の A と半角の A を同一視しません。

Map に一件だけ入れると、衝突が見えなくなる

Map<正規化キー, 回答> として最後の一件で上書きすると、異なる記録が同じキーになったことを後から検出できません。

検索側では、該当する候補をすべて残し、回答本文だけでなく、保存データに含まれる別名も照合します。以下は実装中の判定部分です。pool.answers は、対象の問題に限定した保存済みデータです。

const normalized = normalizeAnswer(answer);
const matches = pool.answers.filter(record =>
  [record.answer, ...(record.aliases || [])]
    .some(text => normalizeAnswer(text) === normalized)
);

const scores = new Set(
  matches.map(record => `${record.tier}:${record.score}`)
);

if (scores.size > 1) {
  return {
    status: 'unavailable',
    reason: 'conflicting_official_records',
    matchCount: matches.length
  };
}

候補が複数でも、稀有度と得点が一致していれば直ちに矛盾とはしません。両者が食い違った場合に、勝手に高い方や最後の一件を選ばない設計です。

候補がない場合も区別します。not_found は「保存済みプールに一致する記録がない」という意味です。「公式ゲームが必ず不正解にする」や「得点は 0」とは断定しません。

小さいテストでも、境界を具体化できる

次は正規化の境界を確認するテストです。

import assert from 'node:assert/strict';

assert.equal(normalizeAnswer('é'), normalizeAnswer('e\u0301'));
assert.equal(normalizeAnswer('co-op'), normalizeAnswer('coop'));
assert.notEqual(normalizeAnswer('A'), normalizeAnswer('A'));

さらに、テスト用の co-op と coop に異なる稀有度・得点を付けると、同じ検索キーから二つの記録が見つかり、衝突として扱うべきだと分かります。これらの語は説明用の架空データで、公式の出題や採点例ではありません。

実装では「入力を便利にすること」と「記録を一つに決めること」を分けると、判定の意味を保ちやすくなります。日付、問題、バージョン、元の綴りを残すことで、検索結果を説明する手掛かりも失わずに済みます。

実装例の背景

対象は、開発中の独立した補助サイト Krillion Answers の回答チェック です。保存済みの公式記録と照合して、稀有度と得点を表示します。サイトの利用は無料です。

本記事のコードと説明は、このプロジェクトの現行コードを確認して整理しました。ゲーム本体の公式判定を代替するものではなく、検索対象は保存済みの回答記録です。

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?