此文已由作者王攀授权网易云社区发布。

欢迎访问网易云社区,了解更多网易技术产品运营经验。

简介

对于今天的移动、桌面客户端应用而言,离线全文检索的需求已经十分强烈,我们日常使用的邮件客户端、云音乐、云笔记、易信等就是离线全文检索的潜在用户。

作为目前使用最为广泛的嵌入式数据库,SQLite3其实内置了全文检索的扩展模块——FTS。FTS分为FTS1、FTS2、FTS3、FTS4和FTS5几个版本,其中FTS1和FTS2已经被废弃,而FTS3在2007年9月4日发布的SQLite 3.5.0中被引入,其增强版FTS4则第一次出现在2010年12月8日的SQLite 3.7.4中。由于FTS3与FTS4有着千丝万缕的联系,所以本文将两种FTS引擎放在一起来介绍。FTS5则它们不兼容,所以笔者将以另外一个文章来单独作介绍。

相比于普通表,FTS3/FTS4其实是两种虚表。当你创建一个名为t的FTS虚表的时候,你会发现数据库中其实创建了若干个普通表用于存储物理数据,它们被称为影子表(shadow tables),分别命名为t_content、t_messageize、t_segdir、t_segments、t_stat等。

编译

想让SQLite支持FTS3/FTS4,在编译SQLite的时候需要打开以下编译开关

-DSQLITE_ENABLE_FTS3

注:Chromium、CEF和iOS7及以后的版本内建的SQLite都默认打开了此选项。

如果想要让FTS3/FTS4支持带括号优先级的高级查询(见下文),那么需要同时打开以下开关:

-DSQLITE_ENABLE_FTS3_PARENTHESIS

注:Chromium、CEF内建SQLite没有打开该开关。

如果想要让FTS3/FTS4支持ICU分词器,则需要再打开以下开关:

-DSQLITE_ENABLE_\ICU

注:Chromium、CEF内建SQLite打开并实现了该开关;iOS自带的没有。

表操作

最简单地创建表的形式:

-- 创建一个fts3表message,包含title和body两列CREATE VIRTUAL TABLE message USING fts3(title, body);-- 创建一个fts4表message,包含title和body两列CREATE VIRTUAL TABLE message USING fts4(title, body);

需要注意的是如果在创建表的时候给某个列指定了类型,那么这些类型将被完全忽略。 我们还可以在建表的时候给表指定分词器。例如:

CREATE VIRTUAL TABLE message USING fts3(title, body, tokenize=porter);

以上创建了一个使用porter分词器的表。此外FTS3/FTS4还支持simple、unicode61和外置的ICU等分词器。对于中文,我们建议使用ICU分词器。此外,FTS3/FTS4还支持自定义的分词器,笔者将在之后介绍FTS5的文章中以FTS5为例介绍自定义分词器。

创建FTS4表的时候我们还可以使用一些特殊选项:

compress=、uncompress= 用于支持压缩和解压缩

content= 用于创建无正文表(只有索引)和外部正文表(正文来自其他表而非虚表本身)等

matchinfo= 用于以FTS3方式存储FTS4,忽略FTS4额外所需的信息,但是功能也会因此受限

notindexed= 指定某个列为非索引列

prefix= 额外为指定自己的前缀创建索引,这可以加快前缀查询(见后文)

删除FTS表非常简单,实用DROP语句即可。

增删改

要向FTS表中插入数据类似普通表:

INSERT INTO message(title, body) VALUES('警告', '10086提醒您:您移动卡上余额不足10元');  
INSERT INTO message(docid, title, body) VALUES(2, '警告', '10086提醒您:您移动卡上余额不足5元');

注意到第二句中我们指定了一个叫docid的列,这是隐藏列rowid的一个别名,类似于普通表。

更新和删除和普通表无异:

UPDATE message SET title = '提示' WHERE rowid = 1;DELETE FROM message WHERE rowid = 1;

查询

查询操作是FTS表存在的最大意义。两类查询在FTS表上是比较高效的,它们是:

  • 仅包含rowid的普通查询

  • 全文检索

SELECT * FROM message WHERE rowid = 1  SELECT * FROM message WHERE body MATCH '10086'

