小巧。快速。可靠。
三选二。
SQLite FTS5 扩展
目录

1. FTS5 概述

FTS5 是一个 SQLite 虚拟表模块,它为数据库应用程序提供了全文搜索功能。在最基本的形式中,全文搜索引擎允许用户有效地搜索大量文档,以查找包含一个或多个搜索词的子集。例如,Google 为万维网用户提供的搜索功能就是一个全文搜索引擎,因为它允许用户搜索网络上包含例如“fts5”之类的词的所有文档。

要使用 FTS5,用户需要创建一个具有一个或多个列的 FTS5 虚拟表。例如

CREATE VIRTUAL TABLE email USING fts5(sender, title, body);

在用于创建 FTS5 表的 CREATE VIRTUAL TABLE 语句中添加类型、约束或主键声明是错误的。创建后,可以使用INSERTUPDATEDELETE语句像其他任何表一样填充 FTS5 表。与任何没有主键声明的其他表一样,FTS5 表有一个名为 rowid 的隐式 INTEGER 主键字段。

上面未显示的是,还可以提供各种选项作为 CREATE VIRTUAL TABLE 语句的一部分提供给 FTS5,以配置新表的各个方面。这些可用于修改 FTS5 表从文档和查询中提取术语的方式,在磁盘上创建额外的索引以加快前缀查询的速度,或创建充当存储在其他位置的内容索引的 FTS5 表。

填充后,有三种方法可以对 FTS5 表的内容执行全文查询

如果使用 MATCH 或 = 运算符,则 MATCH 运算符左侧的表达式通常是 FTS5 表的名称(例外情况是指定列过滤器)。右侧的表达式必须是指定要搜索的术语的文本值。对于表值函数语法,要搜索的术语指定为第一个表参数。例如

-- Query for all rows that contain at least once instance of the term
-- "fts5" (in any column). The following three queries are equivalent.
SELECT * FROM email WHERE email MATCH 'fts5';
SELECT * FROM email WHERE email = 'fts5';
SELECT * FROM email('fts5');

默认情况下,FTS5 全文搜索不区分大小写。与任何其他不包含 ORDER BY 子句的 SQL 查询一样,以上示例以任意顺序返回结果。要按相关性(最相关到最不相关)对结果进行排序,可以如下所示向全文查询添加 ORDER BY

-- Query for all rows that contain at least once instance of the term
-- "fts5" (in any column). Return results in order from best to worst
-- match.  
SELECT * FROM email WHERE email MATCH 'fts5' ORDER BY rank;

除了匹配行的列值和 rowid 之外,应用程序还可以使用FTS5 辅助函数来检索有关匹配行的额外信息。例如,辅助函数可用于检索匹配行的列值的副本,其中所有匹配项的实例都用 html <b></b> 标记包围。辅助函数的调用方式与 SQLite 标量函数相同,只是 FTS5 表的名称指定为第一个参数。例如

-- Query for rows that match "fts5". Return a copy of the "body" column
-- of each row with the matches surrounded by <b></b> tags.
SELECT highlight(email, 2, '<b>', '</b>') FROM email('fts5');

有关可用辅助函数的说明以及有关特殊“rank”列配置的更多详细信息,请参见下文自定义辅助函数也可以在 C 中实现并注册到 FTS5,就像自定义 SQL 函数可以注册到 SQLite 核心一样。

除了搜索包含某个术语的所有行外,FTS5 还允许用户搜索包含

此类高级搜索是通过提供更复杂的 FTS5 查询字符串作为 MATCH 运算符(或 = 运算符,或作为表值函数语法的第一个参数)右侧的文本来请求的。完整的查询语法在此处描述

2. 编译和使用 FTS5

2.1. 将 FTS5 构建为 SQLite 的一部分

截至3.9.0 版(2015-10-14),FTS5 已包含在 SQLite 合并版本中。如果使用两个 autoconf 构建系统之一,则通过在运行配置脚本时指定“--enable-fts5”选项来启用 FTS5。(FTS5 当前对于源代码树配置脚本默认禁用,对于合并配置脚本默认启用,但这些默认值将来可能会更改。)

或者,如果使用其他构建系统编译 sqlite3.c,则通过安排定义 SQLITE_ENABLE_FTS5 预处理器符号。

2.2. 构建可加载扩展

或者,FTS5 可以构建为可加载扩展。

规范的 FTS5 源代码由 SQLite 源代码树的“ext/fts5”目录中的一系列 *.c 和其他文件组成。构建过程将其减少到仅两个文件 - “fts5.c”和“fts5.h” - 这些文件可用于构建 SQLite 可加载扩展。

  1. 从 fossil 获取最新的 SQLite 代码。
  2. 按照如何编译 SQLite中的说明创建 Makefile。
  3. 构建“fts5.c”目标。这也会创建 fts5.h。
$ wget -c https://www.sqlite.org/src/tarball/SQLite-trunk.tgz?uuid=trunk -O SQLite-trunk.tgz
.... output ...
$ tar -xzf SQLite-trunk.tgz
$ cd SQLite-trunk
$ ./configure && make fts5.c
... lots of output ...
$ ls fts5.[ch]
fts5.c        fts5.h

然后,可以将“fts5.c”中的代码编译成可加载扩展或静态链接到应用程序中,如编译可加载扩展中所述。定义了两个入口点,这两个入口点都执行相同操作

另一个文件“fts5.h”不需要编译 FTS5 扩展。它由实现自定义 FTS5 分词器或辅助函数的应用程序使用。

3. 全文查询语法

以下块包含以 BNF 形式表示的 FTS 查询语法的摘要。详细说明如下。

<phrase>    := string [*]
<phrase>    := <phrase> + <phrase>
<neargroup> := NEAR ( <phrase> <phrase> ... [, N] )
<query>     := [ [-] <colspec> :] [^] <phrase>
<query>     := [ [-] <colspec> :] <neargroup>
<query>     := [ [-] <colspec> :] ( <query> )
<query>     := <query> AND <query>
<query>     := <query> OR <query>
<query>     := <query> NOT <query>
<colspec>   := colname
<colspec>   := { colname1 colname2 ... }

3.1. FTS5 字符串

在 FTS 表达式中,可以以两种方式之一指定字符串

3.2. FTS5 短语

fts5 查询中的每个字符串都由分词器进行解析(“分词”),并提取零个或多个标记或术语的列表。例如,默认分词器将字符串“alpha beta gamma”分词成三个单独的标记 - “alpha”、“beta”和“gamma” - 按此顺序。

FTS 查询由短语组成。短语是一个或多个标记的有序列表。查询中每个字符串的标记都构成一个单独的短语。可以使用“+”运算符将两个短语连接成一个大型短语。例如,假设正在使用的分词器模块将输入“one.two.three”分词成三个单独的标记,则以下四个查询都指定了相同的短语

... MATCH '"one two three"'
... MATCH 'one + two + three'
... MATCH '"one two" + three'
... MATCH 'one.two.three'

如果文档包含至少一个与构成短语的标记序列匹配的标记子序列,则该短语与该文档匹配。

3.3. FTS5 前缀查询

如果 FTS 表达式中的字符串后面跟着一个 "*" 字符,则从字符串中提取的最后一个标记将被标记为前缀标记。正如您可能预期的那样,前缀标记匹配任何以其为前缀的文档标记。例如,以下代码块中的前两个查询将匹配任何包含标记“one”紧跟标记“two”,然后是任何以“thr”开头的标记的文档。

... MATCH '"one two thr" * '
... MATCH 'one + two + thr*'
... MATCH '"one two thr*"'      -- May not work as expected!

上面代码块中的最后一个查询可能无法按预期工作。因为 "*" 字符在双引号内,它将被传递给标记器,标记器可能会将其丢弃(或者,根据所使用的特定标记器,将其作为最终标记的一部分包含在内),而不是将其识别为特殊的 FTS 字符。

3.4. FTS5 初始标记查询

如果 "^" 字符出现在不属于 NEAR 查询的短语之前,则该短语仅在该短语从列的第一个标记开始时才匹配文档。“^”语法可以与列过滤器组合使用,但不能插入到短语的中间。

... MATCH '^one'              -- first token in any column must be "one"
... MATCH '^ one + two'       -- phrase "one two" must appear at start of a column
... MATCH '^ "one two"'       -- same as previous 
... MATCH 'a : ^two'          -- first token of column "a" must be "two"
... MATCH 'NEAR(^one, two)'   -- syntax error! 
... MATCH 'one + ^two'        -- syntax error! 
... MATCH '"^one two"'        -- May not work as expected!

3.5. FTS5 NEAR 查询

两个或多个短语可以组合成一个NEAR 组。NEAR 组由标记“NEAR”(区分大小写)后跟一个左括号字符,后跟两个或多个空格分隔的短语,可选地后跟逗号和数字参数N,然后是右括号指定。例如

... MATCH 'NEAR("one two" "three four", 10)'
... MATCH 'NEAR("one two" thr* + four)'

如果未提供N参数,则默认为 10。如果文档包含至少一个标记块,则 NEAR 组匹配该文档

  1. 包含每个短语的至少一个实例,并且
  2. 块中第一个短语的末尾和最后一个短语的开头之间的标记数小于或等于N

例如

CREATE VIRTUAL TABLE f USING fts5(x);
INSERT INTO f(rowid, x) VALUES(1, 'A B C D x x x E F x');

... MATCH 'NEAR(e d, 4)';                      -- Matches!
... MATCH 'NEAR(e d, 3)';                      -- Matches!
... MATCH 'NEAR(e d, 2)';                      -- Does not match!

... MATCH 'NEAR("c d" "e f", 3)';              -- Matches!
... MATCH 'NEAR("c"   "e f", 3)';              -- Does not match!

... MATCH 'NEAR(a d e, 6)';                    -- Matches!
... MATCH 'NEAR(a d e, 5)';                    -- Does not match!

... MATCH 'NEAR("a b c d" "b c" "e f", 4)';    -- Matches!
... MATCH 'NEAR("a b c d" "b c" "e f", 3)';    -- Does not match!

3.6. FTS5 列过滤器

单个短语或 NEAR 组可以通过在其前面加上列名称后跟冒号字符来限制为匹配 FTS 表的指定列中的文本。或通过在其前面加上用括号(“花括号”)括起来的空格分隔的列名称列表后跟冒号字符来限制为一组列。可以使用上面描述的两种字符串形式之一来指定列名。与作为短语一部分的字符串不同,列名称不会传递给标记器模块。列名称在 SQLite 列名称的通常方式下不区分大小写 - 仅对 ASCII 范围字符理解大小写等效性。

... MATCH 'colname : NEAR("one two" "three four", 10)'
... MATCH '"colname" : one + two + three'

... MATCH '{col1 col2} : NEAR("one two" "three four", 10)'
... MATCH '{col2 col1 col3} : one + two + three'

如果列过滤器规范前面带有 "-" 字符,则将其解释为不匹配的列列表。例如

-- Search for matches in all columns except "colname"
... MATCH '- colname : NEAR("one two" "three four", 10)'

-- Search for matches in all columns except "col1", "col2" and "col3"
... MATCH '- {col2 col1 col3} : one + two + three'

列过滤器规范也可以应用于用括号括起来的任意表达式。在这种情况下,列过滤器应用于表达式中的所有短语。嵌套的列过滤器操作只能进一步限制匹配的列子集,它们不能用于重新启用已过滤的列。例如

-- The following are equivalent:
... MATCH '{a b} : ( {b c} : "hello" AND "world" )'
... MATCH '(b : "hello") AND ({a b} : "world")'

最后,可以通过使用列名称作为 MATCH 运算符的 LHS(而不是通常的表名称)来指定单个列的列过滤器。例如

-- Given the following table
CREATE VIRTUAL TABLE ft USING fts5(a, b, c);

-- The following are equivalent
SELECT * FROM ft WHERE b MATCH 'uvw AND xyz';
SELECT * FROM ft WHERE ft MATCH 'b : (uvw AND xyz)';

-- This query cannot match any rows (since all columns are filtered out): 
SELECT * FROM ft WHERE b MATCH 'a : xyz';

3.7. FTS5 布尔运算符

可以使用布尔运算符将短语和 NEAR 组排列成表达式。按照优先级顺序,从最高(最紧密的组合)到最低(最松散的组合),运算符为

运算符功能
<query1> NOT <query2> 如果 query1 匹配且 query2 不匹配则匹配。
<query1> AND <query2> 如果 query1 和 query2 都匹配则匹配。
<query1> OR <query2> 如果 query1 或 query2 匹配则匹配。

可以使用括号对表达式进行分组,以便以通常的方式修改运算符优先级。例如

-- Because NOT groups more tightly than OR, either of the following may
-- be used to match all documents that contain the token "two" but not
-- "three", or contain the token "one".  
... MATCH 'one OR two NOT three'
... MATCH 'one OR (two NOT three)'

-- Matches documents that contain at least one instance of either "one"
-- or "two", but do not contain any instances of token "three".
... MATCH '(one OR two) NOT three'

短语和 NEAR 组也可以通过隐式 AND 运算符连接。为简单起见,这些在上面的 BNF 语法中没有显示。本质上,任何仅由空格分隔的短语或 NEAR 组序列(包括限制为匹配指定列的那些)都将被处理,就像每个短语或 NEAR 组之间都有一个隐式 AND 运算符一样。隐式 AND 运算符永远不会插入括号括起来的表达式的后面或前面。隐式 AND 运算符比所有其他运算符(包括 NOT)的组合更紧密。例如

... MATCH 'one two three'         -- 'one AND two AND three'
... MATCH 'three "one two"'       -- 'three AND "one two"'
... MATCH 'NEAR(one two) three'   -- 'NEAR(one two) AND three'
... MATCH 'one OR two three'      -- 'one OR two AND three'
... MATCH 'one NOT two three'     -- 'one NOT (two AND three)'

... MATCH '(one OR two) three'    -- Syntax error!
... MATCH 'func(one two)'         -- Syntax error!

4. FTS5 表创建和初始化

在“CREATE VIRTUAL TABLE ... USING fts5 ...”语句中指定的每个参数要么是列声明,要么是配置选项。列声明由一个或多个空格分隔的 FTS5 裸字或以 SQLite 可接受的任何方式引用的字符串文字组成。

列声明中的第一个字符串或裸字是列名。尝试将 fts5 表列命名为“rowid”或“rank”,或为列分配与表本身使用的名称相同的名称是错误的。不支持。

列声明中的每个后续字符串或裸字都是修改该列行为的列选项。列选项不区分大小写。与 SQLite 核心不同,FTS5 将无法识别的列选项视为错误。目前,唯一识别的选项是“UNINDEXED”(见下文)

