MiniSearch
MiniSearch
是一个用 JavaScript 编写的小巧但功能强大的内存全文搜索引擎。它对资源利用很有效率,可以在 Node 和浏览器中轻松运行。
试试这个演示应用。
完整的文档和 API 参考在这里,更多关于 MiniSearch
的背景信息,包括与其他类似库的比较,可以查看这篇博客文章。
MiniSearch
遵循语义化版本,并在更新日志中记录发布和变更。
使用场景
MiniSearch
适用于需要全文搜索功能(如前缀搜索、模糊搜索、排名、字段提升等)但索引数据可以存储在进程内存中的场景。虽然你无法用它来索引整个互联网,但有很多用例可以很好地使用 MiniSearch
。通过将索引存储在本地内存中,MiniSearch
可以离线工作,并且可以快速处理查询,无需网络延迟。
一个突出的用例是网页和移动应用中的实时"边输入边搜索",将索引保存在客户端可以实现快速响应的用户界面,无需向搜索服务器发送请求。
特性
-
内存效率高的索引,专为移动浏览器等内存受限的使用场景设计。
-
精确匹配、前缀搜索、模糊匹配、字段提升。
-
自动建议引擎,用于搜索查询的自动完成。
-
现代化的搜索结果排名算法。
-
可随时向索引中添加和删除文档。
-
零外部依赖。
MiniSearch
致力于提供简单的 API,为构建自定义解决方案提供基础构建块,同时保持小巧且经过充分测试的代码库。
安装
使用 npm
:
npm install minisearch
使用 yarn
:
yarn add minisearch
然后在你的项目中 require
或 import
:
// 如果你使用 import:
import MiniSearch from 'minisearch'
// 如果你使用 require:
const MiniSearch = require('minisearch')
或者,如果你更喜欢使用 <script>
标签,你可以从 CDN 引入 MiniSearch:
<script src="https://cdn.jsdelivr.net/npm/minisearch@7.1.0/dist/umd/index.min.js"></script>
在这种情况下,MiniSearch
将作为全局变量出现在你的项目中。
最后,如果你想手动构建库,克隆仓库并运行 yarn build
(或运行 yarn build-minified
获取压缩版本 + 源映射)。编译后的源码将在 dist
文件夹中创建(提供 UMD、ES6 和 ES2015 模块版本)。
使用方法
基本用法
// 一个用于示例的文档集合
const documents = [
{
id: 1,
title: '白鲸记',
text: '叫我伊什梅尔吧。几年前...',
category: '小说'
},
{
id: 2,
title: '禅与摩托车维修艺术',
text: '我看了看我的手表...',
category: '小说'
},
{
id: 3,
title: '神经漫游者',
text: '港口上空的天空是...',
category: '小说'
},
{
id: 4,
title: '禅与射艺',
text: '乍看之下,似乎...',
category: '非小说'
},
// ...更多文档
]
let miniSearch = new MiniSearch({
fields: ['title', 'text'], // 用于全文搜索的字段
storeFields: ['title', 'category'] // 随搜索结果返回的字段
})
// 索引所有文档
miniSearch.addAll(documents)
// 使用默认选项搜索
let results = miniSearch.search('禅 艺术 摩托车')
// => [
// { id: 2, title: '禅与摩托车维修艺术', category: '小说', score: 2.77258, match: { ... } },
// { id: 4, title: '禅与射艺', category: '非小说', score: 1.38629, match: { ... } }
// ]
搜索选项
MiniSearch
支持多种高级搜索行为选项:
// 仅搜索特定字段
miniSearch.search('禅', { fields: ['title'] })
// 提升某些字段的权重(这里是 "title")
miniSearch.search('禅', { boost: { title: 2 } })
// 前缀搜索(使 'moto' 能匹配 'motorcycle')
miniSearch.search('摩托', { prefix: true })
// 在特定类别内搜索
miniSearch.search('禅', {
filter: (result) => result.category === '小说'
})
// 模糊搜索,在这个例子中,最大编辑距离为词长的 0.2 倍,
// 四舍五入到最接近的整数。拼错的 'ismael' 将匹配 'ishmael'。
miniSearch.search('伊什迈尔', { fuzzy: 0.2 })
// 你可以在初始化时设置默认搜索选项
miniSearch = new MiniSearch({
fields: ['title', 'text'],
searchOptions: {
boost: { title: 2 },
fuzzy: 0.2
}
})
miniSearch.addAll(documents)
// 现在默认会执行模糊搜索并提升 "title" 的权重:
miniSearch.search('禅和摩托车')
自动建议
MiniSearch
可以根据不完整的查询提供搜索建议:
miniSearch.autoSuggest('禅 射')
// => [ { suggestion: '禅 射艺 艺术', terms: [ '禅', '射艺', '艺术' ], score: 1.73332 },
// { suggestion: '禅 艺术', terms: [ '禅', '艺术' ], score: 1.21313 } ]
autoSuggest
方法接受与 search
方法相同的选项,所以你可以使用模糊搜索为拼错的词获取建议:
miniSearch.autoSuggest('神经漫游', { fuzzy: 0.2 })
// => [ { suggestion: '神经漫游者', terms: [ '神经漫游者' ], score: 1.03998 } ]
建议根据该搜索可能返回的文档的相关性进行排序。
有时,你可能需要过滤自动建议,比如只针对特定类别。你可以通过提供 filter
选项来实现:
miniSearch.autoSuggest('禅 射', {
filter: (result) => result.category === '小说'
})
// => [ { suggestion: '禅 艺术', terms: [ '禅', '艺术' ], score: 1.21313 } ]
字段提取
默认情况下,文档被假定为具有字段名作为键和字段值作为简单值的普通键值对象。为了支持自定义字段提取逻辑(例如嵌套字段,或需要在标记化之前进行处理的非字符串字段值),可以通过extractField
选项传递自定义字段提取函数:
// 假设我们的文档结构如下:
const documents = [
{ id: 1, title: '白鲸记', author: { name: '赫尔曼·梅尔维尔' }, pubDate: new Date(1851, 9, 18) },
{ id: 2, title: '禅与摩托车维修艺术', author: { name: '罗伯特·派西格' }, pubDate: new Date(1974, 3, 1) },
{ id: 3, title: '神经漫游者', author: { name: '威廉·吉布森' }, pubDate: new Date(1984, 6, 1) },
{ id: 4, title: '禅在弓箭术中', author: { name: '欧根·赫里格尔' }, pubDate: new Date(1948, 0, 1) },
// ...更多
]
// 我们可以通过自定义`extractField`函数来支持嵌套字段(author.name)和日期字段(pubDate):
let miniSearch = new MiniSearch({
fields: ['title', 'author.name', 'pubYear'],
extractField: (document, fieldName) => {
// 如果字段名是'pubYear',则从'pubDate'中提取年份
if (fieldName === 'pubYear') {
const pubDate = document['pubDate']
return pubDate && pubDate.getFullYear().toString()
}
// 访问嵌套字段
return fieldName.split('.').reduce((doc, key) => doc && doc[key], document)
}
})
可以通过调用MiniSearch.getDefault('extractField')
获取默认的字段提取器。
分词
默认情况下,文档通过在Unicode空格或标点符号处分割来进行分词。可以通过传递自定义分词器函数作为tokenize
选项来轻松更改分词逻辑:
// 通过连字符分割进行分词
let miniSearch = new MiniSearch({
fields: ['title', 'text'],
tokenize: (string, _fieldName) => string.split('-')
})
在搜索时,默认使用相同的分词方法,但如果需要不同的搜索时分词,可以传递tokenize
搜索选项:
// 通过连字符分割进行分词
let miniSearch = new MiniSearch({
fields: ['title', 'text'],
tokenize: (string) => string.split('-'), // 索引分词器
searchOptions: {
tokenize: (string) => string.split(/[\s-]+/) // 搜索查询分词器
}
})
可以通过调用MiniSearch.getDefault('tokenize')
获取默认的分词器。
词条处理
默认情况下,词条会被转换为小写。不执行词干提取,也不应用停用词列表。要自定义索引时词条的处理方式,例如标准化、过滤或应用词干提取,可以使用processTerm
选项。processTerm
函数应返回处理后的词条作为字符串,或返回假值以丢弃该词条:
let stopWords = new Set(['and', 'or', 'to', 'in', 'a', 'the', /* ...更多 */ ])
// 执行自定义词条处理(这里丢弃停用词并转为小写)
let miniSearch = new MiniSearch({
fields: ['title', 'text'],
processTerm: (term, _fieldName) =>
stopWords.has(term) ? null : term.toLowerCase()
})
默认情况下,相同的处理也应用于搜索查询。为了对搜索查询应用不同的处理,可以提供processTerm
搜索选项:
let miniSearch = new MiniSearch({
fields: ['title', 'text'],
processTerm: (term) =>
stopWords.has(term) ? null : term.toLowerCase(), // 索引词条处理
searchOptions: {
processTerm: (term) => term.toLowerCase() // 搜索查询处理
}
})
可以通过调用MiniSearch.getDefault('processTerm')
获取默认的词条处理器。
API文档
有关配置选项和方法的详细信息,请参阅API文档。
浏览器和Node兼容性
MiniSearch
支持所有实现ES6(ES2015)JavaScript标准的浏览器和NodeJS版本。这包括所有现代浏览器和NodeJS版本。