小巧。快速。可靠。
三选其二。
SQLite FTS3 和 FTS4 扩展

概述

FTS3 和 FTS4 是 SQLite 虚拟表模块,允许用户对一组文档执行全文搜索。描述全文搜索最常见(也是最有效)的方法是“Google、Yahoo 和 Bing 对放置在万维网上的文档所做的事情”。用户输入一个词语或一系列词语,可能通过二元运算符连接起来,或者分组到一个短语中,全文查询系统会找到最匹配这些词语的文档集,同时考虑用户指定的运算符和分组。本文档介绍了 FTS3 和 FTS4 的部署和使用。

FTS1 和 FTS2 是 SQLite 的过时全文搜索模块。这些较旧的模块存在已知问题,应避免使用。FTS3 的原始代码部分由来自 Google 的 Scott Hess 贡献。现在它作为 SQLite 的一部分进行开发和维护。

1. FTS3 和 FTS4 简介

FTS3 和 FTS4 扩展模块允许用户创建具有内置全文索引的特殊表(以下简称“FTS 表”)。全文索引允许用户高效地查询数据库以查找包含一个或多个词语(以下简称“词条”)的所有行,即使表包含许多大型文档。

例如,如果将“Enron 电子邮件数据集”中的 517430 个文档中的每一个都插入到 FTS 表和使用以下 SQL 脚本创建的普通 SQLite 表中

CREATE VIRTUAL TABLE enrondata1 USING fts3(content TEXT);     /* FTS3 table */
CREATE TABLE enrondata2(content TEXT);                        /* Ordinary table */

然后,可以执行以下两个查询中的任何一个以查找数据库中包含单词“linux”的文档数量(351)。使用一台台式 PC 硬件配置,对 FTS3 表的查询大约在 0.03 秒内返回,而对普通表的查询则需要 22.5 秒。

SELECT count(*) FROM enrondata1 WHERE content MATCH 'linux';  /* 0.03 seconds */
SELECT count(*) FROM enrondata2 WHERE content LIKE '%linux%'; /* 22.5 seconds */

当然,上面的两个查询并不完全等效。例如,LIKE 查询会匹配包含“linuxophobe”或“EnterpriseLinux”等词语的行(碰巧的是,Enron 电子邮件数据集实际上不包含任何此类词语),而 FTS3 表上的 MATCH 查询只选择包含“linux”作为独立词条的行。两种搜索都不区分大小写。FTS3 表在磁盘上占用大约 2006 MB,而普通表仅占用 1453 MB。使用与上面执行 SELECT 查询相同的硬件配置,FTS3 表用了不到 31 分钟的时间填充,而普通表则用了 25 分钟。

1.1. FTS3 和 FTS4 之间的区别

FTS3 和 FTS4 几乎相同。它们共享大部分相同的代码,并且它们的接口也是相同的。不同之处在于

FTS4 是对 FTS3 的增强。FTS3 自 SQLite 版本 3.5.0(2007-09-04)以来一直可用。FTS4 的增强功能是在 SQLite 版本 3.7.4(2010-12-07)中添加的。

您的应用程序应该使用哪个模块,FTS3 还是 FTS4?FTS4 有时比 FTS3 快得多,甚至快几个数量级,这取决于查询,尽管在常见情况下,这两个模块的性能是相似的。FTS4 还提供增强的 matchinfo() 输出,这在对 MATCH 操作的结果进行排名时非常有用。另一方面,在没有 matchinfo=fts3 指令的情况下,FTS4 比 FTS3 需要更多的磁盘空间,尽管在大多数情况下只有百分之一二。

对于新应用程序,推荐使用 FTS4;但是,如果需要与旧版 SQLite 兼容,那么 FTS3 通常也能满足要求。

1.2. 创建和销毁 FTS 表

与其他虚拟表类型一样,使用 CREATE VIRTUAL TABLE 语句创建新的 FTS 表。模块名称(紧跟在 USING 关键字之后)是“fts3”或“fts4”。虚拟表模块参数可以留空,在这种情况下,将创建一个只有一个用户定义的名为“content”的列的 FTS 表。或者,模块参数可以传递一个以逗号分隔的列名称列表。

如果在 CREATE VIRTUAL TABLE 语句中为 FTS 表显式提供了列名称,则可以为每个列可选地指定数据类型名称。这纯粹是语法糖,提供的类型名称不会被 FTS 或 SQLite 内核用于任何目的。这同样适用于与 FTS 列名称一起指定的任何约束 - 它们会被解析,但不会被系统以任何方式使用或记录。

-- Create an FTS table named "data" with one column - "content":
CREATE VIRTUAL TABLE data USING fts3();

-- Create an FTS table named "pages" with three columns:
CREATE VIRTUAL TABLE pages USING fts4(title, keywords, body);

-- Create an FTS table named "mail" with two columns. Datatypes
-- and column constraints are specified along with each column. These
-- are completely ignored by FTS and SQLite. 
CREATE VIRTUAL TABLE mail USING fts3(
  subject VARCHAR(256) NOT NULL,
  body TEXT CHECK(length(body)<10240)
);

除了列名称列表之外,传递给用于创建 FTS 表的 CREATE VIRTUAL TABLE 语句的模块参数还可以用于指定 分词器。这可以通过在列名称位置指定格式为“tokenize=<分词器名称> <分词器参数>”的字符串来完成,其中 <分词器名称> 是要使用的分词器的名称,<分词器参数> 是可选的以空格分隔的限定符列表,传递给分词器实现。分词器规范可以放置在列列表中的任何位置,但每个 CREATE VIRTUAL TABLE 语句最多只允许一个分词器声明。有关使用(以及在必要时实现)分词器的详细说明,请参阅 以下内容

-- Create an FTS table named "papers" with two columns that uses
-- the tokenizer "porter".
CREATE VIRTUAL TABLE papers USING fts3(author, document, tokenize=porter);

-- Create an FTS table with a single column - "content" - that uses
-- the "simple" tokenizer.
CREATE VIRTUAL TABLE data USING fts4(tokenize=simple);

-- Create an FTS table with two columns that uses the "icu" tokenizer.
-- The qualifier "en_AU" is passed to the tokenizer implementation
CREATE VIRTUAL TABLE names USING fts3(a, b, tokenize=icu en_AU);

可以使用普通的 DROP TABLE 语句从数据库中删除 FTS 表。例如

-- Create, then immediately drop, an FTS4 table.
CREATE VIRTUAL TABLE data USING fts4();
DROP TABLE data;

1.3. 填充 FTS 表

FTS 表使用 INSERTUPDATEDELETE 语句填充,与填充普通 SQLite 表的方式相同。

除了用户命名的列(或者如果在 CREATE VIRTUAL TABLE 语句中没有指定任何模块参数,则为“content”列),每个 FTS 表都有一个“rowid”列。FTS 表的 rowid 的行为方式与普通 SQLite 表的 rowid 列相同,不同之处在于,如果使用 VACUUM 命令重建数据库,FTS 表的 rowid 列中存储的值将保持不变。对于 FTS 表,“docid”作为通常的“rowid”、“oid”和“_oid_”标识符的别名是允许的。尝试插入或更新具有数据库中已存在的 docid 值的行将导致错误,就像在普通 SQLite 表中一样。

“docid”与 SQLite 中的 rowid 列的正常别名之间还有一个细微的差别。通常,如果 INSERT 或 UPDATE 语句对 rowid 列的两个或多个别名分配离散值,SQLite 会将 INSERT 或 UPDATE 语句中指定的这些值中最右边的值写入数据库。但是,在插入或更新 FTS 表时,如果同时为“docid”和一个或多个 SQLite rowid 别名分配非空值,则会被认为是错误。请参阅以下示例。

-- Create an FTS table
CREATE VIRTUAL TABLE pages USING fts4(title, body);

-- Insert a row with a specific docid value.
INSERT INTO pages(docid, title, body) VALUES(53, 'Home Page', 'SQLite is a software...');

-- Insert a row and allow FTS to assign a docid value using the same algorithm as
-- SQLite uses for ordinary tables. In this case the new docid will be 54,
-- one greater than the largest docid currently present in the table.
INSERT INTO pages(title, body) VALUES('Download', 'All SQLite source code...');

-- Change the title of the row just inserted.
UPDATE pages SET title = 'Download SQLite' WHERE rowid = 54;

-- Delete the entire table contents.
DELETE FROM pages;

-- The following is an error. It is not possible to assign non-NULL values to both
-- the rowid and docid columns of an FTS table.
INSERT INTO pages(rowid, docid, title, body) VALUES(1, 2, 'A title', 'A document body');

为了支持全文查询,FTS 维护一个反向索引,该索引将数据集中出现的每个唯一词条或单词映射到它在表内容中出现的位置。对于好奇的人来说,下面将完整描述用于将此索引存储在数据库文件中的 数据结构。这种数据结构的一个特点是,数据库中可能随时包含不止一个索引 B 树,而是多个不同的 B 树,这些 B 树会随着行的插入、更新和删除而被逐渐合并。这种技术在写入 FTS 表时提高了性能,但也给使用索引的全文查询带来了一些开销。如果评估特殊的 "optimize" 命令(格式为“INSERT INTO <fts-table>(<fts-table>) VALUES('optimize')”的 SQL 语句),FTS 会将所有现有的索引 B 树合并成一个包含整个索引的大型 B 树。这可能是一项昂贵的操作,但可以加快以后的查询速度。

例如,要优化名为“docs”的 FTS 表的全文索引

-- Optimize the internal structure of FTS table "docs".
INSERT INTO docs(docs) VALUES('optimize');

上面的语句在语法上可能看起来不正确。有关解释,请参阅描述 简单 fts 查询 的部分。

还有一种使用 SELECT 语句调用优化操作的旧方法。新代码应该使用类似于上面的 INSERT 语句来优化 FTS 结构。

1.4. 简单 FTS 查询

与所有其他 SQLite 表(无论是虚拟表还是其他表)一样,使用 SELECT 语句从 FTS 表中检索数据。

可以使用两种不同形式的 SELECT 语句高效地查询 FTS 表

如果这两种查询策略都不能使用,则对 FTS 表的所有查询都是使用对整个表的线性扫描来实现的。如果表包含大量数据,这可能是一种不切实际的方法(本页上的第一个示例表明,使用现代 PC 对 1.5 GB 的数据进行线性扫描大约需要 30 秒)。

-- The examples in this block assume the following FTS table:
CREATE VIRTUAL TABLE mail USING fts3(subject, body);

SELECT * FROM mail WHERE rowid = 15;                -- Fast. Rowid lookup.
SELECT * FROM mail WHERE body MATCH 'sqlite';       -- Fast. Full-text query.
SELECT * FROM mail WHERE mail MATCH 'search';       -- Fast. Full-text query.
SELECT * FROM mail WHERE rowid BETWEEN 15 AND 20;   -- Fast. Rowid lookup.
SELECT * FROM mail WHERE subject = 'database';      -- Slow. Linear scan.
SELECT * FROM mail WHERE subject MATCH 'database';  -- Fast. Full-text query.

在上面所有全文查询中,MATCH 运算符的右侧操作数都是一个包含单个词语的字符串。在这种情况下,MATCH 表达式对包含一个或多个指定词语实例的所有文档都计算为 true(具体取决于查看哪个示例,词语可能是“sqlite”、“search”或“database”)。将单个词语指定为 MATCH 运算符的右侧操作数将导致最简单且最常见的全文查询类型。但是,更复杂的查询也是可能的,包括短语搜索、词语前缀搜索以及搜索包含在定义的彼此接近距离内出现的词语组合的文档。可以在 下面 描述可以使用各种方法查询全文索引。

通常,全文查询不区分大小写。但是,这取决于被查询的 FTS 表使用的特定 分词器。有关详细信息,请参阅有关 分词器 的部分。

上面的段落指出,具有简单词语作为右侧操作数的 MATCH 运算符对包含指定词语的所有文档都计算为 true。在这种情况下,“文档”可以指 FTS 表一行中的单个列存储的数据,也可以指单个行中所有列的内容,具体取决于用作 MATCH 运算符左侧操作数的标识符。如果作为 MATCH 运算符左侧操作数指定的标识符是 FTS 表列名,则搜索词语必须包含在其中的文档是存储在指定列中的值。但是,如果标识符是 FTS 本身的名称,则 MATCH 运算符对 FTS 表的每一行都计算为 true,前提是任何列都包含搜索词语。以下示例演示了这一点

-- Example schema
CREATE VIRTUAL TABLE mail USING fts3(subject, body);

-- Example table population
INSERT INTO mail(docid, subject, body) VALUES(1, 'software feedback', 'found it too slow');
INSERT INTO mail(docid, subject, body) VALUES(2, 'software feedback', 'no feedback');
INSERT INTO mail(docid, subject, body) VALUES(3, 'slow lunch order',  'was a software problem');

-- Example queries
SELECT * FROM mail WHERE subject MATCH 'software';    -- Selects rows 1 and 2
SELECT * FROM mail WHERE body    MATCH 'feedback';    -- Selects row 2
SELECT * FROM mail WHERE mail    MATCH 'software';    -- Selects rows 1, 2 and 3
SELECT * FROM mail WHERE mail    MATCH 'slow';        -- Selects rows 1 and 3

乍一看,上面示例中的最后两个全文查询在语法上似乎不正确,因为有一个表名(“mail”)用作 SQL 表达式。之所以可以接受,是因为每个 FTS 表实际上都有一个与表本身同名的 隐藏 列(在本例中为“mail”)。存储在该列中的值对应用程序没有意义,但可以用作 MATCH 运算符的左侧操作数。此特殊列也可以作为参数传递给 FTS 辅助函数

以下示例说明了上述情况。表达式“docs”、“docs.docs”和“main.docs.docs”都引用列“docs”。但是,表达式“main.docs”不引用任何列。它可以用于引用表,但表名不允许出现在它在下面使用的上下文中。

