Fuse.js:打造极致模糊搜索体验
Fuse.js 完全学习指南:JavaScript模糊搜索库
🎯 什么是 Fuse.js?
Fuse.js 是一个轻量、强大且无依赖的JavaScript模糊搜索库。它提供了简单而强大的模糊搜索功能,可以在任何 JavaScript 环境中使用,包括浏览器和 Node.js。
🌟 核心特点
- 轻量级:压缩后仅 ~12KB,无外部依赖
- 模糊搜索:支持拼写错误、部分匹配等容错搜索
- 高度可配置:提供丰富的配置选项控制搜索行为
- 多字段搜索:支持在对象的多个字段中搜索
- 权重系统:不同字段可以设置不同的搜索权重
- 高亮显示:支持搜索结果高亮显示
- 跨平台:支持浏览器、Node.js、Deno等环境
- TypeScript支持:提供完整的TypeScript类型定义
📦 安装与引入
NPM 安装
# 使用 npm
npm install fuse.js
# 使用 yarn
yarn add fuse.js
引入方式
ES6 模块语法
import Fuse from 'fuse.js'
CommonJS
const Fuse = require('fuse.js')
直接
案例3:React Hooks 集成
import React, { useState, useMemo, useCallback } from 'react'
import Fuse from 'fuse.js'
// 自定义 Hook:useFuseSearch
function useFuseSearch(data, options) {
const [query, setQuery] = useState('')
const fuse = useMemo(() => {
return new Fuse(data, options)
}, [data, options])
const results = useMemo(() => {
if (!query.trim()) {
return data.map((item, index) => ({ item, refIndex: index }))
}
return fuse.search(query)
}, [fuse, query, data])
return {
query,
setQuery,
results,
search: useCallback((searchQuery) => {
return fuse.search(searchQuery)
}, [fuse])
}
}
// 主搜索组件
function ProductSearch() {
const products = [
{
id: 1,
name: 'MacBook Pro 16寸',
brand: 'Apple',
category: '笔记本电脑',
price: 16999,
tags: ['高性能', '创作', '专业']
},
{
id: 2,
name: 'iPhone 14 Pro',
brand: 'Apple',
category: '智能手机',
price: 7999,
tags: ['摄影', '5G', '高端']
},
{
id: 3,
name: 'Surface Laptop 5',
brand: 'Microsoft',
category: '笔记本电脑',
price: 8888,
tags: ['轻薄', '办公', '便携']
},
{
id: 4,
name: 'Galaxy S23 Ultra',
brand: 'Samsung',
category: '智能手机',
price: 9999,
tags: ['大屏', 'S Pen', '摄影']
}
]
const searchOptions = {
keys: [
{ name: 'name', weight: 0.4 },
{ name: 'brand', weight: 0.3 },
{ name: 'category', weight: 0.2 },
{ name: 'tags', weight: 0.1 }
],
threshold: 0.3,
includeMatches: true,
includeScore: true
}
const { query, setQuery, results } = useFuseSearch(products, searchOptions)
const [sortBy, setSortBy] = useState('relevance')
// 排序结果
const sortedResults = useMemo(() => {
const sorted = [...results]
switch (sortBy) {
case 'price-asc':
return sorted.sort((a, b) => a.item.price - b.item.price)
case 'price-desc':
return sorted.sort((a, b) => b.item.price - a.item.price)
case 'name':
return sorted.sort((a, b) => a.item.name.localeCompare(b.item.name))
default:
return sorted // 保持相关性排序
}
}, [results, sortBy])
// 高亮匹配文本
const highlightMatches = (text, matches) => {
if (!matches || matches.length === 0) return text
const match = matches[0]
if (!match) return text
let highlighted = text
const indices = match.indices
for (let i = indices.length - 1; i >= 0; i--) {
const [start, end] = indices[i]
highlighted = highlighted.slice(0, start) +
'' +
highlighted.slice(start, end + 1) +
'' +
highlighted.slice(end + 1)
}
return highlighted
}
return (
{ maxWidth: '800px', margin: '50px auto', padding: '20px' }}>
🛍️ 产品搜索
{/* 搜索栏 */}
{ marginBottom: '20px' }}>
setQuery(e.target.value)}
placeholder="搜索产品名称、品牌或分类..."
style={{
width: '100%',
padding: '12px',
fontSize: '16px',
border: '2px solid #ddd',
borderRadius: '8px',
outline: 'none'
}}
/>
{/* 排序选项 */}
{ marginBottom: '20px', display: 'flex', gap: '10px', alignItems: 'center' }}>
排序:
{ marginLeft: 'auto', color: '#666' }}>
找到 {sortedResults.length} 个结果
{/* 搜索结果 */}
{ display: 'grid', gap: '15px' }}>
{sortedResults.map((result) => {
const product = result.item
const matches = result.matches || []
const nameMatch = matches.find(m => m.key === 'name')
const brandMatch = matches.find(m => m.key === 'brand')
return (
{
border: '1px solid #eee',
borderRadius: '8px',
padding: '20px',
background: 'white',
boxShadow: '0 2px 4px rgba(0,0,0,0.1)'
}}
>
{ display: 'flex', justifyContent: 'space-between', alignItems: 'start' }}>
{ flex: 1 }}>
{ margin: '0 0 5px 0', fontSize: '18px' }}
dangerouslySetInnerHTML={{
__html: nameMatch ? highlightMatches(product.name, [nameMatch]) : product.name
}}
/>
{ margin: '0 0 10px 0', color: '#666' }}>
品牌:
{
__html: brandMatch ? highlightMatches(product.brand, [brandMatch]) : product.brand
}} />
{' | '}
分类:{product.category}
{ display: 'flex', gap: '5px', marginBottom: '10px' }}>
{product.tags.map(tag => (
{
background: '#f0f0f0',
padding: '2px 8px',
borderRadius: '12px',
fontSize: '12px',
color: '#666'
}}
>
{tag}
))}
{ textAlign: 'right' }}>
{ fontSize: '20px', fontWeight: 'bold', color: '#e74c3c' }}>
¥{product.price.toLocaleString()}
{result.score && (
{ fontSize: '12px', color: '#999', marginTop: '5px' }}>
匹配度: {Math.round((1 - result.score) * 100)}%
)}
)
})}
{sortedResults.length === 0 && query && (
{ textAlign: 'center', padding: '50px', color: '#999' }}>
没有找到匹配的产品
)}
)
}
export default ProductSearch
🔧 性能优化建议
1. 大数据集处理
// 对于大数据集,考虑使用 Web Workers
class FuseWorker {
constructor(data, options) {
this.worker = new Worker('/fuse-worker.js')
this.worker.postMessage({ type: 'init', data, options })
}
search(query) {
return new Promise((resolve) => {
this.worker.onmessage = (e) => {
if (e.data.type === 'search-result') {
resolve(e.data.results)
}
}
this.worker.postMessage({ type: 'search', query })
})
}
}
// fuse-worker.js
let fuse
self.onmessage = function(e) {
const { type, data, options, query } = e.data
if (type === 'init') {
importScripts('https://cdn.jsdelivr.net/npm/fuse.js@7.1.0/dist/fuse.min.js')
fuse = new Fuse(data, options)
}
if (type === 'search' && fuse) {
const results = fuse.search(query)
self.postMessage({ type: 'search-result', results })
}
}
2. 防抖搜索
// 使用防抖避免频繁搜索
function useDebounce(value, delay) {
const [debouncedValue, setDebouncedValue] = useState(value)
useEffect(() => {
const handler = setTimeout(() => {
setDebouncedValue(value)
}, delay)
return () => {
clearTimeout(handler)
}
}, [value, delay])
return debouncedValue
}
// 在组件中使用
function SearchComponent() {
const [query, setQuery] = useState('')
const debouncedQuery = useDebounce(query, 300)
const results = useMemo(() => {
if (!debouncedQuery) return []
return fuse.search(debouncedQuery)
}, [debouncedQuery])
// ...
}
3. 结果缓存
// 简单的 LRU 缓存实现
class LRUCache {
constructor(capacity) {
this.capacity = capacity
this.cache = new Map()
}
get(key) {
if (this.cache.has(key)) {
const value = this.cache.get(key)
this.cache.delete(key)
this.cache.set(key, value)
return value
}
return null
}
set(key, value) {
if (this.cache.has(key)) {
this.cache.delete(key)
} else if (this.cache.size >= this.capacity) {
const firstKey = this.cache.keys().next().value
this.cache.delete(firstKey)
}
this.cache.set(key, value)
}
}
// 带缓存的搜索函数
const searchCache = new LRUCache(100)
function cachedSearch(fuse, query) {
const cached = searchCache.get(query)
if (cached) return cached
const results = fuse.search(query)
searchCache.set(query, results)
return results
}
🛠️ 高级功能
1. 自定义评分函数
const options = {
// 自定义字段权重
getFn: (obj, path) => {
// 自定义字段获取逻辑
if (path === 'fullName') {
return `${obj.firstName} ${obj.lastName}`
}
return obj[path]
},
// 自定义排序函数
sortFn: (a, b) => {
// 优先显示完全匹配
if (a.score === 0 && b.score !== 0) return -1
if (a.score !== 0 && b.score === 0) return 1
// 按分数排序
return a.score - b.score
}
}
2. 动态更新索引
class DynamicFuse {
constructor(initialData, options) {
this.options = options
this.data = [...initialData]
this.fuse = new Fuse(this.data, options)
}
add(item) {
this.data.push(item)
this.rebuildIndex()
}
remove(predicate) {
this.data = this.data.filter(item => !predicate(item))
this.rebuildIndex()
}
update(predicate, updater) {
this.data = this.data.map(item =>
predicate(item) ? updater(item) : item
)
this.rebuildIndex()
}
rebuildIndex() {
this.fuse = new Fuse(this.data, this.options)
}
search(query) {
return this.fuse.search(query)
}
}
3. 多语言支持
// 多语言搜索配置
const multiLanguageOptions = {
keys: [
'title.zh',
'title.en',
'description.zh',
'description.en'
],
threshold: 0.3,
// 自定义获取函数支持多语言
getFn: (obj, path) => {
const locale = getCurrentLocale() // 获取当前语言
if (path.includes('.')) {
const [field, lang] = path.split('.')
return obj[field] && obj[field][lang]
}
return obj[path]
}
}
// 多语言数据示例
const multiLanguageData = [
{
id: 1,
title: {
zh: '苹果手机',
en: 'Apple iPhone'
},
description: {
zh: '高端智能手机',
en: 'Premium smartphone'
}
}
]
📝 最佳实践
1. 合理设置阈值
// 不同场景的阈值建议
const thresholds = {
exact: 0.0, // 精确匹配
strict: 0.2, // 严格搜索
moderate: 0.4, // 中等容错
loose: 0.6, // 宽松搜索
veryLoose: 0.8 // 非常宽松
}
// 根据数据类型选择合适的阈值
const getThreshold = (dataType) => {
switch (dataType) {
case 'email':
case 'id':
return thresholds.exact
case 'name':
case 'title':
return thresholds.moderate
case 'description':
case 'content':
return thresholds.loose
default:
return thresholds.moderate
}
}
2. 优化搜索键配置
// 智能权重分配
const getSearchKeys = (dataFields) => {
return dataFields.map(field => {
let weight = 0.1 // 默认权重
// 根据字段类型分配权重
if (field.includes('title') || field.includes('name')) {
weight = 0.4
} else if (field.includes('tag') || field.includes('category')) {
weight = 0.3
} else if (field.includes('description') || field.includes('content')) {
weight = 0.2
}
return { name: field, weight }
})
}
3. 错误处理
class SafeFuse {
constructor(data, options) {
try {
this.fuse = new Fuse(data, options)
this.isReady = true
} catch (error) {
console.error('Fuse.js 初始化失败:', error)
this.isReady = false
}
}
search(query) {
if (!this.isReady) {
console.warn('Fuse.js 未就绪,返回原始数据')
return []
}
try {
return this.fuse.search(query)
} catch (error) {
console.error('搜索出错:', error)
return []
}
}
}
🎯 总结
import React, { useState, useMemo, useCallback } from 'react'
import Fuse from 'fuse.js'
// 自定义 Hook:useFuseSearch
function useFuseSearch(data, options) {
const [query, setQuery] = useState('')
const fuse = useMemo(() => {
return new Fuse(data, options)
}, [data, options])
const results = useMemo(() => {
if (!query.trim()) {
return data.map((item, index) => ({ item, refIndex: index }))
}
return fuse.search(query)
}, [fuse, query, data])
return {
query,
setQuery,
results,
search: useCallback((searchQuery) => {
return fuse.search(searchQuery)
}, [fuse])
}
}
// 主搜索组件
function ProductSearch() {
const products = [
{
id: 1,
name: 'MacBook Pro 16寸',
brand: 'Apple',
category: '笔记本电脑',
price: 16999,
tags: ['高性能', '创作', '专业']
},
{
id: 2,
name: 'iPhone 14 Pro',
brand: 'Apple',
category: '智能手机',
price: 7999,
tags: ['摄影', '5G', '高端']
},
{
id: 3,
name: 'Surface Laptop 5',
brand: 'Microsoft',
category: '笔记本电脑',
price: 8888,
tags: ['轻薄', '办公', '便携']
},
{
id: 4,
name: 'Galaxy S23 Ultra',
brand: 'Samsung',
category: '智能手机',
price: 9999,
tags: ['大屏', 'S Pen', '摄影']
}
]
const searchOptions = {
keys: [
{ name: 'name', weight: 0.4 },
{ name: 'brand', weight: 0.3 },
{ name: 'category', weight: 0.2 },
{ name: 'tags', weight: 0.1 }
],
threshold: 0.3,
includeMatches: true,
includeScore: true
}
const { query, setQuery, results } = useFuseSearch(products, searchOptions)
const [sortBy, setSortBy] = useState('relevance')
// 排序结果
const sortedResults = useMemo(() => {
const sorted = [...results]
switch (sortBy) {
case 'price-asc':
return sorted.sort((a, b) => a.item.price - b.item.price)
case 'price-desc':
return sorted.sort((a, b) => b.item.price - a.item.price)
case 'name':
return sorted.sort((a, b) => a.item.name.localeCompare(b.item.name))
default:
return sorted // 保持相关性排序
}
}, [results, sortBy])
// 高亮匹配文本
const highlightMatches = (text, matches) => {
if (!matches || matches.length === 0) return text
const match = matches[0]
if (!match) return text
let highlighted = text
const indices = match.indices
for (let i = indices.length - 1; i >= 0; i--) {
const [start, end] = indices[i]
highlighted = highlighted.slice(0, start) +
'' +
highlighted.slice(start, end + 1) +
'' +
highlighted.slice(end + 1)
}
return highlighted
}
return (
{ maxWidth: '800px', margin: '50px auto', padding: '20px' }}>
🛍️ 产品搜索
{/* 搜索栏 */}
{ marginBottom: '20px' }}>
setQuery(e.target.value)}
placeholder="搜索产品名称、品牌或分类..."
style={{
width: '100%',
padding: '12px',
fontSize: '16px',
border: '2px solid #ddd',
borderRadius: '8px',
outline: 'none'
}}
/>
{/* 排序选项 */}
{ marginBottom: '20px', display: 'flex', gap: '10px', alignItems: 'center' }}>
排序:
{ marginLeft: 'auto', color: '#666' }}>
找到 {sortedResults.length} 个结果
{/* 搜索结果 */}
{ display: 'grid', gap: '15px' }}>
{sortedResults.map((result) => {
const product = result.item
const matches = result.matches || []
const nameMatch = matches.find(m => m.key === 'name')
const brandMatch = matches.find(m => m.key === 'brand')
return (
{
border: '1px solid #eee',
borderRadius: '8px',
padding: '20px',
background: 'white',
boxShadow: '0 2px 4px rgba(0,0,0,0.1)'
}}
>
{ display: 'flex', justifyContent: 'space-between', alignItems: 'start' }}>
{ flex: 1 }}>
{ margin: '0 0 5px 0', fontSize: '18px' }}
dangerouslySetInnerHTML={{
__html: nameMatch ? highlightMatches(product.name, [nameMatch]) : product.name
}}
/>
{ margin: '0 0 10px 0', color: '#666' }}>
品牌:
{
__html: brandMatch ? highlightMatches(product.brand, [brandMatch]) : product.brand
}} />
{' | '}
分类:{product.category}
{ display: 'flex', gap: '5px', marginBottom: '10px' }}>
{product.tags.map(tag => (
{
background: '#f0f0f0',
padding: '2px 8px',
borderRadius: '12px',
fontSize: '12px',
color: '#666'
}}
>
{tag}
))}
{ textAlign: 'right' }}>
{ fontSize: '20px', fontWeight: 'bold', color: '#e74c3c' }}>
¥{product.price.toLocaleString()}
{result.score && (
{ fontSize: '12px', color: '#999', marginTop: '5px' }}>
匹配度: {Math.round((1 - result.score) * 100)}%
)}
)
})}
{sortedResults.length === 0 && query && (
{ textAlign: 'center', padding: '50px', color: '#999' }}>
没有找到匹配的产品
)}
)
}
export default ProductSearch
// 对于大数据集,考虑使用 Web Workers
class FuseWorker {
constructor(data, options) {
this.worker = new Worker('/fuse-worker.js')
this.worker.postMessage({ type: 'init', data, options })
}
search(query) {
return new Promise((resolve) => {
this.worker.onmessage = (e) => {
if (e.data.type === 'search-result') {
resolve(e.data.results)
}
}
this.worker.postMessage({ type: 'search', query })
})
}
}
// fuse-worker.js
let fuse
self.onmessage = function(e) {
const { type, data, options, query } = e.data
if (type === 'init') {
importScripts('https://cdn.jsdelivr.net/npm/fuse.js@7.1.0/dist/fuse.min.js')
fuse = new Fuse(data, options)
}
if (type === 'search' && fuse) {
const results = fuse.search(query)
self.postMessage({ type: 'search-result', results })
}
}
// 使用防抖避免频繁搜索
function useDebounce(value, delay) {
const [debouncedValue, setDebouncedValue] = useState(value)
useEffect(() => {
const handler = setTimeout(() => {
setDebouncedValue(value)
}, delay)
return () => {
clearTimeout(handler)
}
}, [value, delay])
return debouncedValue
}
// 在组件中使用
function SearchComponent() {
const [query, setQuery] = useState('')
const debouncedQuery = useDebounce(query, 300)
const results = useMemo(() => {
if (!debouncedQuery) return []
return fuse.search(debouncedQuery)
}, [debouncedQuery])
// ...
}
// 简单的 LRU 缓存实现
class LRUCache {
constructor(capacity) {
this.capacity = capacity
this.cache = new Map()
}
get(key) {
if (this.cache.has(key)) {
const value = this.cache.get(key)
this.cache.delete(key)
this.cache.set(key, value)
return value
}
return null
}
set(key, value) {
if (this.cache.has(key)) {
this.cache.delete(key)
} else if (this.cache.size >= this.capacity) {
const firstKey = this.cache.keys().next().value
this.cache.delete(firstKey)
}
this.cache.set(key, value)
}
}
// 带缓存的搜索函数
const searchCache = new LRUCache(100)
function cachedSearch(fuse, query) {
const cached = searchCache.get(query)
if (cached) return cached
const results = fuse.search(query)
searchCache.set(query, results)
return results
}
const options = {
// 自定义字段权重
getFn: (obj, path) => {
// 自定义字段获取逻辑
if (path === 'fullName') {
return `${obj.firstName} ${obj.lastName}`
}
return obj[path]
},
// 自定义排序函数
sortFn: (a, b) => {
// 优先显示完全匹配
if (a.score === 0 && b.score !== 0) return -1
if (a.score !== 0 && b.score === 0) return 1
// 按分数排序
return a.score - b.score
}
}
class DynamicFuse {
constructor(initialData, options) {
this.options = options
this.data = [...initialData]
this.fuse = new Fuse(this.data, options)
}
add(item) {
this.data.push(item)
this.rebuildIndex()
}
remove(predicate) {
this.data = this.data.filter(item => !predicate(item))
this.rebuildIndex()
}
update(predicate, updater) {
this.data = this.data.map(item =>
predicate(item) ? updater(item) : item
)
this.rebuildIndex()
}
rebuildIndex() {
this.fuse = new Fuse(this.data, this.options)
}
search(query) {
return this.fuse.search(query)
}
}
// 多语言搜索配置
const multiLanguageOptions = {
keys: [
'title.zh',
'title.en',
'description.zh',
'description.en'
],
threshold: 0.3,
// 自定义获取函数支持多语言
getFn: (obj, path) => {
const locale = getCurrentLocale() // 获取当前语言
if (path.includes('.')) {
const [field, lang] = path.split('.')
return obj[field] && obj[field][lang]
}
return obj[path]
}
}
// 多语言数据示例
const multiLanguageData = [
{
id: 1,
title: {
zh: '苹果手机',
en: 'Apple iPhone'
},
description: {
zh: '高端智能手机',
en: 'Premium smartphone'
}
}
]
// 不同场景的阈值建议
const thresholds = {
exact: 0.0, // 精确匹配
strict: 0.2, // 严格搜索
moderate: 0.4, // 中等容错
loose: 0.6, // 宽松搜索
veryLoose: 0.8 // 非常宽松
}
// 根据数据类型选择合适的阈值
const getThreshold = (dataType) => {
switch (dataType) {
case 'email':
case 'id':
return thresholds.exact
case 'name':
case 'title':
return thresholds.moderate
case 'description':
case 'content':
return thresholds.loose
default:
return thresholds.moderate
}
}
// 智能权重分配
const getSearchKeys = (dataFields) => {
return dataFields.map(field => {
let weight = 0.1 // 默认权重
// 根据字段类型分配权重
if (field.includes('title') || field.includes('name')) {
weight = 0.4
} else if (field.includes('tag') || field.includes('category')) {
weight = 0.3
} else if (field.includes('description') || field.includes('content')) {
weight = 0.2
}
return { name: field, weight }
})
}
class SafeFuse {
constructor(data, options) {
try {
this.fuse = new Fuse(data, options)
this.isReady = true
} catch (error) {
console.error('Fuse.js 初始化失败:', error)
this.isReady = false
}
}
search(query) {
if (!this.isReady) {
console.warn('Fuse.js 未就绪,返回原始数据')
return []
}
try {
return this.fuse.search(query)
} catch (error) {
console.error('搜索出错:', error)
return []
}
}
}
Fuse.js 是一个功能强大且易用的模糊搜索库,适用于各种 JavaScript 应用场景:
✅ 简单易用:API设计简洁,上手快速
✅ 功能丰富:支持模糊搜索、权重配置、高亮显示等
✅ 高度可配置:提供30+个配置选项满足不同需求
✅ 性能优秀:轻量级设计,适合大数据集处理
✅ 跨平台支持:浏览器、Node.js、Deno 全平台兼容
✅ TypeScript友好:完整的类型定义支持
通过合理配置和优化,Fuse.js 可以为您的应用提供专业级的搜索体验,大大提升用户满意度。
开始您的智能搜索之旅吧! 🔍
💡 开发建议:在实际项目中,建议结合防抖、缓存、虚拟滚动等技术,构建高性能的搜索系统。