FTS5与DIY

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

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

FTS5简介

前文已经介绍了FTS3/FTS4,本文着重介绍它们的继任者FTS5。

FTS5是在SQLite 3.9.0中被引入的,很可惜的是目前很多OS或应用软件都尚未开始使用这个版本或者更新的3.10.x。

注:SQLite 3.9.0中一个非常令人兴奋的版本,除了引入FTS5,还引入了Json1扩展,从此我们可以用它提供的特定函数集直接SQL级操纵列中的JSON而无需“反序列化->修改->序列化”了。

与FTS3/FTS4的不同

建表

  • 创建表时列不能加类型修饰
  • matchinfo=fts3被去掉,使用columnsize=0代替
  • notindexed=被去掉,代之以使用UNINDEXED关键字
  • ICU分词器被去掉。不知道未来是否会支持...
  • compress=、uncompress=和languageid=选项被去掉,而且没有可用的替代功能

SELECT语句

  • MATCH操作符右侧的查询语法更加明确,消除了歧义
  • docid别名支持被取消,现在只能用rowid
  • 全文检索时MATCH操作符左侧必须是表名而不再支持列名
  • FTS5支持ORDER BY rank。rank为一个特殊列,全文检索查询时其值为 bm25()

内建辅助函数发生变化

  • matchinfo()和offsets()函数被去掉,snippet()函数功能也被削弱
  • 支持自定义辅助函数,利用API完全可以构建出被去掉的几个函数的功能,甚至构建更加强大的
  • 内建辅助函数未来将会被进一步改进

其他

  • 当前没有提供与fts4aux表等价的功能
  • FTS3/4 "merge=X,Y"被FTS5 merge command代替
  • FTS3/4 "automerge=X"被FTS5 automerge选项代替

底层实现的不同

  • 倒排表的存储方式发生改变,引用单个词的文档(实例)列表可以被分开存储,这样在查询时可以支持渐进式加载并在某些情况下节省不少内存
  • 索引树合并方式的优化

下面着重针对查询语法和自定义函数、分词器进行详述。

查询

FTS5下MATCH查询语法相对于FTS3/FTS4作了改进。MATCH右侧查询条件的BNF范式可以描述如下:

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

其中string可以是双引号套起来的字符串或者裸词。裸词由连续的以下字符构成:

非ASCII字符
大小写英文字母
数字
下划线
替换符(ASCII/Unicode码点为26)

FTS5下MATCH查询语法更加严谨,对标点符号也更加敏感,其改进带来的一个好处便是减少歧义。同时,在多个列范围内的查询语法也变得相对简单。本文的重点不是讲解查询语法,所以在此不再展开,有兴趣的同学可以从文末给出的链接查看这部分详情。

“简配”的内建函数

当前版本的FTS5模块提供了bm23()、highlight()、snippet()三个内建函数。bm25()函数降低了Okapi BM25函数使用的门槛,算是一种“增配”,而FTS3/FTS4中的snippet()函数现在改名叫highlight()且新函数功能还不如原先强大则算是实打实的“简配”。FTS5下的snippet()函数则提供了命中目标周围单词序列片段的提取,这也算是“简配”,因为这个基本上可以由原版通过参数组合得到。

对于我们项目来说,matchinfo()和offsets()函数的“减配”则是影响最大的!这也坚定了我们使用自定义分词器和自定义辅助函数的决心。

FTS5扩展

SQLite团队在拿掉matchinfo()和offsets()的时候肯定是有考虑的,也确实拿出了切实可行的方案来解决这一问题,那就是更加开发的C API。首先,SQLite提供了三个API分别用于创建自定义分词器、查找当前注册的分词器和创建自定义SQL函数。

typedef struct fts5_api fts5_api;struct fts5_api {    int iVersion;  /* 当前取值为2 */

    /* 创建自定义分词器 */
    int (*xCreateTokenizer)(
        fts5_api *pApi,        const char *zName,        void *pContext,
        fts5_tokenizer *pTokenizer,        void (*xDestroy)(void*)
    );    /* 查找当前注册的分词器 */
    int (*xFindTokenizer)(
        fts5_api *pApi,        const char *zName,        void **ppContext,
        fts5_tokenizer *pTokenizer
    );    /* 创建自定义SQL函数 */
    int (*xCreateFunction)(
        fts5_api *pApi,        const char *zName, /* zName参数指定自定义SQL函数名 */
        void *pContext,
        fts5_extension_function xFunction,        void (*xDestroy)(void*)
    );
};

自定义分词器

要实现自定义分词器,需要实现三个函数xCreate、xDelete和xTokenize。