配置选项由一个 FTS5 裸字(选项名称)后跟一个“=”字符,后跟选项值组成。选项值使用单个 FTS5 裸字或字符串文字指定,同样以 SQLite 核心可接受的任何方式引用。例如

CREATE VIRTUAL TABLE mail USING fts5(sender, title, body, tokenize = 'porter ascii');

目前有以下配置选项

4.1. UNINDEXED 列选项

使用 UNINDEXED 列选项限定的列的内容不会添加到 FTS 索引中。这意味着出于 MATCH 查询和FTS5 辅助函数的目的,该列不包含任何可匹配的标记。

例如,要避免将“uuid”字段的内容添加到 FTS 索引中

CREATE VIRTUAL TABLE customers USING fts5(name, addr, uuid UNINDEXED);

4.2. 前缀索引

默认情况下,FTS5 保持一个索引,记录文档集中每个标记实例的位置。这意味着查询完整标记的速度很快,因为它只需要一次查找,但是查询前缀标记可能会很慢,因为它需要范围扫描。例如,查询前缀标记“abc*”需要对大于或等于“abc”且小于“abd”的所有标记进行范围扫描。

前缀索引是单独的索引,记录一定字符长度的前缀标记的所有实例的位置,用于加速对前缀标记的查询。例如,优化对前缀标记“abc*”的查询需要三个字符前缀的前缀索引。

要向 FTS5 表添加前缀索引,“prefix”选项设置为单个正整数或包含一个或多个正整数的空格分隔列表的文本值。为指定的每个整数创建一个前缀索引。如果在单个 CREATE VIRTUAL TABLE 语句中指定了多个“prefix”选项,则所有选项都适用。

-- Two ways to create an FTS5 table that maintains prefix indexes for
-- two and three character prefix tokens.
CREATE VIRTUAL TABLE ft USING fts5(a, b, prefix='2 3');
CREATE VIRTUAL TABLE ft USING fts5(a, b, prefix=2, prefix=3);

4.3. 标记器

CREATE VIRTUAL TABLE 的“tokenize”选项用于配置 FTS5 表使用的特定标记器。选项参数必须是 FTS5 裸字或 SQL 文本文字。参数的文本本身被视为一个或多个 FTS5 裸字或 SQL 文本文字的空格序列。其中第一个是将要使用的标记器的名称。第二个和后续列表元素(如果存在)是传递给标记器实现的参数。

与选项值和列名不同,作为标记器的 SQL 文本文字必须使用单引号字符引用。例如

-- The following are all equivalent
CREATE VIRTUAL TABLE t1 USING fts5(x, tokenize = 'porter ascii');
CREATE VIRTUAL TABLE t1 USING fts5(x, tokenize = "porter ascii");
CREATE VIRTUAL TABLE t1 USING fts5(x, tokenize = "'porter' 'ascii'");
CREATE VIRTUAL TABLE t1 USING fts5(x, tokenize = '''porter'' ''ascii''');

-- But this will fail:
CREATE VIRTUAL TABLE t1 USING fts5(x, tokenize = '"porter" "ascii"');

-- This will fail too:
CREATE VIRTUAL TABLE t1 USING fts5(x, tokenize = 'porter' 'ascii');

FTS5 有四个内置标记器模块,在后续部分中进行了描述

还可以为 FTS5 创建自定义标记器。执行此操作的 API在此处描述

4.3.1. Unicode61 标记器

unicode 标记器将所有 unicode 字符分类为“分隔符”或“标记”字符。默认情况下,Unicode 6.1 中定义的所有空格和标点符号字符都被视为分隔符,所有其他字符都被视为标记字符。更具体地说,分配给以“L”或“N”开头的通用类别(字母和数字,具体来说)或类别“Co”(“其他,专用使用”)的所有 unicode 字符都被视为标记。所有其他字符都是分隔符。

一个或多个标记字符的每个连续运行都被视为一个标记。标记器根据 Unicode 6.1 中定义的规则不区分大小写。

默认情况下,所有拉丁文字符的变音符号都会被移除。这意味着,例如,“A”、“a”、“À”、“à”、“”和“â”都被认为是等价的。

标记规范中“unicode61”后面的任何参数都被视为交替的选项名称和值的列表。Unicode61 支持以下选项

选项用法
remove_diacritics此选项应设置为“0”、“1”或“2”。默认值为“1”。如果将其设置为“1”或“2”,则会从拉丁文字符中删除变音符号,如上所述。但是,如果将其设置为“1”,则在非常罕见的情况下,单个 unicode 代码点用于表示具有多个变音符号的字符时,不会删除变音符号。例如,不会从代码点 0x1ED9(“带抑扬符和下加点的拉丁小写字母 O”)中删除变音符号。从技术上讲,这是一个错误,但无法在不造成向后兼容性问题的情况下修复。如果此选项设置为“2”,则会从所有拉丁字符中正确删除变音符号。
categories此选项可用于修改被视为对应于标记字符的 Unicode 通用类别集。参数必须由空格分隔的两个字符通用类别缩写(例如“Lu”或“Nd”)列表组成,或者由相同的内容组成,其中第二个字符替换为星号(“*”),解释为通配符模式。默认值为“L* N* Co”。
tokenchars此选项用于指定应视为标记字符的其他 unicode 字符,即使根据 Unicode 6.1 它们是空格或标点符号字符。此选项设置为的所有字符串中的字符都被视为标记字符。
separators

此选项用于指定应视为分隔符的额外 Unicode 字符,即使根据 Unicode 6.1 它们是标记字符。此选项设置为的字符串中的所有字符都被视为分隔符。

例如

-- Create an FTS5 table that does not remove diacritics from Latin
-- script characters, and that considers hyphens and underscore characters
-- to be part of tokens. 
CREATE VIRTUAL TABLE ft USING fts5(a, b,
    tokenize = "unicode61 remove_diacritics 0 tokenchars '-_'"
);

-- Create an FTS5 table that, as well as the default token character classes,
-- considers characters in class "Mn" to be token characters.
CREATE VIRTUAL TABLE ft USING fts5(a, b,
    tokenize = "unicode61 categories 'L* N* Co Mn'"
);

fts5 unicode61 分词器与 fts3/4 unicode61 分词器完全兼容。

4.3.2. ASCII 分词器

ASCII 分词器类似于 Unicode61 分词器,除了

例如

-- Create an FTS5 table that uses the ascii tokenizer, but does not
-- consider numeric characters to be part of tokens.
CREATE VIRTUAL TABLE ft USING fts5(a, b,
    tokenize = "ascii separators '0123456789'"
);

4.3.3. Porter 分词器

Porter 分词器是一个包装分词器。它获取其他一些分词器的输出,并在将其返回给 FTS5 之前,对每个标记应用Porter 词干提取算法。这允许搜索词语如“correction”匹配类似的词语,例如“corrected”或“correcting”。Porter 词干提取算法仅设计用于英语词汇 - 对其他语言使用它可能会或可能不会提高搜索效用。

默认情况下,Porter 分词器作为默认分词器(unicode61)的包装器运行。或者,如果在“tokenize”选项之后添加了一个或多个额外的参数,则将其视为 Porter 词干提取器使用的底层分词器的规范。例如

-- Two ways to create an FTS5 table that uses the porter tokenizer to
-- stem the output of the default tokenizer (unicode61). 
CREATE VIRTUAL TABLE t1 USING fts5(x, tokenize = porter);
CREATE VIRTUAL TABLE t1 USING fts5(x, tokenize = 'porter unicode61');

-- A porter tokenizer used to stem the output of the unicode61 tokenizer,
-- with diacritics removed before stemming.
CREATE VIRTUAL TABLE t1 USING fts5(x, tokenize = 'porter unicode61 remove_diacritics 1');

4.3.4. 三元组分词器

三元组分词器扩展了 FTS5,以支持一般的子字符串匹配,而不是通常的标记匹配。使用三元组分词器时,查询或短语标记可以匹配行内的任何字符序列,而不仅仅是完整的标记。例如

CREATE VIRTUAL TABLE tri USING fts5(a, tokenize="trigram");
INSERT INTO tri VALUES('abcdefghij KLMNOPQRST uvwxyz');

-- The following queries all match the single row in the table
SELECT * FROM tri('cdefg');
SELECT * FROM tri('cdefg AND pqr');
SELECT * FROM tri('"hij klm" NOT stuv');

三元组分词器支持以下选项

选项用法
case_sensitive此值可以设置为 1 或 0(默认值)。如果设置为 1,则匹配区分大小写。否则,如果此选项设置为 0,则匹配不区分大小写。
remove_diacritics此值也可以设置为 1 或 0(默认值)。仅当 case_sensitive 选项设置为 0 时,才能将其设置为 1 - 将这两个选项都设置为 1 是错误的。如果设置了此选项,则在匹配之前会从文本中删除变音符号(例如,使“á”匹配“a”)。
-- A case-sensitive trigram index
CREATE VIRTUAL TABLE tri USING fts5(a, tokenize="trigram case_sensitive 1");

除非设置了 remove_diacritics 选项,否则使用三元组分词器的 FTS5 表也支持索引的 GLOB 和 LIKE 模式匹配。例如

SELECT * FROM tri WHERE a LIKE '%cdefg%';
SELECT * FROM tri WHERE a GLOB '*ij klm*xyz';

如果使用 case_sensitive 选项设置为 1 创建 FTS5 三元组分词器,则它只能索引 GLOB 查询,不能索引 LIKE 查询。

备注

4.4. 外部内容和无内容表

通常,当将行插入到 FTS5 表中时,除了构建索引之外,FTS5 还会创建原始行内容的副本。当用户或辅助函数实现请求 FTS5 表中的列值时,这些值将从该内容的私有副本中读取。“content”选项可用于创建仅存储 FTS 全文索引条目的 FTS5 表。由于列值本身通常比关联的全文索引条目大得多,因此可以节省大量的数据库空间。

有两种方法可以使用“content”选项

4.4.1. 无内容表

通过将“content”选项设置为空字符串来创建无内容的 FTS5 表。例如

CREATE VIRTUAL TABLE f1 USING fts5(a, b, c, content='');

无内容的 FTS5 表不支持 UPDATE 或 DELETE 语句,也不支持不为 rowid 字段提供非 NULL 值的 INSERT 语句。无内容表不支持 REPLACE 冲突处理。REPLACE 和 INSERT OR REPLACE 语句被视为常规 INSERT 语句。可以使用FTS5 删除命令从无内容表中删除行。

尝试读取无内容 FTS5 表中 rowid 以外的任何列值都会返回 SQL NULL 值。

4.4.2. 无内容删除表

从 3.43.0 版本开始,还提供了无内容删除表。通过将 content 选项设置为空字符串并将 contentless_delete 选项设置为 1 来创建无内容删除表。例如

CREATE VIRTUAL TABLE f1 USING fts5(a, b, c, content='', contentless_delete=1);

无内容删除表与无内容表的不同之处在于

-- Supported UPDATE statement:
UPDATE f1 SET a=?, b=?, c=? WHERE rowid=?;

-- This UPDATE is not supported, as it does not supply a new value
-- for column "c".
UPDATE f1 SET a=?, b=? WHERE rowid=?;

除非需要向后兼容性,否则新代码应优先使用无内容删除表而不是无内容表。

4.4.3. 外部内容表

通过将 content 选项设置为同一数据库中的表、虚拟表或视图(以下称为“内容表”)的名称来创建外部内容 FTS5 表。每当 FTS5 需要列值时,它都会按如下方式查询内容表,并将需要值的行的 rowid 绑定到 SQL 变量

SELECT <content_rowid>, <cols> FROM <content> WHERE <content_rowid> = ?;

在上面,<content> 被替换为内容表的名称。默认情况下,<content_rowid> 被替换为文字“rowid”。或者,如果在 CREATE VIRTUAL TABLE 语句中设置了“content_rowid”选项,则替换为该选项的值。<cols> 被替换为 FTS5 表列名的逗号分隔列表。例如

-- If the database schema is: 
CREATE TABLE tbl (a, b, c, d INTEGER PRIMARY KEY);
CREATE VIRTUAL TABLE fts USING fts5(a, c, content=tbl, content_rowid=d);

-- Fts5 may issue queries such as:
SELECT d, a, c FROM tbl WHERE d = ?;

内容表也可以按如下方式查询

SELECT <content_rowid>, <cols> FROM <content> ORDER BY <content_rowid> ASC;
SELECT <content_rowid>, <cols> FROM <content> ORDER BY <content_rowid> DESC;

用户仍然有责任确保外部内容 FTS5 表的内容与内容表保持最新。一种方法是使用触发器。例如

-- Create a table. And an external content fts5 table to index it.
CREATE TABLE tbl(a INTEGER PRIMARY KEY, b, c);
CREATE VIRTUAL TABLE fts_idx USING fts5(b, c, content='tbl', content_rowid='a');

-- Triggers to keep the FTS index up to date.
CREATE TRIGGER tbl_ai AFTER INSERT ON tbl BEGIN
  INSERT INTO fts_idx(rowid, b, c) VALUES (new.a, new.b, new.c);
END;
CREATE TRIGGER tbl_ad AFTER DELETE ON tbl BEGIN
  INSERT INTO fts_idx(fts_idx, rowid, b, c) VALUES('delete', old.a, old.b, old.c);
END;
CREATE TRIGGER tbl_au AFTER UPDATE ON tbl BEGIN
  INSERT INTO fts_idx(fts_idx, rowid, b, c) VALUES('delete', old.a, old.b, old.c);
  INSERT INTO fts_idx(rowid, b, c) VALUES (new.a, new.b, new.c);
END;

与无内容表一样,外部内容表也不支持 REPLACE 冲突处理。任何指定 REPLACE 冲突处理的操作都使用 ABORT 处理。

4.4.4. 外部内容表陷阱

用户有责任确保 FTS5 外部内容表(具有非空 content= 选项的表)与其内容表(content= 选项命名的表)保持一致。如果允许它们变得不一致,则对 FTS5 表的查询结果可能会变得不直观并出现不一致的情况。

在这些情况下,对 FTS5 外部内容表进行查询产生的明显不一致的结果可以理解如下

SELECT <content_rowid>, <cols> FROM <content> WHERE <content_rowid> = ?;

例如,如果使用以下脚本创建数据库

-- Create and populate a table. 
CREATE TABLE tbl(a INTEGER PRIMARY KEY, t TEXT);
INSERT INTO tbl VALUES(1, 'all that glitters');
INSERT INTO tbl VALUES(2, 'is not gold');

