首页
学习
活动
专区
工具
TVP
发布
精选内容/技术社群/优惠产品,尽在小程序
立即前往

SQLite FTS3 and FTS4 Extensions

1.介绍FTS3和FTS4

1.1.FTS3和FTS4之间的差异

1.2.创建和销毁FTS表

1.3.填充FTS表

1.4.简单的FTS查询

1.5.概要

2.编译并启用FTS3和FTS4

3.全文索引查询

3.1.使用增强查询语法设置操作

3.2.使用标准查询语法设置操作

4.辅助功能 - 片段,偏移和匹配信息

4.1.偏移函数

4.2.片段功能

4.3.Matchinfo函数

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

6. FTS4选项

6.1.压缩文件=和非压缩文件=选项

6.2.内容=选项

6.2.1.无内容FTS4表

6.2.2.外部内容FTS4表

6.3.languageid =选项

6.4.matchinfo= 选项

6.5.notindexed= 选项

6.6.前缀=选项

7. FTS3和FTS4的特殊命令

7.1.“优化”命令

7.2.“重建”命令

7.3.“完整性检查”命令

7.4.“merge = X,Y”命令

7.5.“automerge = N”命令

8. Tokenizers

8.1.自定义(应用程序定义)标志符

8.2.查询Tokenizers

9.数据结构

9.1.影子表

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

9.3.段B树格式

9.3.1.段B树叶节点

9.3.2.段B树内部节点

9.4.文件格式

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

概观

FTS3和FTS4是SQLite虚拟表格模块,允许用户对一组文档执行全文搜索。描述全文搜索的最常见(也是最有效的)方式是“Google,Yahoo和Bing在万维网上放置文档时所做的事情”。用户输入一个术语,或一系列条款,由二元运算也许连接或组合成一个短语,和全文查询系统找到一套文件最符合考虑到运营商的条款和集团用户已经指定。本文介绍了FTS3和FTS4的部署和使用。

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

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

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

代码语言:javascript
复制
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。

代码语言:javascript
复制
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包含查询性能优化,可以显着提高全文查询的性能,这些查询包含很常见的条目(存在于大部分表行中)。
  • FTS4支持一些可用于matchinfo()函数的附加选项。
  • 由于它在磁盘上将额外信息存储在两个新的影子表中,以支持性能优化和额外的matchinfo()选项,FTS4表可能会占用比使用FTS3创建的等效表更多的磁盘空间。通常,开销为1-2%或更少,但如果存储在FTS表中的文档非常小,则开销可能高达10%。通过将指令“matchinfo = fts3”指定为FTS4表声明的一部分,可以降低开销,但这会牺牲一些额外支持的matchinfo()选项。
  • FTS4提供钩子(压缩和解压缩选项),允许数据以压缩格式存储,从而减少磁盘使用量和IO。

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列名称一起指定的约束 - 它们被解析但未被系统以任何方式使用或记录。

代码语言:javascript
复制
-- 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 = <tokenizer name> <tokenizer args>”的字符串来替代列名称来完成的,其中<tokenizer name>是要使用的标记器名称,<tokenizer args>是可选的要传递给标记器实现的空白分隔限定符列表。标记器规范可以放在列列表的任何位置,但每个CREATE VIRTUAL TABLE语句最多允许一个标记器声明。请参阅下面的关于使用(以及必要时实现)分词器的详细说明。

代码语言:javascript
复制
-- 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表。例如:

代码语言:javascript
复制
-- Create, then immediately drop, an FTS4 table.
CREATE VIRTUAL TABLE data USING fts4();
DROP TABLE data;

1.3.填充FTS表

使用INSERT,UPDATE和DELETE语句填充FTS表格的方式与普通SQLite表格相同。

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

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

代码语言:javascript
复制
-- 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-tree,但是随着插入,更新和删除行而增量合并的几个不同b-树。此技术在写入FTS表时会提高性能,但会对使用索引的全文查询造成一定的开销。评估特殊的“优化”命令,形式为“INSERT INTO <fts-table>(<fts-table>)VALUES('optimize')”的SQL语句,使FTS将所有现有的索引b-树合并成一个包含整个索引的大型b-树。这可能是一项昂贵的操作,但可能会加速未来的查询。

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

代码语言:javascript
复制
-- 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表:

  • 按rowid查询。如果SELECT语句的WHERE子句包含“rowid =?”形式的子条款,其中?是一个SQL表达式,FTS能够使用SQLite INTEGER PRIMARY KEY索引的等价物直接检索请求的行。
  • 全文查询。如果SELECT语句的WHERE子句包含“<column> MATCH?”形式的子条款,则FTS可以使用内置的全文索引将搜索限制为与全文匹配的文档查询字符串指定为MATCH子句的右侧操作数。

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

代码语言:javascript
复制
-- 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运算符的右侧操作数是一个由单个项组成的字符串。在这种情况下,对于包含指定词的一个或多个实例(“sqlite”,“search”或“database”,取决于您查看哪个示例)的所有文档,MATCH表达式的计算结果为true。将单个术语指定为MATCH运算符的右侧操作数可以产生最简单和最常见的全文查询。然而,更复杂的查询是可能的,包括短语搜索,术语前缀搜索和搜索包含在彼此的定义邻近度内发生的术语的组合的文档。下面描述可以查询全文索引的各种方式。

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

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

代码语言:javascript
复制
-- 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表实际上都有一个HIDDEN列,其名称与表本身相同(在本例中为“mail”)。存储在此列中的值对应用程序无意义,但可用作MATCH运算符的左侧操作数。此特殊列也可以作为参数传递给FTS辅助功能。

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

代码语言:javascript
复制
-- 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表中进行修改和删除,就像使用普通表一样。同样,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(),offset()和matchinfo()可用于支持全文查询。
  6. 每个FTS表都有一个与表本身具有相同名称的隐藏列。隐藏列的每行中包含的值是一个blob,它只用作MATCH运算符的左操作数,或者作为其中一个FTS辅助函数的最左边参数。

尽管FTS3和FTS4包含在SQLite核心源代码中,但它们并未默认启用。为了构建启用了FTS功能的SQLite,在编译时定义预处理器宏SQLITE_ENABLE_FTS3。新的应用程序还应定义SQLITE_ENABLE_FTS3_PARENTHESIS宏以启用增强的查询语法(请参见下文)。通常,这是通过将以下两个开关添加到编译器命令行来完成的:

代码语言:javascript
复制
-DSQLITE_ENABLE_FTS3
-DSQLITE_ENABLE_FTS3_PARENTHESIS

