DoqueDB：MySQL, PostgreSQLと構文を比較してみる

**概要：** 類似文書検索を想定して、データべース作成からデータのロード、検索までの流れを競合ソフトウェアと比較してみました。いくつかの環境で検索結果も比較します。データはおなじみの青空文庫です。

## DoqueDB正式公開しました

長らくお待たせいたしましたが、ようやく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)

こちらでは引き続き紹介記事を書いていくことにしましょう。今回の内容は、競合ソフトウェアとの構文の違いを詳しく比較するというものです。市場において広く経験を重ね、すでに高い評価を得ているソフトウェアたちに対して、あつかましくも **競合ソフトウェア** と呼ばせていただくこと、平にご容赦願います。これらのソフトウェアの開発に当たられている方々には、生暖かくご覧いただければと思います。

## どんなソフトウェアと比較するの？

**DoqueDB** は、もともと日本語の全文検索エンジンとして開発された技術を、リレーショナルデータベースへと再構成して作られたソフトウェアです。社内では、特許検索システムなどでかなりの期間にわたって使われてきました。

日本語の全文検索では、昨今はGroongaが世間を賑わしている状況ですから、ここでもMySQLやPostgreSQLとGroongaとの組み合わせと比較することにします。従来技術の延長として、MySQLとInnoDBの組み合わせも見てみましょう。具体的な比較対象は以下のものになります。
* MySQL + Mroonga (ストレージモード) + Bigram索引/MeCab索引
* MySQL + InnoDB + Bigram索引/MeCab索引
* PostgreSQL + PGroonga + Bigram索引/MeCab索引
* DoqueDB + Bigram索引

比較に用いたバージョンは以下のとおりです。
* MySQL：8.0.34
* Mroonga：13.05
* PostgreSQL：15.5
* postgresql15-pgdg-pgroonga：3.1.6
* DoqueDB：1.0.0

## データベースとテーブルを作る

青空文庫を**類似文書検索**するデータベースを作ることにします。テーブルは1つだけで、カラムは作品ID、タイトル、作者姓、作者名、青空文庫URL、本文(先頭にタイトル、作者姓名を含む)とします。全文検索は本文に対して行うことになりますが、全文索引はあとで作成します。

### MySQL + Mroonga (ストレージモード)

```sql
CREATE DATABASE aozoradb;

USE aozoradb;
CREATE TABLE aozorabunko (
  docid     INT PRIMARY KEY,
  title     VARCHAR(256),
  lastname  VARCHAR(128),
  firstname VARCHAR(128),
  url       VARCHAR(128),
  content   MEDIUMTEXT NOT NULL
) ENGINE=Mroonga DEFAULT CHARSET=utf8mb4
  COLLATE=utf8mb4_ja_0900_as_cs_ks;
```

