Skip to content

Commit 160d158

Browse files
authored
fix: search models result in hub should be sorted by weight (#5954)
1 parent 812a808 commit 160d158

File tree

4 files changed

+25
-74
lines changed

4 files changed

+25
-74
lines changed

web-app/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@
4343
"class-variance-authority": "^0.7.1",
4444
"culori": "^4.0.1",
4545
"emoji-picker-react": "^4.12.2",
46+
"fuse.js": "^7.1.0",
4647
"fzf": "^0.5.2",
4748
"i18next": "^25.0.1",
4849
"katex": "^0.16.22",

web-app/src/lib/__tests__/utils.test.ts

Lines changed: 0 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ import {
33
getProviderLogo,
44
getProviderTitle,
55
getReadableLanguageName,
6-
fuzzySearch,
76
toGigabytes,
87
formatMegaBytes,
98
formatDuration,
@@ -67,40 +66,6 @@ describe('getReadableLanguageName', () => {
6766
})
6867
})
6968

70-
describe('fuzzySearch', () => {
71-
it('returns true for exact matches', () => {
72-
expect(fuzzySearch('hello', 'hello')).toBe(true)
73-
expect(fuzzySearch('test', 'test')).toBe(true)
74-
})
75-
76-
it('returns true for subsequence matches', () => {
77-
expect(fuzzySearch('hlo', 'hello')).toBe(true)
78-
expect(fuzzySearch('js', 'javascript')).toBe(true)
79-
expect(fuzzySearch('abc', 'aabbcc')).toBe(true)
80-
})
81-
82-
it('returns false when needle is longer than haystack', () => {
83-
expect(fuzzySearch('hello', 'hi')).toBe(false)
84-
expect(fuzzySearch('test', 'te')).toBe(false)
85-
})
86-
87-
it('returns false for non-matching patterns', () => {
88-
expect(fuzzySearch('xyz', 'hello')).toBe(false)
89-
expect(fuzzySearch('ba', 'abc')).toBe(false)
90-
})
91-
92-
it('handles empty strings', () => {
93-
expect(fuzzySearch('', '')).toBe(true)
94-
expect(fuzzySearch('', 'hello')).toBe(true)
95-
expect(fuzzySearch('h', '')).toBe(false)
96-
})
97-
98-
it('is case sensitive', () => {
99-
expect(fuzzySearch('H', 'hello')).toBe(false)
100-
expect(fuzzySearch('h', 'Hello')).toBe(false)
101-
})
102-
})
103-
10469
describe('toGigabytes', () => {
10570
it('returns empty string for falsy inputs', () => {
10671
expect(toGigabytes(0)).toBe('')

web-app/src/lib/utils.ts

Lines changed: 0 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -99,27 +99,6 @@ export const isLocalProvider = (provider: string) => {
9999
return extension && 'load' in extension
100100
}
101101

102-
export function fuzzySearch(needle: string, haystack: string) {
103-
const hlen = haystack.length
104-
const nlen = needle.length
105-
if (nlen > hlen) {
106-
return false
107-
}
108-
if (nlen === hlen) {
109-
return needle === haystack
110-
}
111-
outer: for (let i = 0, j = 0; i < nlen; i++) {
112-
const nch = needle.charCodeAt(i)
113-
while (j < hlen) {
114-
if (haystack.charCodeAt(j++) === nch) {
115-
continue outer
116-
}
117-
}
118-
return false
119-
}
120-
return true
121-
}
122-
123102
export const toGigabytes = (
124103
input: number,
125104
options?: { hideUnit?: boolean; toFixed?: number }

web-app/src/routes/hub/index.tsx

Lines changed: 24 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { useVirtualizer } from '@tanstack/react-virtual'
33
import { createFileRoute, useNavigate, useSearch } from '@tanstack/react-router'
44
import { route } from '@/constants/routes'
55
import { useModelSources } from '@/hooks/useModelSources'
6-
import { cn, fuzzySearch } from '@/lib/utils'
6+
import { cn } from '@/lib/utils'
77
import {
88
useState,
99
useMemo,
@@ -38,6 +38,7 @@ import { Progress } from '@/components/ui/progress'
3838
import HeaderPage from '@/containers/HeaderPage'
3939
import { Loader } from 'lucide-react'
4040
import { useTranslation } from '@/i18n/react-i18next-compat'
41+
import Fuse from 'fuse.js'
4142

4243
type ModelProps = {
4344
model: CatalogModel
@@ -62,6 +63,12 @@ function Hub() {
6263
{ value: 'newest', name: t('hub:sortNewest') },
6364
{ value: 'most-downloaded', name: t('hub:sortMostDownloaded') },
6465
]
66+
const searchOptions = {
67+
includeScore: true,
68+
// Search in `author` and in `tags` array
69+
keys: ['model_name', 'quants.model_id'],
70+
}
71+
6572
const { sources, addSource, fetchSources, loading } = useModelSources()
6673
const search = useSearch({ from: route.hub.index as any })
6774
const [searchValue, setSearchValue] = useState('')
@@ -177,24 +184,22 @@ function Hub() {
177184
})
178185
}, [sortSelected, sources])
179186

180-
// Filtered models
187+
// Filtered models (debounced search)
188+
const [debouncedSearchValue, setDebouncedSearchValue] = useState(searchValue)
189+
190+
useEffect(() => {
191+
const handler = setTimeout(() => {
192+
setDebouncedSearchValue(searchValue)
193+
}, 300)
194+
return () => clearTimeout(handler)
195+
}, [searchValue])
196+
181197
const filteredModels = useMemo(() => {
182198
let filtered = sortedModels
183199
// Apply search filter
184-
if (searchValue.length) {
185-
filtered = filtered?.filter(
186-
(e) =>
187-
fuzzySearch(
188-
searchValue.replace(/\s+/g, '').toLowerCase(),
189-
e.model_name.toLowerCase()
190-
) ||
191-
e.quants.some((model) =>
192-
fuzzySearch(
193-
searchValue.replace(/\s+/g, '').toLowerCase(),
194-
model.model_id.toLowerCase()
195-
)
196-
)
197-
)
200+
if (debouncedSearchValue.length) {
201+
const fuse = new Fuse(filtered, searchOptions)
202+
filtered = fuse.search(debouncedSearchValue).map((result) => result.item)
198203
}
199204
// Apply downloaded filter
200205
if (showOnlyDownloaded) {
@@ -212,11 +217,12 @@ function Hub() {
212217
}
213218
return filtered
214219
}, [
215-
searchValue,
216220
sortedModels,
221+
debouncedSearchValue,
217222
showOnlyDownloaded,
218-
llamaProvider?.models,
219223
huggingFaceRepo,
224+
searchOptions,
225+
llamaProvider?.models,
220226
])
221227

222228
// The virtualizer

0 commit comments

Comments
 (0)