请注意,启用FTS3还可使FTS4可用。没有单独的SQLITE_ENABLE_FTS4编译时选项。SQLite的构建既可以支持FTS3,也可以支持FTS4,或者它们都不支持。

如果使用基于合并autoconf的构建系统,则在运行'configure'脚本时设置CPPFLAGS环境变量是设置这些宏的简单方法。例如,以下命令:

代码语言:javascript
复制
CPPFLAGS="-DSQLITE_ENABLE_FTS3 -DSQLITE_ENABLE_FTS3_PARENTHESIS" ./configure <configure options>

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

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

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

如果ICU库的C版本可用,则也可以使用定义的SQLITE_ENABLE_ICU预处理器宏编译FTS。使用该宏进行编译可以使用ICU库的FTS标记器使用指定语言和区域设置的约定将文档分割为词(词)。

代码语言:javascript
复制
-DSQLITE_ENABLE_ICU

关于FTS表格最有用的是可以使用内置全文索引执行的查询。作为从FTS表读取数据的SELECT语句的WHERE子句的一部分,指定“<column> MATCH <全文查询表达式>”形式的子句来执行全文查询。上面描述了返回包含给定术语的所有文档的简单FTS查询。在该讨论中,MATCH运算符的右侧操作数被假定为由单个项组成的字符串。本节介绍FTS表支持的更复杂的查询类型,以及如何通过将更复杂的查询表达式指定为MATCH运算符的右侧操作数来利用它们。

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

  • 令牌或令牌前缀查询。可以查询包含指定词语(上述简单情况)的所有文档或者包含具有指定前缀的词语的所有文档的FTS表格。正如我们所看到的,特定术语的查询表达式只是该术语本身。用于搜索术语前缀的查询表达式是附加了“*”字符的前缀本身。例如:
代码语言:javascript
复制
-- 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*';
  • 通常,令牌或令牌前缀查询与指定为MATCH运算符左侧的FTS表列进行匹配。或者,如果指定了与FTS表本身同名的特殊列,则针对所有列。这可以通过在基本术语查询之前指定一个列名后跟一个“:”字符来覆盖。“:”与要查询的术语之间可能有空格,但列名与“:”字符之间不能有空格。例如:
代码语言:javascript
复制
-- 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';
  • 如果FTS表是FTS4表(不是FTS3),则令牌也可以前缀为“^”字符。在这种情况下,为了匹配令牌,必须在匹配行的任何列中显示为第一个令牌。例子:
代码语言:javascript
复制
-- 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*';
  • 短语查询。短语查询是一种查询,它以指定的顺序检索包含指定的一组术语或术语前缀的所有文档,而不包含间插令牌。短语查询通过用双引号(“)括起空格分隔的术语序列或术语前缀来指定,例如:
代码语言:javascript
复制
-- 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*"';
  • NEAR查询。NEAR查询是一个查询,该查询返回包含两个或更多指定条件或短语的文档,该文档包含彼此指定的邻近区域(默认情况下具有10个或更少的中介术语)。通过在两个短语,标记或标记前缀查询之间放置关键字“NEAR”来指定NEAR查询。要指定除默认值以外的邻近度,可以使用“NEAR / <N> ” 格式的运算符,其中<N>是允许的最大间隔条件数。例如:
代码语言:javascript
复制
-- 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';
  • 一个查询中可能会出现多个NEAR运算符。在这种情况下,由NEAR运算符分隔的每对术语或短语必须出现在文档中彼此指定的邻近范围内。使用与以上示例块中相同的表格和数据:
代码语言:javascript
复制
-- 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查询表达式语言,可以对基本查询的结果执行各种集合操作。目前有三种支持的操作:

  • AND运算符确定两组文档的交集
  • OR运算符计算两组文档的联合
  • NOT运算符(或者,如果使用标准语法,是一个“ - ”运算符)可用于计算一组文档相对于另一组文档的相对补充

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

3.1.使用增强查询语法设置操作

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

AND运算符可以隐式指定。如果出现两个基本查询,且没有运算符将它们分隔到FTS查询字符串中,则结果与两个基本查询由AND运算符分隔的结果相同。例如,查询表达式“隐式运算符”是“隐式AND运算符”的更简洁的版本。

代码语言:javascript
复制
-- 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查询中存在多个设置操作,则运算符的优先级如下所示:

操作者

增强的查询语法优先级

最高优先级(最紧密的分组)。

要么

最低优先级(最宽松的分组)。

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

代码语言:javascript
复制
-- 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”作为标准查询语法查询的一部分被解释为包含术语“和”的一组文档的术语查询。
  2. 括号不支持。
  3. NOT运算符不受支持。标准查询语法不支持NOT运算符,而是支持可应用于基本术语和术语前缀查询(但不适用于短语或NEAR查询)的一元“ - ”运算符。带有一个“ - ”运算符的术语或术语前缀可能不会作为OR运算符的操作数出现。FTS查询可能不完全由术语或术语前缀查询组成,并附有一元“ - ”运算符。
代码语言:javascript
复制
-- 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”。使用标准查询语法时,运算符的优先级为:

操作者

标准查询语法优先

一元“ - ”

最高优先级(最紧密的分组)。

最低优先级(最宽松的分组)。

  1. 以下示例说明了使用标准查询语法的运算符的优先级:
代码语言:javascript
复制
-- 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';

FTS3和FTS4模块提供了三种特殊的SQL标量函数,它们可能对全文查询系统的开发者有用:“片段”,“偏移量”和“匹配信息”。“片段”和“偏移”功能的目的是允许用户在返回的文档中识别查询条款的位置。“匹配信息”功能为用户提供了可用于根据相关性对查询结果进行筛选或排序的指标。

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

代码语言:javascript
复制
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语句中有用。如果在使用“通过rowid查询”或“线性扫描”策略的SELECT中使用,则代码片段和偏移量都会返回空字符串,并且matchinfo函数返回大小为零的字节值。

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

通过以下附带条件,FTS表中与查询表达式中的一个可匹配短语相匹配的每个标记系列被称为“短语匹配”:

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

4.1.偏移函数

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

整数

解释

0

术语实例出现的列号(0表示FTS表的最左边一列,另一个表示最左边的一列,等等)。

1

全文查询表达式中匹配项的数字。查询表达式中的术语按其出现的顺序从0开始编号。

2

列中匹配项的字节偏移量。

3

匹配项的大小(以字节为单位)。

以下块包含使用偏移量函数的示例。

代码语言:javascript
复制
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.片段功能

片段功能用于创建文档文本的格式化片段,作为全文查询结果报告的一部分进行显示。片段函数可以在一个和六个参数之间传递,如下所示:

论据

默认值

描述

0

N / A

