DoqueDB：全文検索を高速化しよう

**概要**：DoqueDBでは全文索引の作り方を細かく調整できます。今回はWikipedia日本語版の本文データを対象に、全文検索を高速に行うための設定方法について解説します。

## 全文検索を高速化したい

皆さんは、ふだんの生活の中で全文検索をどれくらい活用されていらっしゃるでしょうか？

現在では、多くのシステムに全文検索機能が組み込まれており、ユーザーはそれと気づかずにさまざまな場面で全文検索の恩恵を受けています。システムが大きくなるにつれ、扱うデータも大規模なものになり、全文検索の応答時間も高速なものが要求されるようになってきました。

今回の記事では、データベースで全文検索を行う際に、どうすれば高速化できるかについて説明していきます。

### どれくらい高速化できるの？

効果を実感していただくために、最初に結果の一部をお見せしましょう。

以下はWikipedia日本語版本文のデータを登録し、表にあるテキストを検索したときの処理時間です。単位はミリ秒、データ件数は約140万になります。いずれも**N-gramベースの全文索引**を使用しています。

|検索テキスト|DoqueDB<br>標準設定|DoqueDB<br>高速設定|MySQL<br>+Mroonga|PostgreSQL<br>+PGroonga|
|:--|--:|:--:|:--:|:--:|
|第二次世界大戦|65.7|37.0|38.8|112.6|
|設置されている|270.7|63.7|162.0|224.0|
|おはようございます|13.0|4.0|11.2|16.9|
|ミルクチョコレート|16.3|4.7|16.1|23.4|

DoqueDB標準設定はドキュメントや解説記事で使われている標準的な全文索引設定、DoqueDB高速設定は今回解説する高速化を行ったものです。MySQLとPostgreSQLではストレージエンジンにGroongaを使い、トークナイザーとしてTokenBigramを指定しています。N-gramの詳細については以下の記事をご覧ください。