-- Example schema
CREATE VIRTUAL TABLE docs USING fts4(content);

-- Example queries
SELECT * FROM docs WHERE docs MATCH 'sqlite';              -- OK.
SELECT * FROM docs WHERE docs.docs MATCH 'sqlite';         -- OK.
SELECT * FROM docs WHERE main.docs.docs MATCH 'sqlite';    -- OK.
SELECT * FROM docs WHERE main.docs MATCH 'sqlite';         -- Error.

1.5. 摘要

从用户的角度来看,FTS 表在许多方面类似于普通的 SQLite 表。可以使用 INSERT、UPDATE 和 DELETE 命令将数据添加到 FTS 表、在 FTS 表中修改数据以及从 FTS 表中删除数据,就像在普通表中一样。类似地,可以使用 SELECT 命令查询数据。以下列表总结了 FTS 表和普通表之间的区别

  1. 与所有虚拟表类型一样,无法创建附加到 FTS 表的索引或触发器。也不可能使用 ALTER TABLE 命令向 FTS 表添加额外的列(虽然可以使用 ALTER TABLE 重命名 FTS 表)。

  2. 作为创建 FTS 表所使用的“CREATE VIRTUAL TABLE”语句的一部分而指定的“数据类型”将被完全忽略。与将类型 亲和性 应用于插入值的正常规则不同,插入到 FTS 表列(除了特殊的 rowid 列)中的所有值都将在存储之前转换为 TEXT 类型。

  3. FTS 表允许使用特殊别名“docid”来引用所有 虚拟表 支持的 rowid 列。

  4. 支持 FTS MATCH 运算符来执行基于内置全文索引的查询。

  5. 支持 FTS 辅助函数snippet()offsets()matchinfo() 来支持全文查询。

  6. 每个 FTS 表都具有一个与表本身同名的 隐藏列。每行中包含在隐藏列中的值是一个 blob,只有用作 MATCH 运算符的左侧操作数,或者用作 FTS 辅助函数 中的其中一个最左侧参数时才有用。

2. 编译和启用 FTS3 和 FTS4

虽然 FTS3 和 FTS4 包含在 SQLite 核心源代码中,但默认情况下它们没有启用。要使用启用了 FTS 功能的 SQLite 编译,请在编译时定义预处理器宏 SQLITE_ENABLE_FTS3。新的应用程序还应该定义 SQLITE_ENABLE_FTS3_PARENTHESIS 宏以启用 增强查询语法(参见下文)。通常,这是通过向编译器命令行添加以下两个开关来完成的

-DSQLITE_ENABLE_FTS3
-DSQLITE_ENABLE_FTS3_PARENTHESIS

注意,启用 FTS3 也会使 FTS4 可用。没有单独的 SQLITE_ENABLE_FTS4 编译时选项。SQLite 的构建要么同时支持 FTS3 和 FTS4,要么都不支持。

如果使用聚合自动配置基于构建系统的系统,则在运行“configure”脚本时设置 CPPFLAGS 环境变量是一种设置这些宏的简便方法。例如,以下命令

CPPFLAGS="-DSQLITE_ENABLE_FTS3 -DSQLITE_ENABLE_FTS3_PARENTHESIS" ./configure <configure options>

其中 <configure options> 是通常传递给 configure 脚本的那些选项(如果有)。

由于 FTS3 和 FTS4 是虚拟表,因此 SQLITE_ENABLE_FTS3 编译时选项与 SQLITE_OMIT_VIRTUALTABLE 选项不兼容。

如果 SQLite 的构建不包含 FTS 模块,则任何尝试准备 SQL 语句以创建 FTS3 或 FTS4 表或以任何方式删除或访问现有 FTS 表的操作都将失败。返回的错误消息类似于“no such module: ftsN”(其中 N 是 3 或 4)。

如果 C 版本的 ICU 库 可用,则也可以使用定义的 SQLITE_ENABLE_ICU 预处理器宏编译 FTS。使用此宏编译将启用一个 FTS 分词器,该分词器使用 ICU 库根据指定语言和区域设置的约定将文档拆分为词语(单词)。

-DSQLITE_ENABLE_ICU

3. 全文索引查询

FTS 表最有用的地方是使用内置全文索引执行的查询。全文查询通过在从 FTS 表读取数据的 SELECT 语句的 WHERE 子句中指定形式为 "<column> MATCH <全文查询表达式>" 的子句来执行。上面描述了 简单的 FTS 查询,这些查询返回包含给定词语的所有文档。在该讨论中,假设 MATCH 运算符的右侧操作数是一个包含单个词语的字符串。本节描述了 FTS 表支持的更复杂的查询类型,以及如何通过将更复杂的查询表达式指定为 MATCH 运算符的右侧操作数来利用它们。

FTS 表支持三种基本查询类型

-- Virtual table declaration
CREATE VIRTUAL TABLE docs USING fts3(title, body);

-- Query for all documents containing the term "linux":
SELECT * FROM docs WHERE docs MATCH 'linux';

-- Query for all documents containing a term with the prefix "lin". This will match
-- all documents that contain "linux", but also those that contain terms "linear",
--"linker", "linguistic" and so on.
SELECT * FROM docs WHERE docs MATCH 'lin*';
-- Query the database for documents for which the term "linux" appears in
-- the document title, and the term "problems" appears in either the title
-- or body of the document.
SELECT * FROM docs WHERE docs MATCH 'title:linux problems';

-- Query the database for documents for which the term "linux" appears in
-- the document title, and the term "driver" appears in the body of the document
-- ("driver" may also appear in the title, but this alone will not satisfy the
-- query criteria).
SELECT * FROM docs WHERE body MATCH 'title:linux driver';
-- All documents for which "linux" is the first token of at least one
-- column.
SELECT * FROM docs WHERE docs MATCH '^linux';

-- All documents for which the first token in column "title" begins with "lin".
SELECT * FROM docs WHERE body MATCH 'title: ^lin*';
-- Query for all documents that contain the phrase "linux applications".
SELECT * FROM docs WHERE docs MATCH '"linux applications"';

-- Query for all documents that contain a phrase that matches "lin* app*". As well as
-- "linux applications", this will match common phrases such as "linoleum appliances"
-- or "link apprentice".
SELECT * FROM docs WHERE docs MATCH '"lin* app*"';
-- Virtual table declaration.
CREATE VIRTUAL TABLE docs USING fts4();

-- Virtual table data.
INSERT INTO docs VALUES('SQLite is an ACID compliant embedded relational database management system');

-- Search for a document that contains the terms "sqlite" and "database" with
-- not more than 10 intervening terms. This matches the only document in
-- table docs (since there are only six terms between "SQLite" and "database"
-- in the document).
SELECT * FROM docs WHERE docs MATCH 'sqlite NEAR database';

-- Search for a document that contains the terms "sqlite" and "database" with
-- not more than 6 intervening terms. This also matches the only document in
-- table docs. Note that the order in which the terms appear in the document
-- does not have to be the same as the order in which they appear in the query.
SELECT * FROM docs WHERE docs MATCH 'database NEAR/6 sqlite';

-- Search for a document that contains the terms "sqlite" and "database" with
-- not more than 5 intervening terms. This query matches no documents.
SELECT * FROM docs WHERE docs MATCH 'database NEAR/5 sqlite';

-- Search for a document that contains the phrase "ACID compliant" and the term
-- "database" with not more than 2 terms separating the two. This matches the
-- document stored in table docs.
SELECT * FROM docs WHERE docs MATCH 'database NEAR/2 "ACID compliant"';

-- Search for a document that contains the phrase "ACID compliant" and the term
-- "sqlite" with not more than 2 terms separating the two. This also matches
-- the only document stored in table docs.
SELECT * FROM docs WHERE docs MATCH '"ACID compliant" NEAR/2 sqlite';
-- The following query selects documents that contains an instance of the term 
-- "sqlite" separated by two or fewer terms from an instance of the term "acid",
-- which is in turn separated by two or fewer terms from an instance of the term
-- "relational".
SELECT * FROM docs WHERE docs MATCH 'sqlite NEAR/2 acid NEAR/2 relational';

-- This query matches no documents. There is an instance of the term "sqlite" with
-- sufficient proximity to an instance of "acid" but it is not sufficiently close
-- to an instance of the term "relational".
SELECT * FROM docs WHERE docs MATCH 'acid NEAR/2 sqlite NEAR/2 relational';

短语和 NEAR 查询可能不会跨越一行中的多个列。

上面描述的三种基本查询类型可用于查询全文索引,以获取与指定条件匹配的文档集。使用 FTS 查询表达式语言,可以对基本查询的结果执行各种集合操作。目前支持三种操作

FTS 模块可以编译为使用两种略微不同的全文查询语法版本之一,即“标准”查询语法和“增强”查询语法。上面描述的基本词语、词语前缀、短语和 NEAR 查询在这两种语法版本中都是相同的。指定集合操作的方式略有不同。以下两个小节描述了两种查询语法版本中与集合操作相关的部分。有关编译说明,请参阅有关如何 编译 fts 的说明。

3.1. 使用增强查询语法进行集合操作

增强查询语法支持 AND、OR 和 NOT 二元集合运算符。运算符的两个操作数中的每一个都可以是基本 FTS 查询,也可以是另一个 AND、OR 或 NOT 集合操作的结果。运算符必须使用大写字母输入。否则,它们将被解释为基本词语查询,而不是集合运算符。

AND 运算符可以隐式指定。如果两个基本查询在 FTS 查询字符串中出现,之间没有运算符分隔,则结果与两个基本查询之间用 AND 运算符分隔相同。例如,查询表达式“implicit operator”是“implicit AND operator”的更简洁版本。

-- Virtual table declaration
CREATE VIRTUAL TABLE docs USING fts3();

-- Virtual table data
INSERT INTO docs(docid, content) VALUES(1, 'a database is a software system');
INSERT INTO docs(docid, content) VALUES(2, 'sqlite is a software system');
INSERT INTO docs(docid, content) VALUES(3, 'sqlite is a database');

-- Return the set of documents that contain the term "sqlite", and the
-- term "database". This query will return the document with docid 3 only.
SELECT * FROM docs WHERE docs MATCH 'sqlite AND database';

-- Again, return the set of documents that contain both "sqlite" and
-- "database". This time, use an implicit AND operator. Again, document
-- 3 is the only document matched by this query. 
SELECT * FROM docs WHERE docs MATCH 'database sqlite';

-- Query for the set of documents that contains either "sqlite" or "database".
-- All three documents in the database are matched by this query.
SELECT * FROM docs WHERE docs MATCH 'sqlite OR database';

-- Query for all documents that contain the term "database", but do not contain
-- the term "sqlite". Document 1 is the only document that matches this criteria.
SELECT * FROM docs WHERE docs MATCH 'database NOT sqlite';

-- The following query matches no documents. Because "and" is in lowercase letters,
-- it is interpreted as a basic term query instead of an operator. Operators must
-- be specified using capital letters. In practice, this query will match any documents
-- that contain each of the three terms "database", "and" and "sqlite" at least once.
-- No documents in the example data above match this criteria.
SELECT * FROM docs WHERE docs MATCH 'database and sqlite';

以上示例都使用基本全文术语查询作为集合运算演示的两个操作数。短语和 NEAR 查询也可以使用,其他集合运算的结果也可以使用。当 FTS 查询中存在多个集合运算时,运算符的优先级如下

运算符增强查询语法优先级
NOT最高优先级(最紧密的组合)。
AND
OR最低优先级(最松散的组合)。

使用增强查询语法时,可以使用括号来覆盖各种运算符的默认优先级。例如

-- Return the docid values associated with all documents that contain the
-- two terms "sqlite" and "database", and/or contain the term "library".
SELECT docid FROM docs WHERE docs MATCH 'sqlite AND database OR library';

-- This query is equivalent to the above.
SELECT docid FROM docs WHERE docs MATCH 'sqlite AND database'
  UNION
SELECT docid FROM docs WHERE docs MATCH 'library';

-- Query for the set of documents that contains the term "linux", and at least
-- one of the phrases "sqlite database" and "sqlite library".
SELECT docid FROM docs WHERE docs MATCH '("sqlite database" OR "sqlite library") AND linux';

-- This query is equivalent to the above.
SELECT docid FROM docs WHERE docs MATCH 'linux'
  INTERSECT
SELECT docid FROM (
  SELECT docid FROM docs WHERE docs MATCH '"sqlite library"'
    UNION
  SELECT docid FROM docs WHERE docs MATCH '"sqlite database"'
);

3.2. 使用标准查询语法的集合运算

使用标准查询语法的 FTS 查询集合运算与增强查询语法的集合运算类似,但并不完全相同。有以下四点不同

  1. 只支持 AND 运算符的隐式版本。在标准查询语法查询中指定字符串“AND”将被解释为包含术语“and”的文档集的术语查询。

  1. 不支持括号。

  1. 不支持 NOT 运算符。标准查询语法不支持 NOT 运算符,而是支持可以应用于基本术语和术语前缀查询(但不适用于短语或 NEAR 查询)的一元“-”运算符。具有附加一元“-”运算符的术语或术语前缀可能不会作为 OR 运算符的操作数出现。FTS 查询不能完全由具有附加一元“-”运算符的术语或术语前缀查询组成。

-- Search for the set of documents that contain the term "sqlite" but do
-- not contain the term "database".
SELECT * FROM docs WHERE docs MATCH 'sqlite -database';
  1. 集合运算的相对优先级不同。特别是,使用标准查询语法,“OR”运算符的优先级高于“AND”。使用标准查询语法时,运算符的优先级如下

运算符标准查询语法优先级
一元“-”最高优先级(最紧密的组合)。
OR
AND最低优先级(最松散的组合)。
  1. 以下示例说明了使用标准查询语法的运算符优先级
-- Search for documents that contain at least one of the terms "database"
-- and "sqlite", and also contain the term "library". Because of the differences
-- in operator precedences, this query would have a different interpretation using
-- the enhanced query syntax.
SELECT * FROM docs WHERE docs MATCH 'sqlite OR database library';

