こんにちは!今回は、PHPの標準関数であるsoundex()について詳しく解説していきます。発音が似ている単語を同じコードに変換して比較できる、ユニークな関数です!
soundex関数とは?
soundex()関数は、英単語を発音に基づいた4文字のコードに変換する関数です。
Soundexアルゴリズムを使用して、発音が似ている単語を同じコードに変換します。名前の検索やスペルミス対策など、曖昧検索に活用できます!
基本的な構文
soundex(string $string): string
- $string: 変換する文字列(英字)
- 戻り値: 4文字のSoundexコード(最初は英字、残り3桁は数字)
基本的な使用例
シンプルな変換
// 基本的な変換
echo soundex("Smith") . "\n";
// 出力: S530
echo soundex("Smythe") . "\n";
// 出力: S530(同じコード!)
echo soundex("Schmidt") . "\n";
// 出力: S530(これも同じ!)
// 発音が異なる単語
echo soundex("Johnson") . "\n";
// 出力: J525
echo soundex("Jackson") . "\n";
// 出力: J225
似た発音の比較
$name1 = "Robert";
$name2 = "Rupert";
if (soundex($name1) === soundex($name2)) {
echo "{$name1}と{$name2}は似た発音です\n";
} else {
echo "{$name1}と{$name2}は異なる発音です\n";
}
// Robert: R163
// Rupert: R163
// 出力: Robertと Rupertは似た発音です
大文字小文字の扱い
// 大文字小文字は区別されない
echo soundex("SMITH") . "\n"; // S530
echo soundex("smith") . "\n"; // S530
echo soundex("Smith") . "\n"; // S530
// すべて同じコードになる
短い文字列
// 1文字
echo soundex("A") . "\n"; // A000
// 2文字
echo soundex("AB") . "\n"; // A100
// 3文字
echo soundex("ABC") . "\n"; // A120
Soundexアルゴリズムの仕組み
/**
* Soundexアルゴリズムの基本ルール:
*
* 1. 最初の文字を保持
* 2. 以下の数字に変換:
* - B, F, P, V → 1
* - C, G, J, K, Q, S, X, Z → 2
* - D, T → 3
* - L → 4
* - M, N → 5
* - R → 6
* - A, E, I, O, U, H, W, Y → 削除
* 3. 同じ数字が連続する場合は1つにまとめる
* 4. 最初の文字 + 3桁の数字(足りない場合は0で埋める)
*/
// 例: "Robert"
// R → R(保持)
// o → 削除
// b → 1
// e → 削除
// r → 6
// t → 3
// 結果: R163
// 例: "Rupert"
// R → R(保持)
// u → 削除
// p → 1
// e → 削除
// r → 6
// t → 3
// 結果: R163
実践的な使用例
例1: 名前検索システム
class NameSearcher {
private $names = [];
/**
* 名前を追加
*/
public function addName($name, $data = null) {
$soundexCode = soundex($name);
if (!isset($this->names[$soundexCode])) {
$this->names[$soundexCode] = [];
}
$this->names[$soundexCode][] = [
'name' => $name,
'data' => $data
];
}
/**
* 似た発音の名前を検索
*/
public function search($query) {
$soundexCode = soundex($query);
if (!isset($this->names[$soundexCode])) {
return [];
}
return $this->names[$soundexCode];
}
/**
* すべての名前グループを取得
*/
public function getAllGroups() {
return $this->names;
}
/**
* 類似名をカウント
*/
public function countSimilar($name) {
$soundexCode = soundex($name);
return isset($this->names[$soundexCode]) ? count($this->names[$soundexCode]) : 0;
}
}
// 使用例
$searcher = new NameSearcher();
// 名前を登録
$searcher->addName("Smith", ['id' => 1, 'email' => 'smith@example.com']);
$searcher->addName("Smythe", ['id' => 2, 'email' => 'smythe@example.com']);
$searcher->addName("Schmidt", ['id' => 3, 'email' => 'schmidt@example.com']);
$searcher->addName("Johnson", ['id' => 4, 'email' => 'johnson@example.com']);
$searcher->addName("Jonson", ['id' => 5, 'email' => 'jonson@example.com']);
echo "=== 名前検索 ===\n";
// "Smyth"で検索(スペルミス)
$results = $searcher->search("Smyth");
echo "「Smyth」の検索結果:\n";
foreach ($results as $result) {
echo " - {$result['name']} ({$result['data']['email']})\n";
}
// "Johnsen"で検索
$results = $searcher->search("Johnsen");
echo "\n「Johnsen」の検索結果:\n";
foreach ($results as $result) {
echo " - {$result['name']} ({$result['data']['email']})\n";
}
例2: スペルミス許容検索
class FuzzySearcher {
/**
* 曖昧検索
*/
public static function fuzzySearch($needle, $haystack) {
$needleSoundex = soundex($needle);
$matches = [];
foreach ($haystack as $item) {
if (soundex($item) === $needleSoundex) {
$matches[] = $item;
}
}
return $matches;
}
/**
* 類似度スコア付き検索
*/
public static function searchWithScore($needle, $haystack) {
$needleSoundex = soundex($needle);
$results = [];
foreach ($haystack as $item) {
$itemSoundex = soundex($item);
// Soundexが一致
if ($itemSoundex === $needleSoundex) {
// レーベンシュタイン距離で精度を計算
$distance = levenshtein(strtolower($needle), strtolower($item));
$similarity = 1 / ($distance + 1);
$results[] = [
'item' => $item,
'soundex_match' => true,
'distance' => $distance,
'similarity' => $similarity
];
}
}
// 類似度でソート
usort($results, function($a, $b) {
return $b['similarity'] <=> $a['similarity'];
});
return $results;
}
/**
* 複数候補を提案
*/
public static function suggest($input, $dictionary, $maxSuggestions = 5) {
$results = self::searchWithScore($input, $dictionary);
return array_slice(
array_column($results, 'item'),
0,
$maxSuggestions
);
}
}
// 使用例
$dictionary = [
"apple", "application", "apply",
"banana", "band", "bandana",
"orange", "organize", "origin"
];
echo "=== スペルミス許容検索 ===\n";
// "aple"(スペルミス)で検索
$query = "aple";
echo "「{$query}」の検索結果:\n";
$matches = FuzzySearcher::fuzzySearch($query, $dictionary);
print_r($matches);
echo "\n=== スコア付き検索 ===\n";
$results = FuzzySearcher::searchWithScore("aplcation", $dictionary);
foreach ($results as $result) {
echo sprintf(
"%s (類似度: %.2f, 距離: %d)\n",
$result['item'],
$result['similarity'],
$result['distance']
);
}
echo "\n=== 候補提案 ===\n";
$suggestions = FuzzySearcher::suggest("orang", $dictionary, 3);
echo "もしかして: " . implode(", ", $suggestions) . "\n";
例3: 重複検出
class DuplicateDetector {
/**
* 発音が似た重複を検出
*/
public static function findPhoneticDuplicates($items) {
$groups = [];
foreach ($items as $item) {
$soundexCode = soundex($item);
if (!isset($groups[$soundexCode])) {
$groups[$soundexCode] = [];
}
$groups[$soundexCode][] = $item;
}
// 2つ以上の項目があるグループのみ返す
return array_filter($groups, function($group) {
return count($group) > 1;
});
}
/**
* 重複の可能性がある名前をチェック
*/
public static function checkForDuplicates($newName, $existingNames) {
$newSoundex = soundex($newName);
$potentialDuplicates = [];
foreach ($existingNames as $existing) {
if (soundex($existing) === $newSoundex) {
$potentialDuplicates[] = $existing;
}
}
return $potentialDuplicates;
}
/**
* 重複レポートを生成
*/
public static function generateReport($items) {
$duplicates = self::findPhoneticDuplicates($items);
$report = [];
foreach ($duplicates as $soundex => $group) {
$report[] = [
'soundex' => $soundex,
'count' => count($group),
'items' => $group
];
}
// カウントでソート
usort($report, function($a, $b) {
return $b['count'] <=> $a['count'];
});
return $report;
}
}
// 使用例
$names = [
"Smith", "Smythe", "Schmidt",
"Johnson", "Jonson", "Johnsen",
"Brown", "Browne",
"Taylor", "Tailor",
"Anderson", "Andersen"
];
echo "=== 重複検出 ===\n";
$duplicates = DuplicateDetector::findPhoneticDuplicates($names);
foreach ($duplicates as $soundex => $group) {
echo "\nSoundex {$soundex}:\n";
foreach ($group as $name) {
echo " - {$name}\n";
}
}
echo "\n=== 新規名前チェック ===\n";
$newName = "Smyth";
$existing = ["Smith", "Smythe", "Johnson"];
$potentialDups = DuplicateDetector::checkForDuplicates($newName, $existing);
if (!empty($potentialDups)) {
echo "「{$newName}」は以下の名前と似ています:\n";
foreach ($potentialDups as $dup) {
echo " - {$dup}\n";
}
}
echo "\n=== 重複レポート ===\n";
$report = DuplicateDetector::generateReport($names);
foreach ($report as $item) {
echo "Soundex {$item['soundex']}: {$item['count']}件\n";
echo " " . implode(", ", $item['items']) . "\n";
}
例4: データクレンジング
class DataCleaner {
/**
* 名前を正規化
*/
public static function normalizeNames($names) {
$normalized = [];
$groups = [];
// Soundexでグループ化
foreach ($names as $name) {
$soundex = soundex($name);
if (!isset($groups[$soundex])) {
$groups[$soundex] = [];
}
$groups[$soundex][] = $name;
}
// 各グループで最も一般的な綴りを選択
foreach ($groups as $soundex => $group) {
// 最初の項目を代表として使用(実際はより洗練された選択が必要)
$representative = $group[0];
foreach ($group as $variant) {
$normalized[$variant] = $representative;
}
}
return $normalized;
}
/**
* マスターリストと照合
*/
public static function matchToMaster($input, $masterList) {
$inputSoundex = soundex($input);
foreach ($masterList as $master) {
if (soundex($master) === $inputSoundex) {
return $master;
}
}
return null;
}
/**
* データセットをクレンジング
*/
public static function cleanDataset($data, $field, $masterList) {
$cleaned = [];
$unmatched = [];
foreach ($data as $record) {
$value = $record[$field];
$match = self::matchToMaster($value, $masterList);
if ($match !== null) {
$record[$field] = $match;
$cleaned[] = $record;
} else {
$unmatched[] = $record;
}
}
return [
'cleaned' => $cleaned,
'unmatched' => $unmatched
];
}
}
// 使用例
$inputNames = [
"Smyth", "Smith", "Smythe",
"Jonson", "Johnson", "Johnsen"
];
echo "=== 名前正規化 ===\n";
$normalized = DataCleaner::normalizeNames($inputNames);
foreach ($normalized as $variant => $standard) {
echo "{$variant} → {$standard}\n";
}
echo "\n=== マスターリスト照合 ===\n";
$masterList = ["Smith", "Johnson", "Brown"];
$testNames = ["Smythe", "Jonson", "Greene"];
foreach ($testNames as $name) {
$match = DataCleaner::matchToMaster($name, $masterList);
echo "{$name} → " . ($match ?? "マッチなし") . "\n";
}
echo "\n=== データセットクレンジング ===\n";
$dataset = [
['name' => 'Smyth', 'age' => 30],
['name' => 'Jonson', 'age' => 25],
['name' => 'Greene', 'age' => 35]
];
$result = DataCleaner::cleanDataset($dataset, 'name', $masterList);
echo "クレンジング済み: " . count($result['cleaned']) . "件\n";
echo "未マッチ: " . count($result['unmatched']) . "件\n";
例5: 音声認識補助
class VoiceRecognitionHelper {
/**
* 音声入力の候補を生成
*/
public static function generateCandidates($voiceInput, $vocabulary) {
$inputSoundex = soundex($voiceInput);
$candidates = [];
foreach ($vocabulary as $word) {
if (soundex($word) === $inputSoundex) {
$candidates[] = $word;
}
}
return $candidates;
}
/**
* 最適な候補を選択
*/
public static function selectBestCandidate($voiceInput, $vocabulary, $context = []) {
$candidates = self::generateCandidates($voiceInput, $vocabulary);
if (empty($candidates)) {
return null;
}
if (count($candidates) === 1) {
return $candidates[0];
}
// コンテキストベースの選択(簡易版)
foreach ($candidates as $candidate) {
if (in_array($candidate, $context)) {
return $candidate;
}
}
// レーベンシュタイン距離で選択
$bestCandidate = null;
$minDistance = PHP_INT_MAX;
foreach ($candidates as $candidate) {
$distance = levenshtein(strtolower($voiceInput), strtolower($candidate));
if ($distance < $minDistance) {
$minDistance = $distance;
$bestCandidate = $candidate;
}
}
return $bestCandidate;
}
/**
* 音声コマンドを解析
*/
public static function parseVoiceCommand($input, $commandVocabulary) {
$words = explode(' ', $input);
$parsed = [];
foreach ($words as $word) {
$match = self::selectBestCandidate($word, $commandVocabulary);
$parsed[] = $match ?? $word;
}
return implode(' ', $parsed);
}
}
// 使用例
$vocabulary = [
"open", "close", "save", "delete",
"file", "folder", "document",
"create", "edit", "view"
];
echo "=== 音声認識候補 ===\n";
$voiceInput = "oppen"; // "open"の認識ミス
$candidates = VoiceRecognitionHelper::generateCandidates($voiceInput, $vocabulary);
echo "「{$voiceInput}」の候補: " . implode(", ", $candidates) . "\n";
echo "\n=== 最適候補選択 ===\n";
$best = VoiceRecognitionHelper::selectBestCandidate($voiceInput, $vocabulary);
echo "「{$voiceInput}」 → {$best}\n";
echo "\n=== 音声コマンド解析 ===\n";
$command = "oppen the fyle";
$parsed = VoiceRecognitionHelper::parseVoiceCommand($command, $vocabulary);
echo "入力: {$command}\n";
echo "解析: {$parsed}\n";
例6: 姓名インデックス
class NameIndex {
private $index = [];
/**
* 名前を追加
*/
public function add($firstName, $lastName, $data) {
$firstSoundex = soundex($firstName);
$lastSoundex = soundex($lastName);
$key = $firstSoundex . '-' . $lastSoundex;
if (!isset($this->index[$key])) {
$this->index[$key] = [];
}
$this->index[$key][] = [
'first' => $firstName,
'last' => $lastName,
'data' => $data
];
}
/**
* フルネームで検索
*/
public function search($firstName, $lastName) {
$firstSoundex = soundex($firstName);
$lastSoundex = soundex($lastName);
$key = $firstSoundex . '-' . $lastSoundex;
return $this->index[$key] ?? [];
}
/**
* 姓のみで検索
*/
public function searchByLastName($lastName) {
$lastSoundex = soundex($lastName);
$results = [];
foreach ($this->index as $key => $entries) {
if (strpos($key, '-' . $lastSoundex) !== false) {
$results = array_merge($results, $entries);
}
}
return $results;
}
/**
* 統計情報を取得
*/
public function getStatistics() {
return [
'total_keys' => count($this->index),
'total_entries' => array_sum(array_map('count', $this->index)),
'avg_per_key' => count($this->index) > 0 ?
array_sum(array_map('count', $this->index)) / count($this->index) : 0
];
}
}
// 使用例
$index = new NameIndex();
// データ追加
$index->add("John", "Smith", ['id' => 1]);
$index->add("Jon", "Smythe", ['id' => 2]);
$index->add("Jane", "Schmidt", ['id' => 3]);
$index->add("Robert", "Johnson", ['id' => 4]);
$index->add("Bob", "Jonson", ['id' => 5]);
echo "=== フルネーム検索 ===\n";
$results = $index->search("Jon", "Smith");
echo "「Jon Smith」の検索結果:\n";
foreach ($results as $result) {
echo " - {$result['first']} {$result['last']} (ID: {$result['data']['id']})\n";
}
echo "\n=== 姓のみ検索 ===\n";
$results = $index->searchByLastName("Johnsen");
echo "「Johnsen」の検索結果:\n";
foreach ($results as $result) {
echo " - {$result['first']} {$result['last']} (ID: {$result['data']['id']})\n";
}
echo "\n=== 統計情報 ===\n";
$stats = $index->getStatistics();
print_r($stats);
例7: 比較ツール
class SoundexComparator {
/**
* 2つの文字列を比較
*/
public static function compare($str1, $str2) {
$soundex1 = soundex($str1);
$soundex2 = soundex($str2);
return [
'string1' => $str1,
'string2' => $str2,
'soundex1' => $soundex1,
'soundex2' => $soundex2,
'match' => $soundex1 === $soundex2,
'levenshtein' => levenshtein(strtolower($str1), strtolower($str2))
];
}
/**
* 複数の文字列を比較
*/
public static function compareMultiple($strings) {
$results = [];
for ($i = 0; $i < count($strings); $i++) {
for ($j = $i + 1; $j < count($strings); $j++) {
$results[] = self::compare($strings[$i], $strings[$j]);
}
}
return $results;
}
/**
* Soundexコード分析
*/
public static function analyze($string) {
$soundex = soundex($string);
return [
'original' => $string,
'soundex' => $soundex,
'first_letter' => $soundex[0],
'code' => substr($soundex, 1),
'length' => strlen($string)
];
}
}
// 使用例
echo "=== 2つの文字列を比較 ===\n";
$comparison = SoundexComparator::compare("Smith", "Smythe");
print_r($comparison);
echo "\n=== 複数の文字列を比較 ===\n";
$names = ["Smith", "Smythe", "Schmidt", "Johnson"];
$comparisons = SoundexComparator::compareMultiple($names);
foreach ($comparisons as $comp) {
$match = $comp['match'] ? '✓' : '✗';
echo "{$comp['string1']} vs {$comp['string2']}: {$match} ({$comp['soundex1']} vs {$comp['soundex2']})\n";
}
echo "\n=== Soundex分析 ===\n";
$analysis = SoundexComparator::analyze("Robert");
print_r($analysis);
metaphone()との比較
// soundex()とmetaphone()の違い
$names = ["Smith", "Smythe", "Schmidt"];
echo "=== soundex() vs metaphone() ===\n";
foreach ($names as $name) {
echo "{$name}:\n";
echo " soundex: " . soundex($name) . "\n";
echo " metaphone: " . metaphone($name) . "\n";
}
/*
Smith:
soundex: S530
metaphone: SM0
Smythe:
soundex: S530
metaphone: SM0
Schmidt:
soundex: S530
metaphone: SXMT
*/
// metaphone()の方が精度が高い場合もある
制限事項と注意点
// 英語以外の言語
echo soundex("山田") . "\n"; // 正しく動作しない
// 数字のみ
echo soundex("12345") . "\n"; // 1000
// 空文字列
echo soundex("") . "\n"; // 警告が出る
// 特殊文字
echo soundex("O'Brien") . "\n"; // O165(アポストロフィは無視)
まとめ
soundex()関数の特徴をまとめると:
できること:
- 発音に基づいた文字列の比較
- スペルミスの許容
- 類似名の検出
- 曖昧検索の実現
アルゴリズムの特徴:
- 4文字のコード(1文字+3数字)
- 発音が似ていれば同じコードになる
- 大文字小文字を区別しない
推奨される使用場面:
- 名前検索システム
- スペルミス許容検索
- 重複検出
- データクレンジング
- 音声認識補助
利点:
- シンプルで高速
- スペルミスに強い
- 実装が容易
制限事項:
- 英語専用(他の言語では不正確)
- 精度が限定的
- 同じコードでも発音が異なる場合がある
関連関数:
metaphone(): より精度の高い音声マッチングlevenshtein(): 編集距離の計算similar_text(): 文字列の類似度計算
使い分け:
// soundex(): 高速、シンプル、英語名に最適
$code1 = soundex("Smith");
// metaphone(): より精度が高い
$code2 = metaphone("Smith");
// levenshtein(): 編集距離(タイポ検出)
$distance = levenshtein("Smith", "Smyth");
soundex()は、発音ベースの曖昧検索が必要な場合に便利です。特に名前の検索やスペルミス対策で活躍しますが、英語専用であることと精度の限界を理解して使用しましょう!
