DoqueDB：Sudachi辞書で同義語検索しよう

**概要：** 全文検索を実用的に使うためには、**検索もれ**を起こさないことが重要です。キーワード検索を行う場合、表現のわずかな違いで検索できないケースが多々ありますが、同義語検索することでこのような検索もれを大幅に減らすことができます。今回は**Sudachi辞書**を使って同義語検索を行う方法を解説します。

## DoqueDBで同義語検索ができます

前回までの記事では**MySQL**や**PostgreSQL**との比較が主なトピックでしたが、今回は**Elasticsearch**などの全文検索エンジンと重なる分野の話になります。**同義語検索**を使うことで、DBMSの異表記正規化では実現できないレベルの、自由度の高い検索が可能になります。

Sudachi辞書の話に入る前に、DoqueDBの**同義語検索**について説明しておきましょう。いま、NTEXT型のカラムに一般的な日本語向け全文索引が作成されているとします。そのカラムに同義語を含むいくつかのデータを登録しておきます。

```sql
CREATE DATABASE syndb;
CREATE TABLE t (
  col NTEXT
);
CREATE FULLTEXT INDEX idx ON t(col)
  HINT 'KWIC, INVERTED=(INDEXING=DUAL,
        NORMALIZED=(STEMMING=FALSE, DELETESPACE=FALSE),
        TOKENIZER=DUAL:JAP:ALL:2 @NORMRSCID:1 @UNARSCID:1)';
INSERT INTO t VALUES('曖昧 不明確 あやふや 不明瞭 不確か');
INSERT INTO t VALUES('曖昧');
INSERT INTO t VALUES('不明確');
INSERT INTO t VALUES('あやふや');
INSERT INTO t VALUES('不明瞭');
INSERT INTO t VALUES('不確か');
```

上記のカラムに対してCONTAINS述語でキーワード検索を行う際に、**SYNONYM関数**を指定することで同義語検索することができます。SYNONYM関数には、同義語として扱いたいキーワードを引数として羅列します。

```sql
SQL> SELECT col FROM t WHERE col CONTAINS SYNONYM('曖昧' '不明確' 'あやふや');
{col}
{曖昧 不明確 あやふや 不明瞭 不確か}
{曖昧}
{不明確}
{あやふや}
SQL>
```

上記のとおり、同義語のいずれかを含むレコードが検索されました。とはいうものの、同義語検索など使わなくても、OR検索を使えば同じ結果が得られそうですよね？ 試してみます。

```sql
SQL> SELECT col FROM t WHERE col CONTAINS('曖昧'|'不明確'|'あやふや');
{col}
{曖昧 不明確 あやふや 不明瞭 不確か}
{曖昧}
{不明確}
{あやふや}
SQL>
```

はい、結果は同じでした。ではどこが違うのでしょう？ SCORE関数で検索キーワードとの適合度を表示してみます。

```sql
SQL> SELECT SCORE(col),col FROM t WHERE col CONTAINS SYNONYM('曖昧' '不明確' 'あやふや');
{score(col),col}
{2.17571518578366E-1,曖昧 不明確 あやふや 不明瞭 不確か}
{1.810195034572E-1,曖昧}
{1.76776858844922E-1,不明確}
{1.72728533833206E-1,あやふや}

SQL> SELECT SCORE(col),col FROM t WHERE col CONTAINS('曖昧'|'不明確'|'あやふや');
{score(col),col}
{6.90940260417859E-1,曖昧 不明確 あやふや 不明瞭 不確か}
{3.24281295556115E-1,曖昧}
{3.16680952691519E-1,不明確}
{3.09428717133697E-1,あやふや}
SQL>
```

違いがわかりました。同義語検索では複数種類のキーワードとマッチしても検索結果の適合度はそれほど変化しませんが、OR検索ではマッチしたキーワードが多いほど適合度が上昇しています。

したがって、たとえば複数の条件を組み合わせて検索し、結果を適合度の順にソートしたいような場面では、同義語検索を使うことで「**どのキーワードとマッチしてもよい**」という条件をうまい具合に (＝強すぎることなく) 表現できる、ということになるわけです。