4. 辅助函数 - 片段、偏移量和 Matchinfo

FTS3 和 FTS4 模块提供三个特殊的 SQL 标量函数,这些函数可能对全文查询系统的开发人员很有用:“snippet”、 “offsets” 和 “matchinfo”。“snippet” 和 “offsets” 函数的目的是允许用户识别返回文档中查询词语的位置。“matchinfo” 函数为用户提供指标,这些指标可能有助于根据相关性过滤或排序查询结果。

所有三个特殊的 SQL 标量函数的第一个参数必须是函数应用到的 FTS 表的 FTS 隐藏列FTS 隐藏列 是所有 FTS 表中找到的自动生成的列,名称与 FTS 表本身相同。例如,给定一个名为“mail”的 FTS 表

SELECT offsets(mail) FROM mail WHERE mail MATCH <full-text query expression>;
SELECT snippet(mail) FROM mail WHERE mail MATCH <full-text query expression>;
SELECT matchinfo(mail) FROM mail WHERE mail MATCH <full-text query expression>;

这三个辅助函数仅在使用 FTS 表的全文索引的 SELECT 语句中才有用。如果在使用“按行 ID 查询”或“线性扫描”策略的 SELECT 语句中使用,则 snippet 和 offsets 都返回空字符串,matchinfo 函数返回大小为零字节的 blob 值。

所有三个辅助函数都从 FTS 查询表达式中提取一组“可匹配短语”来进行操作。给定查询的可匹配短语集包含表达式中的所有短语(包括未加引号的标记和标记前缀),除了那些以一元“-”运算符(标准语法)为前缀或用作 NOT 运算符的右操作数的子表达式的一部分的短语。

在以下前提下,FTS 表中与查询表达式中的可匹配短语之一匹配的每个标记序列被称为“短语匹配”

  1. 如果可匹配短语是 FTS 查询表达式中由 NEAR 运算符连接的一系列短语的一部分,那么每个短语匹配必须足够接近其他相关类型的短语匹配才能满足 NEAR 条件。
  2. 如果 FTS 查询中的可匹配短语被限制为匹配指定 FTS 表列中的数据,那么只有发生在该列中的短语匹配才会被考虑。

4.1. Offsets 函数

对于使用全文索引的 SELECT 查询,offsets() 函数返回一个包含一系列空格分隔的整数的文本值。对于当前行的每个 短语匹配 中的每个词语,返回列表中都有四个整数。每组四个整数的解释如下

整数解释
0 词语实例所在的列号(FTS 表的最左侧列为 0,下一个最左侧列为 1,依此类推)。
1 匹配词语在全文查询表达式中的词语编号。查询表达式中的词语从 0 开始编号,按其出现的顺序排列。
2 匹配词语在列中的字节偏移量。
3 匹配词语的字节大小。

以下代码块包含使用 offsets 函数的示例。

CREATE VIRTUAL TABLE mail USING fts3(subject, body);
INSERT INTO mail VALUES('hello world', 'This message is a hello world message.');
INSERT INTO mail VALUES('urgent: serious', 'This mail is seen as a more serious mail');

