|
| 1 | +--- |
| 2 | +outline: deep |
| 3 | +--- |
| 4 | + |
| 5 | +<script setup> |
| 6 | +import Badge from '../components/Badge.vue' |
| 7 | +import BlockQuote from '../components/BlockQuote.vue' |
| 8 | +</script> |
| 9 | + |
| 10 | +# Custom Query Filters |
| 11 | + |
| 12 | +The [createPiniaClient](./create-pinia-client#createpiniaclient) function accepts a new `customFilters` option. Custom Filters let you define query superpowers for local queries using the `findInStore` method. |
| 13 | + |
| 14 | +[[toc]] |
| 15 | + |
| 16 | +## Why Use Custom Filters |
| 17 | + |
| 18 | +Custom filters provide the following benefits: |
| 19 | + |
| 20 | +- Custom filters let you attach functionality to your own, custom query parameters. |
| 21 | +- Custom filters run **before** the rest of the query operators, giving you access to the full list of stored items for a service. |
| 22 | +- Custom filters are easier to define than `customSiftOperators` because they need no prior knowledge of custom interfaces. |
| 23 | + |
| 24 | +Note: You cannot override built-in query operators like `$limit`, `$skip`, `$sort`, or `$select` with custom filters. |
| 25 | + |
| 26 | +## The uFuzzy Custom Filter |
| 27 | + |
| 28 | +Feathers-Pinia 4.5+ ships with a built-in custom filter called `uFuzzy`. The `uFuzzy` filter lets you perform fuzzy searches on your local data. In order to use it, you need to install the [@leeoniya/ufuzzy](https://github.com/leeoniya/uFuzzy) package into your project: |
| 29 | + |
| 30 | +```bash |
| 31 | +pnpm install @leeoniya/ufuzzy |
| 32 | +``` |
| 33 | + |
| 34 | +You can now import and use the `createUFuzzyFilter` function to create a custom filter. To keep the feathers-pinia package size smaller, the ufuzzy operator is not provided in the main exports and must be imported from `feathers-pinia/ufuzzy`. In this next example, we create a custom filter called `$fuzzy` that uses the `uFuzzy` operator: |
| 35 | + |
| 36 | +```ts |
| 37 | +import { createUFuzzyFilter } from 'feathers-pinia/ufuzzy' |
| 38 | + |
| 39 | +const api = createPiniaClient(feathersClient, { |
| 40 | + pinia, |
| 41 | + idField: 'id', |
| 42 | + customFilters: [ |
| 43 | + { key: '$fuzzy', operator: createCursorPaginationFilter() }, |
| 44 | + ], |
| 45 | +}) |
| 46 | +``` |
| 47 | + |
| 48 | +You can rename `$fuzzy` to whatever you would like. It will only affect queries that use `findInStore`, including `useFind` when set to `paginateOn: 'client'`. |
| 49 | + |
| 50 | +Now let's use the `$fuzzy` operator to search for messages that contain the phrase "hello world": |
| 51 | + |
| 52 | +```ts |
| 53 | +const { data } = await api.service('messages').findInStore({ |
| 54 | + query: { |
| 55 | + $fuzzy: { |
| 56 | + search: 'hello world', |
| 57 | + fields: ['text'] |
| 58 | + } |
| 59 | + } |
| 60 | +}) |
| 61 | +``` |
| 62 | + |
| 63 | +The `fields` property is an array of fields to search for the `search` term. The `search` term is the string to search for in the specified fields. The `uFuzzy` filter will return all items that fuzzy match the search term in any of the specified fields. |
| 64 | + |
| 65 | +So we can also search across multiple fields. Let's search for users by first name, last name, or email: |
| 66 | + |
| 67 | +```ts |
| 68 | +const { data } = await api.service('users').findInStore({ |
| 69 | + query: { |
| 70 | + $fuzzy: { |
| 71 | + search: 'john', |
| 72 | + fields: ['firstName', 'lastName', 'email'] |
| 73 | + } |
| 74 | + } |
| 75 | +}) |
| 76 | +``` |
| 77 | + |
| 78 | +### Matched __ranges |
| 79 | + |
| 80 | +Search results will have a non-enumerable property added to them called `__ranges`. This property contains the ranges of the matched characters in the search term. This can be useful for highlighting search results in the UI. The `__ranges` property is not enumerable, so it won't show up in JSON.stringify or Vue Devtools. It's an object keyed by field name, so it might have the following structure for the last example: |
| 81 | + |
| 82 | +```ts |
| 83 | +const johnDoe = { |
| 84 | + firstName: 'John', |
| 85 | + lastName: 'Doe', |
| 86 | + |
| 87 | + __ranges: { |
| 88 | + "firstName": [0, 3], |
| 89 | + "lastName": [], |
| 90 | + "email": [], |
| 91 | + } |
| 92 | +} |
| 93 | +``` |
| 94 | + |
| 95 | +Note that only the first 3 characters of the first name matched the search term, even though email also contained a match. This is because the `firstName` field was listed first in the `fields` array. Once a match is found in a field, the search stops and the result is returned. This keeps things fast. |
| 96 | + |
| 97 | +Once you clear the search term, the `__ranges` property will be removed from the result. |
| 98 | + |
| 99 | +### Why not Fuse.js? |
| 100 | + |
| 101 | +Here are observations while working with [Fuse.js](https://www.fusejs.io/) and [uFuzzy](https://github.com/leeoniya/uFuzzy): |
| 102 | + |
| 103 | +1. uFuzzy is **much** faster. We're talking 100x to 1000x faster than Fuse.js. See the benchmarks and compare for yourself. Since we're already working with Vue's reactivity system, there's some overhead involved. The fuzzy search implementation needs to be fast to keep the UI responsive. |
| 104 | +2. Fuse.js is more configurable, which looks great on the surface. In practice, it's only configurable because it doesn't return a great set of results by default. So you need to configure it for every new dataset in order to get good results. The algorithm in uFuzzy tends to return great results using the same configuration across datasets. uFuzzy is also lightly configurable. |
| 105 | +3. uFuzzy returns higher quality results. For example, it prioritizes exact matches before fuzzy matches. This tends to turn up relevant results faster. |
| 106 | +4. Fuse.js is much easier to setup. uFuzzy requires a bit more work to get started. It's more of a low-level API. |
| 107 | +5. Fuse.js supports searching across multiple fields by default. With uFuzzy this requires some extra work. The implementation in feathers-pinia supports searching across multiple fields, making it just as easy to use as Fuse.js. |
| 108 | + |
| 109 | +## Create Your Own Custom Filter |
| 110 | + |
| 111 | +Suppose we have a project that needs to use cursor-based pagination instead of `$skip`-based pagination. Since we can't override `$skip`, we can create a custom filter called `$paginate` that uses an `after` parameter to fetch the next set of items after a specific id. The example might be a bit contrived, but it demonstrates how to create a custom filter. |
| 112 | + |
| 113 | +```ts |
| 114 | +// src/filter.cursor-pagination.ts |
| 115 | + |
| 116 | +// define the shape of your custom operator's options |
| 117 | +export interface PaginateAfterOptions { |
| 118 | + defaultLimit?: number |
| 119 | + idField?: string |
| 120 | +} |
| 121 | + |
| 122 | +// define the shape of the query params specific to your custom operator |
| 123 | +export type PaginateAfterQueryParams { |
| 124 | + after: string |
| 125 | + limit?: number |
| 126 | +} |
| 127 | +const defaultOptions: PaginateAfterOptions = { |
| 128 | + idField: 'id', |
| 129 | + defaultLimit: 10 |
| 130 | +} |
| 131 | + |
| 132 | +export function createCursorPaginationFilter = function (options = {}) { |
| 133 | + const { idField, defaultLimit } = { ...defaultOptions, ...options } |
| 134 | + return <M>(items: M[], queryParams: PaginateAfterQueryParams, query: Record<string, any>) => { |
| 135 | + const { after, limit = defaultLimit } = queryParams |
| 136 | + |
| 137 | + // Find the index of the item with the provided id |
| 138 | + const index = items.findIndex((item: any) => item[idField] === after) |
| 139 | + |
| 140 | + // If the item is not found, return an empty array |
| 141 | + if (index === -1) { |
| 142 | + return [] |
| 143 | + } |
| 144 | + // otherwise return the next set of items after the provided id. |
| 145 | + return items.slice(index + 1, index + limit + 1) |
| 146 | + } |
| 147 | +} |
| 148 | +``` |
| 149 | + |
| 150 | +Now it's ready to import and use in your Feathers Pinia client: |
| 151 | + |
| 152 | +```ts |
| 153 | +// src/feathers-client.ts |
| 154 | + |
| 155 | +import { createCursorPaginationFilter } from './filter.cursor-pagination' |
| 156 | + |
| 157 | +const api = createPiniaClient(feathersClient, { |
| 158 | + pinia, |
| 159 | + idField: 'id', |
| 160 | + customFilters: [ |
| 161 | + { key: '$paginate', operator: createCursorPaginationFilter() }, |
| 162 | + ], |
| 163 | +}) |
| 164 | +``` |
0 commit comments