diff --git a/src/composables/useRegistrySearch.ts b/src/composables/useRegistrySearch.ts index 2960dc74a..8c0c5ffd0 100644 --- a/src/composables/useRegistrySearch.ts +++ b/src/composables/useRegistrySearch.ts @@ -1,7 +1,7 @@ import { watchDebounced } from '@vueuse/core' import type { Hit } from 'algoliasearch/dist/lite/browser' import { memoize, orderBy } from 'lodash' -import { computed, ref, watch } from 'vue' +import { computed, onUnmounted, ref, watch } from 'vue' import { AlgoliaNodePack, @@ -11,10 +11,10 @@ import { import type { NodesIndexSuggestion } from '@/services/algoliaSearchService' import { SortableAlgoliaField } from '@/types/comfyManagerTypes' -const SEARCH_DEBOUNCE_TIME = 256 +const SEARCH_DEBOUNCE_TIME = 320 const DEFAULT_PAGE_SIZE = 64 const DEFAULT_SORT_FIELD = SortableAlgoliaField.Downloads // Set in the index configuration - +const DEFAULT_MAX_CACHE_SIZE = 64 const SORT_DIRECTIONS: Record = { [SortableAlgoliaField.Downloads]: 'desc', [SortableAlgoliaField.Created]: 'desc', @@ -30,7 +30,12 @@ const isDateField = (field: SortableAlgoliaField): boolean => /** * Composable for managing UI state of Comfy Node Registry search. */ -export function useRegistrySearch() { +export function useRegistrySearch( + options: { + maxCacheSize?: number + } = {} +) { + const { maxCacheSize = DEFAULT_MAX_CACHE_SIZE } = options const isLoading = ref(false) const sortField = ref(SortableAlgoliaField.Downloads) const searchMode = ref<'nodes' | 'packs'>('packs') @@ -56,7 +61,10 @@ export function useRegistrySearch() { : [] ) - const { searchPacks, toRegistryPack } = useAlgoliaSearchService() + const { searchPacksCached, toRegistryPack, clearSearchPacksCache } = + useAlgoliaSearchService({ + maxCacheSize + }) const algoliaToRegistry = memoize( toRegistryPack, @@ -77,7 +85,7 @@ export function useRegistrySearch() { if (!options.append) { pageNumber.value = 0 } - const { nodePacks, querySuggestions } = await searchPacks( + const { nodePacks, querySuggestions } = await searchPacksCached( searchQuery.value, { pageSize: pageSize.value, @@ -116,6 +124,8 @@ export function useRegistrySearch() { immediate: true }) + onUnmounted(clearSearchPacksCache) + return { isLoading, pageNumber, diff --git a/src/services/algoliaSearchService.ts b/src/services/algoliaSearchService.ts index 1af48a86d..5b306987a 100644 --- a/src/services/algoliaSearchService.ts +++ b/src/services/algoliaSearchService.ts @@ -1,12 +1,18 @@ +import QuickLRU from '@alloc/quick-lru' import type { BaseSearchParamsWithoutQuery, Hit, + SearchQuery, SearchResponse } from 'algoliasearch/dist/lite/browser' import { liteClient as algoliasearch } from 'algoliasearch/dist/lite/builds/browser' import { omit } from 'lodash' import { components } from '@/types/comfyRegistryTypes' +import { paramsToCacheKey } from '@/utils/formatUtil' + +const DEFAULT_MAX_CACHE_SIZE = 64 +const DEFAULT_MIN_CHARS_FOR_SUGGESTIONS = 2 type SafeNestedProperty< T, @@ -15,6 +21,10 @@ type SafeNestedProperty< > = T[K1] extends undefined | null ? undefined : NonNullable[K2] type RegistryNodePack = components['schemas']['Node'] +type SearchPacksResult = { + nodePacks: Hit[] + querySuggestions: Hit[] +} export interface AlgoliaNodePack { objectID: RegistryNodePack['id'] @@ -91,8 +101,33 @@ type SearchNodePacksParams = BaseSearchParamsWithoutQuery & { restrictSearchableAttributes: SearchAttribute[] } -export const useAlgoliaSearchService = () => { +interface AlgoliaSearchServiceOptions { + /** + * Maximum number of search results to store in the cache. + * The cache is automatically cleared when the component is unmounted. + * @default 64 + */ + maxCacheSize?: number + /** + * Minimum number of characters for suggestions. An additional query + * will be made to the suggestions/completions index for queries that + * are this length or longer. + * @default 3 + */ + minCharsForSuggestions?: number +} + +export const useAlgoliaSearchService = ( + options: AlgoliaSearchServiceOptions = {} +) => { + const { + maxCacheSize = DEFAULT_MAX_CACHE_SIZE, + minCharsForSuggestions = DEFAULT_MIN_CHARS_FOR_SUGGESTIONS + } = options const searchClient = algoliasearch(__ALGOLIA_APP_ID__, __ALGOLIA_API_KEY__) + const searchPacksCache = new QuickLRU({ + maxSize: maxCacheSize + }) const toRegistryLatestVersion = ( algoliaNode: AlgoliaNodePack @@ -141,34 +176,39 @@ export const useAlgoliaSearchService = () => { const searchPacks = async ( query: string, params: SearchNodePacksParams - ): Promise<{ - nodePacks: Hit[] - querySuggestions: Hit[] - }> => { + ): Promise => { const { pageSize, pageNumber } = params const rest = omit(params, ['pageSize', 'pageNumber']) + const requests: SearchQuery[] = [ + { + query, + indexName: 'nodes_index', + attributesToRetrieve: RETRIEVE_ATTRIBUTES, + ...rest, + hitsPerPage: pageSize, + page: pageNumber + } + ] + + const shouldQuerySuggestions = query.length >= minCharsForSuggestions + + // If the query is long enough, also query the suggestions index + if (shouldQuerySuggestions) { + requests.push({ + indexName: 'nodes_index_query_suggestions', + query + }) + } + const { results } = await searchClient.search< AlgoliaNodePack | NodesIndexSuggestion >({ - requests: [ - { - query, - indexName: 'nodes_index', - attributesToRetrieve: RETRIEVE_ATTRIBUTES, - ...rest, - hitsPerPage: pageSize, - page: pageNumber - }, - { - indexName: 'nodes_index_query_suggestions', - query - } - ], + requests, strategy: 'none' }) - const [nodePacks, querySuggestions] = results as [ + const [nodePacks, querySuggestions = { hits: [] }] = results as [ SearchResponse, SearchResponse ] @@ -179,8 +219,27 @@ export const useAlgoliaSearchService = () => { } } + const searchPacksCached = async ( + query: string, + params: SearchNodePacksParams + ): Promise => { + const cacheKey = paramsToCacheKey({ query, ...params }) + const cachedResult = searchPacksCache.get(cacheKey) + if (cachedResult !== undefined) return cachedResult + + const result = await searchPacks(query, params) + searchPacksCache.set(cacheKey, result) + return result + } + + const clearSearchPacksCache = () => { + searchPacksCache.clear() + } + return { searchPacks, - toRegistryPack + searchPacksCached, + toRegistryPack, + clearSearchPacksCache } }