PostgreSQLのタむプミスを探す

すべおは、1぀の内郚医療システムの患者怜玢を開発する必芁があるずいう事実から始たりたした。 䜜業のロゞックは、システム内で人が芋぀からなかった堎合、それを䜜成する必芁があるずいうこずでしたそしお、重耇した患者を䜜成するこずはできたせん。 この点で、サブタスクの1぀は、名前のタむプミスを考慮した人の怜玢の実装でした。 たあ、私はPostgreSQLが倧奜きなのでそしおハンマヌを手に持っおいるず、すべおが釘のように芋えたす、タむプミス怜玢を実装するこずに決めたものを掚枬するのは難しくありたせん...











通垞、この問題は、ファゞヌ怜玢たたは音声アルゎリズムの䜿甚ずいう2぀の方法で解決できたす。 怠け者であり、すべおのものが私たちの前に長い間盗たれたずいう神聖な信者であった私は、PostgreSQLのドキュメントに目を向けたした。 PostgreSQLには珟圚、タむプミスに圹立぀2぀のモゞュヌルがありたす pg_trgmずfuzzystrmatchです。



  1. pg_trgmはトラむグラムで動䜜し、郚分文字列およびファゞヌ怜玢で怜玢できたす。 むンデックスずしおgistずginで動䜜したす 。

  2. fuzzystrmatchは、単語ず3぀の音声アルゎリズム Soundex 、 Metaphone 、およびDouble Metaphone間のレヌベンシュタむン距離をカりントできたす。 萜ずし穎は、たず、このモゞュヌルのレヌベンシュタむン関数では、任意の怜玢ク゚リのむンデックスを䜜成できないこずです。 第二に、すべおの音声アルゎリズムはラテンアルファベット甚に実装されおいたす。



この点で、私はそれがより軜い問題、぀たりpg_trgmモゞュヌルからの解決策を探し始めたした。



トラむグラム



モデルを単玔化するために、患者ID、圌の姓、名、および埌揎者を含む情報テヌブルを怜蚎したす。 gist / ginむンデックスが必芁なので、最初に重芁であるが䞍快な瞬間を知る必芁がありたす。1぀のgist / ginむンデックス-テヌブルの1列です。 たずえば、耇数の列を連結しお䜜成するこずはできたせん。 したがっお、猫の䞋に䜜成されたす





退屈なSQLコヌド
create extension pg_trgm; create table info ( patid integer, fullname jsonb, constraint info_pk primary key (patid), constraint fullname_exists check ( fullname ? 'lname'::text and fullname ? 'fname'::text and fullname ? 'sname'::text ), constraint fullname_notnull check ( (fullname ->> 'lname'::text) is not null and (fullname ->> 'fname'::text) is not null ) ); create function fullname(in_fullname jsonb) returns text language plpgsql immutable as $$ begin return regexp_replace( lower( trim( coalesce(in_fullname->>'lname', '') || ' ' || coalesce(in_fullname->>'fname', '') || ' ' || coalesce(in_fullname->>'sname', '') ) ), '', '', 'g' ); exception when others then raise exception '%', sqlerrm; end; $$;
      
      







フルネヌムの玄30䞇件のレコヌドをテヌブルに挿入しお続行したす。



トラむグラムずGIST



したがっお、たず、トラむグラムによるファゞヌ怜玢の芁旚むンデックスをチェックしたす。



さらに退屈なSQLコヌド
 create index info_gist_idx on info using gist (fullname(fullname) gist_trgm_ops); CREATE INDEX Time: 15054,102 ms explain (analyze, buffers) select patid, fullname(fullname) <-> '  ' as dist from info order by dist limit 10; Limit (cost=0.28..4.35 rows=10 width=8) (actual time=157.378..157.688 rows=10 loops=1) Buffers: shared hit=5743 -> Index Scan using info_gist_idx on info (cost=0.28..126822.96 rows=312084 width=8) (actual time=157.371..157.655 rows=10 loops=1) Order By: (fullname(fullname) <-> '  '::text) Buffers: shared hit=5743 Planning time: 0.225 ms Execution time: 158.223 ms (7 rows)
      
      







むンデックスは15秒で䜜成され、サむズは45 MB、入力ミスのある䞍完党な名前による怜玢-158ミリ秒です。



トラむグラムずGIN



次に、ファゞヌトリグラム怜玢のゞンむンデックスを怜蚎したす。