-- The following query returns a single row (as it matches only the first
-- entry in table "mail". The text returned by the offsets function is
-- "0 0 6 5 1 0 24 5".
--
-- The first set of four integers in the result indicate that column 0
-- contains an instance of term 0 ("world") at byte offset 6. The term instance
-- is 5 bytes in size. The second set of four integers shows that column 1
-- of the matched row contains an instance of term 0 ("world") at byte offset
-- 24. Again, the term instance is 5 bytes in size.
SELECT offsets(mail) FROM mail WHERE mail MATCH 'world';

-- The following query returns also matches only the first row in table "mail".
-- In this case the returned text is "1 0 5 7 1 0 30 7".
SELECT offsets(mail) FROM mail WHERE mail MATCH 'message';

-- The following query matches the second row in table "mail". It returns the
-- text "1 0 28 7 1 1 36 4". Only those occurrences of terms "serious" and "mail"
-- that are part of an instance of the phrase "serious mail" are identified; the
-- other occurrences of "serious" and "mail" are ignored.
SELECT offsets(mail) FROM mail WHERE mail MATCH '"serious mail"';

4.2. Snippet 函数

snippet 函数用于创建文档文本的格式化片段,以供在全文查询结果报告中显示。snippet 函数可以接受 1 到 6 个参数,如下所示

参数默认值说明
0 N/Asnippet 函数的第一个参数必须始终是正在查询的 FTS 表的 FTS 隐藏列,并且要从该列中获取片段。 FTS 隐藏列 是一个自动生成的列,名称与 FTS 表本身相同。
1 "<b>""开始匹配" 文本。
2 "</b>""结束匹配" 文本。
3 "<b>...</b>""省略号" 文本。
4 -1 要从其中提取返回文本片段的 FTS 表列号。列从左到右编号,从零开始。负值表示可以从任何列中提取文本。
5 -15 此整数参数的绝对值用作返回文本值中包含的(近似)标记数。允许的最大绝对值为 64。此参数的值在下文中称为 _N_。

snippet 函数首先尝试找到当前行中包含至少一个与当前行中匹配的每个可匹配短语匹配的短语匹配的、由 _|N|_ 个标记组成的文本片段,其中 _|N|_ 是传递给 snippet 函数的第六个参数的绝对值。如果存储在单个列中的文本包含少于 _|N|_ 个标记,则整个列值会被考虑。文本片段不能跨越多个列。

如果可以找到这样的文本片段,则对它进行以下修改

如果可以找到多个这样的片段,则包含更多“额外”短语匹配的片段优先。选定文本片段的开头可能会向前或向后移动几个标记,以尝试将短语匹配集中到片段的中心。

假设 _N_ 是一个正值,如果找不到包含与每个可匹配短语匹配的短语匹配的片段,则 snippet 函数尝试找到两个包含至少一个与当前行匹配的每个可匹配短语匹配的短语匹配的、近似为 _N_/2 个标记的片段。如果失败,则尝试找到三个包含 _N_/3 个标记的片段,最后尝试找到四个 _N_/4 个标记的片段。如果找不到包含所需短语匹配的四组片段,则选择提供最佳覆盖范围的四个 _N_/4 个标记的片段。

如果 _N_ 是一个负值,并且找不到包含所需短语匹配的单个片段,则 snippet 函数会搜索两个包含 _|N|_ 个标记的片段,然后是三个,然后是四个。换句话说,如果指定的 _N_ 值为负数,则如果需要多个片段才能提供所需的短语匹配覆盖范围,则片段的大小不会减小。

在找到 _M_ 个片段后,其中 _M_ 在 2 到 4 之间,如上面的段落中所述,它们按排序顺序连接在一起,并用“省略号”文本分隔。对文本进行上述三个修改,然后返回。

Note: In this block of examples, newlines and whitespace characters have
been inserted into the document inserted into the FTS table, and the expected
results described in SQL comments. This is done to enhance readability only,
they would not be present in actual SQLite commands or output.

-- Create and populate an FTS table.
CREATE VIRTUAL TABLE text USING fts4();
INSERT INTO text VALUES('
  During 30 Nov-1 Dec, 2-3oC drops. Cool in the upper portion, minimum temperature 14-16oC
  and cool elsewhere, minimum temperature 17-20oC. Cold to very cold on mountaintops,
  minimum temperature 6-12oC. Northeasterly winds 15-30 km/hr. After that, temperature
  increases. Northeasterly winds 15-30 km/hr.
');

-- The following query returns the text value:
--
--   "<b>...</b>cool elsewhere, minimum temperature 17-20oC. <b>Cold</b> to very 
--    <b>cold</b> on mountaintops, minimum temperature 6<b>...</b>".
--
SELECT snippet(text) FROM text WHERE text MATCH 'cold';

-- The following query returns the text value:
--
--   "...the upper portion, [minimum] [temperature] 14-16oC and cool elsewhere,
--    [minimum] [temperature] 17-20oC. Cold..."
--
SELECT snippet(text, '[', ']', '...') FROM text WHERE text MATCH '"min* tem*"'

4.3. Matchinfo 函数

matchinfo 函数返回一个 blob 值。如果它在不使用全文索引的查询(“按行 ID 查询”或“线性扫描”)中使用,则 blob 的大小为零字节。否则,blob 包含零个或多个机器字节序的 32 位无符号整数。返回数组中的整数的确切数量取决于查询和传递给 matchinfo 函数的第二个参数(如果有)的值。

matchinfo 函数用一个或两个参数调用。对于所有辅助函数,第一个参数必须是特殊的 FTS 隐藏列。如果指定了第二个参数,则它必须是一个文本值,该值仅包含字符“p”、“c”、“n”、“a”、“l”、“s”、“x”、“y”和“b”。如果未明确提供第二个参数,则它默认为“pcx”。第二个参数在下文中被称为“格式字符串”。

matchinfo 格式字符串中的字符从左到右处理。格式字符串中的每个字符都会导致一个或多个 32 位无符号整数值添加到返回的数组中。以下表格中的“值”列包含为每个支持的格式字符串字符追加到输出缓冲区的整数值的数量。在给定的公式中,_cols_ 是 FTS 表中的列数,_phrases_ 是查询中的 可匹配短语 的数量。

字符说明
p1 查询中的可匹配短语数量。
c1 FTS 表中用户定义的列数(即不包括 docid 或 FTS 隐藏列)。
x3 * _cols_ * _phrases_ 对于短语和表列的每个不同组合,以下三个值
  • 在当前行中,短语在列中出现的次数。
  • 短语在 FTS 表中所有行中的列中出现的总次数。
  • FTS 表中包含至少一个短语实例的行的总数。
第一组三个值对应于表的最左侧列(列 0)和查询中最左侧的可匹配短语(短语 0)。如果表具有多个列,则输出数组中的第二组三个值对应于短语 0 和列 1。然后是短语 0、列 2,依此类推,遍历表的所有列。对于短语 1、列 0 也是如此,然后是短语 1、列 1,依此类推。换句话说,可以使用以下公式找到短语 _p_ 在列 _c_ 中出现的次数的数据
          hits_this_row  = array[3 * (c + p*cols) + 0]
          hits_all_rows  = array[3 * (c + p*cols) + 1]
          docs_with_hits = array[3 * (c + p*cols) + 2]
ycols * phrases 对于每个不同的短语和表格列组合,计算在该列中出现的可用短语匹配的数量。 这通常与由 matchinfo 'x' 标志 返回的每组三个值中的第一个值相同。 但是,如果短语是未与当前行匹配的子表达式的部分,则 'y' 标志报告的命中次数为零。 这对于包含来自 OR 运算符的子运算符的 AND 运算符的表达式有所不同。 例如,考虑表达式
          a OR (b AND c)
以及文档
          "a c d"
matchinfo 'x' 标志 将为短语 "a" 和 "c" 报告单个命中。 但是,'y' 指令将 "c" 的命中次数报告为零,因为它属于未与文档匹配的子表达式 - (b AND c)。 对于不包含来自 OR 运算符的子运算符的 AND 运算符的查询,'y' 返回的结果值始终与 'x' 返回的结果值相同。

整数数组中的第一个值对应于表格最左边的列(列 0)和查询中的第一个短语(短语 0)。 对应于其他列/短语组合的值可以使用以下公式找到

          hits_for_phrase_p_column_c  = array[c + p*cols]
对于使用 OR 表达式或使用 LIMIT 或返回多行的查询,'y' matchinfo 选项可能比 'x' 速度更快。
b((cols+31)/32) * phrases matchinfo 'b' 标志提供与 matchinfo 'y' 标志 相似的信息,但形式更紧凑。 'b' 不提供精确的命中次数,而是为每个短语/列组合提供一个布尔标志。 如果短语至少在列中出现一次(即,如果 'y' 的相应整数输出为非零),则相应的标志将被设置。 否则清除。

如果表格有 32 列或更少,则为查询中的每个短语输出一个无符号整数。 如果短语至少在列 0 中出现一次,则整数的最低有效位将被设置。 如果短语在列 1 中出现一次或多次,则第二个最低有效位将被设置。 等等。

如果表格超过 32 列,则每增加 32 列或部分,将在每个短语的输出中添加一个额外的整数。 对应于相同短语的整数将被分组在一起。 例如,如果一个有 45 列的表格被查询两个短语,则输出 4 个整数。 第一个对应于短语 0 和表格的列 0-31。 第二个整数包含短语 0 和列 32-44 的数据,等等。

例如,如果 nCol 是表格中的列数,要确定短语 p 是否存在于列 c 中

    p_is_in_c = array[p * ((nCol+31)/32)] & (1 << (c % 32))
n1 FTS4 表格中的行数。 此值仅在查询 FTS4 表格时可用,而 FTS3 表格则不可用。
acols 对于每列,计算存储在列中的文本值的令牌平均数(考虑 FTS4 表格中的所有行)。 此值仅在查询 FTS4 表格时可用,而 FTS3 表格则不可用。
lcols 对于每列,计算存储在 FTS4 表格当前行中的值的长度,以令牌为单位。 此值仅在查询 FTS4 表格时可用,而 FTS3 表格则不可用。 并且仅当 "matchinfo=fts3" 指令未作为用于创建 FTS4 表格的 "CREATE VIRTUAL TABLE" 语句的一部分指定时。
scols 对于每列,计算列值与查询文本共有的最长短语匹配子序列的长度。 例如,如果一个表格列包含文本 'a b c d e',查询为 'a c "d e"',则最长公共子序列的长度为 2(短语 "c" 后跟短语 "d e")。

例如

-- Create and populate an FTS4 table with two columns:
CREATE VIRTUAL TABLE t1 USING fts4(a, b);
INSERT INTO t1 VALUES('transaction default models default', 'Non transaction reads');
INSERT INTO t1 VALUES('the default transaction', 'these semantics present');
INSERT INTO t1 VALUES('single request', 'default data');

-- In the following query, no format string is specified and so it defaults
-- to "pcx". It therefore returns a single row consisting of a single blob
-- value 80 bytes in size (20 32-bit integers - 1 for "p", 1 for "c" and
-- 3*2*3 for "x"). If each block of 4 bytes in the blob is interpreted
-- as an unsigned integer in machine byte-order, the values will be:
--
--     3 2  1 3 2  0 1 1  1 2 2  0 1 1  0 0 0  1 1 1
--
-- The row returned corresponds to the second entry inserted into table t1.
-- The first two integers in the blob show that the query contained three
-- phrases and the table being queried has two columns. The next block of
-- three integers describes column 0 (in this case column "a") and phrase
-- 0 (in this case "default"). The current row contains 1 hit for "default"
-- in column 0, of a total of 3 hits for "default" that occur in column
-- 0 of any table row. The 3 hits are spread across 2 different rows.
--
-- The next set of three integers (0 1 1) pertain to the hits for "default"
-- in column 1 of the table (0 in this row, 1 in all rows, spread across 
-- 1 rows).
--
SELECT matchinfo(t1) FROM t1 WHERE t1 MATCH 'default transaction "these semantics"';

-- The format string for this query is "ns". The output array will therefore
-- contain 3 integer values - 1 for "n" and 2 for "s". The query returns
-- two rows (the first two rows in the table match). The values returned are:
--
--     3  1 1
--     3  2 0
--
-- The first value in the matchinfo array returned for both rows is 3 (the 
-- number of rows in the table). The following two values are the lengths 
-- of the longest common subsequence of phrase matches in each column.
SELECT matchinfo(t1, 'ns') FROM t1 WHERE t1 MATCH 'default transaction';

matchinfo 函数比 snippet 或 offsets 函数快得多。 这是因为 snippet 和 offsets 的实现都需要从磁盘检索正在分析的文档,而 matchinfo 所需的所有数据都可以作为实现全文查询所需的全文索引的相同部分的一部分获得。 这意味着在以下两个查询中,第一个可能比第二个快一个数量级

SELECT docid, matchinfo(tbl) FROM tbl WHERE tbl MATCH <query expression>;
SELECT docid, offsets(tbl) FROM tbl WHERE tbl MATCH <query expression>;

matchinfo 函数提供计算概率 "词袋" 相关性分数所需的所有信息,例如 Okapi BM25/BM25F,可用于对全文搜索应用程序中的结果进行排序。 本文档的附录 A,“搜索应用程序提示”,包含一个有效使用 matchinfo() 函数的示例。

5. Fts4aux - 直接访问全文索引

版本 3.7.6(2011-04-12)开始,SQLite 包含一个名为 "fts4aux" 的新的虚拟表格模块,它可用于直接检查现有 FTS 表格的全文索引。 尽管它的名字,fts4aux 在 FTS3 表格和 FTS4 表格中效果一样好。 Fts4aux 表格是只读的。 修改 fts4aux 表格内容的唯一方法是修改关联的 FTS 表格的内容。 fts4aux 模块自动包含在所有 包含 FTS 的构建 中。

fts4aux 虚拟表格使用一个或两个参数构建。 当使用单个参数时,该参数是将用于访问的 FTS 表格的非限定名称。 要访问不同数据库中的表格(例如,要创建一个将访问 MAIN 数据库中 FTS3 表格的 TEMP fts4aux 表格),请使用两个参数形式,并在第一个参数中给出目标数据库的名称(例如: "main"),并将 FTS3/4 表格的名称作为第二个参数。(fts4aux 的两个参数形式是在 SQLite 版本 3.7.17(2013-05-20)中添加的,并且将在以前的版本中抛出错误。)例如

-- Create an FTS4 table
CREATE VIRTUAL TABLE ft USING fts4(x, y);

-- Create an fts4aux table to access the full-text index for table "ft"
CREATE VIRTUAL TABLE ft_terms USING fts4aux(ft);

-- Create a TEMP fts4aux table accessing the "ft" table in "main"
CREATE VIRTUAL TABLE temp.ft_terms_2 USING fts4aux(main,ft);

对于 FTS 表格中存在的每个词,fts4aux 表格中都有 2 到 N+1 行,其中 N 是关联的 FTS 表格中用户定义列的数量。 fts4aux 表格始终具有相同的四列,如下所示,从左到右

列名列内容
term包含此行的词的文本。
col此列可以包含文本值 '*'(即单个字符,U+002a)或 0 到 N-1 之间的整数,其中 N 再次是相应 FTS 表格中用户定义列的数量。
documents此列始终包含一个大于零的整数值。

如果 "col" 列包含值 '*',则此列包含 FTS 表格中包含至少一个词实例的行数(在任何列中)。 如果 col 包含一个整数值,则此列包含 FTS 表格中包含至少一个词实例在由 col 值标识的列中的行数。 像往常一样,FTS 表格的列从左到右编号,从零开始。
occurrences此列也始终包含一个大于零的整数值。

如果 "col" 列包含值 '*',则此列包含 FTS 表格所有行中词的实例总数(在任何列中)。 否则,如果 col 包含一个整数值,则此列包含出现在由 col 值标识的 FTS 表格列中的词的实例总数。
languageid (隐藏) 此列确定哪个 languageid 用于从 FTS3/4 表格中提取词汇表。

languageid 的默认值为 0。 如果在 WHERE 子句约束中指定了备用语言,则使用该备用语言而不是 0。 每个查询只能有一个 languageid。 换句话说,WHERE 子句不能包含 languageid 上的范围约束或 IN 运算符。

例如,使用上面创建的表格

INSERT INTO ft(x, y) VALUES('Apple banana', 'Cherry');
INSERT INTO ft(x, y) VALUES('Banana Date Date', 'cherry');
INSERT INTO ft(x, y) VALUES('Cherry Elderberry', 'Elderberry');

-- The following query returns this data:
--
--     apple       |  *  |  1  |  1
--     apple       |  0  |  1  |  1
--     banana      |  *  |  2  |  2
--     banana      |  0  |  2  |  2
--     cherry      |  *  |  3  |  3
--     cherry      |  0  |  1  |  1
--     cherry      |  1  |  2  |  2
--     date        |  *  |  1  |  2
--     date        |  0  |  1  |  2
--     elderberry  |  *  |  1  |  2
--     elderberry  |  0  |  1  |  1
--     elderberry  |  1  |  1  |  1
--
SELECT term, col, documents, occurrences FROM ft_terms;

在示例中,"term" 列中的值都是小写,即使它们以混合大小写插入表格 "ft" 中。 这是因为 fts4aux 表格包含由 词法分析器 从文档文本中提取的词。 在这种情况下,由于表格 "ft" 使用 简单词法分析器,这意味着所有词都已折叠为小写。 此外,(例如)没有列 "term" 设置为 "apple" 且列 "col" 设置为 1 的行。 由于列 1 中没有 "apple" 的实例,因此 fts4aux 表格中不存在行。

在事务期间,写入 FTS 表格的一些数据可能会被缓存到内存中,并且仅在事务提交时写入数据库。 但是,fts4aux 模块的实现只能从数据库中读取数据。 在实践中,这意味着如果从包含修改了关联的 FTS 表格的事务中查询 fts4aux 表格,则查询的结果很可能只反映对所做更改的(可能是空的)子集。

6. FTS4 选项

如果 "CREATE VIRTUAL TABLE" 语句指定了模块 FTS4(而不是 FTS3),则特殊的指令 - FTS4 选项 - 与 "tokenize=*" 选项类似,也可以出现在列名称的位置。 FTS4 选项由选项名称、紧随其后的 "=" 字符、然后是选项值组成。 选项值可以选择用单引号或双引号括起来,嵌入的引号字符以与 SQL 字面量相同的方式转义。 在 "=" 字符的两侧不应有空格。 例如,要创建一个选项 "matchinfo" 的值为 "fts3" 的 FTS4 表格

-- Create a reduced-footprint FTS4 table.
CREATE VIRTUAL TABLE papers USING fts4(author, document, matchinfo=fts3);

FTS4 目前支持以下选项

选项解释
compresscompress 选项用于指定压缩函数。 在没有指定解压缩函数的情况下指定压缩函数是错误的。 有关详细信息,请 参见下文
contentcontent 允许将要索引的文本存储在与 FTS4 表格不同的单独表格中,甚至存储在 SQLite 之外。
languageidlanguageid 选项使 FTS4 表格具有一个额外的隐藏整数列,用于标识每行中包含的文本的语言。 使用 languageid 选项允许同一个 FTS4 表格保存多种语言或脚本的文本,每种语言或脚本都有不同的词法分析规则,并且可以独立于其他语言进行查询。
matchinfo当设置为值 "fts3" 时,matchinfo 选项会减少 FTS4 存储的信息量,结果是 matchinfo() 的 "l" 选项不再可用。
notindexed此选项用于指定不为其数据建立索引的列的名称。 存储在未建立索引的列中的值不会被 MATCH 查询匹配。 它们也不会被辅助函数识别。 单个 CREATE VIRTUAL TABLE 语句可以包含任意数量的 notindexed 选项。
order "order" 选项可以设置为 "DESC" 或 "ASC"(不区分大小写)。如果设置为 "DESC",则 FTS4 会以优化按 docid 降序返回结果的方式存储其数据。如果设置为 "ASC"(默认值),则数据结构将针对按 docid 升序返回结果进行优化。换句话说,如果针对 FTS4 表运行的许多查询使用 "ORDER BY docid DESC",则向 CREATE VIRTUAL TABLE 语句添加 "order=desc" 选项可能会提高性能。
prefix此选项可以设置为以逗号分隔的正非零整数列表。对于列表中的每个整数 N,数据库文件中都会创建一个单独的索引,以优化 前缀查询,其中查询项的长度为 N 字节,不包括 "*" 字符,在使用 UTF-8 编码时。有关详细信息,请参见 以下内容
uncompress此选项用于指定解压缩函数。在不指定压缩函数的情况下指定解压缩函数会导致错误。有关详细信息,请参见 以下内容

使用 FTS4 时,如果指定包含 "=" 字符且不是 "tokenize=*" 规范或识别 FTS4 选项的列名,则会导致错误。使用 FTS3 时,未识别指令中的第一个标记将被解释为列名。同样,在单个表声明中指定多个 "tokenize=*" 指令,在使用 FTS4 时会导致错误,而 FTS3 会将第二个和后续的 "tokenize=*" 指令解释为列名。例如

-- An error. FTS4 does not recognize the directive "xyz=abc".
CREATE VIRTUAL TABLE papers USING fts4(author, document, xyz=abc);

-- Create an FTS3 table with three columns - "author", "document"
-- and "xyz".
CREATE VIRTUAL TABLE papers USING fts3(author, document, xyz=abc);

-- An error. FTS4 does not allow multiple tokenize=* directives
CREATE VIRTUAL TABLE papers USING fts4(tokenize=porter, tokenize=simple);

-- Create an FTS3 table with a single column named "tokenize". The
-- table uses the "porter" tokenizer.
CREATE VIRTUAL TABLE papers USING fts3(tokenize=porter, tokenize=simple);

-- An error. Cannot create a table with two columns named "tokenize".
CREATE VIRTUAL TABLE papers USING fts3(tokenize=porter, tokenize=simple, tokenize=icu);

6.1. compress= 和 uncompress= 选项

compress 和 uncompress 选项允许以压缩形式在数据库中存储 FTS4 内容。这两个选项都应设置为使用 sqlite3_create_function() 注册的 SQL 标量函数的名称,该函数接受一个参数。

compress 函数应返回作为参数传递给它的值的压缩版本。每次将数据写入 FTS4 表时,每个列值都将传递给 compress 函数,并将结果值存储在数据库中。compress 函数可以返回任何类型的 SQLite 值(blob、text、real、integer 或 null)。

uncompress 函数应解压缩以前由 compress 函数压缩的数据。换句话说,对于所有 SQLite 值 X,应为真,即 uncompress(compress(X)) 等于 X。当 FTS4 从数据库中读取由 compress 函数压缩的数据时,它会在使用之前将其传递给 uncompress 函数。

如果指定的 compress 或 uncompress 函数不存在,则仍然可以创建表。只有在读取 FTS4 表(如果 uncompress 函数不存在)或写入 FTS4 表(如果 compress 函数不存在)时,才会返回错误。

-- Create an FTS4 table that stores data in compressed form. This
-- assumes that the scalar functions zip() and unzip() have been (or
-- will be) added to the database handle.
CREATE VIRTUAL TABLE papers USING fts4(author, document, compress=zip, uncompress=unzip);

在实现 compress 和 uncompress 函数时,务必注意数据类型。具体来说,当用户从压缩的 FTS 表中读取值时,FTS 返回的值与 uncompress 函数返回的值完全相同,包括数据类型。如果该数据类型与最初传递给 compress 函数的原始值的类型不同(例如,如果 uncompress 函数在 compress 最初传递 TEXT 时返回 BLOB),则用户的查询可能无法按预期执行。

6.2. content= 选项

content 选项允许 FTS4 不存储正在索引的文本。content 选项可以通过两种方式使用

由于索引文档本身通常比全文索引大得多,因此 content 选项可用于实现显着的空间节省。

6.2.1. 无内容 FTS4 表

要创建一个不存储索引文档副本的 FTS4 表,应将 content 选项设置为一个空字符串。例如,以下 SQL 创建了一个包含三列 - "a"、"b" 和 "c" 的 FTS4 表

CREATE VIRTUAL TABLE t1 USING fts4(content="", a, b, c);

可以使用 INSERT 语句将数据插入此类 FTS4 表。但是,与普通的 FTS4 表不同,用户必须提供一个显式的整数 docid 值。例如

-- This statement is Ok:
INSERT INTO t1(docid, a, b, c) VALUES(1, 'a b c', 'd e f', 'g h i');

-- This statement causes an error, as no docid value has been provided:
INSERT INTO t1(a, b, c) VALUES('j k l', 'm n o', 'p q r');

无法更新或删除存储在无内容 FTS4 表中的行。尝试这样做会导致错误。

无内容 FTS4 表也支持 SELECT 语句。但是,尝试检索除 docid 列以外的任何表列的值会导致错误。可以使用辅助函数 matchinfo(),但不能使用 snippet() 和 offsets()。例如

-- The following statements are Ok:
SELECT docid FROM t1 WHERE t1 MATCH 'xxx';
SELECT docid FROM t1 WHERE a MATCH 'xxx';
SELECT matchinfo(t1) FROM t1 WHERE t1 MATCH 'xxx';

-- The following statements all cause errors, as the value of columns
-- other than docid are required to evaluate them.
SELECT * FROM t1;
SELECT a, b FROM t1 WHERE t1 MATCH 'xxx';
SELECT docid FROM t1 WHERE a LIKE 'xxx%';
SELECT snippet(t1) FROM t1 WHERE t1 MATCH 'xxx';

与尝试检索除 docid 以外的列值相关的错误是发生在 sqlite3_step() 中的运行时错误。在某些情况下,例如如果 SELECT 查询中的 MATCH 表达式匹配零行,即使语句确实引用了除 docid 以外的列值,也可能根本不会出现错误。

6.2.2. 外部内容 FTS4 表

“外部内容”FTS4 表类似于无内容表,不同之处在于,如果查询的计算需要除 docid 以外的列的值,FTS4 会尝试从用户指定(以下称为“内容表”)的表(或视图,或虚拟表)中检索该值。FTS4 模块绝不会写入内容表,并且写入内容表不会影响全文索引。确保内容表和全文索引一致的责任在于用户。

通过将 content 选项设置为表(或视图,或虚拟表)的名称来创建外部内容 FTS4 表,FTS4 可以查询该表以在需要时检索列值。如果指定的表不存在,则外部内容表的行为与无内容表相同。例如

CREATE TABLE t2(id INTEGER PRIMARY KEY, a, b, c);
CREATE VIRTUAL TABLE t3 USING fts4(content="t2", a, c);

假设指定的表存在,则其列必须与为 FTS 表定义的列相同或为其超集。外部表也必须与 FTS 表位于同一个数据库文件中。换句话说,外部表不能位于使用 ATTACH 连接的不同数据库文件中,并且 FTS 表和外部内容也不能一个位于 TEMP 数据库中,另一个位于持久数据库文件中(如 MAIN)。

当用户对 FTS 表的查询需要除 docid 以外的列值时,FTS 会尝试从内容表中相应的行读取请求的值,该行的 rowid 值等于当前的 FTS docid。只能查询 FTS/34 表声明中重复的内容表列的子集 - 要检索其他任何列的值,必须直接查询内容表。或者,如果在内容表中找不到这样的行,则使用 NULL 值代替。例如

CREATE TABLE t2(id INTEGER PRIMARY KEY, a, b, c);
CREATE VIRTUAL TABLE t3 USING fts4(content="t2", b, c);

INSERT INTO t2 VALUES(2, 'a b', 'c d', 'e f');
INSERT INTO t2 VALUES(3, 'g h', 'i j', 'k l');
INSERT INTO t3(docid, b, c) SELECT id, b, c FROM t2;
-- The following query returns a single row with two columns containing
-- the text values "i j" and "k l".
--
-- The query uses the full-text index to discover that the MATCH
-- term matches the row with docid=3. It then retrieves the values
-- of columns b and c from the row with rowid=3 in the content table
-- to return.
--
SELECT * FROM t3 WHERE t3 MATCH 'k';

-- Following the UPDATE, the query still returns a single row, this
-- time containing the text values "xxx" and "yyy". This is because the
-- full-text index still indicates that the row with docid=3 matches
-- the FTS4 query 'k', even though the documents stored in the content
-- table have been modified.
--
UPDATE t2 SET b = 'xxx', c = 'yyy' WHERE rowid = 3;
SELECT * FROM t3 WHERE t3 MATCH 'k';

-- Following the DELETE below, the query returns one row containing two
-- NULL values. NULL values are returned because FTS is unable to find
-- a row with rowid=3 within the content table.
--
DELETE FROM t2;
SELECT * FROM t3 WHERE t3 MATCH 'k';

当从外部内容 FTS4 表中删除一行时,FTS4 需要从内容表中检索要删除的行的列值。这样,FTS4 才能更新每个出现在已删除行中的标记的全文索引条目,以指示该行已被删除。如果找不到内容表行,或者如果其包含的值与 FTS 索引的内容不一致,则结果可能难以预测。FTS 索引可能保留与已删除行相对应的条目,这会导致随后的 SELECT 查询返回看似毫无意义的结果。当更新一行时,也会发生这种情况,因为在内部,UPDATE 与 DELETE 后的 INSERT 相同。

这意味着,为了使 FTS 与外部内容表保持同步,必须首先对 FTS 表应用任何 UPDATE 或 DELETE 操作,然后应用于外部内容表。例如

CREATE TABLE t1_real(id INTEGER PRIMARY KEY, a, b, c, d);
CREATE VIRTUAL TABLE t1_fts USING fts4(content="t1_real", b, c);

-- This works. When the row is removed from the FTS table, FTS retrieves
-- the row with rowid=123 and tokenizes it in order to determine the entries
-- that must be removed from the full-text index.
--
DELETE FROM t1_fts WHERE rowid = 123;
DELETE FROM t1_real WHERE rowid = 123;

-- This does not work. By the time the FTS table is updated, the row
-- has already been deleted from the underlying content table. As a result
-- FTS is unable to determine the entries to remove from the FTS index and
-- so the index and content table are left out of sync.
--
DELETE FROM t1_real WHERE rowid = 123;
DELETE FROM t1_fts WHERE rowid = 123;

一些用户可能不希望分别写入全文索引和内容表,而是希望使用数据库触发器来保持全文索引与存储在内容表中的文档集同步。例如,使用前面示例中的表

CREATE TRIGGER t2_bu BEFORE UPDATE ON t2 BEGIN
  DELETE FROM t3 WHERE docid=old.rowid;
END;
CREATE TRIGGER t2_bd BEFORE DELETE ON t2 BEGIN
  DELETE FROM t3 WHERE docid=old.rowid;
END;

CREATE TRIGGER t2_au AFTER UPDATE ON t2 BEGIN
  INSERT INTO t3(docid, b, c) VALUES(new.rowid, new.b, new.c);
END;
CREATE TRIGGER t2_ai AFTER INSERT ON t2 BEGIN
  INSERT INTO t3(docid, b, c) VALUES(new.rowid, new.b, new.c);
END;

DELETE 触发器必须在内容表上进行实际删除之前触发。这样,FTS4 仍然可以检索原始值以更新全文索引。而 INSERT 触发器必须在插入新行后触发,以便处理系统中自动分配 rowid 的情况。UPDATE 触发器必须拆分为两个部分,一个在更新内容表之前触发,另一个在更新内容表之后触发,原因相同。

FTS4“重建”命令 会删除整个全文索引,并根据内容表中的当前文档集重新构建索引。假设 "t3" 是外部内容 FTS4 表的名称,则重建命令如下所示

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

此命令也可以与普通的 FTS4 表一起使用,例如,如果标记器的实现发生更改。尝试重建无内容 FTS4 表维护的全文索引会导致错误,因为没有可用内容进行重建。

6.3. languageid= 选项

当存在 languageid 选项时,它指定另一个 隐藏列 的名称,该列将添加到 FTS4 表中,并用于指定存储在 FTS4 表的每一行中的语言。languageid 隐藏列的名称必须不同于 FTS4 表中的所有其他列名。示例

CREATE VIRTUAL TABLE t1 USING fts4(x, y, languageid="lid")

languageid 列的默认值为 0。插入 languageid 列的任何值都会转换为 32 位(而不是 64 位)有符号整数。

默认情况下,FTS 查询(使用 MATCH 运算符的查询)只考虑 languageid 列设置为 0 的那些行。要查询具有其他 languageid 值的行,必须在查询的 WHERE 子句中添加形式为 "

= " 的约束。例如
SELECT * FROM t1 WHERE t1 MATCH 'abc' AND lid=5;

单个 FTS 查询无法返回具有不同 languageid 值的行。添加使用其他运算符(例如 lid!=5 或 lid<=5)的 WHERE 子句的结果是未定义的。

如果 content 选项与 languageid 选项一起使用,则指定的 languageid 列必须存在于 content= 表中(受通常规则约束 - 如果查询不需要读取内容表,则此限制不适用)。

当使用 languageid 选项时,SQLite 会在 sqlite3_tokenizer_module 对象创建后立即在该对象上调用 xLanguageid(),以便传入标记器应使用的语言 ID。对于任何单个标记器对象,xLanguageid() 方法最多只调用一次。不同的语言可能以不同的方式进行标记,这是单个 FTS 查询无法返回具有不同 languageid 值的行的原因之一。

6.4. matchinfo= 选项

matchinfo 选项只能设置为值 "fts3"。尝试将 matchinfo 设置为除 "fts3" 以外的任何值会导致错误。如果指定了此选项,则会省略 FTS4 存储的一些额外信息。这会减少 FTS4 表占用的磁盘空间量,直到它几乎与等效的 FTS3 表使用的空间量相同,但这同时也意味着通过将 'l' 标志传递给 matchinfo() 函数访问的数据不可用。

6.5. notindexed= 选项

通常,FTS 模块会维护所有表中所有列的所有术语的倒排索引。此选项用于指定不应将条目添加到索引的列的名称。可以使用多个“notindexed”选项来指定应从索引中省略多个列。例如

-- Create an FTS4 table for which only the contents of columns c2 and c4
-- are tokenized and added to the inverted index.
CREATE VIRTUAL TABLE t1 USING fts4(c1, c2, c3, c4, notindexed=c1, notindexed=c3);

存储在未索引列中的值不符合 MATCH 运算符。它们不会影响 offsets() 或 matchinfo() 辅助函数的结果。snippet() 函数也不会返回基于存储在未索引列中的值的摘要。

6.6. prefix= 选项

FTS4 prefix 选项会导致 FTS 以与它始终索引完整术语相同的方式索引指定长度的术语前缀。prefix 选项必须设置为用逗号分隔的正非零整数列表。对于列表中的每个值 N,将索引长度为 N 字节的前缀(使用 UTF-8 编码)。FTS4 使用术语前缀索引来加速 前缀查询。当然,代价是索引术语前缀以及完整术语会增加数据库大小,并减慢 FTS4 表的写入操作。

前缀索引可用于优化 前缀查询 的两种情况。如果查询是针对 N 字节的前缀,那么使用“prefix=N”创建的前缀索引提供了最佳优化。或者,如果没有可用的“prefix=N”索引,则可以使用“prefix=N+1”索引代替。使用“prefix=N+1”索引不如“prefix=N”索引高效,但比根本没有前缀索引要好。

-- Create an FTS4 table with indexes to optimize 2 and 4 byte prefix queries.
CREATE VIRTUAL TABLE t1 USING fts4(c1, c2, prefix="2,4");

-- The following two queries are both optimized using the prefix indexes.
SELECT * FROM t1 WHERE t1 MATCH 'ab*';
SELECT * FROM t1 WHERE t1 MATCH 'abcd*';

-- The following two queries are both partially optimized using the prefix
-- indexes. The optimization is not as pronounced as it is for the queries
-- above, but still an improvement over no prefix indexes at all.
SELECT * FROM t1 WHERE t1 MATCH 'a*';
SELECT * FROM t1 WHERE t1 MATCH 'abc*';

7. FTS3 和 FTS4 的特殊命令

可以使用特殊的 INSERT 操作来向 FTS3 和 FTS4 表发出命令。每个 FTS3 和 FTS4 都有一个隐藏的只读列,其名称与表本身相同。对这个隐藏列的 INSERT 被解释为对 FTS3/4 表的命令。对于名为“xyz”的表,支持以下命令

  • INSERT INTO xyz(xyz) VALUES('optimize');

  • INSERT INTO xyz(xyz) VALUES('rebuild');

  • INSERT INTO xyz(xyz) VALUES('integrity-check');

  • INSERT INTO xyz(xyz) VALUES('merge=X,Y');

  • INSERT INTO xyz(xyz) VALUES('automerge=N');

7.1. “optimize”命令

“optimize”命令会导致 FTS3/4 将所有倒排索引 B 树合并到一个大的完整 B 树中。执行优化将使随后的查询运行得更快,因为要搜索的 B 树更少,并且它可以通过合并冗余条目来减少磁盘使用量。但是,对于大型 FTS 表,运行 optimize 可能与运行 VACUUM 一样昂贵。优化命令本质上必须读取和写入整个 FTS 表,从而产生一个大型事务。

在批处理模式操作中,其中 FTS 表最初是使用大量 INSERT 操作建立的,然后重复查询而没有进一步更改,通常在最后一个 INSERT 之后并且在第一个查询之前运行“optimize”是一个好主意。

7.2. “rebuild”命令

“rebuild”命令会导致 SQLite 丢弃整个 FTS3/4 表,然后从原始文本重新构建它。这个概念类似于 REINDEX,只是它适用于 FTS3/4 表,而不是普通索引。

每当自定义标记器的实现发生更改时,都应运行“rebuild”命令,以便可以重新标记所有内容。当在对原始内容表进行了更改后使用 FTS4 content 选项 时,“rebuild”命令也很有用。

7.3. “integrity-check”命令

“integrity-check”命令会导致 SQLite 通过将这些倒排索引与原始内容进行比较来读取和验证 FTS3/4 表中所有倒排索引的准确性。如果所有倒排索引都正常,“integrity-check”命令将静默成功,但如果发现任何问题,则会失败并显示 SQLITE_CORRUPT 错误。

“integrity-check”命令在概念上类似于 PRAGMA integrity_check。在工作系统中,“integrity-command”应该始终成功。导致完整性检查失败的可能原因包括

  • 应用程序已直接对 FTS 阴影表 进行了更改,而没有使用 FTS3/4 虚拟表,导致阴影表彼此不同步。
  • 使用 FTS4 content 选项 并且未能手动使内容与 FTS4 倒排索引同步。
  • FTS3/4 虚拟表中的错误。(“integrity-check”命令最初被认为是 FTS3/4 测试套件的一部分。)
  • 基础 SQLite 数据库文件的损坏。(有关更多信息,请参阅关于 如何损坏 和 SQLite 数据库的文档。)

7.4. “merge=X,Y”命令

“merge=X,Y”命令(其中 X 和 Y 是整数)会导致 SQLite 对将 FTS3/4 表的各种倒排索引 B 树合并到一个大型 B 树中进行有限的处理。X 值是要合并的“块”的目标数量,Y 是在将合并应用于该级别之前,该级别上所需的 B 树段的最小数量。Y 的值应介于 2 和 16 之间,建议值为 8。X 的值可以是任何正整数,但建议的值约为 100 到 300。

当 FTS 表在同一级别积累了 16 个 B 树段时,对该表的下一个 INSERT 将导致所有 16 个段合并到下一级别的单个 B 树段中。这些级别合并的影响是,大多数对 FTS 表的 INSERT 非常快,并且需要最少的内存,但偶尔的 INSERT 很慢,并且由于需要进行合并而生成一个大型事务。这导致 INSERT 的“尖峰”性能。

为了避免尖峰 INSERT 性能,应用程序可以定期运行“merge=X,Y”命令,可能在空闲线程或空闲进程中,以确保 FTS 表从不积累太多同一级别的 B 树段。可以通过在每几千个文档插入后运行“merge=X,Y”来避免 INSERT 性能尖峰,并且可以最大限度地提高 FTS3/4 的性能。每个“merge=X,Y”命令将在一个单独的事务中运行(当然,除非它们使用 BEGIN...COMMIT 分组在一起)。可以通过选择 100 到 300 范围内的 X 值来保持事务较小。运行合并命令的空闲线程可以通过检查 sqlite3_total_changes() 在每个“merge=X,Y”命令之前和之后的变化量,并在该差异降至 2 以下时停止循环,从而了解何时完成。

7.5. “automerge=N”命令

“automerge=N”命令(其中 N 是 0 到 15 之间的整数,包含 0 和 15)用于配置 FTS3/4 表的“automerge”参数,该参数控制自动增量倒排索引合并。新表的默认 automerge 值为 0,这意味着完全禁用自动增量合并。如果使用“automerge=N”命令修改了 automerge 参数的值,则新参数值将持久存储在数据库中,并由所有随后建立的数据库连接使用。

将 automerge 参数设置为非零值将启用自动增量合并。这会导致 SQLite 在每次 INSERT 操作后进行少量倒排索引合并。执行的合并量旨在使 FTS3/4 表永远不会达到在同一级别有 16 个段的程度,因此必须进行大量的合并才能完成插入。换句话说,自动增量合并旨在防止尖峰 INSERT 性能。

自动增量合并的缺点是,它使 FTS3/4 表上的每个 INSERT、UPDATE 和 DELETE 操作运行速度都稍慢一些,因为必须使用额外的时间来进行增量合并。为了获得最佳性能,建议应用程序禁用自动增量合并,而是使用 "merge" 命令 在空闲进程中保持倒排索引合并良好。但是,如果应用程序的结构不容易允许空闲进程,则使用自动增量合并是一个非常合理的备用解决方案。

automerge 参数的实际值决定了自动倒排索引合并同时合并的索引段数量。如果将该值设置为 N,则系统将等待直到单个级别上至少有 N 个段,然后才开始增量合并它们。设置较低的 N 值会导致段更快地合并,这可能会加快全文查询的速度,并且如果工作负载包含 UPDATE 或 DELETE 操作以及 INSERT 操作,则会减少磁盘上由全文索引使用的空间。但是,它也会增加写入磁盘的数据量。

对于工作负载包含少量 UPDATE 或 DELETE 操作的一般情况,automerge 的一个不错的选择是 8。如果工作负载包含许多 UPDATE 或 DELETE 命令,或者如果查询速度是一个问题,则减少 automerge 到 2 可能会更有优势。

出于向后兼容性的原因,“automerge=1”命令将 automerge 参数设置为 8,而不是 1(值为 1 根本没有意义,因为合并单个段的数据是一个无操作)。

8. 标记器

FTS 标记器是一组从文档或基本 FTS 全文查询中提取术语的规则。

除非在用于创建 FTS 表的 CREATE VIRTUAL TABLE 语句中指定了特定的标记器,否则将使用默认标记器“simple”。simple 标记器根据以下规则从文档或基本 FTS 全文查询中提取标记

  • 术语是合格字符的连续序列,其中合格字符是所有字母数字字符以及所有 Unicode 代码点值大于或等于 128 的字符。所有其他字符在将文档拆分为术语时都会被丢弃。它们唯一的贡献是分隔相邻的术语。

  • 作为标记化过程的一部分,ASCII 范围(Unicode 代码点小于 128)内的所有大写字母都将转换为它们的小写等效项。因此,当使用 simple 标记器时,全文查询不区分大小写。

例如,当一个文档包含文本“Right now, they're very frustrated.”时,从文档中提取并添加到全文索引的术语(按顺序)是“right now they re very frustrated”。这样的文档将匹配“MATCH 'Frustrated'”这样的全文查询,因为 simple 标记器在搜索全文索引之前将查询中的术语转换为小写。

除了“simple”标记器之外,FTS 源代码还提供了一个使用 Porter Stemming 算法 的标记器。此标记器使用相同的规则将输入文档拆分为术语,包括将所有术语折叠为小写,但还会使用 Porter Stemming 算法将相关的英语单词缩减为一个共同的词根。例如,使用与上段相同的输入文档,porter 标记器会提取以下标记:“right now thei veri frustrat”。即使其中一些术语甚至不是英语单词,但在某些情况下,使用它们来构建全文索引比 simple 标记器产生的更易懂的输出更有用。使用 porter 标记器,文档不仅匹配“MATCH 'Frustrated'”这样的全文查询,还匹配“MATCH 'Frustration'”这样的查询,因为“Frustration”这个词被 Porter 词干算法简化为“frustrat”——就像“Frustrated”一样。因此,当使用 porter 标记器时,FTS 不仅能够找到查询术语的确切匹配项,还能找到类似英语术语的匹配项。有关 Porter Stemming 算法的更多信息,请参阅上面链接的页面。

说明“simple”和“porter”标记器之间区别的示例

-- Create a table using the simple tokenizer. Insert a document into it.
CREATE VIRTUAL TABLE simple USING fts3(tokenize=simple);
INSERT INTO simple VALUES('Right now they''re very frustrated');

-- The first of the following two queries matches the document stored in
-- table "simple". The second does not.
SELECT * FROM simple WHERE simple MATCH 'Frustrated';
SELECT * FROM simple WHERE simple MATCH 'Frustration';

-- Create a table using the porter tokenizer. Insert the same document into it
CREATE VIRTUAL TABLE porter USING fts3(tokenize=porter);
INSERT INTO porter VALUES('Right now they''re very frustrated');

-- Both of the following queries match the document stored in table "porter".
SELECT * FROM porter WHERE porter MATCH 'Frustrated';
SELECT * FROM porter WHERE porter MATCH 'Frustration';

如果此扩展是在定义了 SQLITE_ENABLE_ICU 预处理器符号的情况下编译的,那么就存在一个使用 ICU 库实现的内置标记器,名为“icu”。传递给此标记器的 xCreate() 方法(请参阅 fts3_tokenizer.h)的第一个参数可以是 ICU 地域标识符。例如,“tr_TR”代表土耳其语(土耳其使用),或者“en_AU”代表英语(澳大利亚使用)。例如

CREATE VIRTUAL TABLE thai_text USING fts3(text, tokenize=icu th_TH)

ICU 分词器的实现非常简单。它根据 ICU 规则查找词边界来分割输入文本,并丢弃任何完全由空白字符组成的标记。这可能适用于某些语言环境中的某些应用程序,但不适用于所有应用程序。如果需要更复杂的处理,例如实现词干提取或丢弃标点符号,可以通过创建使用 ICU 分词器作为其实现一部分的分词器实现来完成。

"unicode61" 分词器从 SQLite 版本 3.7.13 (2012-06-11) 开始可用。Unicode61 的工作方式与 "simple" 非常相似,只是它根据 Unicode 版本 6.1 中的规则进行简单的 Unicode 大小写折叠,并且它识别 Unicode 空格和标点符号字符,并使用它们来分隔标记。简单的分词器只对 ASCII 字符进行大小写折叠,并且只识别 ASCII 空格和标点符号字符作为标记分隔符。

默认情况下,"unicode61" 尝试从拉丁字母脚本字符中删除变音符号。此行为可以通过添加分词器参数 "remove_diacritics=0" 来覆盖。例如

-- Create tables that remove alldiacritics from Latin script characters
-- as part of tokenization.
CREATE VIRTUAL TABLE txt1 USING fts4(tokenize=unicode61);
CREATE VIRTUAL TABLE txt2 USING fts4(tokenize=unicode61 "remove_diacritics=2");

-- Create a table that does not remove diacritics from Latin script
-- characters as part of tokenization.
CREATE VIRTUAL TABLE txt3 USING fts4(tokenize=unicode61 "remove_diacritics=0");

remove_diacritics 选项可以设置为 "0"、"1" 或 "2"。默认值为 "1"。如果将其设置为 "1" 或 "2",则会如上所述从拉丁字母脚本字符中删除变音符号。但是,如果将其设置为 "1",则在使用单个 Unicode 代码点表示具有多个变音符号的字符的很少见情况下,不会删除变音符号。例如,不会从代码点 0x1ED9 ("LATIN SMALL LETTER O WITH CIRCUMFLEX AND DOT BELOW") 中删除变音符号。这在技术上是一个错误,但无法在不造成向后兼容性问题的情况下修复。如果将此选项设置为 "2",则会正确地从所有拉丁字母字符中删除变音符号。

还可以自定义 unicode61 视为分隔符字符的代码点集。 "separators=" 选项可用于指定一个或多个应视为分隔符字符的额外字符,而 "tokenchars=" 选项可用于指定一个或多个应视为标记的一部分而不是作为分隔符字符的额外字符。例如

-- Create a table that uses the unicode61 tokenizer, but considers "."
-- and "=" characters to be part of tokens, and capital "X" characters to
-- function as separators.
CREATE VIRTUAL TABLE txt3 USING fts4(tokenize=unicode61 "tokenchars=.=" "separators=X");

-- Create a table that considers space characters (codepoint 32) to be
-- a token character
CREATE VIRTUAL TABLE txt4 USING fts4(tokenize=unicode61 "tokenchars= ");

如果作为 "tokenchars=" 参数的一部分指定的字符默认情况下被认为是标记字符,则将其忽略。即使它已被早期 "separators=" 选项标记为分隔符,也是如此。类似地,如果作为 "separators=" 选项的一部分指定的字符默认情况下被视为分隔符字符,则将其忽略。如果指定了多个 "tokenchars=" 或 "separators=" 选项,则处理所有选项。例如

-- Create a table that uses the unicode61 tokenizer, but considers "."
-- and "=" characters to be part of tokens, and capital "X" characters to
-- function as separators. Both of the "tokenchars=" options are processed
-- The "separators=" option ignores the "." passed to it, as "." is by
-- default a separator character, even though it has been marked as a token
-- character by an earlier "tokenchars=" option.
CREATE VIRTUAL TABLE txt5 USING fts4(
    tokenize=unicode61 "tokenchars=." "separators=X." "tokenchars=="
);

传递给 "tokenchars=" 或 "separators=" 选项的参数区分大小写。在上面的示例中,指定 "X" 是分隔符字符不会影响 "x" 的处理方式。

8.1. 自定义(应用程序定义)分词器

除了提供内置的 "simple"、"porter" 和(可能)"icu" 和 "unicode61" 分词器之外,FTS 还提供了一个接口,用于应用程序实现和注册用 C 编写的自定义分词器。用于创建新分词器的接口在 fts3_tokenizer.h 源文件中定义和描述。

注册新的 FTS 分词器类似于在 SQLite 中注册新的虚拟表模块。用户传递一个指向结构的指针,该结构包含指向构成新分词器类型实现的各种回调函数的指针。对于分词器,结构(在 fts3_tokenizer.h 中定义)称为 "sqlite3_tokenizer_module"。

FTS 不会公开用户用来在数据库句柄中注册新分词器类型的 C 函数。相反,指针必须被编码为 SQL blob 值,并通过评估特殊标量函数 "fts3_tokenizer()" 通过 SQL 引擎传递给 FTS。fts3_tokenizer() 函数可以调用一个或两个参数,如下所示

SELECT fts3_tokenizer(<tokenizer-name>);
SELECT fts3_tokenizer(<tokenizer-name>, <sqlite3_tokenizer_module ptr>);

其中 <tokenizer-name> 是 参数,使用 sqlite3_bind_text() 将字符串绑定到该参数,其中字符串标识分词器,<sqlite3_tokenizer_module ptr> 是 参数,使用 sqlite3_bind_blob() 将 BLOB 绑定到该参数,其中 BLOB 的值是指向 sqlite3_tokenizer_module 结构的指针。如果存在第二个参数,则将其注册为分词器 <tokenizer-name> 并返回其副本。如果只传递了一个参数,则返回指向当前注册为 <tokenizer-name> 的分词器实现的指针,以 blob 形式编码。或者,如果不存在这样的分词器,则会引发 SQL 异常(错误)。

在 SQLite 版本 3.11.0 (2016-02-15) 之前,fts3_tokenizer() 的参数可以是字面字符串或 BLOB。它们不必是 绑定参数。但这可能导致 SQL 注入事件中的安全问题。因此,现在默认情况下禁用旧的遗留行为。但是,为了向后兼容真正需要它的应用程序,可以通过调用 sqlite3_db_config(db,SQLITE_DBCONFIG_ENABLE_FTS3_TOKENIZER,1,0) 来启用旧的遗留行为。

以下代码块包含从 C 代码中调用 fts3_tokenizer() 函数的示例

/*
** Register a tokenizer implementation with FTS3 or FTS4.
*/
int registerTokenizer(
  sqlite3 *db,
  char *zName,
  const sqlite3_tokenizer_module *p
){
  int rc;
  sqlite3_stmt *pStmt;
  const char *zSql = "SELECT fts3_tokenizer(?1, ?2)";

  rc = sqlite3_prepare_v2(db, zSql, -1, &pStmt, 0);
  if( rc!=SQLITE_OK ){
    return rc;
  }

  sqlite3_bind_text(pStmt, 1, zName, -1, SQLITE_STATIC);
  sqlite3_bind_blob(pStmt, 2, &p, sizeof(p), SQLITE_STATIC);
  sqlite3_step(pStmt);

  return sqlite3_finalize(pStmt);
}

/*
** Query FTS for the tokenizer implementation named zName.
*/
int queryTokenizer(
  sqlite3 *db,
  char *zName,
  const sqlite3_tokenizer_module **pp
){
  int rc;
  sqlite3_stmt *pStmt;
  const char *zSql = "SELECT fts3_tokenizer(?)";

  *pp = 0;
  rc = sqlite3_prepare_v2(db, zSql, -1, &pStmt, 0);
  if( rc!=SQLITE_OK ){
    return rc;
  }

  sqlite3_bind_text(pStmt, 1, zName, -1, SQLITE_STATIC);
  if( SQLITE_ROW==sqlite3_step(pStmt) ){
    if( sqlite3_column_type(pStmt, 0)==SQLITE_BLOB ){
      memcpy(pp, sqlite3_column_blob(pStmt, 0), sizeof(*pp));
    }
  }

  return sqlite3_finalize(pStmt);
}

8.2. 查询分词器

"fts3tokenize" 虚拟表可用于直接访问任何分词器。以下 SQL 演示了如何创建 fts3tokenize 虚拟表的实例

CREATE VIRTUAL TABLE tok1 USING fts3tokenize('porter');

当然,应在示例中用所需分词器的名称替换 'porter'。如果分词器需要一个或多个参数,则应在 fts3tokenize 声明中用逗号分隔它们(即使它们在常规 fts4 表的声明中用空格分隔)。以下内容创建使用相同分词器的 fts4 和 fts3tokenize 表

CREATE VIRTUAL TABLE text1 USING fts4(tokenize=icu en_AU);
CREATE VIRTUAL TABLE tokens1 USING fts3tokenize(icu, en_AU);

CREATE VIRTUAL TABLE text2 USING fts4(tokenize=unicode61 "tokenchars=@." "separators=123");
CREATE VIRTUAL TABLE tokens2 USING fts3tokenize(unicode61, "tokenchars=@.", "separators=123");

创建虚拟表后,可以按如下方式查询它

SELECT token, start, end, position
  FROM tok1
 WHERE input='This is a test sentence.';

虚拟表将为输入字符串中的每个标记返回一行输出。 "token" 列是标记的文本。 "start" 和 "end" 列是原始输入字符串中标记的开始和结束的字节偏移量。 "position" 列是原始输入字符串中标记的序号。还有一个 "input" 列,它只是 WHERE 子句中指定的输入字符串的副本。请注意,WHERE 子句中必须出现形如 "input=?" 的约束,否则虚拟表将没有要标记的输入,并且不会返回任何行。上面的示例生成以下输出

thi|0|4|0
is|5|7|1
a|8|9|2
test|10|14|3
sentenc|15|23|4

注意,fts3tokenize 虚拟表的结果集中的标记已根据分词器的规则进行转换。由于此示例使用了 "porter" 分词器,因此 "This" 标记被转换为 "thi"。如果需要标记的原始文本,可以使用 "start" 和 "end" 列以及 substr() 函数来检索它。例如

SELECT substr(input, start+1, end-start), token, position
  FROM tok1
 WHERE input='This is a test sentence.';

fts3tokenize 虚拟表可以用于任何分词器,无论是否存在实际使用该分词器的 FTS3 或 FTS4 表。

9. 数据结构

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

9.1. 影子表

对于数据库中的每个 FTS 虚拟表,将创建三个到五个真实(非虚拟)表来存储底层数据。这些真实表称为 "影子表"。真实表的名称为 "%_content"、"%_segdir"、"%_segments"、"%_stat" 和 "%_docsize",其中 "%" 被 FTS 虚拟表的名称替换。

"%_content" 表的最左侧列是名为 "docid" 的 INTEGER PRIMARY KEY 字段。在其之后是 FTS 虚拟表中每列的一列,由用户声明,其名称是在用户提供的列名前加上 "cN",其中 N 是表中列的索引,从左到右编号,从 0 开始。在 %_content 表声明中不使用作为虚拟表声明的一部分提供的数据类型。例如

-- Virtual table declaration
CREATE VIRTUAL TABLE abc USING fts4(a NUMBER, b TEXT, c);

-- Corresponding %_content table declaration
CREATE TABLE abc_content(docid INTEGER PRIMARY KEY, c0a, c1b, c2c);

"%_content" 表包含用户通过用户插入到 FTS 虚拟表中的未经修改的数据。如果用户在插入记录时没有显式提供 "docid" 值,则系统会自动选择一个值。

"%_stat" 和 "%_docsize" 表只有在 FTS 表使用 FTS4 模块而不是 FTS3 时才会创建。此外,如果 FTS4 表是在指定了 "matchinfo=fts3" 指令作为 CREATE VIRTUAL TABLE 语句的一部分创建的情况下,则会省略 %_docsize 表。如果创建了它们,则两个表的模式如下

CREATE TABLE %_stat(
  id INTEGER PRIMARY KEY,
  value BLOB
);

CREATE TABLE %_docsize(
  docid INTEGER PRIMARY KEY,
  size BLOB
);

对于 FTS 表中的每一行,"%_docsize" 表都包含一个具有相同 "docid" 值的相应行。 "size" 字段包含一个由 N 个 FTS varint 组成的 blob,其中 N 是表中用户定义列的数量。 "size" blob 中的每个 varint 是 FTS 表中关联行的对应列中的标记数量。"%_stat" 表始终包含一行,其 "id" 列设置为 0。 "value" 列包含一个由 N+1 个 FTS varint 组成的 blob,其中 N 再次是 FTS 表中用户定义列的数量。blob 中的第一个 varint 设置为 FTS 表中的总行数。第二个和后续 varint 包含存储在 FTS 表所有行对应列中的标记总数。

剩下的两个表,"%_segments" 和 "%_segdir",用于存储全文索引。从概念上讲,此索引是一个查找表,它将每个术语(单词)映射到与包含一个或多个术语出现的 "%_content" 表中的记录相对应的 docid 值集。为了检索包含指定术语的所有文档,FTS 模块查询此索引以确定包含该术语的记录的 docid 值集,然后从 "%_content" 表中检索所需的文档。无论 FTS 虚拟表的模式如何,始终按如下方式创建 "%_segments" 和 "%_segdir" 表

CREATE TABLE %_segments(
  blockid INTEGER PRIMARY KEY,       -- B-tree node id
  block blob                         -- B-tree node data
);

CREATE TABLE %_segdir(
  level INTEGER,
  idx INTEGER,
  start_block INTEGER,               -- Blockid of first node in %_segments
  leaves_end_block INTEGER,          -- Blockid of last leaf node in %_segments
  end_block INTEGER,                 -- Blockid of last node in %_segments
  root BLOB,                         -- B-tree root node
  PRIMARY KEY(level, idx)
);

上面描述的模式并非旨在直接存储全文索引。相反,它用于存储一个或多个 b 树结构。"%_segdir" 表中的每一行都有一个 b 树。"%_segdir" 表行包含 b 树结构的根节点和各种元数据,"%_segments" 表包含所有其他(非根)b 树节点。每个 b 树称为一个 "段"。一旦创建,段 b 树就永远不会更新(尽管它可能被完全删除)。

每个段 b 树使用的键是术语(单词)。除了键之外,每个段 b 树条目还关联一个 "doclist"(文档列表)。doclist 由零个或多个条目组成,其中每个条目包含

  • 一个 docid(文档 ID),以及
  • 一个术语偏移量列表,每个条目对应于文档中术语的一次出现。术语偏移量指示在问题术语之前出现的标记(单词)的数量,而不是字符或字节的数量。例如,短语 "Ancestral voices prophesying war!" 中术语 "war" 的术语偏移量为 3。

文档列表中的条目按 docid 排序。文档列表条目内的位置按升序存储。

逻辑全文索引的内容是通过合并所有段 B 树的内容获得的。如果一个词项存在于多个段 B 树中,那么它将映射到每个单独的文档列表的并集。如果对于单个词项,同一个 docid 在多个文档列表中出现,那么只有属于最近创建的段 B 树的文档列表被认为是有效的。

使用多个 B 树结构而不是单个 B 树可以降低将记录插入 FTS 表的成本。当一个新记录插入到已经包含大量数据的 FTS 表中时,新记录中的许多词项很可能已经存在于大量现有记录中。如果使用单个 B 树,则必须从数据库中加载大型文档列表结构,修改这些结构以包含新的 docid 和词项偏移列表,然后将它们写回数据库。使用多个 B 树表可以避免这种情况,方法是创建一个新的 B 树,它可以稍后与现有的 B 树(或 B 树)合并。B 树结构的合并可以作为后台任务执行,或者在累积一定数量的独立 B 树结构后执行。当然,这种方案会使查询更昂贵(因为 FTS 代码可能需要在多个 B 树中查找单个词项并合并结果),但实际上发现,这种开销通常可以忽略不计。

9.2. 可变长度整数 (varint) 格式

存储为段 B 树节点一部分的整数值使用 FTS varint 格式编码。这种编码类似于,但不完全相同SQLite varint 格式

编码后的 FTS varint 占用 1 到 10 个字节的空间。所需的字节数由编码的整数值的符号和大小决定。更准确地说,用于存储编码整数的字节数取决于整数在 64 位二进制补码表示中的最高有效位集的位置。负值始终将最高有效位设为 1(符号位),因此始终使用完整的 10 个字节进行存储。正整数值可以使用更小的空间进行存储。

编码后的 FTS varint 的最后一个字节的最高有效位被清零。所有前导字节的最高有效位都被设为 1。数据存储在每个字节的剩余 7 个最低有效位中。编码表示形式的第一个字节包含编码整数值的 7 个最低有效位。编码表示形式的第二个字节(如果存在)包含整数值的下一个 7 个最低有效位,依此类推。下表包含编码整数值的示例

十进制十六进制编码表示
430x000000000000002B0x2B
2008150x000000000003106F0xEF 0xA0 0x0C
-10xFFFFFFFFFFFFFFFF0xFF 0xFF 0xFF 0xFF 0xFF 0xFF 0xFF 0xFF 0xFF 0x01

9.3. 段 B 树格式

段 B 树是前缀压缩的 B+ 树。%_segdir 表中每一行都有一个段 B 树(见上文)。段 B 树的根节点存储为 %_segdir 表对应行中 "root" 字段的 Blob。所有其他节点(如果存在)都存储在 %_segments 表的 "blob" 列中。%_segments 表中的节点通过对应行中 blockid 字段的整数值来标识。下表描述了 %_segdir 表的字段

解释
level"level" 和 "idx" 字段的内容共同定义了段 B 树的相对年龄。"level" 字段中存储的值越小,段 B 树创建的时间就越晚。如果两个段 B 树的 "level" 相同,则 "idx" 列中存储的值更大的段更晚。%_segdir 表上的 PRIMARY KEY 约束防止任何两个段在 "level" 和 "idx" 字段中都具有相同的值。
idx见上文。
start_block对应于属于此段 B 树的最小 blockid 节点的 blockid。如果整个段 B 树都适合根节点,则为零。如果存在,此节点始终是叶节点。
leaves_end_block对应于属于此段 B 树的具有最大 blockid 的叶节点的 blockid。如果整个段 B 树都适合根节点,则为零。
end_block此字段可以包含一个整数,也可以包含一个由两个用空格字符(Unicode 代码点 0x20)分隔的整数组成的文本字段。

第一个或唯一的整数是对应于属于此段 B 树的具有最大 blockid 的内部节点的 blockid。如果整个段 B 树都适合根节点,则为零。如果存在,此节点始终是内部节点。

第二个整数(如果存在)是存储在叶页面上的所有数据的总大小(以字节为单位)。如果该值为负数,则该段是未完成的增量合并操作的输出,而绝对值为当前大小(以字节为单位)。

root包含段 B 树根节点的 Blob。

除了根节点外,构成单个段 B 树的节点总是使用连续的 blockid 序列进行存储。此外,构成 B 树单个级别的节点本身也作为连续块存储,按 B 树顺序排列。用于存储 B 树叶的连续 blockid 序列从对应 %_segdir 行的 "start_block" 列中存储的 blockid 值开始分配,并在同一行的 "leaves_end_block" 字段中存储的 blockid 值处结束。因此,可以通过从 "start_block" 到 "leaves_end_block" 按 blockid 顺序遍历 %_segments 表来按键顺序遍历段 B 树的所有叶子。

9.3.1. 段 B 树叶节点

下图描述了段 B 树叶节点的格式。

段 B 树叶节点格式

存储在每个节点上的第一个词项(上图中的 "Term 1")按原样存储。每个后续词项相对于其前一个词项进行前缀压缩。词项按排序顺序(memcmp)存储在页面中。

9.3.2. 段 B 树内部节点

下图描述了段 B 树内部(非叶)节点的格式。

段 B 树内部节点格式

9.4. 文档列表格式

文档列表由一系列 64 位有符号整数组成,使用 FTS varint 格式序列化。每个文档列表条目由一系列两个或多个整数组成,如下所示

  1. docid 值。文档列表中的第一个条目包含文字 docid 值。每个后续文档列表条目的第一个字段包含新 docid 与前一个 docid 之间的差值(始终为正数)。
  2. 零个或多个词项偏移列表。FTS 虚拟表中包含该词项的每一列都包含一个词项偏移列表。词项偏移列表包含以下内容
    1. 常量值 1。对于与列 0 关联的任何词项偏移列表,此字段将被省略。
    2. 列号(第二左边的列为 1,依此类推)。对于与列 0 关联的任何词项偏移列表,此字段将被省略。
    3. 词项偏移列表,从最小到最大排序。不是按原样存储词项偏移值,而是存储的每个整数都是当前词项偏移与前一个词项偏移之间的差值(如果当前词项偏移是第一个,则为零)加上 2。
  3. 常量值 0。

FTS3 文档列表格式

FTS 文档列表条目格式

对于词项出现在 FTS 虚拟表中多个列的文档列表,文档列表中的词项偏移列表按列号顺序存储。这确保了与列 0 关联的词项偏移列表(如果有)始终是第一个,从而允许在这种情况下省略词项偏移列表的前两个字段。

10. 限制

10.1. UTF-16 字节顺序标记问题

对于 UTF-16 数据库,当使用 "simple" 标记器时,可以使用格式错误的 Unicode 字符串来导致 完整性检查特殊命令 错误地报告损坏,或导致 辅助函数 返回错误的结果。更具体地说,该错误可以通过以下任何一种方式触发
  • UTF-16 字节顺序标记 (BOM) 嵌入到插入到 FTS3 表中的 SQL 字符串文字值的开头。例如

    INSERT INTO fts_table(col) VALUES(char(0xfeff)||'text...');
    
  • SQLite 转换为 UTF-16 字节顺序标记的格式错误的 UTF-8 嵌入到插入到 FTS3 表中的 SQL 字符串文字值的开头。

  • 通过将以两个字节 0xFF 和 0xFE 开头的 Blob 转换为文本值创建的文本值(以任何可能的顺序)插入到 FTS3 表中。例如

    INSERT INTO fts_table(col) VALUES(CAST(X'FEFF' AS TEXT));
    
如果以下任何条件为真,则一切正常
  • 数据库编码 为 UTF-8。
  • 所有文本字符串都使用 sqlite3_bind_text() 函数系列中的一个进行插入。
  • 文字字符串不包含字节顺序标记。
  • 使用的标记器将字节顺序标记识别为空白符。(FTS3/4 的默认 "simple" 标记器不认为 BOM 是空白符,但 Unicode 标记器认为是空白符。)
为了出现问题,上述所有条件必须为假。即使上述所有条件都为假,大多数内容仍将正常运行。只有 完整性检查 命令和 辅助函数 可能会给出意外结果。

附录 A: 搜索应用程序提示

FTS 主要旨在支持布尔全文查询 - 查询以查找与指定条件匹配的文档集。但是,许多(大多数?)搜索应用程序要求以某种方式对结果进行排名,以便按照 "相关性" 的顺序排列结果,其中 "相关性" 被定义为执行搜索的用户对返回的文档集中特定元素感兴趣的可能性。当使用搜索引擎查找万维网上的文档时,用户希望返回的第一页结果是最有用或 "最相关" 的文档,并且每个后续页面包含逐渐不太相关的结果。机器如何根据用户的查询确定文档相关性是一个复杂的问题,也是许多正在进行的研究的主题。

一种非常简单的方案可能是统计用户搜索词项在每个结果文档中出现的次数。包含大量词项实例的文档被认为比包含少量词项实例的文档更相关。在 FTS 应用程序中,可以通过统计 offsets 函数返回值中的整数数量来确定每个结果中词项实例的数量。以下示例显示了一个查询,该查询可用于获取用户输入查询的十个最相关结果

-- This example (and all others in this section) assumes the following schema
CREATE VIRTUAL TABLE documents USING fts3(title, content);

-- Assuming the application has supplied an SQLite user function named "countintegers"
-- that returns the number of space-separated integers contained in its only argument,
-- the following query could be used to return the titles of the 10 documents that contain
-- the greatest number of instances of the users query terms. Hopefully, these 10
-- documents will be those that the users considers more or less the most "relevant".
SELECT title FROM documents
  WHERE documents MATCH <query>
  ORDER BY countintegers(offsets(documents)) DESC
  LIMIT 10 OFFSET 0

上面查询可以通过使用 FTS matchinfo 函数来确定每个结果中出现的查询词实例的数量,从而实现更快的执行速度。matchinfo 函数比 offsets 函数效率高得多。此外,matchinfo 函数还提供有关每个查询词在整个文档集中(不仅是当前行)的总出现次数和每个查询词出现的文档数量的额外信息。这可以用来(例如)为不太常见的术语赋予更高的权重,从而提高用户认为更有趣的那些结果的整体计算相关性。

-- If the application supplies an SQLite user function called "rank" that
-- interprets the blob of data returned by matchinfo and returns a numeric
-- relevancy based on it, then the following SQL may be used to return the
-- titles of the 10 most relevant documents in the dataset for a users query.
SELECT title FROM documents
  WHERE documents MATCH <query>
  ORDER BY rank(matchinfo(documents)) DESC
  LIMIT 10 OFFSET 0

上面示例中的 SQL 查询使用的 CPU 比本节中的第一个示例少,但仍然存在一个不明显的性能问题。SQLite 通过检索与用户查询匹配的每一行的 "title" 列的值和来自 FTS 模块的 matchinfo 数据来满足此查询,然后对其进行排序和限制结果。由于 SQLite 的虚拟表接口的工作方式,检索 "title" 列的值需要从磁盘加载整个行(包括 "content" 字段,它可能非常大)。这意味着,如果用户查询匹配数千个文档,那么即使永远不会使用,也可能从磁盘加载数兆字节的 "title" 和 "content" 数据到内存中。

以下示例块中的 SQL 查询是解决此问题的一个方案。在 SQLite 中,当 用于联接的子查询包含 LIMIT 子句 时,会计算子查询的结果并将其存储在临时表中,然后再执行主查询。这意味着 SQLite 将只将匹配用户查询的每一行的 docid 和 matchinfo 数据加载到内存中,确定与十个最相关文档对应的 docid 值,然后只加载这 10 个文档的标题和内容信息。由于 matchinfo 和 docid 值都完全来自全文索引,因此这会导致从数据库加载到内存中的数据量大大减少。

SELECT title FROM documents JOIN (
    SELECT docid, rank(matchinfo(documents)) AS rank
    FROM documents
    WHERE documents MATCH <query>
    ORDER BY rank DESC
    LIMIT 10 OFFSET 0
) AS ranktable USING(docid)
ORDER BY ranktable.rank DESC

下一块 SQL 代码使用解决方案来解决使用 FTS 开发搜索应用程序时可能出现的另外两个问题。

  1. 不能将 snippet 函数与上面的查询一起使用。因为外部查询不包含 "WHERE ... MATCH" 子句,所以不能将 snippet 函数与它一起使用。一种解决方案是在外部查询中复制子查询使用的 WHERE 子句。与之相关的开销通常可以忽略不计。

  2. 文档的相关性可能取决于除 matchinfo 返回值中提供的数据之外的其他因素。例如,数据库中的每个文档都可以根据与其内容无关的因素(来源、作者、年龄、引用数量等)分配一个静态权重。这些值可以由应用程序存储在一个单独的表中,该表可以在子查询中与文档表联接,以便 rank 函数可以访问它们。

此版本的查询与 sqlite.org 文档搜索 应用程序使用的查询非常相似。

-- This table stores the static weight assigned to each document in FTS table
-- "documents". For each row in the documents table there is a corresponding row
-- with the same docid value in this table.
CREATE TABLE documents_data(docid INTEGER PRIMARY KEY, weight);

-- This query is similar to the one in the block above, except that:
--
--   1. It returns a "snippet" of text along with the document title for display. So
--      that the snippet function may be used, the "WHERE ... MATCH ..." clause from
--      the sub-query is duplicated in the outer query.
--
--   2. The sub-query joins the documents table with the document_data table, so that
--      implementation of the rank function has access to the static weight assigned
--      to each document.
SELECT title, snippet(documents) FROM documents JOIN (
    SELECT docid, rank(matchinfo(documents), documents_data.weight) AS rank
    FROM documents JOIN documents_data USING(docid)
    WHERE documents MATCH <query>
    ORDER BY rank DESC
    LIMIT 10 OFFSET 0
) AS ranktable USING(docid)
WHERE documents MATCH <query>
ORDER BY ranktable.rank DESC

上面所有示例查询都返回十个最相关的查询结果。通过修改 OFFSET 和 LIMIT 子句中使用的值,可以轻松构建一个查询来返回(例如)接下来的十个最相关的结果。这可以用来获取搜索应用程序第二页及后续页结果所需的数据。

下一块包含一个使用 C 语言实现的 matchinfo 数据的示例 rank 函数。它允许为每个文档的每一列外部分配一个权重,而不是单个权重。它可以使用 sqlite3_create_function 与 SQLite 注册,就像任何其他用户函数一样。

安全警告:由于它只是一个普通的 SQL 函数,因此 rank() 可以在任何上下文中作为任何 SQL 查询的一部分被调用。这意味着传递的第一个参数可能不是有效的 matchinfo blob。实现者应该注意处理这种情况,而不会导致缓冲区溢出或其他潜在的安全问题。

/*
** SQLite user defined function to use with matchinfo() to calculate the
** relevancy of an FTS match. The value returned is the relevancy score
** (a real value greater than or equal to zero). A larger value indicates 
** a more relevant document.
**
** The overall relevancy returned is the sum of the relevancies of each 
** column value in the FTS table. The relevancy of a column value is the
** sum of the following for each reportable phrase in the FTS query:
**
**   (<hit count> / <global hit count>) * <column weight>
**
** where <hit count> is the number of instances of the phrase in the
** column value of the current row and <global hit count> is the number
** of instances of the phrase in the same column of all rows in the FTS
** table. The <column weight> is a weighting factor assigned to each
** column by the caller (see below).
**
** The first argument to this function must be the return value of the FTS 
** matchinfo() function. Following this must be one argument for each column 
** of the FTS table containing a numeric weight factor for the corresponding 
** column. Example:
**
**     CREATE VIRTUAL TABLE documents USING fts3(title, content)
**
** The following query returns the docids of documents that match the full-text
** query <query> sorted from most to least relevant. When calculating
** relevance, query term instances in the 'title' column are given twice the
** weighting of those in the 'content' column.
**
**     SELECT docid FROM documents 
**     WHERE documents MATCH <query> 
**     ORDER BY rank(matchinfo(documents), 1.0, 0.5) DESC
*/
static void rankfunc(sqlite3_context *pCtx, int nVal, sqlite3_value **apVal){
  int *aMatchinfo;                /* Return value of matchinfo() */
  int nMatchinfo;                 /* Number of elements in aMatchinfo[] */
  int nCol = 0;                   /* Number of columns in the table */
  int nPhrase = 0;                /* Number of phrases in the query */
  int iPhrase;                    /* Current phrase */
  double score = 0.0;             /* Value to return */

  assert( sizeof(int)==4 );

  /* Check that the number of arguments passed to this function is correct.
  ** If not, jump to wrong_number_args. Set aMatchinfo to point to the array
  ** of unsigned integer values returned by FTS function matchinfo. Set
  ** nPhrase to contain the number of reportable phrases in the users full-text
  ** query, and nCol to the number of columns in the table. Then check that the
  ** size of the matchinfo blob is as expected. Return an error if it is not.
  */
  if( nVal<1 ) goto wrong_number_args;
  aMatchinfo = (unsigned int *)sqlite3_value_blob(apVal[0]);
  nMatchinfo = sqlite3_value_bytes(apVal[0]) / sizeof(int);
  if( nMatchinfo>=2 ){
    nPhrase = aMatchinfo[0];
    nCol = aMatchinfo[1];
  }
  if( nMatchinfo!=(2+3*nCol*nPhrase) ){
    sqlite3_result_error(pCtx,
      "invalid matchinfo blob passed to function rank()", -1);
    return;
  }
  if( nVal!=(1+nCol) ) goto wrong_number_args;

  /* Iterate through each phrase in the users query. */
  for(iPhrase=0; iPhrase<nPhrase; iPhrase++){
    int iCol;                     /* Current column */

    /* Now iterate through each column in the users query. For each column,
    ** increment the relevancy score by:
    **
    **   (<hit count> / <global hit count>) * <column weight>
    **
    ** aPhraseinfo[] points to the start of the data for phrase iPhrase. So
    ** the hit count and global hit counts for each column are found in 
    ** aPhraseinfo[iCol*3] and aPhraseinfo[iCol*3+1], respectively.
    */
    int *aPhraseinfo = &aMatchinfo[2 + iPhrase*nCol*3];
    for(iCol=0; iCol<nCol; iCol++){
      int nHitCount = aPhraseinfo[3*iCol];
      int nGlobalHitCount = aPhraseinfo[3*iCol+1];
      double weight = sqlite3_value_double(apVal[iCol+1]);
      if( nHitCount>0 ){
        score += ((double)nHitCount / (double)nGlobalHitCount) * weight;
      }
    }
  }

  sqlite3_result_double(pCtx, score);
  return;

  /* Jump here if the wrong number of arguments are passed to this function */
wrong_number_args:
  sqlite3_result_error(pCtx, "wrong number of arguments to function rank()", -1);
}

此页面最后修改时间为 2023-10-10 17:29:48 UTC