代码片段函数的第一个参数必须始终是被查询的FTS表格的FTS隐藏列,并且代码片段将从中被采用。FTS隐藏列是与FTS表本身具有相同名称的自动生成的列。

1

“<B>”

“开始匹配”文本。

2

“</ B>”

“结束符”文本。

3

“<B> ... </ b>的”

“省略号”文本。

4

-1

FTS表列号从中提取文本的返回片段。列从零开始从左到右进行编号。负值表示可以从任何列中提取文本。

5

-15

此整数参数的绝对值用作要包括在返回的文本值中的(近似)令牌数。最大允许绝对值是64.在下面的讨论中,这个参数的值被称为N.

代码片段函数首先尝试查找由| N |组成的文本片段 当前行中的标记至少包含一个词匹配,用于匹配当前行中某处的每个可匹配短语,其中| N | 是传递给片段函数的第六个参数的绝对值。如果存储在单个列中的文本包含小于| N | 令牌,然后考虑整个列值。文本片段不能跨越多个列。

如果可以找到这样的文本片段,则返回以下修改:

  • 如果文本片段不是从列值的开始处开始的,则将“椭圆”文本作为前缀。
  • 如果文本片段没有在列值的末尾结束,则会在其后面添加“省略号”文本。
  • 对于作为短语匹配一部分的文本片段中的每个令牌,将“开始匹配”文本插入到令牌之前的片段中,并将“结束匹配”文本紧接其后插入。

如果可以找到多于一个这样的片段,则包含大量“额外”短语匹配的片段是有利的。所选文本片段的开始可向前或向后移动几个令牌,以试图将短语匹配集中到片段的中心。

假定N是正值,如果没有找到包含对应于每个可匹配短语的短语匹配的片段,则片段函数试图找到两个大约N / 2个令牌的片段,它们之间包含对于每个可匹配的至少一个短语匹配短语与当前行相匹配。如果失败,则试图找到三个N / 3令牌片段,最后是四个N / 4令牌片段。如果找不到包含所需短语匹配的四个片段的集合,则选择提供最佳覆盖的四个N / 4令牌片段。

如果N是负值,并且找不到包含所需短语匹配的单个片段,则片段函数将搜索| N |的两个片段。每个令牌,然后是三个,然后是四个。换句话说,如果N的指定值为负,如果需要多于一个片段来提供所需的词组匹配覆盖率,则片段的大小不会减小。

在找到M个碎片之后,其中M在两个和四个之间(如上面段落中所述),它们按照排序顺序连接在一起,“椭圆”文本将它们分开。之前列举的三项修改是在文本返回之前对文本执行的。

代码语言:javascript
复制
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值。如果它在不使用全文索引的查询中使用(“rowid”或“线性扫描”查询),则该块的大小为零字节。否则,blob由机器字节顺序中的零个或多个32位无符号整数组成。返回数组中整数的确切数量取决于查询和传递给matchinfo函数的第二个参数(如果有)的值。

matchinfo函数用一个或两个参数调用。至于所有的辅助函数,第一个参数必须是特殊的FTS隐藏列。第二个参数,如果指定的话,必须是只包含字符'p','c','n','a','l','s','x','y'和'b'。如果没有明确提供第二个参数,则默认为“pcx”。第二个参数在下面被称为“格式字符串”。

匹配信息格式字符串中的字符从左到右进行处理。格式字符串中的每个字符都会将一个或多个32位无符号整数值添加到返回的数组中。下表中的“值”列包含每个支持的格式字符串字符附加到输出缓冲区的整数值的数量。在给出的通式中,COLS是在FTS表中的列的数目,和短语是查询中的可匹配的短语的数量。

字符

描述

p

1

查询中可匹配短语的数量。

C

1

FTS表中用户定义列的数量(即不包括docid或FTS隐藏列)。

X

3 * cols *phrases

对于短语和表列的每个不同组合,以下三个值:在当前行中,短语出现在列中的次数。该短语出现在FTS表中所有行的列中的总次数。列中至少包含一个短语实例的FTS表中的总行数。第一组三个值对应于表格(列0)的最左列和查询(短语0)中最左边的可匹配短语。如果该表有多个列,则输出数组中的第二组三个值对应于短语0和第1列。对于该表的所有列,紧接着短语0,第2列等。对于短语1,列0,短语1,列1等等等等。换句话说,

cols*phrases

对于短语和表格列的每种不同组合,列中出现的可用短语匹配数量。这通常与matchinfo'x'标志返回的每个三个集合中的第一个值相同。但是,对于与当前行不匹配的子表达式的一部分的任何短语而言,由'y'标志报告的匹配数为零。这对包含OR运算符后代的AND运算符的表达式有所不同。例如,考虑以下表达式:a OR(b AND c)和文档:“ac d”matchinfo'x'标志将报告短语“a”和“c”的单个命中。然而,'y'指令报告“c”的命中数为零,因为它是与文档不匹配的子表达式的一部分 - (b AND c)。对于不包含来自OR运算符的AND运算符的查询,由'y'返回的结果值总是与由'x'返回的结果值相同。整数值数组中的第一个值对应于表(列0)的最左列和查询中的第一个短语(短语0)。可以使用以下公式定位与其他列/短语组合对应的值:hits_for_phrase_p_column_c = arrayc + p * cols对于使用OR表达式的查询,或使用LIMIT或返回多行的查询,'y'matchinfo选项可能会更快比'x'。总是和'x'返回的一样。整数值数组中的第一个值对应于表(列0)的最左列和查询中的第一个短语(短语0)。可以使用以下公式定位与其他列/短语组合对应的值:hits_for_phrase_p_column_c = arrayc + p * cols对于使用OR表达式的查询,或使用LIMIT或返回多行的查询,'y'matchinfo选项可能会更快比'x'。总是和'x'返回的一样。整数值数组中的第一个值对应于表(列0)的最左列和查询中的第一个短语(短语0)。可以使用以下公式定位与其他列/短语组合对应的值:hits_for_phrase_p_column_c = arrayc + 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是表中的列数,则要确定列c中是否存在短语p:p_is_in_c = arrayp *((nCol + 31)/ 32)&(1 <<(c%32))

ñ

1

FTS4表中的行数。此值仅在查询FTS4表格而非FTS3时可用。

一个

cols

对于每列,存储在列中的文本值中的平均令牌数(考虑FTS4表中的所有行)。此值仅在查询FTS4表格而非FTS3时可用。

cols

对于每列,存储在FTS4表的当前行中的值的长度,以令牌表示。此值仅在查询FTS4表格而非FTS3时可用。只有当“matchinfo = fts3”指令没有被指定为用于创建FTS4表的“CREATE VIRTUAL TABLE”语句的一部分时。