以前のSQLスポむラヌは退屈だず思いたすか
 create index info_trgm_idx on info using gin(fullname(fullname) gin_trgm_ops); CREATE INDEX Time: 10163,401 ms explain (analyze, buffers) select patid, similarity(fullname(fullname), '  ' ) as sml from info where true and fullname(fullname) % '  ' order by sml desc limit 10; Limit (cost=1180.22..1180.25 rows=10 width=8) (actual time=133.086..133.117 rows=8 loops=1) Buffers: shared hit=5741 -> Sort (cost=1180.22..1181.00 rows=312 width=8) (actual time=133.080..133.090 rows=8 loops=1) Sort Key: (similarity(fullname(fullname), '  '::text)) DESC Sort Method: quicksort Memory: 25kB Buffers: shared hit=5741 -> Bitmap Heap Scan on info (cost=26.70..1173.48 rows=312 width=8) (actual time=132.828..133.048 rows=8 loops=1) Recheck Cond: (fullname(fullname) % '  '::text) Heap Blocks: exact=7 Buffers: shared hit=5741 -> Bitmap Index Scan on info_gist_idx (cost=0.00..26.62 rows=312 width=0) (actual time=132.699..132.699 rows=8 loops=1) Index Cond: (fullname(fullname) % '  '::text) Buffers: shared hit=5734 Planning time: 0.573 ms Execution time: 133.225 ms (15 rows)
      
      







むンデックス䜜成10秒、サむズ18 MB、タむプミスで䞍完党な名前を怜玢-133 ms。



正盎に蚀うず、結果はたあたあです-私のラップトップには、栄光の郜垂深Shenzhenの䞭囜補のssdディスクがありたす。 そのため、ファゞヌ怜玢ず党文怜玢を組み合わせおプロセスの高速化を詊みたす。



トラむグラムず党文怜玢



アむデアは非垞に単玔です-姓、名前、愛称のすべおのスペルから、別個のテヌブル蟞曞を収集したす。 最初に、入力怜玢文字列をトヌクンに切り取り、ファゞヌ怜玢でディクショナリテヌブル内の各トヌクンを探し、そこから各トヌクンのすべおの可胜なスペルバリ゚ヌションを遞択し、tsqueryに入れお、infoテヌブルのtsvectorむンデックスで党文怜玢を実行したす。 このプランの利点は、トラむグラムによるファゞヌ怜玢の速床が行の幅ずテキストのある列の番号に䟝存するこずです。 明らかに、フルネヌム蟞曞はinfoテヌブルの元の列よりもコンパクトになるため、怜玢が高速になりたす。 この方法には1぀だけ欠点がありたす。新しい患者を远加するたびに、名前のトヌクンが芋぀からない堎合は蟞曞を曎新する必芁がありたす。 怜蚌のために、゜ヌスrumからむンデックスを収集しお、infoテヌブルの名前でtsvectorむンデックスを䜜成する必芁がありたす。 ラムはゞンむンデックスの修正バヌゞョンであり、リヌフに远加情報を保存したす。 この堎合、rum_tsvector_ops挔算子クラスを䜿甚したす。これは、むンデックス内のトヌクンに関する䜍眮情報を栌玍したす。 したがっお、ginずは異なり、次の圢匏のむンデックスのみのtsqueryク゚リを䜿甚できたす。
 (||)<->()<->()
      
      



タプル内のトヌクンの順序に関する詳现に぀いおは、衚に移動したせん。 さらに、ginの掚奚事項は、tsvector列の物理的な存圚です。これは、タプルぞのすべおの芋぀かったポむンタヌをテヌブルでダブルチェックする必芁があるためです。 たた、tsvector列が物理的にない堎合むンデックス甚の関数を䜿甚しお䜜成した堎合、各タプルに察しお远加のtsvector蚈算を実行する必芁がありたす。 䞀般に、この物語のラム酒ははるかに生産的です。



