前言

MySQL 从 5.7.6版本开始内置了ngram全文解析器,支持了中文、日文、韩文分词等东亚汉字文化圈的语言索引。但是MongoDB直到4.x的版本都不能在社区版为中文提供良好的全文检索支持。(据说商业版是已经支持了)

所以要想在MongoDB中使用中文全文检索的话,要不然就借助外部的检索工具,比如常见的ElasticSearch等,但是毕竟ElasticSearch是用Java写的,需要大量的资源。(我们都用MongoDB了,结果检索引擎比数据库都麻烦)这显然不是我们(这些没有对这个有非常大的需求的)想要的效果。

当然我们说虽然MySQL已经支持了,但肯定仍然撼动不了ElasticSearch等专门搞这个的的地位

事实上,MongoDB在2.x版本就已经支持了英文等部分拉丁文的全文检索。他们最大的特点是以空格分词。所以我们可以巧用这一特性去模拟一下。如何去做呢,那就是手动分词。

手动分词

我们选用nodejieba进行分词,nodejieba底层是用C++实现的,性能很好。当然还有一个用Rust语言实现的版本,速度比较快(但是在保存数据时可能会有问题,我猜可能是因为并发的问题)顺便一提nodejieba对香港繁体/台湾正体支持不好,可以选择使用OpenCC提前转换一下字典,或是边分词边转换。

值得一提的是nodejieba提供的API都是阻塞的同步API,但是nodejieba并不涉及很慢的IO操作,只是计算操作

1、安装nodejieba

yarn add nodejieba

2、学习分词,可以见到效果还是很好的。

console.log(nodejieba.cut('不起眼女主角培育法', true)) //[ '不起眼', '女主角', '培育法' ]

3、nodejieba也提供了提取关键词的API,事实上我在查询的时候我发现在保存数据时保存关键词比分词准确率更精准一点。见后文

console.log(nodejieba.extract('不起眼女主角培育法', 10))

得到的结果是

[
  { word: '培育法', weight: 13.2075304714 },
  { word: '女主角', weight: 9.74179456862 },
  { word: '不起眼', weight: 9.6811699468 }
]

Mongoose保存数据

由于我使用了TypeScript,所以我会配合Typegoose使用,而不是使用Mongoose默认的Schema

1、建立接口,在这里我为key_words字段建立了全文索引。

@modelOptions({options: {customName: 'test_collection'}})
@index(key_words: 'text'})
export class IBook2 {
    @prop({trim: true})
    name!: string;

    @prop({type: () => [String]})
    key_words!: string[];
}

2、分词

const Book2 = getModelForClass(IBook2);
const converterToZH = new OpenCC('tw2s.json');//这里使用了OpenCC进行正体到简体转换
export const createTextSegmentation = async (str: string) => {
    str = (await converterToZH.convertPromise(str)).toLowerCase();
    return nodejieba.extract(str, 10).map(v => v.word) || [str];
}

3、保存数据

const key_words = await createTextSegmentation('不起眼女主角培育法')
await Book2.create({
        name: '不起眼女主角培育法',
        key_words
});

查询数据

通过索引查询,所以还是比较省时间的。

const key_words = await createTextSegmentation('不起眼女主角培育法')
const result = await Book2.find(
        {
            $text: {
                $search: query
            }
        }, {
            score: {$meta: "textScore"}
        }
    ).sort({score: {$meta: "textScore"}});
console.log(result)

检索权重