下面以ICU为分词器针对全文检索进行进一步介绍。

词查询

查询可以针对整个文档或者文档的某些列来进行精确的词查询:

-- 查询包含“移动”关键字的文档SELECT * FROM message WHERE message MATCH '移动'-- 查询消息体包含“移动”关键字的文档SELECT * FROM message WHERE body MATCH '移动'SELECT * FROM message WHERE message MATCH 'body:移动'-- 查询消息体包含“移动”且文档中包含“您”关键字的文档SELECT * FROM message WHERE message MATCH 'body:移动 您'

注意到,用“列名:词”的方式可以指定在某个列上查询,而用空格隔开可以以“且”的方式连接多个条件。

在FTS4下,在词前面加入^,表示该词必须是某个列的第一个词:

SELECT * FROM message WHERE message MATCH 'body:^移动'

特别需要注意的是:英文词必须使用小写。因为后文中很多关键字需要用它们的大写身份来被识别。

前缀查询

我们在词后面加入一个星号(*)即构成以该词为前缀的查询:

-- 下面的查询包含“移动”的文档会被命中SELECT * FROM message WHERE message MATCH '移*'

在FTS4下,^同样适用于前缀查询。

短语查询

如果我们给定一个由词和前缀组成的有序序列,去数据库中匹配一个连续的有序词序列,使得两个序列中词/前缀逐个依序匹配,就构成了短语查询。

-- 下面的查询将匹配以上两条记录SELECT * FROM message WHERE message MATCH '"移 动"';-- 下面的查询将无法匹配,因为原文中“移”出现在“动”之前而查询中则相反SELECT * FROM message WHERE message MATCH '"动 移"';-- 下面的查询将无法匹配,因为“移”、“卡”之间隔了一个“动”SELECT * FROM message WHERE message MATCH '"移 卡"';

注意短语查询必须将有序词/前缀集用双引号引起来,并且将有序集内每个词用空格隔开。

NEAR查询

短语查询要求词之间必须连续重现,但是有时候我们允许他们就近出现,这个时候就需要使用NEAR查询。

SELECT * FROM message WHERE message MATCH '"移 NEAR 动"';

默认情况下两个词允许最大间隔10个词,但是你也可以自定义:

SELECT * FROM message WHERE message MATCH '"移 NEAR/6 动"';

上例最多允许“移”、“动”之间出现6个词。

逻辑操作

FTS3、FTS4支持逻辑条件关键字(必须大写):

AND:逻辑与,取交集;默认不加条件关键字的情况下,就是这种关系。

OR:逻辑或,取并集

NOT:逻辑非,取补集。可以使用 - 代替

-- 以下两个查询是一致的SELECT * FROM message WHERE message MATCH '移 AND 动';SELECT * FROM message WHERE message MATCH '移 动';

优先级方面,NOT高于AND,高于OR。FTS3/FTS4支持使用括号来改变的优先级:

SELECT * FROM message WHERE message MATCH '(移 OR 动) AND 卡';

再次提醒:使用带括号优先级的查询支持,需要打开 -DSQLITE_ENABLE_FTS3_PARENTHESIS 开关编译SQLite

内建函数

FTS3/FTS4支持三个非常有用的内建函数:offsets、snippet、matchinfo。

offsets

offsets函数返回所有匹配项的偏移信息。总体上来说,offsets针对每个匹配项将返回一个四元组,一句话概括就是:词号为term的词在表中第column列的offset字节处命中了连续的size字节的目标。offsets返回所有这样的四元组的文本形式,例如若:

SELECT offsets(tb1) FROM tb1 WHERE tb1 MATCH 'term1 term2';

返回

"0 1 3 4 1 0 0 6"

那么就表示有两处被命中:

  • 第1列的3字节处被2号词命中,命中长度为4

  • 第2列的0字节处被1号词命中,命中长度为6

注意:column、term、offset编号都从0开始的。

snippet

此函数用于返回最佳命中目标及其周围的切片。例如,SQLite官网的搜索功能的高亮显示就是用此函数实现的。

