TanStack Table Ember 指南:模糊过滤(Fuzzy Filtering)原理与实现
【免费下载链接】table🤖 Headless UI for building powerful tables & datagrids for TS/JS - React-Table, Vue-Table, Solid-Table, Svelte-Table项目地址: https://gitcode.com/gh_mirrors/ta/table
本文以 TanStack Table 的 Ember 集成(@tanstack/ember-table)为主线,完整讲解模糊过滤(fuzzy filtering)的配置方法、底层实现与实战技巧。你将学会如何在 Ember(Glimmer)组件中注册自定义fuzzy过滤/排序函数,结合@tanstack/match-sorter-utils实现基于近似匹配的全局过滤与列过滤,并通过排序让最接近的匹配结果置顶。全文配套可运行示例位于 examples/ember/filters-fuzzy,可作为直接参考的完整实现。
什么是模糊过滤
模糊过滤是一种基于近似匹配(approximate match)的过滤技术:它允许你搜索与给定值“相似”而非“完全相等”的数据。典型场景包括:
- 输入带拼写错误的搜索词(如搜索
jhn仍能命中John); - 按名称、地址等非结构化文本模糊检索;
- 希望搜索结果按相关度排序,让最接近的匹配排在最前面。
TanStack Table 本身并不内置模糊匹配算法,而是通过自定义FilterFn接入。官方推荐使用@tanstack/match-sorter-utils库——它是 match-sorter 的分支,由 Kent C. Dodds 编写,为适配 TanStack Table逐行过滤(row by row filtering)的工作方式做了改进:原 match-sorter 在单次遍历中同时完成过滤与排序,而 match-sorter-utils 将**打分(rank)与比较(compare)**拆分为低层工具,可增量地嵌入表格的过滤/排序管线。
[!NOTE] 使用模糊过滤前,需要先安装
@tanstack/match-sorter-utils:pnpm add @tanstack/match-sorter-utils该库是可选的;你也可以编写自己的过滤函数。但 match-sorter-utils 提供的
rankItem既能过滤,又能返回排序所需的RankingInfo,从而让行按与搜索词的距离排序。
打分模型与匹配等级
rankItem的核心是给候选字符串与搜索词之间打出一个匹配分。打分依据定义在 packages/match-sorter-utils/src/index.ts 的rankings常量中,按匹配强度从高到低排列:
| 等级 | 含义 | 值 |
|---|---|---|
CASE_SENSITIVE_EQUAL | 区分大小写的完全相等 | 7 |
EQUAL | 不区分大小写的完全相等 | 6 |
STARTS_WITH | 以搜索词开头 | 5 |
WORD_STARTS_WITH | 以某个单词开头 | 4 |
CONTAINS | 包含搜索词 | 3 |
ACRONYM | 匹配首字母缩写 | 2 |
MATCHES | 字符按顺序散落匹配(模糊匹配下限) | 1 |
NO_MATCH | 不匹配 | 0 |
从 getMatchRanking 的实现可以看到匹配的判定顺序:先比较长度与大小写、再做小写化后的相等判断、前缀匹配、单词开头匹配、包含匹配,最后通过getClosenessRanking计算字符按顺序出现且间距越近分数越高的散落匹配。默认threshold为rankings.MATCHES(见 rankItem),即只要达到“按顺序散落匹配”即视为通过。该实现还通过prepareValueForComparison内置了去音标(diacritics)处理,让英文重音字符也能正常匹配。
模糊过滤的接入配置
安装特性与行模型
模糊过滤通常需要**列过滤(column filtering)与全局过滤(global filtering)**两个特性,若同时启用客户端模糊排序,还需配置filteredRowModel与sortedRowModel(行模型槽位是类型检查的,必须在特性之后声明):
import { useTable, tableFeatures, columnFilteringFeature, globalFilteringFeature, rowSortingFeature, createFilteredRowModel, createSortedRowModel, metaHelper, } from '@tanstack/ember-table' const features = tableFeatures({ columnFilteringFeature, globalFilteringFeature, rowSortingFeature, filteredRowModel: createFilteredRowModel(), // 客户端过滤(服务端过滤请改用 manualFiltering: true) // manualFiltering: true, // 手动服务端过滤 sortedRowModel: createSortedRowModel(), // 客户端排序(服务端排序请改用 manualSorting: true) // manualSorting: true, // 手动服务端排序 filterFns: { fuzzy: fuzzyFilter }, sortFns: { fuzzy: fuzzySort }, filterMeta: metaHelper<FuzzyFilterMeta>(), }) // 在 Glimmer 组件内 table = useTable(() => ({ features, columns, data: this.data, }))[!NOTE] 上面的
filterFns与sortFns注册表只列出了本指南用到的自定义fuzzy函数。也可以展开整个内置注册表(filterFns: { ...filterFns, fuzzy: fuzzyFilter }),但这会把所有内置函数打进你的 bundle。只注册你用到的函数,或者干脆不注册、直接在列的filterFn/sortFn选项中传入函数引用。
为什么需要filterMeta
模糊排序依赖“行的匹配分数”,而分数在过滤阶段产生。filterMeta槽位(通过metaHelper<FuzzyFilterMeta>()创建)用来在过滤时把RankingInfo暂存到行上,供排序阶段复用。从源码看,columnFiltersMeta是createFilteredRowModel在过滤行模型构建时挂到每行上的元数据存储(见 packages/table-core/src/features/column-filtering/columnFilteringFeature.ts),排序函数正是通过row.columnFiltersMeta[columnId]?.itemRank读取这份数据。
在 Ember 集成中,useTable会通过 use-table.ts 用constructTable构造核心表格,并把用户传入的features合并到coreReactivityFeature之上。所有特性、注册表、行模型均在特性对象作用域内生效,因此无需任何declare module模块扩充——filterFns与filterMeta只对使用该features对象创建的表生效。
定义自定义模糊过滤函数
FilterFn接收row、columnId与过滤值,返回布尔值决定该行是否保留。以下实现先用rankItem打分,把结果存入行过滤元数据,再依据itemRank.passed决定去留:
import { rankItem } from '@tanstack/match-sorter-utils' import type { RankingInfo } from '@tanstack/match-sorter-utils' import type { FilterFn, TableFeatures } from '@tanstack/ember-table' interface FuzzyFilterMeta { itemRank?: RankingInfo } // 携带 filterMeta 形态的特性类型 type FuzzyFeatures = TableFeatures & { filterMeta: FuzzyFilterMeta } const fuzzyFilter: FilterFn<FuzzyFeatures, Person> = ( row, columnId, value, addMeta, ) => { // 对单元格值打分 const itemRank = rankItem(row.getValue(columnId), value as string) // 把打分结果存入过滤元数据 addMeta?.({ itemRank }) // 返回该行是否通过过滤 return itemRank.passed }要点说明:
rankItem对row.getValue(columnId)的值与搜索词(value,此处断言为字符串)进行近似打分;addMeta是可选回调,因此用可选链addMeta?.({ itemRank })调用;它把itemRank挂到row.columnFiltersMeta[columnId]上,供排序复用;value as string是类型断言:模糊匹配假定过滤值为字符串。
注册到特性对象
要让过滤函数能以字符串名'fuzzy'被引用,并且让存储的filterMeta得到类型检查,需要在tableFeatures中同时注册filterFns与filterMeta两个槽位:
import { tableFeatures, metaHelper } from '@tanstack/ember-table' const features = tableFeatures({ columnFilteringFeature, globalFilteringFeature, rowSortingFeature, filteredRowModel: createFilteredRowModel(), sortedRowModel: createSortedRowModel(), filterFns: { fuzzy: fuzzyFilter }, sortFns: { fuzzy: fuzzySort }, filterMeta: metaHelper<FuzzyFilterMeta>(), })不需要declare module扩充。filterFns与filterMeta槽位都作用域于该features对象,只影响用它创建的表。
与全局过滤(Global Filtering)配合使用
模糊过滤最常见的用法是配合全局过滤:一个搜索框过滤所有列。做法是在tableFeatures的filterFns槽位注册fuzzyFilter,然后在表格的globalFilterFn选项中引用字符串名'fuzzy':
import { useTable, tableFeatures, columnFilteringFeature, globalFilteringFeature, rowSortingFeature, createFilteredRowModel, createSortedRowModel, metaHelper, } from '@tanstack/ember-table' const features = tableFeatures({ columnFilteringFeature, globalFilteringFeature, rowSortingFeature, filteredRowModel: createFilteredRowModel(), sortedRowModel: createSortedRowModel(), // 需要按模糊分数排序时使用 filterFns: { fuzzy: fuzzyFilter }, sortFns: { fuzzy: fuzzySort }, filterMeta: metaHelper<FuzzyFilterMeta>(), }) // 在 Glimmer 组件内 table = useTable(() => ({ features, columns, data: this.data, globalFilterFn: 'fuzzy', }))运行示例中,输入框通过this.table.setGlobalFilter(target.value)驱动搜索(见 examples/ember/filters-fuzzy/app/templates/application.gts),占位文案为 "Fuzzy search all columns (typos ok)...",即模糊搜索容忍拼写错误。
与列过滤(Column Filtering)配合使用
模糊过滤同样可以作用在单个列上。先按上文方式在filterFns槽位注册fuzzyFilter,然后在列定义的filterFn选项中按名称引用:
const columns = columnHelper.columns([ columnHelper.accessor((row) => `${row.firstName} ${row.lastName}`, { id: 'fullName', header: 'Full Name', cell: (info) => info.getValue(), filterFn: 'fuzzy', // 使用自定义模糊过滤函数 }), // 其他列... ])示例中把模糊过滤应用到了组合firstName与lastName的列上。运行示例更进一步,为firstName、lastName两列均配置了filterFn: 'fuzzy'(见 application.gts)。
基于匹配分数的模糊排序
列过滤下若还想按“与搜索词的相关度”排序,可以定义自定义排序函数:优先比较两行的itemRank,分数相等时回退到字母序:
import { compareItems } from '@tanstack/match-sorter-utils' import { sortFn_alphanumeric } from '@tanstack/ember-table' import type { SortFn } from '@tanstack/ember-table' const fuzzySort: SortFn<FuzzyFeatures, Person> = (rowA, rowB, columnId) => { let dir = 0 // 仅当列存在打分信息时按分数排序 const rankA = rowA.columnFiltersMeta[columnId]?.itemRank const rankB = rowB.columnFiltersMeta[columnId]?.itemRank if (rankA && rankB) { dir = compareItems(rankA, rankB) } // 分数相同时回退到字母序 return dir === 0 ? sortFn_alphanumeric(rowA, rowB, columnId) : dir }compareItems的实现见 packages/match-sorter-utils/src/index.ts:rank高者排前,相等返回 0。这正是把 match-sorter-utils 拆分为“打分”与“比较”两步的原因——过滤阶段打的分,排序阶段直接复用。
注册fuzzySort到tableFeatures的sortFns槽位(如上文配置),再在列定义中按名称引用:
columnHelper.accessor((row) => `${row.firstName} ${row.lastName}`, { id: 'fullName', header: 'Full Name', cell: (info) => info.getValue(), filterFn: 'fuzzy', // 使用自定义模糊过滤函数(注册在 features 中) sortFn: 'fuzzy', // 使用自定义模糊排序函数(注册在 features 中) })也可以把fuzzySort作为函数直接传给列的sortFn选项(此时无需注册)。
完整可运行示例解读
仓库提供了完整的 Ember 模糊过滤示例:examples/ember/filters-fuzzy。其核心组件 application.gts 展示了生产级用法,值得逐段对照:
- 特性组合:除
columnFilteringFeature、globalFilteringFeature、rowSortingFeature外,还叠加了rowPaginationFeature,并注册了includesString、inNumberRange、fuzzy三种过滤函数以及alphanumeric、text、fuzzy三种排序函数(见 application.gts),示范了“只注册用到的函数”的打包友好实践; - 表格选项:
globalFilterFn: 'fuzzy'启用全局模糊搜索;注释中给出initialState、atoms(外部状态所有权)、受控state+ 回调、manualFiltering、filterFromLeafRows、maxLeafRowFilterDepth、getColumnCanGlobalFilter等多种可选配置(见 application.gts),可对照 docs/framework/ember/guide/global-filtering.md 与 docs/framework/ember/guide/column-filtering.md 深入理解; - 数据规模:默认 2,000 行,点击 "Stress Test (1M rows)" 可加载 100 万行验证模糊过滤性能(数据由 faker 生成,见 make-data.ts);
- 模板渲染:通过
<FlexRenderHeader>与<FlexRenderCell>渲染表头与单元格,并配套分页控制、排序指示符(🔼/🔽)与实时 table state 展示,构成完整的交互式演示。
运行该示例:
# 在仓库根目录使用 pnpm(monorepo) pnpm install cd examples/ember/filters-fuzzy pnpm dev使用建议与注意事项
- bundle 体积:优先按需注册
filterFns/sortFns,而不是展开整个内置注册表;不注册时也可直接传函数引用到列选项。 - 服务端过滤:若数据量极大、需服务端过滤,启用
manualFiltering: true(manualSorting: true同理),此时无需createFilteredRowModel/createSortedRowModel,模糊打分逻辑可在服务端实现。 - 过滤方向:
filterFromLeafRows与maxLeafRowFilterDepth控制嵌套行(subRows)的过滤传播方式,示例数据 make-data.ts 已预留subRows字段,可自行验证。 - 类型安全:
FuzzyFeatures = TableFeatures & { filterMeta: FuzzyFilterMeta }将filterMeta形态贯穿到FilterFn与SortFn,确保row.columnFiltersMeta[columnId]?.itemRank的类型可被推断,这正是metaHelper槽位的作用。 - 匹配下限:默认阈值是
rankings.MATCHES;若希望更严格(如要求至少“包含”级别),可在rankItem中通过threshold选项调整,但示例与过滤器实现均未设置,保持默认即可。
延伸阅读
- 全局过滤指南:全局过滤的通用配置与
globalFilterFn说明; - 列过滤指南:列级
filterFn与过滤元数据细节; - 排序指南:
sortFn与sortedRowModel的深入用法; - match-sorter-utils 源码:
rankItem、compareItems与匹配等级的全部实现; - Ember 集成入口:
@tanstack/ember-table导出的全部 API(useTable、tableFeatures、metaHelper、FlexRenderCell等)。
【免费下载链接】table🤖 Headless UI for building powerful tables & datagrids for TS/JS - React-Table, Vue-Table, Solid-Table, Svelte-Table项目地址: https://gitcode.com/gh_mirrors/ta/table
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考