[**Mroonga**](https://mroonga.org/ja/)は全文検索エンジン[**Groonga**](https://groonga.org/ja/)をベースとしたMySQL用のストレージエンジンです。全文検索のみをGroongaで行うラッパーモードと、データストアもGroongaで行うストレージモードの2つの使い方が用意されています。

Mroongaをストレージモードで使うには、ENGINEにMroongaを指定します。青空文庫に含まれる日本語文字はutf8の範囲内ですが、ここではDEFAULT CHARSETとして、utf8では表現できないUnicode追加面の文字(𠮷, 𩸽, 😀など)も格納できるutf8mb4を選択しています。

照合順序にはutf8mb4_ja_0900_as_cs_ksを指定しています。asはアクセント記号(かなの濁点、半濁点も含む)を区別すること、csは大文字小文字を区別すること、ksはひらがなとカタカナを区別することを意味します。照合順序ではソート順も変更されます。utf8mb4_ja_0900_as_cs_ksでは、かなについては「きゃ < キャ < きや < キヤ」の順、漢字についてはJIS X0208順(JIS第1水準漢字は代表音訓の順、JIS第2水準漢字は部首・画数順)に並びます。

MySQLのVARCHARやTEXTは日本語を格納できます。本文(content)は64KBを超えるため、TEXTでなく、16MBまで格納できるMEDIUMTEXTを使います。MroongaはNULL値を許容しないため、NOT NULLも指定します。

### MySQL + InnoDB

```sql
CREATE DATABASE aozoradb;

USE aozoradb;
CREATE TABLE aozorabunko (
  FTS_DOC_ID BIGINT UNSIGNED
    AUTO_INCREMENT NOT NULL PRIMARY KEY,
  docid     INT,
  title     VARCHAR(256),
  lastname  VARCHAR(128),
  firstname VARCHAR(128),
  url       VARCHAR(128),
  content   MEDIUMTEXT
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
  COLLATE=utf8mb4_ja_0900_as_cs_ks;
```

ストレージエンジンにInnoDBを使うときは、ENGINEにInnoDBを指定します。全文検索を行うテーブルにはカラムFTS_DOC_IDを定義し、プライマリキーとする必要があります。contentにNOT NULLを指定する必要はありません。それ以外はMySQL + Mroongaと同じです。

### PostgreSQL + PGroonga

```sql
CREATE DATABASE aozoradb
  WITH ENCODING 'UTF8'
  LC_COLLATE  = 'ja_JP.UTF8'
  LC_CTYPE    = 'ja_JP.UTF8'
  TEMPLATE    = template0;
  
\connect aozoradb
CREATE TABLE aozorabunko (
  docid     INT PRIMARY KEY,
  title     VARCHAR(256),
  lastname  VARCHAR(128),
  firstname VARCHAR(128),
  url       VARCHAR(128),
  content   TEXT
);
```

PostgreSQLでは、データベース作成時にエンコーディングとロケールを指定します。PostgreSQLのUTF8はMySQLのutf8mb4と同様、Unicode追加面の文字も格納できます。照合順序(LC_COLLATE)にはja_JP.UTF8を指定しているため、文字コードが異なる文字は区別され、ソートは文字コード順に行われます。

PostgresQLのVARCHARやTEXTは日本語を格納できます。TEXTには実用上の長さ上限はありません。

### DoqueDB

```sql
CREATE DATABASE aozoradb;
```

DoqueDBでは、今のところSQLクライアントの中から接続先データベースを切り替える手段が用意されていません。いったんSQLクライアントを抜けて、新しく作ったデータベースを指定して接続し、テーブルを作成します。

```sql
CREATE TABLE aozorabunko (
  docid     INT,
  title     NVARCHAR(256),
  lastname  NVARCHAR(128),
  firstname NVARCHAR(128),
  url       VARCHAR(128),
  content   NTEXT,
  PRIMARY KEY(docid)
);
```

DoqueDBでは、シングルバイト文字を格納するときはVARCHARやTEXT、マルチバイト文字を格納するときはNVARCHARやNTEXTを指定します。NTEXTには実用上の長さ上限はありません。


NVARCHARやNTEXTは、Unicode文字をUCS2の並びとして格納します。MySQLのutf8mb4やPostgreSQLのutf8と同様に、Unicode追加面の文字も格納できますが、内部的にはサロゲートペアに相当するUCS2の2文字として扱われます。入出力や検索は通常問題ありませんが、SUBSTRING関数やKWIC関数でサロゲートペアが分割されることがあり、その場合、残った断片は**置換文字**(U+FFFD �, UTF-8では0xEF 0xBF 0xBD)になります。

また、DoqueDBでは照合順序をサポートしていません。PostgreSQLと同様、文字コードが異なる文字は区別され、ソートは文字コード順に行われます。

なお、DoqueDBのSQLクライアントは現在のところ行編集機能を提供していません。対話的にお使いになる場合は、**rlwrap**などの行編集ツールと組み合わせてご利用ください。

## データを一括登録する

データベースができたら、青空文庫のデータをテーブルaozorabunkoに一括登録します。データはCSV形式で、/tmp/data.csvに置かれているものとします。

1レコードは作品ID、タイトル、作者姓、作者名、青空文庫URL、本文をカンマで区切って並べたもので、各レコードは改行で終了します。作品ID以外のデータは二重引用符(")で囲まれており、改行を含むことがあります。以下に例を示します。

```csvs
000014,"あばばばば","芥川","竜之介","https://www.aozora.gr.jp/cards/000879/files/14_14602
.html","あばばばば
芥川龍之介

　保吉はずつと以前からこの店の主人を見知つてゐる。
　ずつと以前から、――或はあの海軍の学校へ赴任した当日だつたかも知れない。彼はふとこの店へマツチを一つ
買ひにはひつた。店には小さい飾り窓があり、窓の中には大将旗を掲げた軍艦三笠の模型のまはりにキユラソオ
の壜だのココアの罐だの干し葡萄の箱だのが並べてある。..."
```

### MySQL + Mroonga

```sql
LOAD DATA INFILE '/tmp/data.csv'
  INTO TABLE aozorabunko
  FIELDS TERMINATED BY ',' OPTIONALLY ENCLOSED BY '"'
  LINES TERMINATED BY '\n';
```

MySQLではLOAD DATA文を使います。FIELDS句でカラムがカンマ(,)で終わること、二重引用符(")で囲まれているカラムがあることを指定します。LINES句はデフォルト値と同じですが、FIELDS句との対比のために指定しています。

### MySQL + InnoDB

```sql
LOAD DATA INFILE '/tmp/data.csv'
  INTO TABLE aozorabunko
  FIELDS TERMINATED BY ',' OPTIONALLY ENCLOSED BY '"'
  LINES TERMINATED BY '\n'
  (docid, title, lastname, firstname, url, content);
```

MySQL + InnoDBでは、LOAD DATA文を使うのはMySQL + Mroongaと同じですが、FTS_DOC_IDは登録の対象でないため、登録するカラムを列挙して明示します。

### PostgreSQL

```sql
COPY aozorabunko
  (docid, title, lastname, firstname, url, content)
  FROM '/tmp/data.csv'
  WITH (FORMAT CSV, DELIMITER ',', QUOTE '"', ESCAPE '"');
```

PostgreSQLではCOPYコマンドを使います。フォーマットとしてCSV、カラムの区切り文字としてカンマ(,)を使うこと、囲み文字として二重引用符(")を使うことを指定しています。

### DoqueDB

```sql
INSERT INTO aozorabunko
  INPUT FROM PATH '/tmp/data.csv'
  HINT 'Code="utf-8" InputField=(1,2,3,4,5,6)';
```

DoqueDBでは、INSERT文で一括登録指定(INPUT FROM)することで、CSVからの一括登録を実行できます。HINT句ではCodeで文字コードを指定し、InputFieldでCSV中のどのフィールドを使用するかを指定します。ここでは指定していませんが、FieldSeparatorやRecordSeparatorを使うと区切り文字を変更できます。

ここでは本文データもCSVファイル内にありますが、第1回記事「[**DoqueDB：もうひとつの全文検索データベース**](https://qiita.com/junnohta999/items/ee4e8332a6ee3f8930bc "https://qiita.com/junnohta999/items/ee4e8332a6ee3f8930bc")」で示したように、カラムを「FILE <パス名>」とすることで、データを個別に外部ファイルから読み込むことも可能です。

## 全文検索ってどんなふうに動くの？

データを一括登録したら、本文を検索するための全文索引を作成します。その前に、全文検索がどんなふうに動いているのかをざっと見ておくことにしましょう。

### テキストを分割して索引語を作る

全文検索では、テキストを分割して索引語を作ります。これを**トークナイズ**と言います。分割方法としてはN-gramと形態素解析が一般的です。**N-gram**とは、任意の位置から始まるN文字を索引語とするもので、Nを2とした**Bigram**がよく使われます。Nが1のときは**Unigram**と呼ばれます。[**形態素解析**](https://ja.wikipedia.org/wiki/%E5%BD%A2%E6%85%8B%E7%B4%A0%E8%A7%A3%E6%9E%90)はテキストを形態素(構文の最小単位)に分割するものです。検索対象として「東京都千代田区」を登録する場合、トークナイズは以下のように行われます。

| Unigram | Bigram | 形態素解析 |
|:-------:|:------:|:----------:|
| 東      | 東京   | 東京       |
| 京      | 京都   | 都         |
| 都      | 都千   | 千代田     |
| 千      | 千代   | 区         |
| 代      | 代田   |            |
| 田      | 田区   |            |
| 区      |        |            |

### 転置索引を作って検索する

次に、これらの索引語を使って**転置索引**と呼ばれる構造を作ります。これは索引語が文書内のどの位置にあるかを値としてもつ構造で、位置情報から文字列を取り出す通常のアクセスとは逆方向であるため「**転置**」と呼ばれます。上の例では次のような転置索引が作られます。(Bigramの「区」については後述します。)

| Unigram | Bigram  | 形態素解析 |
|:-------:|:-------:|:----------:|
| 東, 0   | 東京, 0 | 東京, 0    |
| 京, 1   | 京都, 1 | 都, 2      |
| 都, 2   | 都千, 2 | 千代田, 3  |
| 千, 3   | 千代, 3 | 区, 6      |
| 代, 4   | 代田, 4 |            |
| 田, 5   | 田区, 5 |            |
| 区, 6   | (区, 6) |            |

これらの索引に対して「東京都」を検索してみます。 Unigramでは「東」「京」「都」、Bigramでは「東京」「京都」、形態素解析では「東京」「都」がそれぞれ見つかり、また位置関係も正しいので、結果として「東京都千代田区」が見つかることになります。

### 細かい挙動はけっこう違う

では、索引語の断片はどうなるでしょうか？その結果はシステムにより異なります。**DoqueDB**や**Groonga**では索引語を前方一致検索しており、Bigramで作られた索引でも任意の1文字が検索できます。形態素解析で作られた索引でも先頭からの部分文字列は見つかりますが、途中からの部分文字列は見つかりません。**InnoDB**では、使われているMySQL標準の全文検索が前方一致検索しないらしく、Bigramの索引では任意の1文字、また形態素解析の索引では形態素の部分文字列が見つかりません。また、デフォルトでは1文字の索引語が作られません。各組み合わせで何が検索できるかを表にしておきます。

<table>
  <thead><tr>
    <th rowspan=2>検索語</th>
    <th colspan=2 style="text-align: center;">Groonga</th>
    <th colspan=2 style="text-align: center;">InnoDB ※1</th>
    <th>DoqueDB</th>
  </tr>
  <tr>
    <th>Bigram</th><th>形態素解析</th>
    <th>Bigram</th><th>形態素解析</th>
    <th style="text-align: center;">Bigram</th>
  </tr></thead>
  <tr>
    <td>東</td>
    <td style="text-align: center;">○</td>
    <td style="text-align: center;">○</td>
    <td style="text-align: center;">×</td>
    <td style="text-align: center;">×</td>
    <td style="text-align: center;">○</td>
  </tr><tr>
    <td>東京</td>
    <td style="text-align: center;">○</td>
    <td style="text-align: center;">○</td>
    <td style="text-align: center;">○</td>
    <td style="text-align: center;">○</td>
    <td style="text-align: center;">○</td>
  </tr><tr>
    <td>東京都</td>
    <td style="text-align: center;">○</td>
    <td style="text-align: center;">○</td>
    <td style="text-align: center;">○</td>
    <td style="text-align: center;">○</td>
    <td style="text-align: center;">○</td>
  </tr><tr>
    <td>京都</td>
    <td style="text-align: center;">○</td>
    <td style="text-align: center;">×</td>
    <td style="text-align: center;">○</td>
    <td style="text-align: center;">×</td>
    <td style="text-align: center;">○</td>
  </tr><tr>
    <td>都</td>
    <td style="text-align: center;">○</td>
    <td style="text-align: center;">○</td>
    <td style="text-align: center;">×</td>
    <td style="text-align: center;">×</td>
    <td style="text-align: center;">○</td>
    </tr><tr>
    <td>千代</td>
    <td style="text-align: center;">○</td>
    <td style="text-align: center;">○</td>
    <td style="text-align: center;">○</td>
    <td style="text-align: center;">×</td>
    <td style="text-align: center;">○</td>
   </tr><tr>
    <td>千代田</td>
    <td style="text-align: center;">○</td>
    <td style="text-align: center;">○</td>
    <td style="text-align: center;">○</td>
    <td style="text-align: center;">○</td>
    <td style="text-align: center;">○</td>
   </tr><tr>
    <td>代田</td>
    <td style="text-align: center;">○</td>
    <td style="text-align: center;">×</td>
    <td style="text-align: center;">○</td>
    <td style="text-align: center;">×</td>
    <td style="text-align: center;">○</td>
   </tr><tr>
    <td>区</td>
    <td style="text-align: center;">○ ※2</td>
    <td style="text-align: center;">○</td>
    <td style="text-align: center;">×</td>
    <td style="text-align: center;">×</td>
    <td style="text-align: center;">○ ※2</td>
  </tr>
</table>

　※1：設定変更により、Groongaと同じ検索結果が得られる索引を作ることが可能です。これについては後述します。
　※2：Bigramでは末尾の1文字から始まる索引語は作られないはずですが、特別扱いされているようです。

ご覧いただいたように、形態素解析で作られた索引では、各形態素の2文字め以降から始まる部分文字列は見つからないことがわかりました。とはいえ、これはデメリットとは限りません。全文検索であっても、「京都」で「東京都」が見つかってほしくないケースもあるからです。また、形態素解析による索引のほうが、一般に検索が高速であるとされています。どういう検索を行いたいのか、また使われている形態素解析器のクセ(複合語をどこまで分割するか、など)を理解したうえで、Bigram索引か形態素解析索引かを選択するとよいでしょう。

もうひとつ、英数字記号の扱いにも注意が必要です。たとえば英単語は部分一致してほしくないかもしれません。「early」で「nearly」「clearly」が見つかってほしくない、などの場合です。形態素解析索引はこのようなケースを適切に扱えますが、Bigram索引でも英数字列については特別扱いするものがあります。

## 全文索引を作る

さて、だいぶ脇道にそれてしまいました。全文検索の動きが理解できたところで、それぞれのシステムで本文に対して全文索引を作ることにしましょう。

ここでは日本語を全文検索するために、各システムで標準的に使われていると思われる設定を採用しています。後述する検索結果も含めて、同一条件で比較していないことにご注意ください。

### MySQL + Mroonga + Bigram索引

```sql
CREATE FULLTEXT INDEX mroonga_bigram_index
  ON aozorabunko(content)
  COMMENT 'tokenizer "TokenBigram",
           normalizer "NormalizerAuto"';
```

MySQL + MroongaではCREATE FULLTEXT INDEX文で全文索引を作成します。Bigram索引を作るため、COMMENT句のtokenizerで「TokenBigram」を指定します。索引の作成時には正規化方式も指定できます。ここではnormalizerに「NormalizerAuto」を指定することで、英数字記号カタカナの全角半角、**英字の大文字小文字を区別せずに**検索できるようにしています。

おや？ちょっと待ってください！データベースの作成時に照合順序にcsを指定して、**大文字小文字を区別する**ようにしていたのではありませんでしたっけ？

実は、照合順序と正規化のどちらを使っても、異なる文字を同一視することができます。それぞれ以下のような特徴があります。

<table>
  <thead><tr>
    <th style="text-align: center"; >同一視の手段</th>
    <th style="text-align: center"; >特徴</th>
  </tr></thead>
  <tr>
    <td>照合順序で同一視する</td>
    <td>・検索時に区別されない<br>
        ・同一視した文字はORDER BYで同じ順位になる<br>
        ・同一視の自由度はあまり高くない</td>
  </tr>
  <tr>
    <td>正規化で同一視する</td>
    <td>・検索時に区別されない<br>
        ・同一視した文字でもORDER BYの順位は異なる<br>
        ・同一視の自由度が高い(「クヮ」「カ」の同一視など)</td>
  </tr>
</table>

同一視した文字をソート時にも同じ順位にしたいケースを別とすれば、一般的には正規化で同一視するほうが自由度が高く、扱いやすいでしょう。

なお、TokenBigramをnormalizerとともに指定すると、半角英数字記号のようなASCII文字については空白区切りでトークナイズを行います。したがって、「early」で「nearly」「clearly」は見つからないようになっています。

### MySQL + Mroonga + MeCab索引

```sql
CREATE FULLTEXT INDEX mroonga_mecab_index
  ON aozorabunko(content)
  COMMENT 'tokenizer "TokenMecab",
           normalizer "NormalizerAuto"';
```

上と同じですが、tokenizerに「TokenMecab」を指定しています。TokenMecabでは、広く使われている形態素解析器[**MeCab**](https://ja.wikipedia.org/wiki/MeCab)を用いて形態素解析を実行し、全文索引を作ります。normalizerには同じくNormalizerAutoを指定します。

ここまでMroongaのベースであるGroongaのtokenizerとnormalizerについて説明してきましたが、実はもっと細かい指定が可能です。たとえばTokenBigramでは、英数字記号や空白でどう区切るかをさらに指定できます。normalizerでは、どんな文字を同一視するかを個別に指定する手段が用意されています。正規化については記事を改めて比較させていただくことにしましょう！

### MySQL + InnoDB + Bigram索引

```sql
CREATE FULLTEXT INDEX inno_ngram_index
  ON aozorabunko(content)
  WITH PARSER ngram;
```

InnoDBの場合も、Mroongaと同様にCREATE FULLTEXT INDEX文で全文索引を作成します。WITH PARSER句で「ngram」を指定することにより、MySQLのN-gram全文パーサが使われます。

InnoDBのBigram索引では、「nearly」「clearly」ではそれらだけが見つかりますが、「early」を検索したときはそれ以外の単語も見つかるようです。何が見つかっているのかはよくわかりません。

N-gram全文パーサはデフォルトでBigram索引を作りますが、/etc/my.cnfでトークンサイズを以下のように変更することにより、Unigram索引が作られるようになります。

```text
[mysqld]
ngram_token_size=1
```

後述する検索例はngram_token_size=2のときのものです。

### MySQL + InnoDB + MeCab索引

```sql
CREATE FULLTEXT INDEX inno_mecab_index
  ON aozorabunko(content)
  WITH PARSER mecab;
```

上と同じですが、WITH PARSER句で「mecab」を指定することにより、MySQLのMeCab全文パーサを用いた形態素解析索引が作成されます。

デフォルトでは前述の「都」や「区」のような1文字の索引語は作られませんが、/etc/my.cnfで以下のように設定すれば、1文字の索引語も作られるようになります。

```text
[mysqld]
innodb_ft_min_token_size=1
```

後述する検索例はinnodb_ft_min_token_size=2のときのものです。

### PostgreSQL + PGroonga + Bigram索引

```sql
CREATE EXTENSION IF NOT EXISTS pgroonga;
CREATE INDEX pgroonga_bigram_index
  ON aozorabunko USING pgroonga(content)
  WITH (tokenizer='TokenBigram',
        normalizers='NormalizerAuto');
```

[**PGroonga**](https://pgroonga.github.io/ja/)はPostgreSQLで日本語全文検索を可能にする、**Groonga**ベースの拡張モジュールです。あらかじめCREATE EXTENSION文により拡張機能として組み込んでおきます。

PostgreSQLで全文索引を作成するには、CREATE INDEX文にUSING pgroonga()を指定します。tokenizerとnormalizerを指定することはMySQLと同じですが、PostgreSQLではWITH句を用います。ここではBigram索引を作るため、WITH句のtokenizerに「TokenBigram」を指定し、またnormalizerに「NormalizerAuto」を指定しています。

TokenBigramをnormalizerとともに指定した場合、MySQL + Mroonga + Bigram索引と同様に「early」で「nearly」「clearly」は見つかりません。

### PostgreSQL + PGroonga + MeCab索引

```sql
CREATE EXTENSION IF NOT EXISTS pgroonga;
CREATE INDEX pgroonga_mecab_index
  ON aozorabunko USING pgroonga(content)
  WITH (tokenizer='TokenMecab',
        normalizers='NormalizerAuto');
```

上と同じですが、WITH句のtokenizerに「TokenMecab」を指定することで、MeCab索引が作られるようになります。

### DoqueDB + Bigram索引

```sql
CREATE FULLTEXT INDEX doquedb_bigram_index ON aozorabunko(content)
  HINT 'KWIC, DELAYED,
        INVERTED=(NORMALIZED=(STEMMING=FALSE, DELETESPACE=FALSE),
        INDEXING=DUAL,
        TOKENIZER=DUAL:JAP:ALL:2 @NORMRSCID:1 @UNARSCID:1)';
```

DoqueDBで全文索引を作成するにはCREATE FULLTEXT INDEX文を使います。索引の仕様はHINT句で指定します。上記の指定は以下のような意味をもちます。

| ヒント指定        | 説明                                         |
|:------------------|:---------------------------------------------|
| KWIC              | KWIC関数を使うために必要な情報を格納する     |
| DELAYED           | 全文索引に遅延更新を適用する                 |
| INVERTED=         | 転置ヒントの指定                             |
| NORMALIZED=       | 転置正規化ヒントの指定                       |
| STEMMING=FALSE    | 転置ファイル作成時にステミングを行わない     |
| DELETESPACE=FALSE | 転置ファイル作成時に空白を削除しない         |
| INDEXING=DUAL     | 日本語はN-gramで、英字は単語で索引語を作る   |
| TOKENIZER=        | トークナイザーの指定                         |
| DUAL:JAP:ALL:2    | 索引がDUALで対象は日本語, N-gramのNを2とする |
| ＠NORMRSCID:1     | 正規化にID:1のリソースを使う              |
| ＠UNARSCID:1      | 形態素解析にID:1のリソースを使う          |

KWIC関数については後述します。英字については単語単位で索引語が作られるため、「early」を検索しても「nearly」や「clearly」は見つかりません。

正規化と形態素解析には、DoqueDB組込みの形態素解析器・正規化器を使います。ここではID:1の正規化リソースを指定しているため、全角半角の同一視、大文字小文字の同一視、外来語表記ゆれの同一視などが行われます。現在のところ、リソースはID:1の1種類しか用意されていません。先ほどお話ししたとおり、正規化については後日また記事を改めてご紹介します。

### トランザクションへの対応

データベースの重要な機能のひとつに、トランザクションがあります。これは複数の処理をアトミックに行うためのものです。トランザクションを開始して複数のテーブルを更新する場合、すべてが成功したらコミットを行うことで処理が確定します。いずれかに失敗したらロールバックすれば、すべての処理をなかったことにできます。

Mroongaはトランザクションに対応していません。したがって、全文索引の対象カラムにデータを登録後、ロールバックすると、対象カラムのデータと索引エントリは残ることになり、同時に操作したほかのカラムとの不整合が発生します。このようなケースに対応するには、たとえばレプリケーション構成をとり、マスターのストレージエンジンをInnoDB、スレーブをMroongaとします。更新をマスターで、検索をスレーブで行うようにすれば、トランザクション対応と高速な検索を両立できます。

MySQL + InnoDB、PostgreSQL + PGroonga、DoqueDBの全文索引はいずれもトランザクションに対応しているため、上記のような対策は不要です。

## 類似文書検索を行う

ようやくここまで来ました。それでは各システムで類似文書検索を実行してみましょう。検索を行うSQL構文はストレージエンジンや索引によらず同じであるため、各システムについて1種類のみ示します。

検索するテキストは、前回記事「[**DoqueDB：デモサイトで青空文庫を検索しよう**](https://qiita.com/junnohta999/items/7175d95d1a64fe573090)」でもサンプルとして取り上げた「**船が難破し無人島に漂着した少年たちが島で生き延びて生還する**」です。類似文書検索というのは「文書で文書を検索する」ことであり、通常はもっと長い検索テキストを指定することになります。今回試したどのシステムでも、10,000文字程度の検索であれば破綻なく実行できることを確認しています。

### MySQL + Mroonga + Bigram索引

```sql
SELECT docid, title, lastname, firstname,
    MATCH(content) AGAINST(
    '船が難破し無人島に漂着した少年たちが島で生き延びて生還する'
    IN NATURAL LANGUAGE MODE) AS sco,
    REPLACE(LEFT(content, 150), '\n', ' ') as cont_sp
  FROM aozorabunko
  WHERE MATCH(content) AGAINST(
    '船が難破し無人島に漂着した少年たちが島で生き延びて生還する'
    IN NATURAL LANGUAGE MODE)
  ORDER BY sco DESC LIMIT 1\G
```

MySQLではMATCH ～ AGAINSTを使って全文検索を行います。IN NATURAL LANGUAGE MODEを指定すると、キーワード検索でなく類似文書検索が実行されます。類似度の降順に結果を取得するため、スコア計算にも同じMATCH ～ AGAINSTを指定していますが、オプティマイザにより1回めの処理結果が再利用されるようです。

本文をすべて出力すると長くなるので、150文字程度に切り詰めます。Mroongaにはmroonga_snippet()というユーザー定義関数が用意されていて、これを使えば検索語周辺のテキストが切り出せるはずなのですが、どうやら検索語を明示しない使い方はできないようです。ここではLEFT()関数で本文先頭150文字を取り出し、REPLACE()関数で改行を空白に置き換えています。

結果をMySQLのメタコマンド「\G」で縦型表示します(これ、便利ですね！)。結果はこのようになりました。

```text
*************************** 1. row ***************************
    docid: 42767
    title: 無人島に生きる十六人
 lastname: 須川
firstname: 邦彦
      sco: 309711
  cont_sp: 無人島に生きる十六人 須川邦彦  １  中川船長の話  　これは、今から四十六年前、私が、東
京高等商船学校の実習学生として、練習帆船琴ノ緒丸に乗り組んでいたとき、私たちの教官であった、中川倉
吉先生験談で、私が、腹のそこからかんげきした、一生わすれられない話である。 　四十六年前と
1 row in set (0.01 sec)
```

### PostgreSQL + PGroonga + Bigram索引

```sql
SELECT docid, title, lastname, firstname,
    pgroonga_score(tableoid, ctid) AS sco,
    REGEXP_REPLACE(LEFT(content, 150), '\n', ' ', 'g') AS cont_sp
  FROM aozorabunko
  WHERE content &@*
    '船が難破し無人島に漂着した少年たちが島で生き延びて生還する'
  ORDER BY sco DESC LIMIT 1;
```

PGroongaでは、演算子「&@*」を使って類似文書検索を行います。類似度の降順に結果を取得するため、システムカラムtableoid, ctidを指定してpgroonga_score()関数を呼び出しています。

PGroongaにもpgroonga_snippet_html()という関数がありますが、やはり検索語を指定しないと使えないようです。ここではLEFT()関数で本文先頭150文字を取り出し、REGEXP_REPLACE()関数で改行を空白に置き換えています。

結果はこのようになりました。

```text
 docid |        title      | lastname | firstname |  sco   |             ... cont_sp
-------+-------------------+----------+-----------+--------+------------ ... --------
 42767 | 無人島に生きる十六人 | 須川     | 邦彦       | 309711 | 無人島に生きる十六人 須川邦
彦 １  中川船長の話  　これは、今から四十六年前、私が、東京高等商船学校の実習学生として、練習帆船
琴ノ緒丸に乗り組んでいたとき、私たちの教官であった、中川倉吉先生からきいた、先生の体験談で、私が、
腹のそこからかんげきした、一生わすれられない話である。 　四十六年前と
(1 行)
```

### DoqueDB + Bigram索引

```sql
SELECT docid, title, lastname, firstname, SCORE(content) AS sco,
  KWIC(content FOR 150 ENCLOSE WITH '<BEGIN>' AND '<END>')
  FROM aozorabunko
  WHERE content CONTAINS FREETEXT(
    '船が難破し無人島に漂着した少年たちが島で生き延びて生還する')
  ORDER BY sco DESC LIMIT 1;
```

さて、それではDoqueDBを試してみましょう。類似文書検索を行うため、CONTAINS FREETEXTを指定します。類似度の計算にはSCORE()関数を使います。

DoqueDBでは、検索結果から検索語周辺のテキストを切り出すKWIC(KeyWord In Context)関数が用意されています。これを使って周辺150文字を取り出してみましょう。検索語は検索テキストから自動的に設定されます。検索語の前後を囲む文字列も指定できます。ここでは\<BEGIN\>と\<END\>を指定しています。残念ながら、REPLACE()関数は用意されていません。

結果はこのようになりました。検索テキストから検索語「船」「少年」などが設定され、その周辺のテキストが取り出されたことがわかります。

```text
{docid,title,lastname,firstname,sco,cont}
{1323,海島冒険奇譚　海底軍艦,押川,春浪,8.21913453275337E-1,魅つて居るぜ。』と呟いた英國の古風
な紳士は甲板から自分の<BEGIN>船<END>室へ逃げ込まんとて昇降口から眞逆に滑落ちて腰を※かした、偶然
にも<BEGIN>船<END>の惡魔が御自分に祟つたものであらうか。虎は漸の事で捕押へたが其爲に怪我人が七八
人も出來た。
かゝる樣々の出來事の間、吾等の可憐なる日出雄<BEGIN>少年<END>は、相變らず元氣よく始終甲板を飛廻つ
て居る内}
```

### 検索結果を見る

さて、上記の条件を用いて各システムで類似文書検索を行い、上位30件を出力した結果を表にしてみましょう。今回の検索条件では、どのシステムを使っても、おおむね期待される作品が上位に出現しているようです。DoqueDBの強みをお見せしたかったところですが、そうはうまいこと行きませんでしたね。なお、MroongaとPGroongaの検索結果は似ていますが、それでもまったく同じでないのは興味深いところです。

<table>
  <thead><tr>
    <th rowspan="3" style="text-align: center;">順位</th>
    <th rowspan="2" style="text-align: center;">DoqueDB</th>
    <th colspan="4" style="text-align: center;">MySQL</th>
    <th colspan="2" style="text-align: center;">PostgreSQL</th>
  </tr>
  <tr>
    <th colspan="2" style="text-align: center;">Mroonga</th>
    <th colspan="2" style="text-align: center;">InnoDB</th>
    <th colspan="2" style="text-align: center";>PGroonga</th>
  </tr>
  <tr>
    <th style="text-align: center;">Bigram</th>
    <th style="text-align: center;">Bigram</th>
    <th style="text-align: center;">MeCab</th>
    <th style="text-align: center;">Bigram</th>
    <th style="text-align: center;">MeCab</th>
    <th style="text-align: center;">Bigram</th>
    <th style="text-align: center;">MeCab</th>
  </tr></thead>
  <tr>
    <td>1</td>
    <td>海島冒険奇譚　海底軍艦</td>
    <td>無人島に生きる十六人</td>
    <td>無人島に生きる十六人</td>
    <td>鎖国</td>
    <td>海島冒険奇譚　海底軍艦</td>
    <td>無人島に生きる十六人</td>
    <td>無人島に生きる十六人</td>
  </tr>
  <tr>
    <td>2</td>
    <td>無人島に生きる十六人</td>
    <td>海島冒険奇譚　海底軍艦</td>
    <td>新宝島</td>
    <td>無人島に生きる十六人</td>
    <td>無人島に生きる十六人</td>
    <td>海島冒険奇譚　海底軍艦</td>
    <td>新宝島</td>
  </tr>
  <tr>
    <td>3</td>
    <td>新宝島</td>
    <td>海上の道</td>
    <td>海島冒険奇譚　海底軍艦</td>
    <td>新宝島</td>
    <td>新宝島</td>
    <td>海上の道</td>
    <td>海島冒険奇譚　海底軍艦</td>
  </tr>
  <tr>
    <td>4</td>
    <td>少年連盟</td>
    <td>ウニデス潮流の彼方</td>
    <td>後の業平文治</td>
    <td>海島冒険奇譚　海底軍艦</td>
    <td>鎖国</td>
    <td>ウニデス潮流の彼方</td>
    <td>後の業平文治</td>
  </tr>
  <tr>
    <td>5</td>
    <td>沈黙の水平線</td>
    <td>鎖国</td>
    <td>怪塔王</td>
    <td>海上の道</td>
    <td>少年連盟</td>
    <td>鎖国</td>
    <td>ボニン島物語</td>
  </tr>
  <tr>
    <td>6</td>
    <td>大菩薩峠</td>
    <td>少年連盟</td>
    <td>ボニン島物語</td>
    <td>道標</td>
    <td>少年</td>
    <td>少年連盟</td>
    <td>怪塔王</td>
  </tr>
  <tr>
    <td>7</td>
    <td>怪奇人造島</td>
    <td>新宝島</td>
    <td>思い出す事など</td>
    <td>少年連盟</td>
    <td>少年探偵長</td>
    <td>新宝島</td>
    <td>思い出す事など</td>
  </tr>
  <tr>
    <td>8</td>
    <td>鎖国</td>
    <td>大菩薩峠</td>
    <td>大菩薩峠</td>
    <td>妖怪博士</td>
    <td>海上の道</td>
    <td>大菩薩峠</td>
    <td>大菩薩峠</td>
  </tr>
  <tr>
    <td>9</td>
    <td>ウニデス潮流の彼方</td>
    <td>光をかかぐる人々</td>
    <td>電気鳩</td>
    <td>旅愁</td>
    <td>道標</td>
    <td>光をかかぐる人々</td>
    <td>少年連盟</td>
  </tr>
  <tr>
    <td>10</td>
    <td>海上の道</td>
    <td>後の業平文治</td>
    <td>少年連盟</td>
    <td>超人間Ｘ号</td>
    <td>ウニデス潮流の彼方</td>
    <td>後の業平文治</td>
    <td>電気鳩</td>
  </tr>
  <tr>
    <td>11</td>
    <td>イノチガケ</td>
    <td>ボニン島物語</td>
    <td>真珠</td>
    <td>少年</td>
    <td>火星兵団</td>
    <td>ボニン島物語</td>
    <td>真珠</td>
  </tr>
  <tr>
    <td>12</td>
    <td>恐竜島</td>
    <td>怪塔王</td>
    <td>エリザベスとエセックス</td>
    <td>少年探偵長</td>
    <td>妖怪博士</td>
    <td>怪塔王</td>
    <td>エリザベスとエセックス</td>
  </tr>
  <tr>
    <td>13</td>
    <td>ボニン島物語</td>
    <td>大師の入唐</td>
    <td>怪奇人造島</td>
    <td>ウニデス潮流の彼方</td>
    <td>怪塔王</td>
    <td>大師の入唐</td>
    <td>怪奇人造島</td>
  </tr>
  <tr>
    <td>14</td>
    <td>海難記</td>
    <td>真珠</td>
    <td>恐竜島</td>
    <td>光をかかぐる人々</td>
    <td>光をかかぐる人々</td>
    <td>真珠</td>
    <td>恐竜島</td>
  </tr>
  <tr>
    <td>15</td>
    <td>光をかかぐる人々</td>
    <td>伊沢蘭軒</td>
    <td>南国太平記</td>
    <td>ニールスのふしぎな旅</td>
    <td>経済学及び課税の諸原理</td>
    <td>伊沢蘭軒</td>
    <td>南国太平記</td>
  </tr>
  <tr>
    <td>16</td>
    <td>他計甚&#40637;（竹島）雑誌</td>
    <td>恐竜島</td>
    <td>鎖国</td>
    <td>鉄人Ｑ</td>
    <td>大菩薩峠</td>
    <td>恐竜島</td>
    <td>鎖国</td>
  </tr>
  <tr>
    <td>17</td>
    <td>自分は見た</td>
    <td>他計甚&#40637;（竹島）雑誌</td>
    <td>旅愁</td>
    <td>怪塔王</td>
    <td>鉄人Ｑ</td>
    <td>他計甚&#40637;（竹島）雑誌</td>
    <td>旅愁</td>
  </tr>
  <tr>
    <td>18</td>
    <td>氷島の漁夫</td>
    <td>石の信仰とさえの神と</td>
    <td>淪落の青春</td>
    <td>神州天馬侠</td>
    <td>地獄</td>
    <td>石の信仰とさえの神と</td>
    <td>淪落の青春</td>
  </tr>
  <tr>
    <td>19</td>
    <td>後の業平文治</td>
    <td>故郷七十年</td>
    <td>昭和遊撃隊</td>
    <td>火星兵団</td>
    <td>虎の牙</td>
    <td>故郷七十年</td>
    <td>昭和遊撃隊</td>
  </tr>
  <tr>
    <td>20</td>
    <td>紅毛傾城</td>
    <td>電気鳩</td>
    <td>牡丹</td>
    <td>恐竜島</td>
    <td>暗黒公使</td>
    <td>電気鳩</td>
    <td>ガリバー旅行記</td>
  </tr>
  <tr>
    <td>21</td>
    <td>海底大陸</td>
    <td>淪落の青春</td>
    <td>恐竜艇の冒険</td>
    <td>超人ニコラ</td>
    <td>日本その日その日</td>
    <td>淪落の青春</td>
    <td>他計甚&#40637;（竹島）雑誌</td>
  </tr>
  <tr>
    <td>22</td>
    <td>光と風と夢</td>
    <td>旅愁</td>
    <td>ガリバー旅行記</td>
    <td>大菩薩峠</td>
    <td>透明怪人</td>
    <td>旅愁</td>
    <td>牡丹</td>
  </tr>
  <tr>
    <td>23</td>
    <td>颱風雑俎</td>
    <td>大菩薩峠</td>
    <td>大金塊</td>
    <td>夜光人間</td>
    <td>東京人の堕落時代</td>
    <td>大菩薩峠</td>
    <td>見えざる敵</td>
  </tr>
  <tr>
    <td>24</td>
    <td>幽霊船</td>
    <td>吉田松陰</td>
    <td>他計甚&#40637;（竹島）雑誌</td>
    <td>城</td>
    <td>超人間Ｘ号</td>
    <td>吉田松陰</td>
    <td>恐竜艇の冒険</td>
  </tr>
  <tr>
    <td>25</td>
    <td>少年探偵長</td>
    <td>怪奇人造島</td>
    <td>見えざる敵</td>
    <td>宝島</td>
    <td>超人ニコラ</td>
    <td>怪奇人造島</td>
    <td>大金塊</td>
  </tr>
  <tr>
    <td>26</td>
    <td>「太平洋漏水孔」漂流記</td>
    <td>海底大陸</td>
    <td>人外魔境</td>
    <td>虎の牙</td>
    <td>奇巌城</td>
    <td>エリザベスとエセックス</td>
    <td>我が人生観</td>
  </tr>
  <tr>
    <td>27</td>
    <td>大空魔艦</td>
    <td>私本太平記</td>
    <td>海底大陸</td>
    <td>後の業平文治</td>
    <td>吸血鬼</td>
    <td>海底大陸</td>
    <td>海底大陸</td>
  </tr>
  <tr>
    <td>28</td>
    <td>電気鳩</td>
    <td>新書太閤記</td>
    <td>新書太閤記</td>
    <td>灰色の巨人</td>
    <td>レモンの花の咲く丘へ</td>
    <td>私本太平記</td>
    <td>潜航艇「鷹の城」</td>
  </tr>
  <tr>
    <td>29</td>
    <td>海に生くる人々</td>
    <td>エリザベスとエセックス</td>
    <td>黒田如水</td>
    <td>故郷七十年</td>
    <td>昭和遊撃隊</td>
    <td>新書太閤記</td>
    <td>日記</td>
  </tr>
  <tr>
    <td>30</td>
    <td>海郷風物記</td>
    <td>福翁自伝</td>
    <td>私本太平記</td>
    <td>透明怪人</td>
    <td>魔法博士</td>
    <td>福翁自伝</td>
    <td>人外魔境</td>
  </tr>
</table>

## おわりに

ここまでお付き合いいただきありがとうございました。結構な分量になってしまいましたね。各システムについての説明で誤りなどありましたら、コメントにてご指摘くださるようお願いします。

今回の記事では、我々が競合と考えているソフトウェアたちとのさまざまな構文の違いを見てきました。構文の一覧だけでは実際の使用例をイメージしにくいものです。皆さんが現在お使いの(あるいは導入をご検討中の)ソフトウェアと同じことを、DoqueDBではどうすれば実現できそうか、ざっとご理解いただけたのではないでしょうか？ つたない解説でしたが、皆さんのお役に立ちましたら幸いです。

正規化については簡単に触れただけですが、近いうちにもう少し詳しい解説をさせていただく予定です。また、今回はご紹介できませんでしたが、DoqueDBには**関連語拡張**と呼ばれる機能があります。これは、類似文書検索を行った結果から、ユーザーが求める結果に近い文書をいくつか選択して再検索することにより、よりユーザーの意図に近い検索結果が得られるという機能です。これについてもいずれ解説させていただきます。

最後に、過去のβ版解説記事へのリンクを再掲しておきます。では、またお会いしましょう！

第1回：[DoqueDB：もうひとつの全文検索データベース](https://qiita.com/junnohta999/items/ee4e8332a6ee3f8930bc "https://qiita.com/junnohta999/items/ee4e8332a6ee3f8930bc")
第2回：[DoqueDB：デモサイトで青空文庫を検索しよう](https://qiita.com/junnohta999/items/7175d95d1a64fe573090 "https://qiita.com/junnohta999/items/7175d95d1a64fe573090")