这个函数支持可变参数,我们可以给它传1至6个参数。6个参数按照从0开始编号说明如下: 0:必须使用隐藏列,也就是要查询的虚表名,比如上面的message。
1:返回值中被命中目标开始处的标记文本,默认为“”
2:返回值中被命中目标结束处的标记文本,默认为“”
3:被省略文本的标识,比如“...”
4:强制指定从哪个列提取切片文本,默认为-1,表示可从任意列提取
5:此值的绝对值表示返回值中大致包含多少个单词,最大可取64,默认-15

SELECT snippet(message, '[ ']', '...') FROM message WHERE message MATCH '"移* 余*"'

matchinfo

这是一个更加高效的函数,因为它本身的返回值不需要将整个行全部从磁盘调入内存而只需要查询索引数据。此外,这个函数也提供了足够的用于运行常用结果评价算法的信息。

限于篇幅,本函数不作展开详述,大家可以参考最后给出的链接查阅。

常用特殊命令

FTS3/FTS4支持一些特殊命令来维护索引等。下面是我们会常用的两条:

-- 优化表,本质是将所有独立的小索引树合并成一整棵B树

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

-- 重建索引

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

FTS3与FTS4的区别

FTS3和FTS4是比较相似的,它们共享了很多底层技术,也共享了相同的接口。它们的不同点在于:

  1. FTS4包含了查询优化,可以显著提升高频词的检索性能

  2. FTS4下matchinfo()内建函数得到更多的可选信息

  3. FTS4为了实现1中提到的优点,需要额外的存储空间,不过一般情况下这部分空间开销比较小

  4. FTS4支持hooks来实现压缩存储以减小磁盘开销

优化建议

控制范围:我们真的必要返回所有结果么?是否可以考虑按区间分批返回呢?

考虑matchinfo:有些时候我们只需要返回部分查询结果的偏移量或者片段,这个时候我们可以考虑先用带matchinfo的子查询确定我们需要返回偏移量或片段的rowid集,然后再对这个集合内的记录进行深度的offsets或者snippet。因为offsets和snippet需要从磁盘调取整行数据,并作一定的字符串加工,效率较低。这方面大家可以读下SQLite源码。

考虑外部正文:如果你需要索引的内容完全可以从一个必要的外部表中获取,不妨考虑下外部正文。这样就可以有效减小存储正文所需要的磁盘和时间开销。遗憾的是,通过提取iOS版QQ邮箱某个版本的数据文件,我们发现QQ邮箱这方面似乎做得不太好。

我们的困扰

FTS3/FTS4是好东西,但在实际项目中我们发现它们无法完全满足我们的需求:

查询语法过于模糊,容易产生歧义,搜索结果不可控
内建函数可定制性不够
offsets返回值为字符串,多次realloc和字符串转换,效率太低
一定情况下会更费内存
过时,采用FTS5后未来需要升级数据库

于是我们找到了替代它们的神器——FTS5!结合我们自定义的分词器(代号mmfts5),需求终于被完全满足了。

参考

http://www.sqlite.org/fts3.html

网易云免费体验馆,0成本体验20+款云产品!

更多网易技术、产品、运营经验分享请点击

相关文章:
【推荐】 一个内部增长案例的分享
【推荐】 [5.19 线下活动]Docker Meetup杭州站—拥抱Kubernetes,容器深度实践
【推荐】 6本互联网技术畅销书免费送(数据分析、深度学习、编程语言)!

