Files
jinshen-website/app/utils/fuzzyFilter.ts
R2m1liA 710a0cdc5b
All checks were successful
deploy to server / build-and-deploy (push) Successful in 3m38s
feat: 支持拼音搜索
- 使用pinyin-pro进行汉语拼音转换
- 调整搜索权重
2025-11-10 15:39:47 +08:00

137 lines
3.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import Fuse from 'fuse.js';
interface FuzzyFilterOptions<T> {
/** 匹配关键字 */
keyword: string;
/** 搜索字段 */
keys: Array<FuzzyKeyOf<T>>;
/** 模糊程度 (0~1越低越严格) */
threshold?: number;
/** 最小匹配字符数 */
minMatchCharLength?: number;
/** 当前语言 */
locale?: string;
}
/** 限定 keys 只能选择字符串字段 */
type FuzzyKeyOf<T> = {
[K in keyof T]: T[K] extends string ? K : never;
}[keyof T];
export function fuzzyMatch<T extends object>(
source: T[],
options: FuzzyFilterOptions<T>
): T[] {
const {
keyword,
keys,
threshold = 0.35,
minMatchCharLength = 1,
locale = 'auto',
} = options;
// --- 多语言比较器 ---
const collator = new Intl.Collator(locale === 'auto' ? undefined : locale, {
sensitivity: 'base',
ignorePunctuation: true,
});
// --- 文本标准化函数 ---
const normalizeText = (text: string): string => {
const normalizedText = text.normalize('NFKC').toLowerCase().trim();
const translit = transliterateText(normalizedText);
return `${normalizedText} ${translit}`;
};
/**
* 类型安全的对象取值函数
*/
function getPropertyByPath<T>(
obj: T,
path: string | string[]
): string | undefined {
const keys = Array.isArray(path) ? path : path.split('.');
let value: unknown = obj;
for (const key of keys) {
if (value && typeof value === 'object' && key in value) {
value = (value as Record<string, unknown>)[key];
} else {
return undefined;
}
}
return typeof value === 'string' ? value : undefined;
}
// --- fallback 模糊匹配算法 ---
const fallbackFuzzyMatch = (text: string, pattern: string): boolean => {
const normalizedText = normalizeText(text);
const normalizedPattern = normalizeText(pattern);
let i = 0;
for (const char of normalizedPattern) {
i = normalizedText.indexOf(char, i);
if (i === -1) return false;
i++;
}
return true;
};
const k = keyword.trim();
if (!k) {
return source;
}
if (import.meta.client) {
// 客户端使用Fuze.js进行模糊匹配
const fuse = new Fuse<T>(source, {
keys: keys as string[],
threshold,
minMatchCharLength,
ignoreLocation: true,
findAllMatches: true,
isCaseSensitive: false,
getFn: (obj, path) => {
const value = getPropertyByPath(obj, path);
if (typeof value === 'string') {
const normalized = value
.normalize('NFKC')
.replace(/\s+/g, '')
.toLowerCase()
.trim();
const translit = transliterateText(normalized);
logger.debug(`${normalized} => ${translit}`);
// 拼接原文 + 转写,提升中外文混合匹配效果
return `${normalized} ${translit}`;
}
return value;
},
sortFn: (a, b) => {
const itemA = a.item as T;
const itemB = b.item as T;
const key = keys[0];
const aValue = itemA[key];
const bValue = itemB[key];
if (typeof aValue === 'string' && typeof bValue === 'string') {
return collator.compare(aValue, bValue);
}
return 0;
},
});
logger.debug('Fuzzy search options:', options);
logger.debug('Fuzzy search keyword:', k);
return fuse.search(k).map((result) => result.item);
}
return source.filter((item) =>
keys.some((key) => {
const value = item[key];
return typeof value === 'string' ? fallbackFuzzyMatch(value, k) : false;
})
);
}