fix: search models result in hub should be sorted by weight (#5954)
This commit is contained in:
parent
812a8082b8
commit
160d158152
@ -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",
|
||||||
|
|||||||
@ -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('')
|
||||||
|
|||||||
@ -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 }
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user