搜索功能的重要性
搜索是用户快速找到内容的核心途径。优秀的搜索体验需要满足:
- 快速响应 - 用户输入后即时反馈
- 结果相关 - 返回最匹配的内容
- 容错能力 - 拼写错误也能找到结果
- 交互友好 - 高亮、补全、历史记录
本文将系统介绍前端搜索优化的核心技术。
基础搜索实现
简单关键词匹配
// 基础搜索函数
const searchItems = (items, query) => {
if (!query.trim()) return items
const keywords = query.toLowerCase().split(/\s+/)
return items.filter(item => {
const searchText = `${item.title} ${item.description}`.toLowerCase()
return keywords.every(keyword => searchText.includes(keyword))
})
}
防抖优化
避免频繁触发搜索:
// 防抖 Hook
const useDebounce = (value, delay = 300) => {
const [debouncedValue, setDebouncedValue] = useState(value)
useEffect(() => {
const timer = setTimeout(() => {
setDebouncedValue(value)
}, delay)
return () => clearTimeout(timer)
}, [value, delay])
return debouncedValue
}
// 使用
const SearchComponent = () => {
const [query, setQuery] = useState('')
const debouncedQuery = useDebounce(query, 300)
useEffect(() => {
if (debouncedQuery) {
performSearch(debouncedQuery)
}
}, [debouncedQuery])
}
搜索算法优化
模糊匹配算法
支持拼写容错的搜索:
// Levenshtein 距离计算
const levenshteinDistance = (a, b) => {
const matrix = []
for (let i = 0; i <= b.length; i++) {
matrix[i] = [i]
}
for (let j = 0; j <= a.length; j++) {
matrix[0][j] = j
}
for (let i = 1; i <= b.length; i++) {
for (let j = 1; j <= a.length; j++) {
if (b.charAt(i - 1) === a.charAt(j - 1)) {
matrix[i][j] = matrix[i - 1][j - 1]
} else {
matrix[i][j] = Math.min(
matrix[i - 1][j - 1] + 1, // 替换
matrix[i][j - 1] + 1, // 插入
matrix[i - 1][j] + 1 // 删除
)
}
}
}
return matrix[b.length][a.length]
}
// 模糊搜索
const fuzzySearch = (items, query, threshold = 2) => {
return items.filter(item => {
const words = item.title.toLowerCase().split(/\s+/)
const queryWords = query.toLowerCase().split(/\s+/)
return queryWords.every(qWord =>
words.some(word => levenshteinDistance(word, qWord) <= threshold)
)
})
}
相关度排序
根据匹配程度排序结果:
const calculateRelevance = (item, query) => {
let score = 0
const queryLower = query.toLowerCase()
const titleLower = item.title.toLowerCase()
const descLower = (item.description || '').toLowerCase()
// 完全匹配标题加分最多
if (titleLower === queryLower) score += 100
// 标题包含完整查询词
if (titleLower.includes(queryLower)) score += 50
// 标题开头匹配
if (titleLower.startsWith(queryLower)) score += 30
// 描述包含查询词
if (descLower.includes(queryLower)) score += 20
// 按关键词匹配数量加分
const keywords = queryLower.split(/\s+/)
keywords.forEach(keyword => {
if (titleLower.includes(keyword)) score += 10
if (descLower.includes(keyword)) score += 5
})
return score
}
const searchWithRelevance = (items, query) => {
return items
.map(item => ({
...item,
relevance: calculateRelevance(item, query)
}))
.filter(item => item.relevance > 0)
.sort((a, b) => b.relevance - a.relevance)
}
全文检索方案
使用 FlexSearch
高性能的客户端全文检索库:
import FlexSearch from 'flexsearch'
// 创建索引
const index = new FlexSearch.Document({
document: {
id: 'id',
index: ['title', 'content', 'tags'],
store: ['title', 'url', 'summary']
},
// 中文分词配置
tokenize: 'forward',
encode: (str) => str.toLowerCase().split(/[\s\u4e00-\u9fa5]+/)
})
// 添加文档
articles.forEach(article => {
index.add(article)
})
// 搜索
const results = index.search(query, {
limit: 20,
enrich: true
})
// 处理结果
const formattedResults = results.flatMap(field =>
field.result.map(item => item.doc)
)
使用 Fuse.js
功能丰富的模糊搜索库:
import Fuse from 'fuse.js'
const fuse = new Fuse(items, {
keys: [
{ name: 'title', weight: 2 },
{ name: 'description', weight: 1 },
{ name: 'tags', weight: 1.5 }
],
threshold: 0.3, // 匹配阈值
includeScore: true, // 返回匹配分数
includeMatches: true, // 返回匹配位置
minMatchCharLength: 2,
// 高级配置
ignoreLocation: true,
findAllMatches: true
})
const search = (query) => {
const results = fuse.search(query)
return results.map(result => ({
...result.item,
score: result.score,
matches: result.matches
}))
}
搜索建议与自动补全
实时搜索建议
<template>
<div class="search-container">
<input
v-model="query"
@input="handleInput"
@keydown.down="selectNext"
@keydown.up="selectPrev"
@keydown.enter="confirmSelection"
placeholder="搜索..."
/>
<ul v-if="suggestions.length" class="suggestions">
<li
v-for="(suggestion, index) in suggestions"
:key="suggestion.id"
:class="{ active: index === activeIndex }"
@click="selectSuggestion(suggestion)"
@mouseenter="activeIndex = index"
>
<span v-html="highlightMatch(suggestion.title, query)" />
</li>
</ul>
</div>
</template>
<script setup>
const query = ref('')
const suggestions = ref([])
const activeIndex = ref(-1)
const handleInput = useDebounceFn(async () => {
if (query.value.length < 2) {
suggestions.value = []
return
}
suggestions.value = await fetchSuggestions(query.value)
activeIndex.value = -1
}, 200)
// 高亮匹配文本
const highlightMatch = (text, query) => {
const regex = new RegExp(`(${escapeRegex(query)})`, 'gi')
return text.replace(regex, '<mark>$1</mark>')
}
// 键盘导航
const selectNext = () => {
if (activeIndex.value < suggestions.value.length - 1) {
activeIndex.value++
}
}
const selectPrev = () => {
if (activeIndex.value > 0) {
activeIndex.value--
}
}
</script>
搜索历史
// 搜索历史管理
class SearchHistory {
constructor(key = 'search_history', maxSize = 10) {
this.key = key
this.maxSize = maxSize
}
getHistory() {
const saved = localStorage.getItem(this.key)
return saved ? JSON.parse(saved) : []
}
addQuery(query) {
const history = this.getHistory()
// 去重并添加到开头
const filtered = history.filter(q => q !== query)
filtered.unshift(query)
// 限制数量
const trimmed = filtered.slice(0, this.maxSize)
localStorage.setItem(this.key, JSON.stringify(trimmed))
return trimmed
}
removeQuery(query) {
const history = this.getHistory().filter(q => q !== query)
localStorage.setItem(this.key, JSON.stringify(history))
return history
}
clear() {
localStorage.removeItem(this.key)
return []
}
}
性能优化
虚拟滚动
大量结果时使用虚拟滚动:
<template>
<div
ref="container"
class="search-results"
@scroll="onScroll"
>
<div :style="{ height: totalHeight + 'px', position: 'relative' }">
<div
v-for="item in visibleItems"
:key="item.id"
:style="{
position: 'absolute',
top: item.offset + 'px',
height: itemHeight + 'px'
}"
>
<SearchResultItem :item="item.data" />
</div>
</div>
</div>
</template>
<script setup>
const props = defineProps({
items: Array
})
const container = ref(null)
const itemHeight = 80
const bufferSize = 5
const scrollTop = ref(0)
const totalHeight = computed(() => props.items.length * itemHeight)
const visibleItems = computed(() => {
const containerHeight = container.value?.clientHeight || 500
const startIndex = Math.max(0, Math.floor(scrollTop.value / itemHeight) - bufferSize)
const endIndex = Math.min(
props.items.length,
Math.ceil((scrollTop.value + containerHeight) / itemHeight) + bufferSize
)
return props.items.slice(startIndex, endIndex).map((item, i) => ({
data: item,
offset: (startIndex + i) * itemHeight
}))
})
const onScroll = () => {
scrollTop.value = container.value.scrollTop
}
</script>
Web Worker 搜索
将搜索计算移到 Worker 线程:
// search.worker.js
import Fuse from 'fuse.js'
let fuse = null
self.onmessage = (e) => {
const { type, payload } = e.data
if (type === 'init') {
fuse = new Fuse(payload.items, payload.options)
self.postMessage({ type: 'ready' })
}
if (type === 'search') {
const results = fuse.search(payload.query)
self.postMessage({ type: 'results', results })
}
}
// 主线程使用
const useWorkerSearch = (items, options) => {
const worker = new Worker(new URL('./search.worker.js', import.meta.url))
const results = ref([])
const isReady = ref(false)
// 初始化
worker.postMessage({ type: 'init', payload: { items, options } })
worker.onmessage = (e) => {
if (e.data.type === 'ready') {
isReady.value = true
}
if (e.data.type === 'results') {
results.value = e.data.results
}
}
const search = (query) => {
if (!isReady.value) return
worker.postMessage({ type: 'search', payload: { query } })
}
onUnmounted(() => worker.terminate())
return { results, search, isReady }
}
服务端搜索
API 设计
// 搜索 API 接口设计
// GET /api/search?q=关键词&page=1&limit=20&filters=category:tech
// server/api/search.ts
export default defineEventHandler(async (event) => {
const query = getQuery(event)
const { q, page = 1, limit = 20, filters } = query
// 解析过滤条件
const parsedFilters = parseFilters(filters)
// 调用搜索服务
const results = await searchService.search({
query: q,
page: Number(page),
limit: Number(limit),
filters: parsedFilters
})
return {
items: results.hits,
total: results.total,
page: Number(page),
totalPages: Math.ceil(results.total / Number(limit)),
took: results.took
}
})
搜索结果缓存
// 客户端搜索缓存
const useSearchCache = () => {
const cache = new Map()
const maxSize = 50
const getCacheKey = (query, filters) => {
return JSON.stringify({ query, filters })
}
const get = (query, filters) => {
const key = getCacheKey(query, filters)
const cached = cache.get(key)
if (cached && Date.now() - cached.timestamp < 5 * 60 * 1000) {
return cached.data
}
return null
}
const set = (query, filters, data) => {
// LRU 淘汰
if (cache.size >= maxSize) {
const oldest = cache.keys().next().value
cache.delete(oldest)
}
const key = getCacheKey(query, filters)
cache.set(key, {
data,
timestamp: Date.now()
})
}
return { get, set }
}
搜索体验优化
无结果处理
<template>
<div v-if="results.length === 0" class="no-results">
<IconSearch class="w-16 h-16 text-gray-300" />
<h3>未找到相关结果</h3>
<p>尝试以下建议:</p>
<ul>
<li>检查关键词拼写</li>
<li>尝试使用更通用的词汇</li>
<li>减少筛选条件</li>
</ul>
<!-- 相关推荐 -->
<div v-if="suggestions.length" class="suggestions">
<h4>您可能感兴趣的内容</h4>
<div class="grid grid-cols-3 gap-4">
<ArticleCard v-for="item in suggestions" :key="item.id" :article="item" />
</div>
</div>
</div>
</template>
搜索分析
// 搜索行为追踪
const trackSearch = (query, results, clickedItem) => {
// 记录搜索事件
analytics.track('search', {
query,
resultCount: results.length,
hasResults: results.length > 0
})
// 记录点击事件
if (clickedItem) {
analytics.track('search_click', {
query,
clickedItemId: clickedItem.id,
position: results.findIndex(r => r.id === clickedItem.id) + 1
})
}
}
总结
优化搜索功能的核心要点:
- 算法优化 - 模糊匹配、相关度排序
- 性能优化 - 防抖、虚拟滚动、Web Worker
- 体验优化 - 自动补全、搜索历史、高亮显示
- 容错处理 - 拼写纠错、无结果推荐
通过综合运用这些技术,可以构建出快速、智能、友好的搜索体验。