小号

cols

对于每列,短语的最长子序列的长度与列值与查询文本的公共长度相匹配。例如,如果表格列包含文本'abcde'并且查询为'ac'd e“',则最长公共子序列的长度为2(短语”c“,后面跟着短语”d e“)。

  • 在当前行中,短语出现在列中的次数。
  • 该短语出现在FTS表中所有行的列中的总次数。
  • 列中至少包含一个短语实例的FTS表中的总行数。

第一组三个值对应于表格(列0)的最左列和查询(短语0)中最左边的可匹配短语。如果该表有多个列,则输出数组中的第二组三个值对应于短语0和第1列。对于该表的所有列,紧接着短语0,第2列等。对于短语1,列0,短语1,列1等,换句话说,可以使用以下公式来找到列c中出现短语p的数据:hits_this_row = array3 *(c + p * cols )+ 0 hits_all_rows = array3 *(c + p * cols)+ 1 docs_with_hits = array3 *(c + p * cols)+ 2

代码语言:txt
复制
y  _cols_ \* _phrases_   For each distinct combination of a phrase and table column, the number of usable phrase matches that appear in the column. This is usually identical to the first value in each set of three returned by the [matchinfo 'x' flag](fts3#matchinfo-x). However, the number of hits reported by the 'y' flag is zero for any phrase that is part of a sub-expression that does not match the current row. This makes a difference for expressions that contain AND operators that are descendants of OR operators. For example, consider the expression:           a OR (b AND c) 

和文件:“a c d”

matchinfo'x'标志会报告短语“a”和“c”的单击。但是,'y'指令将“c”的匹配数量报告为零,因为它是与文档不匹配的子表达式的一部分 - (b AND c)。对于不包含来自OR运算符的AND运算符的查询,由'y'返回的结果值总是与由'x'返回的结果值相同。整数值数组中的第一个值对应于表(列0)的最左列和查询中的第一个短语(短语0)。对应于其他列/短语组合的值可以使用以下公式来定位:

代码语言:txt
复制
       hits\_for\_phrase\_p\_column\_c  = array[c + p\*cols] 

对于使用OR表达式的查询,或使用LIMIT或返回多行的查询,'y'matchinfo选项可能比'x'快。b ((cols + 31)/ 32) * 短语 matchinfo'b'标志提供与matchinfo'y'标志相似的信息,但是以更紧凑的形式。'b'为每个短语/列组合提供了一个布尔标志,而不是确切的点击次数。如果短语在列中至少出现一次(即如果相应的'y'的整数输出将不为零),则会设置相应的标志。否则清除。如果该表的列数不超过32列,则为查询中的每个短语输出一个无符号整数。如果短语在列0中至少出现一次,则设置整数的最低有效位。如果短语在列1中出现一次或多次,则设置第二个最低有效位。依此类推。

如果该表的列数超过32列,则每个额外32列或其中一部分的每个短语的输出中会添加一个额外的整数。对应于相同短语的整数聚集在一起。例如,如果查询具有45列的表格两个短语,则输出4个整数。第一个对应于表格的0和0-31列。第二个整数包含短语0和32-44列的数据,依此类推。

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

代码语言:txt
复制
 p\_is\_in\_c = array[p \* ((nCol+31)/32)] & (1 << (c % 32)) 
代码语言:txt
复制
n  1  The number of rows in the FTS4 table. This value is only available when querying FTS4 tables, not FTS3.    a   _cols_  For each column, the average number of tokens in the text values stored in the column (considering all rows in the FTS4 table). This value is only available when querying FTS4 tables, not FTS3.    l   _cols_   For each column, the length of the value stored in the current row of the FTS4 table, in tokens. This value is only available when querying FTS4 tables, not FTS3. And only if the "matchinfo=fts3" directive was not specified as part of the "CREATE VIRTUAL TABLE" statement used to create the FTS4 table.    s   _cols_  For each column, the length of the longest subsequence of phrase matches that the column value has in common with the query text. For example, if a table column contains the text 'a b c d e' and the query is 'a c "d e"', then the length of the longest common subsequence is 2 (phrase "c" followed by phrase "d e").   

例如:

代码语言:javascript
复制
-- 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函数比片段或偏移量函数快得多。这是因为需要执行片段和偏移量来检索从磁盘分析的文档,而匹配信息所需的所有数据可作为实现全文所需全文索引相同部分的一部分查询自己。这意味着在以下两个查询中,第一个可能比第二个查询快一个数量级:

代码语言:javascript
复制
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()函数的示例。

版本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表的名称作为第二个参数。(为SQLite 版本3.7.17(2013-05-20)添加了fts4aux的双参数形式,并会在以前的版本中引发错误。)例如:

代码语言:javascript
复制
-- 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表格总是有四列,如下所示,从左到右:

列名称

列内容

术语

包含此行的术语文本。

该列可能包含文本值'*'(即单个字符,U + 002a)或0到N-1之间的整数,其中N再次是相应FTS表中用户定义列的数量。

文件

此列始终包含一个大于零的整数值。如果“col”列包含值“*”,则此列包含FTS表的至少包含一个实例(在任何列中)的行数。如果col包含整数值,那么此列包含FTS表的行数,该表中至少包含由col值标识的列中的一个术语实例。像往常一样,FTS表的列从左到右编号,从零开始。

事件

该列也总是包含一个大于零的整数值。如果“col”列包含值“*”,那么此列包含FTS表(所有列中)的所有行中该术语的实例总数。否则,如果col包含整数值,则此列包含出现在由col值标识的FTS表列中的术语的实例总数。

languageid(隐藏)

此列确定使用哪个languageid从FTS3 / 4表中提取词汇表。languageid的默认值为0.如果在WHERE子句约束中指定了替代语言,则使用该替代语言而不是0.每个查询只能有一个语言ID。换句话说,WHERE子句不能在languageid中包含范围约束或IN运算符。

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

代码语言:javascript
复制
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表,则查询的结果可能仅反映所做更改的一个(可能是空的)子集。

如果“CREATE VIRTUAL TABLE”语句指定了模块FTS4(不是FTS3),那么类似于“tokenize = *”选项的特殊指令-FTS4选项也可能出现在列名称的位置。FTS4选项包含选项名称,后跟一个“=”字符,后跟选项值。该选项值可以选择性地包含在单引号或双引号中,嵌入的引号字符以与SQL文字相同的方式转义。“=”字符的两边可能没有空格。例如,要创建一个选项“matchinfo”的值设置为“fts3”的FTS4表:

代码语言:javascript
复制
-- Create a reduced-footprint FTS4 table.
CREATE VIRTUAL TABLE papers USING fts4(author, document, matchinfo=fts3);

FTS4目前支持以下选项:

选项

解释

压缩

compress选项用于指定压缩功能。指定压缩函数而不指定解压缩函数是错误的。详情请参阅下文。

内容

内容允许被索引的文本存储在与FTS4表格不同的独立表格中,甚至可以存储在SQLite之外。

languageid

languageid选项使FTS4表具有一个额外的隐藏整数列,用于标识每行中包含的文本的语言。languageid选项的使用允许相同的FTS4表格以多种语言或脚本保存文本,每个语言或脚本具有不同的标记器规则,并且独立于其他语言查询每种语言。

matchinfo

当设置为“fts3”时,matchinfo选项会减少FTS4存储的信息量,结果是matchinfo()的“l”选项不再可用。

notindexed

此选项用于指定数据未编入索引的列的名称。存储在未编制索引的列中的值与MATCH查询不匹配。它们也不被辅助功能识别。单个CREATE VIRTUAL TABLE语句可能包含任何数量的未指定索引的选项。

订购

“订单”选项可以设置为“DESC”或“ASC”(大写或小写)。如果它被设置为“DESC”,那么FTS4以这样的方式存储其数据,以便通过docid以降序优化返回结果。如果它被设置为“ASC”(默认值),那么数据结构会针对docid以升序返回结果进行优化。换句话说,如果许多针对FTS4表的查询使用“ORDER BY docid DESC”,那么它可以提高向CREATE VIRTUAL TABLE语句添加“order = desc”选项的性能。

字首

该选项可以设置为以逗号分隔的正整数非零整数列表。对于列表中的每个整数N,在数据库文件中创建单独的索引以优化前缀查询,其中当使用UTF-8编码时,查询项的长度为N字节,不包括'*'字符。详情请参阅下文。

解压

该选项用于指定解压缩功能。指定解压缩函数而不指定压缩函数是错误的。详情请参阅下文。

使用FTS4时,指定包含“=”字符且不是“tokenize = *”规范或识别的FTS4选项的列名是错误。使用FTS3,无法识别的指令中的第一个标记被解释为列名称。同样,在使用FTS4时,在单个表声明中指定多个“tokenize = *”指令是错误的,而第二个和随后的“tokenize = *”指令被FTS3解释为列名称。例如:

代码语言:javascript
复制
-- 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 =选项

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

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

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

如果指定的压缩或解压缩函数不存在,表格可能仍然被创建。直到读取FTS4表(如果解压缩函数不存在)或写入(如果它是不存在的压缩函数),才会返回错误。

代码语言:javascript
复制
-- 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);