[**全文検索とは**](https://www.doquedb.ricoh.co.jp/blog/20241112/) (DoqueDB Webサイト)

### N-gramが苦手なこと

:::note info
結論だけ知りたい方は、この節を読み飛ばしてけっこうです。次の節「対策方法はあるの？」にお進みください。
:::

日本語の全文検索では、単語の区切りが明確でないことから、一般にN-gram方式の索引、ことに**Nを2としたBigram索引**がよく使われます。N-gram索引には原則として検索漏れが発生しないという利点があるのですが、処理にコストがかかるというデメリットもあります。

実例を見てみましょう。Wikipedia日本語版から「チンチロリン」を探すことにします。Nが2のとき、また3のとき、要素となる索引語を含む文書数は以下のとおりでした。

|索引語|出現文書数|
|:--:|--:|
|チン|102,281|
|チロ|5,171|
|リン|252,802|
|チンチ|702|
|ロリン|2,687|
|チンチロリン|13|

Nが2のときは、「チン」「チロ」「リン」の各要素語を検索し、得られた結果から、同じ文書で要素語の位置関係が正しいものをまとめて検索結果とすることになります。中間結果は10万件に及び、また3つの要素語の位置関係を突き合わせる必要があります。Nが3であれば、中間結果は数千件で済み、また位置関係の突き合わせも2つの要素語について行えばよいことになります。

#### Nが小さいと異なり数が少ない

コストに関係する要因の1つに、**異なる索引語がどの程度存在するか**という問題があります。これを**異なり数**と呼びます。Nが小さいと、文字種によっては異なり数が少なく、検索結果を絞り込む効果が弱くなるわけです。Nを大きくすれば、この問題は改善されます。

以下はひらがな、カタカナ、漢字のそれぞれについてNを1～4としたときの索引語の異なり数、出現文書数や総出現回数の平均値を示したものです。**漢字でNが2の場合**と同じ絞り込みの効果を得るためには、**ひらがな/カタカナではNを4にする必要がある**ことがわかりますね。

<table>
<thead>
<tr><th style="text-align:center;">文字<br>種別</th><th style="text-align:center;">N</th><th style="text-align:center;">異なり数</th><th style="text-align:center;">出現文書<br>(平均)</th><th style="text-align:center;">出現回数<br>(平均)</th><th style="text-align:center;">高頻度語</th></tr>
</thead>
<tbody>
<tr><td rowspan="4" style="text-align:center;">ひら<br>がな</td><td style="text-align:right;">1</td><td style="text-align:right;">83</td><td style="text-align:right;">515,427.5</td><td style="text-align:right;">4,940,773.3</td><td>の, に, た, る, は, と, し, を, で</td></tr>
<tr><td style="text-align:right;">2</td><td style="text-align:right;">6,144</td><td style="text-align:right;">14,507.0</td><td style="text-align:right;">34,383.0</td><td>して, てい, され, した, った</td></tr>
<tr><td style="text-align:right;">3</td><td style="text-align:right;">182,669</td><td style="text-align:right;">416.2</td><td style="text-align:right;">678.0</td><td>ている, である, された, として</td></tr>
<tr><td style="text-align:right;">4</td><td style="text-align:right;">1,034,878</td><td style="text-align:right;">51.4</td><td style="text-align:right;">67.8</td><td>れている, している, されてい</td></tr>
<tr><td rowspan="4" style="text-align:center;">カタ<br>カナ</td><td style="text-align:right;">1</td><td style="text-align:right;">85</td><td style="text-align:right;">420,510.2</td><td style="text-align:right;">2,504,663.1</td><td>ー, ン, ス, ル, ト, リ, イ, ア, ラ</td></tr>
<tr><td style="text-align:right;">2</td><td style="text-align:right;">6,725</td><td style="text-align:right;">10,613.3</td><td style="text-align:right;">24,910.2</td><td>リー, ール, ラン, ター, スト</td></tr>
<tr><td style="text-align:right;">3</td><td style="text-align:right;">228,751</td><td style="text-align:right;">275.4</td><td style="text-align:right;">536.3</td><td>ション, アメリ, メリカ, ランド</td></tr>
<tr><td style="text-align:right;">4</td><td style="text-align:right;">1,111,454</td><td style="text-align:right;">40.0</td><td style="text-align:right;">72.4</td><td>アメリカ, シーズン, ーション</td></tr>
<tr><td rowspan="4" style="text-align:center;">漢字</td><td style="text-align:right;">1</td><td style="text-align:right;">13,375</td><td style="text-align:right;">12,220.2</td><td style="text-align:right;">32,925.2</td><td>年, 日, 月, 大, 人, 本, 学, 国, 中</td></tr>
<tr><td style="text-align:right;">2</td><td style="text-align:right;">1,490,240</td><td style="text-align:right;">94.6</td><td style="text-align:right;">159.9</td><td>日本, 大学, 学校, 放送, 現在</td></tr>
<tr><td style="text-align:right;">3</td><td style="text-align:right;">6,332,077</td><td style="text-align:right;">12.1</td><td style="text-align:right;">16.9</td><td>合衆国, 選手権, 委員会, 株式会</td></tr>
<tr><td style="text-align:right;">4</td><td style="text-align:right;">8,058,176</td><td style="text-align:right;">5.5</td><td style="text-align:right;">7.2</td><td>株式会社, 高等学校, 世界大戦</td></tr>
</tbody>
</table>

#### Nが大きいと前方一致検索が必要

では、Nを大きくしたらどうなるでしょうか？

たとえば、ひらがなのNが4のときに「あ」を検索するとします。この場合、「あ」と一致する索引語が存在しないため、「あ」で始まるすべての索引語を**前方一致検索**し、結果をマージする必要があります。Wikipedia日本語版の場合、「あ」で始まるひらがな4文字の索引語は17,843種類ありました (そのうち半数近くは「あぁぁぁ」のように出現回数1回のみの索引語でしたが)。

前方一致検索も結果のマージも、それほどコストのかかる処理ではありませんが、検索語の長さがNからかけ離れるほど対象となる件数が増え、処理にかかる時間が増大するということですね。

### 対策方法はあるの？

N-gramが苦手とするケースがあることはわかりました。それでは、そのようなケースについてどのように対策すれば、高速な全文検索が可能になるでしょうか？

実現可能かどうかはさておき、いくつか対策方法を考えてみましょう。

#### ①最適なNを選ぶ

検索する文字列に特定の傾向があるなら、その傾向に合わせてNを決めるという方法があります。短い文字列での検索がめったに発生しない場合、Nを3以上にすることで高速化できます。たとえばカタカナ語をメインで検索する場合が該当します。

**Groonga**では、TokenBigramの汎用版であるTokenNgramを使うことで、Nの値を自由に指定できます。

#### ②文字種によりNを変える

英数字、ひらがな、カタカナではNを3以上にすると高速化が期待できますが、Nを大きくすることには短い文字列の検索にコストがかかるというデメリットもあります。Nが2でも十分な**異なり数**が確保できる漢字については、Nを2のままにしたいところです。文字種ごとにNを決められるなら、文字種に合ったNを設定します。

#### ③複数のNを混在させる

どのNを選ぶか悩むくらいなら、さまざまなNを用意すればいいのではないか？という考え方もありますね。検索する文字列に合わせて、最適なNの組み合わせをシステムに選ばせるというわけです。かなりの高速化が期待できそうですが、索引サイズは大幅に増えることになります。

## DoqueDBのトークナイザー

DoqueDBでは、高速な日本語全文検索を実現するために、トークナイザーにさまざまな工夫をしています。詳しく見ていきましょう。

### 細かい設定ができます

:::note info
結論だけ知りたい方は、この節を読み飛ばしてけっこうです。次の節「どんな索引語が作られるの？」にお進みください。
:::

全文索引の作成時に**ヒント句**を記述することで、どのように索引を作るかを細かく設定することができます。索引タイプを**INDEXING**で、トークナイズ指定を**TOKENIZER**で指定します。以下は全文索引スキーマの例です。

```sql
CREATE FULLTEXT INDEX jawiki_idx ON jawiki(content)
  HINT 'KWIC, DELAYED, INVERTED=(
    NORMALIZED=(STEMMING=FALSE, DELETESPACE=FALSE), INDEXING=DUAL,
    TOKENIZER=DUAL:JAP:ALL:2 KAN:2:4 HIR:3:5 KAT:3:5 KAN:HIR KAN:KAT
    @NORMRSCID:1 @UNARSCID:1)';
```

#### 索引タイプ (INDEXING=...)

どんな全文索引を作るのかを指定します。索引タイプにより、利用できる検索方法が変わります。

<dl>
<dt>NGRAM</dt><dd>N-gram索引を作ります。</dd><br>
<dt>WORD</dt><dd>単語索引を作ります。今回は説明を割愛します。</dd><br>
<dt>DUAL</dt><dd>N-gram索引を作り、単語境界の情報も格納します。<br>これにより、単語索引と同様に単語単位の検索が可能になります。</dd>
</dl>

#### トークナイズ指定 (TOKENIZER=...)

入力されたテキストから、どのように索引語を切り出すかを指定します。全文検索において最も重要な部分です。

<dl>
<dt>NGR:&lt;N&gt; [&lt;言語処理リソース&gt; ...]</dt>
<dd>
・<b>N-gramトークナイザー</b>を使用して、長さNのN-gram要素を切り出します。Nには1～8の数値が指定できます。対象テキスト末尾にかかる場合は、Nより短い要素が切り出されることがあります。<br>
・&lt;N&gt;のかわりに「&lt;最小値&gt;:&lt;最大値&gt;」として範囲を指定することも可能です。このときは、最小値から最大値まですべての長さの要素が切り出されます。<br>
・入力は指定された<b>言語処理リソース(※)</b>で異表記正規化されます。<br>
・索引タイプがNGRAMのときに指定できます。</dd><br>
<dt>BNG:JAP:ALL:&lt;N&gt; [&lt;文字種&gt;:&lt;N&gt; ...] [&lt;文字種連接指定&gt;] [&lt;言語処理リソース&gt; ...]</dt>
<dd>
・日本語に対応した<b>ブロック化N-gramトークナイザー</b>を使用します。文字種が同じ文字の並びをブロックとし、各ブロックについて長さNのN-gram要素を切り出します。ブロックの末尾にかかる場合は、Nより短い要素が切り出されることがあります。<br>
・&lt;N&gt;のかわりに「&lt;最小値&gt;:&lt;最大値&gt;」として範囲を指定することも可能です。<br>
・文字種を指定すると、文字種ごとの切り出し長さを指定することが可能です。<br>
・&lt;文字種連接指定&gt;として「&lt;文字種&gt;:&lt;文字種&gt;」を記述すると、その文字種の並びはブロックの境界としません。<br>
・入力は指定された<b>言語処理リソース</b>で異表記正規化されます。<br>
・索引タイプがNGRAMのときに指定できます。
</dd><br>
<dt>DUAL:JAP:ALL:&lt;N&gt; [&lt;文字種&gt;:&lt;N&gt; ...] [&lt;文字種連接指定&gt;] [&lt;言語処理リソース&gt; ...]</dt>
<dd>
・<b>ブロック化N-gramトークナイザー</b>を使用してN-gram要素を切り出します。<br>
・&lt;N&gt;のかわりに「&lt;最小値&gt;:&lt;最大値&gt;」として範囲を指定することも可能です。<br>
・入力は指定された<b>言語処理リソース</b>により形態素解析および異表記正規化が行われ、N-gram要素とともに語境界の情報が格納されます。<br>
・索引タイプがNGRAM, WORD, DUALのときに指定できます。
</dd>
</dl>

※ 言語処理リソースについては、DoqueDBユーザーズマニュアルの [**E. 異表記正規化辞書**](https://doquedb.github.io/doquedb/users.html#secappe) をご覧ください。

### どんな索引語が作られるの？

ここでは、以下の4種類の索引タイプ/トークナイズ指定を選択したとき、どんな索引語が作られるのか見てみます。言語処理リソースについては省略しています。

|種別|ヒント句|説明|
|:--:|:--|:--|
|N-gram|INDEXING=NGRAM,<br>TOKENIZER=NGR:2|・N-gram索引<br>・長さ2のN-gram|
|ブロック化<br>N-gram|INDEXING=NGRAM,<br>TOKENIZER=BNG:JAP:ALL:2|・N-gram索引<br>・長さ2の日本語ブロック化N-gram|
|DUAL<br>標準版|INDEXING=DUAL,<br>TOKENIZER=DUAL:JAP:ALL:2|・DUAL索引 (単語単位検索が可能)<br>・長さ2の日本語ブロック化N-gram|
|DUAL<br>高速版|INDEXING=DUAL,<br>TOKENIZER=DUAL:JAP:ALL:2<br>KAN:2:4　HIR:3:5　KAT:3:5<br>KAN:HIR&nbsp;&nbsp;KAN:KAT|・DUAL索引 (単語単位検索が可能)<br>・長さ2の日本語ブロック化N-gram<br>・漢字は2～4文字、ひらがな/カタカ<br>　ナは3～5文字で切り出し<br>・漢字→ひらがな/カタカナの変化は<br>　ブロックの境界としない|

**DUAL標準版**は、Qiita記事「[**DoqueDB：MySQL, PostgreSQLと構文を比較してみる**](https://qiita.com/junnohta999/items/0afe45f8050bbc4af2ed)」、Webサイトの性能比較記事「[**MySQL, PostgreSQLと全文検索性能を比較する**](https://www.doquedb.ricoh.co.jp/blog/20240905/)」で使われている、DoqueDBの標準的な設定です。**DUAL高速版**は、前述の「対策方法はあるの？」で示した対策を施したバージョンです。

たとえば「**冷やし中華始めました**」を入力すると、索引語は以下のように切り出されます。**DUAL高速版**では、さまざまな検索文字列に対して、少ない索引語の組み合わせで検索できそうなことがわかりますね。

|位置|N-gram|ブロック化<br>&nbsp;&nbsp;N-gram,<br>DUAL標準版|DUAL高速版|
|:--:|:--|:--|:--|
|1|冷や|冷|冷や|
|2|やし|やし|冷やし|
|3|し中|し|やし|
|4|中華|中華|し|
|5|華始|華始|中華|
|6|始め|始|中華始|
|7|めま|めま|中華始め|
|8|まし|まし|華始|
|9|した|した|華始め|
|10|た|た|華始めま|
|11|||始め|
|12|||始めま|
|13|||始めまし|
|14|||めまし|
|15|||めました|
|16|||ました|
|17|||した|
|18|||た|

ブロック化N-gramの場合、ブロック境界はユーザーからは見えませんし、意識する必要もありません。それとは別に、ユーザーから見える境界として、形態素の区切りである**語境界**があります。ちょっと横道にそれて、その話をしましょう。

### 単語単位でも検索したい

場合によっては、単語の断片とマッチさせたくないこともあります。たとえば「京都」で「東京都」が見つかってほしくないような場面です。Groongaでは、TokenBigramのかわりにTokenMecabを使って**形態素解析方式**の索引を作れば、そのような検索を行うことができます。

DoqueDBでは、索引タイプにDUALを指定することで、入力が形態素解析され、N-gram索引を作るとともに**語境界**の情報が登録されます。上記の例であれば、以下の「/」の位置が語境界になります。

<dl><dd>/冷やし/中華/始め/まし/た/</dd></dl>

検索時に検索テキストを **SIMPLEWORD()** で囲むと、検索テキストの先頭と末尾が語境界にマッチするものだけが検索されます。たとえば「contains('京都新宿')」は「東京都新宿区」にもマッチしますが、「contains simpleword('京都新宿')」は「東京都新宿区」にマッチしません。

```sql
$ sqli -remote localhost 54321 -user root -password doqadmin -database jawiki
SQL>select id,title,kwic(content for 30 enclose with '/') from jawiki
    where content contains('京都新宿') limit 3;
{id,title,kwic(content for 30 enclose with '/')}
{237,赤塚不二夫,横山孝雄、高井研一郎等と東/京都新宿/区十二社（現在の西新宿）に}
{489,倉田真由美,制作会社「たまくら」（東/京都新宿/区）が東京国税局の税務調査で}
{743,くじらいいく子,日 - ）は、日本の漫画家。東/京都新宿/区出身。女性。青年漫画誌}
SQL>select id,title,kwic(content for 30 enclose with '/') from jawiki
    where content contains simpleword('京都新宿') limit 3;
{id,title,kwic(content for 30 enclose with '/')}
SQL>
```

## 高速化してみました

さて、高速化のためにどうすればよいかはわかったので、実際に試してみることにしましょう。

Wikipedia日本語版本文をデータベースに登録したうえで、N-gram、ブロック化N-gram、DUAL標準版、DUAL高速版の全文索引を作成し、さまざまなテキストで検索にかかった時間を調べました。本文の取得にかかるコストを除外したいので、検索結果の件数のみ取得しています。

比較のため、MySQL+Mroonga、PostgreSQL+PGroongaでの処理時間も掲載しています。これらは記事作成時点の最新版でないため、参考値とお考えください。

### 全文索引のスキーマ

全文索引のスキーマと検索クエリは以下のとおりです。

**DoqueDB**

```sql
CREATE DATABASE jawiki;

CREATE TABLE jawiki (
  id      INT,
  title   NTEXT,
  content NTEXT,
  PRIMARY KEY(id)
);

-- 全文索引：N-gram
CREATE FULLTEXT INDEX jawiki_idx ON jawiki(content)
  HINT 'KWIC, DELAYED, INVERTED=(
    NORMALIZED=(STEMMING=FALSE, DELETESPACE=FALSE), INDEXING=NGRAM,
    TOKENIZER=NGR:2 @NORMRSCID:1 @UNARSCID:1)';

-- 全文索引：ブロック化N-gram
CREATE FULLTEXT INDEX jawiki_idx ON jawiki(content)
  HINT 'KWIC, DELAYED, INVERTED=(
    NORMALIZED=(STEMMING=FALSE, DELETESPACE=FALSE), INDEXING=NGRAM,
    TOKENIZER=BNG:JAP:ALL:2 @NORMRSCID:1 @UNARSCID:1)';

-- 全文索引：DUAL標準版
CREATE FULLTEXT INDEX jawiki_idx ON jawiki(content)
  HINT 'KWIC, DELAYED, INVERTED=(
    NORMALIZED=(STEMMING=FALSE, DELETESPACE=FALSE), INDEXING=DUAL,
    TOKENIZER=DUAL:JAP:ALL:2 @NORMRSCID:1 @UNARSCID:1)';

-- 全文索引：DUAL高速版
CREATE FULLTEXT INDEX jawiki_idx ON jawiki(content)
  HINT 'KWIC, DELAYED, INVERTED=(
    NORMALIZED=(STEMMING=FALSE, DELETESPACE=FALSE), INDEXING=DUAL,
    TOKENIZER=DUAL:JAP:ALL:2 KAN:2:4 HIR:3:5 KAT:3:5 KAN:HIR KAN:KAT
    @NORMRSCID:1 @UNARSCID:1)';

SELECT COUNT(*) FORM jawiki WHERE content CONTAINS('おはようございます');
```

比較対象とするMySQL+Mroonga、PostgreSQL+PGroongaのスキーマと検索クエリも示します。トークナイザーはTokenBigramの標準設定です。

**MySQL+Mroonga**

```sql
CREATE DATABASE jawiki;

CREATE TABLE jawiki (
  id        INT PRIMARY KEY,
  title     TEXT,
  content   MEDIUMTEXT NOT NULL
) ENGINE = Mroonga DEFAULT CHARSET=utf8mb4
  COLLATE=utf8mb4_ja_0900_as_cs_ks;
  
CREATE FULLTEXT INDEX jawiki_idx
  ON jawiki(content)
  COMMENT 'tokenizer "TokenBigram",
           normalizer "NormalizerAuto"';

SELECT COUNT(*) FROM jawiki 
  WHERE MATCH(content) AGAINST('おはようございます' IN BOOLEAN MODE);
```

**PostgreSQL+PGroonga**

```sql
CREATE DATABASE jawiki
  WITH ENCODING 'UTF8'
  LC_COLLATE  = 'ja_JP.UTF8'
  LC_CTYPE    = 'ja_JP.UTF8'
  TEMPLATE    = template0;
  
CREATE TABLE jawiki (
  id        INT PRIMARY KEY,
  title     TEXT,
  content   TEXT
);

CREATE INDEX jawiki_idx
  ON jawiki USING pgroonga(content)
  WITH (tokenizer='TokenBigram',
        normalizers='NormalizerAuto');

SELECT COUNT(*) FROM jawiki WHERE content &@ 'おはようございます';
```

### DBサイズは大きくなります

作成されたデータベースのファイルサイズを索引種別ごとに見てみましょう。ブロック化N-gramやDUAL標準版は文字種別をまたぐ索引語を作らないため、単純なN-gramより小さくなります。DUAL標準版は語境界の情報をもつため、ブロック化N-gramより少し大きくなります。また、DUAL高速版は多くの索引語を生成するため、サイズがかなり大きくなるほか、全文索引の作成にもサイズの増加に見合った時間がかかります。

|索引種別|N-gram|ブロック化<br>N-gram|DUAL標準版|DUAL高速版|
|:--:|:--:|:--:|:--:|:--:|
|DBサイズ|7.0GB|5.8GB|5.9GB|13GB|

さて、もったいを付けるのもこれぐらいにしておきましょうか。

### おお、速いぞ！

それでは結果です！ ダララララララ… (ドラムロール)

まず、Wikipedia日本語版本文で出現回数の多いテキストの結果をお見せしましょう。ブロック化N-gramの検索速度はDUAL標準版と大差ないため、割愛します。件数はMroonga/PGroongaによるもので、DoqueDBでは正規化の仕様が異なるため、多少前後します。処理時間の単位はミリ秒です。

DUAL高速版の検索速度については、DUAL標準版の数倍～10倍程度高速という結果になりました。ひらがなの長い並びはN-gram検索の不得意とするところですが、そのような検索でも十分な性能が得られています。

<table>
<thead>
<tr><th style="text-align:center;" rowspan="2">検索テキスト</th><th style="text-align:center;" rowspan="2">件数</th><th style="text-align:center;" colspan="3">DoqueDB</th><th style="text-align:center;" rowspan="2">MySQL+<br>Mroonga</th><th style="text-align:center;" rowspan="2">Postgre<br>SQL+PG<br>roonga</th></tr>
<tr><th style="text-align:center;" >N-gram</th><th style="text-align:center;" >DUAL<br>標準版</th><th style="text-align:center;" >DUAL<br>高速版</th></tr>
</thead>
<tbody>
<tr><td>新型コロナウイルス</td><td style="text-align:right;">12501</td><td style="text-align:right;">56.7</td><td style="text-align:right;">63.7</td><td style="text-align:right;">21.7</td><td style="text-align:right;">23.3</td><td style="text-align:right;">60.1</td></tr>
<tr><td>第二次世界大戦</td><td style="text-align:right;">37812</td><td style="text-align:right;">67.0</td><td style="text-align:right;">65.7</td><td style="text-align:right;">37.0</td><td style="text-align:right;">38.8</td><td style="text-align:right;">112.6</td></tr>
<tr><td>中華人民共和国</td><td style="text-align:right;">19912</td><td style="text-align:right;">35.7</td><td style="text-align:right;">37.0</td><td style="text-align:right;">25.7</td><td style="text-align:right;">19.4</td><td style="text-align:right;">63.4</td></tr>
<tr><td>以下の通りである</td><td style="text-align:right;">19258</td><td style="text-align:right;">116.3</td><td style="text-align:right;">250.3</td><td style="text-align:right;">41.3</td><td style="text-align:right;">86.9</td><td style="text-align:right;">143.1</td></tr>
<tr><td>設置されている</td><td style="text-align:right;">21133</td><td style="text-align:right;">169.0</td><td style="text-align:right;">270.7</td><td style="text-align:right;">63.7</td><td style="text-align:right;">162.0</td><td style="text-align:right;">224.0</td></tr>
<tr><td>考えられている</td><td style="text-align:right;">31867</td><td style="text-align:right;">285.7</td><td style="text-align:right;">214.7</td><td style="text-align:right;">67.0</td><td style="text-align:right;">193.6</td><td style="text-align:right;">271.8</td></tr>
<tr><td>行われた</td><td style="text-align:right;">126918</td><td style="text-align:right;">215.3</td><td style="text-align:right;">370.7</td><td style="text-align:right;">55.7</td><td style="text-align:right;">142.9</td><td style="text-align:right;">319.1</td></tr>
<tr><td>されるようになった</td><td style="text-align:right;">20554</td><td style="text-align:right;">400.7</td><td style="text-align:right;">397.7</td><td style="text-align:right;">37.0</td><td style="text-align:right;">324.3</td><td style="text-align:right;">385.7</td></tr>
<tr><td>することができる</td><td style="text-align:right;">29953</td><td style="text-align:right;">254.7</td><td style="text-align:right;">255.0</td><td style="text-align:right;">54.0</td><td style="text-align:right;">185.4</td><td style="text-align:right;">250.3</td></tr>
<tr><td>することになった</td><td style="text-align:right;">23834</td><td style="text-align:right;">417.3</td><td style="text-align:right;">423.0</td><td style="text-align:right;">40.7</td><td style="text-align:right;">299.2</td><td style="text-align:right;">356.2</td></tr>
<tr><td>となっている</td><td style="text-align:right;">150871</td><td style="text-align:right;">679.7</td><td style="text-align:right;">666.7</td><td style="text-align:right;">135.0</td><td style="text-align:right;">483.9</td><td style="text-align:right;">656.4</td></tr>
<tr><td>されている</td><td style="text-align:right;">376035</td><td style="text-align:right;">688.0</td><td style="text-align:right;">679.0</td><td style="text-align:right;">150.0</td><td style="text-align:right;">699.3</td><td style="text-align:right;">1040.9</td></tr>
<tr><td>チャンピオンズリーグ</td><td style="text-align:right;">4591</td><td style="text-align:right;">72.0</td><td style="text-align:right;">68.7</td><td style="text-align:right;">12.3</td><td style="text-align:right;">35.6</td><td style="text-align:right;">58.7</td></tr>
<tr><td>インターナショナル</td><td style="text-align:right;">10499</td><td style="text-align:right;">76.7</td><td style="text-align:right;">77.3</td><td style="text-align:right;">19.3</td><td style="text-align:right;">50.9</td><td style="text-align:right;">90.1</td></tr>
<tr><td>バスケットボール</td><td style="text-align:right;">11385</td><td style="text-align:right;">57.3</td><td style="text-align:right;">54.0</td><td style="text-align:right;">21.7</td><td style="text-align:right;">33.4</td><td style="text-align:right;">69.9</td></tr>
<tr><td>インターネット</td><td style="text-align:right;">23489</td><td style="text-align:right;">86.7</td><td style="text-align:right;">83.0</td><td style="text-align:right;">26.3</td><td style="text-align:right;">51.5</td><td style="text-align:right;">104.8</td></tr>
</tbody>
</table>

次に、出現回数の少ないテキストの検索結果です。こちらはさまざまなパターンで安定して高速ですね。

<table>
<thead>
<tr><th style="text-align:center;" rowspan="2">検索テキスト</th><th style="text-align:center;" rowspan="2">件数</th><th style="text-align:center;" colspan="3">DoqueDB</th><th style="text-align:center;" rowspan="2">MySQL+<br>Mroonga</th><th style="text-align:center;" rowspan="2">Postgre<br>SQL+PG<br>roonga</th></tr>
<tr><th style="text-align:center;" >N-gram</th><th style="text-align:center;" >DUAL<br>標準版</th><th style="text-align:center;" >DUAL<br>高速版</th></tr>
</thead>
<tbody>
<tr><td>高等小学校を卒業し</td><td style="text-align:right;">103</td><td style="text-align:right;">20.3</td><td style="text-align:right;">95.3</td><td style="text-align:right;">6.0</td><td style="text-align:right;">13.5</td><td style="text-align:right;">20.7</td></tr>
<tr><td>間違えられることが</td><td style="text-align:right;">99</td><td style="text-align:right;">42.3</td><td style="text-align:right;">81.7</td><td style="text-align:right;">6.7</td><td style="text-align:right;">51.1</td><td style="text-align:right;">63.9</td></tr>
<tr><td>体育大会陸上競技</td><td style="text-align:right;">98</td><td style="text-align:right;">6.3</td><td style="text-align:right;">6.0</td><td style="text-align:right;">4.7</td><td style="text-align:right;">6.6</td><td style="text-align:right;">11.4</td></tr>
<tr><td>世界仰天ニュース</td><td style="text-align:right;">97</td><td style="text-align:right;">9.7</td><td style="text-align:right;">10.7</td><td style="text-align:right;">5.3</td><td style="text-align:right;">18.5</td><td style="text-align:right;">23.0</td></tr>
<tr><td>されたものであるため</td><td style="text-align:right;">101</td><td style="text-align:right;">334.7</td><td style="text-align:right;">332.0</td><td style="text-align:right;">7.7</td><td style="text-align:right;">190.4</td><td style="text-align:right;">230.4</td></tr>
<tr><td>おはようございます</td><td style="text-align:right;">86</td><td style="text-align:right;">12.7</td><td style="text-align:right;">13.0</td><td style="text-align:right;">4.0</td><td style="text-align:right;">11.2</td><td style="text-align:right;">16.9</td></tr>
<tr><td>はっきりしたことは</td><td style="text-align:right;">101</td><td style="text-align:right;">90.0</td><td style="text-align:right;">85.7</td><td style="text-align:right;">4.7</td><td style="text-align:right;">49.3</td><td style="text-align:right;">76.6</td></tr>
<tr><td>ことにしたのである</td><td style="text-align:right;">102</td><td style="text-align:right;">272.0</td><td style="text-align:right;">267.3</td><td style="text-align:right;">7.3</td><td style="text-align:right;">148.6</td><td style="text-align:right;">186.9</td></tr>
<tr><td>ワンポイントリリーフ</td><td style="text-align:right;">83</td><td style="text-align:right;">28.3</td><td style="text-align:right;">23.3</td><td style="text-align:right;">3.7</td><td style="text-align:right;">28.8</td><td style="text-align:right;">49.7</td></tr>
<tr><td>クォーターファイナル</td><td style="text-align:right;">83</td><td style="text-align:right;">29.7</td><td style="text-align:right;">26.7</td><td style="text-align:right;">5.3</td><td style="text-align:right;">23.6</td><td style="text-align:right;">33.2</td></tr>
<tr><td>ミルクチョコレート</td><td style="text-align:right;">64</td><td style="text-align:right;">17.3</td><td style="text-align:right;">16.3</td><td style="text-align:right;">4.7</td><td style="text-align:right;">16.1</td><td style="text-align:right;">23.4</td></tr>
</tbody>
</table>




以上のとおり、高速化という目的については満足できる結果が得られました。あとは索引サイズの増加が容認できるかどうかですね。

比較対象とさせていただいたMySQL+Mroonga、PostgreSQL+PGroongaについては、トークナイザーとしてTokenMecabを使うことにより、単語境界をまたがない検索に限定されますが、検索速度が数倍に向上する可能性があることを申し添えておきます。全般的な性能比較については、DoqueDB Webサイトにある以下の記事をご覧ください。

[**DoqueDB：MySQL, PostgreSQLと全文検索性能を比較する**](https://www.doquedb.ricoh.co.jp/blog/20240905/) (DoqueDB Webサイト)

### 測定環境

測定は以下の環境で行いました。

* 環境：VMware上の仮想環境
* CPU：8 vCPU
* メモリ：16GB
* OS：AlmaLinux 9.4

ソフトウェアバージョンは以下のとおりです。

* DoqueDB：1.1.1
* MySQL：8.0.41
* Mroonga：14.14
* PostgreSQL：16.4
* PGroonga：3.2.4
* Groonga：14.0.9

処理時間の計測は以下の方法で行いました。

<details><summary>DoqueDB：sqliに-timeオプションを指定</summary>

```sql
$ sqli -remote localhost 54321 -user root -password doqadmin -database jawiki -time
SQL>select count(*) from jawiki where content contains('おはようございます');
{count(*)}
{86}
TIME: 6 ms
SQL>
```
</details>

<details><summary>MySQL：show profilesを使う</summary>

```sql
$ mysql mysql -A -u root -p
Enter password: 
...
mysql> set profiling = 1;
Query OK, 0 rows affected, 1 warning (0.03 sec)

mysql> set profiling_history_size = 1;
Query OK, 0 rows affected, 1 warning (0.00 sec)

mysql> use jawiki;
Database changed
mysql> select count(*) from jawiki where match(content)
    -> against('おはようございます' in boolean mode);
+----------+
| COUNT(*) |
+----------+
|       86 |
+----------+
1 row in set (0.01 sec)

mysql> show profiles;
+------------------------------------------------------------------------------+
| Query_ID | Duration   | Query                                                |
+------------------------------------------------------------------------------+
|        1 | 0.01335800 | SELECT COUNT(*) FROM jawiki WHERE MATCH(content) ... |
+------------------------------------------------------------------------------+
1 row in set, 1 warning (0.00 sec)

mysql>
```
</details>

<details><summary>PostgreSQL：\timingをONにする</summary>

```sql
$ sudo -u postgres psql
[sudo] *** のパスワード:
psql (16.4)
"help"でヘルプを表示します。

postgres=# \c jawiki;
データベース"jawiki"にユーザー"postgres"として接続しました。
jawiki=# \timing
タイミングは on です。
jawiki=# select count(*) from jawiki where content &@ 'おはようございます';
 count 
-------
    86
(1 行)

時間: 19.914 ミリ秒
jawiki=#
```
</details>

**特記事項**

* DoqueDB以外のソフトウェアは記事公開時点より前のバージョンであり、最新版では性能が改善されている可能性があります。
* PostgreSQLでは初期状態で割り当てられているメモリが少ないため、shared_buffersを128MB → 4GB、work_memを4MB → 100MBに増やしています。

## おわりに

今回の記事では、索引語の切り出し方を細かく調整することで、さまざまな場面で全文検索を高速に行う方法についてご紹介しました。辞書に拾いきれない長いカタカナ語を高速に検索できることは、たとえば医療の現場における診療情報サマリの検索など、さまざまな場面で威力を発揮してくれるでしょう。

DoqueDB WebサイトとGitHubリポジトリのURLを以下に掲載しておきます。それでは、またお会いしましょう！

**DoqueDB Webサイト**
[https://www.doquedb.ricoh.co.jp](https://www.doquedb.ricoh.co.jp)

**GitHubリポジトリ**
[https://github.com/DoqueDB/doquedb](https://github.com/DoqueDB/doquedb)