-- Create an external content FTS5 table 
CREATE VIRTUAL TABLE ft USING fts5(t, content='tbl', content_rowid='a');

则内容表包含两行,但 FTS 索引不包含与它们相对应的条目。在这种情况下,以下查询将返回不一致的结果,如下所示

-- Returns 2 rows.  Because the query does not use the FTS index, it is
-- effectively executed against table 'tbl' directly, and so returns
-- both rows.
SELECT * FROM t1;

-- Returns 0 rows.  This query does use the FTS index, which currently
-- contains no entries. So it returns 0 rows.
SELECT rowid, t FROM t1('gold')

或者,如果数据库创建和填充如下

-- Create and populate a table. 
CREATE TABLE tbl(a INTEGER PRIMARY KEY, t TEXT);

-- Create an external content FTS5 table 
CREATE VIRTUAL TABLE ft USING fts5(t, content='tbl', content_rowid='a');
INSERT INTO ft(rowid, t) VALUES(1, 'all that glitters');
INSERT INTO ft(rowid, t) VALUES(2, 'is not gold');

则内容表为空,但 FTS 索引包含 6 个不同标记的条目。在这种情况下,以下查询将返回不一致的结果,如下所示

-- Returns 0 rows.  Since it does not use the FTS index, the query is
-- passed directly through to table 'tbl', which contains no data.
SELECT * FROM t1;

-- Returns 1 row. The "rowid" field of the returned row is 2, and
-- the "t" field set to NULL. "t" is set to NULL because when the external
-- content table "tbl" was queried for the data associated with the row
-- with a=2 ("a" is the content_rowid column), none could be found.
SELECT rowid, t FROM t1('gold')

如上一节所述,内容表上的触发器是确保 FTS5 外部内容表保持一致的好方法。但是,仅当在内容表中插入、更新或删除行时才会触发触发器。这意味着,例如,如果数据库创建如下

-- Create and populate a table. 
CREATE TABLE tbl(a INTEGER PRIMARY KEY, t TEXT);
INSERT INTO tbl VALUES(1, 'all that glitters');
INSERT INTO tbl VALUES(2, 'is not gold');

-- Create an external content FTS5 table 
CREATE VIRTUAL TABLE ft USING fts5(t, content='tbl', content_rowid='a');

-- Create triggers to keep the FTS5 table up to date
CREATE TRIGGER tbl_ai AFTER INSERT ON tbl BEGIN
  INSERT INTO ft(rowid, t) VALUES (new.a, new.t);
END;
<similar triggers for update + delete>

则内容表和外部内容 FTS5 表不一致,因为创建触发器不会将内容表中的现有行复制到 FTS 索引中。触发器只能确保在创建后对内容表进行的更新反映在 FTS 索引中。

在这种情况下,以及 FTS 索引及其内容表变得不一致的任何其他情况下,可以使用'rebuild'命令完全丢弃 FTS 索引的内容,并根据内容表的当前内容重新构建它。

4.5. Columnsize 选项

通常,FTS5 在数据库中维护一个特殊的备份表,该表在单独的表中存储插入到主 FTS5 表中的每个列值的标记大小。此备份表由xColumnSize API 函数使用,该函数又由内置的 bm25 排名函数使用(并且可能对其他排名函数也有用)。

为了节省空间,可以通过将 columnsize 选项设置为零来省略此备份表。例如

-- A table without the xColumnSize() values stored on disk:
CREATE VIRTUAL TABLE ft USING fts5(a, b, c, columnsize=0);

-- Three equivalent ways of creating a table that does store the
-- xColumnSize() values on disk:
CREATE VIRTUAL TABLE ft USING fts5(a, b, c);
CREATE VIRTUAL TABLE ft USING fts5(a, b, c, columnsize=1);
CREATE VIRTUAL TABLE ft USING fts5(a, b, columnsize='1', c);

将 columnsize 选项设置为除 0 或 1 之外的任何值都是错误的。

如果 FTS5 表配置为 columnsize=0 但不是无内容表,则 xColumnSize API 函数仍然有效,但运行速度慢得多。在这种情况下,它不是直接从数据库中读取要返回的值,而是读取文本值本身并在需要时计算其中的标记。

或者,如果表也是无内容表,则适用以下内容

存储 xColumnSize 值的表的名称(除非指定了 columnsize=0)为“<name>_docsize”,其中 <name> 是 FTS5 表本身的名称。sqlite3_analyzer 工具可用于现有数据库,以确定通过使用 columnsize=0 重新创建 FTS5 表可以节省多少空间。

4.6. Detail 选项

对于文档中的每个词条,FTS5维护的FTS索引会存储文档的rowid、包含该词条的列号以及词条在列值中的偏移量。“detail”选项可用于省略部分信息。这减少了索引在数据库文件中的占用空间,但也降低了系统的功能和效率。

“detail”选项可以设置为“full”(默认值)、“column”或“none”。例如