SQLite FTS3/FTS4与一些使用心得的更多相关文章

  1. SQLite数据库_c/s架构的心得

    1.使用是Navicat Premium软件, Microsoft Windows版本. 2.选择SQLite并新建数据库: 3.将建好的SQLite数据库,放到新建的项目的debug文件下中, 并在 ...

  2. FTS5与DIY

    此文已由作者王荣涛授权网易云社区发布. 欢迎访问网易云社区,了解更多网易技术产品运营经验. FTS5简介 前文已经介绍了FTS3/FTS4,本文着重介绍它们的继任者FTS5. FTS5是在SQLite ...

  3. IT四大名著

    标题耸人听闻,sorry. CPU.操作系统.编译器和数据库我都不会.我英语也不行,但我认识所有的字母.:-) 万一有人感兴趣呢?https://sqlite.org/doclist.htmlThe ...

  4. 简单的学习心得:网易云课堂Android开发第六章SQLite与ContentProvider

    一.SQLite 1.基本操作: (1)创建数据库:在SQLiteOpenHelper的子类构造器中创建. (2)创建表:在SQLiteOpenHelper的子类onCreate方法中,调用execS ...

  5. SQLite学习心得

    SQLite是一款很有名气的小型开源跨平台数据库,作为目前最流行的开源嵌入式关系型数据库,在系统结构设计中正在扮演着越来越重要的角色. 本文主要沿着 http://www.cppblog.com/we ...

  6. android操作sqlite数据库及心得

    写这篇文章主要是网上的对sqlite的操作太多且太杂,非常多时候都不能非常好的运用到自己的项目中,结构不清晰,我自己写了一篇适合刚刚接触的人看的操作方法. 近来用android时要将一些数据保存起来, ...

  7. Mysql和sqlite数据库操作心得

    经过最近一段时间的实际工作发现,原来只是认为Mysql和sqlite是分别独立的,数据传输和共享或有障碍,其实这是一个误区.当我们想要将sqlite中的数据存放到mysql中,最好的方法就是利用中间文 ...

  8. Linq To Sqlite使用心得

    若要使用Linq To Sqlite类库,可以安装Devart Linq Connect Model,如图: 新建这个Model就可以和Linq To Sql一样使用Linq模型,下载地址:https ...

  9. Android编程心得-在Assets文件夹中放入.sql文件实现创建SQlite表的操作

    当我们在使用SQLiteOpenHelper时,经常使用db.execSQL(String sql)方法写入对应语句实现创建表的操作,这样的确可以实现业务逻辑.与此同时还有一种更灵活的方法,从asse ...

随机推荐

  1. 3springboot:springboot配置文件(配置文件占位符、Profile、配置文件的加载位置)

    1.配置文件占位符 RaandomValuePropertySourcr:配置文件可以使用随机数     ${random.value}    ${random.int}  ${random.long ...

  2. Java 内部类综述

    转载自:https://blog.csdn.net/justloveyou_/article/details/53245561

  3. C# 对WinForm应用程序的App.config的加密

    默认情况下,我们需要对App.config文件里的connectionStrings片断进行加密处理,ASP.NET IIS 注册工具 (Aspnet_regiis.exe)可以胜任这个工作,但这个工 ...

  4. Zookeeper简介和安装(四)

    一.简介: Zookeeper是一个分布式协调服务,提供的服务如下: 命名服务:类似于DNS,但仅对于节点 配置管理:服务配置信息的管理 集群管理:Dubbo使用Zookeeper实现服务治理 分布式 ...

  5. 设计四个线程,其中两个线程每次对j增加1,另外两个线程对j每次减1,写出程序

    /* * 设计4个线程,其中两个线程每次对j增加1,另外两个线程对j每次减少1.写出程序. */ public class ThreadTest { private int j; public sta ...

  6. datatable去掉表头默认排序

    禁用排序:"ordering":false 某一列禁用排序:"orderable":false 以某一列排序:"order":[[x,&qu ...

  7. Oracle批量删除表格数据

    在开发阶段往Oracle数据库中多个表格中导入了许多测试数据,倘若一张张表执行"truncate table tablename"语句显得十分繁琐.在PL/SQL中可以用代码进行批 ...

  8. Dubbo 安装ZooKeeper环境

    一.在Windows 安装ZooKeeper 1.下载ZooKeeper 2.解压,修改ZooKeeper配置文件 复制一份zoo_sample.cfg文件,改名位zoo.cfg,打开编辑,设置数据保 ...

  9. HTML中汉字空格占位符

    == 普通的英文半角空格   ==   ==   == no-break space (普通的英文半角空格但不换行)   == 中文全角空格 (一个中文宽度)   ==   == en空格 (半个中文 ...

  10. Jquery之倒计时计算

      setInterval(); function setDate(setTime){ var date = new Date();//获取系统当前时间 )+)+"-"+date. ...