Sqlite で全文検索

fts5 を利用して全文検索。

準備
========
まず最初に SQLITE_ENABLE_FTS5 を指定して sqlite をコンパイルし直さないといけない。

```cpp:sqlite_lib.c
#define SQLITE_ENABLE_FTS5 1
#include "..\sqlite-amalgamation-3130000\sqlite3.c"
```

全文検索の仕組みを作る。
========

トークナイザーを準備
-----
1. sqlite の 標準のトークナイザーは英語にしか対応していない。
1. sqlite の fts モジュールは単語単位の 前方一致か、完全一致 しか対応していない。(っぽい？)

なのでトークナイザーを作らないと行けない。

```cpp:test.cpp
#include "sqlite3.h"
#include <string>
#include <sstream>

static void makeFtsIndex(
	sqlite3_context *context,
	int argc,
	sqlite3_value **argv
) {
	//	tokenize text to allow fts backword/partial search.
	//	'alpha beta gamma あいうえお' -> 'alpha lpha pha ha a beta eta ta a gamma amma mma ma a あいうえお いうえお うえお えお お'
	//	https://blog.kapeli.com/sqlite-fts-contains-and-suffix-matches

	//	backward search.
	//		select * from ftstable('eta');
	//	
	//	partial search
	//		select * from ftstable('eta*');

	std::wstringstream result;

	for (int i = 0; i < argc; ++i) {
		if (sqlite3_value_type(argv[i]) == SQLITE_NULL) {
			continue;
		}
		std::wstring value;
		value = (wchar_t *)sqlite3_value_text16(argv[i]);

		for (size_t _Off = 0; ;) {
			size_t found = value.find(L' ', _Off);
			std::wstring part = value.substr(_Off, found - _Off);

			for (int j = 0, end = part.length(); j != end; ++j) {
				result << L" " << part.substr(j);
			}

			if (found == std::wstring::npos) {
				break;
			}

			_Off = value.find_first_not_of(L' ', found);
		}
	}

	std::wstring retstr = result.str();
	if (retstr.length()) {
		retstr = retstr.substr(1);
	}

	sqlite3_result_text16(context, retstr.c_str(), -1, SQLITE_TRANSIENT);
}


main() {
	sqlite3 *pdb;
	sqlite3_open(":memory:", &pdb);
	sqlite3_create_function(pdb, "makeFtsIndex", 1, SQLITE_UTF8, NULL, &makeFtsIndex, NULL, NULL);
	    // SQLITE_UTF8 はもしかしたら SQLITE_UTF16
```

部分検索が目的なので、こんな感じのトークナイザー。

N-Gram を使えみたいな情報もググったら出てくるけど、この方法と、N-Gram の方法とでメリット・デメリットが良くわかっていないのツッコミ待ち。

テーブルを作るよ
-----

最初に普通にテーブルを作る。

```sql
CREATE TABLE FILE
	(id			INTEGER PRIMARY KEY AUTOINCREMENT
	,path		TEXT UNIQUE COLLATE NOCASE
	,attributes	INTEGER
	,modified	INTEGER
	,created	INTEGER
	,size		INTEGER
	);
```


で、トークン情報を貯めるテーブルを作る。

```sql
CREATE VIRTUAL TABLE _fts USING fts5(
	path, content='FILE', content_rowid='id'
	);
```

で、FILE テーブルを操作した際に一緒にトークン情報を操作してくれるトリガーも作る。

```sql
CREATE TRIGGER file_ai AFTER INSERT ON FILE BEGIN
	INSERT INTO _fts (rowid, path) VALUES (new.id, makeFtsIndex(new.path));
END;
CREATE TRIGGER file_ad AFTER DELETE ON FILE BEGIN
	INSERT INTO _fts (fts_idx, rowid, path) VALUES('delete', old.id, makeFtsIndex(old.path));
END;
CREATE TRIGGER file_au AFTER UPDATE OF path ON FILE BEGIN
	INSERT INTO _fts (fts_idx, rowid, path) VALUES('delete', old.id, makeFtsIndex(old.path));
	INSERT INTO _fts (rowid, path) VALUES (new.id, makeFtsIndex(new.path));
END;
```

だいたい sqlite のドキュメントに載ってる情報そのまんまだけど、最初に作ったトークナイザーを利用してる。
UPDATE トリガーは path 列に変更があった時のみ反応するように変更。

_fts への操作が全部 INSERT じゃん！というのはそういうものなのだとしか言い様がない。


情報をいれるよ
======

```sql
INSERT INTO FILE (path, size, modified, created, attributes) VALUES (@path, @size, @modified, @created, @attributes);
```

普通に INSERT の SQL を発行するだけ。
トークン情報の操作は、トリガーがやってくれる。

DELETE や UPDATE もトリガー任せ。

検索するよ
======

```sql
SELECT * FROM FILE WHERE id IN (
  SELECT rowid FROM _fts WHERE _fts MATCH 'et*'
);
```
これで、```LIKE %et%``` 相当。速度比較は省略です。
検索は _fts を検索して、得られた rowid を FILE テーブルで検索という形。

fts5 は fts3/4 と少し SQL が違うので注意らしいです。
相違点はドキュメント見てくださいですー。

まとめ
========
結局、fts5 じゃなくてもトークナイザーの作り方でどうとでもなりそう。
新しいっぽいから fts5 を利用しましたが、SqliteManager や公式の sqlite3.exe が fts5 に対応してないよ！とか言ってくるので、結構頭きます。ふつうに fts3/4 使ったほうがいいんじゃないですかね。

蛇足
========
重火力の脚パーツはとても重要なのです。

```sql
-- トークン 情報削除
DROP TRIGGER file_ai;
DROP TRIGGER file_ad;
DROP TRIGGER file_au;
DROP TABLE _fts;

-- トークン 情報生成
CREATE VIRTUAL TABLE _fts USING fts5(
	path, content='FILE', content_rowid='id'
	);

CREATE TRIGGER file_ai AFTER INSERT ON FILE BEGIN
	INSERT INTO _fts (rowid, path) VALUES (new.id, makeFtsIndex(new.path));
END;
CREATE TRIGGER file_ad AFTER DELETE ON FILE BEGIN
	INSERT INTO _fts (fts_idx, rowid, path) VALUES('delete', old.id, makeFtsIndex(old.path));
END;
CREATE TRIGGER file_au AFTER UPDATE ON FILE BEGIN
	INSERT INTO _fts (fts_idx, rowid, path) VALUES('delete', old.id, makeFtsIndex(old.path));
	INSERT INTO _fts (rowid, path) VALUES (new.id, makeFtsIndex(new.path));
END;

-- 既存テーブルから一気に トークン情報をいれる
INSERT INTO _fts (rowid, path) SELECT id, makeFtsIndex(path) FROM FILE;
```