在实现压缩和解压缩功能时,注意数据类型非常重要。特别是,当用户从压缩的FTS表中读取值时,FTS返回的值与解压缩函数返回的值(包括数据类型)完全相同。如果该数据类型与传递给压缩函数的原始值的数据类型不同(例如,如果解压缩函数在压缩最初传递为TEXT时返回BLOB),则用户查询可能无法按预期方式运行。

6.2.内容=选项

内容选项允许FTS4放弃存储被编入索引的文本。内容选项可以以两种方式使用:

  • 索引文档根本不存储在SQLite数据库中(“无内容”FTS4表),或者
  • 索引文档存储在由用户创建和管理的数据库表(“外部内容”FTS4表)中。

由于索引文档本身通常比全文索引大得多,因此可以使用内容选项来节省大量空间。

6.2.1. Contentless FTS4 Tables

为了创建一个完全不存储索引文档副本的FTS4表,应该将内容选项设置为空字符串。例如,以下SQL创建了包含三列 - “a”,“b”和“c”的FTS4表格:

代码语言:javascript
复制
CREATE VIRTUAL TABLE t1 USING fts4(content="", a, b, c);

可以使用INSERT语句将数据插入到这样的FTS4表中。但是,与普通的FTS4表格不同,用户必须提供明确的整数docid值。例如:

代码语言:javascript
复制
-- 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()和偏移量()可能不会。例如:

代码语言:javascript
复制
-- 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表达式匹配0行,即使语句确实引用除docid以外的列值,也可能不会有任何错误。

6.2.2.外部内容FTS4表

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

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

代码语言:javascript
复制
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的内容表中的相应列读取所请求的值。或者,如果在内容表中找不到这样的行,则会使用NULL值。例如:

代码语言:javascript
复制
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与外部内容表保持同步,任何UPDATE或DELETE操作必须首先应用于FTS表,然后应用于外部内容表。例如:

代码语言:javascript
复制
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;

与单独编写全文索引和内容表格不同,某些用户可能希望使用数据库触发器来保持全文索引与存储在内容表格中的一组文档保持最新。例如,使用前面例子中的表格:

代码语言:javascript
复制
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表的名称,重建命令如下所示:

代码语言:javascript
复制
INSERT INTO t3(t3) VALUES('rebuild');

例如,如果令牌生成器的实现更改,则该命令也可以用于普通的FTS4表。尝试重建由无内容FTS4表维护的全文索引是错误的,因为没有内容可用于重建。

6.3.languageid =选项

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

代码语言:javascript
复制
CREATE VIRTUAL TABLE t1 USING fts4(x, y, languageid="lid")

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

默认情况下,FTS查询(使用MATCH运算符的那些查询)仅考虑那些languageid列设置为0的行。要查询具有其他languageid值的行,可以使用“

=“必须添加到查询WHERE子句中,例如:

代码语言:javascript
复制
SELECT * FROM t1 WHERE t1 MATCH 'abc' AND lid=5;

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

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

当使用languageid选项时,SQLite会在创建对象后立即调用sqlite3_tokenizer_module对象上的xLanguageid(),以便传入标记器应使用的语言标识。对于任何单个标记器对象,xLanguageid()方法永远不会被调用多次。不同的语言可能被标记为不同的事实是没有单个FTS查询可以返回具有不同languageID值的行的原因之一。

6.4.matchinfo =选项

matchinfo选项只能设置为值“fts3”。尝试将matchinfo设置为除“fts3”之外的任何内容都是错误。如果指定了这个选项,那么由FTS4存储的一些额外信息被省略。这减少了FTS4表占用的磁盘空间量,直到它几乎与等价的FTS3表使用的数量相同,但也意味着通过将'l'标志传递给matchinfo()来访问数据,功能不可用。

6.5.notindexed =选项

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

代码语言:javascript
复制
-- 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运算符匹配的条件。它们不会影响offset()或matchinfo()辅助函数的结果。snippet()函数也不会根据存储在未索引列中的值返回片段。

6.6.前缀=选项

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

