探秘Fuse模糊搜索原理和评分机制
Fuse.js 是一个轻量级的模糊搜索库,支持多种搜索功能。
核心能力:多样化搜索支持
| Token | Match type | Description |
|---|---|---|
jscript |
fuzzy-match | Items that fuzzy match jscript |
=scheme |
exact-match | Items that are scheme |
'python |
include-match | Items that include python |
!ruby |
inverse-exact-match | Items that do not include ruby |
^java |
prefix-exact-match | Items that start with java |
!^earlang |
inverse-prefix-exact-match | Items that do not start with earlang |
.js$ |
suffix-exact-match | Items that end with .js |
!.go$ |
inverse-suffix-exact-match | Items that do not end with .go |
- 逻辑查询操作:支持复杂逻辑组合查询。
{
$and: [
{ title: 'old war' }, // Fuzzy "old war"
{ color: "'blue" }, // Exact match for blue
{
$or: [
{ title: '^lock' }, // Starts with "lock"
{ title: '!arts' } // Does not have "arts"
]
}
]
}
基础用法
以下是一个使用 Fuse.js 进行扩展搜索的示例:
const list = [
{ text: 'hello word' },
{ text: 'how are you' },
{ text: 'indeed fine hello foo' },
{ text: 'I am fine' },
{ text: 'smithee' },
{ text: 'smith' }
];
const options = {
useExtendedSearch: true,
includeMatches: true,
includeScore: true,
threshold: 0.5,
minMatchCharLength: 4,
keys: ['text']
};
const fuse = new Fuse(list, options);
// 查询精确匹配 "smith"
const result = fuse.search('=smith');
console.log(result);
主流程
原理分析
1. 初始化阶段
在初始化阶段,Fuse.js 会创建索引并处理关键参数:
- KeyStore:存储字段路径及其权重(
key\_weight / total\_key\_weight),权重的计算会影响最终匹配得分。
{
_keys: [
{
path: [
"title",
],
id: "title",
weight: 0.3333333333333333,
src: "title",
getFn: null,
},
...
],
_keyMap: {
title: {
path: [
"title",
],
id: "title",
weight: 0.3333333333333333,
src: "title",
getFn: null,
},
...
},
}
- 归一化指标 (Norm):衡量字段长度对搜索权重的影响,公式如下:
norm = 1 / Math.pow(numTokens, 0.5 * weight)
说明:numTokens 为分词数量,weight 为字段权重。
norm 是在搜索引擎索引中为每个文档或词项计算的一个归一化值,它用于调整文档的相关性,确保搜索结果排序的公平性和准确性。它考虑了文档长度、词频、字段权重等多个因素,帮助提高搜索引擎的检索效率和精确度。Fuse.js 的分词逻辑对中文支持较差,不支持定制分词器❌
2. 搜索匹配阶段
2.1核心流程
根据输入的关键词以及文档类型,选择不同处理方式。内部实现大体一致:创建searcher搜索器,然后根据关键词选择对应的matcher匹配器完成搜索:
let results = isString(query)
? isString(this._docs[0])
? this._searchStringList(query)
: this._searchObjectList(query)
: this._searchLogical(query);
2.2搜索器设计
创建searcher的逻辑如下:
export function createSearcher(pattern, options) {
for (let i = 0, len = registeredSearchers.length; i < len; i += 1) {
let searcherClass = registeredSearchers[i]
if (searcherClass.condition(pattern, options)) {
return new searcherClass(pattern, options)
}
}
return new BitapSearch(pattern, options)
}
这里有两个很巧妙的设计:
- 打包时控制registeredSearchers实现拆包逻辑,减小包体积:通过 registeredSearchers 来控制加载哪些搜索器,在打包时只选择必要的搜索器,避免不需要的代码被加载,从而减小最终包体积。
- 工厂、策略模式实现扩展搜索:通过工厂模式根据查询模式选择不同的搜索器,结合策略模式来管理各种匹配策略。
// ❗Order is important. DO NOT CHANGE.
const searchers = [
ExactMatch,
IncludeMatch,
PrefixExactMatch,
InversePrefixExactMatch,
InverseSuffixExactMatch,
SuffixExactMatch,
InverseExactMatch,
FuzzyMatch
]
const searchersLen = searchers.length
// Regex to split by spaces, but keep anything in quotes together
const SPACE_RE = / +(?=(?:[^\"]*\"[^\"]*\")*[^\"]*$)/
const OR_TOKEN = '|'
// Return a 2D array representation of the query, for simpler parsing.
// Example:
// "^core go$ | rb$ | py$ xy$" => [["^core", "go$"], ["rb$"], ["py$", "xy$"]]
export default function parseQuery(pattern, options = {}) {
return pattern.split(OR_TOKEN).map((item) => {
let query = item
.trim()
.split(SPACE_RE)
.filter((item) => item && !!item.trim())
let results = []
for (let i = 0, len = query.length; i < len; i += 1) {
const queryItem = query[i]
// 1. Handle multiple query match (i.e, once that are quoted, like `"hello world"`)
let found = false
let idx = -1
while (!found && ++idx < searchersLen) {
const searcher = searchers[idx]
// 正则匹配对应的 searcher
let token = searcher.isMultiMatch(queryItem)
if (token) {
results.push(new searcher(token, options))
found = true
}
}
if (found) {
continue
}
// 2. Handle single query matches (i.e, once that are *not* quoted)
idx = -1
while (++idx < searchersLen) {
const searcher = searchers[idx]
// 正则匹配对应的 searcher
let token = searcher.isSingleMatch(queryItem)
if (token) {
results.push(new searcher(token, options))
break
}
}
}
return results
})
}
2.3搜索匹配与得分机制
Fuse.js 中的不同匹配类型(如精确匹配、包含匹配、模糊匹配等)都实现了 search 方法来进行关键词匹配。在匹配时,score 用于衡量匹配的质量,score 越小,表示匹配度越高。
// ExactMatch
search(text) {
const isMatch = text === this.pattern
return {
isMatch,
score: isMatch ? 0 : 1,
indices: [0, this.pattern.length - 1]
}
}
// IncludeMatch
search(text) {
let location = 0
let index
const indices = []
const patternLen = this.pattern.length
// Get all exact matches
while ((index = text.indexOf(this.pattern, location)) > -1) {
location = index + patternLen
indices.push([index, location - 1])
}
const isMatch = !!indices.length
return {
isMatch,
score: isMatch ? 0 : 1,
indices
}
}
//...
FuzzyMatch采用 BitMap 算法实现,作为兜底逻辑
3. 匹配结果处理
- 最终得分计算由以下公式计算,得分越接近 0,匹配度越高:
export default function computeScore(
results,
{ ignoreFieldNorm = Config.ignoreFieldNorm }
) {
results.forEach((result) => {
let totalScore = 1
result.matches.forEach(({ key, norm, score }) => {
const weight = key ? key.weight : null
totalScore *= Math.pow(
score === 0 && weight ? Number.EPSILON : score,
(weight || 1) * (ignoreFieldNorm ? 1 : norm)
)
})
result.score = totalScore
})
}
搜索引擎的理论
搜索引擎的核心是通过构建高效的索引结构和评分机制来优化信息检索过程。Fuse.js 在此基础上做了针对小型数据集的性能优化和功能扩展。以下从搜索引擎的原理角度,结合 Fuse.js 的实现,探讨其关键理论与实践。
索引机制
索引是搜索引擎提升检索效率的核心。Fuse.js 在初始化时构建了基于字段的轻量级索引,同时通过归一化操作确保评分的公平性。
索引的基本组成
Fuse.js 的索引包含字段路径、字段权重和分词信息,用于快速定位匹配结果:
{
_keys: [
{
path: ["title"],
id: "title",
weight: 0.333,
src: "title",
getFn: null,
},
...
]
}
- 字段路径(path) :表示在数据对象中定位字段的路径。
- 字段权重(weight) :字段在搜索中的相对重要性(影响最终得分)。
- 归一化(norm) :Fuse.js 使用归一化因子修正字段的长度影响,以确保短字段不会过分主导搜索结果。
归一化的计算公式
在 Fuse.js 中,归一化公式为:
- numTokens:字段分词后的长度。
- weight:字段的权重。
归一化的核心思想源自信息检索领域的 TF-IDF(词频-逆文档频率) 模型,其目的是平衡字段长度对评分的影响。
搜索流程
Fuse.js 的搜索过程分为以下几步:
- 查询解析:将用户输入的关键词解析为一个逻辑结构,支持多种匹配规则(如模糊匹配、前缀匹配等)。
- 匹配器选择:通过
createSearcher方法动态选择适合的匹配器,并根据查询模式初始化。 - 逐项匹配:遍历数据集中的每个记录项,使用匹配器对目标字段进行匹配。
- 评分计算:根据匹配结果和权重计算最终得分。
评分机制
评分机制是搜索引擎用来衡量结果相关性的核心。Fuse.js 结合了字段权重和归一化因子,以确保评分结果的准确性。
Fuse.js 中的总评分公式为:
- score_i:字段的匹配得分(越接近 0,相关性越高)。
- weight_i:字段的权重(默认为 1,可自定义)。
- norm_i:字段的归一化因子,调整字段长度对得分的影响。
Fuse.js 的评分机制强调结果排序的相对性,即使某些字段完全匹配,也会综合其他字段的相关性,确保整体排序更加合理。
Bitap 算法的作用
模糊匹配在搜索引擎中是提高用户体验的重要功能,Fuse.js 使用的 Bitap 算法能够快速计算编辑距离,解决拼写错误或不精确输入的问题。\
Bitap 算法的作用
模糊匹配在搜索引擎中是提高用户体验的重要功能,Fuse.js 使用的 Bitap 算法能够快速计算编辑距离,解决拼写错误或不精确输入的问题。
Bitap 算法核心原理
- 位图表示:将文本和模式转化为位图,通过位操作高效计算匹配结果。
- 编辑距离:通过调整位图,支持指定的最大编辑距离,从而实现模糊匹配。
- 渐进式优化:Bitap 通过提前终止无效匹配,减少无用计算。
Bitap 算法的高效性使其成为小型数据集模糊搜索的最佳选择,但对大规模数据集可能存在性能瓶颈。❌
多条件查询与逻辑操作
Fuse.js 提供了 $and、$or 等逻辑操作符,允许用户构建复杂的多条件查询。
这种设计源于布尔检索模型,其核心思想是通过逻辑表达式组合多个查询条件,从而提升搜索结果的准确性。

浙公网安备 33010602011771号