- Published on
简单拼音搜索引擎
- Authors
- Name
- Yuga Sun
本文主要是通过分析pinyin-engine源码,一步步基于 Javascript 实现一个拼音搜索引擎,并修复了大写英文字母搜索 BUG 和缓存上一次搜索结果 BUG。全部源码(包括注释)都发布在 Github 上:demo-pinyin-engine
本文主要分为两部分:
- 实现简单的搜索文字匹配
- 结合汉字拼音字典实现拼音搜索引擎
PS: 本文均基于 ES6 语法实现,考虑到兼容性,通过 webpack工具 来进行编译输出 ES5 语法。
实现简单的搜索文字匹配
创建搜索引擎类
一个基本的搜索类,必然包含所要搜索的数据集和需要搜索的属性集合,而且必须实现一个搜索方法,代码如下:
class PinyinEngine {
/**
* 构造函数
*
* @param {Object} data 用户提供的需要搜索的数据集
* @param {Array} indexs 用户提供查询的属性数组 - 对象键值
* @memberof PinyinEngine
*/
constructor(data, indexs) {
this.data = data
this.indexs = typeof indexs === 'string' ? [indexs] : indexs
}
query(keyword) {
// 遍历数据集
return this.data.filter((item) => {
let result = false
// 遍历需要搜索的键值数组,找到需要搜索
this.indexs.map((index) => {
if (item[index].indexOf(keyword) !== -1) {
result = true
}
})
return result
})
}
}
module.exports = PinyinEngine
以上代码通过 Array.prototype.filter 方法来对数据集进行筛选查找,并对需要查找的对象属性数组进行遍历,只需要其中任何一个属性的值包含所要搜索的关键字,就返回结果。
初始化数据列表
此步骤主要是渲染搜索出来的相关数据,包括默认的所有数据:
// 遍历拼接数据列表
function createTmpl(data) {
var txt = []
for (var i in data) {
txt.push('<li><a href="javascript:;" id="')
txt.push(data[i].id)
txt.push('">')
txt.push(data[i].name)
txt.push('</a></li>')
}
txt = txt.join('')
txt =
txt === '' ? '<li><div class="tmpl-schoolBox-noContent">此地区暂时没有数据..</div></li>' : txt
return '<ul>' + txt + '</ul>'
}
// 更新DOM内容
function loadSchool(data, timeEnd, initial) {
// initial 为true, 默认渲染所有,直接输出缓存的模板 tmplCache
var html = initial ? tmplCache : createTmpl(data)
$unisContent.innerHTML = html
$log.innerHTML = '(' + data.length + '条测试数据,索引创建耗时: ' + timeEnd + '毫秒'
}
// 初始化列表
// 缓存全部数据的列表模板,提高性能
tmplCache = createTmpl(_demoData)
loadSchool(_demoData, initTime, true)
监听输入框输入和值改变事件
当输入框输入和值变化时,需要监听并执行搜索的 query
方法来输出搜索结果:
/ 绑定输入事件
$input.oninput = $input.onpropertychange = function () {
var val = $input.value
if (val === oldVal) return
oldVal = $input.value
clearTimeout(timer)
timer = setTimeout(function () {
var time = new Timer()
// 如果val为空,则不需搜搜,直接渲染所有
if (val === '') {
loadSchool(_demoData, time.end(), true)
} else {
// 进行查询,输出结果
var list = engine.query(val)
loadSchool(list, time.end())
}
}, 40) // 延时可以减小查询频率
}
结合中文拼音字典,实现拼音搜索
准备中文拼音字典
创建 cn_dict.json
文件,结构如下:
{
"一": ["yi"],
"丁:: ["ding"]
...
}
实现此字典文件的目的是为了,当用户输入拼音关键字时,通过此字典来匹配出相应的汉字。然后通过第一步实现的 query 方法查找输出结果。 那么在执行 query 之前,我们需要对输入的拼音组合进行分词拼接,输出组合。
源码中的
cn_dist.json
是对上面的结构的对象进行了压缩加密,然后通过decode.js
进行解码输出,因为本文重点是拼音搜索,所以在此不做介绍。
创建中文分词方法
在构建搜索引擎的时候,我们需要根据需要查询的属性数组props
,获取数据集data
中对应属性的值 - 中文字符串
,然后对中文字符创进行分词,也就是循环遍历中文字符串,通过 拼音字典
找到每个中文所对应的 拼音
,然后拼接出所有拼音组合的可能性,便于我们搜索,于是给 PinyinEngine
添加 participle
私有方法:
/**
* 将内容进行分词
*
* @static
* @param {String} words 目标字符串
* @param {Object} dict 字典
* @returns {String}
* @memberof PinyinEngine
*/
static participle (words, dict) {
words = words.replace(/\s/g, '') // 去除空白字符
let result = `${words}`
// k 存放汉字全拼
// keywords[1] 存放的汉字首字母
const keywords = [[], []]
// 遍历文字内容
for (const char of words) {
const pinyin = dict[char] // 获取汉字对应拼音
if (pinyin) {
keywords[0].push(pinyin)
if (words.length > 1) {
keywords[1].push(pinyin.map(p => p.charAt(0)))
}
}
}
// 循环拼接拼音字符
// 1. 拼接keywords[0]中存放的汉字拼音
// 2. 拼接keywords[1]中存放的汉字拼音首字母
// 3. 拼接原汉字和1、2中生成的拼音字符
for (const list of keywords) {
let current = list.shift()
while (list.length) {
const array = []
const next = list.shift()
for (const c of current) {
for (const n of next) {
array.push(c + n)
}
}
current = array
}
if (current) {
result += `\u0001${current.join('\u0001')}`
}
}
return result
}
// 清华大学 -> 清华大学qinghuadaxueqinghuataixueqhdxqhtx
初始化拼音搜索索引数组
有了 participle
分词方法,我们在初始化的时候就可以对每个中文字符串进行分词,并存入到 this.indexs
索引中,然后改写构造函数为:
/**
* 构造函数
*
* @param {Array} data 用户提供的需要搜索的数据集
* @param {Array|String} props 用户提供搜索索引 - 对象键值
* @memberof PinyinEngine
*/
constructor (data, props = []) {
this.indexs = [] // 索索索引数组
this.data = data
this.dict = DICT
props = typeof props === 'string' ? [props] : props
// 遍历数据集进行索引对应的值进行分词
data.map((item) => {
let keywords = ''
if (typeof item === 'string') {
keywords = PinyinEngine.participle(item, DICT)
} else { // item 为对象
for (const key of props) {
const words = item[key]
if (words) {
keywords += PinyinEngine.participle(words, DICT)
}
}
}
// 建立拼音搜索索引数组
// 考虑到值为大写英文字母的情况
this.indexs.push(keywords.toLowerCase())
})
}
根据拼音搜索索引进行拼音搜索
这样当我们在执行 query
方法时,对我们构建的 indexs - 拼音索引
进行遍历,判断如果某个索引值包含了搜索关键字,就存入到输出数组中,从而得到搜索结果:
/**
* 查询方法
*
* @param {String} keyword 需要查找的关键字
* @returns {Array}
* @memberof PinyinEngine
*/
query (keyword) {
keyword = keyword.replace(/\s/g, '').toLowerCase()
let indexs = this.indexs
let data = this.data
let result = []
// 遍历数据集
indexs.map((item, index) => {
// 遍历需要搜索的键值数组,找到包含搜索关键字的索引值
if (item.indexOf(keyword) !== -1) {
result.push(data[index])
}
})
return result
}
优化搜索结果
到这里我们已经实现了拼音搜索引擎的核心功能了,但是有个问题就是,我们在重复搜索同一个拼音字符的时候,搜索都是全局遍历的,那么我们是不是可以将上一次的搜索结果进行缓存呢,这样当再次搜索相同字符如果包含了上一次搜索的字符,可以将搜索索引缩小为上一次搜索缓存的索引数组,搜索的数据集也是,这样是不是更快。
对于 PinyinEngine
添加一个属性 history
专门用来存放我们的搜索结果,history
含有三个属性值:
keyword
存放搜索关键字indexs
存放上一次搜索的索引data
存放上一次的搜索结果
修改 constructor
:
constructor (data, indexs = [], dict = {}) {
this.indexs = []
this.history = { keyword: '', indexs: [], data: [] }
// ...
}
修改 query
方法:
/**
* 查询方法
*
* @param {String} keyword 需要查找的关键字
* @returns {Array}
* @memberof PinyinEngine
*/
query (keyword) {
keyword = keyword.replace(/\s/g, '').toLowerCase()
let indexs = this.indexs
let data = this.data
const history = this.history
const result = []
// 性能优化: 在上一次搜索结果中查询
if (history.data.length && keyword.indexOf(history.keyword) === 0) {
indexs = history.indexs
data = history.data
}
history.keyword = keyword
history.indexs = []
history.data = []
// 遍历数据集
indexs.map((item, index) => {
// 遍历需要搜索的键值数组,找到需要搜索
if (item.indexOf(keyword) !== -1) {
history.indexs.push(item)
history.data.push(data[index])
result.push(data[index])
}
})
return result
}
总结
本篇就到这里结束了,再次感谢 @糖饼 大神提供的 pinyin-engine 库,才有了这篇文章。当然,本文只提供了中文字典,感兴趣的可以添加不同语言的字典,来扩展为各国语言的搜索引擎,pinyin-engine这个库,就是扩展了繁体的拼音搜索,感兴趣的可以去研究研究。