typedef struct Fts5Tokenizer Fts5Tokenizer;typedef struct fts5_tokenizer fts5_tokenizer;struct fts5_tokenizer {int (*xCreate)(void*, const char **azArg, int nArg, Fts5Tokenizer **ppOut);void (*xDelete)(Fts5Tokenizer*);int (*xTokenize)(Fts5Tokenizer*, 
    void *pCtx,    int flags,              /* 一些以FTS5_TOKENIZE_为前缀的常量标志位,用于指明调用来源 */
    const char *pText, int nText, 
    int (*xToken)(        void *pCtx,         /* xTokenize()函数第二个参数的指针副本 */
        int tflags,         /* 一些以FTS5_TOKEN_为前缀的常量标志位,用于鉴别是否开启同义识别 */
        const char *pToken, /* 指向包含token的buffer */
        int nToken,         /* token大小,单位为字节 */
        int iStart,         /* token在输入文本中的字节偏移量 */
        int iEnd            /* token最后一个字符在输入文本中的偏移量+1 */
    )
);
};

实践中,我们往往会在xCreate中生成上下文,在xDelete中销毁上下文,而xTokenize则是真正实现分词的核心逻辑。当xTokenize被调用时,SQLite给了我们几个参数:

flags用于指定调用来源,是来自创建文档还是全文检索
pText用于制定输入文本
nText用于指明输入文本大小
xToken则是一个回掉函数。当分词器确定一个单词之后,需要调用这个回掉告诉FTS5驱动框架这个单词的信息。

看似简单的过程,实则暗含一些坑,你需要区分是字节还是单词编号,而这些FTS5官方文档中是没有显著说明的。笔者在编写mmfts5时就被坑过,后来是靠阅读SQLite源码结合一定的推理才确定了细节。

下面代码给出了一个非常简单(甚至有些简陋)的分词示例,用以展示xTokenize的实现:

int MyTokenize(Fts5Tokenizer*, 
    void *pCtx,    int flags,    const char *pText, int nText, 
    int (*xToken)(void *, int, const char *, int, int, int)) {    int rc;    int start = -1;    int end;    for (end = 0; end < nText; end++) {        if (isspace(pText[end])) {            if (start != -1) {
                rc = xToken(pCtx, 0, pText, nText, start, end);
                start = -1;                if (rc != SQLITE_OK) {                    return rc;
                }
            }
        } else {            if (start == -1) {
                start = end;
            }
        }
    }    if (start != -1) {        return xToken(pCtx, 0, pText, nText, start, end);
    }    return SQLITE_OK;
}

特别需要注意的是如果xToken返回非SQLITE_OK,则分词过程需要立即终止,我们的mmfts5在查询模式下就利用这一点在找到目标后就停止分词以避免不必要的性能损耗。

自定义SQL函数

根据前文我们可以通过fts5_api->xCreateFunction可以创建并注册自定义SQL函数。自定义SQL函数定义如下:

typedef struct Fts5ExtensionApi Fts5ExtensionApi;typedef struct Fts5Context Fts5Context;typedef struct Fts5PhraseIter Fts5PhraseIter;typedef void (*fts5_extension_function)(    const Fts5ExtensionApi *pApi,   /* API对象本身 */
    Fts5Context *pFts,              /* fts上下文 */
    sqlite3_context *pCtx,          /* 返回值上下文 */
    int nVal,                       /* apVal参数个数 */
    sqlite3_value **apVal           /* 参数列表指针 */);

作为示例,我们编写一个最简单的自定义函数,如下:

void MySQLFunc(const Fts5ExtensionApi *pApi,
                Fts5Context *pFts,
                sqlite3_context *pCtx,                int nVal,
                sqlite3_value **apVal) {
    sqlite3_result_int(pCtx, 0);
}

这个函数永远返回整数0,如果这个函数被注册为名叫“MyFunc”,这意味着你使用类似以下的SQL语句查询message表将得到空集或者一系列只包含0的行。

SELECT MyFunc(message) FROM message WHERE message MATCH ‘中‘

为了让自定义SQL函数有所作为,Fts5ExtensionApi对象提供了丰富的API,这些API足以组合出比matchinfo()、offsets()更加强大的功能。下面利用注释对它们作简要说明:

struct Fts5ExtensionApi {    int iVersion;                   /* 当前固定取值为1 */