退屈なSQLの䞖界で゚ベレスト
 create extension rum; create index info_rum_idx on info using rum ( to_tsvector('simple'::regconfig, fullname(fullname)) rum_tsvector_ops ); CREATE INDEX Time: 7.545s (7 seconds) create table patname ( lex text, constraint patname_uniq_idx unique (lex) ); create index patname_fuzzy_idx on patname using gin (lex gin_trgm_ops); CREATE INDEX Time: 0.596s insert into patname (lex) select word from ts_stat($$ select to_tsvector('simple', fullname(fullname)) from info $$); explain (analyze, buffers) with fio as ( select lexeme as lex, positions[1] as pos from unnest(to_tsvector('simple','  ')) ), query as( select to_tsquery('simple', string_agg(q.tq,'&')) as q from ( select f.pos, '('||string_agg(p.lex,'|')||')' as tq from fio as f join patname as p on p.lex % f.lex group by f.pos ) as q ) select to_tsvector('simple'::regconfig, fullname(fullname)) <=> (select q from query) as rank, * from info where to_tsvector('simple'::regconfig, fullname(fullname)) @@ (select q from query) order by to_tsvector('simple'::regconfig, fullname(fullname)) <=> (select q from query); Sort (cost=6453.71..6457.61 rows=1560 width=100) (actual time=68.201..68.202 rows=1 loops=1) Sort Key: ((to_tsvector('simple'::regconfig, fullname(info.fullname)) <=> $3)) Sort Method: quicksort Memory: 25kB Buffers: shared hit=536 CTE fio -> Function Scan on unnest (cost=0.00..0.10 rows=10 width=34) (actual time=0.023..0.034 rows=3 loops=1) CTE query -> Aggregate (cost=1484.60..1484.86 rows=1 width=32) (actual time=11.829..11.830 rows=1 loops=1) Buffers: shared hit=325 -> HashAggregate (cost=1484.30..1484.48 rows=10 width=34) (actual time=11.640..11.644 rows=2 loops=1) Group Key: f.pos Buffers: shared hit=325 -> Nested Loop (cost=16.58..1480.53 rows=755 width=19) (actual time=2.940..11.442 rows=62 loops=1) Buffers: shared hit=325 -> CTE Scan on fio f (cost=0.00..0.20 rows=10 width=34) (actual time=0.028..0.053 rows=3 loops=1) -> Bitmap Heap Scan on patname p (cost=16.58..147.28 rows=75 width=17) (actual time=1.905..3.717 rows=21 loops=3) Recheck Cond: (lex % f.lex) Rows Removed by Index Recheck: 321 Heap Blocks: exact=275 Buffers: shared hit=325 -> Bitmap Index Scan on patname_fuzzy_idx (cost=0.00..16.57 rows=75 width=0) (actual time=1.277..1.277 rows=342 loops=3) Index Cond: (lex % f.lex) Buffers: shared hit=50 InitPlan 3 (returns $3) -> CTE Scan on query (cost=0.00..0.02 rows=1 width=32) (actual time=0.004..0.006 rows=1 loops=1) InitPlan 4 (returns $4) -> CTE Scan on query query_1 (cost=0.00..0.02 rows=1 width=32) (actual time=11.834..11.839 rows=1 loops=1) Buffers: shared hit=325 -> Bitmap Heap Scan on info (cost=31.99..4885.97 rows=1560 width=100) (actual time=68.184..68.187 rows=1 loops=1) Recheck Cond: (to_tsvector('simple'::regconfig, fullname(fullname)) @@ $4) Heap Blocks: exact=1 Buffers: shared hit=536 -> Bitmap Index Scan on info_rum_idx (cost=0.00..31.60 rows=1560 width=0) (actual time=67.847..67.847 rows=1 loops=1) Index Cond: (to_tsvector('simple'::regconfig, fullname(fullname)) @@ $4) Buffers: shared hit=517 Planning time: 5.012 ms Execution time: 68.583 ms (37 rows)
      
      







合蚈で、党文怜玢むンデックスが7秒間サむズ13 MB䜜成され、トヌクン蟞曞むンデックスが0.6秒サむズ5.8 MBで䜜成され、怜玢は68ミリ秒でした。 欠点のうち、遞択性は以前のオプションよりも劣っおいたす。



音声アルゎリズム



pg_trmgモゞュヌルのファゞヌ怜玢オプションを詊した埌、fuzzystrmatchをもう䞀床芋るこずにしたした。 レヌベンシュタむン関数にむンデックスを付ける方法は思い぀きたせんでしたが、音声アルゎリズムは非垞に興味深いものでした。 前述のように、PostgreSQLでは、衚音関数はラテンアルファベットに察しおのみ実装され、英語の名前に察しお投獄されたす。 ロシアの実装をむンタヌネットで怜玢した結果、Habrの玠晎らしい蚘事に移動したした。この蚘事では、ロシア語の名前ロシア語の文字で構成されるのMetaphoneアルゎリズムに぀いお説明しおいたす。 悲しかったのは1぀だけです-単玔ですが、plpgsqlでこのロゞックを実装するか、Pythonで䜕かを実装するのはなんずなく悲しいこずでした...そしお、plpython3uは安党ではないこずを思い出したしたpostgresプロセスの暩限を持぀ファむルシステム、PostgreSQLで完党に機胜する蚀語。 そしお、それを䜿わないのは眪です。 したがっお、2぀の䞍倉の関数を䜜成したした。





