SQL Server におけるフルテキスト検索の落とし穴

## はじめに
自分が現在関わっているシステムでは、RDBに Microsoft の SQL Server を利用しており、その機能である「フルテキスト検索」も利用しています。
これらを運用する中で、フルテキスト検索が実行できない状態になるといった事象がありましたので、その原因と対応方法を忘れないように書き留めておきたいと思います。
結論を先に書いておきますと、DBスナップショットからDBを復元したことが原因でした。

## 前提
* 利用しているRDBは SQL Server
* システム内で SQL Server の「フルテキスト検索」機能を利用

「フルテキスト検索」というのは、MS公式では以下のように説明されています。
> フルテキスト クエリでは、英語や日本語などの特定の言語の規則に基づいて語や句を操作することにより、フルテキスト インデックス内のテキスト データに対して言語検索を実行できます。 フルテキスト クエリには、単純な語や句、または複数の形式の語や句を含めることができます。

例によってさっぱり意味が分かりませんが、ざっくり説明いたしますと、**大量の文章から特定の単語を抽出する処理を非常に効率よくやってくれる機能**です。
単純にSQLで `WHERE XX LIKE '%○○%'` のように記述するよりも非常に高速に処理できます。
[フルテキスト検索クエリと LIKE 述語の比較](https://learn.microsoft.com/ja-jp/sql/relational-databases/search/full-text-search?view=sql-server-ver16#like)
膨大な数のブログ記事やメール本文から、ある単語が含まれている部分を検索したいときとかあると思いますが、そういうときに利用するといいよね！って機能です。


## 何をした？
ここから何をしたかを書いていきます。

### 運用方法を変えた
DBに対するデプロイの運用方法を少し変更しました。
#### 変更前の運用
システムのバージョンアップをする際、当然DBに対しての変更（テーブルや列の追加等）も発生します。
デプロイ時は、それら諸々の変更を実行するクエリをDBに対して流すのですが、万一のことを考えて、初めに**変更前のDBのバックアップを取得**していました。クエリが間違っていて途中で処理が止まってしまったーなんてことになった時でも変更前に戻せますからね。
ここで言う"バックアップ"とは、**BACKUPコマンドで取得するフルバックアップファイル**のことを指します。
※もちろんテスト環境で検証して問題ないことを確認したうえで実行しますが、それでもテスト環境と本番環境で微妙に設定が異なっていて、テスト環境では気づけなかったーというのはあるあるだと思います。

#### この運用の何が課題だったか
「バックアップを取得する」 = 「バックアップファイルが出力される」 ので、当然ストレージを圧迫するわけです。もちろん、バックアップファイルは元のDBに比べたらある程度圧縮できるので、サイズは小さくできます。とはいえ、元のDBのサイズが 数百GB単位 となると、バックアップファイルもそれなりのサイズになります。つまり **バックアップファイルを格納できる分の余白をストレージに残しておく必要がある** わけで、これが課題でした。無駄なストレージは利用しないほうがコストを抑えられますから。
#### 変更後の運用
「何かあったときのためにDBを変更前に戻せる状態にしておきたい」が、「無駄なストレージは使用したくない」という課題を解決するために、**DBのバックアップと復元方法を変更**しました。
具体的には、**「DBのフルバックアップファイルの作成」** ⇒ **「DBスナップショットの作成」** に変更しました。
有事の際は、**「バックアップファイルからリストア」** ではなく、 **「DBスナップショットからの復元」** をすることになります。

#### DBスナップショットとは？
DBスナップショットというのは SQL Server の機能の1つですが、名称や実装方法が異なるだけで、他のRDBでも同じような機能はあると思います。
このDBスナップショットについて、MS公式の説明では以下のように説明があります。
> データベース スナップショットは、SQL Server データベース (ソース データベース) の読み取り専用の静的ビューです。 

> データベース スナップショットは、スナップショットが作成されたときと同じ状態でデータの読み取り専用ビューを提供しますが、ソース データベースに変更が加えられると、スナップショット ファイルのサイズは大きくなります。

スナップショット作成後は、元となったDBに変更が加わるたびにサイズが大きくなっていくということで、これは差分バックアップのイメージに近いんですかね。
また、DBスナップショットの利点としてこうも記載されています。
> ソース データベースでユーザー エラーが発生したときは、特定のデータベース スナップショットの作成時の状態にソース データベースを戻すことができます。 失われるデータは、スナップショットの作成時点よりも後に行ったデータベースへの更新内容だけです。

これらを大事なところだけまとめると、
* DBスナップショットを作成した時点では、スナップショット自体のサイズはほぼゼロ
* DBスナップショットを作成した時点の状態にいつでも復元することができる

ということです。これらは、課題であった
* 何かあったときのためにDBを変更前に戻せる状態にしておきたい
* 無駄なストレージは使用したくない

これら2点を解決するのにとても相性が良いいわけです。
また、特に問題なく作業が完了したなら、その時点でDBスナップショットを削除することで、DBスナップショットのサイズが大きくなることも防げます。

※ちなみにSQL Server が稼働している DBサーバー はVM（仮想マシン）です。なのでVM自体のチェックポイントイメージを取得しておいて、なにかあったらそれを復元すりゃいいじゃん！と思う方もいると思います。そしてその通りだと思います。ですが、今回は諸々の事情でVMレベルでロールバックすることが許されていないという状況でした。そこらへんは察してください。


## 何が起きたか
前述のような運用に変更後、テスト環境で初めてのデプロイ作業を実施したときにその現象は起こりました。
テスト環境ということもあり、DBに対しての変更クエリの実行に何度か失敗とかしてたわけです。
それでも、作業実施前に作成したDBスナップショットがあるので、そいつに復元してやることで、何事もなかったかのように一から作業をやり直せました。うーん便利だなーとか言いつつデプロイ作業は完了し、システムの動作検証を開始しました。

### フルテキスト検索ができない！
システムでフルテキスト検索を利用する機能を検証したときのことです。例外が発生しました。
この時点で以下2点の担保は取れているので、原因がパッと思いつきませんでした。
* DBへのデプロイはすべて問題なく完了している
* 検索する値（入力値）とDBに登録されている内容を考慮しても正常に検索できるはず

そもそも、**検索した結果、正常に検索できずに結果が0件だった** ではなく、**検索処理自体で例外が発生している** 状態でした。

### フルテキストカタログ、フルテキストインデックス が消えて無くなっていた
なぜ例外が発生するのか詳細に調べたところ、DBからフルテキストカタログとフルテキストインデックスの両方が消滅していることがわかりました。
これら2つは、「フルテキスト検索」を実行するために用意しておかなければならないモノとお考え下さい。
* フルテキストインデックス
　⇒ 大量のテキストデータの内容を高速に検索するための特別なインデックス
* フルテキストカタログ
　⇒ フルテキストインデックスを管理するための仮想的なコンテナ

フルテキスト検索をするうえで必要なこれらが無い状態なので、そりゃあエラー出ますよねということでした。

## 原因
では、なんでフルテキストカタログとフルテキストインデックスが消えたの？ということになりますね。
原因特定するの大変そう……と億劫になるのもつかの間、あらやだ、ちゃあんと公式に書いてありました。
[制限事項と制約事項](https://learn.microsoft.com/ja-jp/sql/relational-databases/databases/revert-a-database-to-a-database-snapshot?view=sql-server-ver16#Restrictions)
> 復帰を行うと、すべてのフルテキスト カタログが削除されます。

SQL Server さんは何も悪くありませんでした。疑ってごめんね。そういう仕様なんです。仕様なのでこれはどうしようもないです。というか公式の情報をちゃんと読み込んでから利用しろよ！って話でした。すみません。

まあ削除されるにはそれなりの理由があるそうですが、それはそこまで重要ではありません。

**“DBスナップショットからDBを復元すると、すべてのフルテキストカタログが削除される”**

これが今回一番重要な情報です。

## 対策
じゃあ、フルテキスト検索を利用しているDBでは、DBスナップショットからの復元はできないのか？と言われるとそうではなくて、単純に**DBスナップショットから復元後に再作成してあげればいい**らしいです。
DBスナップショットからの復元とフルテキストカタログ・インデックスの作成はセットで実行してあげる必要があるわけです。
作成自体は `CREATE FULLTEXT CATALOG ～～` や `CREATE FULLTEXT INDEX ON ～～` を実行すればいいだけなので難しくはありませんが、それらの自動化の仕組みや、再作成にどれくらい時間がかかるか等の検討は必要そうです。
~~というかいちばんの対策は公式のドキュメントをしっかり読むこと~~

## 余談
同じような現象って、変更前の運用で行っていた、`BACKUP`および`RESTORE`コマンドによるバックアップファイルの作成・復元ではなぜ発生しないのでしょうか？

`BACKUP`コマンドで作成したバックアップファイルには **データベースのすべてが含まれます。** これは当然フルテキストカタログやインデックスも含まれます。
一方、DBスナップショットは、変更されたデータページのみをキャプチャしていて、**フルテキストの動的な挙動やメタデータは含まれません。**
このような違いがあるため、DBスナップショットからの復元時のみ、フルテキストカタログが削除され、結果的に再作成が必要になるということらしいです。

バックアップファイルの作成・復元と、DBスナップショット作成・復元は、同じような機能に見えて挙動や用途が全然違います。これらを用途に応じて正しく使い分けるのが大事なんですね。以下にそれぞれの特徴をまとめてみましたのでご参考までに。
### バックアップファイルとDBスナップショットの比較
<table>
  <tr>
    <th>特徴</th>
    <th>フルバックアップ (BACKUP)</th>
    <th>DBスナップショット (SNAPSHOT)</th>
  </tr>
  <tr>
    <td>基本概念</td>
    <td>データベースの完全なコピーを作成する</td>
    <td>データベースのある時点の読み取り専用のビューを作成する</td>
  </tr>
  <tr>
    <td>メリット</td>
    <td>- 完全なデータ復元が可能<br>- バックアップファイルを別のサーバーに移動して復元可能<br>- フルテキストカタログとインデックスも含む</td>
    <td>- 作成に時間がかからない<br>- ディスクスペースの使用が少ない<br>- データ復元が速い</td>
  </tr>
  <tr>
    <td>デメリット</td>
    <td>- 大規模なデータベースでは作成に時間がかかる<br>- ディスクを多く使用する</td>
    <td>- 元のデータベースに依存する<br>- 書き込みは不可<br></td>
  </tr>
  <tr>
    <td>ユースケース</td>
    <td>- 定期的なデータ保護<br>- 災害復旧<br>- データベース移行</td>
    <td>- 一時的な変更前の状態の保存<br>- 即時のデータ復旧<br>- レポーティング</td>
  </tr>
  <tr>
    <td>フルテキスト検索</td>
    <td>カタログ・インデックスがバックアップに含まれる</td>
    <td>カタログ・インデックスはスナップショットには含まれない</td>
  </tr>
  <tr>
    <td>作成時の影響</td>
    <td>データベースのパフォーマンスに影響を与える可能性あり</td>
    <td>データベースのパフォーマンスにほとんど影響を与えない</td>
  </tr>
  <tr>
    <td>サイズ</td>
    <td>データベースのサイズに比例したバックアップファイルが作成される</td>
    <td>データの変更があった部分だけがスナップショットで保持される</td>
  </tr>
</table>

## まとめ
SQL Server において、フルテキスト検索とDBスナップショットを利用している場合に、注意しなければならない点として、**DBスナップショットからの復元後は、フルテキストカタログおよびフルテキストインデックスを再作成する必要がある** という点を学びました。

おそらく、SQL Server に詳しい人からすると「当然やん。何当たり前のこと言うとんねん」と鼻で笑われることかもしれませんが、お恥ずかしながら今回の件が無かったら知ることもなかったと思います。まあテスト環境での検証の段階で気づいてよかったと思うことにします。

あと、ちゃんと公式のドキュメントは読み（ry

以上です。ありがとうございました。

## 参考
[フルテキスト検索](https://learn.microsoft.com/ja-jp/sql/relational-databases/search/full-text-search)
[データベース スナップショット (SQL Server)](https://learn.microsoft.com/ja-jp/sql/relational-databases/databases/database-snapshots-sql-server)
[データベースをデータベース スナップショットに戻す](https://learn.microsoft.com/ja-jp/sql/relational-databases/databases/revert-a-database-to-a-database-snapshot)