Skip to content

Commit 23e0d26

Browse files
add fuzzy searching to assets dialog (#6286)
## Summary Add `useFuse` for assets searching to enable fuzzy searching with typo tolerance. https://github.com/user-attachments/assets/0c55bb77-3223-45ab-8c05-713f8ce4e58b ┆Issue is synchronized with this [Notion page](https://www.notion.so/PR-6286-add-fuzzy-searching-to-assets-dialog-2976d73d3650813da63acadde2cf49c6) by [Unito](https://www.unito.io)
1 parent 95b3b50 commit 23e0d26

File tree

2 files changed

+141
-27
lines changed

2 files changed

+141
-27
lines changed

src/platform/assets/composables/useAssetBrowser.ts

Lines changed: 26 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import { computed, ref } from 'vue'
22
import type { Ref } from 'vue'
3+
import { useFuse } from '@vueuse/integrations/useFuse'
4+
import type { UseFuseOptions } from '@vueuse/integrations/useFuse'
35

46
import { d, t } from '@/i18n'
57
import type { FilterState } from '@/platform/assets/components/AssetFilterBar.vue'
@@ -15,19 +17,6 @@ function filterByCategory(category: string) {
1517
}
1618
}
1719

18-
function filterByQuery(query: string) {
19-
return (asset: AssetItem) => {
20-
if (!query) return true
21-
const lowerQuery = query.toLowerCase()
22-
const description = getAssetDescription(asset)
23-
return (
24-
asset.name.toLowerCase().includes(lowerQuery) ||
25-
(description && description.toLowerCase().includes(lowerQuery)) ||
26-
asset.tags.some((tag) => tag.toLowerCase().includes(lowerQuery))
27-
)
28-
}
29-
}
30-
3120
function filterByFileFormats(formats: string[]) {
3221
return (asset: AssetItem) => {
3322
if (formats.length === 0) return true
@@ -160,9 +149,31 @@ export function useAssetBrowser(
160149
return assets.value.filter(filterByCategory(selectedCategory.value))
161150
})
162151

152+
const fuseOptions: UseFuseOptions<AssetItem> = {
153+
fuseOptions: {
154+
keys: [
155+
{ name: 'name', weight: 0.4 },
156+
{ name: 'tags', weight: 0.3 }
157+
],
158+
threshold: 0.4, // Higher threshold for typo tolerance (0.0 = exact, 1.0 = match all)
159+
ignoreLocation: true, // Search anywhere in the string, not just at the beginning
160+
includeScore: true
161+
},
162+
matchAllWhenSearchEmpty: true
163+
}
164+
165+
const { results: fuseResults } = useFuse(
166+
searchQuery,
167+
categoryFilteredAssets,
168+
fuseOptions
169+
)
170+
171+
const searchFiltered = computed(() =>
172+
fuseResults.value.map((result) => result.item)
173+
)
174+
163175
const filteredAssets = computed(() => {
164-
const filtered = categoryFilteredAssets.value
165-
.filter(filterByQuery(searchQuery.value))
176+
const filtered = searchFiltered.value
166177
.filter(filterByFileFormats(filters.value.fileFormats))
167178
.filter(filterByBaseModels(filters.value.baseModels))
168179

tests-ui/platform/assets/composables/useAssetBrowser.test.ts

Lines changed: 115 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -136,8 +136,8 @@ describe('useAssetBrowser', () => {
136136
})
137137
})
138138

139-
describe('Search Functionality', () => {
140-
it('searches across asset name', async () => {
139+
describe('Fuzzy Search Functionality', () => {
140+
it('searches across asset name with exact match', async () => {
141141
const assets = [
142142
createApiAsset({ name: 'realistic_vision.safetensors' }),
143143
createApiAsset({ name: 'anime_style.ckpt' }),
@@ -149,45 +149,148 @@ describe('useAssetBrowser', () => {
149149
searchQuery.value = 'realistic'
150150
await nextTick()
151151

152-
expect(filteredAssets.value).toHaveLength(2)
152+
expect(filteredAssets.value.length).toBeGreaterThanOrEqual(1)
153153
expect(
154-
filteredAssets.value.every((asset) =>
154+
filteredAssets.value.some((asset) =>
155155
asset.name.toLowerCase().includes('realistic')
156156
)
157157
).toBe(true)
158158
})
159159

160-
it('searches in user metadata description', async () => {
160+
it('searches across asset tags', async () => {
161161
const assets = [
162162
createApiAsset({
163163
name: 'model1.safetensors',
164-
user_metadata: { description: 'fantasy artwork model' }
164+
tags: ['models', 'checkpoints']
165165
}),
166166
createApiAsset({
167167
name: 'model2.safetensors',
168-
user_metadata: { description: 'portrait photography' }
168+
tags: ['models', 'loras']
169169
})
170170
]
171171

172172
const { searchQuery, filteredAssets } = useAssetBrowser(ref(assets))
173173

174-
searchQuery.value = 'fantasy'
174+
searchQuery.value = 'checkpoints'
175175
await nextTick()
176176

177-
expect(filteredAssets.value).toHaveLength(1)
178-
expect(filteredAssets.value[0].name).toBe('model1.safetensors')
177+
expect(filteredAssets.value.length).toBeGreaterThanOrEqual(1)
178+
expect(filteredAssets.value[0].tags).toContain('checkpoints')
179+
})
180+
181+
it('supports fuzzy matching with typos', async () => {
182+
const assets = [
183+
createApiAsset({ name: 'checkpoint_model.safetensors' }),
184+
createApiAsset({ name: 'lora_model.safetensors' })
185+
]
186+
187+
const { searchQuery, filteredAssets } = useAssetBrowser(ref(assets))
188+
189+
// Intentional typo - fuzzy search should still find it
190+
searchQuery.value = 'chckpoint'
191+
await nextTick()
192+
193+
expect(filteredAssets.value.length).toBeGreaterThanOrEqual(1)
194+
expect(filteredAssets.value[0].name).toContain('checkpoint')
195+
})
196+
197+
it('handles empty search by returning all assets', async () => {
198+
const assets = [
199+
createApiAsset({ name: 'test1.safetensors' }),
200+
createApiAsset({ name: 'test2.safetensors' })
201+
]
202+
203+
const { searchQuery, filteredAssets } = useAssetBrowser(ref(assets))
204+
205+
searchQuery.value = ''
206+
await nextTick()
207+
208+
expect(filteredAssets.value).toHaveLength(2)
179209
})
180210

181-
it('handles empty search results', async () => {
211+
it('handles no search results', async () => {
182212
const assets = [createApiAsset({ name: 'test.safetensors' })]
183213

184214
const { searchQuery, filteredAssets } = useAssetBrowser(ref(assets))
185215

186-
searchQuery.value = 'nonexistent'
216+
searchQuery.value = 'completelydifferentstring123'
187217
await nextTick()
188218

189219
expect(filteredAssets.value).toHaveLength(0)
190220
})
221+
222+
it('performs case-insensitive search', async () => {
223+
const assets = [
224+
createApiAsset({ name: 'RealisticVision.safetensors' }),
225+
createApiAsset({ name: 'anime_style.ckpt' })
226+
]
227+
228+
const { searchQuery, filteredAssets } = useAssetBrowser(ref(assets))
229+
230+
searchQuery.value = 'REALISTIC'
231+
await nextTick()
232+
233+
expect(filteredAssets.value.length).toBeGreaterThanOrEqual(1)
234+
expect(filteredAssets.value[0].name).toContain('Realistic')
235+
})
236+
237+
it('combines fuzzy search with format filter', async () => {
238+
const assets = [
239+
createApiAsset({ name: 'my_checkpoint_model.safetensors' }),
240+
createApiAsset({ name: 'my_checkpoint_model.ckpt' }),
241+
createApiAsset({ name: 'different_lora.safetensors' })
242+
]
243+
244+
const { searchQuery, updateFilters, filteredAssets } = useAssetBrowser(
245+
ref(assets)
246+
)
247+
248+
searchQuery.value = 'checkpoint'
249+
updateFilters({
250+
sortBy: 'name-asc',
251+
fileFormats: ['safetensors'],
252+
baseModels: []
253+
})
254+
await nextTick()
255+
256+
expect(filteredAssets.value.length).toBeGreaterThanOrEqual(1)
257+
expect(
258+
filteredAssets.value.every((asset) =>
259+
asset.name.endsWith('.safetensors')
260+
)
261+
).toBe(true)
262+
expect(
263+
filteredAssets.value.some((asset) => asset.name.includes('checkpoint'))
264+
).toBe(true)
265+
})
266+
267+
it('combines fuzzy search with base model filter', async () => {
268+
const assets = [
269+
createApiAsset({
270+
name: 'realistic_sd15.safetensors',
271+
user_metadata: { base_model: 'SD1.5' }
272+
}),
273+
createApiAsset({
274+
name: 'realistic_sdxl.safetensors',
275+
user_metadata: { base_model: 'SDXL' }
276+
})
277+
]
278+
279+
const { searchQuery, updateFilters, filteredAssets } = useAssetBrowser(
280+
ref(assets)
281+
)
282+
283+
searchQuery.value = 'realistic'
284+
updateFilters({
285+
sortBy: 'name-asc',
286+
fileFormats: [],
287+
baseModels: ['SDXL']
288+
})
289+
await nextTick()
290+
291+
expect(filteredAssets.value).toHaveLength(1)
292+
expect(filteredAssets.value[0].name).toBe('realistic_sdxl.safetensors')
293+
})
191294
})
192295

193296
describe('Combined Search and Filtering', () => {

0 commit comments

Comments
 (0)