[PHP]soundex関数を完全解説!音声的に類似した文字列を比較する方法

PHP

こんにちは!今回は、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()は、発音ベースの曖昧検索が必要な場合に便利です。特に名前の検索やスペルミス対策で活躍しますが、英語専用であることと精度の限界を理解して使用しましょう!

タイトルとURLをコピーしました