-
Notifications
You must be signed in to change notification settings - Fork 1.4k
C++ 全文搜索
全文搜索(Full-Text-Search,简称 FTS),是 SQLite 提供的功能之一。它支持更快速、更便捷地搜索数据库内的文本内容,常用于应用内的全局搜索等功能。
WCDB 内建了全文搜索的支持,对中文、日文等非空格分割的语言做了针对性的优化;对英文做了词性还原,使搜索不受词形、时态的限制,从而使搜索更加精确。同时还支持了区分符号、中文简繁体转换、拼音搜索等中文场景的搜索强化能力。
虚拟表是 SQLite 的一个特性,可以更自由地自定义数据库操作的行为。在模型绑定一章,我们提到了虚拟表映射,但没有具体介绍。而在全文搜索中,它是不可或缺的一部分。
// SampleFTS.hpp
class SampleFTS {
public:
int identifier;
std::string content;
std::string summary;
WCDB_CPP_ORM_DECLARATION(SampleFTS)
};
// SampleFTS.cpp
WCDB_CPP_ORM_IMPLEMENTATION_BEGIN(SampleFTS)
WCDB_CPP_SYNTHESIZE(identifier)
WCDB_CPP_SYNTHESIZE(content)
WCDB_CPP_SYNTHESIZE(summary)
WCDB_CPP_UNINDEXED(identifier) //设置identifier列不建立fts索引
WCDB_CPP_VIRTUAL_TABLE_MODULE(WCDB::Module::FTS5) //设置fts版本
WCDB_CPP_VIRTUAL_TABLE_TOKENIZE(WCDB::BuiltinTokenizer::Verbatim) //设置分词器
WCDB_CPP_ORM_IMPLEMENTATION_END
// 注册本数据库需要用到的分词器
database.addTokenizer(WCDB::BuiltinTokenizer::Verbatim);
// 创建虚表
bool ret = database.createVirtualTable<SampleFTS>("sampleVirtualTable"); // 数据库此时会被自动打开
全文搜索的虚拟表映射一般只需定义FTS版本和分词器即可,这里还使用FTS专用的列约束WCDB_CPP_UNINDEXED
设置identifier
这个不参与全文搜索的列不建索引,这样可以节省空间。定义完成后,先调用addTokenizer
接口往数据库中注册分词器,这个操作要在使用到这个分词器之前执行,然后再调用 createVirtualTable
接口根据字段映射和虚拟表映射创建虚拟表。
全文搜索的速度依赖于其索引。
SampleFTS english;
english.identifier = 1;
english.content = "WCDB is a cross-platform database framework developed by WeChat.";
english.summary = "WCDB is an efficient, complete, easy-to-use mobile database framework used in the WeChat application. It can be a replacement for Core Data, SQLite & FMDB.";
SampleFTS chinese;
chinese.identifier = 2;
chinese.content = "WCDB 是微信开发的跨平台数据库框架";
english.summary = "WCDB 是微信中使用的高效、完整、易用的移动数据库框架。它可以作为 CoreData、SQLite 和 FMDB 的替代。";
ret &= database.insertObjects<SampleFTS>({english, chinese}, "sampleVirtualTable");
建立索引的操作与普通表插入数据基本一致。
全文搜索与普通表不同,必须使用 match
函数进行查找。
WCDB::Optional<SampleFTS> objectMatchFrame = database.getFirstObject<SampleFTS>("sampleVirtualTable",
WCDB_FIELD(SampleFTS::content).match("frame*"));
printf("%s", objectMatchFrame.value().content.c_str()); // 输出 "WCDB is a cross-platform database framework developed by WeChat."
// 词形还原特性,通过 "efficiency" 也可以搜索到 "efficient"
WCDB::Optional<SampleFTS> objectMatchEffiency = database.getFirstObject<SampleFTS>("sampleVirtualTable",
WCDB_FIELD(SampleFTS::summary).match("efficiency"));
printf("%s", objectMatchEffiency.value().summary.c_str()); // 输出 "WCDB is an efficient, complete, easy-to-use mobile database framework used in the WeChat application. It can be a replacement for Core Data, SQLite & FMDB."
SQLite 分词必须从首字母查起,如"frame*",而类似"*amework"这样从单词中间查起是不支持的。
全文搜索中有一列隐藏字段,它与表名一致。通过它可以对全表的所有字段进行查询。
WCDB::Column tableColumn = WCDB::Column("sampleVirtualTable");
WCDB::OptionalValueArray<SampleFTS> objects = database.getAllObjects<SampleFTS>("sampleVirtualTable", tableColumn.match(@"SQLite"));
printf("%s", objects.value()[0].summary.c_str()); // 输出 "WCDB is an efficient, complete, easy-to-use mobile database framework used in the WeChat application. It can be a replacement for Core Data, SQLite & FMDB."
printf("%s", objects.value()[1].summary.c_str()); // 输出 "WCDB 是微信中使用的高效、完整、易用的移动数据库框架。它可以作为 CoreData、SQLite 和 FMDB 的替代。"
分词器是全文搜索的关键模块,它实现将输入内容拆分成多个Token并提供这些Token的位置,搜索引擎再对这些Token建立索引。
SQLite的FTS组件有提供内置的分词器,同时还支持自定义分词器。WCDB 在 SQLite 原有分词器的基础上,自己实现了下面三个分词器:
-
WCDB::BuiltinTokenizer::OneOrBinary
和WCDB::BuiltinTokenizer::LegacyOneOrBinary
,用于FTS3,这两个分词器只是名字不一样,逻辑是一样的。 -
WCDB::BuiltinTokenizer::Verbatim
,用于 FTS5,逻辑上和WCDB::BuiltinTokenizer::OneOrBinary
基本一致。 -
WCDB::BuiltinTokenizer::Pinyin
,用于 FTS5,可以实现拼音搜索。使用时需要使用static Database::configPinyinConverter()
接口配置汉字到拼音的映射表。
WCDB::BuiltinTokenizer::OneOrBinary
和WCDB::BuiltinTokenizer::Verbatim
的用法和功能基本一样,上面已经有示例,这里就不再补充。下面通过例子介绍一下拼音搜索的实现方法:
class PinyinObject {
public:
std::string content;
WCDB_CPP_ORM_DECLARATION(PinyinObject)
};
// SampleFTS.cpp
WCDB_CPP_ORM_IMPLEMENTATION_BEGIN(PinyinObject)
WCDB_CPP_SYNTHESIZE(content)
WCDB_CPP_VIRTUAL_TABLE_MODULE(WCDB::Module::FTS5)
WCDB_CPP_VIRTUAL_TABLE_TOKENIZE(WCDB::BuiltinTokenizer::Pinyin)//配置 Pinyin 分词器
WCDB_CPP_ORM_IMPLEMENTATION_END
// 创建汉字拼音映射表,支持配置多音字
std::map<WCDB::UnsafeStringView, std::vector<WCDB::UnsafeStringView>> pinyinDict = {
{ "单", { "shan", "dan", "chan" } },
{ "于", { "yu" } },
{ "骑", { "qi" } },
{ "模", { "mo", "mu" } },
{ "具", { "ju" } },
{ "车", { "che" } }
};
// 将拼音映射应用到拼音转换逻辑
WCDB::Database::configPinyinConverter([=](const WCDB::UnsafeStringView &token) {
if (pinyinDict.find(token) == pinyinDict.end()) {
return std::vector<WCDB::StringView>();
}
auto pinyins = pinyinDict.at(token);
std::vector<WCDB::StringView> result;
for (auto pinyin : pinyins) {
result.emplace_back(pinyin);
}
return result;
});
// 注册拼音分词器
database.addTokenizer(WCDB::BuiltinTokenizer::Pinyin);
//创建虚表
ret = database.createVirtualTable<PinyinObject>("pinyinTable");
//写入数据
PinyinObject object;
object.content = "单于骑模具单车";
ret &= database.insertObjects<PinyinObject>(object, "pinyinTable");
//支持多音字搜索、拼音首字母搜索和拼音前缀搜索
std::vector<std::string> querys = {
"\"shan yu qi mu ju dan che\"",
"\"chan yu qi mo ju shan che\"",
"\"dan yu qi mo ju chan che\"",
"\"dan yu qi mu ju ch\"*",
"\"dan yu qi mo ju d\"*",
"\"s y q m j d c\"",
"\"c y q m j s c\"",
"\"c y q m j\"",
};
for(std::string &query : querys) {
WCDB::OptionalValueArray<PinyinObject> objects = database.getAllObjects<PinyinObject>("pinyinTable", WCDB_FIELD(PinyinObject::content).match(query));
printf("%s", objects.value()[0].content.c_str()); // 都是输出 单于骑模具单车
}
使用了拼音分词器之后,无法再用原内容来搜索,只能搜索拼音。如果需要支持原文搜索的话,需要再另外建一个FTS表来支持。在两个表的情况下,可以使用
WCDB_CPP_VIRTUAL_TABLE_EXTERNAL_CONTENT
来配置只保存一份原文,减小空间占用,原理见[SQLite External Content Tables][SQLite-External-Content-Tables]。
现有的分词器还可以传入参数来做一些配置,需要添加参数的分词器需要使用WCDB_CPP_VIRTUAL_TABLE_TOKENIZE_WITH_PARAMETERS
宏来配置。WCDB 实现的WCDB::BuiltinTokenizer::OneOrBinary
和WCDB::BuiltinTokenizer::Verbatim
两个分词器有下面三个可配置参数:
-
WCDB::BuiltinTokenizer::Parameter::NeedSymbol
,WCDB的分词器默认不会对符号进行分词,所以搜索符号是搜不到的。如果需要支持搜索符号,需要配置这个参数。 -
WCDB::BuiltinTokenizer::Parameter::SimplifyChinese
配置了之后可以支持用简体汉字来搜索繁体汉字,不过需要使用static Database::configTraditionalChineseConverter()
来配置简繁体汉字映射表。 -
WCDB::BuiltinTokenizer::Parameter::SkipStemming
关闭英文单词的词性还原功能。
这三个配置参数可以叠加配置。
下面以简体汉字搜繁体汉字为例,介绍分词器参数的用法:
// SampleFTS.hpp
class SampleFTS {
public:
std::string content;
WCDB_CPP_ORM_DECLARATION(SampleFTS)
};
// SampleFTS.cpp
WCDB_CPP_ORM_IMPLEMENTATION_BEGIN(SampleFTS)
WCDB_CPP_SYNTHESIZE(content)
WCDB_CPP_VIRTUAL_TABLE_MODULE(WCDB::Module::FTS5) //设置fts版本
// 设置分词器, 并配置支持简体搜繁体
// 这个宏可以同时配置多个参数
WCDB_CPP_VIRTUAL_TABLE_TOKENIZE_WITH_PARAMETERS(WCDB::BuiltinTokenizer::Verbatim, WCDB::BuiltinTokenizer::Parameter::SimplifyChinese)
WCDB_CPP_ORM_IMPLEMENTATION_END
//配置简繁体汉字映射表,需要在建索引和搜索前设置
WCDB::Database::configTraditionalChineseConverter([](const WCDB::UnsafeStringView &token) {
if (token.compare("們") == 0) {
return WCDB::StringView("们");
} else if (token.compare("員") == 0) {
return WCDB::StringView("员");
}
return WCDB::StringView(token);
});
//给数据库注册分词器
database.addTokenizer(WCDB::BuiltinTokenizer::Verbatim);
//创建虚表
ret = database.createVirtualTable<SampleFTS>("sampleVirtualTable");
//建索引
SampleFTS object;
object.content = "我們是程序員";
database.insertObjects<SampleFTS>(object, "sampleVirtualTable");
//可以使用繁体来搜索
WCDB::Optional<SampleFTS> matchedObject1 = database.getFirstObject<SampleFTS>("sampleVirtualTable", WCDB_FIELD(SampleFTS::content).match("我們是程序員"));
printf("%s", matchedObject1.value().content.c_str());// 输出繁体原文 我們是程序員
//也可以使用简体来搜索
WCDB::Optional<SampleFTS> matchedObject2 = database.getFirstObject<SampleFTS>("sampleVirtualTable", WCDB_FIELD(SampleFTS::content).match("我们是程序员"));
printf("%s", matchedObject2.value().content.c_str());// 输出繁体原文 我們是程序員
开发者除了可以使用内置的分词器,还可以根据自己的需求自定义分词器。自定义的分词器需要继承 WCDB::AbstractFTSTokenizer
这个基类来实现,它的原型如下:
class AbstractFTSTokenizer {
public:
// 参数传入的是上一节提到的分词器配置参数
// 其中 pCtx 参数只在 FTS5中有用。
AbstractFTSTokenizer(const char *const *azArg, int nArg, void *pCtx);
virtual ~AbstractFTSTokenizer() = 0;
// 每次要分词一个新内容时,都会调用这个接口将内容传入。
// 其中 flags 参数只在FTS5中会有值,可能是下面几种值中的一个或几个合并值,用来描述分词的场景:
// FTS5_TOKENIZE_QUERY 0x0001 查询时分词
// FTS5_TOKENIZE_PREFIX 0x0002 前缀搜索分词
// FTS5_TOKENIZE_DOCUMENT 0x0004 建索引时分词
// FTS5_TOKENIZE_AUX 0x0008 搜索辅助函数调用分词
virtual void loadInput(const char *pText, int nText, int flags) = 0;
// 回调分词结果。每次上层逻辑需要获取下个Token时,会调用这个接口
// 如果当前内容已经分词完毕,则返回 WCDB::ErrorCodeDone;
// 还有下个Token则返回 WCDB::ErrorCodeOK;
// 出错则返回其他错误码。
virtual int nextToken(const char **ppToken,// 保存下个Token的指针
int *nToken, //下个Token的字节长度
int *iStart, //下个Token在原文中的字节起始位置
int *iEnd, //下个Token在原文中的字节结束位置
int *tflags, //只在 FTS5 中有效,同个文本有多个同义词 Token 时,第二 Token 开始 tflags 要赋值为 FTS5_TOKEN_COLOCATED
int *iPosition//只在 FTS3/4中有效,表示 Token 位置
)
= 0;
};
自定义了分词器之后,还需要使用static Database::registerTokenizer()
接口将新分词器注册到 WCDB 中才能使用,下面自定义一个以空格为分割符来分词的简单分词器,来说明自定义分词器的方法:
class SimpleTokenizer : public WCDB::AbstractFTSTokenizer {
public:
SimpleTokenizer(const char *const *azArg, int nArg, void *pCtx)
: WCDB::AbstractFTSTokenizer(azArg, nArg, pCtx) {
}
void loadInput(const char *pText, int nText, int flags) override final {
// 保存待分词内容,文本不用拷贝
input = pText;
inputLength = nText;
// 复位其他状态变量
lastLocation = 0;
position = 0;
}
int nextToken(const char **ppToken, int *nToken, int *iStart, int *iEnd, int *tflags, int *iPosition) {
if(input == nullptr || inputLength <= lastLocation) {
// 分词结束
return WCDB::ErrorCodeDone;
}
// 记录下个 Token 的字节起止位置
int start = lastLocation;
int end = lastLocation;
for(int i = lastLocation; i < inputLength; i++) {
if( input[i] == ' ' ) {// 匹配空格
if( start == end ) {
// 起始遇到连续空格,跳过
start = i + 1;
end = start;
} else {
// 找到当前 Token 的结尾
break;
}
} else {
// 记录当前位置,继续往下遍历
end = i;
}
}
if( start == end ) {
// 没有下个 Token 了,结束分词
return WCDB::ErrorCodeDone;
}
// 记录当前 Token 的结束位置,作为下个分词的遍历起点
lastLocation = end;
*ppToken = input + start;
*nToken = end - start;
*iStart = start;
*iEnd = end;
position++;
if( iPosition != nullptr ) {
*iPosition = position;
}
return WCDB::ErrorCodeOK;
}
private:
const char* input;
size_t inputLength;
int lastLocation;
int position;
};
// 如果分词器是用于 FTS3/4 的,使用 FTS3TokenizerModuleTemplate 来注册到 WCDB
WCDB::Database::registerTokenizer("FTS3SimpleTokenizer", WCDB::FTS3TokenizerModuleTemplate<SimpleTokenizer>::specialize());
// 如果分词器是用于 FTS5 的,使用 FTS5TokenizerModuleTemplate 来注册到 WCDB
// 其中 specializeWithContext 的入参,会作为分词器构造函数中的 pCtx 参数来作用到分词器;这个参数可以用来串联上下文
WCDB::Database::registerTokenizer("FTS5SimpleTokenizer", WCDB::FTS5TokenizerModuleTemplate<SimpleTokenizer>::specializeWithContext(nullptr));
分词器的名字是全局唯一的,FTS3/4的分词器不能和 FTS5 的分词器重名,也不能和已有的分词器重名。
未完待续。
- 欢迎使用 WCDB
- 基础教程
- 进阶教程
- 欢迎使用 WCDB
- 基础教程
- 进阶教程
- 欢迎使用 WCDB
- 基础教程
- 进阶教程
- 欢迎使用 WCDB
- 基础教程
- 进阶教程