import Algolia, { SearchClient, AlgoliaSearchOptions } from 'algoliasearch' import { RequestOptions } from '@algolia/transporter' import { SearchOptions, SearchResponse, MultipleQueriesResponse, Hit } from '@algolia/client-search' import { ref, isRef, watch, Ref, getCurrentInstance, onServerPrefetch, onMounted, computed } from '@vue/composition-api' let defaultClient: SearchClient export function createAlgoliaClient (appId: string, apiKey: string, options?: AlgoliaSearchOptions) { return Algolia(appId, apiKey, options) } export function setDefaultAlgoliaClient (client: SearchClient) { defaultClient = client } export function getDefaultAlgoliaClient () { return defaultClient } export type QueryOptions = RequestOptions & SearchOptions export interface UseSearchOptions { skipEmptyQuery?: boolean skipSSR?: boolean } export interface UseSearchReturn { searchText: Ref result: Ref allResults: Ref page: Ref loading: Ref error: Ref search: () => Promise } export function useSearch ( indexName: string, queryParameters: QueryOptions | Ref, defaultQueryParameters: QueryOptions, algoliaClient: SearchClient, options: UseSearchOptions, ): UseSearchReturn> export function useSearch ( indexName: string, queryParameters: QueryOptions[] | Ref, defaultQueryParameters: QueryOptions, algoliaClient: SearchClient, options: UseSearchOptions, ): UseSearchReturn> export function useSearch ( indexName: string, queryParameters: QueryOptions | QueryOptions[] | Ref = {}, defaultQueryParameters: QueryOptions = {}, algoliaClient: SearchClient = defaultClient, options: UseSearchOptions = {}, ): UseSearchReturn | MultipleQueriesResponse> { const vm: any = getCurrentInstance() // Prevent racing conditions let requestId = 0 // SSR const isServer = vm.$isServer let firstResolve: Function let firstReject: Function onServerPrefetch(() => new Promise((resolve, reject) => { firstResolve = resolve firstReject = reject })) // Data const index = algoliaClient.initIndex(indexName) const searchText = ref('') const result: Ref | MultipleQueriesResponse> = ref(null) const allResults: Ref<(SearchResponse | MultipleQueriesResponse)[]> = ref([]) const error = ref(null) const loading = ref(0) const page = ref(0) if (isServer) { search() } else { if (vm.$nuxt) { const data = vm.$nuxt.context.nuxtState[`algolia_${indexName}`] if (data) { result.value = data allResults.value = [data] delete vm.$nuxt.context.nuxtState[`algolia_${indexName}`] } } if (!result.value) { onMounted(() => { search() }) } watch(searchText, () => search(true)) watch(isRef(queryParameters) ? queryParameters : () => queryParameters, () => search(true), { deep: true, }) } async function search (newParams = false) { if (newParams) { page.value = 0 } error.value = null const currentRequestId = ++requestId if ( (options.skipEmptyQuery && !searchText.value) || (isServer && options.skipSSR) ) { result.value = null } else { loading.value++ try { let response: SearchResponse | MultipleQueriesResponse const queryParametersValue = isRef(queryParameters) ? queryParameters.value : queryParameters const isMultiQueries = Array.isArray(queryParametersValue) if (isMultiQueries) { response = await algoliaClient.multipleQueries(queryParametersValue.map(p => ({ indexName, query: searchText.value, ...defaultQueryParameters, ...p, page: page.value, }))) } else { response = await index.search(searchText.value, { ...defaultQueryParameters, ...queryParametersValue, page: page.value, }) } if (requestId !== currentRequestId) { return } result.value = response if (response) { if (newParams) { allResults.value = [] } allResults.value.push(response) } if (isServer && vm.$nuxt) { vm.$nuxt.context.ssrContext.nuxt[`algolia_${indexName}`] = result.value } if (firstResolve) { firstResolve() firstResolve = null } return response } catch (e) { console.error(e) error.value = e if (firstReject) { firstReject(e) firstReject = null } } finally { loading.value-- } } } return { searchText, result, allResults, page: computed({ get: () => page.value, set: (value) => { page.value = value search() }, }), loading: computed(() => loading.value > 0), error, search, } } function isMultiResult (value: SearchResponse | MultipleQueriesResponse): value is MultipleQueriesResponse { return Array.isArray((value as MultipleQueriesResponse).results) } export function combineMultiQueryResults< TResult = any, > (rawResult: SearchResponse | MultipleQueriesResponse, distinctKey: string): { hits: Hit[] } { if (!rawResult) { return null } if (!isMultiResult(rawResult)) { return rawResult } const hits: { hit: Hit, index: number }[] = [] const seen = new Map>() for (const r of rawResult.results) { for (let i = 0; i < r.hits.length; i++) { const hit = r.hits[i] if (seen.has(hit[distinctKey])) { const existing = seen.get(hit[distinctKey]) if (existing._rankingInfo?.userScore < hit._rankingInfo?.userScore) { // @ts-ignore existing._rankingInfo = hit._rankingInfo } continue } seen.set(hit[distinctKey], hit) hits.push({ hit, index: i, }) } } hits.sort((a, b) => { const indexComparison = a.index - b.index if (indexComparison !== 0) { return indexComparison } return b.hit._rankingInfo?.userScore - a.hit._rankingInfo?.userScore }) return { hits: hits.map(h => h.hit), } }