象と戯れ

 | 

2008-07-15

ひらがな←→カタカナ

14:34 | ひらがな←→カタカナ - 象と戯れ を含むブックマーク はてなブックマーク - ひらがな←→カタカナ - 象と戯れ

お題はこちら。

http://mlog.euqset.org/archives/pgsql-jp.ml.postgresql.jp/39492.html

検索条件として 'トマト'、 'とまと'、 'トマと'

のいずれで検索しても トマトのレコードが取得できる。

つまり、アルファベットならばILIKEとかlower()関数とかがあって正規化はシンプルなのですが、日本語には「ひらがな」と「カタカナ」があるので正規化が標準装備されていることはまれですね。

で、自分の回答。

以前こんなことをしたことがあります。

where translate(query, 'あいうえおかきくけこさしすせそたちつてと...', 'アイウエオカキクケコサシスセソタチツテト...') =

translate(name, 'あいうえおかきくけこさしすせそたちつてと...', 'アイウエオカキクケコサシスセソタチツテト...')

ひらがなを逐一カタカナへ正規化してやれば、漏れなく検索できるというしくみ。ソースコードにべた書きできるし他の文字セットでも単純に利用できるので手軽ではあるのですが、我ながらひどいw。ちなみに、これをやるときは必ずひらがな→カタカナにしたほうがよいです。なぜならカタカナにはひらがなにない文字「ヴ」などがあるから(文字セットにもよる)。

で、その後のメールで「パフォーマンスは?」というご指摘を頂いたので、どのぐらい遅いのだろうと思って調べる事にしました。

調べる前にtranslate()関数の挙動を推測。

  1. ソース文字列から一字ずつ取り出して、
  2. 変換元の先頭からマッチする一字があるかどうかを調べ、
  3. あればその位置と同じ位置にある変換先の文字へ置換える

というわけなので、最大でO(NM)ってところでしょうか。M(変換テーブル)が長いときにN(ソース文字列)がMに含まれないような文字ばかり(今回のケースでは漢字とか)だと、効率は限りなくO(NM)に近づきますね。

そんなわけでデータ探し。手っ取り早くIPA辞書Mecabのパッケージからゲット。

DROP TABLE IF EXISTS ipa_name;

CREATE TABLE ipa_name(
  val varchar,
  unknown1 int,
  unknown2 int,
  unknown3 int,
  hinshi1 varchar,
  hinshi2 varchar,
  type1 varchar,
  type2 varchar,
  unknown4 varchar,
  unknown5 varchar,
  body varchar,
  yomi1 varchar,
  yomi2 varchar
);

SET client_encoding TO EUC_JP;
COPY ipa_name FROM '/tmp/Noun.name.csv' CSV;

内容としては、こんなかんじのテーブルです。

SELECT count(*) FROM ipa_name; --> 34202
SELECT relpages FROM pg_class WHERE relname = 'ipa_name'; --> 606

で、実行。

EXPLAIN ANALYZE
SELECT * FROM ipa_name
WHERE 'トマト' = val;

Seq Scan on ipa_name  (cost=0.00..772.65 rows=67 width=332) (actual time=6.182..7.865 rows=1 loops=1)
  Filter: ('トマト'::text = (val)::text)
Total runtime: 7.895 ms
EXPLAIN ANALYZE
SELECT * FROM ipa_name
WHERE translate('とまと', 
'あいうえおかきくけこさしすせそたちつてとなにぬねのはひふへほまみむめもやゆよわをんがぎぐげござじずぜぞだぢづでどばびぶべぼぱぴぷぺぽっゃゅょ',
'アイウエオカキクケコサシスセソタチツテトナニヌネノハヒフヘホマミムメモヤユヨワヲンガギグゲゴザジズゼゾダヂヅデドバビブベボパピプペポッャュョ'
) = translate(val, 
'あいうえおかきくけこさしすせそたちつてとなにぬねのはひふへほまみむめもやゆよわをんがぎぐげござじずぜぞだぢづでどばびぶべぼぱぴぷぺぽっゃゅょ',
'アイウエオカキクケコサシスセソタチツテトナニヌネノハヒフヘホマミムメモヤユヨワヲンガギグゲゴザジズゼゾダヂヅデドバビブベボパピプペポッャュョ'
)

Seq Scan on ipa_name  (cost=0.00..805.98 rows=67 width=332) (actual time=156.302..184.534 rows=1 loops=1)
  Filter: ('トマト'::text = translate((val)::text, 'あいうえおかきくけこさしすせそたちつてとなにぬねのはひふへほまみむめもやゆよわをんがぎぐげござじずぜぞだぢづでどばびぶべぼぱぴぷぺぽっゃゅょ'::text, 'アイウエオカキクケコサシスセソタチツテトナニヌネノハヒフヘホマミムメモヤユヨワヲンガギグゲゴザジズゼゾダヂヅデドバビブベボパピプペポッャュョ'::text))
Total runtime: 184.559 ms

ウヰスキー」の「ヰ」も考慮するとなると、さらに変換表が増えますね。

ちなみに、別解として他の方から、正規表現をつかう方法もありました。

EXPLAIN ANALYZE
SELECT * FROM ipa_name
WHERE val ~ '(と|ト)(ま|マ)(と|ト)';

Seq Scan on ipa_name  (cost=0.00..1033.53 rows=87 width=94) (actual time=29.168..34.399 rows=1 loops=1)
  Filter: ((val)::text ~ '(と|ト)(ま|マ)(と|ト)'::text)
Total runtime: 34.424 ms

この方法だと、クライアントアプリケーションが賢くSQLを生成しないといけないので、translateの方法に比べるとちょっと面倒ですが、こちらも一度作ってしまえばメンテは楽でしょう。

使いやすさとパフォーマンスを考えて、ご利用は計画的に

 |