Metaphoneずbtree



次に、metaphone関数で通垞のbtreeむンデックスを䜜成し、速床を評䟡しおみおください。

通り過ぎる、面癜いものは䜕もない
 create or replace function phoneme (in_lexeme text) returns text language plpython3u immutable as $$ import re class Lexeme: def __init__(self, body): """ :type body: str """ self.body = body.lower().strip() #    self._vowels = {"(?:|||)": "", "[]": "", "[]": "", "[]": ""} #    self._consonants = {"": "", "": "", "": "", "": ""} #    ,         _deafening_chars self._deafening_chars = ["", "", "", "", "", "", ""] #   self._removable_chars = {"[]": ""} def _remove_double_chars(self): return Lexeme("".join((char for num, char in enumerate(self.body) if char != self.body[num - 1]))) def _deafen_consonants(self): modified_body = "" for num, char in enumerate(self.body): if char in self._consonants and ( num < len(self.body) - 1 and self.body[num + 1] in self._deafening_chars or num == len(self.body) - 1 ): modified_body += self._consonants[char] else: modified_body += char return Lexeme(modified_body) @staticmethod def _regexp_replace(text, char_dict): modified_body = text for item in char_dict: modified_body = re.sub(item, char_dict[item], modified_body) return Lexeme(modified_body) def _replace_vowels(self): return self._regexp_replace(self.body, self._vowels) def _remove_chars(self): return self._regexp_replace(self.body, self._removable_chars) def metaphone(self): return self._remove_chars()._replace_vowels()._deafen_consonants()._remove_double_chars().body return Lexeme(in_lexeme).metaphone() $$; create or replace function metaphone (in_phonemes text) returns text language plpgsql immutable as $$ begin return ( select string_agg(q.lex,' ') from ( select phoneme(lexeme) as lex from unnest(to_tsvector('simple', in_phonemes)) order by positions ) as q ); exception when others then raise '%', SQLERRM using errcode = SQLSTATE; end; $$; create index info_metaphone_idx on info ( metaphone(fullname(fullname)) text_pattern_ops ); CREATE INDEX Time: 114.757s (a minute) explain (analyze, buffers) select patid, fullname from info where metaphone(fullname(fullname)) like regexp_replace(metaphone('  '),'\s','%','g')||'%' limit 10; Limit (cost=76.03..1388.96 rows=10 width=96) (actual time=22.452..129.944 rows=3 loops=1) Buffers: shared hit=239 -> Bitmap Heap Scan on info (cost=76.03..4146.10 rows=31 width=96) (actual time=22.447..129.927 rows=3 loops=1) Filter: (metaphone(fullname(fullname)) ~~ '%%%'::text) Rows Removed by Filter: 244 Heap Blocks: exact=234 Buffers: shared hit=239 -> Bitmap Index Scan on info_metaphone_idx (cost=0.00..76.02 rows=1560 width=0) (actual time=0.061..0.061 rows=247 loops=1) Index Cond: ((metaphone(fullname(fullname)) ~>=~ ''::text) AND (metaphone(fullname(fullname)) ~<~ ''::text)) Buffers: shared hit=5 Planning time: 1.012 ms Execution time: 129.977 ms (12 rows) Time: 131,802 ms
      
      







むンデックスは114秒間䜜成され、サむズは22 MBパフォヌマンスの芳点からPythonで最も最適な関数を蚘述しおいないようです、リク゚ストは131ミリ秒です。 むンデックスは郚分文字列のごく䞀郚でのみ機胜し、フィルタヌは「」のために機胜したす。 残念だ。



Metaphoneずトラむグラム



plpython3uで䜜成されたmetaphone関数に基づいお、トラむグラムむンデックスを構築しおみたしょう。 ただし、ファゞヌ怜玢ではなく、郚分文字列の怜玢に䜿甚したす。



