需求背景
公司項目為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}`
}
小結
文章最后歡迎各位大佬留言討論。