でも、検索のたびに同義語を手で指定するのは面倒ですよね。世の中にある同義語辞書を使って自動的に同義語検索できれば便利そうです。はい、ちゃんとそういうものがあるのでした。

## Sudachi同義語辞書が使えます

同義語辞書を用いて特定のキーワードを複数の同義語に展開する処理を、DoqueDBでは**同義語展開**と呼んでいます。DoqueDBには同義語展開を用いた同義語検索の機能が用意されていますが、同義語辞書はユーザーが作成する必要があります。今回は**Sudachi同義語辞書**を利用してDoqueDBの同義語辞書を作成し、これを用いて同義語検索を行う事例を見ていきたいと思います。

なお、すぐにSudachi同義語辞書を用いた同義語検索を試してみたい方向けにバイナリ形式の辞書もご用意しております。「[**どうやったら使えるようになるの？**](https://qiita.com/items/e3a1b01a33d88e51e879/#%E3%81%A9%E3%81%86%E3%82%84%E3%81%A3%E3%81%9F%E3%82%89%E4%BD%BF%E3%81%88%E3%82%8B%E3%82%88%E3%81%86%E3%81%AB%E3%81%AA%E3%82%8B%E3%81%AE)」をご覧ください。

### まずは検索例をお見せしましょう

同義語展開は**EXPAND_SYNONYM関数**で行います。バイナリパッケージに含まれる**青空文庫**のサンプルデータベースを使って、「**前途**」を同義語検索してみましょう。KWIC関数により、検索結果の該当箇所付近を表示しています。詳細については第1回記事「[**DoqueDB：もうひとつの全文検索データベース**](https://qiita.com/junnohta999/items/ee4e8332a6ee3f8930bc)」をご覧ください。「前途」だけでなく「未来」「行く末」などが見つかっていますね。

```sql
SQL> SELECT title, lastName, firstName,
    KWIC(content FOR 30 ENCLOSE WITH '<<<' AND '>>>')
  FROM AozoraBunko
  WHERE content CONTAINS EXPAND_SYNONYM('前途')
  LIMIT 10;
{title,lastName,firstName,kwic(content for 30 enclose with '<<<' and '>>>')}
{青春の逆説,織田,作之助,気持になれなかった。（あの人は<<<前途>>>ある高等学校の学生さんだもの}
{海に生くる人々,葉山,嘉樹,入れることができた。それにはペンキで<<<未来>>>派の絵のような模様が、ベタ}
{西湖の屍人,海野,十三,ひとしかった。非常に心配して、<<<行く末>>>をいろいろと思い煩っている}
{海島冒険奇譚　海底軍艦,押川,春浪,覺えてしまうので、武村兵曹は此兒<<<將來>>>は非常に有望な撰手であると}
{思想と風俗,戸坂,潤,なっているが、今いった面白さは<<<将来>>>社会においても止揚されて伝承}
{光と風と夢,中島,敦,それ以外の職業に従っている<<<将来>>>の自分を想像して見ることが不可能}
{新版　放浪記,林,芙美子,ようなものと地図を私にくれた。<<<行く先>>>の私の仕事は、薬学生の助手}
{道標,宮本,百合子,いるでしょう？　大した力だと思うんです。何だか<<<未来>>>は底なしと}
{惜別,太宰,治,聞かせたが、あの最初の講義は、自分の<<<前途>>>を暗示し激励してくれている}
{津軽,太宰,治,財政もまた窮乏の極度に達し、<<<前途>>>暗澹たるうちにも、八代信明、}
SQL>
```

あるキーワードがどのように同義語展開されるかについてはSQLで確認できます。「USING 'UNA:1'」は全文索引作成時の「＠UNARSCID:1」と同じく、IDが1の形態素解析リソースを使用するという意味です。

```sql
SQL> SELECT EXPAND_SYNONYM('前途' USING 'UNA:1') FROM SYSTEM_SESSION;
{expand_synonym('前途' using 'UNA:1')}
{{前途,ＦＵＴＵＲＥ,フューチャー,先行き,将来,未来,行く先,行く末}}
SQL>
```

検索結果の4件めにある「將來」はここには出てきませんが、前回記事「[**DoqueDB：MySQL, PostgreSQLと正規化を比較してみる**](https://qiita.com/junnohta999/items/8e280748220b11d9f6a0)」で解説した、DoqueDBの正規化機能によって「将来」と同一視されています。

### Sudachi同義語辞書って何なの？

Sudachi同義語辞書というのは、[**ワークス徳島人工知能NLP研究所**](https://worksapplications.github.io/Sudachi/)が公開している、語数が約6万8千語、同義語グループ数が約2万5千グループの日本語同義語辞書です。DoqueDBと同じくApache License 2.0で公開されているため、商用でも利用できます。

公開されているのはソーステキストで、さまざまな属性をもつ同義語のグループが空行で区切られて並んでいます。前述の検索例に対応するのは以下のグループです。

```text
001051,1,0,1,0,0,0,(時間),前途,,
001051,1,0,2,0,0,0,(時間),将来,,
001051,1,0,3,0,0,0,(時間),未来,,
001051,1,0,4,0,0,0,(時間),行く先,,
001051,1,0,5,0,0,0,(時間),行く末,,
001051,1,0,6,0,0,0,(時間),先行き,,
001051,1,0,7,0,0,0,(時間),フューチャー,,
001051,1,0,7,0,0,1,(時間),future,,
```

Sudachi同義語辞書には表記以外にもいくつかの情報が格納されていますが、同義語展開に影響する情報として**展開制御フラグ**があります。第3カラムが「1」のエントリは、「自分自身が展開のトリガーとはならないが、同グループ内の別の見出しからは展開される」ことを意味します。つまり、「**一方向の展開を定義できる**」ことになります。このフラグは「その同義語グループ以外の語義も持っている」あるいは「同義語展開するとノイズが増えることが懸念される」語に設定されているようです。

たとえば以下のデータからは、

```text
001202,1,0,1,0,0,0,(),願い,,
001202,1,0,1,0,0,2,(),ねがい,,
001202,1,0,2,0,0,0,(),望み,,
001202,1,0,3,0,0,0,(),希望,,
001202,1,0,4,0,0,0,(),願望,,
001202,1,0,5,0,0,0,(),所望,,
001202,1,1,6,0,0,0,(),ホープ,,
001202,1,1,6,0,0,1,(),hope,,
...
025151,1,0,1,0,0,0,(人名),ホープ,,
025151,1,1,1,0,0,1,(人名),Hope,,
```

* 「願い」は自分自身と「ねがい」…「ホープ」「hope」に展開される
* 「ホープ」は自分自身と「Hope」にのみ展開される
* 「hope」「Hope」は自分自身以外には展開されない

ことがわかりますね。

EXPAND_SYNONYM関数の結果は以下のようになりました。英字が全角大文字になるのはDoqueDBの異表記正規化によるものです。

```sql
SQL> SELECT EXPAND_SYNONYM('願い' USING 'UNA:1') FROM SYSTEM_SESSION;
{expand_synonym('願い' using 'UNA:1')}
{{願い,ＨＯＰＥ,ホープ,ねがい,希望,所望,望み,願望}}

SQL> SELECT EXPAND_SYNONYM('ホープ' USING 'UNA:1') FROM SYSTEM_SESSION;
{expand_synonym('ホープ' using 'UNA:1')}
{{ホープ,ＨＯＰＥ}}

SQL> SELECT EXPAND_SYNONYM('ＨＯＰＥ' USING 'UNA:1') FROM SYSTEM_SESSION;
{expand_synonym('ＨＯＰＥ' using 'UNA:1')}
{{ＨＯＰＥ}}
SQL> 
```

### どうやったら使えるようになるの？

DoqueDBで使えるSudachi同義語辞書は、DoqueDBのバイナリパッケージには含まれていません。DoqueDBの[**doquedb-sudachi-synonymリポジトリ**](https://github.com/DoqueDB/doquedb-sudachi-synonym/)で公開されているバイナリ形式の辞書をダウンロードすることで、使えるようになります。インストール方法については[**README_ja.md**](https://github.com/DoqueDB/doquedb-sudachi-synonym/blob/main/README_ja.md)をご覧ください。辞書ソースからビルドする手段も用意されていますので、同義語辞書を自分で拡張することも可能です。

## DoqueDBの同義語展開のしくみ

辞書ビルドの流れと、同義語展開がどのように行われるのかをお見せしましょう。

辞書ビルドは、上記**README_ja.md**にも説明がありますが、GitHubのdoquedbリポジトリにあるuna/1.0/resource/tools/make/make-norm-sudachiで行います。Sudachi同義語辞書の前述のデータは、まず以下の形に加工されます。「*」で始まるデータは展開先にはなりますが、展開元にはなりません。(sudachi.tmp1)

```text
願い
ねがい
望み
希望
願望
所望
*ホープ
*hope
...
ホープ
*Hope
```

このデータから、展開元と展開先リストを並べた中間データが作られます。「ホープ」の展開先が自分自身と「ＨＯＰＥ」だけであること、「ＨＯＰＥ」を展開元とする行が存在しないことがおわかりいただけるでしょうか。(sudachi.tmp2)

```text
ホープ ホープ,ＨＯＰＥ
ねがい ねがい,ＨＯＰＥ,ホープ,希望,所望,望み,願い,願望
希望 希望,ＨＯＰＥ,ホープ,ねがい,所望,望み,願い,願望
所望 所望,ＨＯＰＥ,ホープ,ねがい,希望,望み,願い,願望
望み 望み,ＨＯＰＥ,ホープ,ねがい,希望,所望,願い,願望
願い 願い,ＨＯＰＥ,ホープ,ねがい,希望,所望,望み,願望
願望 願望,ＨＯＰＥ,ホープ,ねがい,希望,所望,望み,願い
```

同義語展開では、辞書引きと展開先の表記リストの取得に**形態素解析辞書**の構造を用いており、展開元を辞書語、展開先リストをアプリケーション情報として格納しています。

同義語展開の処理では、入力の各部分文字列についてこの辞書を用いて辞書引きを行い、辞書語とマッチした部分文字列を、アプリケーション情報のリストにある語で置き換えます。

したがって、EXPAND_SYNONYM関数には単語だけでなく文章を渡すこともできます。文章の中に複数の同義語があれば、そのすべての組み合わせが生成されます。(同義語検索された結果はそのまま文字列としてキーワード検索されるので、文章を渡すことに実用上の意味はあまりないのですが…)。

```sql
SQL> SELECT EXPAND_SYNONYM('希望の朝だ' USING 'UNA:1') FROM SYSTEM_SESSION;
{expand_synonym('希望の朝だ' using 'UNA:1')}
{{希望の朝だ,希望のＭＯＲＮＩＮＧだ,希望のモーニングだ,ＨＯＰＥの朝だ,ＨＯＰＥのＭＯＲＮＩＮＧだ,
ＨＯＰＥのモーニングだ,ホープの朝だ,ホープのＭＯＲＮＩＮＧだ,ホープのモーニングだ,ねがいの朝だ,
ねがいのＭＯＲＮＩＮＧだ,ねがいのモーニングだ,所望の朝だ,所望のＭＯＲＮＩＮＧだ,所望のモーニングだ,
望みの朝だ,望みのＭＯＲＮＩＮＧだ,望みのモーニングだ,願いの朝だ,願いのＭＯＲＮＩＮＧだ,
願いのモーニングだ,願望の朝だ,願望のＭＯＲＮＩＮＧだ,願望のモーニングだ}}
SQL>
```

## 注意すべき点がいくつかあります

同義語検索を使うときに注意すべき点がいくつかあります。ここではそれらについて見ていきましょう。

### 文字列としてマッチします

同義語展開された結果は、検索対象に対して文字列としてマッチします。つまり、単語の一部としてマッチするケースがあるということです。同義語検索の趣旨からすれば、それらはノイズということになります。全文索引の作成時にINDEXINGとしてWORD, NGRAM, DUALのいずれを指定した場合でも、基本的には変わりません。

例を見てみましょう。「アマチュア」を同義語検索したところ、同義語「アマ」が部分一致した作品が大量にヒットしました。

```sql
SQL> SELECT EXPAND_SYNONYM('アマチュア' USING 'UNA:1') FROM SYSTEM_SESSION;
{expand_synonym('アマチュア' using 'UNA:1')}
{{アマチュア,アマ,ＡＭＡＴＥＵＲ}}

SQL> SELECT title, lastName, firstName,
    KWIC(content FOR 30 ENCLOSE WITH '<<<' AND '>>>')
  FROM AozoraBunko
  WHERE content CONTAINS EXPAND_SYNONYM('アマチュア')
  LIMIT 10;
{title,lastName,firstName,kwic(content for 30 enclose with '<<<' and '>>>')}
{小熊秀雄全集-14,小熊,秀雄,でもふだんから、いちばん親孝行な、<<<アマ>>>ム・エチカッポ（雀のこと）}
{海島冒険奇譚　海底軍艦,押川,春浪,位置に立たせたら本國横濱の<<<アマチユーア>>>倶樂部の先生方には負けぬ}
{思想と風俗,戸坂,潤,られてある。こういう現代ブルジョア・アカデミー的カテゴリーの}
{光と風と夢,中島,敦,飽く迄首狩を固執しているが、ツ<<<アマ>>>サンガ族は首の代りに耳を斬取る}
{道標,宮本,百合子,出品されて注目をひいていた。<<<アマ>>>ンジャンのシャボン箱の絵のよう}
{世界の一環としての日本,戸坂,潤,方法を前進させたのだということは、非常に興味のある点である。}
{獄中への手紙,宮本,百合子,の役に立って居ります。ウォータ<<<アマ>>>ンです。
　こういうまとまり}
{獄中への手紙,宮本,百合子,は、あと四五回でまとまる由です。マ<<<アマ>>>アというところです。まる四}
{レ・ミゼラブル,豊島,与志雄,まで、一八一〇年には、ゾロ<<<アマ>>>ントー街の下とサルペートリエール}
{安吾の新日本地理,坂口,安吾,終戦直後には大昔と変りのない<<<アマ>>>の小舟でさかんに密輸や密入国}
SQL> 
```

対策としては、同義語辞書を作る際に、短いカタカナ語を同義語から除外する、などの方法が考えられます。ただし、現在公開されているバイナリ版辞書ではそのような配慮は行われていません。

### 登録された活用形しか展開されません

Sudachi同義語辞書には形容詞や動詞などの活用語が含まれます。たとえば以下のグループには「多い」や「比べる」などのエントリがあります。しかし、辞書データとして登録されているのは終止形だけです。

```text
001148,2,0,1,0,0,0,(),多い,,
001148,1,1,2,0,0,0,(),多数,,
...
001192,1,0,1,0,0,0,(),比較,,
001192,1,1,2,0,0,0,(),対比,,
001192,2,0,3,0,0,0,(),比べる,,
```

これらの語は非活用語と同じく同義語展開の対象になりますが、活用形はそのままで展開されます。したがって、同義語検索の結果には「多かった」「比べれば」などの活用形は含まれません。同義語検索と、活用形を指定したOR検索の結果を比べてみましょう。

```sql
SQL> SELECT EXPAND_SYNONYM('比較' USING 'UNA:1') FROM SYSTEM_SESSION;
{expand_synonym('比較' using 'UNA:1')}
{{比較,対比,比べる}}

SQL> SELECT title, lastName, firstName,
    KWIC(content FOR 30 ENCLOSE WITH '<<<' AND '>>>')
  FROM AozoraBunko
  WHERE content CONTAINS EXPAND_SYNONYM('比較')
  LIMIT 10;
{title,lastName,firstName,kwic(content for 30 enclose with '<<<' and '>>>')}
{歯車,芥川,竜之介,痛切だつた。僕はこの主人公に<<<比べる>>>と、どのくらゐ僕の阿呆だつた}
{本所両国,芥川,竜之介,の文明に疲れた生活上の落伍者が<<<比較>>>的大勢住んでゐた町である。}
{青春の逆説,織田,作之助,た。風呂屋も代替りをしなかった。<<<比較>>>的変遷の多い筈の薬屋も動か}
{海に生くる人々,葉山,嘉樹,者よりも、ブロックの方が<<<比較>>>にならぬほど重大なんだ、しかし、}
{西湖の屍人,海野,十三,そして年齢のころは皆、四十から下の<<<比較>>>的わかい男女であって、いずれ}
{電気看板の神経,海野,十三,感覚に危険を訴えて呉れるから、<<<比較>>>的安全だ。それに反して、電気}
{海島冒険奇譚　海底軍艦,押川,春浪,へば航海中船の動搖を感ずる事が<<<比較>>>的に少ない爲で、此室を占領}
{思想と風俗,戸坂,潤,安心と自信とをさえ齎すものだ。この際<<<比較>>>的安い映画館は何と云っても}
{光と風と夢,中島,敦,人としては異数のことだ。彼に<<<比べる>>>と、白人ではあるが、料理人}
{道標,宮本,百合子,いるような渋い鈍重な笑顔とは<<<比較>>>にならないほど、酸っぱい渋い}

SQL> SELECT title, lastName, firstName,
    KWIC(content FOR 30 ENCLOSE WITH '<<<' AND '>>>')
  FROM AozoraBunko
  WHERE content CONTAINS('比べた'|'比べれば'|'比べられ')
  LIMIT 10;
{title,lastName,firstName,kwic(content for 30 enclose with '<<<' and '>>>')}
{光と風と夢,中島,敦,あの奇矯な義人の博大な人間愛に<<<比べられ>>>た光栄を、先ず、感謝しよう}
{道標,宮本,百合子,や争いを、今夜の話の内容と<<<比べれば>>>、どちらの側にも打算がなく}
{大菩薩峠,中里,介山,生れた子供……この子供の不幸に<<<比べた>>>ら、わたしの不幸などは、言う}
{環礁,中島,敦,ココロ（心？）、ハミガキ等に<<<比べれば>>>、何といつても堂々たる名前}
{獄中への手紙,宮本,百合子,、一般音楽会のレベルの、文学と<<<比べられ>>>ぬ低さとの関係もあって、}
{日本文化私観,坂口,安吾,の大建築も、土俵の上の力士達に<<<比べれば>>>、余りに小さく貧弱である}
{ヤミ論語,坂口,安吾,つつあるではないか。これに<<<比べれば>>>、三文ダラク論者のヒネクレル}
{安吾の新日本地理,坂口,安吾,も知れぬ原始の山野を歩くのに<<<比べれば>>>、南鮮と北日本を結ぶ航海}
{環礁,中島,敦,ココロ（心？）、ハミガキなどに<<<比べれば>>>、何といっても堂々たる名前}
{茶話,薄田,泣菫,刀剣は皆なまくらで鏡と<<<比べた>>>らてんで談話にもならなかつたさうだ}
SQL> 
```

このように、同義語検索では活用語尾の展開は自動的には行われません。特定の活用形を同義語として検索したい場合は、その表記をユーザー自身で辞書に登録していただく必要があります。

## おわりに

今回の記事ではDoqueDBの同義語検索機能を紹介してきました。全文検索は往々にしてノイズとの闘いになりがちですが、同義語検索は上手に使えば検索の自由度を大きく高めることができます。同義語や類義語に限らなくても、同時に検索したいキーワードを同義語辞書にまとめておくことでクエリを単純化できるなど、使い方はさまざまです。ぜひこの機能をアプリケーションにお役立てください。

DoqueDBのWebサイトとGitHubのURLは以下のとおりです。今後も引き続き、DoqueDBの便利な活用方法を解説していきます。それでは、またお会いしましょう！

DoqueDB Webサイト
https://www.doquedb.ricoh.co.jp

GitHubリポジトリ
https://github.com/DoqueDB/doquedb

## 謝辞

自然言語処理の分野で多くの有益かつ高度な資源を公開されている、[**ワークス徳島人工知能NLP研究所**](https://worksapplications.github.io/Sudachi/)に感謝の意を表します。