在两种情况下,前缀索引可用于优化前缀查询。如果查询的前缀为N个字节,则使用“前缀= N”创建的前缀索引提供最佳优化。或者,如果没有“prefix = N”索引可用,则可以使用“prefix = N + 1”索引代替。使用“前缀= N + 1”索引比“前缀= N”索引效率低,但是根本比没有前缀索引好。

代码语言:javascript
复制
-- 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*';

特殊的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.“优化”命令

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

在批处理模式操作中,最初使用大量INSERT操作构建FTS表,然后重复查询而无需进一步更改,在最后一个INSERT之后和第一个查询之前运行“优化”通常是一个好主意。

7.2.“重建”命令

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

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

7.3.“完整性检查”命令

“完整性检查”命令使SQLite通过比较这些倒排索引与原始内容,来读取并验证FTS3 / 4表中所有倒排索引的准确性。如果反转索引全部正常,“完整性检查”命令静静地成功,但如果发现任何问题,将失败并显示SQLITE_CORRUPT错误。

“完整性检查”命令在概念上与PRAGMA integrity_check类似。在一个工作系统中,“完整性命令”应该总是成功的。完整性检查失败的可能原因包括:

  • 应用程序直接更改了FTS影子表,而不使用FTS3 / 4虚拟表,导致影子表彼此不同步。
  • 使用FTS4内容选项并且未能手动保持内容与FTS4倒排索引同步。
  • FTS3 / 4虚拟表中的错误。(“完整性检查”命令最初是作为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-tree段中。这些级别合并的效果是大多数插入到FTS表中的INSERT非常快并且占用的内存很少,但偶尔的INSERT很慢,并且由于需要合并而产生大量事务。这导致INSERT的“棘手”性能。

为了避免棘手的INSERT性能,应用程序可以定期运行“merge = X,Y”命令,可能在空闲线程或空闲进程中,以确保FTS表不会在相同级别累积太多b-树段。通常可以避免INSERT性能峰值,通过在每几千个文档插入后运行“merge = X,Y”,可以最大化FTS3 / 4的性能。每个“merge = X,Y”命令将在单独的事务中运行(当然,除非它们使用BEGIN ... COMMIT分组在一起)。通过在100到300范围内为X选择一个值,事务可以保持很小。运行合并命令的空闲线程可以通过检查sqlite3_total_changes()在每个“merge = X,Y”

7.5.“automerge = N”命令

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

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

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

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

对于工作负载仅包含少量UPDATE或DELETE操作的情况下的通用情况,automerge的一个好选择是8.如果工作负载包含许多UPDATE或DELETE命令,或者如果查询速度是一个问题,则可以将automerge减少到2 。

出于向后兼容性的原因,“automerge = 1”命令将automerge参数设置为8,而不是1(因为合并单个段中的数据为空操作,值为1无论如何都无济于事)。

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

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

  • 术语是符合条件的字符的连续序列,其中符合条件的字符都是字母数字字符,并且Unicode代码点值大于或等于128的所有字符。将文档拆分为术语时,所有其他字符都将被丢弃。他们唯一的贡献是将相邻的术语分开。
  • 作为标记化过程的一部分,ASCII范围内的所有大写字符(Unicode码点小于128)都将转换为小写字母。因此,使用简单标记器时,全文查询不区分大小写。

例如,当文档中包含“现在,他们非常沮丧”的文档时,从文档中提取并添加到全文索引中的术语依次为“现在他们非常沮丧”。这样的文档将匹配诸如“MATCH'沮丧'”的全文查询,因为在搜索全文索引之前,简单标记器将查询中的术语转换为小写。

除了“简单”标记器之外,FTS源代码还包含一个使用Porter Stemming算法的标记器。这个标记器使用相同的规则将输入文档分解为术语,包括将所有术语折成小写,但也使用Porter Stemming算法将相关的英语单词减少到公共根。例如,使用与上面段落中相同的输入文档,搬运工标记器将提取下列标记:“现在就是这样的沮丧”。即使这些术语中的一些甚至不是英文单词,但在某些情况下,使用它们来构建全文索引比简单标记器产生的更易理解的输出更有用。使用搬运工分词器,文档不仅匹配诸如“MATCH'沮丧'”的全文查询,而且匹配诸如“MATCH'挫折'”之类的查询,因为术语“挫折” 由Porter stemmer算法减少到“挫败” - 就像“沮丧”一样。因此,在使用搬运工分词器时,FTS不仅能够查找所查询词语的完全匹配,还能够与相似的英语词条匹配。有关Porter Stemmer算法的更多信息,请参阅上面链接的页面。

举例说明“简单”和“搬运工”标记器之间的区别:

代码语言:javascript
复制
-- 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”英语。例如:

代码语言:javascript
复制
CREATE VIRTUAL TABLE thai_text USING fts3(text, tokenize=icu th_TH)

ICU标记器实现非常简单。它根据ICU规则分割输入文本以找到单词边界并丢弃完全由空格组成的任何令牌。这可能适用于某些地区的某些应用程序,但不是全部。如果需要更复杂的处理,例如为了实现词干或丢弃标点符号,可以通过创建一个使用ICU标记器作为其实现的一部分的标记器实现来完成。

SQLite 版本3.7.13(2012-06-11)开始提供“unicode61”标记器。Unicode61的工作原理与“简单”非常相似,只不过它根据Unicode版本6.1中的规则进行简单的unicode案例折叠,它识别unicode空格和标点符号并使用它们分隔标记。简单标记器仅对ASCII字符进行大小写折叠,并且仅将ASCII空格和标点符号识别为标记分隔符。

默认情况下,“unicode61”也会从拉丁脚本字符中删除所有变音符号。此行为可以通过添加标记器参数“remove_diacritics = 0”来覆盖。例如:

代码语言:javascript
复制
-- Create tables that remove diacritics 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=1");

-- 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");

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

代码语言:javascript
复制
-- 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 =”参数一部分的字符默认被认为是一个标记字符,它将被忽略。即使它已被较早的“分隔符=”选项标记为分隔符,也是如此。同样,如果默认指定为“分隔符=”选项一部分的字符作为分隔符,则忽略该字符。如果指定了多个“tokenchars =”或“separators =”选项,则会处理所有选项。例如:

代码语言:javascript
复制
-- 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.自定义(应用程序定义)标志符

除了提供内置的“简单”,“搬运工”和(可能)“icu”和“unicode61”标记器之外,FTS还为应用程序提供了一个接口,用于实现和注册用C编写的自定义标记器。用于创建新标记器是在fts3_tokenizer.h源文件中定义和描述的。

注册一个新的FTS tokenizer类似于使用SQLite注册一个新的虚拟表模块。用户将指针传递给包含指向各种回调函数的指针的结构,这些回调函数组成新的标记器类型的实现。对于标记器,结构(在fts3_tokenizer.h中定义)称为“sqlite3_tokenizer_module”。

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

代码语言:javascript
复制
SELECT fts3_tokenizer(<tokenizer-name>);
SELECT fts3_tokenizer(<tokenizer-name>, <sqlite3_tokenizer_module ptr>);

其中<tokenizer-name>是一个标识tokenizer的字符串,<sqlite3_tokenizer_module ptr>是一个指向编码为SQL blob的sqlite3_tokenizer_module结构的指针。如果第二个参数存在,它将被注册为tokenizer <tokenizer-name>并返回其副本。如果只传递一个参数,则返回一个指向当前注册为<tokenizer-name>的tokenizer实现的指针,并将其编码为blob。或者,如果不存在这样的标记器,则会引发SQL异常(错误)。

由于安全考虑,SQLite 版本3.11.0(2016-02-15)和更高版本仅在使用-DSQLITE_ENABLE_FTS3_TOKENIZER选项编译库时才启用第二种形式的fts3_tokenizer()函数。在早期版本中,它始终可用。从SQLite 版本3.12.0(2016-03-29)开始,第二种形式的fts3_tokenizer()也可以在运行时通过调用sqlite3_db_config(db,SQLITE_DBCONFIG_ENABLE_FTS3_TOKENIZER,1,0)来激活。

安全警告:如果支持fts3_tokenizer()的双参数形式的fts3 / 4扩展版本部署在恶意用户可以运行任意SQL的环境中,则应禁止这些用户调用双参数fts3_tokenizer( )功能。这可以使用授权回调完成,也可以通过调用sqlite3_db_config(db,SQLITE_DBCONFIG_ENABLE_FTS3_TOKENIZER,0,0)禁用双参数fts3_tokenizer()接口来完成。

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

代码语言:javascript
复制
/*
** 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(?, ?)";

  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.查询Tokenizers

“fts3tokenize”虚拟表可用于直接访问任何标记器。以下SQL演示了如何创建fts3tokenize虚拟表的实例:

代码语言:javascript
复制
CREATE VIRTUAL TABLE tok1 USING fts3tokenize('porter');

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

代码语言:javascript
复制
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");

一旦虚拟表格被创建,它可以被查询如下:

代码语言:javascript
复制
SELECT token, start, end, position 
  FROM tok1
 WHERE input='This is a test sentence.';

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

代码语言:javascript
复制
thi|0|4|0
is|5|7|1
a|8|9|2
test|10|14|3
sentenc|15|23|4

请注意,来自fts3tokenize虚拟表的结果集中的标记已根据标记器的规则进行了转换。由于这个例子使用了“porter”标记器,所以“This”标记被转换为“thi”。如果令牌的原始文本是需要的,可以使用substr()函数的“start”和“end”列检索它。例如:

代码语言:javascript
复制
SELECT substr(input, start+1, end-start), token, position
  FROM tok1
 WHERE input='This is a test sentence.';

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

本节高层介绍FTS模块将其索引和内容存储在数据库中的方式。这是没有必要阅读或理解本节中的材料,以便使用FTS在应用程序中。但是,试图分析和理解FTS性能特征的应用程序开发人员或考虑增强现有FTS功能集的开发人员可能会很有用。

9.1.影子表

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

“%_content”表的最左边一列是一个名为“docid”的INTEGER PRIMARY KEY字段。在此之后是用户声明的FTS虚拟表的每列的一列,通过用“c N ” 预先给用户提供的列名来命名,其中N是表中列的索引,从左开始编号从0开始向右。作为虚拟表声明的一部分提供的数据类型不会用作%_content表声明的一部分。例如:

代码语言:javascript
复制
-- 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”值,则系统会自动选择一个值。

仅当FTS表使用FTS4模块而非FTS3时,才会创建%_stat和%_docsize表。此外,如果使用作为CREATE VIRTUAL TABLE语句的一部分指定的“matchinfo = fts3”伪指令创建FTS4表,则会省略%_docsize表。如果它们被创建,那么这两个表的模式如下所示:

代码语言:javascript
复制
CREATE TABLE %_stat(
  id INTEGER PRIMARY KEY, 
  value BLOB
);

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

对于FTS表中的每一行,%_docsize表都包含具有相同“docid”值的对应行。“大小”字段包含一个由N个 FTS varints 组成的块,其中N是表中用户定义列的数量。“size”blob中的每个varint都是FTS表中关联行的相应列中的标记数量。%_stat表总是包含一行,其中“id”列设置为0.“value”列包含由N + 1个 FTS varints 组成的blob ,其中N又是FTS表中用户定义列的数量。blob中的第一个varint设置为FTS表中的总行数。第二个和随后的varint包含存储在FTS表的所有行的相应列中的令牌总数。

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

代码语言:javascript
复制
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-tree结构。%_segdir表中的每行都有一个b-tree。%_segdir表行包含根节点和与b-tree结构关联的各种元数据,%_segments表包含所有其他(非根)b-树节点。每个B树被称为“段”。一旦创建完成,分段B树永远不会更新(尽管它可能完全被删除)。

每个分段b-tree使用的键是词(单词)。除了关键之外,每个分段b-tree条目还有一个关联的“doclist”(文档列表)。一个doclist包含零个或多个条目,其中每个条目包括:

  • 一个docid(文档ID)和
  • 术语偏移量列表,文档中每个术语的出现次数。术语偏移量表示在术语之前出现的令牌(单词)的数量,而不是字符或字节的数量。例如,术语“战争”一词在“祖先的声音预言战争!是3。

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

逻辑全文索引的内容是通过合并所有分段b-树的内容找到的。如果一个术语存在于多个分段B树中,则它映射到每个单独文档列表的联合。如果对于单个术语,在多个文档列表中出现相同的docid,则只有属于最近创建的分段b-树的文档列表才被认为是有效的。

使用多个B树结构而不是单个B树来降低向FTS表中插入记录的成本。将新记录插入已包含大量数据的FTS表中时,很可能新记录中的许多术语已存在于大量现有记录中。如果使用单个b-tree,那么大型doclist结构将不得不从数据库中加载,修改为包含新的docid和term-offset列表,然后写回数据库。使用多个b-tree表格可以通过创建一个新的b-tree来避免这种情况,该b-tree可以在之后与现有的b-tree(或b-tree)合并。b-树结构的合并可以作为背景任务执行,或者一旦累积了一定数量的单独的b-树结构。当然,

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

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

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

编码的FTS varint的最后一个字节清除了最高有效位。所有前面的字节都设置了最高有效位。数据存储在每个字节的其余七个最低有效位中。编码表示的第一个字节包含编码整数值的最低有效七位。编码表示的第二个字节(如果存在)包含整数值的七个次最低有效位,依此类推。下表包含编码整数值的示例:

十进制

十六进制

编码表示

43

0x000000000000002B

0x2B

200815

0x000000000003106F

0x9C 0xA0 0x0C

-1

0xFFFFFFFFFFFFFFFF

0xFF 0xFF 0xFF 0xFF 0xFF 0xFF 0xFF 0xFF 0xFF 0x01

9.3.段B树格式

段b树是前缀压缩的b +树。%_segdir表中的每一行都有一个分段b-tree(参见上文)。段b-树的根节点作为blob存储在%_segdir表的相应行的“根”字段中。所有其他节点(如果存在)都存储在%_segments表的“blob”列中。%_segments表内的节点由相应行的blockid字段中的整数值标识。下表介绍了%_segdir表的字段:

解释

水平

在它们之间,“级别”和“idx”字段的内容定义段b树的相对年龄。存储在“级别”字段中的值越小,最近创建了段b-树。如果两个分段b-树具有相同的“等级”,则存储在“idx”列中具有较大值的分段更近。%_segdir表上的PRIMARY KEY约束可以防止“level”和“idx”字段中的任何两个段具有相同的值。

IDX

往上看。

start_block

对应于属于此分段b-tree的具有最小blockid的节点的blockid。如果整个段b树适合根节点,则为零。如果存在,这个节点总是一个叶节点。

leaves_end_block

与具有最大blockid的叶节点相对应的blockid,该blockid属于此分段b-tree。如果整个段b树适合根节点,则为零。

end_block

该字段可能包含整数或文本字段,由两个由空格字符分隔的整数组成(unicode codepoint 0x20)。第一个或唯一的整数是与具有属于该段b树的最大blockid的内部节点相对应的blockid。如果整个段b树适合根节点,则为零。如果存在,该节点始终是内部节点。第二个整数(如果存在)是叶节点上存储的所有数据的总大小(以字节为单位)。如果该值为负数,则该段是未完成的增量合并操作的输出,并且绝对值是当前大小(以字节为单位)。

包含段b树的根节点的Blob。

除了根节点之外,组成单个分段B树的节点始终使用连续的blockid序列进行存储。此外,组成b级树的单个级别的节点本身以b树顺序存储为连续块。用于存储b树叶的连续序列的块ID被分配,开始于存储在相应的%_ segdir行的“start_block”列中的blockid值,并且完成存储在其相同的“leaves_end_block”字段中的块ID值行。因此,可以通过按顺序从“start_block”到“leaves_end_block”遍历%_segments表来遍历段b树的所有叶子。

9.3.1.段B树叶节点

下图描述了分段b-树叶节点的格式。

段B树叶节点格式

存储在每个节点上的第一项(上图中的“术语1”)是逐字存储的。随后的每个术语都相对于其前身进行了前缀压缩。条款以排序(memcmp)顺序存储在页面中。

9.3.2.段B树内部节点

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

段B树内部节点格式

9.4.文件格式

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

  1. docid值。doclist中的第一个条目包含文字docid值。每个后续doclist条目的第一个字段包含新docid和前一个(总是正数)之间的差异。
  2. 零个或多个术语偏移列表。包含该术语的FTS虚拟表的每一列都有一个术语偏移量列表。术语偏移量列表包括以下内容:
    1. 常量值1.对于与列0关联的任何项 - 偏移量列表,此字段被省略。
    2. 列号(1表示第二个最左边的列等)。对于与列0关联的任何项偏移列表,该字段被省略。
    3. 术语偏移列表,从最小到最大排序。不是直接存储术语偏移值,而是存储的每个整数是当前术语偏移量和前一个术语偏移量之间的差值(如果当前术语偏移量是第一个,则为零)加2。
  3. 常量值为0。

FTS3 Doclist格式

FTS文件列表输入格式

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

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

一个非常简单的方案可能是计算每个结果文档中用户搜索项的实例数。包含许多术语实例的那些文档被认为比每个术语实例数量少的文档更相关。在FTS应用程序中,可以通过计算偏移量函数的返回值中的整数数量来确定每个结果中的词项实例的数量。以下示例显示了一个可用于获取用户输入的查询的十个最相关结果的查询:

代码语言:javascript
复制
-- 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函数比偏移函数有效得多。此外,matchinfo函数还提供有关整个文档集(不仅是当前行)中每个查询词的出现次数以及每个查询词出现的文档数量的额外信息。这可以用于(例如)将较高的权重附加到较不常用的项,这可能增加用户认为更有趣的那些结果的整体计算相关性。

代码语言:javascript
复制
-- 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通过在对用户查询匹配的每一行检索FTS模块的“标题”列和匹配信息数据,然后排序和限制结果,来满足此查询。由于SQLite虚拟表接口的工作方式,检索“标题”列的值需要从磁盘加载整个行(包括“内容”字段,可能很大)。这意味着如果用户查询匹配数千个文档,则可以将数兆字节的“标题”和“内容”数据从磁盘加载到内存中,即使它们永远不会用于任何目的。

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

代码语言:javascript
复制
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. 片段函数不能用于上述查询。由于外部查询不包含“WHERE ... MATCH”子句,因此可能不会使用它的代码片段功能。一种解决方案是复制外部查询中的子查询使用的WHERE子句。与此相关的开销通常可以忽略不计。
  2. 文档的相关性可能取决于matchinfo的返回值中可用的数据以外的内容。例如,数据库中的每个文档可以基于与其内容(来源,作者,年龄,参考数量等)无关的因素被分配静态权重。这些值可以由应用程序存储在一个单独的表中,该表可以与子查询中的文档表连接,以便rank函数可以访问它们。

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

代码语言:javascript
复制
-- 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数据的示例等级函数。它不是一个单独的权重,而是允许将一个权重从外部分配给每个文档的每一列。它可以像使用sqlite3_create_function的任何其他用户函数一样在SQLite中注册。

安全警告:因为它只是一个普通的SQL函数,所以可以在任何上下文中调用rank()作为任何SQL查询的一部分。这意味着传递的第一个参数可能不是有效的匹配信息块。执行者应该小心处理这种情况,而不会导致缓冲区溢出或其他潜在的安全问题。

代码语言:javascript
复制
/*
** 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&gt) * <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&gt) * <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);
}
代码语言:txt
复制
 SQLite is in the Public Domain.

扫码关注腾讯云开发者

领取腾讯云代金券