-- The following two lines are equivalent (because the default value
-- of "detail" is "full". 
CREATE VIRTUAL TABLE ft1 USING fts5(a, b, c);
CREATE VIRTUAL TABLE ft1 USING fts5(a, b, c, detail=full);

CREATE VIRTUAL TABLE ft2 USING fts5(a, b, c, detail=column);
CREATE VIRTUAL TABLE ft3 USING fts5(a, b, c, detail=none);

如果“detail”选项设置为column,则对于每个词条,FTS索引仅记录rowid和列号,省略词条偏移量信息。这会导致以下限制

如果“detail”选项设置为none,则对于每个词条,FTS索引仅存储rowid。列和偏移量信息都被省略。除了上面列出的detail=column模式的限制之外,这还带来了以下额外限制

在一个索引大量电子邮件(磁盘上1636 MiB)的测试中,FTS索引在detail=full时磁盘上为743 MiB,detail=column时为340 MiB,detail=none时为134 MiB。

4.7. Tokendata选项

此选项仅对实现自定义标记器的应用程序有用。通常,标记器可以返回由任意字节序列组成的标记,包括0x00字节。但是,如果表指定了tokendata=1选项,则fts5会忽略标记中第一个0x00字节和任何尾随数据以进行匹配。它仍然存储标记器返回的整个标记,但fts5核心会忽略它。

完整的标记版本,包括任何0x00字节和尾随数据,可通过自定义辅助函数中的xQueryTokenxInstToken API获得。

这可能对排名函数很有用。自定义标记器可以向某些文档标记添加额外数据,允许排名函数为某些标记的命中赋予更大的权重(例如,文档标题中的标记)。

或者,自定义标记器和自定义辅助函数的组合可用于实现非对称搜索。标记器可以(例如)对每个文档标记返回标记的大小写规范化和未标记版本,后跟一个0x00字节,然后是文档中标记的完整文本。查询时,fts5将提供结果,就像查询中的所有字符都已进行大小写规范化和未标记一样。然后,可以在查询的WHERE子句中使用自定义辅助函数来过滤掉任何基于文档或查询词条中的二级或三级标记不匹配的行。

5. 辅助函数

辅助函数类似于SQL标量函数,但它们只能在FTS5表上的全文查询(使用MATCH运算符或带有三元组标记器的LIKE/GLOB)中使用。它们的结果不仅根据传递给它们的参数计算,还根据当前匹配和匹配的行计算。例如,辅助函数可以返回一个数值,指示匹配的准确性(请参阅bm25()函数),或者返回匹配行中包含一个或多个搜索词条的文本片段(请参阅snippet()函数)。

要调用辅助函数,应将FTS5表的名称指定为第一个参数。其他参数可以跟随第一个参数,具体取决于要调用的特定辅助函数。例如,要调用“highlight”函数

SELECT highlight(email, 2, '<b>', '</b>') FROM email WHERE email MATCH 'fts5'

作为FTS5一部分提供的内置辅助函数在下一节中描述。应用程序还可以在C中实现自定义辅助函数

5.1. 内置辅助函数

FTS5提供三个内置辅助函数

5.1.1. bm25()函数

内置辅助函数bm25()返回一个实数值,指示当前行与全文查询的匹配程度。匹配越好,返回的值在数值上越小。以下查询可用于按从最佳匹配到最差匹配的顺序返回匹配项

SELECT * FROM fts WHERE fts MATCH ? ORDER BY bm25(fts)

为了计算文档得分,全文查询被分成其组成短语。然后,文档D和查询Q的bm25得分计算如下

在上述公式中,nPhrase是查询中的短语数量。|D|是当前文档中的标记数量,avgdl是FTS5表中所有文档中标记的平均数量。k1b都是常数,分别硬编码为1.2和0.75。

公式开头的“-1”项在大多数BM25算法的实现中都找不到。如果没有它,更好的匹配将被分配数值更高的BM25得分。由于默认排序顺序为“升序”,这意味着将“ORDER BY bm25(fts)”附加到查询会导致结果按从最差匹配到最佳匹配的顺序返回。需要“DESC”关键字才能首先返回最佳匹配。为了避免此陷阱,FTS5对BM25的实现会在返回结果之前将其乘以-1,确保更好的匹配被分配数值较低的得分。

IDF(qi)是查询短语i的反文档频率。它的计算方法如下,其中N是FTS5表中的总行数,n(qi)是包含至少一个短语i实例的总行数

最后,f(qi,D)是短语i的短语频率。默认情况下,这只是短语在当前行中出现的次数。但是,通过向bm25() SQL函数传递额外的实数值参数,可以为表的每一列分配不同的权重,并按如下方式计算短语频率

其中wc是分配给列c的权重,n(qi,c)是短语i在当前行的列c中出现的次数。传递给bm25()的第一个参数(在表名之后)是分配给FTS5表最左侧列的权重。第二个是分配给第二左侧列的权重,依此类推。如果没有足够的参数用于所有表列,则剩余列将被分配权重1.0。如果有太多尾随参数,则会忽略额外的参数。例如

-- Assuming the following schema:
CREATE VIRTUAL TABLE email USING fts5(sender, title, body);

-- Return results in bm25 order, with each phrase hit in the "sender"
-- column considered the equal of 10 hits in the "body" column, and
-- each hit in the "title" column considered as valuable as 5 hits in
-- the "body" column.
SELECT * FROM email WHERE email MATCH ? ORDER BY bm25(email, 10.0, 5.0);

请参阅维基百科以获取有关BM25及其变体的更多信息

5.1.2. highlight()函数

highlight()函数返回当前行指定列中的文本副本,其中插入了额外的标记文本以标记短语匹配的开始和结束。

highlight()必须在表名之后恰好调用三个参数。解释如下

  1. 一个整数,指示要从中读取文本的FTS表列的索引。列从左到右编号,从零开始。
  2. 在每个短语匹配之前插入的文本。
  3. 在每个短语匹配之后插入的文本。

例如

-- Return a copy of the text from the leftmost column of the current
-- row, with phrase matches marked using html "b" tags.
SELECT highlight(fts, 0, '<b>', '</b>') FROM fts WHERE fts MATCH ?

在两个或多个短语实例重叠(共享一个或多个标记)的情况下,将为每一组重叠的短语插入一个打开和关闭标记。例如

-- Assuming this:
CREATE VIRTUAL TABLE ft USING fts5(a);
INSERT INTO ft VALUES('a b c x c d e');
INSERT INTO ft VALUES('a b c c d e');
INSERT INTO ft VALUES('a b c d e');

-- The following SELECT statement returns these three rows:
--   '[a b c] x [c d e]'
--   '[a b c] [c d e]'
--   '[a b c d e]'
SELECT highlight(ft, 0, '[', ']') FROM ft WHERE ft MATCH 'a+b+c AND c+d+e';

5.1.3. snippet()函数

snippet()函数类似于highlight(),但它不是返回整个列值,而是自动选择和提取要处理和返回的简短文档文本片段。snippet()函数必须传递表名参数之后的五个参数

  1. 一个整数,指示要从中选择返回文本的FTS表列的索引。列从左到右编号,从零开始。负值表示应自动选择列。
  2. 在返回文本中每个短语匹配之前插入的文本。
  3. 在返回文本中每个短语匹配之后插入的文本。
  4. 添加到所选文本的开头或结尾的文本,以指示返回的文本未出现在其列的开头或结尾,分别表示。
  5. 返回文本中的最大标记数。这必须大于零且等于或小于64。

5.2. 按辅助函数结果排序

所有FTS5表都具有一个名为“rank”的特殊隐藏列。如果当前查询不是全文查询(即,如果它不包含MATCH运算符),则“rank”列的值始终为NULL。否则,在全文查询中,列rank默认包含与执行不带尾随参数的bm25()辅助函数返回的值相同的值。

从rank列读取与在查询中直接使用bm25()函数之间的差异仅在按返回值排序时才重要。在这种情况下,使用“rank”比使用bm25()更快。

-- The following queries are logically equivalent. But the second may
-- be faster, particularly if the caller abandons the query before
-- all rows have been returned (or if the queries were modified to 
-- include LIMIT clauses).
SELECT * FROM fts WHERE fts MATCH ? ORDER BY bm25(fts);
SELECT * FROM fts WHERE fts MATCH ? ORDER BY rank;

代替使用不带尾随参数的bm25(),可以根据每个查询或通过为FTS表设置不同的持久默认值来配置映射到rank列的特定辅助函数。

为了更改单个查询的rank列映射,将类似于以下任一项的术语添加到查询的WHERE子句中

rank MATCH 'auxiliary-function-name(arg1, arg2, ...)'
rank = 'auxiliary-function-name(arg1, arg2, ...)'

MATCH或=运算符的右侧必须是一个常量表达式,该表达式计算结果为一个字符串,该字符串由要调用的辅助函数以及括号内零个或多个逗号分隔的参数组成。参数必须是SQL字面量。例如

-- The following queries are logically equivalent. But the second may
-- be faster. See above. 
SELECT * FROM fts WHERE fts MATCH ? ORDER BY bm25(fts, 10.0, 5.0);
SELECT * FROM fts WHERE fts MATCH ? AND rank MATCH 'bm25(10.0, 5.0)' ORDER BY rank;

表值函数语法也可用于指定替代排名函数。在这种情况下,描述排名函数的文本应指定为第二个表值函数参数。以下三个查询是等效的

SELECT * FROM fts WHERE fts MATCH ? AND rank MATCH 'bm25(10.0, 5.0)' ORDER BY rank;
SELECT * FROM fts WHERE fts = ? AND rank = 'bm25(10.0, 5.0)' ORDER BY rank;
SELECT * FROM fts WHERE fts(?, 'bm25(10.0, 5.0)') ORDER BY rank;

可以使用FTS5 rank配置选项修改表的rank列的默认映射。

6. 特殊的 INSERT 命令

6.1. 'automerge' 配置选项

FTS5 并没有使用单个磁盘数据结构来存储全文索引,而是使用了一系列的 B 树。每次新的事务提交时,都会将包含已提交事务内容的新 B 树写入数据库文件。当查询全文索引时,必须单独查询每个 B 树,并将结果合并后才能返回给用户。

为了防止数据库中的 B 树数量过多(导致查询速度变慢),会定期将较小的 B 树合并成包含相同数据的单个较大的 B 树。默认情况下,这会在修改全文索引的 INSERT、UPDATE 或 DELETE 语句中自动发生。'automerge' 参数决定每次合并多少个较小的 B 树。将其设置为较小的值可以加快查询速度(因为它们只需要查询和合并来自较少 B 树的结果),但也可能降低数据库写入速度(因为每个 INSERT、UPDATE 或 DELETE 语句都必须作为自动合并过程的一部分做更多工作)。

组成全文索引的每个 B 树都根据其大小分配到一个“级别”。级别 0 的 B 树是最小的,因为它们包含单个事务的内容。较高级别的 B 树是将两个或多个级别 0 的 B 树合并在一起的结果,因此它们更大。当存在M个或更多相同级别的 B 树时,FTS5 就会开始将 B 树合并在一起,其中M是 'automerge' 参数的值。

'automerge' 参数的最大允许值为 16。默认值为 4。将 'automerge' 参数设置为 0 将完全禁用 B 树的自动增量合并。

INSERT INTO ft(ft, rank) VALUES('automerge', 8);

6.2. 'crisismerge' 配置选项

'crisismerge' 选项类似于 'automerge',它决定了组成全文索引的组件 B 树如何以及多久合并一次。一旦在全文索引的单个级别上存在C个或更多 B 树,其中C是 'crisismerge' 选项的值,则该级别上的所有 B 树将立即合并成一个 B 树。

此选项与 'automerge' 选项的区别在于,当达到 'automerge' 限制时,FTS5 仅开始将 B 树合并在一起。大部分工作是在后续的 INSERT、UPDATE 或 DELETE 操作中执行的。而当达到 'crisismerge' 限制时,所有有问题的 B 树将立即合并。这意味着触发危机合并的 INSERT、UPDATE 或 DELETE 可能需要很长时间才能完成。

默认的 'crisismerge' 值为 16。没有最大限制。尝试将 'crisismerge' 参数设置为 0 或 1 等效于将其设置为默认值 (16)。尝试将 'crisismerge' 选项设置为负值是错误的。

INSERT INTO ft(ft, rank) VALUES('crisismerge', 16);

6.3. 'delete' 命令

此命令仅适用于 外部内容表无内容表。它用于从全文索引中删除与单行关联的索引条目。此命令和 delete-all 命令是删除无内容表全文索引中条目的唯一方法。

为了使用此命令删除一行,必须将文本值 'delete' 插入到与表同名的特殊列中。要删除行的行 ID 将插入到 rowid 列中。插入到其他列中的值必须与当前存储在表中的值匹配。例如

-- Insert a row with rowid=14 into the fts5 table.
INSERT INTO ft(rowid, a, b, c) VALUES(14, $a, $b, $c);

-- Remove the same row from the fts5 table.
INSERT INTO ft(ft, rowid, a, b, c) VALUES('delete', 14, $a, $b, $c);

如果作为 'delete' 命令一部分插入文本列中的值与当前存储在表中的值不同,则结果可能无法预测。

原因很容易理解:当文档插入到 FTS5 表中时,会在全文索引中添加一个条目来记录每个标记在新文档中的位置。当文档被删除时,需要原始数据才能确定需要从全文索引中删除的条目集。因此,如果在使用此命令删除行时提供给 FTS5 的数据与在插入时确定标记实例集所使用的数据不同,则某些全文索引条目可能无法正确删除,或者 FTS5 可能会尝试删除不存在的索引条目。这可能使全文索引处于不可预测的状态,从而导致未来的查询结果不可靠。

6.4. 'delete-all' 命令

此命令仅适用于 外部内容表无内容表(包括 无内容删除表)。它删除全文索引中的所有条目。

INSERT INTO ft(ft) VALUES('delete-all');

6.5. 'deletemerge' 配置选项

'deletemerge' 选项仅由 无内容删除表 使用。

当从无内容删除表中删除一行时,与其标记关联的条目不会立即从 FTS 索引中删除。相反,包含已删除行行 ID 的“墓碑”标记将附加到包含行 FTS 索引条目的 B 树。当查询 B 树时,将从结果中省略存在墓碑标记的任何查询结果行。当 B 树与其他 B 树合并时,已删除的行及其墓碑标记都将被丢弃。

此选项指定 B 树中必须具有墓碑标记的行所占的最小百分比,然后才能使 B 树有资格合并 - 无论是通过 自动 合并还是显式用户 'merge' 命令 - 即使它不满足由 'automerge' 和 'usermerge' 选项确定的通常标准。

例如,要指定 FTS5 应在 B 树的 15% 的行具有关联的墓碑标记后考虑合并组件 B 树

INSERT INTO ft(ft, rank) VALUES('deletemerge', 15);

此选项的默认值为 10。尝试将其设置为小于零将恢复默认值。将此选项设置为 0 或大于 100 将确保由于墓碑标记而永远不会使 B 树有资格合并。

6.6. 'integrity-check' 命令

此命令用于验证全文索引在内部是否一致,以及(可选)它是否与任何 外部内容表 一致。

'integrity-check' 命令通过将文本值 'integrity-check' 插入到与 FTS5 表同名的特殊列中来调用。如果为“rank”列提供值,则该值必须为 0 或 1。例如

INSERT INTO ft(ft) VALUES('integrity-check');
INSERT INTO ft(ft, rank) VALUES('integrity-check', 0);
INSERT INTO ft(ft, rank) VALUES('integrity-check', 1);

以上三种形式对于所有不是外部内容表的 FTS 表都是等效的。它们检查索引数据结构是否未损坏,并且如果 FTS 表不是无内容表,则检查索引的内容是否与表本身的内容匹配。

对于外部内容表,只有当为 rank 列指定的值为 1 时,才会将索引的内容与外部内容表的内容进行比较。

在所有情况下,如果发现任何差异,命令将失败并出现 SQLITE_CORRUPT_VTAB 错误。

6.7. 'merge' 命令

INSERT INTO ft(ft, rank) VALUES('merge', 500);

此命令将 B 树结构合并在一起,直到大约 N 页合并数据写入数据库,其中 N 是作为 'merge' 命令一部分指定参数的绝对值。每页的大小由 FTS5 pgsz 选项 配置。

如果参数为正值,则只有在以下任一条件为真时,B 树结构才有资格合并

可以通过在执行命令前后检查 sqlite3_total_changes() API 返回的值来判断 'merge' 命令是否找到任何要合并的 B 树。如果这两个值之间的差值为 2 或更大,则表示已执行工作。如果差异小于 2,则 'merge' 命令是无操作的。在这种情况下,没有理由再次执行相同的 'merge' 命令,至少在 FTS 表下次更新之前是这样。

如果参数为负值,并且 FTS 索引中存在多个级别的 B 树结构,则在开始合并操作之前,所有 B 树结构都将分配到同一级别。此外,如果参数为负值,则不会尊重 usermerge 配置选项的值 - 即使来自同一级别的两个 B 树也可以合并在一起。

上述内容意味着执行 'merge' 命令,直到 sqlite3_total_changes() 返回值在前后差异小于 2,这与 FTS5 optimize 命令 的优化方式相同。但是,如果在此过程中将新的 B 树添加到 FTS 索引中,FTS5 会将新 B 树移动到与现有 B 树相同的级别并重新启动合并。为避免这种情况,只有第一次调用 'merge' 应指定负参数。每次后续调用 'merge' 应指定正值,以便即使将新的 B 树添加到 FTS 索引中,第一次调用启动的合并也会运行到完成。

6.8. 'optimize' 命令

此命令将构成全文索引的所有单个 B 树合并成一个大型 B 树结构。这确保了全文索引在数据库中占用最少的空间,并且处于最快的查询形式。

有关全文索引及其组件 B 树之间关系的更多详细信息,请参阅 FTS5 automerge 选项 的文档。

INSERT INTO ft(ft) VALUES('optimize');

因为它会重新组织整个 FTS 索引,所以 optimize 命令可能需要很长时间才能运行。可以使用 FTS5 merge 命令 将优化 FTS 索引的工作分成多个步骤。为此

其中 N 是在每次调用 merge 命令时要合并的数据页数。当 sqlite3_total_changes() 函数在 merge 命令前后返回的值的差异降至低于 2 时,应用程序应停止调用 merge。合并命令可以作为相同或单独的事务的一部分发出,并且可以由相同或不同的数据库客户端发出。有关更多详细信息,请参阅 merge 命令 的文档。

6.9. 'pgsz' 配置选项

此命令用于设置持久“pgsz”选项。

FTS5维护的全文索引存储为数据库表中的一系列固定大小的blob。组成全文索引的所有blob不必严格地大小相同。pgsz选项决定后续索引编写器创建的所有blob的大小。默认值为1000。

INSERT INTO ft(ft, rank) VALUES('pgsz', 4072);

6.10. “rank”配置选项

此命令用于设置持久化的“rank”选项。

rank选项用于更改rank列的默认辅助函数映射。此选项应设置为与上面针对"rank MATCH ?"术语所述相同的文本格式。例如

INSERT INTO ft(ft, rank) VALUES('rank', 'bm25(10.0, 5.0)');

6.11. “rebuild”命令

此命令首先删除整个全文索引,然后根据表的内容或内容表重新构建索引。它在无内容表中不可用。

INSERT INTO ft(ft) VALUES('rebuild');

6.12. “secure-delete”配置选项

此命令用于设置持久化的布尔值“secure-delete”选项。例如

INSERT INTO ft(ft, rank) VALUES('secure-delete', 1);

通常,当FTS5表中的条目被更新或删除时,不是从全文索引中删除条目,而是将删除键添加到事务创建的新的B树中。这很有效率,但意味着旧的全文索引条目会保留在数据库文件中,直到最终被全文索引上的合并操作删除。任何拥有数据库访问权限的人都可以使用这些条目来轻松地重建已删除的FTS5表行的内容。但是,如果将“secure-delete”选项设置为1,则在更新或删除现有的FTS5表行时,会实际从数据库中删除全文条目。这速度较慢,但可以防止旧的全文条目被用来重建已删除的表行。

此选项确保旧的全文条目无法被拥有数据库SQL访问权限的攻击者访问。为了进一步确保攻击者无法通过访问SQLite数据库文件本身来恢复这些条目,应用程序还必须使用类似于 "PRAGMA secure_delete = 1"的命令启用SQLite内核安全删除选项。

警告:一旦设置了此选项并更新或删除了一个或多个表行,则低于3.42.0版本的任何FTS5版本都可能无法再读取或写入FTS5表(此选项首次可用的版本)。尝试这样做会导致错误,并显示类似“invalid fts5 file format (found 5, expected 4) - run 'rebuild'”的错误消息。可以通过使用3.42.0或更高版本运行'rebuild'命令来恢复FTS5文件格式,以便旧版本的FTS5可以读取。

secure-delete选项的默认值为0。

6.13. “usermerge”配置选项

此命令用于设置持久化的“usermerge”选项。

usermerge选项类似于automerge和crisismerge选项。它是使用正参数的“merge”命令将合并在一起的B树段的最小数量。例如

INSERT INTO ft(ft, rank) VALUES('usermerge', 4);

usermerge选项的默认值为4。最小允许值为2,最大值为16。

7. 扩展FTS5

FTS5具有允许通过以下方式进行扩展的API:

本文档中描述的内置分词器和辅助函数都是使用下面描述的公开API实现的。

在将新的辅助函数或分词器实现注册到FTS5之前,应用程序必须获取指向“fts5_api”结构的指针。每个与FTS5扩展注册的数据库连接都有一个fts5_api结构。要获取指针,应用程序使用单个参数调用SQL用户定义函数fts5()。该参数必须使用sqlite3_bind_pointer()接口设置为指向fts5_api对象的指针的指针。以下示例代码演示了此技术

/*
** Return a pointer to the fts5_api pointer for database connection db.
** If an error occurs, return NULL and leave an error in the database
** handle (accessible using sqlite3_errcode()/errmsg()).
*/
fts5_api *fts5_api_from_db(sqlite3 *db){
  fts5_api *pRet = 0;
  sqlite3_stmt *pStmt = 0;

  if( SQLITE_OK==sqlite3_prepare(db, "SELECT fts5(?1)", -1, &pStmt, 0) ){
    sqlite3_bind_pointer(pStmt, 1, (void*)&pRet, "fts5_api_ptr", NULL);
    sqlite3_step(pStmt);
  }
  sqlite3_finalize(pStmt);
  return pRet;
}

向后兼容性警告:在SQLite 3.20.0(2017-08-01)版本之前,fts5()的工作方式略有不同。必须修改扩展FTS5的旧版应用程序以使用上面显示的新技术。

fts5_api结构定义如下。它公开了三种方法,分别用于注册新的辅助函数和分词器,以及用于检索现有分词器。后者旨在促进类似于内置porter分词器的“分词器包装器”的实现。

typedef struct fts5_api fts5_api;
struct fts5_api {
  int iVersion;                   /* Currently always set to 2 */

  /* Create a new tokenizer */
  int (*xCreateTokenizer)(
    fts5_api *pApi,
    const char *zName,
    void *pUserData,
    fts5_tokenizer *pTokenizer,
    void (*xDestroy)(void*)
  );

  /* Find an existing tokenizer */
  int (*xFindTokenizer)(
    fts5_api *pApi,
    const char *zName,
    void **ppUserData,
    fts5_tokenizer *pTokenizer
  );

  /* Create a new auxiliary function */
  int (*xCreateFunction)(
    fts5_api *pApi,
    const char *zName,
    void *pUserData,
    fts5_extension_function xFunction,
    void (*xDestroy)(void*)
  );
};

要调用fts5_api对象的某个方法,应将fts5_api指针本身作为方法的第一个参数传递,然后传递其他特定于方法的参数。例如

rc = pFts5Api->xCreateTokenizer(pFts5Api, ... other args ...);

fts5_api结构方法在以下各节中分别进行了描述。

7.1. 自定义分词器

要创建自定义分词器,应用程序必须实现三个函数:分词器构造函数(xCreate)、析构函数(xDelete)和执行实际分词的函数(xTokenize)。每个函数的类型与fts5_tokenizer结构的成员变量相同

typedef struct Fts5Tokenizer Fts5Tokenizer;
typedef struct fts5_tokenizer fts5_tokenizer;
struct fts5_tokenizer {
  int (*xCreate)(void*, const char **azArg, int nArg, Fts5Tokenizer **ppOut);
  void (*xDelete)(Fts5Tokenizer*);
  int (*xTokenize)(Fts5Tokenizer*, 
      void *pCtx,
      int flags,            /* Mask of FTS5_TOKENIZE_* flags */
      const char *pText, int nText, 
      int (*xToken)(
        void *pCtx,         /* Copy of 2nd argument to xTokenize() */
        int tflags,         /* Mask of FTS5_TOKEN_* flags */
        const char *pToken, /* Pointer to buffer containing token */
        int nToken,         /* Size of token in bytes */
        int iStart,         /* Byte offset of token within input text */
        int iEnd            /* Byte offset of end of token within input text */
      )
  );
};

/* Flags that may be passed as the third argument to xTokenize() */
#define FTS5_TOKENIZE_QUERY     0x0001
#define FTS5_TOKENIZE_PREFIX    0x0002
#define FTS5_TOKENIZE_DOCUMENT  0x0004
#define FTS5_TOKENIZE_AUX       0x0008

/* Flags that may be passed by the tokenizer implementation back to FTS5
** as the third argument to the supplied xToken callback. */
#define FTS5_TOKEN_COLOCATED    0x0001      /* Same position as prev. token */

通过调用fts5_api对象的xCreateTokenizer()方法将实现注册到FTS5模块。如果已存在名称相同的分词器,则将其替换。如果将非NULL xDestroy参数传递给xCreateTokenizer(),则在关闭数据库句柄或替换分词器时,使用作为唯一参数传递的pUserData指针的副本调用它。

如果成功,xCreateTokenizer()返回SQLITE_OK。否则,它返回SQLite错误代码。在这种情况下,不会调用xDestroy函数。

当FTS5表使用自定义分词器时,FTS5核心会调用xCreate()一次来创建分词器,然后调用xTokenize()零次或多次来分词字符串,然后调用xDelete()来释放xCreate()分配的任何资源。更具体地说

xCreate

此函数用于分配和初始化分词器实例。需要分词器实例来实际分词文本。

传递给此函数的第一个参数是应用程序在将fts5_tokenizer对象注册到FTS5时提供的(void*)指针的副本(xCreateTokenizer()的第三个参数)。第二个和第三个参数是包含分词器参数(如果有)的空终止字符串数组,这些参数在创建FTS5表的CREATE VIRTUAL TABLE语句的一部分中指定在分词器名称之后。

最后一个参数是输出变量。如果成功,则应将(*ppOut)设置为指向新的分词器句柄,并返回SQLITE_OK。如果发生错误,则应返回SQLITE_OK以外的值。在这种情况下,fts5假定*ppOut的最终值为未定义。

xDelete

此函数用于删除之前使用xCreate()分配的分词器句柄。Fts5保证此函数将对每次成功调用xCreate()都调用一次。

xTokenize

此函数预计会分词参数pText指示的nText字节字符串。pText可能是或可能不是空终止的。传递给此函数的第一个参数是指向由对xCreate()的早期调用返回的Fts5Tokenizer对象的指针。

第二个参数指示FTS5请求分词提供的文本的原因。这始终是以下四个值之一

  • FTS5_TOKENIZE_DOCUMENT - 文档正在插入到FTS表中或从FTS表中删除。正在调用分词器以确定要添加到(或从)FTS索引中的令牌集。

  • FTS5_TOKENIZE_QUERY - 正在对FTS索引执行MATCH查询。正在调用分词器来分词作为查询的一部分指定的裸词或带引号的字符串。

  • (FTS5_TOKENIZE_QUERY | FTS5_TOKENIZE_PREFIX) - 与FTS5_TOKENIZE_QUERY相同,除了裸词或带引号的字符串后面跟着一个“*”字符,表示分词器返回的最后一个令牌将被视为令牌前缀。

  • FTS5_TOKENIZE_AUX - 正在调用分词器以满足辅助函数发出的fts5_api.xTokenize()请求。或者辅助函数对columnsize=0数据库发出的fts5_api.xColumnSize()请求。

对于输入字符串中的每个令牌,都必须调用提供的回调xToken()。它的第一个参数应该是作为xTokenize()的第二个参数传递的指针的副本。第三个和第四个参数是指向包含令牌文本的缓冲区的指针,以及令牌的字节大小。第4个和第5个参数是从输入中派生令牌的文本的第一字节和紧跟在文本之后的第一个字节的字节偏移量。

传递给xToken()回调的第二个参数(“tflags”)通常应设置为0。例外情况是分词器支持同义词。在这种情况下,请参阅下面的讨论以了解详细信息。

FTS5假定按它们在输入文本中出现的顺序为每个令牌调用xToken()回调。

如果xToken()回调返回SQLITE_OK以外的任何值,则应放弃分词,并且xTokenize()方法应立即返回xToken()返回值的副本。或者,如果输入缓冲区已用尽,则xTokenize()应返回SQLITE_OK。最后,如果xTokenize()实现本身发生错误,则它可能会放弃分词并返回SQLITE_OK或SQLITE_DONE以外的任何错误代码。

7.1.1. 同义词支持

自定义分词器也可以支持同义词。考虑用户希望查询诸如“first place”之类的短语的情况。使用内置分词器,FTS5查询“first + place”将匹配文档集中“first place”的实例,但不匹配“1st place”等替代形式。在某些应用程序中,最好匹配“first place”或“1st place”的所有实例,而不管用户在MATCH查询文本中指定了哪种形式。

在FTS5中有多种方法可以解决此问题

  1. 将所有同义词映射到单个令牌。在这种情况下,使用上面的示例,这意味着分词器对输入“first”和“1st”返回相同的令牌。假设该令牌实际上是“first”,因此当用户插入文档“I won 1st place”时,会为令牌“i”、“won”、“first”和“place”添加到索引中。如果用户随后查询“1st + place”,则分词器将“first”替换为“1st”,并且查询按预期工作。

  2. 分别查询每个查询术语的所有同义词的索引。在这种情况下,在分词查询文本时,分词器可能会为文档中的单个术语提供多个同义词。然后,FTS5会分别查询每个同义词的索引。例如,面对查询

    ... MATCH 'first place'
    

    分词器为MATCH查询中的第一个令牌提供“1st”和“first”作为同义词,并且FTS5有效地运行类似于以下内容的查询

    ... MATCH '(first OR 1st) place'
    

    但出于辅助函数的目的,查询仍然显示只包含两个短语——“(first OR 1st)”被视为单个短语。

  3. 通过将单个术语的多个同义词添加到FTS索引中。使用此方法,在分词文档文本时,分词器会为每个令牌提供多个同义词。因此,当分词诸如“I won first place”之类的文档时,会为“i”、“won”、“first”、“1st”和“place”添加到FTS索引中。

    这样,即使分词器在分词查询文本时不提供同义词(它不应该这样做——这样做效率低下),用户查询“first + place”或“1st + place”也没有关系,因为FTS索引中存在对应于第一个令牌这两种形式的条目。

无论是在解析文档还是查询文本时,任何指定了包含FTS5_TOKEN_COLOCATED位的tflags参数的xToken调用都被视为为前一个令牌提供同义词。例如,在解析文档“I won first place”时,支持同义词的分词器将调用xToken() 5次,如下所示

xToken(pCtx, 0, "i",                      1,  0,  1);
xToken(pCtx, 0, "won",                    3,  2,  5);
xToken(pCtx, 0, "first",                  5,  6, 11);
xToken(pCtx, FTS5_TOKEN_COLOCATED, "1st", 3,  6, 11);
xToken(pCtx, 0, "place",                  5, 12, 17);

在首次调用 xToken() 时指定 FTS5_TOKEN_COLOCATED 标志是错误的。可以通过依次多次调用 xToken(FTS5_TOKEN_COLOCATED) 为单个标记指定多个同义词。为单个标记提供的同义词数量没有限制。

在许多情况下,上述方法 (1) 是最佳方法。它不会向 FTS 索引添加额外数据,也不需要 FTS5 查询多个术语,因此在磁盘空间和查询速度方面效率很高。但是,它对前缀查询的支持不是很好。如果,如上所述,分词器将标记“first”替换为“1st”,则查询

... MATCH '1s*'

将不匹配包含标记“1st”的文档(因为分词器可能不会将“1s”映射到“first”的任何前缀)。

为了获得完整的前缀支持,可能更喜欢方法 (3)。在这种情况下,因为索引包含“first”和“1st”的条目,所以像 'fi*' 或 '1s*' 这样的前缀查询将正确匹配。但是,由于向 FTS 索引添加了额外的条目,因此此方法在数据库中使用了更多空间。

方法 (2) 提供了 (1) 和 (3) 之间的中点。使用此方法,像 '1s*' 这样的查询将匹配包含字面标记“1st”的文档,但不匹配“first”(假设分词器无法为前缀提供同义词)。但是,像 '1st' 这样的非前缀查询将匹配“1st”和“first”。此方法不需要额外的磁盘空间,因为没有向 FTS 索引添加额外的条目。另一方面,它可能需要更多的 CPU 周期来运行 MATCH 查询,因为每个同义词都需要对 FTS 索引进行单独的查询。

使用方法 (2) 或 (3) 时,重要的是分词器仅在分词文档文本(方法 (3))或查询文本(方法 (2))时提供同义词,而不是两者都提供。这样做不会导致任何错误,但效率低下。

7.2. 自定义辅助函数

实现自定义辅助函数类似于实现标量 SQL 函数。实现应该是一个类型为 fts5_extension_function 的 C 函数,定义如下

typedef struct Fts5ExtensionApi Fts5ExtensionApi;
typedef struct Fts5Context Fts5Context;
typedef struct Fts5PhraseIter Fts5PhraseIter;

typedef void (*fts5_extension_function)(
  const Fts5ExtensionApi *pApi,   /* API offered by current FTS version */
  Fts5Context *pFts,              /* First arg to pass to pApi functions */
  sqlite3_context *pCtx,          /* Context for returning result/error */
  int nVal,                       /* Number of values in apVal[] array */
  sqlite3_value **apVal           /* Array of trailing arguments */
);

通过调用 fts5_api 对象的 xCreateFunction() 方法将实现注册到 FTS5 模块。如果已存在具有相同名称的辅助函数,则将其替换为新函数。如果将非 NULL xDestroy 参数传递给 xCreateFunction(),则在数据库句柄关闭或注册的辅助函数被替换时,使用作为唯一参数传递的 pUserData 指针的副本调用它。

如果成功,xCreateFunction() 返回 SQLITE_OK。否则,它返回一个 SQLite 错误代码。在这种情况下,不会调用 xDestroy 函数。

传递给辅助函数回调的最后三个参数(上述 pCtx、nVal 和 apVal)类似于传递给标量 SQL 函数实现的三个参数。apVal[] 数组包含传递给辅助函数的所有 SQL 参数(第一个参数除外)。实现应通过内容句柄 pCtx 返回结果或错误。

传递给辅助函数回调的第一个参数是指向一个结构体的指针(上述 pApi),该结构体包含可以调用的方法,以便获取有关当前查询或行的信息。第二个参数是不透明句柄(上述 pFts),应将其作为任何此类方法调用的第一个参数传递。例如,以下辅助函数返回当前行所有列中的标记总数

/*
** Implementation of an auxiliary function that returns the number
** of tokens in the current row (including all columns).
*/
static void column_size_imp(
  const Fts5ExtensionApi *pApi,
  Fts5Context *pFts,
  sqlite3_context *pCtx,
  int nVal,
  sqlite3_value **apVal
){
  int rc;
  int nToken;
  rc = pApi->xColumnSize(pFts, -1, &nToken);
  if( rc==SQLITE_OK ){
    sqlite3_result_int(pCtx, nToken);
  }else{
    sqlite3_result_error_code(pCtx, rc);
  }
}

下一节详细描述了提供给辅助函数实现的 API。在源代码的“fts5_aux.c”文件中可以找到更多示例。

7.2.1. 自定义辅助函数 API 概述

本节概述了辅助函数 API 的功能。它没有描述每个函数。有关完整描述,请参阅下面的参考文本

调用时,辅助函数实现可以访问允许其查询 FTS5 以获取各种信息的 API。其中一些 API 返回与正在访问的 FTS5 表的当前行相关的信息,一些返回与 FTS5 查询将访问的整行集相关的信息,还有一些返回与 FTS5 表相关的信息。给定如下填充的 FTS5 表

CREATE VIRTUAL TABLE ft USING fts5(a, b);
INSERT INTO ft(rowid, a, b) VALUES
        (1, 'ab cd', 'cd de one'),
        (2, 'de fg', 'fg gh'),
        (3, 'gh ij', 'ij ab three four');

以及查询

SELECT my_aux_function(ft) FROM ft('ab')

则将为行 1 和 3 调用自定义辅助函数(包含标记“ab”的所有行,因此与查询匹配)。

表中的行/列数:xRowCount、xColumnCount

可以使用xRowCount API 查询系统以获取 FTS5 表中的行总数。这提供了表中的行总数,而不是与当前查询匹配的行数。

表列从左到右从 0 开始编号。“rowid”列不计数 - 仅用户声明的列 - 因此在上面的示例中,列“a”是列 0,列“b”是列 1。在辅助函数实现中,可以使用xColumnCount API 确定正在查询的表有多少列。如果在上述示例中从 my_aux_function 辅助函数的实现中调用 xColumnCount() API,则它返回 2。

来自当前行的数据:xColumnText、xRowid

可以使用xRowid API 查找当前行的 rowid 值。可以使用xColumnText 获取存储在当前行指定列中的文本。

标记计数:xColumnSize、xColumnTotalSize

FTS5 将插入 fts 表中的文档划分为标记。这些通常只是单词,可能折叠为大写或小写,并删除任何标点符号。例如,默认的unicode61 分词器 将文本“The tokenizer is case-insensitive”分词为 5 个标记列表 -“the”、“tokenizer”、“is”、“case”和“insensitive”。从文本中提取标记的确切方式由分词器确定。

辅助函数 API 提供函数来查询当前行指定列中的标记数(xColumnSize API),或查询表所有行指定列中的标记数(xColumnTotalSize API)。对于本节顶部的示例,在访问行 1 时,xColumnSize 返回列 0 的 2 和列 1 的 3。无论当前行如何,xColumnTotalSize 都返回列 0 的 6 和列 1 的 9。

当前全文查询:xPhraseCount、xPhraseSize、xQueryToken

FTS5 查询包含一个或多个短语xPhraseCountxPhraseSizexQueryToken API 允许辅助函数实现查询系统以获取当前查询的详细信息。xPhraseCount API 返回当前查询中短语的数量。例如,如果如下查询 FTS5 表

SELECT my_aux_function(ft) FROM ft('ab AND "cd ef gh" OR ij + kl')

并且从辅助函数的实现中调用 xPhraseCount() API,它返回 3(三个短语分别是“ab”、“ce ef gh”和“ij kl”)。

短语按在查询中出现的顺序从 0 开始编号。可以使用 xPhraseSize() API 查询指定查询短语中的标记数。在上面的示例中,短语 0 包含 1 个标记,短语 1 包含 3 个标记,短语 2 包含 2 个标记。

可以使用 xQueryToken API 访问指定查询短语中指定标记的文本。标记在其短语中从左到右从 0 开始编号。例如,如果使用 xQueryToken API 请求上述示例中短语 2 的标记 1,则它返回文本“kl”。短语 0 的标记 0 是“ab”。

当前行中的短语命中:xPhraseFirst、xPhraseNext

这两个 API 函数可用于遍历当前行中指定查询短语的匹配项。短语匹配项由列和当前行中的标记偏移量标识。例如,假设以下示例表

CREATE VIRTUAL TABLE ft2 USING fts5(x, y);
INSERT INTO ft2(rowid, x, y) VALUES
        (1, 'xxx one two xxx five xxx six', 'seven four'),
        (2, 'five four four xxx six', 'three four five six four five six');

用以下内容查询

SELECT my_aux_function(ft2) FROM ft2(
    '("one two" OR "three") AND y:four NEAR(five six, 2)'
);

上面的查询包含 5 个短语 -“one two”、“three”、“four”、“five”和“six”。它匹配表的所有行,因此为每一行调用辅助函数。

在行 1 中,对于短语 0,“one two”,只有一个匹配项需要迭代 - 在列 0 标记偏移量 1 处。列号为 0,因为匹配项出现在最左边的列中。标记偏移量为 1,因为在列值中短语匹配之前正好有一个标记(“xxx”)。对于短语 1,“three”,没有匹配项。短语 2,“four”,有一个匹配项,在列 1,标记偏移量 0 处。短语 3,“five”,有一个匹配项,在列 0,标记偏移量 4 处,短语 4,“six”,有一个匹配项,在列 0 标记偏移量 6 处。

下表显示了示例中每一行每个短语的匹配项集。每个匹配项都表示为(列号,标记偏移量)

短语 0短语 1短语 2短语 3短语 4
1(0, 1) (1, 1)(0, 4)(0, 6)
2(1,0)(1, 1), (1,4)(1, 2), (1, 5)(1, 3), (1, 6)

第二行稍微复杂一些。没有出现短语 0。短语 1(“three”)出现一次,在列 1 标记偏移量 0 处。尽管在列 0 中有短语 2(“four”)的实例,但 API 不会报告任何一个,因为短语 4 有一个列过滤器 -“y:”。列过滤器过滤掉的匹配项不计入。类似地,尽管短语 3 和 4 确实出现在行 2 的列“x”中,但它们被NEAR 过滤器过滤掉了。NEAR 过滤器过滤掉的匹配项也不计入。

当前行中的短语命中(2):xInstCount、xInst

xInstCountxInst API 提供对与上面描述的 xPhraseFirst 和 xPhraseNext 相同的信息的访问。不同之处在于,xInstCount/xInst API 不是遍历单个指定短语的匹配项,而是将所有匹配项整理到一个扁平数组中,并按在当前行中出现的顺序排序。然后可以随机访问此数组的元素。

每个数组元素包含三个值

使用与上面 xPhraseFirst/xPhraseNext 相同的示例数据和查询,通过 xInstCount/xInst 访问的数组包含以下每个行的条目

xInstCount/xInst 数组
1(0, 0, 1), (3, 0, 4), (4, 0, 6), (2, 1, 1)
2(1, 1, 0), (2, 1, 1), (3, 1, 2), (4, 1, 3), (2, 1, 4), (3, 1, 5), (4, 1, 6)

数组的每个条目称为短语匹配。短语匹配项按顺序编号,从 0 开始。因此,在上面的示例中,在行 2 中,短语匹配项 3 是 (4, 1, 3) - 查询的短语 4 匹配列 1,标记偏移量 3。

7.2.2. 自定义辅助函数 API 参考

struct Fts5ExtensionApi {
  int iVersion;                   /* Currently always set to 3 */

  void *(*xUserData)(Fts5Context*);

  int (*xColumnCount)(Fts5Context*);
  int (*xRowCount)(Fts5Context*, sqlite3_int64 *pnRow);
  int (*xColumnTotalSize)(Fts5Context*, int iCol, sqlite3_int64 *pnToken);

  int (*xTokenize)(Fts5Context*, 
    const char *pText, int nText, /* Text to tokenize */
    void *pCtx,                   /* Context passed to xToken() */
    int (*xToken)(void*, int, const char*, int, int, int)       /* Callback */
  );

  int (*xPhraseCount)(Fts5Context*);
  int (*xPhraseSize)(Fts5Context*, int iPhrase);

  int (*xInstCount)(Fts5Context*, int *pnInst);
  int (*xInst)(Fts5Context*, int iIdx, int *piPhrase, int *piCol, int *piOff);

  sqlite3_int64 (*xRowid)(Fts5Context*);
  int (*xColumnText)(Fts5Context*, int iCol, const char **pz, int *pn);
  int (*xColumnSize)(Fts5Context*, int iCol, int *pnToken);

  int (*xQueryPhrase)(Fts5Context*, int iPhrase, void *pUserData,
    int(*)(const Fts5ExtensionApi*,Fts5Context*,void*)
  );
  int (*xSetAuxdata)(Fts5Context*, void *pAux, void(*xDelete)(void*));
  void *(*xGetAuxdata)(Fts5Context*, int bClear);

  int (*xPhraseFirst)(Fts5Context*, int iPhrase, Fts5PhraseIter*, int*, int*);
  void (*xPhraseNext)(Fts5Context*, Fts5PhraseIter*, int *piCol, int *piOff);

  int (*xPhraseFirstColumn)(Fts5Context*, int iPhrase, Fts5PhraseIter*, int*);
  void (*xPhraseNextColumn)(Fts5Context*, Fts5PhraseIter*, int *piCol);

  /* Below this point are iVersion>=3 only */
  int (*xQueryToken)(Fts5Context*, 
      int iPhrase, int iToken, 
      const char **ppToken, int *pnToken
  );
  int (*xInstToken)(Fts5Context*, int iIdx, int iToken, const char**, int*);
};
void *(*xUserData)(Fts5Context*)

返回在扩展函数注册时传递给 xCreateFunction() API 的 pUserData 指针的副本。

int (*xColumnTotalSize)(Fts5Context*, int iCol, sqlite3_int64 *pnToken)

如果参数 iCol 小于零,则将输出变量 *pnToken 设置为 FTS5 表中的标记总数。或者,如果 iCol 非负但小于表中的列数,则返回列 iCol 中的标记总数,考虑 FTS5 表中的所有行。

如果参数 iCol 大于或等于表中的列数,则返回 SQLITE_RANGE。或者,如果发生错误(例如 OOM 条件或 IO 错误),则返回相应的 SQLite 错误代码。

int (*xColumnCount)(Fts5Context*)

返回表中的列数。

int (*xColumnSize)(Fts5Context*, int iCol, int *pnToken)

如果参数 iCol 小于零,则将输出变量 *pnToken 设置为当前行中令牌的总数。或者,如果 iCol 非负但小于表中的列数,则将 *pnToken 设置为当前行中第 iCol 列的令牌数。

如果参数 iCol 大于或等于表中的列数,则返回 SQLITE_RANGE。或者,如果发生错误(例如 OOM 条件或 IO 错误),则返回相应的 SQLite 错误代码。

如果使用带有“columnsize=0”选项创建的 FTS5 表,则此函数可能会非常低效。

int (*xColumnText)(Fts5Context*, int iCol, const char **pz, int *pn)

如果参数 iCol 小于零,或大于或等于表中的列数,则返回 SQLITE_RANGE。

否则,此函数尝试检索当前文档中第 iCol 列的文本。如果成功,则将 (*pz) 设置为指向包含以 utf-8 编码的文本的缓冲区的指针,将 (*pn) 设置为缓冲区的大小(以字节为单位,而不是字符),并返回 SQLITE_OK。否则,如果发生错误,则返回 SQLite 错误代码,并且 (*pz) 和 (*pn) 的最终值未定义。

int (*xPhraseCount)(Fts5Context*)

返回当前查询表达式中的短语数。

int (*xPhraseSize)(Fts5Context*, int iPhrase)

如果参数 iCol 小于零,或大于或等于当前查询中短语的数量(由 xPhraseCount 返回),则返回 0。否则,此函数返回查询中第 iPhrase 个短语的令牌数。短语从零开始编号。

int (*xInstCount)(Fts5Context*, int *pnInst)

将 *pnInst 设置为查询中所有短语在当前行中出现的总数。如果成功,则返回 SQLITE_OK,如果发生错误,则返回错误代码(即 SQLITE_NOMEM)。

如果使用带有“detail=none”或“detail=column”选项创建的 FTS5 表,则此 API 可能会非常慢。如果 FTS5 表是用“detail=none”或“detail=column”和“content=”选项创建的(即,如果它是一个无内容表),则此 API 始终返回 0。

int (*xInst)(Fts5Context*, int iIdx, int *piPhrase, int *piCol, int *piOff)

查询当前行中第 iIdx 个短语匹配的详细信息。短语匹配从零开始编号,因此 iIdx 参数应大于或等于零且小于 xInstCount() 输出的值。如果 iIdx 小于零或大于或等于 xInstCount() 返回的值,则返回 SQLITE_RANGE。

否则,输出参数 *piPhrase 设置为短语编号,*piCol 设置为其出现的列,*piOff 设置为短语第一个令牌的令牌偏移量。如果成功,则返回 SQLITE_OK,如果发生错误,则返回错误代码(即 SQLITE_NOMEM)。

如果使用带有“detail=none”或“detail=column”选项创建的 FTS5 表,则此 API 可能会非常慢。

sqlite3_int64 (*xRowid)(Fts5Context*)

返回当前行的 rowid。

int (*xTokenize)(Fts5Context*, const char *pText, int nText, void *pCtx, int (*xToken)(void*, int, const char*, int, int, int) )

使用属于 FTS5 表的标记器对文本进行标记。

int (*xQueryPhrase)(Fts5Context*, int iPhrase, void *pUserData, int(*)(const Fts5ExtensionApi*,Fts5Context*,void*) )

此 API 函数用于查询当前查询的第 iPhrase 个短语的 FTS 表。具体来说,一个等效于

... FROM ftstable WHERE ftstable MATCH $p ORDER BY rowid

其中 $p 设置为等效于当前查询的第 iPhrase 个短语的短语的查询被执行。应用于当前查询的第 iPhrase 个短语的任何列过滤器都包含在 $p 中。对于访问的每一行,都会调用作为第四个参数传递的回调函数。传递给回调函数的上下文和 API 对象可用于访问每个匹配行的属性。调用 Api.xUserData() 返回作为第三个参数传递给 pUserData 的指针的副本。

如果参数 iPhrase 小于零,或大于或等于查询中短语的数量(由 xPhraseCount() 返回),则此函数返回 SQLITE_RANGE。

如果回调函数返回除 SQLITE_OK 之外的任何值,则查询将被放弃,并且 xQueryPhrase 函数立即返回。如果返回值为 SQLITE_DONE,则 xQueryPhrase 返回 SQLITE_OK。否则,错误代码将向上传播。

如果查询在没有问题的情况下运行完成,则返回 SQLITE_OK。或者,如果在查询完成或被回调中断之前发生某些错误,则返回 SQLite 错误代码。

int (*xSetAuxdata)(Fts5Context*, void *pAux, void(*xDelete)(void*))

将作为第二个参数传递的指针保存为扩展函数的“辅助数据”。然后,可以通过作为同一 MATCH 查询的一部分进行的当前或任何未来对同一 fts5 扩展函数的调用使用 xGetAuxdata() API 检索该指针。

每个扩展函数为每个 FTS 查询(MATCH 表达式)分配一个辅助数据插槽。如果对单个 FTS 查询多次调用扩展函数,则所有调用共享一个辅助数据上下文。

如果在调用此函数时已存在辅助数据指针,则将其替换为新指针。如果最初与原始指针一起指定了 xDelete 回调,则此时会调用它。

在 FTS5 查询完成后,也会对辅助数据指针调用 xDelete 回调(如果已指定)。

如果在此函数中发生错误(例如 OOM 条件),则辅助数据将设置为 NULL 并返回错误代码。如果 xDelete 参数不为 NULL,则在返回之前会对辅助数据指针调用它。

void *(*xGetAuxdata)(Fts5Context*, int bClear)

返回 fts5 扩展函数的当前辅助数据指针。有关详细信息,请参阅 xSetAuxdata() 方法。

如果 bClear 参数非零,则在此函数返回之前清除辅助数据(设置为 NULL)。在这种情况下,不会调用 xDelete(如果有)。

int (*xRowCount)(Fts5Context*, sqlite3_int64 *pnRow)

此函数用于检索表中的行总数。换句话说,与

SELECT count(*) FROM ftstable;
int (*xPhraseFirst)(Fts5Context*, int iPhrase, Fts5PhraseIter*, int*, int*)

此函数与类型 Fts5PhraseIter 和 xPhraseNext 方法一起使用,用于迭代当前行中单个查询短语的所有实例。这与通过 xInstCount/xInst API 可访问的信息相同。虽然 xInstCount/xInst API 使用起来更方便,但在某些情况下,此 API 可能会更快。要迭代第 iPhrase 个短语的实例,请使用以下代码

Fts5PhraseIter iter;
int iCol, iOff;
for(pApi->xPhraseFirst(pFts, iPhrase, &iter, &iCol, &iOff);
    iCol>=0;
    pApi->xPhraseNext(pFts, &iter, &iCol, &iOff)
){
  // An instance of phrase iPhrase at offset iOff of column iCol
}

Fts5PhraseIter 结构在上面定义。应用程序不应直接修改此结构 - 它只能按上面所示与 xPhraseFirst() 和 xPhraseNext() API 方法一起使用(以及 xPhraseFirstColumn() 和 xPhraseNextColumn() 如以下所示)。

如果使用带有“detail=none”或“detail=column”选项创建的 FTS5 表,则此 API 可能会非常慢。如果 FTS5 表是用“detail=none”或“detail=column”和“content=”选项创建的(即,如果它是一个无内容表),则此 API 始终迭代一个空集(所有对 xPhraseFirst() 的调用都将 iCol 设置为 -1)。

void (*xPhraseNext)(Fts5Context*, Fts5PhraseIter*, int *piCol, int *piOff)

请参阅上面的 xPhraseFirst。

int (*xPhraseFirstColumn)(Fts5Context*, int iPhrase, Fts5PhraseIter*, int*)

此函数和 xPhraseNextColumn() 与上面描述的 xPhraseFirst() 和 xPhraseNext() API 类似。不同之处在于,这些 API 不是迭代当前行中短语的所有实例,而是用于迭代当前行中包含一个或多个指定短语实例的列集。例如

Fts5PhraseIter iter;
int iCol;
for(pApi->xPhraseFirstColumn(pFts, iPhrase, &iter, &iCol);
    iCol>=0;
    pApi->xPhraseNextColumn(pFts, &iter, &iCol)
){
  // Column iCol contains at least one instance of phrase iPhrase
}

如果使用带有“detail=none”选项创建的 FTS5 表,则此 API 可能会非常慢。如果 FTS5 表是用“detail=none” “content=”选项创建的(即,如果它是一个无内容表),则此 API 始终迭代一个空集(所有对 xPhraseFirstColumn() 的调用都将 iCol 设置为 -1)。

使用此 API 及其配套的 xPhraseFirstColumn() 访问的信息也可以使用 xPhraseFirst/xPhraseNext(或 xInst/xInstCount)获取。此 API 的主要优点是,当与“detail=column”表一起使用时,它比这些替代方案效率更高。

void (*xPhraseNextColumn)(Fts5Context*, Fts5PhraseIter*, int *piCol)

请参阅上面的 xPhraseFirstColumn。

int (*xQueryToken)(Fts5Context*, int iPhrase, int iToken, const char **ppToken, int *pnToken )

这用于访问当前查询的第 iPhrase 个短语的第 iToken 个令牌。在返回之前,输出参数 *ppToken 设置为指向包含请求令牌的缓冲区的指针,*pnToken 设置为此缓冲区的大小(以字节为单位)。

如果 iPhrase 或 iToken 小于零,或者 iPhrase 大于或等于由 xPhraseCount() 报告的查询中短语的数量,或者 iToken 等于或大于短语中令牌的数量,则返回 SQLITE_RANGE,并且 *ppToken 和 *pnToken 都被清零。

输出文本不是指定令牌的查询文本的副本。它是标记器模块的输出。对于 tokendata=1 表,这包括任何嵌入的 0x00 和尾随数据。

int (*xInstToken)(Fts5Context*, int iIdx, int iToken, const char**, int*)

这用于访问当前行中第 iIdx 个短语命中项的第 iToken 个令牌。如果 iIdx 小于零或大于或等于 xInstCount() 返回的值,则返回 SQLITE_RANGE。否则,输出变量 (*ppToken) 设置为指向包含匹配文档令牌的缓冲区的指针,(*pnToken) 设置为该缓冲区的大小(以字节为单位)。如果指定的令牌匹配前缀查询词,则此 API 不可用。在这种情况下,两个输出变量始终设置为 0。

输出文本不是被标记化的文档文本的副本。它是标记器模块的输出。对于 tokendata=1 表,这包括任何嵌入的 0x00 和尾随数据。

如果使用带有“detail=none”或“detail=column”选项创建的 FTS5 表,则此 API 可能会非常慢。

8. fts5vocab 虚拟表模块

fts5vocab 虚拟表模块允许用户直接从 FTS5 全文索引中提取信息。fts5vocab 模块是 FTS5 的一部分 - 只要有 FTS5,它就可用。

每个 fts5vocab 表都与一个 FTS5 表关联。fts5vocab 表通常通过在 CREATE VIRTUAL TABLE 语句中用作列名的两个参数来创建 - 关联的 FTS5 表的名称和 fts5vocab 表的类型。目前有三种类型的 fts5vocab 表;“row”、“col”和“instance”。除非在“temp”数据库中创建 fts5vocab 表,否则它必须与关联的 FTS5 表位于同一数据库中。

-- Create an fts5vocab "row" table to query the full-text index belonging
-- to FTS5 table "ft1".
CREATE VIRTUAL TABLE ft1_v USING fts5vocab('ft1', 'row');

-- Create an fts5vocab "col" table to query the full-text index belonging
-- to FTS5 table "ft2".
CREATE VIRTUAL TABLE ft2_v USING fts5vocab(ft2, col);

-- Create an fts5vocab "instance" table to query the full-text index
-- belonging to FTS5 table "ft3".
CREATE VIRTUAL TABLE ft3_v USING fts5vocab(ft3, instance);

如果在 temp 数据库中创建 fts5vocab 表,则它可能与任何附加数据库中的 FTS5 表关联。为了将 fts5vocab 表附加到位于除“temp”之外的数据库中的 FTS5 表,数据库的名称将插入 CREATE VIRTUAL TABLE 参数中的 FTS5 表名称之前。例如

-- Create an fts5vocab "row" table to query the full-text index belonging
-- to FTS5 table "ft1" in database "main".
CREATE VIRTUAL TABLE temp.ft1_v USING fts5vocab(main, 'ft1', 'row');

-- Create an fts5vocab "col" table to query the full-text index belonging
-- to FTS5 table "ft2" in attached database "aux".
CREATE VIRTUAL TABLE temp.ft2_v USING fts5vocab('aux', ft2, col);

-- Create an fts5vocab "instance" table to query the full-text index
-- belonging to FTS5 table "ft3" in attached database "other".
CREATE VIRTUAL TABLE temp.ft2_v USING fts5vocab('aux', ft3, 'instance');

在除“temp”之外的任何数据库中创建 fts5vocab 表时,指定三个参数会导致错误。

类型为“row”的 fts5vocab 表包含关联的 FTS5 表中每个不同术语的一行。表列如下所示

内容
term存储在 FTS5 索引中的术语。
doc包含至少一个术语实例的行数。
cnt整个 FTS5 表中术语的实例总数。

类型为“col”的 fts5vocab 表包含关联的 FTS5 表中每个不同术语/列组合的一行。表列如下所示

内容
term存储在 FTS5 索引中的术语。
col包含术语的 FTS5 表列的名称。
docFTS5 表中列 $col 包含至少一个术语实例的行数。
cnt出现在 FTS5 表的列 $col 中的术语的实例总数(考虑所有行)。

一个类型为“instance”的 fts5vocab 表包含关联的 FTS 索引中存储的每个术语实例对应的一行。假设 FTS5 表是用 'detail' 选项设置为 'full' 创建的,则表列如下所示

内容
term存储在 FTS5 索引中的术语。
doc包含术语实例的文档的 rowid。
col包含术语实例的列的名称。
偏移量术语实例在其列中的索引。术语按出现顺序从 0 开始编号。

如果 FTS5 表是用 'detail' 选项设置为 'col' 创建的,则实例虚拟表的 offset 列始终包含 NULL。在这种情况下,表中每一对唯一的术语/文档/列组合都对应一行。或者,如果 FTS5 表是用 'detail' 设置为 'none' 创建的,则 offsetcol 始终都包含 NULL 值。对于 detail=none 的 FTS5 表,fts5vocab 表中每一对唯一的术语/文档组合都对应一行。

示例

-- Assuming a database created using:
CREATE VIRTUAL TABLE ft1 USING fts5(c1, c2);
INSERT INTO ft1 VALUES('apple banana cherry', 'banana banana cherry');
INSERT INTO ft1 VALUES('cherry cherry cherry', 'date date date');

-- Then querying the following fts5vocab table (type "col") returns:
--
--    apple  | c1 | 1 | 1
--    banana | c1 | 1 | 1
--    banana | c2 | 1 | 2
--    cherry | c1 | 2 | 4
--    cherry | c2 | 1 | 1
--    date   | c3 | 1 | 3
--
CREATE VIRTUAL TABLE ft1_v_col USING fts5vocab(ft1, col);

-- Querying an fts5vocab table of type "row" returns:
--
--    apple  | 1 | 1
--    banana | 1 | 3
--    cherry | 2 | 5
--    date   | 1 | 3
--
CREATE VIRTUAL TABLE ft1_v_row USING fts5vocab(ft1, row);

-- And, for type "instance"
INSERT INTO ft1 VALUES('apple banana cherry', 'banana banana cherry');
INSERT INTO ft1 VALUES('cherry cherry cherry', 'date date date');
--
--    apple  | 1 | c1 | 0
--    banana | 1 | c1 | 1
--    banana | 1 | c2 | 0
--    banana | 1 | c2 | 1
--    cherry | 1 | c1 | 2
--    cherry | 1 | c2 | 2
--    cherry | 2 | c1 | 0
--    cherry | 2 | c1 | 1
--    cherry | 2 | c1 | 2
--    date   | 2 | c2 | 0
--    date   | 2 | c2 | 1
--    date   | 2 | c2 | 2
--
CREATE VIRTUAL TABLE ft1_v_instance USING fts5vocab(ft1, instance);

9. FTS5 数据结构

本节从高级别描述了 FTS 模块在数据库中存储其索引和内容的方式。在应用程序中使用 FTS 无需阅读或理解本节中的内容。但是,对于尝试分析和理解 FTS 性能特征的应用程序开发人员,或者对于考虑增强现有 FTS 功能集的开发人员来说,这可能会有用。

当在数据库中创建 FTS5 虚拟表时,会在数据库中创建 3 到 5 个实际表。这些被称为“影子表”,虚拟表模块使用它们来存储持久数据。用户不应直接访问它们。许多其他虚拟表模块,包括 FTS3rtree,也创建和使用影子表。

FTS5 创建以下影子表。在每种情况下,实际的表名都基于 FTS5 虚拟表的名称(在下文中,用 % 替换虚拟表的名称以查找实际的影子表名)。

-- This table contains most of the full-text index data. 
CREATE TABLE %_data(id INTEGER PRIMARY KEY, block BLOB);

-- This table contains the remainder of the full-text index data. 
-- It is almost always much smaller than the %_data table. 
CREATE TABLE %_idx(segid, term, pgno, PRIMARY KEY(segid, term)) WITHOUT ROWID;

-- Contains the values of persistent configuration parameters.
CREATE TABLE %_config(k PRIMARY KEY, v) WITHOUT ROWID;

-- Contains the size of each column of each row in the virtual table
-- in tokens. This shadow table is not present if the "columnsize"
-- option is set to 0.
CREATE TABLE %_docsize(id INTEGER PRIMARY KEY, sz BLOB);

-- Contains the actual data inserted into the FTS5 table. There
-- is one "cN" column for each indexed column in the FTS5 table.
-- This shadow table is not present for contentless or external 
-- content FTS5 tables. 
CREATE TABLE %_content(id INTEGER PRIMARY KEY, c0, c1...);

以下部分更详细地描述了如何使用这五个表来存储 FTS5 数据。

9.1. 变长整数格式

下面的部分引用以“变长整数”形式存储的 64 位有符号整数。FTS5 使用与 SQLite 核心各种位置使用的相同的变长整数格式。

变长整数的长度在 1 到 9 字节之间。变长整数由零个或多个高位比特设置为 1 的字节以及一个高位比特设置为 0 的字节组成,或者九个字节,取较短者。前八个字节中的每个字节的低七位以及第九个字节的所有 8 位都用于重构 64 位二进制补码整数。变长整数是大端序的:从变长整数的较早字节中获取的比特比从较晚字节中获取的比特更重要。

9.2. FTS 索引(%_idx 和 %_data 表)

FTS 索引是一个有序的键值存储,其中键是文档术语或术语前缀,关联的值是“文档列表”。文档列表是一个打包的 varint 数组,它编码术语在 FTS5 表中每个实例的位置。单个术语实例的位置定义为以下组合:

FTS 索引为数据集中每个标记最多包含 (nPrefix+1) 个条目,其中 nPrefix 是定义的 前缀索引 的数量。

与主 FTS 索引(不是前缀索引的那个)关联的键以字符“0”作为前缀。第一个前缀索引的键以“1”作为前缀。第二个前缀索引的键以“2”作为前缀,依此类推。例如,如果标记“document”插入到一个 FTS5 表中,并且 前缀索引 由 prefix="2 4" 指定,则添加到 FTS 索引的键将是“0document”、“1do”和“2docu”。

FTS 索引条目不是存储在单个树或哈希表结构中。相反,它们存储在一系列称为“段 B 树”的不可变 B 树状结构中。每次提交对 FTS5 表的写入时,都会添加一个或多个(但通常只有一个)新的段 B 树,其中包含新条目和任何已删除条目的墓碑。当查询 FTS 索引时,读取器依次查询每个段 B 树并将结果合并,优先考虑较新的数据。

每个段 B 树都被分配一个数字级别。当作为提交事务的一部分将新的段 B 树写入数据库时,它被分配到级别 0。属于单个级别的段 B 树会定期合并在一起,以创建一个分配到下一级别的单个更大的段 B 树(即,级别 0 段 B 树合并以成为单个级别 1 段 B 树)。因此,数值较大的级别包含(通常)较大段 B 树中的较旧数据。请参阅 'automerge''crisismerge''usermerge' 选项,以及 'merge''optimize' 命令,了解有关如何控制合并的详细信息。

在与术语或术语前缀关联的文档列表非常大的情况下,可能存在关联的 文档列表索引。文档列表索引类似于 B 树的内部节点集。它允许有效地查询大型文档列表以获取 rowid 或 rowid 范围。例如,在处理如下所示的查询时:

SELECT ... FROM fts_table('term') WHERE rowid BETWEEN ? AND ?

FTS5 使用段 B 树索引来定位术语“term”的文档列表,然后使用其文档列表索引(假设存在)来有效地识别具有所需范围内的 rowid 的匹配子集。

9.2.1. %_data 表的 Rowid 空间

CREATE TABLE %_data(
  id INTEGER PRIMARY KEY,
  block BLOB
);

%_data 表用于存储三种类型的记录

系统中的每个段 B 树都被分配一个唯一的 16 位段 id。只有在原始所有者段 B 树完全合并到更高级别的段 B 树之后,才能重新使用段 id。在段 B 树内,每个叶子页面都被分配一个唯一的页面编号——第一个叶子页面为 1,第二个叶子页面为 2,依此类推。

每个文档列表索引叶子页面也都被分配一个页面编号。文档列表索引中的第一个(最左侧)叶子页面被分配与其术语所在的段 B 树叶子页面相同的页面编号(因为文档列表索引仅为具有非常长文档列表的术语创建,每个段 B 树叶子最多有一个术语具有关联的文档列表索引)。将此页面编号称为 P。如果文档列表很大,以至于需要第二个叶子,则第二个叶子被分配页面编号 P+1。第三个叶子 P+2。文档列表索引 B 树的每一层(叶子、叶子的父节点、祖父母节点等)都以这种方式分配页面编号,从页面编号 P 开始。

用于在 %_data 表中存储任何给定段 B 树叶子或文档列表索引叶子或节点的“id”值如下组成:

Rowid 位内容
38..43 (16 位) 段 B 树 id 值。
37 (1 位) 文档列表索引标志。对于文档列表索引页面设置为 1,对于段 B 树叶子设置为 0。
32..36 (5 位) 树的高度。对于段 B 树和文档列表索引叶子,此值设置为 0;对于文档列表索引叶子的父节点,设置为 1;对于祖父母节点,设置为 2,依此类推。
0..31 (32 位) 页面编号

9.2.2. 结构记录格式

结构记录标识构成当前 FTS 索引的段 B 树集,以及任何正在进行的增量合并操作的详细信息。它存储在 id=10 的 %_data 表中。结构记录以单个 32 位无符号值开头——cookie 值。每次修改结构时,此值都会递增。在 cookie 值之后是三个 varint 值,如下所示:

然后,对于从 0 到 nLevel 的每个级别:

9.2.3. 平均值记录格式

平均值记录始终存储在 %_data 表的 id=1 处,它不存储任何东西的平均值。相反,它包含一个 (nCol+1) 个打包 varint 值的向量,其中 nCol 是 FTS5 表中的列数,包括未索引的列。第一个 varint 包含 FTS5 表中的行总数。第二个包含存储在最左侧 FTS5 表列中的所有值中的标记总数。第三个包含所有值的标记数,依此类推。未索引列的值始终为零。

9.2.4. 段 B 树格式

9.2.4.1. 键/文档列表格式

键/文档列表格式是一种用于以排序顺序存储一系列键(文档术语或以单个字符为前缀的术语前缀以标识它们所属的特定索引),每个键都与其关联的文档列表一起存储。该格式由交替的键和打包在一起的文档列表组成。

第一个键存储为:

每个后续键存储为:

例如,如果 FTS5 键/文档列表记录中的前两个键是“0challenger”和“0chandelier”,则第一个键存储为 varint 11,后跟 11 个字节“0challenger”,第二个键存储为 varint 4 和 7,后跟 7 个字节“ndelier”。

doclist 0 doclist 1 key/doclist 2... key 0 data key 0 size (varint) key 1 prefix size (varint) key 1 suffix size (varint) key 1 prefix data

图 1 - 术语/文档列表格式

每个文档列表都通过其 rowid 值标识包含至少一个术语或术语前缀实例的行,以及一个关联的位置列表或“poslist”,该列表枚举术语实例在行中的位置。从这个意义上说,“位置”被定义为列号和列值中的术语偏移量。

在一个 doclist 中,文档始终按 rowid 排序的顺序存储。doclist 中的第一个 rowid 按原样存储,作为一个 varint。它后面紧跟着其关联的位置列表。接下来,第一个 rowid 和第二个 rowid 之间的差值(作为一个 varint),后面跟着 doclist 中第二个 rowid 关联的文档列表。以此类推。

无法通过解析来确定 doclist 的大小。这必须存储在外部。有关 FTS5 中如何实现这一点的详细信息,请参阅下面的部分

position list 0 position list 1 position list 2... rowid 0 (varint) rowid 1 (delta-encoded varint) rowid 3 (delta-encoded varint)

图 2 - Doclist 格式

位置列表 - 通常缩写为“poslist” - 识别所讨论的标记在每一行中出现的列和标记偏移量。poslist 的格式为

col 0 offset-list 0x01 col i offset-list nSize*2 + bDel (varint) column number (i) nSize bytes

图 3 - 位置列表 (poslist) 及第 0 列和第 i 列中的偏移量

9.2.4.2. 分页

如果它足够小(默认情况下,这意味着小于 4000 字节),则段 B 树的整个内容可以按照上一节中描述的键/doclist 格式存储为 %_data 表中的单个 blob。否则,键/doclist 将被分割成页面(默认情况下,每个页面大约 4000 字节),并存储在 %_data 表中的一组连续条目中(请参阅以上内容了解详细信息)。

当键/doclist 分割成页面时,格式会进行以下修改

每个页面还有一个固定大小的 4 字节标头和一个大小可变的页脚。标头分为 2 个 16 位大端整数字段。它们包含

页面页脚由一系列 varint 组成,这些 varint 包含页面上出现的每个键的字节偏移量。如果页面上没有键,则页面页脚的大小为零字节。

hdr modified key/doclist data footer 4 bytes variable size

图 4 - 页面格式

9.2.4.3. 段索引格式

将段 B 树的内容格式化为键/doclist 格式,然后将其拆分为页面,得到的结果与 B+ 树的叶子非常相似。与其为该 B+ 树的内部节点创建格式并将其与叶子一起存储在 %_data 表中,不如将这些节点上本来存储的键添加到 %_idx 表中,定义如下

CREATE TABLE %_idx(
  segid INTEGER,              -- segment id
  term TEXT,                  -- prefix of first key on page
  pgno INTEGER,               -- (2*pgno + bDoclistIndex)
  PRIMARY KEY(segid, term)
);

对于每个包含至少一个键的“叶子”页面,都会向 %_idx 表中添加一个条目。字段设置如下

内容
segid整数段 ID。
term页面上第一个键的最小子前缀,该前缀大于前一个页面上的所有键。对于段中的第一个页面,此前缀的大小为零字节。
pgno此字段同时编码页面号(在段内 - 从 1 开始)和 doclist 索引标志。如果页面的最后一个键具有关联的 doclist 索引,则设置 doclist 索引标志。此字段的值为
       (pgno*2 + bDoclistIndexFlag)

然后,为了查找可能包含术语 t 的段 i 的叶子,FTS5 不会搜索内部节点,而是运行查询

SELECT pgno FROM %_idx WHERE segid=$i AND term>=$t ORDER BY term LIMIT 1

9.2.4.4. Doclist 索引格式

上一节中描述的段索引允许通过术语或假设存在所需大小的前缀索引的术语前缀来有效地查询段 B 树。本节中描述的数据结构(doclist 索引)允许 FTS5 有效地搜索与单个术语或术语前缀关联的 doclist 中的 rowid 或 rowid 范围。

并非所有键都具有关联的 doclist 索引。默认情况下,仅当键的 doclist 跨越超过 4 个段 B 树叶子页面时,才会为该键添加 doclist 索引。Doclist 索引本身就是 B 树,叶子和内部节点都作为 %_data 表中的条目存储,但在实践中,大多数 doclist 都足够小,可以容纳在一个叶子上。FTS5 使用与段 B 树叶子相同的粗略大小(默认情况下为 4000 字节)来存储 doclist 索引节点和叶子。

Doclist 索引叶子和内部节点使用相同的页面格式。第一个字节是“标志”字节。对于 doclist 索引 B 树的根页面,此字节设置为 0x00,对于所有其他页面,此字节设置为 0x01。页面的其余部分是一系列紧密打包的 varint,如下所示

对于 doclist 索引中最左边的 doclist 索引叶子,最左边的子页面是包含键本身的页面之后的第一个段 B 树叶子。

9.3. 文档大小表 (%_docsize 表)

CREATE TABLE %_docsize(
    id INTEGER PRIMARY KEY,   -- id of FTS5 row this record pertains to
    sz BLOB                   -- blob containing nCol packed varints
);

许多常见的搜索结果排名函数需要作为输入结果文档的大小(以标记为单位)(因为在短文档中搜索词命中被认为比在长文档中更重要)。为了快速访问此信息,对于 FTS5 表中的每一行,在 %_docsize 阴影表中都存在一个对应的记录(具有相同的 rowid),其中包含该行中每个列值的大小(以标记为单位)。

列值大小存储在一个 blob 中,该 blob 为 FTS5 表的每一列(从左到右)包含一个打包的 varint。当然,varint 包含相应列值中的标记总数。未索引的列包含在此 varint 向量中;对于它们,值始终设置为零。

此表由xColumnSize API 使用。可以通过指定columnsize=0选项完全省略此表。在这种情况下,xColumnSize API 仍然可用于辅助函数,但运行速度要慢得多。

9.4. 表内容 (%_content 表)

CREATE TABLE %_content(id INTEGER PRIMARY KEY, c0, c1...);

实际的表内容 - 插入到 FTS5 表中的值 - 存储在 %_content 表中。此表为 FTS5 表的每一列(包括任何未索引的列)创建一个“c*”列。最左边的 FTS5 表列的值存储在 %_content 表的“c0”列中,下一列的值存储在“c1”列中,依此类推。

对于外部内容或无内容 FTS5 表,此表完全省略。表。

9.5. 配置选项 (%_config 表)

CREATE TABLE %_config(k PRIMARY KEY, v) WITHOUT ROWID;

此表存储任何持久配置选项的值。列“k”存储选项的名称(文本),列“v”存储值。示例内容

sqlite> SELECT * FROM fts_tbl_config;
┌─────────────┬──────┐
│      k      │  v   │
├─────────────┼──────┤
│ crisismerge │ 8    │
│ pgsz        │ 8000 │
│ usermerge   │ 4    │
│ version     │ 4    │
└─────────────┴──────┘

附录 A:与 FTS3/4 的比较

还有类似但更成熟的FTS3/4模块可用。FTS5 是 FTS4 的新版本,其中包含各种修复程序和解决方法,用于解决在不牺牲向后兼容性的情况下无法在 FTS4 中修复的问题。其中一些问题在下面描述

应用程序移植指南

为了使用 FTS5 而不是 FTS3 或 FTS4,应用程序通常只需要进行最少的修改。其中大部分属于三类 - 创建 FTS 表时使用的 CREATE VIRTUAL TABLE 语句所需的更改、执行针对表查询的 SELECT 查询所需的更改以及使用FTS 辅助函数的应用程序所需的更改。

CREATE VIRTUAL TABLE 语句的更改

  1. 模块名称必须从“fts3”或“fts4”更改为“fts5”。

  2. 所有类型信息或约束规范都必须从列定义中删除。FTS3/4 会忽略列定义中列名称后面的所有内容,FTS5 会尝试解析它(如果解析失败,它会报告错误)。

  3. “matchinfo=fts3”选项不可用。“columnsize=0”选项等效。

  4. notindexed= 选项不可用。向列定义中添加UNINDEXED等效。

  5. ICU 分词器不可用。

  6. compress=、uncompress= 和 languageid= 选项不可用。目前还没有等效的功能。

 -- FTS3/4 statement 
CREATE VIRTUAL TABLE t1 USING fts4(
  linkid INTEGER,
  header CHAR(20),
  text VARCHAR,
  notindexed=linkid,
  matchinfo=fts3,
  tokenizer=unicode61
);

 -- FTS5 equivalent (note - the "tokenizer=unicode61" option is not
 -- required as this is the default for FTS5 anyway)
CREATE VIRTUAL TABLE t1 USING fts5(
  linkid UNINDEXED,
  header,
  text,
  columnsize=0
);

SELECT 语句的更改

  1. “docid”别名不存在。应用程序必须改用“rowid”。

  2. 当列过滤器作为 FTS 查询的一部分和使用列作为 MATCH 运算符的 LHS 指定时,查询的行为略有不同。对于具有“a”和“b”列以及类似于以下查询的表

    ... a MATCH 'b: string'
    

    FTS3/4 搜索“b”列中的匹配项。但是,FTS5 始终返回零行,因为结果首先根据“b”列进行过滤,然后根据“a”列进行过滤,没有留下任何结果。换句话说,在 FTS3/4 中,内部过滤器会覆盖外部过滤器,在 FTS5 中,会应用这两个过滤器。

  3. FTS 查询语法(MATCH 运算符的右侧)在某些方面发生了变化。FTS5 语法非常接近 FTS4 的“增强语法”。主要区别在于,FTS5 对查询字符串中无法识别的标点符号和类似字符更加严格。大多数适用于 FTS3/4 的查询也应该适用于 FTS5,不适用的查询应该返回解析错误。

辅助函数更改

FTS5 没有 matchinfo() 或 offsets() 函数,并且 snippet() 函数的功能不如 FTS3/4 中的功能齐全。但是,由于 FTS5 提供了一个允许应用程序创建自定义辅助函数的 API,因此可以在应用程序代码中实现任何所需的功能。

FTS5 提供的内置辅助函数集将来可能会得到改进。

其他问题

  1. fts4aux 模块提供的功能现在由fts5vocab提供。这两个表的模式略有不同。

  2. FTS3/4 的“merge=X,Y”命令已被FTS5 合并命令取代。

  3. FTS3/4 的“automerge=X”命令已被FTS5 自动合并选项取代。

技术差异总结

FTS5 与 FTS3/4 类似,因为每个模块的主要任务都是维护一个索引,该索引将每个唯一标记映射到该标记在一组文档中出现的实例列表,其中每个实例都由其出现的文档及其在该文档中的位置标识。例如

-- Given the following SQL:
CREATE VIRTUAL TABLE ft USING fts5(a, b);
INSERT INTO ft(rowid, a, b) VALUES(1, 'X Y', 'Y Z');
INSERT INTO ft(rowid, a, b) VALUES(2, 'A Z', 'Y Y');

-- The FTS5 module creates the following mapping on disk:
A --> (2, 0, 0)
X --> (1, 0, 0)
Y --> (1, 0, 1) (1, 1, 0) (2, 1, 0) (2, 1, 1)
Z --> (1, 1, 1) (2, 0, 1)

在上面的示例中,每个三元组通过rowid、列号(列从左到右依次编号,从0开始)和列值中的位置来标识标记实例的位置(列值中的第一个标记为0,第二个为1,依此类推)。使用此索引,FTS5能够及时回答诸如“包含标记'A'的所有文档的集合”或“包含序列'Y Z'的所有文档的集合”之类的查询。与单个标记关联的实例列表称为“实例列表”。

FTS3/4和FTS5之间的主要区别在于,在FTS3/4中,每个实例列表都存储为单个大型数据库记录,而在FTS5中,大型实例列表则分布在多个数据库记录中。这对处理包含大型列表的大型数据库具有以下影响。

由于这些原因,许多复杂的查询可能使用更少的内存并在使用FTS5时运行得更快。

FTS5与FTS3/4的不同之处还包括以下方面: