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", "class-variance-authority": "^0.7.1",
"culori": "^4.0.1", "culori": "^4.0.1",
"emoji-picker-react": "^4.12.2", "emoji-picker-react": "^4.12.2",
"fuse.js": "^7.1.0",
"fzf": "^0.5.2", "fzf": "^0.5.2",
"i18next": "^25.0.1", "i18next": "^25.0.1",
"katex": "^0.16.22", "katex": "^0.16.22",

View File

@ -3,7 +3,6 @@ import {
getProviderLogo, getProviderLogo,
getProviderTitle, getProviderTitle,
getReadableLanguageName, getReadableLanguageName,
fuzzySearch,
toGigabytes, toGigabytes,
formatMegaBytes, formatMegaBytes,
formatDuration, 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', () => { describe('toGigabytes', () => {
it('returns empty string for falsy inputs', () => { it('returns empty string for falsy inputs', () => {
expect(toGigabytes(0)).toBe('') expect(toGigabytes(0)).toBe('')

View File

@ -99,27 +99,6 @@ export const isLocalProvider = (provider: string) => {
return extension && 'load' in extension 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 = ( export const toGigabytes = (
input: number, input: number,
options?: { hideUnit?: boolean; toFixed?: 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 { createFileRoute, useNavigate, useSearch } from '@tanstack/react-router'
import { route } from '@/constants/routes' import { route } from '@/constants/routes'
import { useModelSources } from '@/hooks/useModelSources' import { useModelSources } from '@/hooks/useModelSources'
import { cn, fuzzySearch } from '@/lib/utils' import { cn } from '@/lib/utils'
import { import {
useState, useState,
useMemo, useMemo,
@ -38,6 +38,7 @@ import { Progress } from '@/components/ui/progress'
import HeaderPage from '@/containers/HeaderPage' import HeaderPage from '@/containers/HeaderPage'
import { Loader } from 'lucide-react' import { Loader } from 'lucide-react'
import { useTranslation } from '@/i18n/react-i18next-compat' import { useTranslation } from '@/i18n/react-i18next-compat'
import Fuse from 'fuse.js'
type ModelProps = { type ModelProps = {
model: CatalogModel model: CatalogModel
@ -62,6 +63,12 @@ function Hub() {
{ value: 'newest', name: t('hub:sortNewest') }, { value: 'newest', name: t('hub:sortNewest') },
{ value: 'most-downloaded', name: t('hub:sortMostDownloaded') }, { 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 { sources, addSource, fetchSources, loading } = useModelSources()
const search = useSearch({ from: route.hub.index as any }) const search = useSearch({ from: route.hub.index as any })
const [searchValue, setSearchValue] = useState('') const [searchValue, setSearchValue] = useState('')
@ -177,24 +184,22 @@ function Hub() {
}) })
}, [sortSelected, sources]) }, [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(() => { const filteredModels = useMemo(() => {
let filtered = sortedModels let filtered = sortedModels
// Apply search filter // Apply search filter
if (searchValue.length) { if (debouncedSearchValue.length) {
filtered = filtered?.filter( const fuse = new Fuse(filtered, searchOptions)
(e) => filtered = fuse.search(debouncedSearchValue).map((result) => result.item)
fuzzySearch(
searchValue.replace(/\s+/g, '').toLowerCase(),
e.model_name.toLowerCase()
) ||
e.quants.some((model) =>
fuzzySearch(
searchValue.replace(/\s+/g, '').toLowerCase(),
model.model_id.toLowerCase()
)
)
)
} }
// Apply downloaded filter // Apply downloaded filter
if (showOnlyDownloaded) { if (showOnlyDownloaded) {
@ -212,11 +217,12 @@ function Hub() {
} }
return filtered return filtered
}, [ }, [
searchValue,
sortedModels, sortedModels,
debouncedSearchValue,
showOnlyDownloaded, showOnlyDownloaded,
llamaProvider?.models,
huggingFaceRepo, huggingFaceRepo,
searchOptions,
llamaProvider?.models,
]) ])
// The virtualizer // The virtualizer