PDF Library for Delphiは、コード単位ではなく正準等価性によってテキストを一致させることができます。そのため、合成済み文字として入力されたクエリでも、基底文字に結合文字を組み合わせた形で格納されているコンテンツを見つけられますし、その逆も可能です。この挙動は2つの検索オプションで制御します。soCanonicalEquivalentは照合時にUnicode正規化を有効にし、soGraphemeClustersは、すべてのヒットとワイルドカードの各ステップを、完全な書記素クラスタに限定します
これが修正するバグは、ドキュメント検索において最も多く報告され、最も理解されていないものの1つです。ユーザーが名前を検索して結果が見つからず、その名前をドキュメントからコピーして検索ボックスに貼り付けると、今度は見つかります。目に見える形で壊れているところはどこにもありません。2つの文字列は見た目が同一で、印刷しても同一に見えますが、比較すると等しくありません。一方はU+00E9であり、もう一方はU+0065の後にU+0301が続く形だからです
なぜ同じ単語が等しくないと判定されるのか?
Unicodeは、同じ抽象文字に対して複数のエンコーディングを許容しています。発音区別符号付きのラテン文字は、合成済みのコードポイントとしても、基底文字と結合文字の組み合わせとしても存在し得ます。ハングルの音節は、合成済みの音節としても、分解されたチャモ(字母)としても存在し得ます。あるPDFがどちらを含んでいるかは、作成ツール、プラットフォーム、時にはフォントに依存しますが、それらのいずれも、検索している本人には見えません
単純な大文字小文字の畳み込みだけではこれを解決できない理由は、偶発的なものではなく構造的なものです。大文字小文字の畳み込みやアクセントの畳み込みは、コード単位レベルで1対1です。畳み込まれた文字列は元の文字列と同じ長さを持つため、畳み込んだテキスト内の一致位置は、元のテキストにおける一致位置と一致します。しかし正規化は1対1ではありません。1つの合成済み文字が2つや3つのコード単位になったり、分解された並びが1つに畳み込まれたりします。その変換の後では、位置はもはや抽出したテキストとは一致しなくなります
ヒットの座標を元のテキストに対応させ続ける
ここが、正規化された検索が単に正しいだけでなく実際に使えるものになるかどうかを左右する部分です。正規化によって生成される各コード単位は、それを生み出した元のUTF-16テキストの開始位置と終了位置を記録します。再帰的な分解は親の元の範囲を引き継ぎ、合成は入力の範囲をマージします。一致が見つかると、ライブラリはそのマッピング区間の中から最小の開始位置と最大の終了位置を走査して求めます
その結果、MatchStart、MatchLength、文脈文字列、そして2つの置換エントリポイントは、いずれも正規化された中間表現ではなく、元の抽出済みテキストを指し示し続けます。このマッピングがなければ、正規化された検索は、ヒットが存在することは教えてくれても、それがどこにあるのかを確実には教えてくれず、ハイライト表示は不正確になり、レダクション(黒塗り)は危険なものになってしまいます
正規化処理そのものは自己完結しています。Unicode 15.1に基づく、正準分解・合成・正準結合クラスのコンパクトなテーブルを持ち、ハングルはテーブルのエントリではなくアルゴリズム的な規則で処理されます。外部データファイルから何かを読み込むことも、プラットフォームの正規化APIを呼び出すこともないため、Windowsのサービス、Linuxのデーモン、FPCビルドは、同じ入力に対してすべて同一の結果を生成します
正準等価性を使って検索する
オプションは集合であるため、正準等価性は、単語単位の一致、ワイルドカード、発音区別符号を無視する畳み込みといった既存の挙動と組み合わせられます:
uses
PDFlibrary;
var
Lib: TPDFlib;
Hits: array of TPDFlibSearchHit;
Found, I: Integer;
begin
Lib := TPDFlib.Create;
try
Lib.LoadFromFile('contracts.pdf', '');
SetLength(Hits, 500);
Found := Lib.SearchText('Bäcker', [soCanonicalEquivalent, soWholeWord],
'', Hits); // 空のページ範囲 = ドキュメント全体
for I := 0 to Found - 1 do
Log(Format('page %d: "%s" at %d (%d chars)',
[Hits[I].Page, Hits[I].MatchText, Hits[I].MatchStart,
Hits[I].MatchLength]));
finally
Lib.Free;
end;
end;
正規化がオプトインになっているのには理由があります。NFDテキストとその位置マッピングを構築するのはコストがかかる作業であり、ASCIIのみのドキュメントに対するほとんどの検索ではそもそも必要ありません。このオプションを使用すると、各テキストブロックは、結合文字を除去した形とそうでない形という2つの変換済みの形をキャッシュします。そのため、同じブロックに対する一連のクエリは、クエリごとに正規化するのではなく1回だけ正規化されます。大文字小文字の畳み込みは、これまでどおり、より安価な1対1の経路をそのまま通ります
書記素クラスタの境界がないと何が壊れるのか?
コード単位は文字ではなく、文字はユーザーが知覚するものでもありません。国旗の絵文字は2つの地域標識コードポイントです。家族の絵文字は、ゼロ幅結合子でつながれた複数のコードポイントです。インド系文字の連字は、子音、ヴィラーマ、もう1つの子音です。アクセントが2つ重なった文字は3つのコードポイントです。これらのいずれかの途中で一致させたり切断したりすると、ゴミのように表示される断片ができてしまいます
soGraphemeClustersは、リテラルであれワイルドカードであれ、すべてのヒットの両端を、完全な拡張書記素クラスタの境界に制約します。この分割処理は拡張規則を実装しています。CRとLFのペアリング、制御文字、ハングル音節クラス、ExtendとSpacingMark、Prepend、絵文字のZWJシーケンス、地域標識のペアリング、インド系文字の連字での分割です。境界がサロゲートペアの内部に生成されることは決してなく、それだけでも、基本多言語面を超えるあらゆるコンテンツにおける、破損した結果という丸ごと1つのクラスの問題を排除します
このオプションはワイルドカードの消費も制御します。ここは、素朴な実装であれば依然として誤った位置で切断してしまう箇所です。単一文字のワイルドカードは、ちょうど1つの完全なクラスタ分だけ前進し、連続ワイルドカードのバックトラックはクラスタの境界の間だけを移動します:
// soGraphemeClustersを使わない場合、「?」がクラスタの半分だけを消費し、
// 結合文字が宙に浮いた状態で終わるヒットが返ることがある
Found := Lib.SearchText('c?té',
[soWildcards, soCanonicalEquivalent, soGraphemeClusters], '', Hits);
// 同じ境界は置換処理も保護するため、レダクションやコンテンツの
// 書き換えが絵文字やアクセント付き文字を分断することはない
Replaced := Lib.SearchAndReplaceText('naïve', 'plain',
[soCanonicalEquivalent, soGraphemeClusters], '1-20');
実際のワークロードに合わせてオプションを選ぶ
3つの組み合わせで、ほとんどのケースをカバーできます。社内向けのドキュメント検索ボックスでは、soCanonicalEquivalentとsoDiacriticInsensitiveを組み合わせることで、ユーザーが期待する寛容な挙動が得られ、両方のエンコーディング形式、およびアクセント付き・アクセントなしの両方の綴りにマッチします。誤検出にコストが伴う法務やコンプライアンス目的の検索では、soCanonicalEquivalentをsoCaseSensitiveとsoWholeWordとともに使用し、アクセントの畳み込みはオフのままにしてください。そうすれば、等価性は正確でエンコーディングに依存しないものになります
ドキュメントを変更するあらゆる処理には、例外なくsoGraphemeClustersを追加してください。わずかに間違った範囲を返す検索は、読み手を誤解させるだけで済みますが、同じ間違った範囲を使う置換やレダクションは、その誤りをファイルに書き込んでしまいます。削除範囲を間違えることの帰結については、本物のレダクションとコンテンツ削除で説明しています
スループットが重要な場合は、バッチ用のエントリポイントを優先してください。SearchTextBatchは、各ページのテキストブロックが常駐している間に、空でないすべてのクエリを実行するため、クエリごとにページを再抽出することを避け、キャッシュされた正規化結果を再利用します。ストリーミング版は、呼び出し側が用意したサイズ固定のバッファなしでヒットを出力します。その基盤となる抽出モデルについては、テキスト検索とページ要素の列挙で説明しています
これがオプションでは済まない文字体系
韓国語では、正準等価性の有無が、名前を見つけられるかどうかを分ける決定的な違いになります。実際のドキュメントでは、合成済み音節と分解されたチャモの両方が一般的だからです。ベトナム語では、複数重なった発音区別符号によって、合成された形式が完全に作成ツール次第になります。インド系文字体系では、連字の扱いが、ヒットの境界が読める位置に来るかどうかを左右します。日本語と中国語では、検索側は比較的シンプルですが、レイアウト側はそうではありません。詳しくは日本語・中国語の縦書きで説明しています
経験則は単純です。コーパスに英語以外の言語が1つでも含まれているなら、正準等価性をオンにし、コストが高すぎると判断する前にまず測定してください。ほとんどのドキュメント集合ではコストは高くありません。そして、代替案は、ユーザーが最も見つけたいと思っている、まさにその名前で静かに失敗する検索機能です
Unicode対応の検索、抽出、レダクション、テキスト書き換えは、Delphi、C++Builder、Free Pascal向けの1つのエンジンを共有しています。完全な機能一覧はPDF Library for Delphiページに掲載されています