[Manager] Add suggestions to search (#3041)

This commit is contained in:
Christian Byrne
2025-03-14 10:24:28 -07:00
committed by GitHub
parent c6b3e2a0ed
commit 8be25883cd
4 changed files with 96 additions and 27 deletions

View File

@@ -32,6 +32,7 @@
v-model:searchQuery="searchQuery" v-model:searchQuery="searchQuery"
v-model:searchMode="searchMode" v-model:searchMode="searchMode"
:searchResults="searchResults" :searchResults="searchResults"
:suggestions="suggestions"
/> />
<div class="flex-1 overflow-auto"> <div class="flex-1 overflow-auto">
<div <div
@@ -141,8 +142,14 @@ const tabs = ref<TabItem[]>([
]) ])
const selectedTab = ref<TabItem>(tabs.value[0]) const selectedTab = ref<TabItem>(tabs.value[0])
const { searchQuery, pageNumber, isLoading, searchResults, searchMode } = const {
useRegistrySearch() searchQuery,
pageNumber,
isLoading,
searchResults,
searchMode,
suggestions
} = useRegistrySearch()
pageNumber.value = 0 pageNumber.value = 0
const isInitialLoad = computed( const isInitialLoad = computed(

View File

@@ -1,15 +1,29 @@
<template> <template>
<div class="relative w-full p-6"> <div class="relative w-full p-6">
<div class="flex items-center w-full"> <div class="flex items-center w-full">
<IconField class="w-5/12"> <AutoComplete
<InputIcon class="pi pi-search" /> v-model.lazy="searchQuery"
<InputText :suggestions="suggestions || []"
v-model="searchQuery" :placeholder="$t('manager.searchPlaceholder')"
:placeholder="$t('manager.searchPlaceholder')" :complete-on-focus="false"
class="w-full rounded-2xl" :delay="8"
autofocus optionLabel="query"
/> class="w-full"
</IconField> @complete="stubTrue"
@option-select="onOptionSelect"
:pt="{
pcInputText: {
root: {
autofocus: true,
class: 'w-5/12 rounded-2xl'
}
},
loader: {
style: 'display: none'
}
}"
>
</AutoComplete>
</div> </div>
<div class="flex mt-3 text-sm"> <div class="flex mt-3 text-sm">
<div class="flex gap-6 ml-1"> <div class="flex gap-6 ml-1">
@@ -34,18 +48,21 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import IconField from 'primevue/iconfield' import { stubTrue } from 'lodash'
import InputIcon from 'primevue/inputicon' import AutoComplete, {
import InputText from 'primevue/inputtext' AutoCompleteOptionSelectEvent
} from 'primevue/autocomplete'
import { computed } from 'vue' import { computed } from 'vue'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
import SearchFilterDropdown from '@/components/dialog/content/manager/registrySearchBar/SearchFilterDropdown.vue' import SearchFilterDropdown from '@/components/dialog/content/manager/registrySearchBar/SearchFilterDropdown.vue'
import type { NodesIndexSuggestion } from '@/services/algoliaSearchService'
import type { PackField, SearchOption } from '@/types/comfyManagerTypes' import type { PackField, SearchOption } from '@/types/comfyManagerTypes'
import { components } from '@/types/comfyRegistryTypes' import { components } from '@/types/comfyRegistryTypes'
const props = defineProps<{ const { searchResults } = defineProps<{
searchResults?: components['schemas']['Node'][] searchResults?: components['schemas']['Node'][]
suggestions?: NodesIndexSuggestion[]
}>() }>()
const searchQuery = defineModel<string>('searchQuery') const searchQuery = defineModel<string>('searchQuery')
@@ -55,7 +72,7 @@ const sortField = defineModel<PackField>('sortField', { default: 'downloads' })
const { t } = useI18n() const { t } = useI18n()
const hasResults = computed( const hasResults = computed(
() => searchQuery.value?.trim() && props.searchResults?.length () => searchQuery.value?.trim() && searchResults?.length
) )
const sortOptions: SearchOption<PackField>[] = [ const sortOptions: SearchOption<PackField>[] = [
@@ -68,4 +85,8 @@ const filterOptions: SearchOption<string>[] = [
{ id: 'packs', label: t('manager.filter.nodePack') }, { id: 'packs', label: t('manager.filter.nodePack') },
{ id: 'nodes', label: t('g.nodes') } { id: 'nodes', label: t('g.nodes') }
] ]
const onOptionSelect = (event: AutoCompleteOptionSelectEvent) => {
searchQuery.value = event.value.query
}
</script> </script>

View File

@@ -7,6 +7,7 @@ import {
SearchAttribute, SearchAttribute,
useAlgoliaSearchService useAlgoliaSearchService
} from '@/services/algoliaSearchService' } from '@/services/algoliaSearchService'
import type { NodesIndexSuggestion } from '@/services/algoliaSearchService'
import { PackField } from '@/types/comfyManagerTypes' import { PackField } from '@/types/comfyManagerTypes'
const SEARCH_DEBOUNCE_TIME = 16 const SEARCH_DEBOUNCE_TIME = 16
@@ -23,6 +24,7 @@ export function useRegistrySearch() {
const pageNumber = ref(0) const pageNumber = ref(0)
const searchQuery = ref('') const searchQuery = ref('')
const results = ref<AlgoliaNodePack[]>([]) const results = ref<AlgoliaNodePack[]>([])
const suggestions = ref<NodesIndexSuggestion[]>([])
const searchAttributes = computed<SearchAttribute[]>(() => const searchAttributes = computed<SearchAttribute[]>(() =>
searchMode.value === 'nodes' ? ['comfy_nodes'] : ['name', 'description'] searchMode.value === 'nodes' ? ['comfy_nodes'] : ['name', 'description']
@@ -49,11 +51,16 @@ export function useRegistrySearch() {
const onQueryChange = async () => { const onQueryChange = async () => {
isLoading.value = true isLoading.value = true
results.value = await searchPacks(searchQuery.value, { const { nodePacks, querySuggestions } = await searchPacks(
pageSize: pageSize.value, searchQuery.value,
pageNumber: pageNumber.value, {
restrictSearchableAttributes: searchAttributes.value pageSize: pageSize.value,
}) pageNumber: pageNumber.value,
restrictSearchableAttributes: searchAttributes.value
}
)
results.value = nodePacks
suggestions.value = querySuggestions
isLoading.value = false isLoading.value = false
} }
@@ -71,6 +78,7 @@ export function useRegistrySearch() {
sortField, sortField,
searchMode, searchMode,
searchQuery, searchQuery,
suggestions,
searchResults: resultsAsRegistryPacks, searchResults: resultsAsRegistryPacks,
nodeSearchResults: resultsAsNodes nodeSearchResults: resultsAsNodes
} }

View File

@@ -1,6 +1,7 @@
import type { import type {
BaseSearchParamsWithoutQuery, BaseSearchParamsWithoutQuery,
Hit Hit,
SearchResponse
} from 'algoliasearch/dist/lite/browser' } from 'algoliasearch/dist/lite/browser'
import { liteClient as algoliasearch } from 'algoliasearch/dist/lite/builds/browser' import { liteClient as algoliasearch } from 'algoliasearch/dist/lite/builds/browser'
import { omit } from 'lodash' import { omit } from 'lodash'
@@ -50,6 +51,8 @@ export interface AlgoliaNodePack {
'latest_version', 'latest_version',
'comfy_node_extract_status' 'comfy_node_extract_status'
> >
/** `total_install` index only */
icon_url: RegistryNodePack['icon']
} }
export type SearchAttribute = keyof AlgoliaNodePack export type SearchAttribute = keyof AlgoliaNodePack
@@ -70,6 +73,20 @@ const RETRIEVE_ATTRIBUTES: SearchAttribute[] = [
'id' 'id'
] ]
export interface NodesIndexSuggestion {
nb_words: number
nodes_index: {
exact_nb_hits: number
facets: {
exact_matches: Record<string, number>
analytics: Record<string, any>
}
}
objectID: RegistryNodePack['id']
popularity: number
query: string
}
type SearchNodePacksParams = BaseSearchParamsWithoutQuery & { type SearchNodePacksParams = BaseSearchParamsWithoutQuery & {
pageSize: number pageSize: number
pageNumber: number pageNumber: number
@@ -112,6 +129,7 @@ export const useAlgoliaSearchService = () => {
license: algoliaNode.license, license: algoliaNode.license,
downloads: algoliaNode.total_install, downloads: algoliaNode.total_install,
status: algoliaNode.status, status: algoliaNode.status,
icon: algoliaNode.icon_url,
latest_version: toRegistryLatestVersion(algoliaNode), latest_version: toRegistryLatestVersion(algoliaNode),
publisher: toRegistryPublisher(algoliaNode) publisher: toRegistryPublisher(algoliaNode)
} }
@@ -123,11 +141,16 @@ export const useAlgoliaSearchService = () => {
const searchPacks = async ( const searchPacks = async (
query: string, query: string,
params: SearchNodePacksParams params: SearchNodePacksParams
): Promise<Hit<AlgoliaNodePack>[]> => { ): Promise<{
nodePacks: Hit<AlgoliaNodePack>[]
querySuggestions: Hit<NodesIndexSuggestion>[]
}> => {
const { pageSize, pageNumber } = params const { pageSize, pageNumber } = params
const rest = omit(params, ['pageSize', 'pageNumber']) const rest = omit(params, ['pageSize', 'pageNumber'])
const { results } = await searchClient.search<AlgoliaNodePack>({ const { results } = await searchClient.search<
AlgoliaNodePack | NodesIndexSuggestion
>({
requests: [ requests: [
{ {
query, query,
@@ -135,15 +158,25 @@ export const useAlgoliaSearchService = () => {
attributesToRetrieve: RETRIEVE_ATTRIBUTES, attributesToRetrieve: RETRIEVE_ATTRIBUTES,
...rest, ...rest,
hitsPerPage: pageSize, hitsPerPage: pageSize,
length: pageSize,
page: pageNumber page: pageNumber
},
{
indexName: 'nodes_index_query_suggestions',
query
} }
], ],
strategy: 'none' strategy: 'none'
}) })
// Narrow from `SearchResponse<T> | SearchForFacetValuesResponse` to `SearchResponse<T>` const [nodePacks, querySuggestions] = results as [
return 'hits' in results[0] ? results[0].hits : [] // Only querying a single index for now SearchResponse<AlgoliaNodePack>,
SearchResponse<NodesIndexSuggestion>
]
return {
nodePacks: nodePacks.hits,
querySuggestions: querySuggestions.hits
}
} }
return { return {