    /* 获取自定义函数的上下文,这个在xCreateFunction的第三个参数中给出 */
    void *(*xUserData)(Fts5Context*);    /* 获取表的列的总数 */
    int (*xColumnCount)(Fts5Context*);    /* 获取表的行的总数 */
    int (*xRowCount)(Fts5Context*, sqlite3_int64 *pnRow);    /* 获取第iCol列的单词数,如果iCol为负则返回权标的单词数 */
    int (*xColumnTotalSize)(Fts5Context*, int iCol, sqlite3_int64 *pnToken);    /* 对指定文本进行分词 */
    int (*xTokenize)(Fts5Context*, 
        const char *pText, int nText,        void *pCtx,        int (*xToken)(void*, int, const char*, int, int, int)
    );    /* 返回当前查询表达式中的短语(phrase,见前文)数 */
    int (*xPhraseCount)(Fts5Context*);    /* 返回第iPhrase个短语中的单词数,iPhrase基于0 */
    int (*xPhraseSize)(Fts5Context*, int iPhrase);    /* 返回当前行中命中短语的次数 */
    int (*xInstCount)(Fts5Context*, int *pnInst);    /* 查询当前行第iIdx次命中的详情。piPhrase、piCol、piOff分别返回命中短语编号、命中列、列偏移量 */
    int (*xInst)(Fts5Context*, int iIdx, int *piPhrase, int *piCol, int *piOff);    /* 当前行的rowid */
    sqlite3_int64 (*xRowid)(Fts5Context*);    /* 获取当前行第iCol列的数据 */
    int (*xColumnText)(Fts5Context*, int iCol, const char **pz, int *pn);    /* 获取行某个列单词数,如果iCol为负则返回整行的单词数 */
    int (*xColumnSize)(Fts5Context*, int iCol, int *pnToken);    /* 按rowid升序遍历所有命中第iPhrase个短语的行 */
    int (*xQueryPhrase)(Fts5Context*, int iPhrase, void *pUserData,        int(*)(const Fts5ExtensionApi*,Fts5Context*,void*)
    );    /* 以下两个用于自定义辅助数据操作,用于单次或者多次API间传递数据,xDelete负责销毁数据*/
    int (*xSetAuxdata)(Fts5Context*, void *pAux, void(*xDelete)(void*));    void *(*xGetAuxdata)(Fts5Context*, int bClear);    /* 以下两个用于单词的迭代器操作,对于第iPhrase个短语的访问比较高效、方便但对于全部短语的遍历不太方便 */
    void (*xPhraseFirst)(Fts5Context*, int iPhrase, Fts5PhraseIter*, int*, int*);    void (*xPhraseNext)(Fts5Context*, Fts5PhraseIter*, int *piCol, int *piOff);
};

作为示例,我们再将之前的MySQLFunc修改成如下的自定义函数并注册为“MyRowId”:

void MyRowIdFunc(const Fts5ExtensionApi *pApi,
                Fts5Context *pFts,
                sqlite3_context *pCtx,                int nVal,
                sqlite3_value **apVal) {
    sqlite3_result_int64(pCtx, pApi->xRowid(pCtx));
}

那么执行如下SQL语句将返回空集或者一系列包含“中”的数据行的rowid:

SELECT MyFunc(message) FROM message WHERE message MATCH ‘中‘

总之,FTS5的可定制性非常高,是时候彻底和FTS3/FTS4说再见了~

参考

http://sqlite.org/fts5.html

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

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

相关文章:
【推荐】 Vue框架核心之数据劫持
【推荐】 从疑似华住集团4.93亿开房信息泄露看个人如何预防信息泄露

原文地址:https://www.cnblogs.com/zyfd/p/9803393.html

时间: 2024-10-07 20:51:38

FTS5与DIY的相关文章

F - Free DIY Tour(动态规划)

这道题也可以用深搜做,可以深搜本来就不熟,好久没做早忘了,明天看看咋做的 Description Weiwei is a software engineer of ShiningSoft. He has just excellently fulfilled a software project with his fellow workers. His boss is so satisfied with their job that he decide to provide them a free

酷客多小程序DIY体系全面升级,还加入了这些新功能

在这个追求个性的时代,很多人都不愿追随大流,而是更喜欢DIY.首页模板的DIY功能一直都备受酷客多小粉丝的喜爱,昨晚伴随着扫码点餐一起推出的,还有模板DIY的全新页面.新的DIY界面加入了首页视频.背景.客服三个功能,操作流程相比之前简化了许多,商家只用拖动想要的组件到相应的位置并且链接到相应入口,就能完成模板的设计. 本次新增的DIY首页模板的三个新功能小编逐一介绍一下 1. 首页背景 新增的背景设置功能,商家可以自己设定首页的颜色或者是首页图片,使得小程序首页搭配更和谐,满足大部分追求个性化

【我的技术我做主】IT屌丝DIY打造6盘位家用NAS服务器

