前端本地模糊搜索1.0 按照匹配位置加权
需求背景
公司项目为Saas ERP系统,客户需要快速开单需要避免接口带来的延迟问题。所以需要将商品数据保存在本地。所以本地搜索 + 权重 这一套组合拳需要前端自己实现。
搜索示例
示例1:输入:"男士真皮钱包"进行模糊匹配优先匹配完全同→ 如 【男士真皮钱包】若无结果,展示包含 搜索内容 的商品(OR逻辑)→ 如 【商务男士真皮钱包】→ 如 【商务男士真皮钱包plus版】→ 如 【plus版商务男士真皮钱包】规则:输入:"123"结果:123123xxxxxx123xxx123xxx
解决方案
- 通过搜索内容对 相关字符串进行切割,以第一个切割位置为准。(例如:"testabc"用"a"分割得到[“test”, “bc”])
- 针对 ‘test’ 和 ‘bc’ 进行加权重计算 (切割后前面字符串长度 + 1) * 1000 + 切割后后面字符串总长度
技术细节
搜索字段优先级排列
let endResultData = []
// 搜索关键词转为小写字母
const searchLower = this.searchText.toLocaleLowerCase().trim()
// 所有支持搜索的字段,且此处也是搜索的优先级顺序。
let allKeys = ['b', 'c', 'j', 'zjf', 'v', 'w', 'x']
// 储存优先级字段
let allKeyObj = {}
// 构造存储不匹配搜索项key的map
allKeys.map(item => {Reflect.set(allKeyObj, item, {equalArr: [],hasMapKeys: [],})
})
区分完全匹配、部分匹配、完全不匹配数据
let otherItems = []
let resultListMap = new Map()
dataList.map((item, idx) => {const filterText = item.filterText// 匹配策略if (filterText.includes(searchLower)) {let equalKeylet hasKeyallKeys.map(keyIt => {let str = String(item[keyIt]).toLocaleLowerCase()if (str === searchLower) {equalKey = keyIt} else if (str.includes(searchLower)) {hasKey = keyIt}})if (equalKey) {// 完全匹配} else {if (hasKey) {// 部分匹配} else otherItems.push(item) // 不匹配}}
})
权重计算逻辑
- 根据关键字使用split 拆分字段
- 根据字段拆分结果计算权重 (切割后前面字符串长度 + 1) * 1000 + 切割后后面字符串总长度
- 排序整合返回搜索结果数据
/*** 对指定字段进行分割,并计算匹配部分的长度信息(用于搜索结果排序或高亮处理)* @param {Object} data - 原始数据对象,需包含待处理的字段* @param {string} key - 数据对象中需要处理的字段名* @param {string} searchLower - 搜索关键词(小写格式,用于分割字符串)* @returns {Object} - 返回包含原始数据和计算长度信息的新对象*/
splitSortSearchData(data, key, searchLower) {// 使用搜索词分割目标字符串,生成数组(例如:"testabc"用"a"分割得到["test", "bc"])const keySplitArr = data[key].split(searchLower)/*** 计算剩余部分总长度的工具函数* @param {Array} endArr - 分割后的剩余部分数组* @returns {number} - 剩余部分字符总长度*/const comptedEndLen = endArr => endArr.map(item => item.length).reduce((x, y) => x + y, 0)// 获取分割后的剩余部分(排除第一个匹配项之前的内容)let endLenArr = keySplitArr.slice(1)let endLen = 0/* 计算剩余部分总长度逻辑:1. 当剩余部分只有1个元素时,直接取长度(需排除空字符串情况)2. 多个元素时累加各段长度3. 无剩余元素时保持默认值0*/if (endLenArr.length == 1) {// 处理单个剩余元素的情况(例如:精确匹配结尾时可能产生空字符串)if (endLenArr[0]) endLen = endLenArr[0].length// 多个剩余元素时计算总长度(例如:多次匹配产生的多段文本)} else if (endLenArr.length > 1) endLen = comptedEndLen(endLenArr)// 返回增强后的数据对象,包含:// - 原始数据的所有属性// - startLen: 第一个匹配项前的字符长度(用于判断匹配位置)// - endLen: 匹配项之后所有剩余字符总长度(用于相关性排序)return {...data,// 保留原始数据startLen: keySplitArr[0].length,// 首个分割段的长度(搜索词首次出现前的字符数)endLen,// 后续所有分割段的字符总数(越小说明匹配越靠前/内容越相关)}
}/*** 拼接唯一key* @param {* Object} item 计算好前后空格的每一项* @param {* Number} idx* @returns string*/
calcOnlyKey(item, idx) {return `${(item.startLen + 1) * 1000 + item.endLen}д${idx}`
}
计算完成合并计算结果
let mapKeys = []
allKeys.forEach((item, idx) => {/* 将完全匹配的加入到最终数组中 */endResultData = endResultData.concat(allKeyObj[item].equalArr)/** 针对每一类数据进行排序*/mapKeys = mapKeys.concat(allKeyObj[item].hasMapKeys.sort((a, b) => Number(a.split('д')[0]) - Number(b.split('д')[0])))
})
mapKeys.map(mkey => endResultData.push(resultListMap.get(mkey)))
endResultData.concat(otherItems)
console.log('筛选出来数据长度:', endResultData.length)
完整代码
// 搜索函数
onSearch(dataList) {let endResultData = []// 搜索关键词转为小写字母const searchLower = this.searchText.toLocaleLowerCase().trim()// 所有支持搜索的字段,且此处也是搜索的优先级顺序。let allKeys = ['b', 'c', 'j', 'zjf', 'v', 'w', 'x']// 储存优先级字段let allKeyObj = {}// 构造存储不匹配搜索项key的mapallKeys.map(item => {Reflect.set(allKeyObj, item, {equalArr: [],hasMapKeys: [],})})let otherItems = []let resultListMap = new Map()dataList.map((item, idx) => {const filterText = item.filterText// 匹配策略if (filterText.includes(searchLower)) {let equalKeylet hasKeyallKeys.map(keyIt => {let str = String(item[keyIt]).toLocaleLowerCase()if (str === searchLower) {equalKey = keyIt} else if (str.includes(searchLower)) {hasKey = keyIt}})if (equalKey) {const splitItem = this.splitSortSearchData(item, equalKey, searchLower)allKeyObj[equalKey].equalArr.push(splitItem)} else {if (hasKey) {const splitItem = this.splitSortSearchData(item, hasKey, searchLower)const key = this.calcOnlyKey(splitItem, idx)allKeyObj[hasKey].hasMapKeys.push(key)resultListMap.set(key, splitItem)} else otherItems.push(item)}}})let mapKeys = []allKeys.forEach((item, idx) => {/* 将完全匹配的加入到最终数组中 */endResultData = endResultData.concat(allKeyObj[item].equalArr)/** 针对每一类数据进行排序*/mapKeys = mapKeys.concat(allKeyObj[item].hasMapKeys.sort((a, b) => Number(a.split('д')[0]) - Number(b.split('д')[0])))})mapKeys.map(mkey => endResultData.push(resultListMap.get(mkey)))endResultData.concat(otherItems)console.log('筛选出来数据长度:', endResultData.length)return endResultData
}/**
* 对指定字段进行分割,并计算匹配部分的长度信息(用于搜索结果排序或高亮处理)
* @param {Object} data - 原始数据对象,需包含待处理的字段
* @param {string} key - 数据对象中需要处理的字段名
* @param {string} searchLower - 搜索关键词(小写格式,用于分割字符串)
* @returns {Object} - 返回包含原始数据和计算长度信息的新对象
*/
splitSortSearchData(data, key, searchLower) {// 使用搜索词分割目标字符串,生成数组(例如:"testabc"用"a"分割得到["test", "bc"])const keySplitArr = data[key].split(searchLower)/*** 计算剩余部分总长度的工具函数* @param {Array} endArr - 分割后的剩余部分数组* @returns {number} - 剩余部分字符总长度*/const comptedEndLen = endArr => endArr.map(item => item.length).reduce((x, y) => x + y, 0)// 获取分割后的剩余部分(排除第一个匹配项之前的内容)let endLenArr = keySplitArr.slice(1)let endLen = 0/* 计算剩余部分总长度逻辑:1. 当剩余部分只有1个元素时,直接取长度(需排除空字符串情况)2. 多个元素时累加各段长度3. 无剩余元素时保持默认值0*/if (endLenArr.length == 1) {// 处理单个剩余元素的情况(例如:精确匹配结尾时可能产生空字符串)if (endLenArr[0]) endLen = endLenArr[0].length// 多个剩余元素时计算总长度(例如:多次匹配产生的多段文本)} else if (endLenArr.length > 1) endLen = comptedEndLen(endLenArr)// 返回增强后的数据对象,包含:// - 原始数据的所有属性// - startLen: 第一个匹配项前的字符长度(用于判断匹配位置)// - endLen: 匹配项之后所有剩余字符总长度(用于相关性排序)return {...data,// 保留原始数据startLen: keySplitArr[0].length,// 首个分割段的长度(搜索词首次出现前的字符数)endLen,// 后续所有分割段的字符总数(越小说明匹配越靠前/内容越相关)}
}
/**
* 拼接唯一key
* @param {* Object} item 计算好前后空格的每一项
* @param {* Number} idx
* @returns string
*/
calcOnlyKey(item, idx) {return `${(item.startLen + 1) * 1000 + item.endLen}д${idx}`
}
小结
文章最后欢迎各位大佬留言讨论。