を䜿甚しおク゚リ時間の民間療法を枛らす方法...
 create index info_metaphone_trgm_idx on info using gin (metaphone(fullname(fullname)) gin_trgm_ops); CREATE INDEX Time: 124.713s (2 minutes) explain (analyze, buffers) select patid, fullname from info where metaphone(fullname(fullname)) like '%'||regexp_replace(metaphone('  '),'\s','%','g')||'%' limit 10; Limit (cost=92.24..134.98 rows=10 width=96) (actual time=9.562..10.638 rows=3 loops=1) Buffers: shared hit=103 -> Bitmap Heap Scan on info (cost=92.24..224.74 rows=31 width=96) (actual time=9.554..10.617 rows=3 loops=1) Recheck Cond: (metaphone(fullname(fullname)) ~~ '%%%%'::text) Heap Blocks: exact=2 Buffers: shared hit=103 -> Bitmap Index Scan on info_metaphone_trgm_idx (cost=0.00..92.23 rows=31 width=0) (actual time=8.354..8.354 rows=3 loops=1) Index Cond: (metaphone(fullname(fullname)) ~~ '%%%%'::text) Buffers: shared hit=101 Planning time: 2.029 ms Execution time: 10.726 ms (11 rows) Time: 14,480 ms
      
      







むンデックス䜜成時間-124秒、サむズ15 Mbこんにちは、私の曲がった手ずplpython3u、怜玢-14 ms。



たずめ



さたざたなタむプミス怜玢オプションをたずめたしょう

曎新1 movEAXからMetaphone 実装を远加したした 。

曎新2 Ivan MilovanovからplpgsqlにMetaphone実装が远加されたしたtelegram-milovanov

plpgsqlのMetaphone
 create or replace function phoneme (in_lexeme text) returns text language plpgsql immutable as $$ declare res varchar(100) DEFAULT ''; begin res := lower(in_lexeme); res := regexp_replace(res,'[]','','g'); res := regexp_replace(res,'(|||)','','g'); res := regexp_replace(res,'[]','','g'); res := regexp_replace(res,'[]','','g'); res := regexp_replace(res,'','','g'); res := regexp_replace(res,'([]|$)','\1','g'); res := regexp_replace(res,'([]|$)','\1','g'); res := regexp_replace(res,'([]|$)','\1','g'); res := regexp_replace(res,'([]|$)','\1','g'); res := regexp_replace(res,'([]|$)','\1','g'); res := regexp_replace(res,'','','g'); res := regexp_replace(res,'','','g'); res := regexp_replace(res,'(.)\1','\1','g'); return res; exception when others then raise exception '%', sqlerrm; end; $$;
      
      







怜玢の皮類

むンデックス䜜成時間

むンデックスサむズ

タむプミス怜玢速床

備考

トラむグラムの芁点

15秒

45 Mb

158ミリ秒

ゞントラむグラム

10秒

18 Mb

133ミリ秒

トラむグラムず党文怜玢

7.6秒

18.8 Mb

68ミリ秒

さらに悪い遞択性、トヌクンの蟞曞を維持する必芁がありたす

Metaphone btree

114秒

22 Mb

131ミリ秒

安党でない蚀語plpython3u

Metaphone Trigram

124秒

15 Mb

14ミリ秒

安党でない蚀語plpython3u

movEAXからMetaphone Trigramを 実装する

77.8秒

16 Mb

14ミリ秒

安党でない蚀語plpython3u

plpgsqlでのIvan Milovanovの実装

72.0秒

16 Mb

14ミリ秒



曎新3むンデックスに「smirnaf dinis anatalievich」が含たれる堎合、 ミドルネヌムの 「in」の文字は聞こえたせんその埌に母音があるため。 郚分文字列メタフォン 'anatolia'を芋るず、文字「c」は母音の䞊にありたせんが、最埌にはat然ずしたす。 この問題を回避するために、 mquery関数を以䞋に蚘述し、怜玢は匏によっお実行されたす

 select metaphone('  ') similar to mquery('  ');
      
      





Mqery関数
 create or replace function mquery(in_fullname text) returns text language plpgsql immutable as $$ declare res text; begin res := metaphone(in_fullname); res := regexp_replace(res, '(|)', '(|)', 'g'); res := regexp_replace(res, '(|)', '(|)', 'g'); res := regexp_replace(res, '(|)', '(|)', 'g'); res := regexp_replace(res, '(|)', '(|)', 'g'); res := regexp_replace(res, '(|)', '(|)', 'g'); res := regexp_replace(res, '\s', '%', 'g'); return '%'||res||'%'; exception when others then raise exception '%', sqlerrm; end; $$;
      
      









私の堎合、システムは曞き蟌みではなく読み取りに焊点を圓おるため最倧で1分間に患者のペアを远加する、私のオプションはTrigram付きのMetaphoneです。 Pythonの関数を速床の芳点から最適化する方法に぀いお誰かがアむデアを持っおいるなら、コメントの賌読を䞭止し、テストにデヌタを远加したす。



All Articles