一.为什么需要NAS存储? 一直以来用的百度云,并自己配置了一个2TB的硬盘做日常数据备份,后来发现百度云限速!而且存在各种各样的不安全(苹果事件.米国事件的都懂的啦!),而且自己2TB的硬盘一直没有做数据备份一直感觉不安全(搞IT人的心病),没有RAID数据安全无法保证,加上现在给孩子照相越来越多.蓝光高清.各种测试需要存储空间,NAS的需求越来越严重了,所以建立一个自己的NAS存储势在必行!当然在成本.造价.功能考虑,性价比当然是越高越好了! 二.硬件选型 1.主板 u 支持双千兆网口的(端

DIY PCB电路板制作(简单方便快捷)

原作者:步云天地 平时我们自己画了PCB板,但是出于价钱考虑,又不想发出去打板,或者不知道自己画的板是否存在问题,或者自己想DIY一下....(或者..自己想吧...嘻嘻) 过去有感光干膜,金属蚀刻制作,现在我针对感光干膜制作说一下制作过程,,原因是感光干膜制作相对于前两张成本较低些,但是步骤多一些,不过这不重要,相信看了下面的讲解之后,一定会......... 好了,,话不多少,马上切入主题::::::::::::: 首先介绍下制作的材料准备:   产品 数量 单价 金额 1 感光蓝油100克

DIY 空气质量检测表

DIY 空气质量检测表 前几天逛淘宝看到有空气颗粒物浓度测量的传感器,直接是 3.3V TTL 电压串口输出的,也不贵,也就 100 多一点.觉得挺好就买了个,这两天自己捣鼓了个小程序,搞了个软件界面的空气质量检测表.程序写的很简单,但是感觉这个小软件还是挺实用的,所以就写篇博客,大家用我的代码很容易就自己 DIY 一套. 硬件准备 传感器用的是 攀藤科技 PMS7003M .除了攀藤科技,还有几家这种传感器做的应该也不错,不过我没去用过,也没仔细调研.(之所以用的这家,不过是因为在 newsm

DIY你的低成本ROS机器人

ROS入门课程上线一个月以来,收到了很多童鞋们的来信,大家普遍反映学习之后能够快速的入门ROS,也能够在仿真环境中进行slam_gmapping,导航等.但是对于那些没有机器人平台的同学离真正操作ROS机器人还有一些疑惑,目前ROS机器人普遍的价格都在2500以上,(以淘宝某款与笔者采用相同配置,设计思路相同的机器人相比,能够节约近一大半的费用),对于预算有限的学生或者工薪阶层还是比较昂贵的,这篇博客笔者就带大家DIY一个ROS机器人,预算大约为500元(不包括激光雷达),下面是我挑选的硬件(为

从零DIY机械键盘/主控方案

自从有了第一套机械键盘,先后修改了接口方案,安装了LED灯等,但是始终无法满足自己的DIY欲望. 于是想到最简单的方法就是用现成的主控,而主控来源于废弃的键盘,如下图: 这种主控也是矩阵方式,只需要测出需要的相应键位然后焊接好就行,完成图如下 采用了o 5脚红轴机械轴..玩lol的朋友应该熟悉这些键位~ 但是我仍然想做一个60/88/104的键盘,同时能够自己编程写入不同的组合键 实现不同的功能,甚至可以实现全键无冲. 使用现成的主控方案完全不能满足以上想法,于是就打算自己从主控开始. 关于ke

一起talk C栗子吧(第六十六回:C语言实例--DIY字符串比较函数)

各位看官们,大家好,上一回中咱们说的是DIY字符串连接函数的例子,这一回咱们说的例子是:DIY字符串比较函数.闲话休提,言归正转.让我们一起talk C栗子吧! 我们在前面的章回中介绍过字符串比较函数,时间不长,但是有些看官已经忘记了,为了加深看官们对字符串比较函数的印象,我们准备DIY字符串比较函数.Just do it by yourself! 我们在前面的章回中一共介绍了两个字符串比较函数:strcmp,strncmp.接下来我们分别介绍如何DIY这两个字符串比较函数. DIY strcm

我有DIY一Android遥控-所有开源

1.试用 记得宋宝华在「设备驱动开发具体解释」提出一个这种理论「软件和硬件互相渗透对方的领地」,这次证明还是确实是这样,使用上层APP软件加上简单的更为简单的硬件设计就能够完毕一个遥控器了. 有开发应用程序网友发E-mail问网上那种DIY的红外遥控器是怎样工作的.查了一下眼下有两种方式,一种是基于USB一种是基于耳机孔.就简单的回复了一信息. 说是将音频信号/USB信号转换为红外信号. 事后自己都认为有点敷衍,还好自己硬件了解一些.软件也会开发.就将他们结合一下. 试用次合,如今开发规律比較清