fix: search models result in hub should be sorted by weight (#5954)

This commit is contained in:
Louis 2025-07-28 23:33:11 +07:00 committed by GitHub
parent 812a8082b8
commit 160d158152
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 25 additions and 74 deletions

View File

@ -43,6 +43,7 @@
"class-variance-authority": "^0.7.1",
"culori": "^4.0.1",
"emoji-picker-react": "^4.12.2",
"fuse.js": "^7.1.0",
"fzf": "^0.5.2",
"i18next": "^25.0.1",
"katex": "^0.16.22",

View File

@ -3,7 +3,6 @@ import {
getProviderLogo,
getProviderTitle,
getReadableLanguageName,
fuzzySearch,
toGigabytes,
formatMegaBytes,
formatDuration,
@ -67,40 +66,6 @@ describe('getReadableLanguageName', () => {
})
})
describe('fuzzySearch', () => {
it('returns true for exact matches', () => {
expect(fuzzySearch('hello', 'hello')).toBe(true)
expect(fuzzySearch('test', 'test')).toBe(true)
})
it('returns true for subsequence matches', () => {
expect(fuzzySearch('hlo', 'hello')).toBe(true)
expect(fuzzySearch('js', 'javascript')).toBe(true)
expect(fuzzySearch('abc', 'aabbcc')).toBe(true)
})
it('returns false when needle is longer than haystack', () => {
expect(fuzzySearch('hello', 'hi')).toBe(false)
expect(fuzzySearch('test', 'te')).toBe(false)
})
it('returns false for non-matching patterns', () => {
expect(fuzzySearch('xyz', 'hello')).toBe(false)
expect(fuzzySearch('ba', 'abc')).toBe(false)
})
it('handles empty strings', () => {
expect(fuzzySearch('', '')).toBe(true)
expect(fuzzySearch('', 'hello')).toBe(true)
expect(fuzzySearch('h', '')).toBe(false)
})
it('is case sensitive', () => {
expect(fuzzySearch('H', 'hello')).toBe(false)
expect(fuzzySearch('h', 'Hello')).toBe(false)
})
})
describe('toGigabytes', () => {
it('returns empty string for falsy inputs', () => {
expect(toGigabytes(0)).toBe('')

View File

@ -99,27 +99,6 @@ export const isLocalProvider = (provider: string) => {
return extension && 'load' in extension
}
export function fuzzySearch(needle: string, haystack: string) {
const hlen = haystack.length
const nlen = needle.length
if (nlen > hlen) {
return false
}
if (nlen === hlen) {
return needle === haystack
}
outer: for (let i = 0, j = 0; i < nlen; i++) {
const nch = needle.charCodeAt(i)
while (j < hlen) {
if (haystack.charCodeAt(j++) === nch) {
continue outer
}
}
return false
}
return true
}
export const toGigabytes = (
input: number,
options?: { hideUnit?: boolean; toFixed?: number }

View File

@ -3,7 +3,7 @@ import { useVirtualizer } from '@tanstack/react-virtual'
import { createFileRoute, useNavigate, useSearch } from '@tanstack/react-router'
import { route } from '@/constants/routes'
import { useModelSources } from '@/hooks/useModelSources'
import { cn, fuzzySearch } from '@/lib/utils'
import { cn } from '@/lib/utils'
import {
useState,
useMemo,
@ -38,6 +38,7 @@ import { Progress } from '@/components/ui/progress'
import HeaderPage from '@/containers/HeaderPage'
import { Loader } from 'lucide-react'
import { useTranslation } from '@/i18n/react-i18next-compat'
import Fuse from 'fuse.js'
type ModelProps = {
model: CatalogModel
@ -62,6 +63,12 @@ function Hub() {
{ value: 'newest', name: t('hub:sortNewest') },
{ value: 'most-downloaded', name: t('hub:sortMostDownloaded') },
]
const searchOptions = {
includeScore: true,
// Search in `author` and in `tags` array
keys: ['model_name', 'quants.model_id'],
}
const { sources, addSource, fetchSources, loading } = useModelSources()
const search = useSearch({ from: route.hub.index as any })
const [searchValue, setSearchValue] = useState('')
@ -177,24 +184,22 @@ function Hub() {
})
}, [sortSelected, sources])
// Filtered models
// Filtered models (debounced search)
const [debouncedSearchValue, setDebouncedSearchValue] = useState(searchValue)
useEffect(() => {
const handler = setTimeout(() => {
setDebouncedSearchValue(searchValue)
}, 300)
return () => clearTimeout(handler)
}, [searchValue])
const filteredModels = useMemo(() => {
let filtered = sortedModels
// Apply search filter
if (searchValue.length) {
filtered = filtered?.filter(
(e) =>
fuzzySearch(
searchValue.replace(/\s+/g, '').toLowerCase(),
e.model_name.toLowerCase()
) ||
e.quants.some((model) =>
fuzzySearch(
searchValue.replace(/\s+/g, '').toLowerCase(),
model.model_id.toLowerCase()
)
)
)
if (debouncedSearchValue.length) {
const fuse = new Fuse(filtered, searchOptions)
filtered = fuse.search(debouncedSearchValue).map((result) => result.item)
}
// Apply downloaded filter
if (showOnlyDownloaded) {
@ -212,11 +217,12 @@ function Hub() {
}
return filtered
}, [
searchValue,
sortedModels,
debouncedSearchValue,
showOnlyDownloaded,
llamaProvider?.models,
huggingFaceRepo,
searchOptions,
llamaProvider?.models,
])
// The virtualizer