From 07276fdd5ea79fd0164f71b6483e702d7cc03c88 Mon Sep 17 00:00:00 2001 From: Renzo van Hootegem Date: Thu, 15 Aug 2024 16:59:03 +0200 Subject: [PATCH 01/70] Tried to implement algolia categories --- .../mesh/algoliaHitToMagentoCategory.ts | 26 ++++++++++++++ .../algolia-mesh/mesh/getCategoryResults.ts | 34 +++++++++++++++++++ .../mesh/getCategoryResultsInput.ts | 12 +++++++ packages/algolia-mesh/mesh/getIndexName.ts | 4 +-- packages/algolia-mesh/mesh/resolvers.ts | 17 +++++++++- 5 files changed, 90 insertions(+), 3 deletions(-) create mode 100644 packages/algolia-mesh/mesh/algoliaHitToMagentoCategory.ts create mode 100644 packages/algolia-mesh/mesh/getCategoryResults.ts create mode 100644 packages/algolia-mesh/mesh/getCategoryResultsInput.ts diff --git a/packages/algolia-mesh/mesh/algoliaHitToMagentoCategory.ts b/packages/algolia-mesh/mesh/algoliaHitToMagentoCategory.ts new file mode 100644 index 0000000000..7d4e19aff8 --- /dev/null +++ b/packages/algolia-mesh/mesh/algoliaHitToMagentoCategory.ts @@ -0,0 +1,26 @@ +import { + RequireFields, + ResolverFn, + ResolversParentTypes, + ResolversTypes, + MeshContext, + QuerycategoriesArgs, + Algoliahit, +} from '@graphcommerce/graphql-mesh' + +export type CategoriesItemsItem = NonNullable< + Awaited< + ReturnType< + ResolverFn< + ResolversTypes['CategoryResult'], + ResolversParentTypes['Query'], + MeshContext, + RequireFields + > + > + >['items'] +>[] + +export function algoliaHitToMagentoCategory(algoliaHit: Algoliahit): CategoriesItemsItem | null { + return {} +} diff --git a/packages/algolia-mesh/mesh/getCategoryResults.ts b/packages/algolia-mesh/mesh/getCategoryResults.ts new file mode 100644 index 0000000000..8f4fa5211b --- /dev/null +++ b/packages/algolia-mesh/mesh/getCategoryResults.ts @@ -0,0 +1,34 @@ +import { MeshContext, QueryproductsArgs } from '@graphcommerce/graphql-mesh' +import type { GraphQLResolveInfo } from 'graphql' +import { getCategoryResultsInput } from './getCategoryResultsInput' +import { getIndexName } from './getIndexName' + +export async function getCategoryResults( + args: QueryproductsArgs, + context: MeshContext, + info: GraphQLResolveInfo, +) { + return context.algolia.Query.algolia_searchSingleIndex({ + args: { + indexName: getIndexName(context, 'categories'), + input: await getCategoryResultsInput(args), + }, + selectionSet: /* GraphQL */ ` + { + nbPages + hitsPerPage + page + queryID + nbHits + hits { + __typename + objectID + additionalProperties + } + facets + } + `, + context, + info, + }) +} diff --git a/packages/algolia-mesh/mesh/getCategoryResultsInput.ts b/packages/algolia-mesh/mesh/getCategoryResultsInput.ts new file mode 100644 index 0000000000..6bcc731ec3 --- /dev/null +++ b/packages/algolia-mesh/mesh/getCategoryResultsInput.ts @@ -0,0 +1,12 @@ +import { Queryalgolia_searchSingleIndexArgs, QueryproductsArgs } from '@graphcommerce/graphql-mesh' + +// eslint-disable-next-line @typescript-eslint/require-await +export async function getCategoryResultsInput( + args: QueryproductsArgs, +): Promise { + return { + query: args.search ?? '', + hitsPerPage: args.pageSize ? args.pageSize : 10, + page: args.currentPage ? args.currentPage - 1 : 0, + } +} diff --git a/packages/algolia-mesh/mesh/getIndexName.ts b/packages/algolia-mesh/mesh/getIndexName.ts index 4678635a25..a50337ee07 100644 --- a/packages/algolia-mesh/mesh/getIndexName.ts +++ b/packages/algolia-mesh/mesh/getIndexName.ts @@ -5,7 +5,7 @@ function getStoreHeader(context: MeshContext) { return (context as MeshContext & { headers: Record }).headers.store } -export function getIndexName(context: MeshContext) { +export function getIndexName(context: MeshContext, indexType: string = 'products') { const storeCode = getStoreHeader(context) ?? storefrontConfigDefault().magentoStoreCode - return `${import.meta.graphCommerce.algolia.indexNamePrefix}${storeCode}_products` + return `${import.meta.graphCommerce.algolia.indexNamePrefix}${storeCode}_${indexType}` } diff --git a/packages/algolia-mesh/mesh/resolvers.ts b/packages/algolia-mesh/mesh/resolvers.ts index dbf935277f..613cb5b4d5 100644 --- a/packages/algolia-mesh/mesh/resolvers.ts +++ b/packages/algolia-mesh/mesh/resolvers.ts @@ -8,12 +8,13 @@ import { algoliaFacetsToAggregations, getCategoryList } from './algoliaFacetsToA import { algoliaHitToMagentoProduct, ProductsItemsItem } from './algoliaHitToMagentoProduct' import { getAlgoliaSettings } from './getAlgoliaSettings' import { getAttributeList } from './getAttributeList' +import { getCategoryResults } from './getCategoryResults' import { getGroupId } from './getGroupId' import { getSearchResults } from './getSearchResults' import { getSearchSuggestions } from './getSearchSuggestions' +import { isSuggestionsEnabled } from './getSearchSuggestionsInput' import { getStoreConfig } from './getStoreConfig' import { sortingOptions } from './sortOptions' -import { isSuggestionsEnabled } from './getSearchSuggestionsInput' function isAlgoliaResponse( root: T, @@ -115,5 +116,19 @@ export const resolvers: Resolvers = { algolia_queryID: (await searchResults)?.queryID, } }, + categories: async (root, args, context, info) => { + const todo = { algoliaCategories: false } + if (!todo.algoliaCategories) return context.m2.Query.categories({ root, args, context, info }) + const items = await getCategoryResults(args, context, info) + return { + items: [], + page_info: { + current_page: 1, + page_size: 20, + total_pages: 1, + }, + total_count: 0, + } + }, }, } From f3609b38aa96ddcee0c88debebe016a69c161e96 Mon Sep 17 00:00:00 2001 From: Renzo van Hootegem Date: Fri, 16 Aug 2024 16:19:47 +0200 Subject: [PATCH 02/70] Created resolver for algolia categories --- .../mesh/algoliaHitToMagentoCategory.ts | 15 +++++++++--- .../mesh/algoliaHitToMagentoProduct.ts | 2 +- packages/algolia-mesh/mesh/resolvers.ts | 24 +++++++++++++++++-- 3 files changed, 35 insertions(+), 6 deletions(-) diff --git a/packages/algolia-mesh/mesh/algoliaHitToMagentoCategory.ts b/packages/algolia-mesh/mesh/algoliaHitToMagentoCategory.ts index 7d4e19aff8..b7be5850dd 100644 --- a/packages/algolia-mesh/mesh/algoliaHitToMagentoCategory.ts +++ b/packages/algolia-mesh/mesh/algoliaHitToMagentoCategory.ts @@ -7,6 +7,7 @@ import { QuerycategoriesArgs, Algoliahit, } from '@graphcommerce/graphql-mesh' +import { assertAdditional } from './algoliaHitToMagentoProduct' export type CategoriesItemsItem = NonNullable< Awaited< @@ -19,8 +20,16 @@ export type CategoriesItemsItem = NonNullable< > > >['items'] ->[] +>[number] -export function algoliaHitToMagentoCategory(algoliaHit: Algoliahit): CategoriesItemsItem | null { - return {} +export function algoliaHitToMagentoCategory(hit: Algoliahit): CategoriesItemsItem { + const { objectID, additionalProperties } = hit + + return { + name: additionalProperties?.name, + children: null, + products: null, + uid: objectID, + redirect_code: 0, + } } diff --git a/packages/algolia-mesh/mesh/algoliaHitToMagentoProduct.ts b/packages/algolia-mesh/mesh/algoliaHitToMagentoProduct.ts index daca077e06..33dc528e56 100644 --- a/packages/algolia-mesh/mesh/algoliaHitToMagentoProduct.ts +++ b/packages/algolia-mesh/mesh/algoliaHitToMagentoProduct.ts @@ -13,7 +13,7 @@ import { } from '@graphcommerce/graphql-mesh' import { GetStoreConfigReturn } from './getStoreConfig' -function assertAdditional( +export function assertAdditional( additional: unknown, ): additional is AlgoliaProductHitAdditionalProperties { return true diff --git a/packages/algolia-mesh/mesh/resolvers.ts b/packages/algolia-mesh/mesh/resolvers.ts index 613cb5b4d5..5e4533e7fc 100644 --- a/packages/algolia-mesh/mesh/resolvers.ts +++ b/packages/algolia-mesh/mesh/resolvers.ts @@ -5,6 +5,7 @@ import { } from '@graphcommerce/graphql-mesh' import type { GraphQLResolveInfo } from 'graphql' import { algoliaFacetsToAggregations, getCategoryList } from './algoliaFacetsToAggregations' +import { algoliaHitToMagentoCategory, CategoriesItemsItem } from './algoliaHitToMagentoCategory' import { algoliaHitToMagentoProduct, ProductsItemsItem } from './algoliaHitToMagentoProduct' import { getAlgoliaSettings } from './getAlgoliaSettings' import { getAttributeList } from './getAttributeList' @@ -117,9 +118,28 @@ export const resolvers: Resolvers = { } }, categories: async (root, args, context, info) => { - const todo = { algoliaCategories: false } + const todo = { algoliaCategories: true } if (!todo.algoliaCategories) return context.m2.Query.categories({ root, args, context, info }) - const items = await getCategoryResults(args, context, info) + const algoliaResponse = await getCategoryResults(args, context, info) + const items: (CategoriesItemsItem | null)[] = [] + if (!algoliaResponse?.hits) { + return { + items: [], + page_info: { + current_page: 1, + page_size: 20, + total_pages: 1, + }, + total_count: 0, + } + } + for (const hit of algoliaResponse.hits) { + if (hit?.objectID) { + const category = algoliaHitToMagentoCategory(hit) + items.push(category) + } + } + return { items: [], page_info: { From bc014b5c016be5763977fdd828035de57465510f Mon Sep 17 00:00:00 2001 From: Renzo van Hootegem Date: Tue, 20 Aug 2024 13:08:41 +0200 Subject: [PATCH 03/70] Return items --- packages/algolia-mesh/mesh/resolvers.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/algolia-mesh/mesh/resolvers.ts b/packages/algolia-mesh/mesh/resolvers.ts index 5e4533e7fc..193d24de8e 100644 --- a/packages/algolia-mesh/mesh/resolvers.ts +++ b/packages/algolia-mesh/mesh/resolvers.ts @@ -141,7 +141,7 @@ export const resolvers: Resolvers = { } return { - items: [], + items, page_info: { current_page: 1, page_size: 20, From d8766263f97e03316aa10eb1f856164f087a5bbf Mon Sep 17 00:00:00 2001 From: Renzo van Hootegem Date: Thu, 22 Aug 2024 09:37:02 +0200 Subject: [PATCH 04/70] Correct config in personalization readme --- packages/algolia-mesh/mesh/algoliaHitToMagentoCategory.ts | 1 + packages/algolia-personalization/README.md | 8 +++++--- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/packages/algolia-mesh/mesh/algoliaHitToMagentoCategory.ts b/packages/algolia-mesh/mesh/algoliaHitToMagentoCategory.ts index b7be5850dd..6846bbaffa 100644 --- a/packages/algolia-mesh/mesh/algoliaHitToMagentoCategory.ts +++ b/packages/algolia-mesh/mesh/algoliaHitToMagentoCategory.ts @@ -29,6 +29,7 @@ export function algoliaHitToMagentoCategory(hit: Algoliahit): CategoriesItemsIte name: additionalProperties?.name, children: null, products: null, + image: additionalProperties?.image_url, uid: objectID, redirect_code: 0, } diff --git a/packages/algolia-personalization/README.md b/packages/algolia-personalization/README.md index c7d825938a..e05a075213 100644 --- a/packages/algolia-personalization/README.md +++ b/packages/algolia-personalization/README.md @@ -21,9 +21,11 @@ An implementation of Algolia personalization/Analytics through the GraphQL Mesh. ```js const config = { - // Even if you do not use personalization, enabling analytics still allows you to track events in Algolia. - algoliaEnableAnalytics: true, - algoliaPersonalizationEnabled: true, + algolia: { + // Even if you do not use personalization, enabling analytics still allows you to track events in Algolia. + algoliaEnableAnalytics: true, + algoliaPersonalizationEnabled: true, + }, } ``` From 9c26a6aa8a25c23cac0d1bbddf60757f165f31b4 Mon Sep 17 00:00:00 2001 From: Renzo van Hootegem Date: Fri, 23 Aug 2024 14:04:59 +0200 Subject: [PATCH 05/70] Fixed bug where purchase event was not send --- .../algolia-personalization/hooks/useSendAlgoliaEvent.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/algolia-personalization/hooks/useSendAlgoliaEvent.ts b/packages/algolia-personalization/hooks/useSendAlgoliaEvent.ts index bf7befde86..a5134612a5 100644 --- a/packages/algolia-personalization/hooks/useSendAlgoliaEvent.ts +++ b/packages/algolia-personalization/hooks/useSendAlgoliaEvent.ts @@ -4,12 +4,12 @@ import { GoogleEventTypes, sendEvent } from '@graphcommerce/google-datalayer' import { useApolloClient } from '@graphcommerce/graphql' import type { AlgoliaEventsItems_Input } from '@graphcommerce/graphql-mesh' import { CustomerDocument } from '@graphcommerce/magento-customer/hooks/Customer.gql' +import { isFilterTypeEqual, ProductFilterParams } from '@graphcommerce/magento-product' import { cookie } from '@graphcommerce/next-ui' import { useDebounce } from '@graphcommerce/react-hook-form' import { useEventCallback } from '@mui/material' import { useRef } from 'react' import { AlgoliaSendEventDocument } from '../mutations/AlgoliaSendEvent.gql' -import { isFilterTypeEqual, ProductFilterParams } from '@graphcommerce/magento-product' const getSHA256Hash = async (input: string) => { const textAsBuffer = new TextEncoder().encode(input) @@ -184,7 +184,7 @@ const dataLayerToAlgoliaMap: { const queryID = relevant?.[0]?.queryID const filters = [...new Set(...relevant.map((item) => item.filters))] - if (filters.length) { + if (filters.length > 0) { // There is a max of 10 filters per event, if there are more than 10 items // we need to split the event into multiple events for (let i = 0; i < filters.length; i += 10) { @@ -249,7 +249,7 @@ const dataLayerToAlgoliaMap: { const relevant = objectIDs.map((objectID) => mapping[objectID]) const filters = [...new Set(...relevant.map((item) => item.filters))] - if (filters.length) { + if (filters.length > 0) { // There is a max of 10 filters per event, if there are more than 10 items // we need to split the event into multiple events for (let i = 0; i < filters.length; i += 10) { From 214c9602fcc847022b9d93c3e6dc62e9fba77857 Mon Sep 17 00:00:00 2001 From: Renzo van Hootegem Date: Tue, 27 Aug 2024 12:20:37 +0200 Subject: [PATCH 06/70] Map breadcrumbs --- packages/algolia-mesh/mesh/algoliaHitToMagentoCategory.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/packages/algolia-mesh/mesh/algoliaHitToMagentoCategory.ts b/packages/algolia-mesh/mesh/algoliaHitToMagentoCategory.ts index 6846bbaffa..06cfbb5b1d 100644 --- a/packages/algolia-mesh/mesh/algoliaHitToMagentoCategory.ts +++ b/packages/algolia-mesh/mesh/algoliaHitToMagentoCategory.ts @@ -7,7 +7,6 @@ import { QuerycategoriesArgs, Algoliahit, } from '@graphcommerce/graphql-mesh' -import { assertAdditional } from './algoliaHitToMagentoProduct' export type CategoriesItemsItem = NonNullable< Awaited< @@ -22,6 +21,11 @@ export type CategoriesItemsItem = NonNullable< >['items'] >[number] +function mapBreadcrumbs(algoliaPath) { + const pathArray = algoliaPath.split('/') + return pathArray.map((item) => ({ category_name: item })) +} + export function algoliaHitToMagentoCategory(hit: Algoliahit): CategoriesItemsItem { const { objectID, additionalProperties } = hit @@ -32,5 +36,6 @@ export function algoliaHitToMagentoCategory(hit: Algoliahit): CategoriesItemsIte image: additionalProperties?.image_url, uid: objectID, redirect_code: 0, + breadcrumbs: mapBreadcrumbs(additionalProperties?.path), } } From 0eed7ad89bbf755c864cc8592ad3704c9759f1bd Mon Sep 17 00:00:00 2001 From: Renzo van Hootegem Date: Tue, 27 Aug 2024 12:23:01 +0200 Subject: [PATCH 07/70] Reduced length of values, so event gets send --- .../hooks/useSendAlgoliaEvent.ts | 23 +++++++++---------- 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/packages/algolia-personalization/hooks/useSendAlgoliaEvent.ts b/packages/algolia-personalization/hooks/useSendAlgoliaEvent.ts index a5134612a5..5c3416c6b1 100644 --- a/packages/algolia-personalization/hooks/useSendAlgoliaEvent.ts +++ b/packages/algolia-personalization/hooks/useSendAlgoliaEvent.ts @@ -208,12 +208,12 @@ const dataLayerToAlgoliaMap: { eventSubtype: 'addToCart', objectIDs: eventData.items.map((item) => atob(item.item_uid)), objectData: eventData.items.map((item) => ({ - discount: { Float: item.discount ?? 0 }, - price: { Float: Number(item.price.toFixed(15)) }, + discount: { Float: Number(item.discount?.toFixed(14)) ?? 0 }, + price: { Float: Number(item.price.toFixed(14)) }, quantity: item.quantity, })), currency: eventData.currency, - value: { Float: Number(eventData.value.toFixed(15)) }, + value: { Float: Number(eventData.value.toFixed(14)) }, ...common, }, } satisfies AlgoliaEventsItems_Input) @@ -226,11 +226,11 @@ const dataLayerToAlgoliaMap: { objectIDs: eventData.items.map((item) => atob(item.item_uid)), objectData: eventData.items.map((item) => ({ discount: { Float: item.discount ?? 0 }, - price: { Float: Number(item.price.toFixed(15)) }, + price: { Float: Number(item.price.toFixed(14)) }, quantity: item.quantity, })), currency: eventData.currency, - value: { Float: Number(eventData.value.toFixed(15)) }, + value: { Float: Number(eventData.value.toFixed(14)) }, ...common, }, } satisfies AlgoliaEventsItems_Input) @@ -272,13 +272,13 @@ const dataLayerToAlgoliaMap: { eventSubtype: 'purchase', objectIDs: eventData.items.map((item) => atob(item.item_uid)), objectData: eventData.items.map((item) => ({ - discount: { Float: item.discount ?? 0 }, - price: { Float: Number(item.price.toFixed(15)) }, + discount: { Float: Number(item.discount?.toFixed(14)) ?? 0 }, + price: { Float: Number(item.price.toFixed(14)) }, quantity: item.quantity, queryID: mapping[atob(item.item_uid)]?.queryID, })), currency: eventData.currency, - value: { Float: Number(eventData.value.toFixed(15)) }, + value: { Float: Number(eventData.value.toFixed(13)) }, ...common, }, } satisfies AlgoliaEventsItems_Input) @@ -290,12 +290,12 @@ const dataLayerToAlgoliaMap: { eventSubtype: 'purchase', objectIDs: eventData.items.map((item) => atob(item.item_uid)), objectData: eventData.items.map((item) => ({ - discount: { Float: item.discount ?? 0 }, - price: { Float: Number(item.price.toFixed(15)) }, + discount: { Float: Number(item.discount?.toFixed(14)) ?? 0 }, + price: { Float: Number(item.price.toFixed(14)) }, quantity: item.quantity, })), currency: eventData.currency, - value: { Float: Number(eventData.value.toFixed(15)) }, + value: { Float: Number(eventData.value.toFixed(13)) }, ...common, }, } satisfies AlgoliaEventsItems_Input) @@ -356,7 +356,6 @@ export function useSendAlgoliaEvent() { cookie(ALGOLIA_USER_TOKEN_COOKIE_NAME, userToken, { sameSite: true }) } - // todo check if valid // if (authenticatedUserToken) { // userToken = authenticatedUserToken // } From ceaea5dc3b3607aa054e13b6f7ca479738d3a373 Mon Sep 17 00:00:00 2001 From: Renzo van Hootegem Date: Tue, 27 Aug 2024 13:24:52 +0200 Subject: [PATCH 08/70] Added typing --- .../mesh/algoliaHitToMagentoCategory.ts | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/packages/algolia-mesh/mesh/algoliaHitToMagentoCategory.ts b/packages/algolia-mesh/mesh/algoliaHitToMagentoCategory.ts index 06cfbb5b1d..3765f90fa4 100644 --- a/packages/algolia-mesh/mesh/algoliaHitToMagentoCategory.ts +++ b/packages/algolia-mesh/mesh/algoliaHitToMagentoCategory.ts @@ -6,8 +6,19 @@ import { MeshContext, QuerycategoriesArgs, Algoliahit, + AlgoliaProductHitAdditionalProperties, } from '@graphcommerce/graphql-mesh' +export type AlgoliaCategoryHitAddiotonalProperties = AlgoliaProductHitAdditionalProperties & { + path: string +} + +export function assertAdditional( + additional: unknown, +): additional is AlgoliaCategoryHitAddiotonalProperties { + return true +} + export type CategoriesItemsItem = NonNullable< Awaited< ReturnType< @@ -29,6 +40,8 @@ function mapBreadcrumbs(algoliaPath) { export function algoliaHitToMagentoCategory(hit: Algoliahit): CategoriesItemsItem { const { objectID, additionalProperties } = hit + if (!assertAdditional(additionalProperties)) return null + return { name: additionalProperties?.name, children: null, From 0959d190ce8b5a40cd215c9a4726c6e5bcdee559 Mon Sep 17 00:00:00 2001 From: Renzo van Hootegem Date: Tue, 27 Aug 2024 15:52:27 +0200 Subject: [PATCH 09/70] Mapped url path --- .../mesh/algoliaHitToMagentoCategory.ts | 18 ++++++++++++++---- packages/algolia-mesh/mesh/resolvers.ts | 3 ++- 2 files changed, 16 insertions(+), 5 deletions(-) diff --git a/packages/algolia-mesh/mesh/algoliaHitToMagentoCategory.ts b/packages/algolia-mesh/mesh/algoliaHitToMagentoCategory.ts index 3765f90fa4..5a269714ad 100644 --- a/packages/algolia-mesh/mesh/algoliaHitToMagentoCategory.ts +++ b/packages/algolia-mesh/mesh/algoliaHitToMagentoCategory.ts @@ -8,9 +8,12 @@ import { Algoliahit, AlgoliaProductHitAdditionalProperties, } from '@graphcommerce/graphql-mesh' +import { algoliaUrlToUrlKey } from './algoliaHitToMagentoProduct' +import { GetStoreConfigReturn } from './getStoreConfig' export type AlgoliaCategoryHitAddiotonalProperties = AlgoliaProductHitAdditionalProperties & { path: string + url: string } export function assertAdditional( @@ -33,15 +36,20 @@ export type CategoriesItemsItem = NonNullable< >[number] function mapBreadcrumbs(algoliaPath) { - const pathArray = algoliaPath.split('/') - return pathArray.map((item) => ({ category_name: item })) + const pathArray = algoliaPath.split(' / ') + return pathArray.map((item) => ({ + category_name: item, + category_uid: 0, + })) } -export function algoliaHitToMagentoCategory(hit: Algoliahit): CategoriesItemsItem { +export function algoliaHitToMagentoCategory( + hit: Algoliahit, + storeConfig: GetStoreConfigReturn, +): CategoriesItemsItem { const { objectID, additionalProperties } = hit if (!assertAdditional(additionalProperties)) return null - return { name: additionalProperties?.name, children: null, @@ -49,6 +57,8 @@ export function algoliaHitToMagentoCategory(hit: Algoliahit): CategoriesItemsIte image: additionalProperties?.image_url, uid: objectID, redirect_code: 0, + url_key: '', + url_path: algoliaUrlToUrlKey(additionalProperties.url, storeConfig?.base_link_url), breadcrumbs: mapBreadcrumbs(additionalProperties?.path), } } diff --git a/packages/algolia-mesh/mesh/resolvers.ts b/packages/algolia-mesh/mesh/resolvers.ts index 193d24de8e..ec6460f21e 100644 --- a/packages/algolia-mesh/mesh/resolvers.ts +++ b/packages/algolia-mesh/mesh/resolvers.ts @@ -122,6 +122,7 @@ export const resolvers: Resolvers = { if (!todo.algoliaCategories) return context.m2.Query.categories({ root, args, context, info }) const algoliaResponse = await getCategoryResults(args, context, info) const items: (CategoriesItemsItem | null)[] = [] + const storeConfig = await getStoreConfig(context) if (!algoliaResponse?.hits) { return { items: [], @@ -135,7 +136,7 @@ export const resolvers: Resolvers = { } for (const hit of algoliaResponse.hits) { if (hit?.objectID) { - const category = algoliaHitToMagentoCategory(hit) + const category = algoliaHitToMagentoCategory(hit, storeConfig) items.push(category) } } From 2c49cc5042a7ccfd6b2a09556c1fb02804f7ca19 Mon Sep 17 00:00:00 2001 From: Renzo van Hootegem Date: Tue, 27 Aug 2024 15:52:53 +0200 Subject: [PATCH 10/70] Added user token to suggestions query --- packages/algolia-mesh/mesh/getSearchSuggestionsInput.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/algolia-mesh/mesh/getSearchSuggestionsInput.ts b/packages/algolia-mesh/mesh/getSearchSuggestionsInput.ts index e813d2cff1..360eb32315 100644 --- a/packages/algolia-mesh/mesh/getSearchSuggestionsInput.ts +++ b/packages/algolia-mesh/mesh/getSearchSuggestionsInput.ts @@ -1,5 +1,6 @@ import { MeshContext, Queryalgolia_searchSingleIndexArgs } from '@graphcommerce/graphql-mesh' import { getIndexName } from './getIndexName' +import { getUserToken } from '@graphcommerce/algolia-personalization/mesh/getUserToken' export function isSuggestionsEnabled() { return Boolean(import.meta.graphCommerce.algolia.suggestionsSuffix) @@ -18,5 +19,6 @@ export async function getSearchSuggestionsInput( query: search, hitsPerPage: 5, analytics: true, + userToken: getUserToken(context), } } From 3e560c33edeb49393e361c83d2ff408bf5cf3682 Mon Sep 17 00:00:00 2001 From: Renzo van Hootegem Date: Fri, 30 Aug 2024 11:26:26 +0200 Subject: [PATCH 11/70] algolia-recommend: added openapi --- .../plugins/meshConfigAlgoliaInsights.ts | 5 +- .../algolia-recommend-spec.yaml | 1816 +++++++++++++++++ .../plugins/meshConfigAlgoliaRecommend.ts | 59 + .../scripts/generate-recommend-spec.mts | 74 + 4 files changed, 1950 insertions(+), 4 deletions(-) create mode 100644 packages/algolia-recommend/algolia-recommend-spec.yaml create mode 100644 packages/algolia-recommend/plugins/meshConfigAlgoliaRecommend.ts create mode 100644 packages/algolia-recommend/scripts/generate-recommend-spec.mts diff --git a/packages/algolia-personalization/plugins/meshConfigAlgoliaInsights.ts b/packages/algolia-personalization/plugins/meshConfigAlgoliaInsights.ts index 6a9fe56c0c..24b1f2be7a 100644 --- a/packages/algolia-personalization/plugins/meshConfigAlgoliaInsights.ts +++ b/packages/algolia-personalization/plugins/meshConfigAlgoliaInsights.ts @@ -31,10 +31,7 @@ export const meshConfig: FunctionPlugin = ( 'X-Algolia-Application-Id': graphCommerceConfig.algolia.applicationId, 'X-Algolia-API-Key': graphCommerceConfig.algolia.searchOnlyApiKey, }, - selectQueryOrMutationField: [ - { type: 'Query', fieldName: 'searchSingleIndex' }, - { type: 'Query', fieldName: 'searchForFacetValues' }, - ], + selectQueryOrMutationField: [{ type: 'Query', fieldName: 'sendEvent' }], }, }, transforms: [ diff --git a/packages/algolia-recommend/algolia-recommend-spec.yaml b/packages/algolia-recommend/algolia-recommend-spec.yaml new file mode 100644 index 0000000000..69b75f66e5 --- /dev/null +++ b/packages/algolia-recommend/algolia-recommend-spec.yaml @@ -0,0 +1,1816 @@ +{ + "openapi": "3.0.2", + "info": { + "title": "Recommend API", + "description": "The Recommend API lets you retrieve recommendations from one of Algolia's AI recommendation models that you previously trained on your data.\n\n## Client libraries\n\nUse Algolia's API clients and libraries to reliably integrate Algolia's APIs with your apps.\nThe official API clients are covered by Algolia's [Service Level Agreement](https://www.algolia.com/policies/sla/).\n\nSee: [Algolia's ecosystem](https://www.algolia.com/doc/guides/getting-started/how-algolia-works/in-depth/ecosystem/)\n\n## Base URLs\n\nThe base URLs for requests to the Recommend API are:\n\n- `https://{APPLICATION_ID}.algolia.net`\n- `https://{APPLICATION_ID}-dsn.algolia.net`.\n If your subscription includes a [Distributed Search Network](https://dashboard.algolia.com/infra),\n this ensures that requests are sent to servers closest to users.\n\nBoth URLs provide high availability by distributing requests with load balancing.\n\n**All requests must use HTTPS.**\n\n## Retry strategy\n\nTo guarantee a high availability, implement a retry strategy for all API requests using the URLs of your servers as fallbacks:\n\n- `https://{APPLICATION_ID}-1.algolianet.com`\n- `https://{APPLICATION_ID}-2.algolianet.com`\n- `https://{APPLICATION_ID}-3.algolianet.com`\n\nThese URLs use a different DNS provider than the primary URLs.\nYou should randomize this list to ensure an even load across the three servers.\n\nAll Algolia API clients implement this retry strategy.\n\n## Authentication\n\nTo authenticate your API requests, add these headers:\n\n- `x-algolia-application-id`. Your Algolia application ID.\n- `x-algolia-api-key`. An API key with the necessary permissions to make the request.\n The required access control list (ACL) to make a request is listed in each endpoint's reference.\n\nYou can find your application ID and API key in the [Algolia dashboard](https://dashboard.algolia.com/account).\n\n## Request format\n\nRequest bodies must be JSON objects.\n\n## Response status and errors\n\nThe Recommend API returns JSON responses.\nSince JSON doesn't guarantee any specific ordering, don't rely on the order of attributes in the API response.\n\nSuccessful responses return a `2xx` status. Client errors return a `4xx` status. Server errors are indicated by a `5xx` status.\nError responses have a `message` property with more information.\n\n## Version\n\nThe current version of the Recommend API is version 1, as indicated by the `/1/` in each endpoint's URL.\n", + "version": "1.0.0" + }, + "paths": { + "/1/indexes/*/recommendations": { + "post": { + "tags": ["recommend"], + "operationId": "getRecommendations", + "x-use-read-transporter": true, + "x-cacheable": true, + "x-acl": ["search"], + "x-legacy-signature-recommend": true, + "summary": "Retrieve recommendations", + "description": "Retrieves recommendations from selected AI models.\n", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "title": "getRecommendationsParams", + "description": "Recommend request body.", + "type": "object", + "additionalProperties": false, + "properties": { + "requests": { + "type": "array", + "description": "Recommendation request with parameters depending on the requested model.", + "items": { "$ref": "#/components/schemas/recommendationsRequest" } + } + }, + "required": ["requests"] + } + } + } + }, + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "title": "getRecommendationsResponse", + "type": "object", + "additionalProperties": false, + "properties": { + "results": { + "type": "array", + "items": { "$ref": "#/components/schemas/recommendationsResults" } + } + }, + "required": ["results"] + } + } + } + }, + "400": { "$ref": "#/components/responses/BadRequest" }, + "402": { "$ref": "#/components/responses/FeatureNotEnabled" }, + "403": { "$ref": "#/components/responses/MethodNotAllowed" }, + "404": { "$ref": "#/components/responses/IndexNotFound" } + } + } + } + }, + "components": { + "securitySchemes": { + "appId": { + "type": "apiKey", + "in": "header", + "name": "x-algolia-application-id", + "description": "Your Algolia application ID." + }, + "apiKey": { + "type": "apiKey", + "in": "header", + "name": "x-algolia-api-key", + "description": "Your Algolia API key with the necessary permissions to make the request.\nPermissions are controlled through access control lists (ACL) and access restrictions.\nThe required ACL to make a request is listed in each endpoint's reference.\n" + } + }, + "parameters": { + "PathInPath": { + "name": "path", + "in": "path", + "description": "Path of the endpoint, anything after \"/1\" must be specified.", + "required": true, + "schema": { "type": "string", "example": "/keys" } + }, + "Parameters": { + "name": "parameters", + "in": "query", + "description": "Query parameters to apply to the current query.", + "schema": { "type": "object", "additionalProperties": true } + }, + "IndexName": { + "name": "indexName", + "in": "path", + "description": "Name of the index on which to perform the operation.", + "required": true, + "schema": { "type": "string", "example": "YourIndexName" } + }, + "Models": { + "in": "path", + "name": "model", + "required": true, + "description": "[Recommend model](https://www.algolia.com/doc/guides/algolia-recommend/overview/#recommend-models).\n", + "schema": { "$ref": "#/components/schemas/recommendModels" } + }, + "ObjectID": { + "name": "objectID", + "in": "path", + "description": "Unique record identifier.", + "required": true, + "schema": { "$ref": "#/components/schemas/objectID" } + } + }, + "schemas": { + "ErrorBase": { + "description": "Error.", + "type": "object", + "x-keep-model": true, + "additionalProperties": true, + "properties": { + "message": { "type": "string", "example": "Invalid Application-Id or API-Key" } + } + }, + "indexName": { + "type": "string", + "example": "products", + "description": "Index name (case-sensitive)." + }, + "query": { + "type": "string", + "description": "Search query.", + "default": "", + "x-categories": ["Search"] + }, + "searchParamsQuery": { + "type": "object", + "additionalProperties": false, + "properties": { "query": { "$ref": "#/components/schemas/query" } } + }, + "filters": { + "type": "string", + "description": "Filter expression to only include items that match the filter criteria in the response.\n\nYou can use these filter expressions:\n\n- **Numeric filters.** ` `, where `` is one of `<`, `<=`, `=`, `!=`, `>`, `>=`.\n- **Ranges.** `: TO ` where `` and `` are the lower and upper limits of the range (inclusive).\n- **Facet filters.** `:` where `` is a facet attribute (case-sensitive) and `` a facet value.\n- **Tag filters.** `_tags:` or just `` (case-sensitive).\n- **Boolean filters.** `: true | false`.\n\nYou can combine filters with `AND`, `OR`, and `NOT` operators with the following restrictions:\n\n- You can only combine filters of the same type with `OR`.\n **Not supported:** `facet:value OR num > 3`.\n- You can't use `NOT` with combinations of filters.\n **Not supported:** `NOT(facet:value OR facet:value)`\n- You can't combine conjunctions (`AND`) with `OR`.\n **Not supported:** `facet:value OR (facet:value AND facet:value)`\n\nUse quotes around your filters, if the facet attribute name or facet value has spaces, keywords (`OR`, `AND`, `NOT`), or quotes.\nIf a facet attribute is an array, the filter matches if it matches at least one element of the array.\n\nFor more information, see [Filters](https://www.algolia.com/doc/guides/managing-results/refine-results/filtering/).\n", + "example": "(category:Book OR category:Ebook) AND _tags:published", + "x-categories": ["Filtering"] + }, + "facetFilters": { + "description": "Filter the search by facet values, so that only records with the same facet values are retrieved.\n\n**Prefer using the `filters` parameter, which supports all filter types and combinations with boolean operators.**\n\n- `[filter1, filter2]` is interpreted as `filter1 AND filter2`.\n- `[[filter1, filter2], filter3]` is interpreted as `filter1 OR filter2 AND filter3`.\n- `facet:-value` is interpreted as `NOT facet:value`.\n\nWhile it's best to avoid attributes that start with a `-`, you can still filter them by escaping with a backslash:\n`facet:\\-value`.\n", + "example": [["category:Book", "category:-Movie"], "author:John Doe"], + "oneOf": [ + { "type": "array", "items": { "$ref": "#/components/schemas/facetFilters" } }, + { "type": "string" } + ], + "x-categories": ["Filtering"] + }, + "optionalFilters": { + "description": "Filters to promote or demote records in the search results.\n\nOptional filters work like facet filters, but they don't exclude records from the search results.\nRecords that match the optional filter rank before records that don't match.\nIf you're using a negative filter `facet:-value`, matching records rank after records that don't match.\n\n- Optional filters don't work on virtual replicas.\n- Optional filters are applied _after_ sort-by attributes.\n- Optional filters don't work with numeric attributes.\n", + "example": ["category:Book", "author:John Doe"], + "oneOf": [ + { "type": "array", "items": { "$ref": "#/components/schemas/optionalFilters" } }, + { "type": "string" } + ], + "x-categories": ["Filtering"] + }, + "numericFilters": { + "description": "Filter by numeric facets.\n\n**Prefer using the `filters` parameter, which supports all filter types and combinations with boolean operators.**\n\nYou can use numeric comparison operators: `<`, `<=`, `=`, `!=`, `>`, `>=`. Comparsions are precise up to 3 decimals.\nYou can also provide ranges: `facet: TO `. The range includes the lower and upper boundaries.\nThe same combination rules apply as for `facetFilters`.\n", + "example": [["inStock = 1", "deliveryDate < 1441755506"], "price < 1000"], + "oneOf": [ + { "type": "array", "items": { "$ref": "#/components/schemas/numericFilters" } }, + { "type": "string" } + ], + "x-categories": ["Filtering"] + }, + "tagFilters": { + "description": "Filter the search by values of the special `_tags` attribute.\n\n**Prefer using the `filters` parameter, which supports all filter types and combinations with boolean operators.**\n\nDifferent from regular facets, `_tags` can only be used for filtering (including or excluding records).\nYou won't get a facet count.\nThe same combination and escaping rules apply as for `facetFilters`.\n", + "example": [["Book", "Movie"], "SciFi"], + "oneOf": [ + { "type": "array", "items": { "$ref": "#/components/schemas/tagFilters" } }, + { "type": "string" } + ], + "x-categories": ["Filtering"] + }, + "page": { + "type": "integer", + "description": "Page of search results to retrieve.", + "default": 0, + "minimum": 0, + "x-categories": ["Pagination"] + }, + "aroundLatLng": { + "type": "string", + "description": "Coordinates for the center of a circle, expressed as a comma-separated string of latitude and longitude.\n\nOnly records included within circle around this central location are included in the results.\nThe radius of the circle is determined by the `aroundRadius` and `minimumAroundRadius` settings.\nThis parameter is ignored if you also specify `insidePolygon` or `insideBoundingBox`.\n", + "example": "40.71,-74.01", + "default": "", + "x-categories": ["Geo-Search"] + }, + "aroundLatLngViaIP": { + "type": "boolean", + "description": "Whether to obtain the coordinates from the request's IP address.", + "default": false, + "x-categories": ["Geo-Search"] + }, + "aroundRadiusAll": { + "title": "all", + "type": "string", + "description": "Return all records with a valid `_geoloc` attribute. Don't filter by distance.", + "enum": ["all"] + }, + "aroundRadius": { + "description": "Maximum radius for a search around a central location.\n\nThis parameter works in combination with the `aroundLatLng` and `aroundLatLngViaIP` parameters.\nBy default, the search radius is determined automatically from the density of hits around the central location.\nThe search radius is small if there are many hits close to the central coordinates.\n", + "oneOf": [ + { + "type": "integer", + "minimum": 1, + "description": "Maximum search radius around a central location in meters." + }, + { "$ref": "#/components/schemas/aroundRadiusAll" } + ], + "x-categories": ["Geo-Search"] + }, + "aroundPrecisionFromValue": { + "title": "range objects", + "type": "array", + "items": { + "title": "range", + "type": "object", + "description": "Range object with lower and upper values in meters to define custom ranges.", + "properties": { + "from": { + "type": "integer", + "description": "Lower boundary of a range in meters. The Geo ranking criterion considers all records within the range to be equal.", + "example": 20 + }, + "value": { + "type": "integer", + "description": "Upper boundary of a range in meters. The Geo ranking criterion considers all records within the range to be equal." + } + } + } + }, + "aroundPrecision": { + "description": "Precision of a coordinate-based search in meters to group results with similar distances.\n\nThe Geo ranking criterion considers all matches within the same range of distances to be equal.\n", + "oneOf": [ + { + "type": "integer", + "default": 10, + "description": "Distance in meters to group results by similar distances.\n\nFor example, if you set `aroundPrecision` to 100, records wihin 100 meters to the central coordinate are considered to have the same distance,\nas are records between 100 and 199 meters.\n" + }, + { "$ref": "#/components/schemas/aroundPrecisionFromValue" } + ], + "x-categories": ["Geo-Search"] + }, + "insideBoundingBox": { + "type": "array", + "items": { + "type": "array", + "minItems": 4, + "maxItems": 4, + "items": { "type": "number", "format": "double" } + }, + "description": "Coordinates for a rectangular area in which to search.\n\nEach bounding box is defined by the two opposite points of its diagonal, and expressed as latitude and longitude pair:\n`[p1 lat, p1 long, p2 lat, p2 long]`.\nProvide multiple bounding boxes as nested arrays.\nFor more information, see [rectangular area](https://www.algolia.com/doc/guides/managing-results/refine-results/geolocation/#filtering-inside-rectangular-or-polygonal-areas).\n", + "example": [ + [47.3165, 4.9665, 47.3424, 5.0201], + [40.9234, 2.1185, 38.643, 1.9916] + ], + "x-categories": ["Geo-Search"] + }, + "insidePolygon": { + "type": "array", + "items": { + "type": "array", + "minItems": 6, + "maxItems": 20000, + "items": { "type": "number", "format": "double" } + }, + "description": "Coordinates of a polygon in which to search.\n\nPolygons are defined by 3 to 10,000 points. Each point is represented by its latitude and longitude.\nProvide multiple polygons as nested arrays.\nFor more information, see [filtering inside polygons](https://www.algolia.com/doc/guides/managing-results/refine-results/geolocation/#filtering-inside-rectangular-or-polygonal-areas).\nThis parameter is ignored if you also specify `insideBoundingBox`.\n", + "example": [ + [47.3165, 4.9665, 47.3424, 5.0201, 47.32, 4.9], + [40.9234, 2.1185, 38.643, 1.9916, 39.2587, 2.0104] + ], + "x-categories": ["Geo-Search"] + }, + "supportedLanguage": { + "type": "string", + "description": "ISO code for a supported language.", + "enum": [ + "af", + "ar", + "az", + "bg", + "bn", + "ca", + "cs", + "cy", + "da", + "de", + "el", + "en", + "eo", + "es", + "et", + "eu", + "fa", + "fi", + "fo", + "fr", + "ga", + "gl", + "he", + "hi", + "hu", + "hy", + "id", + "is", + "it", + "ja", + "ka", + "kk", + "ko", + "ku", + "ky", + "lt", + "lv", + "mi", + "mn", + "mr", + "ms", + "mt", + "nb", + "nl", + "no", + "ns", + "pl", + "ps", + "pt", + "pt-br", + "qu", + "ro", + "ru", + "sk", + "sq", + "sv", + "sw", + "ta", + "te", + "th", + "tl", + "tn", + "tr", + "tt", + "uk", + "ur", + "uz", + "zh" + ] + }, + "userToken": { + "type": "string", + "description": "Unique pseudonymous or anonymous user identifier.\n\nThis helps with analytics and click and conversion events.\nFor more information, see [user token](https://www.algolia.com/doc/guides/sending-events/concepts/usertoken/).\n", + "example": "test-user-123", + "x-categories": ["Personalization"] + }, + "baseSearchParamsWithoutQuery": { + "type": "object", + "additionalProperties": false, + "properties": { + "similarQuery": { + "type": "string", + "description": "Keywords to be used instead of the search query to conduct a more broader search.\n\nUsing the `similarQuery` parameter changes other settings:\n\n- `queryType` is set to `prefixNone`.\n- `removeStopWords` is set to true.\n- `words` is set as the first ranking criterion.\n- All remaining words are treated as `optionalWords`.\n\nSince the `similarQuery` is supposed to do a broad search, they usually return many results.\nCombine it with `filters` to narrow down the list of results.\n", + "default": "", + "example": "comedy drama crime Macy Buscemi", + "x-categories": ["Search"] + }, + "filters": { "$ref": "#/components/schemas/filters" }, + "facetFilters": { "$ref": "#/components/schemas/facetFilters" }, + "optionalFilters": { "$ref": "#/components/schemas/optionalFilters" }, + "numericFilters": { "$ref": "#/components/schemas/numericFilters" }, + "tagFilters": { "$ref": "#/components/schemas/tagFilters" }, + "sumOrFiltersScores": { + "type": "boolean", + "description": "Whether to sum all filter scores.\n\nIf true, all filter scores are summed.\nOtherwise, the maximum filter score is kept.\nFor more information, see [filter scores](https://www.algolia.com/doc/guides/managing-results/refine-results/filtering/in-depth/filter-scoring/#accumulating-scores-with-sumorfiltersscores).\n", + "default": false, + "x-categories": ["Filtering"] + }, + "restrictSearchableAttributes": { + "type": "array", + "items": { "type": "string" }, + "example": ["title", "author"], + "description": "Restricts a search to a subset of your searchable attributes.\nAttribute names are case-sensitive.\n", + "default": [], + "x-categories": ["Filtering"] + }, + "facets": { + "type": "array", + "items": { "type": "string" }, + "description": "Facets for which to retrieve facet values that match the search criteria and the number of matching facet values.\n\nTo retrieve all facets, use the wildcard character `*`.\nFor more information, see [facets](https://www.algolia.com/doc/guides/managing-results/refine-results/faceting/#contextual-facet-values-and-counts).\n", + "default": [], + "example": ["*"], + "x-categories": ["Faceting"] + }, + "facetingAfterDistinct": { + "type": "boolean", + "description": "Whether faceting should be applied after deduplication with `distinct`.\n\nThis leads to accurate facet counts when using faceting in combination with `distinct`.\nIt's usually better to use `afterDistinct` modifiers in the `attributesForFaceting` setting,\nas `facetingAfterDistinct` only computes correct facet counts if all records have the same facet values for the `attributeForDistinct`.\n", + "default": false, + "x-categories": ["Faceting"] + }, + "page": { "$ref": "#/components/schemas/page" }, + "offset": { + "type": "integer", + "description": "Position of the first hit to retrieve.", + "x-categories": ["Pagination"] + }, + "length": { + "type": "integer", + "description": "Number of hits to retrieve (used in combination with `offset`).", + "minimum": 0, + "maximum": 1000, + "x-categories": ["Pagination"] + }, + "aroundLatLng": { "$ref": "#/components/schemas/aroundLatLng" }, + "aroundLatLngViaIP": { "$ref": "#/components/schemas/aroundLatLngViaIP" }, + "aroundRadius": { "$ref": "#/components/schemas/aroundRadius" }, + "aroundPrecision": { "$ref": "#/components/schemas/aroundPrecision" }, + "minimumAroundRadius": { + "type": "integer", + "description": "Minimum radius (in meters) for a search around a location when `aroundRadius` isn't set.", + "minimum": 1, + "x-categories": ["Geo-Search"] + }, + "insideBoundingBox": { "$ref": "#/components/schemas/insideBoundingBox" }, + "insidePolygon": { "$ref": "#/components/schemas/insidePolygon" }, + "naturalLanguages": { + "type": "array", + "items": { "$ref": "#/components/schemas/supportedLanguage" }, + "description": "ISO language codes that adjust settings that are useful for processing natural language queries (as opposed to keyword searches):\n\n- Sets `removeStopWords` and `ignorePlurals` to the list of provided languages.\n- Sets `removeWordsIfNoResults` to `allOptional`.\n- Adds a `natural_language` attribute to `ruleContexts` and `analyticsTags`.\n", + "default": [], + "x-categories": ["Languages"] + }, + "ruleContexts": { + "type": "array", + "items": { "type": "string" }, + "description": "Assigns a rule context to the search query.\n\n[Rule contexts](https://www.algolia.com/doc/guides/managing-results/rules/rules-overview/how-to/customize-search-results-by-platform/#whats-a-context) are strings that you can use to trigger matching rules.\n", + "default": [], + "example": ["mobile"], + "x-categories": ["Rules"] + }, + "personalizationImpact": { + "type": "integer", + "description": "Impact that Personalization should have on this search.\n\nThe higher this value is, the more Personalization determines the ranking compared to other factors.\nFor more information, see [Understanding Personalization impact](https://www.algolia.com/doc/guides/personalization/personalizing-results/in-depth/configuring-personalization/#understanding-personalization-impact).\n", + "default": 100, + "minimum": 0, + "maximum": 100, + "x-categories": ["Personalization"] + }, + "userToken": { "$ref": "#/components/schemas/userToken" }, + "getRankingInfo": { + "type": "boolean", + "description": "Whether the search response should include detailed ranking information.", + "default": false, + "x-categories": ["Advanced"] + }, + "synonyms": { + "type": "boolean", + "description": "Whether to take into account an index's synonyms for this search.", + "default": true, + "x-categories": ["Advanced"] + }, + "clickAnalytics": { + "type": "boolean", + "description": "Whether to include a `queryID` attribute in the response.\n\nThe query ID is a unique identifier for a search query and is required for tracking [click and conversion events](https://www.algolia.com/guides/sending-events/getting-started/).\n", + "default": false, + "x-categories": ["Analytics"] + }, + "analytics": { + "type": "boolean", + "description": "Whether this search will be included in Analytics.", + "default": true, + "x-categories": ["Analytics"] + }, + "analyticsTags": { + "type": "array", + "items": { "type": "string" }, + "description": "Tags to apply to the query for [segmenting analytics data](https://www.algolia.com/doc/guides/search-analytics/guides/segments/).", + "default": [], + "x-categories": ["Analytics"] + }, + "percentileComputation": { + "type": "boolean", + "description": "Whether to include this search when calculating processing-time percentiles.", + "default": true, + "x-categories": ["Advanced"] + }, + "enableABTest": { + "type": "boolean", + "description": "Whether to enable A/B testing for this search.", + "default": true, + "x-categories": ["Advanced"] + } + } + }, + "baseSearchParams": { + "allOf": [ + { "$ref": "#/components/schemas/searchParamsQuery" }, + { "$ref": "#/components/schemas/baseSearchParamsWithoutQuery" } + ] + }, + "hitsPerPage": { + "type": "integer", + "description": "Number of hits per page.", + "default": 20, + "minimum": 1, + "maximum": 1000, + "x-categories": ["Pagination"] + }, + "typoToleranceEnum": { + "type": "string", + "title": "typo tolerance", + "description": "- `min`. Return matches with the lowest number of typos.\n For example, if you have matches without typos, only include those.\n But if there are no matches without typos (with 1 typo), include matches with 1 typo (2 typos).\n- `strict`. Return matches with the two lowest numbers of typos.\n With `strict`, the Typo ranking criterion is applied first in the `ranking` setting.\n", + "enum": ["min", "strict"] + }, + "typoTolerance": { + "description": "Whether [typo tolerance](https://www.algolia.com/doc/guides/managing-results/optimize-search-results/typo-tolerance/) is enabled and how it is applied.\n\nIf typo tolerance is true, `min`, or `strict`, [word splitting and concetenation](https://www.algolia.com/doc/guides/managing-results/optimize-search-results/handling-natural-languages-nlp/in-depth/splitting-and-concatenation/) is also active.\n", + "oneOf": [ + { + "type": "boolean", + "default": true, + "description": "Whether typo tolerance is active. If true, matches with typos are included in the search results and rank after exact matches." + }, + { "$ref": "#/components/schemas/typoToleranceEnum" } + ], + "x-categories": ["Typos"] + }, + "ignorePlurals": { + "description": "Treat singular, plurals, and other forms of declensions as equivalent.\nYou should only use this feature for the languages used in your index.\n", + "example": ["ca", "es"], + "oneOf": [ + { + "type": "array", + "description": "ISO code for languages for which this feature should be active.\nThis overrides languages you set with `queryLanguages`.\n", + "items": { "$ref": "#/components/schemas/supportedLanguage" } + }, + { + "type": "boolean", + "description": "If true, `ignorePlurals` is active for all languages included in `queryLanguages`, or for all supported languages, if `queryLanguges` is empty.\nIf false, singulars, plurals, and other declensions won't be considered equivalent.\n", + "default": false + } + ], + "x-categories": ["Languages"] + }, + "removeStopWords": { + "description": "Removes stop words from the search query.\n\nStop words are common words like articles, conjunctions, prepositions, or pronouns that have little or no meaning on their own.\nIn English, \"the\", \"a\", or \"and\" are stop words.\n\nYou should only use this feature for the languages used in your index.\n", + "example": ["ca", "es"], + "oneOf": [ + { + "type": "array", + "description": "ISO code for languages for which stop words should be removed. This overrides languages you set in `queryLanguges`.", + "items": { "$ref": "#/components/schemas/supportedLanguage" } + }, + { + "type": "boolean", + "default": false, + "description": "If true, stop words are removed for all languages you included in `queryLanguages`, or for all supported languages, if `queryLanguages` is empty.\nIf false, stop words are not removed.\n" + } + ], + "x-categories": ["Languages"] + }, + "queryType": { + "type": "string", + "enum": ["prefixLast", "prefixAll", "prefixNone"], + "description": "Determines if and how query words are interpreted as prefixes.\n\nBy default, only the last query word is treated as prefix (`prefixLast`).\nTo turn off prefix search, use `prefixNone`.\nAvoid `prefixAll`, which treats all query words as prefixes.\nThis might lead to counterintuitive results and makes your search slower.\n\nFor more information, see [Prefix searching](https://www.algolia.com/doc/guides/managing-results/optimize-search-results/override-search-engine-defaults/in-depth/prefix-searching/).\n", + "default": "prefixLast", + "x-categories": ["Query strategy"] + }, + "removeWordsIfNoResults": { + "type": "string", + "enum": ["none", "lastWords", "firstWords", "allOptional"], + "example": "firstWords", + "description": "Strategy for removing words from the query when it doesn't return any results.\nThis helps to avoid returning empty search results.\n\n- `none`.\n No words are removed when a query doesn't return results.\n\n- `lastWords`.\n Treat the last (then second to last, then third to last) word as optional,\n until there are results or at most 5 words have been removed.\n\n- `firstWords`.\n Treat the first (then second, then third) word as optional,\n until there are results or at most 5 words have been removed.\n\n- `allOptional`.\n Treat all words as optional.\n\nFor more information, see [Remove words to improve results](https://www.algolia.com/doc/guides/managing-results/optimize-search-results/empty-or-insufficient-results/in-depth/why-use-remove-words-if-no-results/).\n", + "default": "none", + "x-categories": ["Query strategy"] + }, + "mode": { + "type": "string", + "enum": ["neuralSearch", "keywordSearch"], + "description": "Search mode the index will use to query for results.\n\nThis setting only applies to indices, for which Algolia enabled NeuralSearch for you.\n", + "default": "keywordSearch", + "x-categories": ["Query strategy"] + }, + "semanticSearch": { + "type": "object", + "description": "Settings for the semantic search part of NeuralSearch.\nOnly used when `mode` is `neuralSearch`.\n", + "properties": { + "eventSources": { + "oneOf": [ + { + "type": "array", + "description": "Indices from which to collect click and conversion events.\n\nIf null, the current index and all its replicas are used.\n", + "items": { "type": "string" } + }, + { "type": "null" } + ] + } + } + }, + "exactOnSingleWordQuery": { + "type": "string", + "enum": ["attribute", "none", "word"], + "description": "Determines how the [Exact ranking criterion](https://www.algolia.com/doc/guides/managing-results/optimize-search-results/override-search-engine-defaults/in-depth/adjust-exact-settings/#turn-off-exact-for-some-attributes) is computed when the search query has only one word.\n\n- `attribute`.\n The Exact ranking criterion is 1 if the query word and attribute value are the same.\n For example, a search for \"road\" will match the value \"road\", but not \"road trip\".\n\n- `none`.\n The Exact ranking criterion is ignored on single-word searches.\n\n- `word`.\n The Exact ranking criterion is 1 if the query word is found in the attribute value.\n The query word must have at least 3 characters and must not be a stop word.\n Only exact matches will be highlighted,\n partial and prefix matches won't.\n", + "default": "attribute", + "x-categories": ["Query strategy"] + }, + "alternativesAsExact": { + "type": "string", + "enum": ["ignorePlurals", "singleWordSynonym", "multiWordsSynonym"], + "x-categories": ["Query strategy"] + }, + "advancedSyntaxFeatures": { + "type": "string", + "enum": ["exactPhrase", "excludeWords"], + "x-categories": ["Query strategy"] + }, + "distinct": { + "description": "Determines how many records of a group are included in the search results.\n\nRecords with the same value for the `attributeForDistinct` attribute are considered a group.\nThe `distinct` setting controls how many members of the group are returned.\nThis is useful for [deduplication and grouping](https://www.algolia.com/doc/guides/managing-results/refine-results/grouping/#introducing-algolias-distinct-feature).\n\nThe `distinct` setting is ignored if `attributeForDistinct` is not set.\n", + "example": 1, + "oneOf": [ + { + "type": "boolean", + "description": "Whether deduplication is turned on. If true, only one member of a group is shown in the search results." + }, + { + "type": "integer", + "description": "Number of members of a group of records to include in the search results.\n\n- Don't use `distinct > 1` for records that might be [promoted by rules](https://www.algolia.com/doc/guides/managing-results/rules/merchandising-and-promoting/how-to/promote-hits/).\n The number of hits won't be correct and faceting won't work as expected.\n- With `distinct > 1`, the `hitsPerPage` parameter controls the number of returned groups.\n For example, with `hitsPerPage: 10` and `distinct: 2`, up to 20 records are returned.\n Likewise, the `nbHits` response attribute contains the number of returned groups.\n", + "minimum": 0, + "maximum": 4, + "default": 0 + } + ], + "x-categories": ["Advanced"] + }, + "maxFacetHits": { + "type": "integer", + "description": "Maximum number of facet values to return when [searching for facet values](https://www.algolia.com/doc/guides/managing-results/refine-results/faceting/#search-for-facet-values).", + "maximum": 100, + "default": 10, + "x-categories": ["Advanced"] + }, + "order": { + "description": "Explicit order of facets or facet values.\n\nThis setting lets you always show specific facets or facet values at the top of the list.\n", + "type": "array", + "items": { "type": "string" } + }, + "facets": { + "description": "Order of facet names.", + "type": "object", + "additionalProperties": false, + "properties": { "order": { "$ref": "#/components/schemas/order" } } + }, + "sortRemainingBy": { + "description": "Order of facet values that aren't explicitly positioned with the `order` setting.\n\n- `count`.\n Order remaining facet values by decreasing count.\n The count is the number of matching records containing this facet value.\n\n- `alpha`.\n Sort facet values alphabetically.\n\n- `hidden`.\n Don't show facet values that aren't explicitly positioned.\n", + "type": "string", + "enum": ["count", "alpha", "hidden"] + }, + "hide": { + "description": "Hide facet values.", + "type": "array", + "items": { "type": "string" } + }, + "value": { + "type": "object", + "additionalProperties": false, + "properties": { + "order": { "$ref": "#/components/schemas/order" }, + "sortRemainingBy": { "$ref": "#/components/schemas/sortRemainingBy" }, + "hide": { "$ref": "#/components/schemas/hide" } + } + }, + "values": { + "description": "Order of facet values. One object for each facet.", + "type": "object", + "additionalProperties": { + "x-additionalPropertiesName": "facet", + "$ref": "#/components/schemas/value" + } + }, + "facetOrdering": { + "description": "Order of facet names and facet values in your UI.", + "type": "object", + "additionalProperties": false, + "properties": { + "facets": { "$ref": "#/components/schemas/facets" }, + "values": { "$ref": "#/components/schemas/values" } + } + }, + "redirectURL": { + "description": "The redirect rule container.", + "type": "object", + "additionalProperties": false, + "properties": { "url": { "type": "string" } } + }, + "renderingContent": { + "description": "Extra data that can be used in the search UI.\n\nYou can use this to control aspects of your search UI, such as, the order of facet names and values\nwithout changing your frontend code.\n", + "type": "object", + "additionalProperties": false, + "properties": { + "facetOrdering": { "$ref": "#/components/schemas/facetOrdering" }, + "redirect": { "$ref": "#/components/schemas/redirectURL" } + }, + "x-categories": ["Advanced"] + }, + "reRankingApplyFilter": { + "description": "Restrict [Dynamic Re-Ranking](https://www.algolia.com/doc/guides/algolia-ai/re-ranking/) to records that match these filters.\n", + "oneOf": [ + { "type": "array", "items": { "$ref": "#/components/schemas/reRankingApplyFilter" } }, + { "type": "string", "x-categories": ["Filtering"] } + ] + }, + "indexSettingsAsSearchParams": { + "type": "object", + "additionalProperties": false, + "properties": { + "attributesToRetrieve": { + "type": "array", + "items": { "type": "string" }, + "example": ["author", "title", "content"], + "description": "Attributes to include in the API response.\n\nTo reduce the size of your response, you can retrieve only some of the attributes.\nAttribute names are case-sensitive.\n\n- `*` retrieves all attributes, except attributes included in the `customRanking` and `unretrievableAttributes` settings.\n- To retrieve all attributes except a specific one, prefix the attribute with a dash and combine it with the `*`: `[\"*\", \"-ATTRIBUTE\"]`.\n- The `objectID` attribute is always included.\n", + "default": ["*"], + "x-categories": ["Attributes"] + }, + "ranking": { + "type": "array", + "items": { "type": "string" }, + "description": "Determines the order in which Algolia returns your results.\n\nBy default, each entry corresponds to a [ranking criteria](https://www.algolia.com/doc/guides/managing-results/relevance-overview/in-depth/ranking-criteria/).\nThe tie-breaking algorithm sequentially applies each criterion in the order they're specified.\nIf you configure a replica index for [sorting by an attribute](https://www.algolia.com/doc/guides/managing-results/refine-results/sorting/how-to/sort-by-attribute/),\nyou put the sorting attribute at the top of the list.\n\n**Modifiers**\n\n- `asc(\"ATTRIBUTE\")`.\n Sort the index by the values of an attribute, in ascending order.\n- `desc(\"ATTRIBUTE\")`.\n Sort the index by the values of an attribute, in descending order.\n\nBefore you modify the default setting,\nyou should test your changes in the dashboard,\nand by [A/B testing](https://www.algolia.com/doc/guides/ab-testing/what-is-ab-testing/).\n", + "default": [ + "typo", + "geo", + "words", + "filters", + "proximity", + "attribute", + "exact", + "custom" + ], + "x-categories": ["Ranking"] + }, + "customRanking": { + "type": "array", + "items": { "type": "string" }, + "example": ["desc(popularity)", "asc(price)"], + "description": "Attributes to use as [custom ranking](https://www.algolia.com/doc/guides/managing-results/must-do/custom-ranking/).\nAttribute names are case-sensitive.\n\nThe custom ranking attributes decide which items are shown first if the other ranking criteria are equal.\n\nRecords with missing values for your selected custom ranking attributes are always sorted last.\nBoolean attributes are sorted based on their alphabetical order.\n\n**Modifiers**\n\n- `asc(\"ATTRIBUTE\")`.\n Sort the index by the values of an attribute, in ascending order.\n\n- `desc(\"ATTRIBUTE\")`.\n Sort the index by the values of an attribute, in descending order.\n\nIf you use two or more custom ranking attributes,\n[reduce the precision](https://www.algolia.com/doc/guides/managing-results/must-do/custom-ranking/how-to/controlling-custom-ranking-metrics-precision/) of your first attributes,\nor the other attributes will never be applied.\n", + "default": [], + "x-categories": ["Ranking"] + }, + "relevancyStrictness": { + "type": "integer", + "example": 90, + "description": "Relevancy threshold below which less relevant results aren't included in the results.\n\nYou can only set `relevancyStrictness` on [virtual replica indices](https://www.algolia.com/doc/guides/managing-results/refine-results/sorting/in-depth/replicas/#what-are-virtual-replicas).\nUse this setting to strike a balance between the relevance and number of returned results.\n", + "default": 100, + "x-categories": ["Ranking"] + }, + "attributesToHighlight": { + "type": "array", + "items": { "type": "string" }, + "example": ["author", "title", "conten", "content"], + "description": "Attributes to highlight.\n\nBy default, all searchable attributes are highlighted.\nUse `*` to highlight all attributes or use an empty array `[]` to turn off highlighting.\nAttribute names are case-sensitive.\n\nWith highlighting, strings that match the search query are surrounded by HTML tags defined by `highlightPreTag` and `highlightPostTag`.\nYou can use this to visually highlight matching parts of a search query in your UI.\n\nFor more information, see [Highlighting and snippeting](https://www.algolia.com/doc/guides/building-search-ui/ui-and-ux-patterns/highlighting-snippeting/js/).\n", + "x-categories": ["Highlighting and Snippeting"] + }, + "attributesToSnippet": { + "type": "array", + "items": { "type": "string" }, + "example": ["content:80", "description"], + "description": "Attributes for which to enable snippets.\nAttribute names are case-sensitive.\n\nSnippets provide additional context to matched words.\nIf you enable snippets, they include 10 words, including the matched word.\nThe matched word will also be wrapped by HTML tags for highlighting.\nYou can adjust the number of words with the following notation: `ATTRIBUTE:NUMBER`,\nwhere `NUMBER` is the number of words to be extracted.\n", + "default": [], + "x-categories": ["Highlighting and Snippeting"] + }, + "highlightPreTag": { + "type": "string", + "description": "HTML tag to insert before the highlighted parts in all highlighted results and snippets.", + "default": "", + "x-categories": ["Highlighting and Snippeting"] + }, + "highlightPostTag": { + "type": "string", + "description": "HTML tag to insert after the highlighted parts in all highlighted results and snippets.", + "default": "", + "x-categories": ["Highlighting and Snippeting"] + }, + "snippetEllipsisText": { + "type": "string", + "description": "String used as an ellipsis indicator when a snippet is truncated.", + "default": "…", + "x-categories": ["Highlighting and Snippeting"] + }, + "restrictHighlightAndSnippetArrays": { + "type": "boolean", + "description": "Whether to restrict highlighting and snippeting to items that at least partially matched the search query.\nBy default, all items are highlighted and snippeted.\n", + "default": false, + "x-categories": ["Highlighting and Snippeting"] + }, + "hitsPerPage": { "$ref": "#/components/schemas/hitsPerPage" }, + "minWordSizefor1Typo": { + "type": "integer", + "description": "Minimum number of characters a word in the search query must contain to accept matches with [one typo](https://www.algolia.com/doc/guides/managing-results/optimize-search-results/typo-tolerance/in-depth/configuring-typo-tolerance/#configuring-word-length-for-typos).", + "default": 4, + "x-categories": ["Typos"] + }, + "minWordSizefor2Typos": { + "type": "integer", + "description": "Minimum number of characters a word in the search query must contain to accept matches with [two typos](https://www.algolia.com/doc/guides/managing-results/optimize-search-results/typo-tolerance/in-depth/configuring-typo-tolerance/#configuring-word-length-for-typos).", + "default": 8, + "x-categories": ["Typos"] + }, + "typoTolerance": { "$ref": "#/components/schemas/typoTolerance" }, + "allowTyposOnNumericTokens": { + "type": "boolean", + "description": "Whether to allow typos on numbers in the search query.\n\nTurn off this setting to reduce the number of irrelevant matches\nwhen searching in large sets of similar numbers.\n", + "default": true, + "x-categories": ["Typos"] + }, + "disableTypoToleranceOnAttributes": { + "type": "array", + "items": { "type": "string" }, + "example": ["sku"], + "description": "Attributes for which you want to turn off [typo tolerance](https://www.algolia.com/doc/guides/managing-results/optimize-search-results/typo-tolerance/).\nAttribute names are case-sensitive.\n\nReturning only exact matches can help when:\n\n- [Searching in hyphenated attributes](https://www.algolia.com/doc/guides/managing-results/optimize-search-results/typo-tolerance/how-to/how-to-search-in-hyphenated-attributes/).\n- Reducing the number of matches when you have too many.\n This can happen with attributes that are long blocks of text, such as product descriptions.\n\nConsider alternatives such as `disableTypoToleranceOnWords` or adding synonyms if your attributes have intentional unusual spellings that might look like typos.\n", + "default": [], + "x-categories": ["Typos"] + }, + "ignorePlurals": { "$ref": "#/components/schemas/ignorePlurals" }, + "removeStopWords": { "$ref": "#/components/schemas/removeStopWords" }, + "keepDiacriticsOnCharacters": { + "type": "string", + "example": "øé", + "description": "Characters for which diacritics should be preserved.\n\nBy default, Algolia removes diacritics from letters.\nFor example, `é` becomes `e`. If this causes issues in your search,\nyou can specify characters that should keep their diacritics.\n", + "default": "", + "x-categories": ["Languages"] + }, + "queryLanguages": { + "type": "array", + "items": { "$ref": "#/components/schemas/supportedLanguage" }, + "example": ["es"], + "description": "Languages for language-specific query processing steps such as plurals, stop-word removal, and word-detection dictionaries.\n\nThis setting sets a default list of languages used by the `removeStopWords` and `ignorePlurals` settings.\nThis setting also sets a dictionary for word detection in the logogram-based [CJK](https://www.algolia.com/doc/guides/managing-results/optimize-search-results/handling-natural-languages-nlp/in-depth/normalization/#normalization-for-logogram-based-languages-cjk) languages.\nTo support this, you must place the CJK language **first**.\n\n**You should always specify a query language.**\nIf you don't specify an indexing language, the search engine uses all [supported languages](https://www.algolia.com/doc/guides/managing-results/optimize-search-results/handling-natural-languages-nlp/in-depth/supported-languages/),\nor the languages you specified with the `ignorePlurals` or `removeStopWords` parameters.\nThis can lead to unexpected search results.\nFor more information, see [Language-specific configuration](https://www.algolia.com/doc/guides/managing-results/optimize-search-results/handling-natural-languages-nlp/in-depth/language-specific-configurations/).\n", + "default": [], + "x-categories": ["Languages"] + }, + "decompoundQuery": { + "type": "boolean", + "description": "Whether to split compound words in the query into their building blocks.\n\nFor more information, see [Word segmentation](https://www.algolia.com/doc/guides/managing-results/optimize-search-results/handling-natural-languages-nlp/in-depth/language-specific-configurations/#splitting-compound-words).\nWord segmentation is supported for these languages: German, Dutch, Finnish, Swedish, and Norwegian.\nDecompounding doesn't work for words with [non-spacing mark Unicode characters](https://www.charactercodes.net/category/non-spacing_mark).\nFor example, `Gartenstühle` won't be decompounded if the `ü` consists of `u` (U+0075) and `◌̈` (U+0308).\n", + "default": true, + "x-categories": ["Languages"] + }, + "enableRules": { + "type": "boolean", + "description": "Whether to enable rules.", + "default": true, + "x-categories": ["Rules"] + }, + "enablePersonalization": { + "type": "boolean", + "description": "Whether to enable Personalization.", + "default": false, + "x-categories": ["Personalization"] + }, + "queryType": { "$ref": "#/components/schemas/queryType" }, + "removeWordsIfNoResults": { "$ref": "#/components/schemas/removeWordsIfNoResults" }, + "mode": { "$ref": "#/components/schemas/mode" }, + "semanticSearch": { "$ref": "#/components/schemas/semanticSearch" }, + "advancedSyntax": { + "type": "boolean", + "description": "Whether to support phrase matching and excluding words from search queries.\n\nUse the `advancedSyntaxFeatures` parameter to control which feature is supported.\n", + "default": false, + "x-categories": ["Query strategy"] + }, + "optionalWords": { + "type": "array", + "items": { "type": "string" }, + "example": ["blue", "iphone case"], + "description": "Words that should be considered optional when found in the query.\n\nBy default, records must match all words in the search query to be included in the search results.\nAdding optional words can help to increase the number of search results by running an additional search query that doesn't include the optional words.\nFor example, if the search query is \"action video\" and \"video\" is an optional word,\nthe search engine runs two queries. One for \"action video\" and one for \"action\".\nRecords that match all words are ranked higher.\n\nFor a search query with 4 or more words **and** all its words are optional,\nthe number of matched words required for a record to be included in the search results increases for every 1,000 records:\n\n- If `optionalWords` has less than 10 words, the required number of matched words increases by 1:\n results 1 to 1,000 require 1 matched word, results 1,001 to 2000 need 2 matched words.\n- If `optionalWords` has 10 or more words, the number of required matched words increases by the number of optional words dividied by 5 (rounded down).\n For example, with 18 optional words: results 1 to 1,000 require 1 matched word, results 1,001 to 2000 need 4 matched words.\n\nFor more information, see [Optional words](https://www.algolia.com/doc/guides/managing-results/optimize-search-results/empty-or-insufficient-results/#creating-a-list-of-optional-words).\n", + "default": [], + "x-categories": ["Query strategy"] + }, + "disableExactOnAttributes": { + "type": "array", + "items": { "type": "string" }, + "example": ["description"], + "description": "Searchable attributes for which you want to [turn off the Exact ranking criterion](https://www.algolia.com/doc/guides/managing-results/optimize-search-results/override-search-engine-defaults/in-depth/adjust-exact-settings/#turn-off-exact-for-some-attributes).\nAttribute names are case-sensitive.\n\nThis can be useful for attributes with long values, where the likelyhood of an exact match is high,\nsuch as product descriptions.\nTurning off the Exact ranking criterion for these attributes favors exact matching on other attributes.\nThis reduces the impact of individual attributes with a lot of content on ranking.\n", + "default": [], + "x-categories": ["Query strategy"] + }, + "exactOnSingleWordQuery": { "$ref": "#/components/schemas/exactOnSingleWordQuery" }, + "alternativesAsExact": { + "type": "array", + "items": { "$ref": "#/components/schemas/alternativesAsExact" }, + "description": "Alternatives of query words that should be considered as exact matches by the Exact ranking criterion.\n\n- `ignorePlurals`.\n Plurals and similar declensions added by the `ignorePlurals` setting are considered exact matches.\n\n- `singleWordSynonym`.\n Single-word synonyms, such as \"NY/NYC\" are considered exact matches.\n\n- `multiWordsSynonym`.\n Multi-word synonyms, such as \"NY/New York\" are considered exact matches.\n", + "default": ["ignorePlurals", "singleWordSynonym"], + "x-categories": ["Query strategy"] + }, + "advancedSyntaxFeatures": { + "type": "array", + "items": { "$ref": "#/components/schemas/advancedSyntaxFeatures" }, + "description": "Advanced search syntax features you want to support.\n\n- `exactPhrase`.\n Phrases in quotes must match exactly.\n For example, `sparkly blue \"iPhone case\"` only returns records with the exact string \"iPhone case\".\n\n- `excludeWords`.\n Query words prefixed with a `-` must not occur in a record.\n For example, `search -engine` matches records that contain \"search\" but not \"engine\".\n\nThis setting only has an effect if `advancedSyntax` is true.\n", + "default": ["exactPhrase", "excludeWords"], + "x-categories": ["Query strategy"] + }, + "distinct": { "$ref": "#/components/schemas/distinct" }, + "replaceSynonymsInHighlight": { + "type": "boolean", + "description": "Whether to replace a highlighted word with the matched synonym.\n\nBy default, the original words are highlighted even if a synonym matches.\nFor example, with `home` as a synonym for `house` and a search for `home`,\nrecords matching either \"home\" or \"house\" are included in the search results,\nand either \"home\" or \"house\" are highlighted.\n\nWith `replaceSynonymsInHighlight` set to `true`, a search for `home` still matches the same records,\nbut all occurences of \"house\" are replaced by \"home\" in the highlighted response.\n", + "default": false, + "x-categories": ["Highlighting and Snippeting"] + }, + "minProximity": { + "type": "integer", + "minimum": 1, + "maximum": 7, + "description": "Minimum proximity score for two matching words.\n\nThis adjusts the [Proximity ranking criterion](https://www.algolia.com/doc/guides/managing-results/relevance-overview/in-depth/ranking-criteria/#proximity)\nby equally scoring matches that are farther apart.\n\nFor example, if `minProximity` is 2, neighboring matches and matches with one word between them would have the same score.\n", + "default": 1, + "x-categories": ["Advanced"] + }, + "responseFields": { + "type": "array", + "items": { "type": "string" }, + "description": "Properties to include in the API response of `search` and `browse` requests.\n\nBy default, all response properties are included.\nTo reduce the response size, you can select, which attributes should be included.\n\nYou can't exclude these properties:\n`message`, `warning`, `cursor`, `serverUsed`, `indexUsed`,\n`abTestVariantID`, `parsedQuery`, or any property triggered by the `getRankingInfo` parameter.\n\nDon't exclude properties that you might need in your search UI.\n", + "default": ["*"], + "x-categories": ["Advanced"] + }, + "maxFacetHits": { "$ref": "#/components/schemas/maxFacetHits" }, + "maxValuesPerFacet": { + "type": "integer", + "description": "Maximum number of facet values to return for each facet.", + "default": 100, + "maximum": 1000, + "x-categories": ["Faceting"] + }, + "sortFacetValuesBy": { + "type": "string", + "description": "Order in which to retrieve facet values.\n\n- `count`.\n Facet values are retrieved by decreasing count.\n The count is the number of matching records containing this facet value.\n\n- `alpha`.\n Retrieve facet values alphabetically.\n\nThis setting doesn't influence how facet values are displayed in your UI (see `renderingContent`).\nFor more information, see [facet value display](https://www.algolia.com/doc/guides/building-search-ui/ui-and-ux-patterns/facet-display/js/).\n", + "default": "count", + "x-categories": ["Faceting"] + }, + "attributeCriteriaComputedByMinProximity": { + "type": "boolean", + "description": "Whether the best matching attribute should be determined by minimum proximity.\n\nThis setting only affects ranking if the Attribute ranking criterion comes before Proximity in the `ranking` setting.\nIf true, the best matching attribute is selected based on the minimum proximity of multiple matches.\nOtherwise, the best matching attribute is determined by the order in the `searchableAttributes` setting.\n", + "default": false, + "x-categories": ["Advanced"] + }, + "renderingContent": { "$ref": "#/components/schemas/renderingContent" }, + "enableReRanking": { + "type": "boolean", + "description": "Whether this search will use [Dynamic Re-Ranking](https://www.algolia.com/doc/guides/algolia-ai/re-ranking/).\n\nThis setting only has an effect if you activated Dynamic Re-Ranking for this index in the Algolia dashboard.\n", + "default": true, + "x-categories": ["Filtering"] + }, + "reRankingApplyFilter": { + "oneOf": [{ "$ref": "#/components/schemas/reRankingApplyFilter" }, { "type": "null" }] + } + } + }, + "searchParamsObject": { + "title": "Search parameters as object", + "description": "Each parameter value, including the `query` must not be larger than 512 bytes.", + "allOf": [ + { "$ref": "#/components/schemas/baseSearchParams" }, + { "$ref": "#/components/schemas/indexSettingsAsSearchParams" } + ] + }, + "searchParams": { + "title": "queryParameters", + "allOf": [ + { "$ref": "#/components/schemas/searchParamsObject" }, + { + "type": "object", + "description": "Search parameters for filtering the recommendations." + } + ] + }, + "baseRecommendRequest": { + "type": "object", + "additionalProperties": false, + "properties": { + "indexName": { "$ref": "#/components/schemas/indexName" }, + "threshold": { + "type": "number", + "format": "double", + "minimum": 0, + "maximum": 100, + "description": "Minimum score a recommendation must have to be included in the response." + }, + "maxRecommendations": { + "type": "integer", + "minimum": 1, + "maximum": 1000, + "default": 30, + "description": "Maximum number of recommendations to retrieve.\nBy default, all recommendations are returned and no fallback request is made.\nDepending on the available recommendations and the other request parameters,\nthe actual number of recommendations may be lower than this value.\n" + }, + "queryParameters": { "$ref": "#/components/schemas/searchParams" } + }, + "required": ["indexName", "threshold"] + }, + "fbtModel": { + "type": "string", + "description": "Frequently bought together model.\n\nThis model recommends items that have been purchased within 1 day with the item with the ID `objectID`.\n", + "enum": ["bought-together"] + }, + "objectID": { + "type": "string", + "description": "Unique record identifier.", + "example": "test-record-123" + }, + "frequentlyBoughtTogether": { + "type": "object", + "properties": { + "model": { "$ref": "#/components/schemas/fbtModel" }, + "objectID": { "$ref": "#/components/schemas/objectID" } + }, + "required": ["model", "objectID"] + }, + "boughtTogetherQuery": { + "title": "Frequently bought together", + "allOf": [ + { "$ref": "#/components/schemas/baseRecommendRequest" }, + { "$ref": "#/components/schemas/frequentlyBoughtTogether" } + ] + }, + "relatedModel": { + "type": "string", + "description": "Related products or similar content model.\n\nThis model recommends items that are similar to the item with the ID `objectID`.\nSimilarity is determined from the user interactions and attributes.\n", + "enum": ["related-products"] + }, + "fallbackParams": { + "title": "fallbackParameters", + "allOf": [ + { "$ref": "#/components/schemas/searchParamsObject" }, + { + "type": "object", + "description": "Search parameters to use for a fallback request if there aren't enough recommendations." + } + ] + }, + "relatedProducts": { + "type": "object", + "properties": { + "model": { "$ref": "#/components/schemas/relatedModel" }, + "objectID": { "$ref": "#/components/schemas/objectID" }, + "fallbackParameters": { "$ref": "#/components/schemas/fallbackParams" } + }, + "required": ["model", "objectID"] + }, + "relatedQuery": { + "title": "Related products", + "allOf": [ + { "$ref": "#/components/schemas/baseRecommendRequest" }, + { "$ref": "#/components/schemas/relatedProducts" } + ] + }, + "facetName": { + "type": "string", + "description": "Facet attribute. To be used in combination with `facetValue`.\nIf specified, only recommendations matching the facet filter will be returned.\n" + }, + "facetValue": { + "type": "string", + "description": "Facet value. To be used in combination with `facetName`.\nIf specified, only recommendations matching the facet filter will be returned.\n" + }, + "trendingItemsModel": { + "description": "Trending items model.\n\nTrending items are determined from the number of conversion events collected on them.\n", + "type": "string", + "enum": ["trending-items"] + }, + "trendingItems": { + "type": "object", + "properties": { + "facetName": { "$ref": "#/components/schemas/facetName" }, + "facetValue": { "$ref": "#/components/schemas/facetValue" }, + "model": { "$ref": "#/components/schemas/trendingItemsModel" }, + "fallbackParameters": { "$ref": "#/components/schemas/searchParamsObject" } + }, + "required": ["model"] + }, + "trendingItemsQuery": { + "title": "Trending items", + "allOf": [ + { "$ref": "#/components/schemas/baseRecommendRequest" }, + { "$ref": "#/components/schemas/trendingItems" } + ] + }, + "trendingFacetsModel": { + "type": "string", + "description": "Trending facet values model.\n\nThis model recommends trending facet values for the specified facet attribute.\n", + "enum": ["trending-facets"] + }, + "trendingFacets": { + "type": "object", + "properties": { + "facetName": { + "description": "Facet attribute for which to retrieve trending facet values." + }, + "model": { "$ref": "#/components/schemas/trendingFacetsModel" }, + "fallbackParameters": { "$ref": "#/components/schemas/fallbackParams" } + }, + "required": ["facetName", "model"] + }, + "trendingFacetsQuery": { + "title": "Trending facet values", + "allOf": [ + { "$ref": "#/components/schemas/baseRecommendRequest" }, + { "$ref": "#/components/schemas/trendingFacets" } + ] + }, + "lookingSimilarModel": { + "type": "string", + "description": "Looking similar model.\n\nThis model recommends items that look similar to the item with the ID `objectID` based on image attributes in your index.\n", + "enum": ["looking-similar"] + }, + "lookingSimilar": { + "type": "object", + "properties": { + "model": { "$ref": "#/components/schemas/lookingSimilarModel" }, + "objectID": { "$ref": "#/components/schemas/objectID" }, + "fallbackParameters": { "$ref": "#/components/schemas/fallbackParams" } + }, + "required": ["model", "objectID"] + }, + "lookingSimilarQuery": { + "title": "Looking similar", + "allOf": [ + { "$ref": "#/components/schemas/baseRecommendRequest" }, + { "$ref": "#/components/schemas/lookingSimilar" } + ] + }, + "recommendedForYouModel": { + "type": "string", + "description": "\"Recommened for you\" model.\n", + "enum": ["recommended-for-you"] + }, + "recommendedForYou": { + "type": "object", + "properties": { + "model": { "$ref": "#/components/schemas/recommendedForYouModel" }, + "fallbackParameters": { "$ref": "#/components/schemas/fallbackParams" } + }, + "required": ["model"] + }, + "recommendedForYouQuery": { + "title": "Recommended for you", + "allOf": [ + { "$ref": "#/components/schemas/baseRecommendRequest" }, + { "$ref": "#/components/schemas/recommendedForYou" } + ] + }, + "recommendationsRequest": { + "oneOf": [ + { "$ref": "#/components/schemas/boughtTogetherQuery" }, + { "$ref": "#/components/schemas/relatedQuery" }, + { "$ref": "#/components/schemas/trendingItemsQuery" }, + { "$ref": "#/components/schemas/trendingFacetsQuery" }, + { "$ref": "#/components/schemas/lookingSimilarQuery" }, + { "$ref": "#/components/schemas/recommendedForYouQuery" } + ] + }, + "processingTimeMS": { + "type": "integer", + "description": "Time the server took to process the request, in milliseconds.", + "example": 20 + }, + "RedirectRuleIndexMetadata": { + "type": "object", + "properties": { + "source": { "type": "string", "description": "Source index for the redirect rule." }, + "dest": { "type": "string", "description": "Destination index for the redirect rule." }, + "reason": { "type": "string", "description": "Reason for the redirect rule." }, + "succeed": { "type": "boolean", "description": "Redirect rule status." }, + "data": { + "title": "redirectRuleIndexData", + "type": "object", + "description": "Redirect rule data.", + "required": ["ruleObjectID"], + "properties": { "ruleObjectID": { "type": "string" } } + } + }, + "required": ["data", "succeed", "reason", "dest", "source"] + }, + "userData": { + "type": "object", + "example": { "settingID": "f2a7b51e3503acc6a39b3784ffb84300", "pluginVersion": "1.6.0" }, + "description": "An object with custom data.\n\nYou can store up to 32kB as custom data.\n", + "default": {}, + "x-categories": ["Advanced"] + }, + "baseSearchResponse": { + "type": "object", + "additionalProperties": true, + "required": ["processingTimeMS"], + "properties": { + "abTestID": { + "type": "integer", + "description": "A/B test ID. This is only included in the response for indices that are part of an A/B test." + }, + "abTestVariantID": { + "type": "integer", + "minimum": 1, + "description": "Variant ID. This is only included in the response for indices that are part of an A/B test." + }, + "aroundLatLng": { + "type": "string", + "description": "Computed geographical location.", + "example": "40.71,-74.01", + "pattern": "^(-?\\d+(\\.\\d+)?),\\s*(-?\\d+(\\.\\d+)?)$" + }, + "automaticRadius": { + "type": "string", + "description": "Distance from a central coordinate provided by `aroundLatLng`." + }, + "exhaustive": { + "title": "exhaustive", + "type": "object", + "description": "Whether certain properties of the search response are calculated exhaustive (exact) or approximated.", + "properties": { + "facetsCount": { + "type": "boolean", + "title": "facetsCount", + "description": "Whether the facet count is exhaustive (`true`) or approximate (`false`). See the [related discussion](https://support.algolia.com/hc/en-us/articles/4406975248145-Why-are-my-facet-and-hit-counts-not-accurate-)." + }, + "facetValues": { + "type": "boolean", + "title": "facetValues", + "description": "The value is `false` if not all facet values are retrieved." + }, + "nbHits": { + "type": "boolean", + "title": "nbHits", + "description": "Whether the `nbHits` is exhaustive (`true`) or approximate (`false`). When the query takes more than 50ms to be processed, the engine makes an approximation. This can happen when using complex filters on millions of records, when typo-tolerance was not exhaustive, or when enough hits have been retrieved (for example, after the engine finds 10,000 exact matches). `nbHits` is reported as non-exhaustive whenever an approximation is made, even if the approximation didn’t, in the end, impact the exhaustivity of the query." + }, + "rulesMatch": { + "type": "boolean", + "title": "rulesMatch", + "description": "Rules matching exhaustivity. The value is `false` if rules were enable for this query, and could not be fully processed due a timeout. This is generally caused by the number of alternatives (such as typos) which is too large." + }, + "typo": { + "type": "boolean", + "title": "typo", + "description": "Whether the typo search was exhaustive (`true`) or approximate (`false`). An approximation is done when the typo search query part takes more than 10% of the query budget (ie. 5ms by default) to be processed (this can happen when a lot of typo alternatives exist for the query). This field will not be included when typo-tolerance is entirely disabled." + } + } + }, + "exhaustiveFacetsCount": { + "type": "boolean", + "description": "See the `facetsCount` field of the `exhaustive` object in the response.", + "deprecated": true + }, + "exhaustiveNbHits": { + "type": "boolean", + "description": "See the `nbHits` field of the `exhaustive` object in the response.", + "deprecated": true + }, + "exhaustiveTypo": { + "type": "boolean", + "description": "See the `typo` field of the `exhaustive` object in the response.", + "deprecated": true + }, + "facets": { + "title": "facets", + "type": "object", + "additionalProperties": { + "x-additionalPropertiesName": "facet", + "type": "object", + "additionalProperties": { + "x-additionalPropertiesName": "facet count", + "type": "integer" + } + }, + "description": "Facet counts.", + "example": { "category": { "food": 1, "tech": 42 } } + }, + "facets_stats": { + "type": "object", + "description": "Statistics for numerical facets.", + "additionalProperties": { + "title": "facetStats", + "type": "object", + "properties": { + "min": { + "type": "number", + "format": "double", + "description": "Minimum value in the results." + }, + "max": { + "type": "number", + "format": "double", + "description": "Maximum value in the results." + }, + "avg": { + "type": "number", + "format": "double", + "description": "Average facet value in the results." + }, + "sum": { + "type": "number", + "format": "double", + "description": "Sum of all values in the results." + } + } + } + }, + "index": { + "type": "string", + "example": "indexName", + "description": "Index name used for the query." + }, + "indexUsed": { + "type": "string", + "description": "Index name used for the query. During A/B testing, the targeted index isn't always the index used by the query.", + "example": "indexNameAlt" + }, + "message": { "type": "string", "description": "Warnings about the query." }, + "nbSortedHits": { + "type": "integer", + "description": "Number of hits selected and sorted by the relevant sort algorithm.", + "example": 20 + }, + "parsedQuery": { + "type": "string", + "description": "Post-[normalization](https://www.algolia.com/doc/guides/managing-results/optimize-search-results/handling-natural-languages-nlp/#what-does-normalization-mean) query string that will be searched.", + "example": "george clo" + }, + "processingTimeMS": { "$ref": "#/components/schemas/processingTimeMS" }, + "processingTimingsMS": { + "type": "object", + "description": "Experimental. List of processing steps and their times, in milliseconds. You can use this list to investigate performance issues." + }, + "queryAfterRemoval": { + "type": "string", + "description": "Markup text indicating which parts of the original query have been removed to retrieve a non-empty result set." + }, + "redirect": { + "title": "redirect", + "type": "object", + "description": "[Redirect results to a URL](https://www.algolia.com/doc/guides/managing-results/rules/merchandising-and-promoting/how-to/redirects/), this this parameter is for internal use only.\n", + "properties": { + "index": { + "type": "array", + "items": { "$ref": "#/components/schemas/RedirectRuleIndexMetadata" } + } + } + }, + "renderingContent": { "$ref": "#/components/schemas/renderingContent" }, + "serverTimeMS": { + "type": "integer", + "description": "Time the server took to process the request, in milliseconds.", + "example": 20 + }, + "serverUsed": { + "type": "string", + "description": "Host name of the server that processed the request.", + "example": "c2-uk-3.algolia.net" + }, + "userData": { "$ref": "#/components/schemas/userData" }, + "queryID": { + "type": "string", + "description": "Unique identifier for the query. This is used for [click analytics](https://www.algolia.com/doc/guides/analytics/click-analytics/).", + "example": "a00dbc80a8d13c4565a442e7e2dca80a" + } + } + }, + "nbHits": { "type": "integer", "description": "Number of results (hits).", "example": 20 }, + "nbPages": { "type": "integer", "description": "Number of pages of results.", "example": 1 }, + "SearchPagination": { + "type": "object", + "additionalProperties": false, + "properties": { + "page": { "$ref": "#/components/schemas/page" }, + "nbHits": { "$ref": "#/components/schemas/nbHits" }, + "nbPages": { "$ref": "#/components/schemas/nbPages" }, + "hitsPerPage": { "$ref": "#/components/schemas/hitsPerPage" } + }, + "required": ["page", "nbHits", "nbPages", "hitsPerPage"] + }, + "highlightResultMap": { + "type": "object", + "description": "Surround words that match the query with HTML tags for highlighting.", + "additionalProperties": { + "x-additionalPropertiesName": "attribute", + "$ref": "#/components/schemas/highlightResult" + } + }, + "highlightedValue": { + "type": "string", + "description": "Highlighted attribute value, including HTML tags.", + "example": "George Clooney" + }, + "matchLevel": { + "type": "string", + "description": "Whether the whole query string matches or only a part.", + "enum": ["none", "partial", "full"] + }, + "highlightResultOption": { + "type": "object", + "description": "Surround words that match the query with HTML tags for highlighting.", + "additionalProperties": false, + "properties": { + "value": { "$ref": "#/components/schemas/highlightedValue" }, + "matchLevel": { "$ref": "#/components/schemas/matchLevel" }, + "matchedWords": { + "type": "array", + "description": "List of matched words from the search query.", + "example": ["action"], + "items": { "type": "string" } + }, + "fullyHighlighted": { + "type": "boolean", + "description": "Whether the entire attribute value is highlighted." + } + }, + "required": ["value", "matchLevel", "matchedWords"], + "x-discriminator-fields": ["matchLevel", "matchedWords"] + }, + "highlightResultOptionMap": { + "type": "object", + "description": "Surround words that match the query with HTML tags for highlighting.", + "additionalProperties": { + "x-additionalPropertiesName": "attribute", + "$ref": "#/components/schemas/highlightResultOption" + } + }, + "highlightResultOptionArray": { + "type": "array", + "description": "Surround words that match the query with HTML tags for highlighting.", + "items": { "$ref": "#/components/schemas/highlightResultOption" } + }, + "highlightResult": { + "oneOf": [ + { "$ref": "#/components/schemas/highlightResultMap" }, + { "$ref": "#/components/schemas/highlightResultOption" }, + { "$ref": "#/components/schemas/highlightResultOptionMap" }, + { "$ref": "#/components/schemas/highlightResultOptionArray" } + ] + }, + "snippetResultMap": { + "type": "object", + "description": "Snippets that show the context around a matching search query.", + "additionalProperties": { + "x-additionalPropertiesName": "attribute", + "$ref": "#/components/schemas/snippetResult" + } + }, + "snippetResultOption": { + "type": "object", + "description": "Snippets that show the context around a matching search query.", + "additionalProperties": false, + "properties": { + "value": { "$ref": "#/components/schemas/highlightedValue" }, + "matchLevel": { "$ref": "#/components/schemas/matchLevel" } + }, + "required": ["value", "matchLevel"], + "x-discriminator-fields": ["matchLevel"] + }, + "snippetResultOptionMap": { + "type": "object", + "description": "Snippets that show the context around a matching search query.", + "additionalProperties": { + "x-additionalPropertiesName": "attribute", + "$ref": "#/components/schemas/snippetResultOption" + } + }, + "snippetResultOptionArray": { + "type": "array", + "description": "Snippets that show the context around a matching search query.", + "items": { "$ref": "#/components/schemas/snippetResultOption" } + }, + "snippetResult": { + "oneOf": [ + { "$ref": "#/components/schemas/snippetResultMap" }, + { "$ref": "#/components/schemas/snippetResultOption" }, + { "$ref": "#/components/schemas/snippetResultOptionMap" }, + { "$ref": "#/components/schemas/snippetResultOptionArray" } + ] + }, + "matchedGeoLocation": { + "type": "object", + "properties": { + "lat": { + "type": "number", + "format": "double", + "description": "Latitude of the matched location." + }, + "lng": { + "type": "number", + "format": "double", + "description": "Longitude of the matched location." + }, + "distance": { + "type": "integer", + "description": "Distance between the matched location and the search location (in meters)." + } + } + }, + "personalization": { + "type": "object", + "properties": { + "filtersScore": { "type": "integer", "description": "The score of the filters." }, + "rankingScore": { "type": "integer", "description": "The score of the ranking." }, + "score": { "type": "integer", "description": "The score of the event." } + } + }, + "rankingInfo": { + "type": "object", + "description": "Object with detailed information about the record's ranking.", + "additionalProperties": false, + "properties": { + "filters": { + "type": "integer", + "minimum": 0, + "description": "Whether a filter matched the query." + }, + "firstMatchedWord": { + "type": "integer", + "minimum": 0, + "description": "Position of the first matched word in the best matching attribute of the record." + }, + "geoDistance": { + "type": "integer", + "minimum": 0, + "description": "Distance between the geo location in the search query and the best matching geo location in the record, divided by the geo precision (in meters)." + }, + "geoPrecision": { + "type": "integer", + "minimum": 1, + "description": "Precision used when computing the geo distance, in meters." + }, + "matchedGeoLocation": { "$ref": "#/components/schemas/matchedGeoLocation" }, + "personalization": { "$ref": "#/components/schemas/personalization" }, + "nbExactWords": { + "type": "integer", + "minimum": 0, + "description": "Number of exactly matched words." + }, + "nbTypos": { + "type": "integer", + "minimum": 0, + "description": "Number of typos encountered when matching the record." + }, + "promoted": { + "type": "boolean", + "description": "Whether the record was promoted by a rule." + }, + "proximityDistance": { + "type": "integer", + "minimum": 0, + "description": "Number of words between multiple matches in the query plus 1. For single word queries, `proximityDistance` is 0." + }, + "userScore": { + "type": "integer", + "description": "Overall ranking of the record, expressed as a single integer. This attribute is internal." + }, + "words": { "type": "integer", "minimum": 1, "description": "Number of matched words." }, + "promotedByReRanking": { + "type": "boolean", + "description": "Whether the record is re-ranked." + } + }, + "required": ["nbTypos", "firstMatchedWord", "geoDistance", "nbExactWords", "userScore"] + }, + "distinctSeqID": { "type": "integer" }, + "recommendScore": { + "type": "number", + "format": "double", + "minimum": 0, + "maximum": 100, + "description": "Recommendation score." + }, + "recommendHit": { + "type": "object", + "description": "Recommend hit.", + "additionalProperties": true, + "required": ["objectID", "_score"], + "properties": { + "objectID": { "$ref": "#/components/schemas/objectID" }, + "_highlightResult": { "$ref": "#/components/schemas/highlightResultMap" }, + "_snippetResult": { "$ref": "#/components/schemas/snippetResultMap" }, + "_rankingInfo": { "$ref": "#/components/schemas/rankingInfo" }, + "_distinctSeqID": { "$ref": "#/components/schemas/distinctSeqID" }, + "_score": { "$ref": "#/components/schemas/recommendScore" } + } + }, + "trendingFacetHit": { + "type": "object", + "description": "Trending facet hit.", + "required": ["_score", "facetName", "facetValue"], + "properties": { + "_score": { "$ref": "#/components/schemas/recommendScore" }, + "facetName": { "$ref": "#/components/schemas/facetName" }, + "facetValue": { "$ref": "#/components/schemas/facetValue" } + } + }, + "recommendationsHit": { + "oneOf": [ + { "$ref": "#/components/schemas/recommendHit" }, + { "$ref": "#/components/schemas/trendingFacetHit" } + ] + }, + "recommendationsHits": { + "type": "object", + "additionalProperties": false, + "properties": { + "hits": { + "type": "array", + "items": { "$ref": "#/components/schemas/recommendationsHit" } + } + }, + "required": ["hits"] + }, + "recommendationsResults": { + "allOf": [ + { "$ref": "#/components/schemas/baseSearchResponse" }, + { "$ref": "#/components/schemas/SearchPagination" }, + { "$ref": "#/components/schemas/recommendationsHits" } + ] + }, + "recommendModels": { + "type": "string", + "enum": ["related-products", "bought-together", "trending-facets", "trending-items"] + }, + "updatedAt": { + "type": "string", + "example": "2023-07-04T12:49:15Z", + "description": "Date and time when the object was updated, in RFC 3339 format." + }, + "ruleID": { + "title": "objectID", + "type": "string", + "description": "Unique identifier of a rule object." + }, + "context": { + "type": "string", + "pattern": "[A-Za-z0-9_-]+", + "description": "An additional restriction that only triggers the rule, when the search has the same value as `ruleContexts` parameter.\nFor example, if `context: mobile`, the rule is only triggered when the search request has a matching `ruleContexts: mobile`.\nA rule context must only contain alphanumeric characters.\n", + "example": "mobile" + }, + "Condition": { + "type": "object", + "description": "Condition that triggers the rule.\nIf not specified, the rule is triggered for all recommendations.\n", + "properties": { + "filters": { "$ref": "#/components/schemas/filters" }, + "context": { "$ref": "#/components/schemas/context" } + } + }, + "HideConsequenceObject": { + "type": "object", + "description": "Object ID of the recommendation you want to exclude.", + "properties": { "objectID": { "$ref": "#/components/schemas/objectID" } } + }, + "HideConsequence": { + "type": "array", + "description": "Exclude items from recommendations.", + "minItems": 1, + "items": { "$ref": "#/components/schemas/HideConsequenceObject" } + }, + "PromoteConsequenceObject": { + "type": "object", + "description": "Object ID and position of the recommendation you want to pin.", + "properties": { + "objectID": { "$ref": "#/components/schemas/objectID" }, + "position": { + "type": "integer", + "description": "Index in the list of recommendations where to place this item.", + "minimum": 0 + } + } + }, + "PromoteConsequence": { + "type": "array", + "description": "Place items at specific positions in the list of recommendations.", + "minItems": 1, + "items": { "$ref": "#/components/schemas/PromoteConsequenceObject" } + }, + "AutoFacetFilter": { + "type": "object", + "description": "Facet attribute. Only recommendations with the same value (or only recommendations with a different value) as the original viewed item are included.", + "properties": { + "facet": { "type": "string", "description": "Facet attribute." }, + "negative": { + "type": "boolean", + "description": "Whether the filter is negative.\nIf true, recommendations must not have the same value for the `facet` attribute.\nIf false, recommendations must have the same value for the `facet` attribute.\n" + } + } + }, + "ParamsConsequence": { + "type": "object", + "description": "Filter or boost recommendations matching a facet filter.", + "properties": { + "automaticFacetFilters": { + "type": "array", + "description": "Filter recommendations that match or don't match the same `facet:facet_value` combination as the viewed item.", + "items": { "$ref": "#/components/schemas/AutoFacetFilter" } + }, + "filters": { "$ref": "#/components/schemas/filters" }, + "optionalFilters": { + "type": "array", + "description": "Filters to promote or demote records in the search results.\n\nOptional filters work like facet filters, but they don't exclude records from the search results.\nRecords that match the optional filter rank before records that don't match.\nMatches with higher weights (``) rank before matches with lower weights.\nIf you're using a negative filter `facet:-value`, matching records rank after records that don't match.\n", + "items": { "type": "string" }, + "example": ["category:books", "category:-movies"] + } + } + }, + "Consequence": { + "type": "object", + "description": "Effect of the rule.", + "properties": { + "hide": { "$ref": "#/components/schemas/HideConsequence" }, + "promote": { "$ref": "#/components/schemas/PromoteConsequence" }, + "params": { "$ref": "#/components/schemas/ParamsConsequence" } + } + }, + "RecommendRule": { + "type": "object", + "description": "Recommend rule.", + "additionalProperties": false, + "properties": { + "_metadata": { + "title": "ruleMetadata", + "type": "object", + "description": "Rule metadata.", + "properties": { "lastUpdate": { "$ref": "#/components/schemas/updatedAt" } } + }, + "objectID": { "$ref": "#/components/schemas/ruleID" }, + "condition": { "$ref": "#/components/schemas/Condition" }, + "consequence": { "$ref": "#/components/schemas/Consequence" }, + "description": { + "type": "string", + "description": "Description of the rule's purpose. This can be helpful for display in the Algolia dashboard.", + "example": "Display a promotional banner" + }, + "enabled": { + "type": "boolean", + "default": true, + "description": "Indicates whether to enable the rule. If it isn't enabled, it isn't applied at query time." + } + } + }, + "taskID": { + "type": "integer", + "format": "int64", + "example": 1514562690001, + "description": "Unique identifier of a task.\n\nA successful API response means that a task was added to a queue. It might not run immediately. You can check the task's progress with the [`task` operation](#tag/Indices/operation/getTask) and this `taskID`.\n" + }, + "deletedAt": { + "type": "string", + "example": "2023-06-27T14:42:38.831Z", + "description": "Date and time when the object was deleted, in RFC 3339 format." + }, + "taskStatus": { + "type": "string", + "enum": ["published", "notPublished"], + "description": "Task status, `published` if the task is completed, `notPublished` otherwise." + }, + "parameters_query": { "type": "string", "description": "Search query.", "default": "" }, + "parameters_page": { + "type": "integer", + "minimum": 0, + "description": "Requested page of the API response." + }, + "parameters_hitsPerPage": { + "type": "integer", + "default": 20, + "minimum": 1, + "maximum": 1000, + "description": "Maximum number of hits per page." + } + }, + "responses": { + "BadRequest": { + "description": "Bad request or request arguments.", + "content": { + "application/json": { "schema": { "$ref": "#/components/schemas/ErrorBase" } } + } + }, + "FeatureNotEnabled": { + "description": "This feature is not enabled on your Algolia account.", + "content": { + "application/json": { "schema": { "$ref": "#/components/schemas/ErrorBase" } } + } + }, + "MethodNotAllowed": { + "description": "Method not allowed with this API key.", + "content": { + "application/json": { "schema": { "$ref": "#/components/schemas/ErrorBase" } } + } + }, + "IndexNotFound": { + "description": "Index not found.", + "content": { + "application/json": { "schema": { "$ref": "#/components/schemas/ErrorBase" } } + } + }, + "DeletedAt": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "title": "deletedAtResponse", + "description": "Response, taskID, and deletion timestamp.", + "additionalProperties": false, + "type": "object", + "required": ["taskID", "deletedAt"], + "properties": { + "taskID": { "$ref": "#/components/schemas/taskID" }, + "deletedAt": { "$ref": "#/components/schemas/deletedAt" } + } + } + } + } + } + } + } +} diff --git a/packages/algolia-recommend/plugins/meshConfigAlgoliaRecommend.ts b/packages/algolia-recommend/plugins/meshConfigAlgoliaRecommend.ts new file mode 100644 index 0000000000..e2598282e4 --- /dev/null +++ b/packages/algolia-recommend/plugins/meshConfigAlgoliaRecommend.ts @@ -0,0 +1,59 @@ +import type { meshConfig as meshConfigBase } from '@graphcommerce/graphql-mesh/meshConfig' +import type { FunctionPlugin, PluginConfig } from '@graphcommerce/next-config' + +export const config: PluginConfig = { + module: '@graphcommerce/graphql-mesh/meshConfig', + type: 'function', +} + +export const meshConfig: FunctionPlugin = ( + prev, + baseConfig, + graphCommerceConfig, +) => + prev( + { + ...baseConfig, + sources: [ + ...baseConfig.sources, + { + name: 'algoliaRecommend', + handler: { + openapi: { + endpoint: `https://${graphCommerceConfig.algolia.applicationId}.algolia.net/`, + source: '@graphcommerce/algolia-recommend/algolia-recommend-spec.yaml', + ignoreErrorResponses: true, + schemaHeaders: { + 'X-Algolia-Application-Id': graphCommerceConfig.algolia.applicationId, + 'X-Algolia-API-Key': graphCommerceConfig.algolia.searchOnlyApiKey, + }, + operationHeaders: { + 'X-Algolia-Application-Id': graphCommerceConfig.algolia.applicationId, + 'X-Algolia-API-Key': graphCommerceConfig.algolia.searchOnlyApiKey, + }, + selectQueryOrMutationField: [{ type: 'Mutation', fieldName: 'getRecommendations' }], + }, + }, + transforms: [ + { + prefix: { + value: 'algolia_', + includeRootOperations: true, + includeTypes: false, + mode: 'bare', + }, + }, + { + prefix: { + value: 'Algolia', + includeRootOperations: false, + includeTypes: true, + mode: 'bare', + }, + }, + ], + }, + ], + }, + graphCommerceConfig, + ) diff --git a/packages/algolia-recommend/scripts/generate-recommend-spec.mts b/packages/algolia-recommend/scripts/generate-recommend-spec.mts new file mode 100644 index 0000000000..1e81eea695 --- /dev/null +++ b/packages/algolia-recommend/scripts/generate-recommend-spec.mts @@ -0,0 +1,74 @@ +import yaml from 'js-yaml' +import { writeFile, readFile } from 'node:fs/promises' +import { OpenAPIV3 } from 'openapi-types' +import prettier from 'prettier' +import conf from '@graphcommerce/prettier-config-pwa' + +const response = await fetch( + 'https://raw.githubusercontent.com/algolia/api-clients-automation/main/specs/bundled/recommend.yml', +) + +const openApiSchema = yaml.load(await response.text()) as OpenAPIV3.Document + +const allMethods = [ + OpenAPIV3.HttpMethods.TRACE, + OpenAPIV3.HttpMethods.POST, + OpenAPIV3.HttpMethods.PUT, + OpenAPIV3.HttpMethods.GET, + OpenAPIV3.HttpMethods.DELETE, + OpenAPIV3.HttpMethods.PATCH, + OpenAPIV3.HttpMethods.OPTIONS, + OpenAPIV3.HttpMethods.HEAD, +] + +const { info, openapi, components, tags, ...rest } = openApiSchema + +function filterPaths( + paths: OpenAPIV3.PathsObject, + allow: Record, +): OpenAPIV3.PathsObject { + const allowedEntries = Object.entries(allow) + + return Object.fromEntries( + Object.entries(paths) + .map(([path, pathItem]) => { + if (!pathItem) return [path, pathItem] + const newValue = pathItem + + const [allowedPath, allowedMethods] = + allowedEntries.find(([allowedPath]) => allowedPath === path) ?? [] + + if (!allowedPath || !allowedMethods) return [path, undefined] + + allMethods + .filter((method) => !allowedMethods.includes(method)) + .forEach((method) => { + newValue[method] = undefined + }) + + return [path, newValue] + }) + .filter(([path, pathItem]) => { + if (!pathItem) return false + if (allMethods.every((key) => !pathItem[key])) return false + return true + }), + ) +} + +const newSchema: OpenAPIV3.Document = { + openapi, + info, + paths: filterPaths(openApiSchema.paths, { + '/1/indexes/*/recommendations': [OpenAPIV3.HttpMethods.POST], + }), + components, +} + +await writeFile( + './algolia-recommend-spec.yaml', + await prettier.format(JSON.stringify(newSchema), { + parser: 'json', + ...conf, + }), +) From 939c647db12cb267a0228c3229c1165ba44cfca7 Mon Sep 17 00:00:00 2001 From: Renzo van Hootegem Date: Fri, 30 Aug 2024 14:41:43 +0200 Subject: [PATCH 12/70] Created recommend schema --- packages/algolia-recommend/mesh/resolvers.ts | 11 +++++++ packages/algolia-recommend/package.json | 30 +++++++++++++++++++ .../plugins/meshConfigAlgoliaRecommend.ts | 4 +++ .../schema/RecommendSchema.graphqls | 7 +++++ packages/algolia-recommend/tsconfig.json | 5 ++++ 5 files changed, 57 insertions(+) create mode 100644 packages/algolia-recommend/mesh/resolvers.ts create mode 100644 packages/algolia-recommend/package.json create mode 100644 packages/algolia-recommend/schema/RecommendSchema.graphqls create mode 100644 packages/algolia-recommend/tsconfig.json diff --git a/packages/algolia-recommend/mesh/resolvers.ts b/packages/algolia-recommend/mesh/resolvers.ts new file mode 100644 index 0000000000..052f19f6d1 --- /dev/null +++ b/packages/algolia-recommend/mesh/resolvers.ts @@ -0,0 +1,11 @@ +import { MeshContext, Resolvers } from '@graphcommerce/graphql-mesh' + +export const resolvers: Resolvers = { + Query: { + recommendations: { + resolve: async (root, args, context: MeshContext, info) => { + const algoliaResponse = await context.algoliaRecommend.Mutation.algolia_getRecommendations() + }, + }, + }, +} diff --git a/packages/algolia-recommend/package.json b/packages/algolia-recommend/package.json new file mode 100644 index 0000000000..d1d8728481 --- /dev/null +++ b/packages/algolia-recommend/package.json @@ -0,0 +1,30 @@ +{ + "name": "@graphcommerce/algolia-recommend", + "homepage": "https://www.graphcommerce.org/", + "repository": "github:graphcommerce-org/graphcommerce", + "version": "9.0.0-canary.76", + "sideEffects": false, + "prettier": "@graphcommerce/prettier-config-pwa", + "eslintConfig": { + "extends": "@graphcommerce/eslint-config-pwa", + "parserOptions": { + "project": "./tsconfig.json" + } + }, + "scripts": { + "generate": "tsx scripts/generate-recommend-spec.mts" + }, + "peerDependencies": { + "@graphcommerce/graphql": "^9.0.0-canary.76", + "@graphcommerce/graphql-mesh": "^9.0.0-canary.76", + "@graphcommerce/magento-product": "^9.0.0-canary.76", + "@graphcommerce/magento-search": "^9.0.0-canary.76", + "@graphcommerce/next-config": "^9.0.0-canary.76", + "@graphcommerce/next-ui": "^9.0.0-canary.76", + "react": "^18.2.0" + }, + "devDependencies": { + "graphql": "^16.0.0", + "tsx": "^4.16.2" + } +} diff --git a/packages/algolia-recommend/plugins/meshConfigAlgoliaRecommend.ts b/packages/algolia-recommend/plugins/meshConfigAlgoliaRecommend.ts index e2598282e4..e4a5367d82 100644 --- a/packages/algolia-recommend/plugins/meshConfigAlgoliaRecommend.ts +++ b/packages/algolia-recommend/plugins/meshConfigAlgoliaRecommend.ts @@ -54,6 +54,10 @@ export const meshConfig: FunctionPlugin = ( ], }, ], + additionalResolvers: [ + ...(baseConfig.additionalResolvers ?? []), + '@graphcommerce/algolia-recommend/mesh/resolvers.ts', + ], }, graphCommerceConfig, ) diff --git a/packages/algolia-recommend/schema/RecommendSchema.graphqls b/packages/algolia-recommend/schema/RecommendSchema.graphqls new file mode 100644 index 0000000000..20bc91e1b6 --- /dev/null +++ b/packages/algolia-recommend/schema/RecommendSchema.graphqls @@ -0,0 +1,7 @@ +type Recommendations { + items: [ProductInterface] +} + +extend type Query { + recommendations: Recommendations +} diff --git a/packages/algolia-recommend/tsconfig.json b/packages/algolia-recommend/tsconfig.json new file mode 100644 index 0000000000..7398153dd6 --- /dev/null +++ b/packages/algolia-recommend/tsconfig.json @@ -0,0 +1,5 @@ +{ + "exclude": ["**/node_modules", "**/.*/"], + "include": ["**/*.ts", "**/*.tsx"], + "extends": "@graphcommerce/typescript-config-pwa/nextjs.json" +} From 80bcdd17d8948c72fad00e02bbf5c3f8508ab47a Mon Sep 17 00:00:00 2001 From: Renzo van Hootegem Date: Fri, 30 Aug 2024 14:57:32 +0200 Subject: [PATCH 13/70] added recommendType param --- packages/algolia-recommend/schema/RecommendSchema.graphqls | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/algolia-recommend/schema/RecommendSchema.graphqls b/packages/algolia-recommend/schema/RecommendSchema.graphqls index 20bc91e1b6..3b3318a273 100644 --- a/packages/algolia-recommend/schema/RecommendSchema.graphqls +++ b/packages/algolia-recommend/schema/RecommendSchema.graphqls @@ -3,5 +3,5 @@ type Recommendations { } extend type Query { - recommendations: Recommendations + recommendations(recommendType: String): Recommendations } From 1c4ef1e4323e2f45a35845841f687ad3f3964b41 Mon Sep 17 00:00:00 2001 From: Renzo van Hootegem Date: Fri, 30 Aug 2024 15:02:02 +0200 Subject: [PATCH 14/70] added recommendType to recoomend query --- packages/algolia-recommend/mesh/resolvers.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/packages/algolia-recommend/mesh/resolvers.ts b/packages/algolia-recommend/mesh/resolvers.ts index 052f19f6d1..efb80487a6 100644 --- a/packages/algolia-recommend/mesh/resolvers.ts +++ b/packages/algolia-recommend/mesh/resolvers.ts @@ -4,7 +4,12 @@ export const resolvers: Resolvers = { Query: { recommendations: { resolve: async (root, args, context: MeshContext, info) => { - const algoliaResponse = await context.algoliaRecommend.Mutation.algolia_getRecommendations() + const algoliaResponse = await context.algoliaRecommend.Mutation.algolia_getRecommendations({ + args: { recommendType: 'test' }, + selectionSet: `{ + }`, + }) + return [] }, }, }, From 652e28446fcbaed62b521fa32c3a9addb6b43934 Mon Sep 17 00:00:00 2001 From: Renzo van Hootegem Date: Mon, 2 Sep 2024 11:42:22 +0200 Subject: [PATCH 15/70] Added config for enabling categories --- packages/algolia-mesh/Config.graphqls | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/packages/algolia-mesh/Config.graphqls b/packages/algolia-mesh/Config.graphqls index ea0763946d..086c1b63f6 100644 --- a/packages/algolia-mesh/Config.graphqls +++ b/packages/algolia-mesh/Config.graphqls @@ -23,6 +23,11 @@ input GraphCommerceAlgoliaConfig { """ catalogEnabled: Boolean + """ + By default fetching categories will not use algolia. Set this to true to enable Algolia for categories. + """ + categoryEnabled: Boolean + """ Enable Algolia customer group pricing. """ From 084b48eabdf708c8e07a2e5dbaa18d4ce02ed7f4 Mon Sep 17 00:00:00 2001 From: Renzo van Hootegem Date: Mon, 2 Sep 2024 14:05:18 +0200 Subject: [PATCH 16/70] Added fallback when algolia returns error --- packages/algolia-mesh/mesh/resolvers.ts | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/packages/algolia-mesh/mesh/resolvers.ts b/packages/algolia-mesh/mesh/resolvers.ts index dbf935277f..892a4ac4d3 100644 --- a/packages/algolia-mesh/mesh/resolvers.ts +++ b/packages/algolia-mesh/mesh/resolvers.ts @@ -35,6 +35,10 @@ function hasSuggestionsRequest(info: GraphQLResolveInfo) { return hasSelectionSetPath(info.operation.selectionSet, 'products.suggestions') } +function isGraphQLError(err: unknown): err is GraphQLError { + return !!(err as GraphQLError)?.message +} + export const resolvers: Resolvers = { Products: { aggregations: async (root, _args, context) => { @@ -101,14 +105,18 @@ export const resolvers: Resolvers = { if (!isAgolia) return context.m2.Query.products({ root, args, context, info }) + const searchResults = hasSearchRequest(info) ? getSearchResults(args, context, info) : null + + if (isGraphQLError(await searchResults)) { + return context.m2.Query.products({ root, args, context, info }) + } + const searchSuggestsions = isSuggestionsEnabled() && hasSuggestionsRequest(info) && args.search && getSearchSuggestions(args.search, context) - const searchResults = hasSearchRequest(info) ? getSearchResults(args, context, info) : null - return { algoliaSearchResults: await searchResults, suggestions: (await searchSuggestsions) || null, From e7104b7f14cfc4f0e62d8f85f82f442b0a19fbac Mon Sep 17 00:00:00 2001 From: Renzo van Hootegem Date: Mon, 2 Sep 2024 14:09:13 +0200 Subject: [PATCH 17/70] Await searchresults once --- packages/algolia-mesh/mesh/resolvers.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/algolia-mesh/mesh/resolvers.ts b/packages/algolia-mesh/mesh/resolvers.ts index 892a4ac4d3..86234ea689 100644 --- a/packages/algolia-mesh/mesh/resolvers.ts +++ b/packages/algolia-mesh/mesh/resolvers.ts @@ -105,9 +105,9 @@ export const resolvers: Resolvers = { if (!isAgolia) return context.m2.Query.products({ root, args, context, info }) - const searchResults = hasSearchRequest(info) ? getSearchResults(args, context, info) : null - - if (isGraphQLError(await searchResults)) { + const searchResponse = hasSearchRequest(info) ? getSearchResults(args, context, info) : null + const searchResults = await searchResponse + if (isGraphQLError(searchResults)) { return context.m2.Query.products({ root, args, context, info }) } @@ -118,9 +118,9 @@ export const resolvers: Resolvers = { getSearchSuggestions(args.search, context) return { - algoliaSearchResults: await searchResults, + algoliaSearchResults: searchResults, suggestions: (await searchSuggestsions) || null, - algolia_queryID: (await searchResults)?.queryID, + algolia_queryID: searchResults?.queryID, } }, }, From c758a5033e7b45b13b91cdedb24a517e4811f7f1 Mon Sep 17 00:00:00 2001 From: Renzo van Hootegem Date: Tue, 3 Sep 2024 09:31:03 +0200 Subject: [PATCH 18/70] Moved the await for searchresults down after other requests --- packages/algolia-mesh/mesh/resolvers.ts | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/packages/algolia-mesh/mesh/resolvers.ts b/packages/algolia-mesh/mesh/resolvers.ts index 86234ea689..fd55110625 100644 --- a/packages/algolia-mesh/mesh/resolvers.ts +++ b/packages/algolia-mesh/mesh/resolvers.ts @@ -105,11 +105,9 @@ export const resolvers: Resolvers = { if (!isAgolia) return context.m2.Query.products({ root, args, context, info }) - const searchResponse = hasSearchRequest(info) ? getSearchResults(args, context, info) : null - const searchResults = await searchResponse - if (isGraphQLError(searchResults)) { - return context.m2.Query.products({ root, args, context, info }) - } + const searchResultsResponse = hasSearchRequest(info) + ? getSearchResults(args, context, info) + : null const searchSuggestsions = isSuggestionsEnabled() && @@ -117,10 +115,14 @@ export const resolvers: Resolvers = { args.search && getSearchSuggestions(args.search, context) + const searchResults = await searchResultsResponse + if (isGraphQLError(searchResults)) { + return context.m2.Query.products({ root, args, context, info }) + } return { - algoliaSearchResults: searchResults, + algoliaSearchResults: checkResponse(searchResults, context, root, args, info), suggestions: (await searchSuggestsions) || null, - algolia_queryID: searchResults?.queryID, + algolia_queryID: await searchResults?.queryID, } }, }, From c10b2256da1e462e247af048809e6fc2a169848d Mon Sep 17 00:00:00 2001 From: Renzo van Hootegem Date: Tue, 3 Sep 2024 11:51:59 +0200 Subject: [PATCH 19/70] Fixed error --- packages/algolia-mesh/mesh/resolvers.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/algolia-mesh/mesh/resolvers.ts b/packages/algolia-mesh/mesh/resolvers.ts index fd55110625..649ef3b55d 100644 --- a/packages/algolia-mesh/mesh/resolvers.ts +++ b/packages/algolia-mesh/mesh/resolvers.ts @@ -120,9 +120,9 @@ export const resolvers: Resolvers = { return context.m2.Query.products({ root, args, context, info }) } return { - algoliaSearchResults: checkResponse(searchResults, context, root, args, info), + algoliaSearchResults: searchResults, suggestions: (await searchSuggestsions) || null, - algolia_queryID: await searchResults?.queryID, + algolia_queryID: searchResults?.queryID, } }, }, From f51e3f18dd2fba44e98624ad86957e64feec9244 Mon Sep 17 00:00:00 2001 From: Renzo van Hootegem Date: Tue, 3 Sep 2024 12:00:26 +0200 Subject: [PATCH 20/70] Added import for graphqlerror --- packages/algolia-mesh/mesh/resolvers.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/algolia-mesh/mesh/resolvers.ts b/packages/algolia-mesh/mesh/resolvers.ts index 649ef3b55d..592699f0fd 100644 --- a/packages/algolia-mesh/mesh/resolvers.ts +++ b/packages/algolia-mesh/mesh/resolvers.ts @@ -3,7 +3,7 @@ import { hasSelectionSetPath, type Resolvers, } from '@graphcommerce/graphql-mesh' -import type { GraphQLResolveInfo } from 'graphql' +import type { GraphQLError, GraphQLResolveInfo } from 'graphql' import { algoliaFacetsToAggregations, getCategoryList } from './algoliaFacetsToAggregations' import { algoliaHitToMagentoProduct, ProductsItemsItem } from './algoliaHitToMagentoProduct' import { getAlgoliaSettings } from './getAlgoliaSettings' From e96946dd9ce0ed591eda642216ca6004362aa945 Mon Sep 17 00:00:00 2001 From: Paul Hachmang Date: Wed, 4 Sep 2024 11:09:47 +0200 Subject: [PATCH 21/70] Cleanup resolver --- packages/algolia-mesh/mesh/resolvers.ts | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/packages/algolia-mesh/mesh/resolvers.ts b/packages/algolia-mesh/mesh/resolvers.ts index 592699f0fd..fa5059a346 100644 --- a/packages/algolia-mesh/mesh/resolvers.ts +++ b/packages/algolia-mesh/mesh/resolvers.ts @@ -105,24 +105,21 @@ export const resolvers: Resolvers = { if (!isAgolia) return context.m2.Query.products({ root, args, context, info }) - const searchResultsResponse = hasSearchRequest(info) - ? getSearchResults(args, context, info) - : null - const searchSuggestsions = isSuggestionsEnabled() && hasSuggestionsRequest(info) && args.search && getSearchSuggestions(args.search, context) - const searchResults = await searchResultsResponse - if (isGraphQLError(searchResults)) { + const searchResults = hasSearchRequest(info) ? getSearchResults(args, context, info) : null + + if (isGraphQLError(await searchResults)) return context.m2.Query.products({ root, args, context, info }) - } + return { - algoliaSearchResults: searchResults, + algoliaSearchResults: await searchResults, suggestions: (await searchSuggestsions) || null, - algolia_queryID: searchResults?.queryID, + algolia_queryID: (await searchResults)?.queryID, } }, }, From 98a0b8ffe43e37ed1855c5112029983d2b37ea39 Mon Sep 17 00:00:00 2001 From: Renzo van Hootegem Date: Fri, 6 Sep 2024 11:36:34 +0200 Subject: [PATCH 22/70] Created get Recommendations query --- packages/algolia-recommend/Config.graphqls | 3 ++ .../mesh/getRecommendations.ts | 32 +++++++++++++++ .../mesh/getRecommendationsInput.ts | 39 +++++++++++++++++++ packages/algolia-recommend/package.json | 3 +- 4 files changed, 75 insertions(+), 2 deletions(-) create mode 100644 packages/algolia-recommend/Config.graphqls create mode 100644 packages/algolia-recommend/mesh/getRecommendations.ts create mode 100644 packages/algolia-recommend/mesh/getRecommendationsInput.ts diff --git a/packages/algolia-recommend/Config.graphqls b/packages/algolia-recommend/Config.graphqls new file mode 100644 index 0000000000..2d5b16d3e3 --- /dev/null +++ b/packages/algolia-recommend/Config.graphqls @@ -0,0 +1,3 @@ +extend input GraphCommerceAlgoliaConfig { + recommendEnabled: Boolean +} diff --git a/packages/algolia-recommend/mesh/getRecommendations.ts b/packages/algolia-recommend/mesh/getRecommendations.ts new file mode 100644 index 0000000000..4cc99419b5 --- /dev/null +++ b/packages/algolia-recommend/mesh/getRecommendations.ts @@ -0,0 +1,32 @@ +import { MeshContext, QueryproductsArgs } from '@graphcommerce/graphql-mesh' +import type { GraphQLResolveInfo } from 'graphql' +import { getRecommendationsInput } from './getRecommendationsInput' + +export async function getRecommendations( + args: QueryproductsArgs, + context: MeshContext, + info: GraphQLResolveInfo, + objectId: string, + recommendType: string, +) { + return context.algoliaRecommend.Query.algolia_getRecommendations({ + args: { + input: await getRecommendationsInput(args, context, objectId, recommendType), + }, + selectionSet: /* GraphQL */ ` + { + results { + nbHits + hits { + ... on AlgoliarecommendHit { + objectID + additionalProperties + } + } + } + } + `, + context, + info, + }) +} diff --git a/packages/algolia-recommend/mesh/getRecommendationsInput.ts b/packages/algolia-recommend/mesh/getRecommendationsInput.ts new file mode 100644 index 0000000000..0878feefc7 --- /dev/null +++ b/packages/algolia-recommend/mesh/getRecommendationsInput.ts @@ -0,0 +1,39 @@ +import { getIndexName } from '@graphcommerce/algolia-mesh/mesh/getIndexName' +import { + MeshContext, + AlgoliagetRecommendationsParams_Input, + // AlgoliarelatedModel, +} from '@graphcommerce/graphql-mesh' + +// function returnAlgoliaModelString(model: string): AlgoliarelatedModel { +// const returnObject = { +// related: 'related_products', +// } +// return model +// } + +function getRequest(model, objectId, indexName) { + const returnObject = { + related: { + Related_products_Input: { + indexName, + objectID: objectId, + threshold: 100, + model: 'related_products', + }, + }, + } + + return returnObject[model] ? returnObject[model] : {} +} +export function getRecommendationsInput( + // args: QueryproductsArgs, + context: MeshContext, + objectId: string, + model: string, +): AlgoliagetRecommendationsParams_Input { + const indexName = getIndexName(context) + return { + requests: [getRequest(model, objectId, indexName)], + } +} diff --git a/packages/algolia-recommend/package.json b/packages/algolia-recommend/package.json index d1d8728481..5366e11aae 100644 --- a/packages/algolia-recommend/package.json +++ b/packages/algolia-recommend/package.json @@ -15,10 +15,9 @@ "generate": "tsx scripts/generate-recommend-spec.mts" }, "peerDependencies": { + "@graphcommerce/algolia-mesh": "^9.0.0-canary.76", "@graphcommerce/graphql": "^9.0.0-canary.76", "@graphcommerce/graphql-mesh": "^9.0.0-canary.76", - "@graphcommerce/magento-product": "^9.0.0-canary.76", - "@graphcommerce/magento-search": "^9.0.0-canary.76", "@graphcommerce/next-config": "^9.0.0-canary.76", "@graphcommerce/next-ui": "^9.0.0-canary.76", "react": "^18.2.0" From 472e82edf8337e9e4e7408d7210d54628480f467 Mon Sep 17 00:00:00 2001 From: Renzo van Hootegem Date: Fri, 6 Sep 2024 11:37:08 +0200 Subject: [PATCH 23/70] Corrected typing of recommend in the mesh --- .../algolia-recommend/plugins/meshConfigAlgoliaRecommend.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/algolia-recommend/plugins/meshConfigAlgoliaRecommend.ts b/packages/algolia-recommend/plugins/meshConfigAlgoliaRecommend.ts index e4a5367d82..157df0aa10 100644 --- a/packages/algolia-recommend/plugins/meshConfigAlgoliaRecommend.ts +++ b/packages/algolia-recommend/plugins/meshConfigAlgoliaRecommend.ts @@ -31,7 +31,7 @@ export const meshConfig: FunctionPlugin = ( 'X-Algolia-Application-Id': graphCommerceConfig.algolia.applicationId, 'X-Algolia-API-Key': graphCommerceConfig.algolia.searchOnlyApiKey, }, - selectQueryOrMutationField: [{ type: 'Mutation', fieldName: 'getRecommendations' }], + selectQueryOrMutationField: [{ type: 'Query', fieldName: 'getRecommendations' }], }, }, transforms: [ From 4afdd958fa7a679e4f4e624be55a4965d167a802 Mon Sep 17 00:00:00 2001 From: Renzo van Hootegem Date: Fri, 6 Sep 2024 14:39:48 +0200 Subject: [PATCH 24/70] Recommend progress TODO --- .../mesh/algoliaHitToMagentoProduct.ts | 10 +- packages/algolia-mesh/mesh/resolvers.ts | 10 +- .../mesh/getRecommendations.ts | 27 ++- .../mesh/getRecommendationsInput.ts | 1 - .../mesh/recommendHitToMagentoProduct.ts | 188 ++++++++++++++++++ .../plugins/algoliaMeshAddRecommend.ts | 58 ++++++ 6 files changed, 285 insertions(+), 9 deletions(-) create mode 100644 packages/algolia-recommend/mesh/recommendHitToMagentoProduct.ts create mode 100644 packages/algolia-recommend/plugins/algoliaMeshAddRecommend.ts diff --git a/packages/algolia-mesh/mesh/algoliaHitToMagentoProduct.ts b/packages/algolia-mesh/mesh/algoliaHitToMagentoProduct.ts index daca077e06..8ef3ffd59a 100644 --- a/packages/algolia-mesh/mesh/algoliaHitToMagentoProduct.ts +++ b/packages/algolia-mesh/mesh/algoliaHitToMagentoProduct.ts @@ -11,6 +11,8 @@ import { ResolversParentTypes, ResolversTypes, } from '@graphcommerce/graphql-mesh' +// eslint-disable-next-line import/no-extraneous-dependencies +import { GraphQLResolveInfo } from 'graphql' import { GetStoreConfigReturn } from './getStoreConfig' function assertAdditional( @@ -120,6 +122,12 @@ export function algoliaHitToMagentoProduct( hit: Algoliahit, storeConfig: GetStoreConfigReturn, customerGroup: number, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + context: MeshContext, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + info: GraphQLResolveInfo, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + nested: boolean = false, ): ProductsItemsItem | null { const { objectID, additionalProperties } = hit if (!assertAdditional(additionalProperties)) return null @@ -173,7 +181,7 @@ export function algoliaHitToMagentoProduct( // options_container: null, // price_tiers: [], // product_links: [], - // related_products: [], + // related_products: getRecommendations(), // short_description: null, // small_image: null, // special_price: null, diff --git a/packages/algolia-mesh/mesh/resolvers.ts b/packages/algolia-mesh/mesh/resolvers.ts index dbf935277f..25bac2c74d 100644 --- a/packages/algolia-mesh/mesh/resolvers.ts +++ b/packages/algolia-mesh/mesh/resolvers.ts @@ -79,7 +79,7 @@ export const resolvers: Resolvers = { } }, - items: async (root, args, context) => { + items: async (root, args, context, info) => { if (!isAlgoliaResponse(root)) return root.items ?? null const items: (ProductsItemsItem | null)[] = [] @@ -87,7 +87,13 @@ export const resolvers: Resolvers = { const config = await getStoreConfig(context) for (const hit of root.algoliaSearchResults.hits) { if (hit?.objectID) { - const product = algoliaHitToMagentoProduct(hit, config, getGroupId(context)) + const product = algoliaHitToMagentoProduct( + hit, + config, + getGroupId(context), + context, + info, + ) items.push(product) } } diff --git a/packages/algolia-recommend/mesh/getRecommendations.ts b/packages/algolia-recommend/mesh/getRecommendations.ts index 4cc99419b5..cdb1350f44 100644 --- a/packages/algolia-recommend/mesh/getRecommendations.ts +++ b/packages/algolia-recommend/mesh/getRecommendations.ts @@ -1,17 +1,19 @@ -import { MeshContext, QueryproductsArgs } from '@graphcommerce/graphql-mesh' +import { MeshContext, ProductInterface } from '@graphcommerce/graphql-mesh' import type { GraphQLResolveInfo } from 'graphql' import { getRecommendationsInput } from './getRecommendationsInput' +import { recommendHitToMagentoProduct } from './recommendHitToMagentoProduct' +import { algoliaHitToMagentoProduct, ProductsItemsItem } from '@graphcommerce/algolia-mesh' +import { getStoreConfig } from '@graphcommerce/algolia-mesh/mesh/getStoreConfig' export async function getRecommendations( - args: QueryproductsArgs, context: MeshContext, info: GraphQLResolveInfo, objectId: string, recommendType: string, -) { - return context.algoliaRecommend.Query.algolia_getRecommendations({ +): Promise { + const recommendItems = await context.algoliaRecommend.Query.algolia_getRecommendations({ args: { - input: await getRecommendationsInput(args, context, objectId, recommendType), + input: getRecommendationsInput(context, objectId, recommendType), }, selectionSet: /* GraphQL */ ` { @@ -29,4 +31,19 @@ export async function getRecommendations( context, info, }) + + const items: ProductsItemsItem[] = [] + const config = await getStoreConfig(context) + recommendItems?.results.map((result) => + result?.hits.forEach((hit) => { + if (hit) { + const item = algoliaHitToMagentoProduct(hit, config, 0, context, info) + if (item) { + items.push(item) + } + } + }), + ) + + return items } diff --git a/packages/algolia-recommend/mesh/getRecommendationsInput.ts b/packages/algolia-recommend/mesh/getRecommendationsInput.ts index 0878feefc7..fe05eca70c 100644 --- a/packages/algolia-recommend/mesh/getRecommendationsInput.ts +++ b/packages/algolia-recommend/mesh/getRecommendationsInput.ts @@ -27,7 +27,6 @@ function getRequest(model, objectId, indexName) { return returnObject[model] ? returnObject[model] : {} } export function getRecommendationsInput( - // args: QueryproductsArgs, context: MeshContext, objectId: string, model: string, diff --git a/packages/algolia-recommend/mesh/recommendHitToMagentoProduct.ts b/packages/algolia-recommend/mesh/recommendHitToMagentoProduct.ts new file mode 100644 index 0000000000..6e053cbbc9 --- /dev/null +++ b/packages/algolia-recommend/mesh/recommendHitToMagentoProduct.ts @@ -0,0 +1,188 @@ +import { GetStoreConfigReturn } from '@graphcommerce/algolia-mesh/mesh/getStoreConfig' +import { + AlgoliaPrice, + AlgoliaProductHitAdditionalProperties, + Algoliahit, + CurrencyEnum, + MeshContext, + PriceRange, + QueryproductsArgs, + RequireFields, + ResolverFn, + ResolversParentTypes, + ResolversTypes, +} from '@graphcommerce/graphql-mesh' +function assertAdditional( + additional: unknown, +): additional is AlgoliaProductHitAdditionalProperties { + return true +} + +const algoliaTypeToTypename = { + bundle: 'BundleProduct', + simple: 'SimpleProduct', + configurable: 'ConfigurableProduct', + downloadable: 'DownloadableProduct', + virtual: 'VirtualProduct', + grouped: 'GroupedProduct', + giftcard: 'GiftCardProduct', +} as const + +function mapPriceRange( + price: AlgoliaProductHitAdditionalProperties['price'], + storeConfig: GetStoreConfigReturn, + customerGroup = 0, +): PriceRange { + if (!storeConfig?.default_display_currency_code) throw new Error('Currency is required') + + const key = storeConfig.default_display_currency_code as keyof AlgoliaPrice + const currency = storeConfig.default_display_currency_code as CurrencyEnum + + const maxRegular = price?.[key]?.default_max ?? 0 + const maxFinal = price?.[key]?.[`group_${customerGroup}_max`] ?? price?.[key]?.default_max ?? 0 + + const minRegular = price?.[key]?.default ?? 0 + const minFinal = price?.[key]?.[`group_${customerGroup}`] ?? price?.[key]?.default + + return { + maximum_price: { + regular_price: { + currency, + value: maxRegular, + }, + final_price: { + currency, + value: maxFinal, + }, + discount: { + percent_off: + maxRegular !== maxFinal && maxRegular > 0 ? 1 - (maxFinal / maxRegular) * 100 : 0, + amount_off: maxRegular - maxFinal, + }, + // fixed_product_taxes + }, + minimum_price: { + regular_price: { + currency, + value: price?.[key]?.default, + }, + final_price: { + currency, + value: minFinal, + }, + discount: { + percent_off: + minRegular !== minFinal && minRegular > 0 ? 1 - (minFinal / minRegular) * 100 : 0, + amount_off: minRegular - minFinal, + }, + // fixed_product_taxes + }, + } +} + +function algoliaUrlToUrlKey(url?: string | null, base?: string | null): string | null { + if (!url || !base) return null + return url.replace(base, '') +} + +/** + * For the URL https://configurator.reachdigital.dev/media/catalog/product/cache/d911de87cf9e562637815cc5a14b1b05/1/0/1087_1_3.jpg + * Remove /cache/HASH from the URL but only if the url contains media/catalog/product + * @param url + */ +function getOriginalImage(url?: string | undefined | null) { + if (!url || !url.includes('media/catalog/product')) return url + return url.replace(/\/cache\/[a-z0-9]+/, '') +} + +export type ProductsItemsItem = NonNullable< + Awaited< + ReturnType< + ResolverFn< + ResolversTypes['Products'], + ResolversParentTypes['Query'], + MeshContext, + RequireFields + > + > + >['items'] +>[number] & { + __typename: (typeof algoliaTypeToTypename)[keyof typeof algoliaTypeToTypename] +} + +/** + * Mapping function to map Algolia hit to Magento product. + * + * You can create a FunctionPlugin to modify the behavior of this function or implement brand specific code. + */ +export function recommendHitToMagentoProduct( + hit: Algoliahit, + storeConfig: GetStoreConfigReturn, + customerGroup: number, +): ProductsItemsItem | null { + const { objectID, additionalProperties } = hit + if (!assertAdditional(additionalProperties)) return null + + const { + sku, + created_at, + image_url, + is_stock, + + price, + thumbnail_url, + type_id, + url, + + // not used + ordered_qty, + visibility_catalog, + visibility_search, + rating_summary, + + // The rest will be spread into the product + ...rest + } = additionalProperties + + return { + redirect_code: 0, + __typename: algoliaTypeToTypename[type_id as keyof typeof algoliaTypeToTypename], + uid: btoa(objectID), + sku: Array.isArray(sku) ? sku[0] : `${sku}`, + price_range: mapPriceRange(price, storeConfig, customerGroup), + created_at: created_at ? new Date(created_at).toISOString() : null, + stock_status: is_stock ? 'IN_STOCK' : 'OUT_OF_STOCK', + review_count: 0, + rating_summary: Number(rating_summary), + reviews: { items: [], page_info: {} }, + // canonical_url: null, + // categories: [], + // country_of_manufacture: null, + // crosssell_products: [], + // custom_attributesV2: null, + // description: null, + // gift_message_available: null, + image: { url: getOriginalImage(image_url) }, + // media_gallery: [], + // meta_keyword: null, + // meta_title: null, + // new_from_date: null, + // new_to_date: null, + // only_x_left_in_stock: null, + // options_container: null, + // price_tiers: [], + // product_links: [], + // related_products: getRecommendations(), + // short_description: null, + // small_image: null, + // special_price: null, + // special_to_date: null, + small_image: { url: getOriginalImage(thumbnail_url) }, + swatch_image: getOriginalImage(image_url), + thumbnail: { url: getOriginalImage(thumbnail_url) }, + // upsell_products: [], + url_key: algoliaUrlToUrlKey(url, storeConfig?.base_link_url), + url_suffix: storeConfig?.product_url_suffix, + ...rest, + } +} diff --git a/packages/algolia-recommend/plugins/algoliaMeshAddRecommend.ts b/packages/algolia-recommend/plugins/algoliaMeshAddRecommend.ts new file mode 100644 index 0000000000..7123cce4b7 --- /dev/null +++ b/packages/algolia-recommend/plugins/algoliaMeshAddRecommend.ts @@ -0,0 +1,58 @@ +import type { + algoliaHitToMagentoProduct as algoliaHitToMagentoProductType, + ProductsItemsItem, +} from '@graphcommerce/algolia-mesh' +import { FunctionPlugin, PluginConfig } from '@graphcommerce/next-config' +import { getRecommendations } from '../mesh/getRecommendations' +import { MeshContext, ProductInterface } from '@graphcommerce/graphql-mesh' +import { GraphQLResolveInfo } from 'graphql' + +export const config: PluginConfig = { + type: 'function', + module: '@graphcommerce/algolia-mesh', + ifConfig: 'algolia.recommendEnabled', +} + +export const algoliaHitToMagentoProduct: FunctionPlugin< + typeof algoliaHitToMagentoProductType +> = async ( + prev, + hit, + storeConfig, + customerGroup, + context: MeshContext, + info: GraphQLResolveInfo, + nested: boolean, +) => { + const result: ProductsItemsItem | null = prev( + hit, + storeConfig, + customerGroup, + context, + info, + true, + ) + if (nested) { + return result + } + + const objectId: string = hit.objectID + if (result && 'related_products' in result) { + const test = await getRecommendations(context, info, objectId, 'related_products') + result.related_products = [ + { + price_range: { minimum_price: { final_price: {}, regular_price: {} }, maximum_price: null }, + rating_summary: 0, + review_count: 0, + uid: 'test', + redirect_code: 0, + reviews: { page_info: {}, items: [] }, + }, + ] + result.upsell_products = [] + + // related_products?: Maybe>>, + } + + return result +} From b56e653228b35729fd94d528d468c12e14223b04 Mon Sep 17 00:00:00 2001 From: Paul Hachmang Date: Mon, 9 Sep 2024 09:25:34 +0200 Subject: [PATCH 25/70] Updated recommedations --- .../mesh/algoliaHitToMagentoProduct.ts | 8 +- .../algolia-mesh/mesh/getAlgoliaSettings.ts | 2 +- packages/algolia-mesh/mesh/getGroupId.ts | 2 +- packages/algolia-mesh/mesh/getIndexName.ts | 2 +- .../algolia-mesh/mesh/getSearchResults.ts | 2 +- .../mesh/getSearchResultsInput.ts | 2 +- .../algolia-mesh/mesh/getSearchSuggestions.ts | 2 +- .../mesh/getSearchSuggestionsInput.ts | 2 +- ...ctFilterInputToAlgoliafacetFiltersInput.ts | 2 +- .../mesh/getRecommendations.ts | 49 ------ packages/algolia-recommend/mesh/resolvers.ts | 164 ++++++++++++++++-- .../plugins/algoliaMeshAddRecommend.ts | 58 ------- 12 files changed, 161 insertions(+), 134 deletions(-) delete mode 100644 packages/algolia-recommend/mesh/getRecommendations.ts delete mode 100644 packages/algolia-recommend/plugins/algoliaMeshAddRecommend.ts diff --git a/packages/algolia-mesh/mesh/algoliaHitToMagentoProduct.ts b/packages/algolia-mesh/mesh/algoliaHitToMagentoProduct.ts index 8ef3ffd59a..3fc3d59250 100644 --- a/packages/algolia-mesh/mesh/algoliaHitToMagentoProduct.ts +++ b/packages/algolia-mesh/mesh/algoliaHitToMagentoProduct.ts @@ -1,4 +1,4 @@ -import { +import type { AlgoliaPrice, AlgoliaProductHitAdditionalProperties, Algoliahit, @@ -122,12 +122,6 @@ export function algoliaHitToMagentoProduct( hit: Algoliahit, storeConfig: GetStoreConfigReturn, customerGroup: number, - // eslint-disable-next-line @typescript-eslint/no-unused-vars - context: MeshContext, - // eslint-disable-next-line @typescript-eslint/no-unused-vars - info: GraphQLResolveInfo, - // eslint-disable-next-line @typescript-eslint/no-unused-vars - nested: boolean = false, ): ProductsItemsItem | null { const { objectID, additionalProperties } = hit if (!assertAdditional(additionalProperties)) return null diff --git a/packages/algolia-mesh/mesh/getAlgoliaSettings.ts b/packages/algolia-mesh/mesh/getAlgoliaSettings.ts index eeb5fdb7df..1718c88e01 100644 --- a/packages/algolia-mesh/mesh/getAlgoliaSettings.ts +++ b/packages/algolia-mesh/mesh/getAlgoliaSettings.ts @@ -1,4 +1,4 @@ -import { AlgoliasettingsResponse, MeshContext } from '@graphcommerce/graphql-mesh' +import type { AlgoliasettingsResponse, MeshContext } from '@graphcommerce/graphql-mesh' import { getIndexName } from './getIndexName' let settingsCache: AlgoliasettingsResponse | null = null diff --git a/packages/algolia-mesh/mesh/getGroupId.ts b/packages/algolia-mesh/mesh/getGroupId.ts index 5ab317916b..5960d63b00 100644 --- a/packages/algolia-mesh/mesh/getGroupId.ts +++ b/packages/algolia-mesh/mesh/getGroupId.ts @@ -1,4 +1,4 @@ -import { MeshContext } from '@graphcommerce/graphql-mesh' +import type { MeshContext } from '@graphcommerce/graphql-mesh' export function getGroupId(context: MeshContext): number { const { headers } = context as MeshContext & { headers?: Record } diff --git a/packages/algolia-mesh/mesh/getIndexName.ts b/packages/algolia-mesh/mesh/getIndexName.ts index 4678635a25..d7d6b95603 100644 --- a/packages/algolia-mesh/mesh/getIndexName.ts +++ b/packages/algolia-mesh/mesh/getIndexName.ts @@ -1,4 +1,4 @@ -import { MeshContext } from '@graphcommerce/graphql-mesh' +import type { MeshContext } from '@graphcommerce/graphql-mesh' import { storefrontConfigDefault } from '@graphcommerce/next-ui' function getStoreHeader(context: MeshContext) { diff --git a/packages/algolia-mesh/mesh/getSearchResults.ts b/packages/algolia-mesh/mesh/getSearchResults.ts index ba4697aba3..129c97fba1 100644 --- a/packages/algolia-mesh/mesh/getSearchResults.ts +++ b/packages/algolia-mesh/mesh/getSearchResults.ts @@ -1,4 +1,4 @@ -import { MeshContext, QueryproductsArgs } from '@graphcommerce/graphql-mesh' +import type { MeshContext, QueryproductsArgs } from '@graphcommerce/graphql-mesh' import type { GraphQLResolveInfo } from 'graphql' import { getAlgoliaSettings } from './getAlgoliaSettings' import { getSearchResultsInput } from './getSearchResultsInput' diff --git a/packages/algolia-mesh/mesh/getSearchResultsInput.ts b/packages/algolia-mesh/mesh/getSearchResultsInput.ts index 4f52d4d6ec..729c2bbf14 100644 --- a/packages/algolia-mesh/mesh/getSearchResultsInput.ts +++ b/packages/algolia-mesh/mesh/getSearchResultsInput.ts @@ -1,4 +1,4 @@ -import { +import type { MeshContext, Queryalgolia_searchSingleIndexArgs, QueryproductsArgs, diff --git a/packages/algolia-mesh/mesh/getSearchSuggestions.ts b/packages/algolia-mesh/mesh/getSearchSuggestions.ts index feb0336bab..2c76e3bae6 100644 --- a/packages/algolia-mesh/mesh/getSearchSuggestions.ts +++ b/packages/algolia-mesh/mesh/getSearchSuggestions.ts @@ -1,4 +1,4 @@ -import { MeshContext, SearchSuggestion } from '@graphcommerce/graphql-mesh' +import type { MeshContext, SearchSuggestion } from '@graphcommerce/graphql-mesh' import { filterNonNullableKeys } from '@graphcommerce/next-ui' import { getSearchSuggestionsInput, getSuggestionsIndexName } from './getSearchSuggestionsInput' diff --git a/packages/algolia-mesh/mesh/getSearchSuggestionsInput.ts b/packages/algolia-mesh/mesh/getSearchSuggestionsInput.ts index e813d2cff1..df0c8a7145 100644 --- a/packages/algolia-mesh/mesh/getSearchSuggestionsInput.ts +++ b/packages/algolia-mesh/mesh/getSearchSuggestionsInput.ts @@ -1,4 +1,4 @@ -import { MeshContext, Queryalgolia_searchSingleIndexArgs } from '@graphcommerce/graphql-mesh' +import type { MeshContext, Queryalgolia_searchSingleIndexArgs } from '@graphcommerce/graphql-mesh' import { getIndexName } from './getIndexName' export function isSuggestionsEnabled() { diff --git a/packages/algolia-mesh/mesh/productFilterInputToAlgoliafacetFiltersInput.ts b/packages/algolia-mesh/mesh/productFilterInputToAlgoliafacetFiltersInput.ts index 532ed13ba1..0fa91fd4e2 100644 --- a/packages/algolia-mesh/mesh/productFilterInputToAlgoliafacetFiltersInput.ts +++ b/packages/algolia-mesh/mesh/productFilterInputToAlgoliafacetFiltersInput.ts @@ -1,4 +1,4 @@ -import { +import type { ProductAttributeFilterInput, AlgolianumericFilters_Input, } from '@graphcommerce/graphql-mesh' diff --git a/packages/algolia-recommend/mesh/getRecommendations.ts b/packages/algolia-recommend/mesh/getRecommendations.ts deleted file mode 100644 index cdb1350f44..0000000000 --- a/packages/algolia-recommend/mesh/getRecommendations.ts +++ /dev/null @@ -1,49 +0,0 @@ -import { MeshContext, ProductInterface } from '@graphcommerce/graphql-mesh' -import type { GraphQLResolveInfo } from 'graphql' -import { getRecommendationsInput } from './getRecommendationsInput' -import { recommendHitToMagentoProduct } from './recommendHitToMagentoProduct' -import { algoliaHitToMagentoProduct, ProductsItemsItem } from '@graphcommerce/algolia-mesh' -import { getStoreConfig } from '@graphcommerce/algolia-mesh/mesh/getStoreConfig' - -export async function getRecommendations( - context: MeshContext, - info: GraphQLResolveInfo, - objectId: string, - recommendType: string, -): Promise { - const recommendItems = await context.algoliaRecommend.Query.algolia_getRecommendations({ - args: { - input: getRecommendationsInput(context, objectId, recommendType), - }, - selectionSet: /* GraphQL */ ` - { - results { - nbHits - hits { - ... on AlgoliarecommendHit { - objectID - additionalProperties - } - } - } - } - `, - context, - info, - }) - - const items: ProductsItemsItem[] = [] - const config = await getStoreConfig(context) - recommendItems?.results.map((result) => - result?.hits.forEach((hit) => { - if (hit) { - const item = algoliaHitToMagentoProduct(hit, config, 0, context, info) - if (item) { - items.push(item) - } - } - }), - ) - - return items -} diff --git a/packages/algolia-recommend/mesh/resolvers.ts b/packages/algolia-recommend/mesh/resolvers.ts index efb80487a6..c7bfb456e6 100644 --- a/packages/algolia-recommend/mesh/resolvers.ts +++ b/packages/algolia-recommend/mesh/resolvers.ts @@ -1,16 +1,156 @@ -import { MeshContext, Resolvers } from '@graphcommerce/graphql-mesh' +/* eslint-disable @typescript-eslint/require-await */ +import { algoliaHitToMagentoProduct } from '@graphcommerce/algolia-mesh' +import { getGroupId } from '@graphcommerce/algolia-mesh/mesh/getGroupId' +import { getIndexName } from '@graphcommerce/algolia-mesh/mesh/getIndexName' +import { getStoreConfig } from '@graphcommerce/algolia-mesh/mesh/getStoreConfig' +import { nonNullable } from '@graphcommerce/algolia-mesh/mesh/utils' +import type { + Algoliahit, + AlgoliarecommendationsRequest_Input, + AlgoliarecommendHit, + AlgoliatrendingFacetHit, + MeshContext, + Resolvers, +} from '@graphcommerce/graphql-mesh' +import type { GraphQLResolveInfo } from 'graphql' + +function isAlgoliaRecommendHit( + hit: AlgoliarecommendHit | AlgoliatrendingFacetHit | Algoliahit | null, +): hit is Algoliahit { + return !!hit && '__typename' in hit && hit.__typename === 'AlgoliarecommendHit' +} + +function createAlgoliaRecommendationRequest( + keyInput: K, + additionalArgs?: AlgoliarecommendationsRequest_Input[K], +) { + return async ( + root: { uid: string }, + args: unknown, + context: MeshContext, + info: GraphQLResolveInfo, + ) => { + const storeConfig = await getStoreConfig(context) + const groupId = getGroupId(context) + + const results = + (await context.algoliaRecommend.Query.algolia_getRecommendations({ + key: keyInput, + argsFromKeys: (keys) => ({ + input: { + requests: keys + .map((key): AlgoliarecommendationsRequest_Input | null => { + const baseTypes: Omit< + NonNullable< + AlgoliarecommendationsRequest_Input[keyof AlgoliarecommendationsRequest_Input] + >, + 'model' + > = { + indexName: getIndexName(context), + threshold: 60, + maxRecommendations: 8, + ...additionalArgs, + // queryParameters: {}, + } + + if (key === 'Trending_items_Input') + return { + Trending_items_Input: { + ...baseTypes, + model: 'trending_items', + facetName: '', + facetValue: '', + }, + } + + if (key === 'Trending_facet_values_Input') + return { + Trending_facet_values_Input: { + ...baseTypes, + model: 'trending_facets', + facetName: '', + }, + } + + if (key === 'Frequently_bought_together_Input') + return { + Frequently_bought_together_Input: { + ...baseTypes, + model: 'bought_together', + objectID: atob(root.uid), + }, + } + + if (key === 'Looking_similar_Input') + return { + Looking_similar_Input: { + ...baseTypes, + model: 'looking_similar', + objectID: atob(root.uid), + }, + } + + if (key === 'Recommended_for_you_Input') + return { + Recommended_for_you_Input: { + ...baseTypes, + model: 'recommended_for_you', + }, + } + + if (key === 'Related_products_Input') + return { + Related_products_Input: { + ...baseTypes, + model: 'related_products', + objectID: atob(root.uid), + }, + } + + return null + }) + .filter(nonNullable), + }, + }), + valuesFromResults: (res, keys) => + keys + .map((_key, index) => res?.results[index]) + .map((r) => + r?.hits + .map((hit) => + hit && isAlgoliaRecommendHit(hit) + ? algoliaHitToMagentoProduct(hit, storeConfig, groupId) + : null, + ) + .filter(nonNullable), + ) ?? null, + selectionSet: /* GraphQL */ ` + { + results { + nbHits + hits { + ... on AlgoliarecommendHit { + objectID + additionalProperties + } + } + } + } + `, + context, + info, + })) ?? null + + console.log(keyInput, root.uid, results) + + return results + } +} export const resolvers: Resolvers = { - Query: { - recommendations: { - resolve: async (root, args, context: MeshContext, info) => { - const algoliaResponse = await context.algoliaRecommend.Mutation.algolia_getRecommendations({ - args: { recommendType: 'test' }, - selectionSet: `{ - }`, - }) - return [] - }, - }, + ConfigurableProduct: { + // upsell_products: () => [], + related_products: createAlgoliaRecommendationRequest('Related_products_Input'), + // crosssell_products: createAlgoliaRecommendationRequest('Frequently_bought_together_Input'), }, } diff --git a/packages/algolia-recommend/plugins/algoliaMeshAddRecommend.ts b/packages/algolia-recommend/plugins/algoliaMeshAddRecommend.ts deleted file mode 100644 index 7123cce4b7..0000000000 --- a/packages/algolia-recommend/plugins/algoliaMeshAddRecommend.ts +++ /dev/null @@ -1,58 +0,0 @@ -import type { - algoliaHitToMagentoProduct as algoliaHitToMagentoProductType, - ProductsItemsItem, -} from '@graphcommerce/algolia-mesh' -import { FunctionPlugin, PluginConfig } from '@graphcommerce/next-config' -import { getRecommendations } from '../mesh/getRecommendations' -import { MeshContext, ProductInterface } from '@graphcommerce/graphql-mesh' -import { GraphQLResolveInfo } from 'graphql' - -export const config: PluginConfig = { - type: 'function', - module: '@graphcommerce/algolia-mesh', - ifConfig: 'algolia.recommendEnabled', -} - -export const algoliaHitToMagentoProduct: FunctionPlugin< - typeof algoliaHitToMagentoProductType -> = async ( - prev, - hit, - storeConfig, - customerGroup, - context: MeshContext, - info: GraphQLResolveInfo, - nested: boolean, -) => { - const result: ProductsItemsItem | null = prev( - hit, - storeConfig, - customerGroup, - context, - info, - true, - ) - if (nested) { - return result - } - - const objectId: string = hit.objectID - if (result && 'related_products' in result) { - const test = await getRecommendations(context, info, objectId, 'related_products') - result.related_products = [ - { - price_range: { minimum_price: { final_price: {}, regular_price: {} }, maximum_price: null }, - rating_summary: 0, - review_count: 0, - uid: 'test', - redirect_code: 0, - reviews: { page_info: {}, items: [] }, - }, - ] - result.upsell_products = [] - - // related_products?: Maybe>>, - } - - return result -} From 5182a4ae5ed6aa436c38f58fe593991827d69bfc Mon Sep 17 00:00:00 2001 From: Renzo van Hootegem Date: Mon, 9 Sep 2024 16:08:25 +0200 Subject: [PATCH 26/70] Bug fix --- packages/algolia-mesh/mesh/resolvers.ts | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/packages/algolia-mesh/mesh/resolvers.ts b/packages/algolia-mesh/mesh/resolvers.ts index 25bac2c74d..dbf935277f 100644 --- a/packages/algolia-mesh/mesh/resolvers.ts +++ b/packages/algolia-mesh/mesh/resolvers.ts @@ -79,7 +79,7 @@ export const resolvers: Resolvers = { } }, - items: async (root, args, context, info) => { + items: async (root, args, context) => { if (!isAlgoliaResponse(root)) return root.items ?? null const items: (ProductsItemsItem | null)[] = [] @@ -87,13 +87,7 @@ export const resolvers: Resolvers = { const config = await getStoreConfig(context) for (const hit of root.algoliaSearchResults.hits) { if (hit?.objectID) { - const product = algoliaHitToMagentoProduct( - hit, - config, - getGroupId(context), - context, - info, - ) + const product = algoliaHitToMagentoProduct(hit, config, getGroupId(context)) items.push(product) } } From 684c5bb9a5c1b0c31e9a555eb520baafd1dd5c4d Mon Sep 17 00:00:00 2001 From: Renzo van Hootegem Date: Mon, 9 Sep 2024 16:30:44 +0200 Subject: [PATCH 27/70] Cleanup --- .../mesh/getRecommendationsInput.ts | 7 - .../mesh/recommendHitToMagentoProduct.ts | 188 ------------------ packages/algolia-recommend/mesh/resolvers.ts | 2 - 3 files changed, 197 deletions(-) delete mode 100644 packages/algolia-recommend/mesh/recommendHitToMagentoProduct.ts diff --git a/packages/algolia-recommend/mesh/getRecommendationsInput.ts b/packages/algolia-recommend/mesh/getRecommendationsInput.ts index fe05eca70c..fbc7756fb8 100644 --- a/packages/algolia-recommend/mesh/getRecommendationsInput.ts +++ b/packages/algolia-recommend/mesh/getRecommendationsInput.ts @@ -5,13 +5,6 @@ import { // AlgoliarelatedModel, } from '@graphcommerce/graphql-mesh' -// function returnAlgoliaModelString(model: string): AlgoliarelatedModel { -// const returnObject = { -// related: 'related_products', -// } -// return model -// } - function getRequest(model, objectId, indexName) { const returnObject = { related: { diff --git a/packages/algolia-recommend/mesh/recommendHitToMagentoProduct.ts b/packages/algolia-recommend/mesh/recommendHitToMagentoProduct.ts deleted file mode 100644 index 6e053cbbc9..0000000000 --- a/packages/algolia-recommend/mesh/recommendHitToMagentoProduct.ts +++ /dev/null @@ -1,188 +0,0 @@ -import { GetStoreConfigReturn } from '@graphcommerce/algolia-mesh/mesh/getStoreConfig' -import { - AlgoliaPrice, - AlgoliaProductHitAdditionalProperties, - Algoliahit, - CurrencyEnum, - MeshContext, - PriceRange, - QueryproductsArgs, - RequireFields, - ResolverFn, - ResolversParentTypes, - ResolversTypes, -} from '@graphcommerce/graphql-mesh' -function assertAdditional( - additional: unknown, -): additional is AlgoliaProductHitAdditionalProperties { - return true -} - -const algoliaTypeToTypename = { - bundle: 'BundleProduct', - simple: 'SimpleProduct', - configurable: 'ConfigurableProduct', - downloadable: 'DownloadableProduct', - virtual: 'VirtualProduct', - grouped: 'GroupedProduct', - giftcard: 'GiftCardProduct', -} as const - -function mapPriceRange( - price: AlgoliaProductHitAdditionalProperties['price'], - storeConfig: GetStoreConfigReturn, - customerGroup = 0, -): PriceRange { - if (!storeConfig?.default_display_currency_code) throw new Error('Currency is required') - - const key = storeConfig.default_display_currency_code as keyof AlgoliaPrice - const currency = storeConfig.default_display_currency_code as CurrencyEnum - - const maxRegular = price?.[key]?.default_max ?? 0 - const maxFinal = price?.[key]?.[`group_${customerGroup}_max`] ?? price?.[key]?.default_max ?? 0 - - const minRegular = price?.[key]?.default ?? 0 - const minFinal = price?.[key]?.[`group_${customerGroup}`] ?? price?.[key]?.default - - return { - maximum_price: { - regular_price: { - currency, - value: maxRegular, - }, - final_price: { - currency, - value: maxFinal, - }, - discount: { - percent_off: - maxRegular !== maxFinal && maxRegular > 0 ? 1 - (maxFinal / maxRegular) * 100 : 0, - amount_off: maxRegular - maxFinal, - }, - // fixed_product_taxes - }, - minimum_price: { - regular_price: { - currency, - value: price?.[key]?.default, - }, - final_price: { - currency, - value: minFinal, - }, - discount: { - percent_off: - minRegular !== minFinal && minRegular > 0 ? 1 - (minFinal / minRegular) * 100 : 0, - amount_off: minRegular - minFinal, - }, - // fixed_product_taxes - }, - } -} - -function algoliaUrlToUrlKey(url?: string | null, base?: string | null): string | null { - if (!url || !base) return null - return url.replace(base, '') -} - -/** - * For the URL https://configurator.reachdigital.dev/media/catalog/product/cache/d911de87cf9e562637815cc5a14b1b05/1/0/1087_1_3.jpg - * Remove /cache/HASH from the URL but only if the url contains media/catalog/product - * @param url - */ -function getOriginalImage(url?: string | undefined | null) { - if (!url || !url.includes('media/catalog/product')) return url - return url.replace(/\/cache\/[a-z0-9]+/, '') -} - -export type ProductsItemsItem = NonNullable< - Awaited< - ReturnType< - ResolverFn< - ResolversTypes['Products'], - ResolversParentTypes['Query'], - MeshContext, - RequireFields - > - > - >['items'] ->[number] & { - __typename: (typeof algoliaTypeToTypename)[keyof typeof algoliaTypeToTypename] -} - -/** - * Mapping function to map Algolia hit to Magento product. - * - * You can create a FunctionPlugin to modify the behavior of this function or implement brand specific code. - */ -export function recommendHitToMagentoProduct( - hit: Algoliahit, - storeConfig: GetStoreConfigReturn, - customerGroup: number, -): ProductsItemsItem | null { - const { objectID, additionalProperties } = hit - if (!assertAdditional(additionalProperties)) return null - - const { - sku, - created_at, - image_url, - is_stock, - - price, - thumbnail_url, - type_id, - url, - - // not used - ordered_qty, - visibility_catalog, - visibility_search, - rating_summary, - - // The rest will be spread into the product - ...rest - } = additionalProperties - - return { - redirect_code: 0, - __typename: algoliaTypeToTypename[type_id as keyof typeof algoliaTypeToTypename], - uid: btoa(objectID), - sku: Array.isArray(sku) ? sku[0] : `${sku}`, - price_range: mapPriceRange(price, storeConfig, customerGroup), - created_at: created_at ? new Date(created_at).toISOString() : null, - stock_status: is_stock ? 'IN_STOCK' : 'OUT_OF_STOCK', - review_count: 0, - rating_summary: Number(rating_summary), - reviews: { items: [], page_info: {} }, - // canonical_url: null, - // categories: [], - // country_of_manufacture: null, - // crosssell_products: [], - // custom_attributesV2: null, - // description: null, - // gift_message_available: null, - image: { url: getOriginalImage(image_url) }, - // media_gallery: [], - // meta_keyword: null, - // meta_title: null, - // new_from_date: null, - // new_to_date: null, - // only_x_left_in_stock: null, - // options_container: null, - // price_tiers: [], - // product_links: [], - // related_products: getRecommendations(), - // short_description: null, - // small_image: null, - // special_price: null, - // special_to_date: null, - small_image: { url: getOriginalImage(thumbnail_url) }, - swatch_image: getOriginalImage(image_url), - thumbnail: { url: getOriginalImage(thumbnail_url) }, - // upsell_products: [], - url_key: algoliaUrlToUrlKey(url, storeConfig?.base_link_url), - url_suffix: storeConfig?.product_url_suffix, - ...rest, - } -} diff --git a/packages/algolia-recommend/mesh/resolvers.ts b/packages/algolia-recommend/mesh/resolvers.ts index c7bfb456e6..ad78b5f85e 100644 --- a/packages/algolia-recommend/mesh/resolvers.ts +++ b/packages/algolia-recommend/mesh/resolvers.ts @@ -141,8 +141,6 @@ function createAlgoliaRecommendationRequest Date: Tue, 10 Sep 2024 17:21:14 +0200 Subject: [PATCH 28/70] Refactor resolvers for Algolia recommendations --- packages/algolia-recommend/Config.graphqls | 43 +++- packages/algolia-recommend/README.md | 3 + .../algolia-recommend-spec.yaml | 2 + .../mesh/createProductMapper.ts | 19 ++ .../mesh/createValueFacetMapper.ts | 13 + .../mesh/getRecommendations.ts | 70 +++++ .../mesh/getRecommendationsInput.ts | 31 --- packages/algolia-recommend/mesh/resolvers.ts | 240 ++++++++---------- .../schema/AlgoliaRecommendations.graphqls | 44 ++++ .../schema/RecommendSchema.graphqls | 7 - .../scripts/generate-recommend-spec.mts | 23 +- 11 files changed, 319 insertions(+), 176 deletions(-) create mode 100644 packages/algolia-recommend/README.md create mode 100644 packages/algolia-recommend/mesh/createProductMapper.ts create mode 100644 packages/algolia-recommend/mesh/createValueFacetMapper.ts create mode 100644 packages/algolia-recommend/mesh/getRecommendations.ts delete mode 100644 packages/algolia-recommend/mesh/getRecommendationsInput.ts create mode 100644 packages/algolia-recommend/schema/AlgoliaRecommendations.graphqls delete mode 100644 packages/algolia-recommend/schema/RecommendSchema.graphqls diff --git a/packages/algolia-recommend/Config.graphqls b/packages/algolia-recommend/Config.graphqls index 2d5b16d3e3..8a56e08d91 100644 --- a/packages/algolia-recommend/Config.graphqls +++ b/packages/algolia-recommend/Config.graphqls @@ -1,3 +1,44 @@ +""" +Location where the recommendation model should be shown. This replaces the related_products, upsell_products and crosssell_products fields on products. +""" +enum GraphCommerceAlgoliaRecommendationLocation { + DISABLED + RELATED_PRODUCTS + UPSELL_PRODUCTS + CROSSSELL_PRODUCTS +} + extend input GraphCommerceAlgoliaConfig { - recommendEnabled: Boolean + """ + https://www.algolia.com/doc/guides/algolia-recommend/overview/#related-products-and-related-content + + Set the location where the related products model should be shown. + Good default is RELATED_PRODUCTS, which is shown on the Product page by default. + + Before enabling, make sure the model is trained in Algolia. + Do not assign multiple recommendation models to the same location. + """ + relatedProducts: GraphCommerceAlgoliaRecommendationLocation + + """ + https://www.algolia.com/doc/guides/algolia-recommend/overview/#looking-similar + + Set the location where the looking similar model should be shown. + Good default is UPSELL_PRODUCTS, which is shown on the Product page by default. + + Before enabling, make sure the model is trained in Algolia. + Do not assign multiple recommendation models to the same location. + """ + lookingSimilar: GraphCommerceAlgoliaRecommendationLocation + + """ + https://www.algolia.com/doc/guides/algolia-recommend/overview/#frequently-bought-together + + Set the location where the frequently bought together model should be shown. + Good default is CROSSSELL_PRODUCTS, which is shown on the Cart page. + + Before enabling, make sure the model is trained in Algolia. + Do not assign multiple recommendation models to the same location. + """ + frequentlyBoughtTogether: GraphCommerceAlgoliaRecommendationLocation } diff --git a/packages/algolia-recommend/README.md b/packages/algolia-recommend/README.md new file mode 100644 index 0000000000..42762d6c51 --- /dev/null +++ b/packages/algolia-recommend/README.md @@ -0,0 +1,3 @@ +# Algolia Recommend + +This package contains the Algolia Recommend API client and mesh resolvers. diff --git a/packages/algolia-recommend/algolia-recommend-spec.yaml b/packages/algolia-recommend/algolia-recommend-spec.yaml index 69b75f66e5..902daaa06c 100644 --- a/packages/algolia-recommend/algolia-recommend-spec.yaml +++ b/packages/algolia-recommend/algolia-recommend-spec.yaml @@ -534,6 +534,7 @@ ], "x-categories": ["Typos"] }, + "booleanString": { "type": "string", "enum": ["true", "false"] }, "ignorePlurals": { "description": "Treat singular, plurals, and other forms of declensions as equivalent.\nYou should only use this feature for the languages used in your index.\n", "example": ["ca", "es"], @@ -543,6 +544,7 @@ "description": "ISO code for languages for which this feature should be active.\nThis overrides languages you set with `queryLanguages`.\n", "items": { "$ref": "#/components/schemas/supportedLanguage" } }, + { "$ref": "#/components/schemas/booleanString" }, { "type": "boolean", "description": "If true, `ignorePlurals` is active for all languages included in `queryLanguages`, or for all supported languages, if `queryLanguges` is empty.\nIf false, singulars, plurals, and other declensions won't be considered equivalent.\n", diff --git a/packages/algolia-recommend/mesh/createProductMapper.ts b/packages/algolia-recommend/mesh/createProductMapper.ts new file mode 100644 index 0000000000..0c3e54dc9f --- /dev/null +++ b/packages/algolia-recommend/mesh/createProductMapper.ts @@ -0,0 +1,19 @@ +import { algoliaHitToMagentoProduct } from '@graphcommerce/algolia-mesh' +import { getGroupId } from '@graphcommerce/algolia-mesh/mesh/getGroupId' +import { getStoreConfig } from '@graphcommerce/algolia-mesh/mesh/getStoreConfig' +import type { + MeshContext, + AlgoliarecommendationsHit, + Algoliahit, +} from '@graphcommerce/graphql-mesh' + +export async function createProductMapper(context: MeshContext) { + const storeConfig = await getStoreConfig(context) + const groupId = getGroupId(context) + + const isAlgoliaRecommendHit = (hit: AlgoliarecommendationsHit | Algoliahit): hit is Algoliahit => + '__typename' in hit && hit.__typename === 'AlgoliarecommendHit' + + return (hit: AlgoliarecommendationsHit) => + isAlgoliaRecommendHit(hit) ? algoliaHitToMagentoProduct(hit, storeConfig, groupId) : null +} diff --git a/packages/algolia-recommend/mesh/createValueFacetMapper.ts b/packages/algolia-recommend/mesh/createValueFacetMapper.ts new file mode 100644 index 0000000000..1289a7f164 --- /dev/null +++ b/packages/algolia-recommend/mesh/createValueFacetMapper.ts @@ -0,0 +1,13 @@ +import type { + AlgoliarecommendationsHit, + AlgoliatrendingFacetHit, + TrendingFacet, +} from '@graphcommerce/graphql-mesh' + +export function createFacetValueMapper() { + const isAlgoliaRecommendHit = (hit: AlgoliarecommendationsHit): hit is AlgoliatrendingFacetHit => + !!hit && '__typename' in hit && hit.__typename === 'AlgoliatrendingFacetHit' + + return (hit: AlgoliarecommendationsHit): TrendingFacet | null => + isAlgoliaRecommendHit(hit) ? hit : null +} diff --git a/packages/algolia-recommend/mesh/getRecommendations.ts b/packages/algolia-recommend/mesh/getRecommendations.ts new file mode 100644 index 0000000000..a27480629e --- /dev/null +++ b/packages/algolia-recommend/mesh/getRecommendations.ts @@ -0,0 +1,70 @@ +import { getIndexName } from '@graphcommerce/algolia-mesh/mesh/getIndexName' +import type { + AlgoliarecommendationsRequest_Input, + MeshContext, + AlgoliarecommendationsHit, +} from '@graphcommerce/graphql-mesh' +import { nonNullable } from '@graphcommerce/next-ui' +import type { GraphQLResolveInfo } from 'graphql' +import type { Simplify } from 'type-fest' + +const inputToModel = { + Trending_items_Input: 'trending_items' as const, + Trending_facet_values_Input: 'trending_facets' as const, + Frequently_bought_together_Input: 'bought_together' as const, + Looking_similar_Input: 'looking_similar' as const, + Related_products_Input: 'related_products' as const, +} + +export async function getRecommendations< + K extends keyof AlgoliarecommendationsRequest_Input, + Input extends AlgoliarecommendationsRequest_Input[K], + R, +>( + keyInput: K, + args: Simplify, 'indexName' | 'model'>>, + context: MeshContext, + info: GraphQLResolveInfo, + mapper: (hit: AlgoliarecommendationsHit) => R, +) { + return ( + (await context.algoliaRecommend.Query.algolia_getRecommendations({ + key: keyInput, + argsFromKeys: (keys) => ({ + input: { + requests: keys + .map( + (key) => + ({ + [key]: { + model: inputToModel[key as string], + indexName: getIndexName(context), + ...args, + }, + }) as unknown as AlgoliarecommendationsRequest_Input, + ) + .filter(nonNullable), + }, + }), + valuesFromResults: (res, keys) => + keys + .map((_key, index) => res?.results[index]) + .map((r) => r?.hits.map((hit) => hit && mapper(hit)).filter(nonNullable)) ?? null, + selectionSet: /* GraphQL */ ` + { + results { + nbHits + hits { + ... on AlgoliarecommendHit { + objectID + additionalProperties + } + } + } + } + `, + context, + info, + })) ?? null + ) +} diff --git a/packages/algolia-recommend/mesh/getRecommendationsInput.ts b/packages/algolia-recommend/mesh/getRecommendationsInput.ts deleted file mode 100644 index fbc7756fb8..0000000000 --- a/packages/algolia-recommend/mesh/getRecommendationsInput.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { getIndexName } from '@graphcommerce/algolia-mesh/mesh/getIndexName' -import { - MeshContext, - AlgoliagetRecommendationsParams_Input, - // AlgoliarelatedModel, -} from '@graphcommerce/graphql-mesh' - -function getRequest(model, objectId, indexName) { - const returnObject = { - related: { - Related_products_Input: { - indexName, - objectID: objectId, - threshold: 100, - model: 'related_products', - }, - }, - } - - return returnObject[model] ? returnObject[model] : {} -} -export function getRecommendationsInput( - context: MeshContext, - objectId: string, - model: string, -): AlgoliagetRecommendationsParams_Input { - const indexName = getIndexName(context) - return { - requests: [getRequest(model, objectId, indexName)], - } -} diff --git a/packages/algolia-recommend/mesh/resolvers.ts b/packages/algolia-recommend/mesh/resolvers.ts index ad78b5f85e..5b756ad9b0 100644 --- a/packages/algolia-recommend/mesh/resolvers.ts +++ b/packages/algolia-recommend/mesh/resolvers.ts @@ -1,154 +1,122 @@ /* eslint-disable @typescript-eslint/require-await */ -import { algoliaHitToMagentoProduct } from '@graphcommerce/algolia-mesh' -import { getGroupId } from '@graphcommerce/algolia-mesh/mesh/getGroupId' -import { getIndexName } from '@graphcommerce/algolia-mesh/mesh/getIndexName' -import { getStoreConfig } from '@graphcommerce/algolia-mesh/mesh/getStoreConfig' -import { nonNullable } from '@graphcommerce/algolia-mesh/mesh/utils' +import fragments from '@graphcommerce/graphql/generated/fragments.json' import type { - Algoliahit, - AlgoliarecommendationsRequest_Input, - AlgoliarecommendHit, - AlgoliatrendingFacetHit, MeshContext, + ProductInterfaceResolvers, + ResolverFn, Resolvers, + ResolversParentTypes, + ResolversTypes, } from '@graphcommerce/graphql-mesh' -import type { GraphQLResolveInfo } from 'graphql' +import { + GraphCommerceAlgoliaRecommendationLocation, + InputMaybe, + Maybe, +} from '@graphcommerce/next-config' +import { createProductMapper } from './createProductMapper' +import { createFacetValueMapper } from './createValueFacetMapper' +import { getRecommendations } from './getRecommendations' -function isAlgoliaRecommendHit( - hit: AlgoliarecommendHit | AlgoliatrendingFacetHit | Algoliahit | null, -): hit is Algoliahit { - return !!hit && '__typename' in hit && hit.__typename === 'AlgoliarecommendHit' -} - -function createAlgoliaRecommendationRequest( - keyInput: K, - additionalArgs?: AlgoliarecommendationsRequest_Input[K], -) { - return async ( - root: { uid: string }, - args: unknown, - context: MeshContext, - info: GraphQLResolveInfo, - ) => { - const storeConfig = await getStoreConfig(context) - const groupId = getGroupId(context) +type ProductTypes = NonNullable>> +const productTypes = fragments.possibleTypes.ProductInterface as ProductTypes[] - const results = - (await context.algoliaRecommend.Query.algolia_getRecommendations({ - key: keyInput, - argsFromKeys: (keys) => ({ - input: { - requests: keys - .map((key): AlgoliarecommendationsRequest_Input | null => { - const baseTypes: Omit< - NonNullable< - AlgoliarecommendationsRequest_Input[keyof AlgoliarecommendationsRequest_Input] - >, - 'model' - > = { - indexName: getIndexName(context), - threshold: 60, - maxRecommendations: 8, - ...additionalArgs, - // queryParameters: {}, - } +const resolvers: Resolvers = { + Query: { + trendingProducts: async (root, args, context, info) => + getRecommendations( + 'Trending_items_Input', + { threshold: 75, ...args.input }, + context, + info, + await createProductMapper(context), + ), + trendingFacetValues: (root, args, context, info) => + getRecommendations( + 'Trending_facet_values_Input', + { threshold: 75, ...args.input }, + context, + info, + createFacetValueMapper(), + ), + }, +} - if (key === 'Trending_items_Input') - return { - Trending_items_Input: { - ...baseTypes, - model: 'trending_items', - facetName: '', - facetValue: '', - }, - } +function isEnabled(location: InputMaybe | undefined) { + return location && location !== 'DISABLED' +} - if (key === 'Trending_facet_values_Input') - return { - Trending_facet_values_Input: { - ...baseTypes, - model: 'trending_facets', - facetName: '', - }, - } +function enumToLocation( + location: InputMaybe | undefined, +) { + if (!isEnabled(location)) throw Error('Check for isEnabled before calling this function') + if (location === 'CROSSSELL_PRODUCTS') return 'crosssell_products' as const + if (location === 'UPSELL_PRODUCTS') return 'upsell_products' as const + return 'related_products' as const +} - if (key === 'Frequently_bought_together_Input') - return { - Frequently_bought_together_Input: { - ...baseTypes, - model: 'bought_together', - objectID: atob(root.uid), - }, - } +type ProductResolver = ResolverFn< + Maybe>>, + ResolversParentTypes['ProductInterface'], + MeshContext, + Record +> - if (key === 'Looking_similar_Input') - return { - Looking_similar_Input: { - ...baseTypes, - model: 'looking_similar', - objectID: atob(root.uid), - }, - } +if (isEnabled(import.meta.graphCommerce.algolia.relatedProducts)) { + const resolver: ProductResolver = async (root, args, context, info) => { + if (!root.uid) return null + return getRecommendations( + 'Related_products_Input', + { objectID: atob(root.uid), threshold: 75 }, + context, + info, + await createProductMapper(context), + ) + } - if (key === 'Recommended_for_you_Input') - return { - Recommended_for_you_Input: { - ...baseTypes, - model: 'recommended_for_you', - }, - } + productTypes.forEach((productType) => { + if (!resolvers[productType]) resolvers[productType] = {} + resolvers[productType][enumToLocation(import.meta.graphCommerce.algolia.relatedProducts)] = + resolver + }) +} - if (key === 'Related_products_Input') - return { - Related_products_Input: { - ...baseTypes, - model: 'related_products', - objectID: atob(root.uid), - }, - } +if (isEnabled(import.meta.graphCommerce.algolia.lookingSimilar)) { + const resolver: ProductResolver = async (root, args, context, info) => { + if (!root.uid) return null + return getRecommendations( + 'Looking_similar_Input', + { objectID: atob(root.uid), threshold: 75 }, + context, + info, + await createProductMapper(context), + ) + } - return null - }) - .filter(nonNullable), - }, - }), - valuesFromResults: (res, keys) => - keys - .map((_key, index) => res?.results[index]) - .map((r) => - r?.hits - .map((hit) => - hit && isAlgoliaRecommendHit(hit) - ? algoliaHitToMagentoProduct(hit, storeConfig, groupId) - : null, - ) - .filter(nonNullable), - ) ?? null, - selectionSet: /* GraphQL */ ` - { - results { - nbHits - hits { - ... on AlgoliarecommendHit { - objectID - additionalProperties - } - } - } - } - `, - context, - info, - })) ?? null + productTypes.forEach((productType) => { + if (!resolvers[productType]) resolvers[productType] = {} + resolvers[productType][enumToLocation(import.meta.graphCommerce.algolia.lookingSimilar)] = + resolver + }) +} - return results +if (isEnabled(import.meta.graphCommerce.algolia.frequentlyBoughtTogether)) { + const resolver: ProductResolver = async (root, args, context, info) => { + if (!root.uid) return null + return getRecommendations( + 'Frequently_bought_together_Input', + { objectID: atob(root.uid), threshold: 75 }, + context, + info, + await createProductMapper(context), + ) } -} -export const resolvers: Resolvers = { - ConfigurableProduct: { - // upsell_products: () => [], - related_products: createAlgoliaRecommendationRequest('Related_products_Input'), - // crosssell_products: createAlgoliaRecommendationRequest('Frequently_bought_together_Input'), - }, + productTypes.forEach((productType) => { + if (!resolvers[productType]) resolvers[productType] = {} + resolvers[productType][ + enumToLocation(import.meta.graphCommerce.algolia.frequentlyBoughtTogether) + ] = resolver + }) } + +export default resolvers diff --git a/packages/algolia-recommend/schema/AlgoliaRecommendations.graphqls b/packages/algolia-recommend/schema/AlgoliaRecommendations.graphqls new file mode 100644 index 0000000000..6b6292eafc --- /dev/null +++ b/packages/algolia-recommend/schema/AlgoliaRecommendations.graphqls @@ -0,0 +1,44 @@ +input TrendingProductsInput { + maxRecommendations: Int = 10 + + """ + Minimum score a recommendation must have to be included in the response. + """ + threshold: NonNegativeFloat! = 75 + + """ + Facet attribute. To be used in combination with `facetValue`. + If specified, only recommendations matching the facet filter will be returned. + """ + facetName: String + + """ + Facet value. To be used in combination with `facetName`. + If specified, only recommendations matching the facet filter will be returned. + """ + facetValue: String +} + +input TrendingFacetValuesInput { + maxRecommendations: Int = 10 + + """ + Minimum score a recommendation must have to be included in the response. + """ + threshold: NonNegativeFloat! = 75 + """ + Facet attribute for which to retrieve trending facet values. + """ + facetName: String! +} + +type TrendingFacet { + facetName: String! + facetValue: String! +} + +extend type Query { + trendingProducts(input: TrendingProductsInput!): [ProductInterface] + + trendingFacetValues(input: TrendingFacetValuesInput!): [TrendingFacet] +} diff --git a/packages/algolia-recommend/schema/RecommendSchema.graphqls b/packages/algolia-recommend/schema/RecommendSchema.graphqls deleted file mode 100644 index 3b3318a273..0000000000 --- a/packages/algolia-recommend/schema/RecommendSchema.graphqls +++ /dev/null @@ -1,7 +0,0 @@ -type Recommendations { - items: [ProductInterface] -} - -extend type Query { - recommendations(recommendType: String): Recommendations -} diff --git a/packages/algolia-recommend/scripts/generate-recommend-spec.mts b/packages/algolia-recommend/scripts/generate-recommend-spec.mts index 1e81eea695..b4699e9b75 100644 --- a/packages/algolia-recommend/scripts/generate-recommend-spec.mts +++ b/packages/algolia-recommend/scripts/generate-recommend-spec.mts @@ -56,13 +56,34 @@ function filterPaths( ) } +function isRef(value: any): value is OpenAPIV3.ReferenceObject { + return typeof value === 'object' && '$ref' in value +} + const newSchema: OpenAPIV3.Document = { openapi, info, paths: filterPaths(openApiSchema.paths, { '/1/indexes/*/recommendations': [OpenAPIV3.HttpMethods.POST], }), - components, + components: { + ...openApiSchema.components, + schemas: Object.fromEntries( + Object.entries(openApiSchema.components?.schemas ?? {}).map(([schemaKey, schema]) => { + if (isRef(schema) || schemaKey !== 'recommendedForYouQuery') return [schemaKey, schema] + + return [ + schemaKey, + { + ...schema, + oneOf: schema.oneOf?.filter( + (item) => !isRef(item) || item.$ref !== '#/components/schemas/recommendedForYouQuery', + ), + }, + ] + }), + ), + }, } await writeFile( From d74f16f9d5f31a99bc1810c32f119835605e4f6d Mon Sep 17 00:00:00 2001 From: Paul Hachmang Date: Tue, 10 Sep 2024 17:24:13 +0200 Subject: [PATCH 29/70] Rename script --- packages/algolia-personalization/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/algolia-personalization/package.json b/packages/algolia-personalization/package.json index faa5428d6d..5caf0e942d 100644 --- a/packages/algolia-personalization/package.json +++ b/packages/algolia-personalization/package.json @@ -12,7 +12,7 @@ } }, "scripts": { - "generate-insights": "tsx scripts/generate-insights-spec.mts" + "generate": "tsx scripts/generate-insights-spec.mts" }, "peerDependencies": { "@graphcommerce/algolia-mesh": "^9.0.0-canary.76", From 9b18c5f09105df439544a8291895fbf8c82a9452 Mon Sep 17 00:00:00 2001 From: Paul Hachmang Date: Wed, 11 Sep 2024 11:42:44 +0200 Subject: [PATCH 30/70] Created algolia_looking_similar, algolia_frequently_bought_together and algolia_related_products fields on products --- packages/algolia-mesh/Config.graphqls | 3 + packages/algolia-recommend/mesh/resolvers.ts | 89 +++++++++++++---- .../schema/AlgoliaRecommendations.graphqls | 95 +++++++++++++++++-- 3 files changed, 161 insertions(+), 26 deletions(-) diff --git a/packages/algolia-mesh/Config.graphqls b/packages/algolia-mesh/Config.graphqls index ea0763946d..41c90c6cff 100644 --- a/packages/algolia-mesh/Config.graphqls +++ b/packages/algolia-mesh/Config.graphqls @@ -1,3 +1,6 @@ +""" +Algolia configuration for GraphCommerce. +""" input GraphCommerceAlgoliaConfig { """ Configure your Algolia application ID. diff --git a/packages/algolia-recommend/mesh/resolvers.ts b/packages/algolia-recommend/mesh/resolvers.ts index 5b756ad9b0..7d4daf5f53 100644 --- a/packages/algolia-recommend/mesh/resolvers.ts +++ b/packages/algolia-recommend/mesh/resolvers.ts @@ -1,12 +1,16 @@ /* eslint-disable @typescript-eslint/require-await */ import fragments from '@graphcommerce/graphql/generated/fragments.json' import type { + AlgoliaLookingSimilarInput, MeshContext, ProductInterfaceResolvers, + RequireFields, ResolverFn, Resolvers, ResolversParentTypes, ResolversTypes, + SimpleProductalgolia_looking_similarArgs, + VirtualProductalgolia_looking_similarArgs, } from '@graphcommerce/graphql-mesh' import { GraphCommerceAlgoliaRecommendationLocation, @@ -18,7 +22,7 @@ import { createFacetValueMapper } from './createValueFacetMapper' import { getRecommendations } from './getRecommendations' type ProductTypes = NonNullable>> -const productTypes = fragments.possibleTypes.ProductInterface as ProductTypes[] +const productInterfaceTypes = fragments.possibleTypes.ProductInterface as ProductTypes[] const resolvers: Resolvers = { Query: { @@ -62,56 +66,54 @@ type ProductResolver = ResolverFn< > if (isEnabled(import.meta.graphCommerce.algolia.relatedProducts)) { - const resolver: ProductResolver = async (root, args, context, info) => { - if (!root.uid) return null - return getRecommendations( + const resolve: ProductResolver = async (root, args, context, info) => + getRecommendations( 'Related_products_Input', { objectID: atob(root.uid), threshold: 75 }, context, info, await createProductMapper(context), ) - } - productTypes.forEach((productType) => { + productInterfaceTypes.forEach((productType) => { if (!resolvers[productType]) resolvers[productType] = {} - resolvers[productType][enumToLocation(import.meta.graphCommerce.algolia.relatedProducts)] = - resolver + resolvers[productType][enumToLocation(import.meta.graphCommerce.algolia.relatedProducts)] = { + selectionSet: `{ uid }`, + resolve, + } }) } if (isEnabled(import.meta.graphCommerce.algolia.lookingSimilar)) { - const resolver: ProductResolver = async (root, args, context, info) => { - if (!root.uid) return null - return getRecommendations( + const resolve: ProductResolver = async (root, args, context, info) => + getRecommendations( 'Looking_similar_Input', { objectID: atob(root.uid), threshold: 75 }, context, info, await createProductMapper(context), ) - } - productTypes.forEach((productType) => { + productInterfaceTypes.forEach((productType) => { if (!resolvers[productType]) resolvers[productType] = {} - resolvers[productType][enumToLocation(import.meta.graphCommerce.algolia.lookingSimilar)] = - resolver + resolvers[productType][enumToLocation(import.meta.graphCommerce.algolia.lookingSimilar)] = { + selectionSet: `{ uid }`, + resolve, + } }) } if (isEnabled(import.meta.graphCommerce.algolia.frequentlyBoughtTogether)) { - const resolver: ProductResolver = async (root, args, context, info) => { - if (!root.uid) return null - return getRecommendations( + const resolver: ProductResolver = async (root, args, context, info) => + getRecommendations( 'Frequently_bought_together_Input', { objectID: atob(root.uid), threshold: 75 }, context, info, await createProductMapper(context), ) - } - productTypes.forEach((productType) => { + productInterfaceTypes.forEach((productType) => { if (!resolvers[productType]) resolvers[productType] = {} resolvers[productType][ enumToLocation(import.meta.graphCommerce.algolia.frequentlyBoughtTogether) @@ -119,4 +121,51 @@ if (isEnabled(import.meta.graphCommerce.algolia.frequentlyBoughtTogether)) { }) } +type AlgoliaRecommendationResolver = ResolverFn< + Maybe>>, + ResolversParentTypes['ProductInterface'], + MeshContext, + { input?: AlgoliaLookingSimilarInput | null } +> + +const similar: AlgoliaRecommendationResolver = async (root, args, context, info) => + getRecommendations( + 'Looking_similar_Input', + { objectID: atob(root.uid), threshold: 75, maxRecommendations: 8, ...args.input }, + context, + info, + await createProductMapper(context), + ) + +const related: AlgoliaRecommendationResolver = async (root, args, context, info) => + getRecommendations( + 'Related_products_Input', + { objectID: atob(root.uid), threshold: 75, maxRecommendations: 8, ...args.input }, + context, + info, + await createProductMapper(context), + ) + +const together: AlgoliaRecommendationResolver = async (root, args, context, info) => + getRecommendations( + 'Frequently_bought_together_Input', + { objectID: atob(root.uid), threshold: 75, maxRecommendations: 8, ...args.input }, + context, + info, + await createProductMapper(context), + ) + +productInterfaceTypes.forEach((productType) => { + if (!resolvers[productType]) resolvers[productType] = {} + + resolvers[productType].algolia_looking_similar = { selectionSet: `{ uid }`, resolve: similar } + + resolvers[productType].algolia_related_products = { selectionSet: `{ uid }`, resolve: related } + + resolvers[productType].algolia_frequently_bought_together = { + selectionSet: `{ uid }`, + resolve: together, + } +}) + export default resolvers diff --git a/packages/algolia-recommend/schema/AlgoliaRecommendations.graphqls b/packages/algolia-recommend/schema/AlgoliaRecommendations.graphqls index 6b6292eafc..7559c1cae1 100644 --- a/packages/algolia-recommend/schema/AlgoliaRecommendations.graphqls +++ b/packages/algolia-recommend/schema/AlgoliaRecommendations.graphqls @@ -1,10 +1,10 @@ input TrendingProductsInput { - maxRecommendations: Int = 10 + maxRecommendations: Int = 8 """ Minimum score a recommendation must have to be included in the response. """ - threshold: NonNegativeFloat! = 75 + threshold: Float! = 75 """ Facet attribute. To be used in combination with `facetValue`. @@ -20,19 +20,19 @@ input TrendingProductsInput { } input TrendingFacetValuesInput { - maxRecommendations: Int = 10 + maxRecommendations: Int = 8 """ Minimum score a recommendation must have to be included in the response. """ - threshold: NonNegativeFloat! = 75 + threshold: Float! = 75 """ Facet attribute for which to retrieve trending facet values. """ facetName: String! } -type TrendingFacet { +type TrendingFacetValue { facetName: String! facetValue: String! } @@ -40,5 +40,88 @@ type TrendingFacet { extend type Query { trendingProducts(input: TrendingProductsInput!): [ProductInterface] - trendingFacetValues(input: TrendingFacetValuesInput!): [TrendingFacet] + trendingFacetValues(input: TrendingFacetValuesInput!): [TrendingFacetValue] +} + +input AlgoliaLookingSimilarInput { + maxRecommendations: Int = 8 + + """ + Minimum score a recommendation must have to be included in the response. + """ + threshold: Float! = 75 +} + +input AlgoliaFrequentlyBoughtTogetherInput { + maxRecommendations: Int = 8 + + """ + Minimum score a recommendation must have to be included in the response. + """ + threshold: Float! = 75 +} + +input AlgoliaRelatedProductsInput { + maxRecommendations: Int = 8 + + """ + Minimum score a recommendation must have to be included in the response. + """ + threshold: Float! = 75 +} + +interface ProductInterface { + algolia_looking_similar(input: AlgoliaLookingSimilarInput): [ProductInterface] + algolia_frequently_bought_together( + input: AlgoliaFrequentlyBoughtTogetherInput + ): [ProductInterface] + algolia_related_products(input: AlgoliaRelatedProductsInput): [ProductInterface] +} + +type SimpleProduct implements ProductInterface { + algolia_looking_similar(input: AlgoliaLookingSimilarInput): [ProductInterface] + algolia_frequently_bought_together( + input: AlgoliaFrequentlyBoughtTogetherInput + ): [ProductInterface] + algolia_related_products(input: AlgoliaRelatedProductsInput): [ProductInterface] +} + +type VirtualProduct implements ProductInterface { + algolia_looking_similar(input: AlgoliaLookingSimilarInput): [ProductInterface] + algolia_frequently_bought_together( + input: AlgoliaFrequentlyBoughtTogetherInput + ): [ProductInterface] + algolia_related_products(input: AlgoliaRelatedProductsInput): [ProductInterface] +} + +type ConfigurableProduct implements ProductInterface { + algolia_looking_similar(input: AlgoliaLookingSimilarInput): [ProductInterface] + algolia_frequently_bought_together( + input: AlgoliaFrequentlyBoughtTogetherInput + ): [ProductInterface] + algolia_related_products(input: AlgoliaRelatedProductsInput): [ProductInterface] +} + +type BundleProduct implements ProductInterface { + algolia_looking_similar(input: AlgoliaLookingSimilarInput): [ProductInterface] + algolia_frequently_bought_together( + input: AlgoliaFrequentlyBoughtTogetherInput + ): [ProductInterface] + algolia_related_products(input: AlgoliaRelatedProductsInput): [ProductInterface] +} + +type DownloadableProduct implements ProductInterface { + algolia_looking_similar(input: AlgoliaLookingSimilarInput): [ProductInterface] + algolia_frequently_bought_together( + input: AlgoliaFrequentlyBoughtTogetherInput + ): [ProductInterface] + algolia_related_products(input: AlgoliaRelatedProductsInput): [ProductInterface] +} + +type GroupedProduct implements ProductInterface { + algolia_looking_similar(input: AlgoliaLookingSimilarInput): [ProductInterface] + algolia_frequently_bought_together( + input: AlgoliaFrequentlyBoughtTogetherInput + ): [ProductInterface] + algolia_related_products(input: AlgoliaRelatedProductsInput): [ProductInterface] } From e2dc6aef564e927fb61679db5fedcad2474a4660 Mon Sep 17 00:00:00 2001 From: Paul Hachmang Date: Wed, 11 Sep 2024 13:34:02 +0200 Subject: [PATCH 31/70] Move spec to yaml --- .../algolia-recommend-spec.yaml | 4631 ++++++++++------- 1 file changed, 2813 insertions(+), 1818 deletions(-) diff --git a/packages/algolia-recommend/algolia-recommend-spec.yaml b/packages/algolia-recommend/algolia-recommend-spec.yaml index 902daaa06c..fbbb03eab5 100644 --- a/packages/algolia-recommend/algolia-recommend-spec.yaml +++ b/packages/algolia-recommend/algolia-recommend-spec.yaml @@ -1,1818 +1,2813 @@ -{ - "openapi": "3.0.2", - "info": { - "title": "Recommend API", - "description": "The Recommend API lets you retrieve recommendations from one of Algolia's AI recommendation models that you previously trained on your data.\n\n## Client libraries\n\nUse Algolia's API clients and libraries to reliably integrate Algolia's APIs with your apps.\nThe official API clients are covered by Algolia's [Service Level Agreement](https://www.algolia.com/policies/sla/).\n\nSee: [Algolia's ecosystem](https://www.algolia.com/doc/guides/getting-started/how-algolia-works/in-depth/ecosystem/)\n\n## Base URLs\n\nThe base URLs for requests to the Recommend API are:\n\n- `https://{APPLICATION_ID}.algolia.net`\n- `https://{APPLICATION_ID}-dsn.algolia.net`.\n If your subscription includes a [Distributed Search Network](https://dashboard.algolia.com/infra),\n this ensures that requests are sent to servers closest to users.\n\nBoth URLs provide high availability by distributing requests with load balancing.\n\n**All requests must use HTTPS.**\n\n## Retry strategy\n\nTo guarantee a high availability, implement a retry strategy for all API requests using the URLs of your servers as fallbacks:\n\n- `https://{APPLICATION_ID}-1.algolianet.com`\n- `https://{APPLICATION_ID}-2.algolianet.com`\n- `https://{APPLICATION_ID}-3.algolianet.com`\n\nThese URLs use a different DNS provider than the primary URLs.\nYou should randomize this list to ensure an even load across the three servers.\n\nAll Algolia API clients implement this retry strategy.\n\n## Authentication\n\nTo authenticate your API requests, add these headers:\n\n- `x-algolia-application-id`. Your Algolia application ID.\n- `x-algolia-api-key`. An API key with the necessary permissions to make the request.\n The required access control list (ACL) to make a request is listed in each endpoint's reference.\n\nYou can find your application ID and API key in the [Algolia dashboard](https://dashboard.algolia.com/account).\n\n## Request format\n\nRequest bodies must be JSON objects.\n\n## Response status and errors\n\nThe Recommend API returns JSON responses.\nSince JSON doesn't guarantee any specific ordering, don't rely on the order of attributes in the API response.\n\nSuccessful responses return a `2xx` status. Client errors return a `4xx` status. Server errors are indicated by a `5xx` status.\nError responses have a `message` property with more information.\n\n## Version\n\nThe current version of the Recommend API is version 1, as indicated by the `/1/` in each endpoint's URL.\n", - "version": "1.0.0" - }, - "paths": { - "/1/indexes/*/recommendations": { - "post": { - "tags": ["recommend"], - "operationId": "getRecommendations", - "x-use-read-transporter": true, - "x-cacheable": true, - "x-acl": ["search"], - "x-legacy-signature-recommend": true, - "summary": "Retrieve recommendations", - "description": "Retrieves recommendations from selected AI models.\n", - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "title": "getRecommendationsParams", - "description": "Recommend request body.", - "type": "object", - "additionalProperties": false, - "properties": { - "requests": { - "type": "array", - "description": "Recommendation request with parameters depending on the requested model.", - "items": { "$ref": "#/components/schemas/recommendationsRequest" } - } - }, - "required": ["requests"] - } - } - } - }, - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "title": "getRecommendationsResponse", - "type": "object", - "additionalProperties": false, - "properties": { - "results": { - "type": "array", - "items": { "$ref": "#/components/schemas/recommendationsResults" } - } - }, - "required": ["results"] - } - } - } - }, - "400": { "$ref": "#/components/responses/BadRequest" }, - "402": { "$ref": "#/components/responses/FeatureNotEnabled" }, - "403": { "$ref": "#/components/responses/MethodNotAllowed" }, - "404": { "$ref": "#/components/responses/IndexNotFound" } - } - } - } - }, - "components": { - "securitySchemes": { - "appId": { - "type": "apiKey", - "in": "header", - "name": "x-algolia-application-id", - "description": "Your Algolia application ID." - }, - "apiKey": { - "type": "apiKey", - "in": "header", - "name": "x-algolia-api-key", - "description": "Your Algolia API key with the necessary permissions to make the request.\nPermissions are controlled through access control lists (ACL) and access restrictions.\nThe required ACL to make a request is listed in each endpoint's reference.\n" - } - }, - "parameters": { - "PathInPath": { - "name": "path", - "in": "path", - "description": "Path of the endpoint, anything after \"/1\" must be specified.", - "required": true, - "schema": { "type": "string", "example": "/keys" } - }, - "Parameters": { - "name": "parameters", - "in": "query", - "description": "Query parameters to apply to the current query.", - "schema": { "type": "object", "additionalProperties": true } - }, - "IndexName": { - "name": "indexName", - "in": "path", - "description": "Name of the index on which to perform the operation.", - "required": true, - "schema": { "type": "string", "example": "YourIndexName" } - }, - "Models": { - "in": "path", - "name": "model", - "required": true, - "description": "[Recommend model](https://www.algolia.com/doc/guides/algolia-recommend/overview/#recommend-models).\n", - "schema": { "$ref": "#/components/schemas/recommendModels" } - }, - "ObjectID": { - "name": "objectID", - "in": "path", - "description": "Unique record identifier.", - "required": true, - "schema": { "$ref": "#/components/schemas/objectID" } - } - }, - "schemas": { - "ErrorBase": { - "description": "Error.", - "type": "object", - "x-keep-model": true, - "additionalProperties": true, - "properties": { - "message": { "type": "string", "example": "Invalid Application-Id or API-Key" } - } - }, - "indexName": { - "type": "string", - "example": "products", - "description": "Index name (case-sensitive)." - }, - "query": { - "type": "string", - "description": "Search query.", - "default": "", - "x-categories": ["Search"] - }, - "searchParamsQuery": { - "type": "object", - "additionalProperties": false, - "properties": { "query": { "$ref": "#/components/schemas/query" } } - }, - "filters": { - "type": "string", - "description": "Filter expression to only include items that match the filter criteria in the response.\n\nYou can use these filter expressions:\n\n- **Numeric filters.** ` `, where `` is one of `<`, `<=`, `=`, `!=`, `>`, `>=`.\n- **Ranges.** `: TO ` where `` and `` are the lower and upper limits of the range (inclusive).\n- **Facet filters.** `:` where `` is a facet attribute (case-sensitive) and `` a facet value.\n- **Tag filters.** `_tags:` or just `` (case-sensitive).\n- **Boolean filters.** `: true | false`.\n\nYou can combine filters with `AND`, `OR`, and `NOT` operators with the following restrictions:\n\n- You can only combine filters of the same type with `OR`.\n **Not supported:** `facet:value OR num > 3`.\n- You can't use `NOT` with combinations of filters.\n **Not supported:** `NOT(facet:value OR facet:value)`\n- You can't combine conjunctions (`AND`) with `OR`.\n **Not supported:** `facet:value OR (facet:value AND facet:value)`\n\nUse quotes around your filters, if the facet attribute name or facet value has spaces, keywords (`OR`, `AND`, `NOT`), or quotes.\nIf a facet attribute is an array, the filter matches if it matches at least one element of the array.\n\nFor more information, see [Filters](https://www.algolia.com/doc/guides/managing-results/refine-results/filtering/).\n", - "example": "(category:Book OR category:Ebook) AND _tags:published", - "x-categories": ["Filtering"] - }, - "facetFilters": { - "description": "Filter the search by facet values, so that only records with the same facet values are retrieved.\n\n**Prefer using the `filters` parameter, which supports all filter types and combinations with boolean operators.**\n\n- `[filter1, filter2]` is interpreted as `filter1 AND filter2`.\n- `[[filter1, filter2], filter3]` is interpreted as `filter1 OR filter2 AND filter3`.\n- `facet:-value` is interpreted as `NOT facet:value`.\n\nWhile it's best to avoid attributes that start with a `-`, you can still filter them by escaping with a backslash:\n`facet:\\-value`.\n", - "example": [["category:Book", "category:-Movie"], "author:John Doe"], - "oneOf": [ - { "type": "array", "items": { "$ref": "#/components/schemas/facetFilters" } }, - { "type": "string" } - ], - "x-categories": ["Filtering"] - }, - "optionalFilters": { - "description": "Filters to promote or demote records in the search results.\n\nOptional filters work like facet filters, but they don't exclude records from the search results.\nRecords that match the optional filter rank before records that don't match.\nIf you're using a negative filter `facet:-value`, matching records rank after records that don't match.\n\n- Optional filters don't work on virtual replicas.\n- Optional filters are applied _after_ sort-by attributes.\n- Optional filters don't work with numeric attributes.\n", - "example": ["category:Book", "author:John Doe"], - "oneOf": [ - { "type": "array", "items": { "$ref": "#/components/schemas/optionalFilters" } }, - { "type": "string" } - ], - "x-categories": ["Filtering"] - }, - "numericFilters": { - "description": "Filter by numeric facets.\n\n**Prefer using the `filters` parameter, which supports all filter types and combinations with boolean operators.**\n\nYou can use numeric comparison operators: `<`, `<=`, `=`, `!=`, `>`, `>=`. Comparsions are precise up to 3 decimals.\nYou can also provide ranges: `facet: TO `. The range includes the lower and upper boundaries.\nThe same combination rules apply as for `facetFilters`.\n", - "example": [["inStock = 1", "deliveryDate < 1441755506"], "price < 1000"], - "oneOf": [ - { "type": "array", "items": { "$ref": "#/components/schemas/numericFilters" } }, - { "type": "string" } - ], - "x-categories": ["Filtering"] - }, - "tagFilters": { - "description": "Filter the search by values of the special `_tags` attribute.\n\n**Prefer using the `filters` parameter, which supports all filter types and combinations with boolean operators.**\n\nDifferent from regular facets, `_tags` can only be used for filtering (including or excluding records).\nYou won't get a facet count.\nThe same combination and escaping rules apply as for `facetFilters`.\n", - "example": [["Book", "Movie"], "SciFi"], - "oneOf": [ - { "type": "array", "items": { "$ref": "#/components/schemas/tagFilters" } }, - { "type": "string" } - ], - "x-categories": ["Filtering"] - }, - "page": { - "type": "integer", - "description": "Page of search results to retrieve.", - "default": 0, - "minimum": 0, - "x-categories": ["Pagination"] - }, - "aroundLatLng": { - "type": "string", - "description": "Coordinates for the center of a circle, expressed as a comma-separated string of latitude and longitude.\n\nOnly records included within circle around this central location are included in the results.\nThe radius of the circle is determined by the `aroundRadius` and `minimumAroundRadius` settings.\nThis parameter is ignored if you also specify `insidePolygon` or `insideBoundingBox`.\n", - "example": "40.71,-74.01", - "default": "", - "x-categories": ["Geo-Search"] - }, - "aroundLatLngViaIP": { - "type": "boolean", - "description": "Whether to obtain the coordinates from the request's IP address.", - "default": false, - "x-categories": ["Geo-Search"] - }, - "aroundRadiusAll": { - "title": "all", - "type": "string", - "description": "Return all records with a valid `_geoloc` attribute. Don't filter by distance.", - "enum": ["all"] - }, - "aroundRadius": { - "description": "Maximum radius for a search around a central location.\n\nThis parameter works in combination with the `aroundLatLng` and `aroundLatLngViaIP` parameters.\nBy default, the search radius is determined automatically from the density of hits around the central location.\nThe search radius is small if there are many hits close to the central coordinates.\n", - "oneOf": [ - { - "type": "integer", - "minimum": 1, - "description": "Maximum search radius around a central location in meters." - }, - { "$ref": "#/components/schemas/aroundRadiusAll" } - ], - "x-categories": ["Geo-Search"] - }, - "aroundPrecisionFromValue": { - "title": "range objects", - "type": "array", - "items": { - "title": "range", - "type": "object", - "description": "Range object with lower and upper values in meters to define custom ranges.", - "properties": { - "from": { - "type": "integer", - "description": "Lower boundary of a range in meters. The Geo ranking criterion considers all records within the range to be equal.", - "example": 20 - }, - "value": { - "type": "integer", - "description": "Upper boundary of a range in meters. The Geo ranking criterion considers all records within the range to be equal." - } - } - } - }, - "aroundPrecision": { - "description": "Precision of a coordinate-based search in meters to group results with similar distances.\n\nThe Geo ranking criterion considers all matches within the same range of distances to be equal.\n", - "oneOf": [ - { - "type": "integer", - "default": 10, - "description": "Distance in meters to group results by similar distances.\n\nFor example, if you set `aroundPrecision` to 100, records wihin 100 meters to the central coordinate are considered to have the same distance,\nas are records between 100 and 199 meters.\n" - }, - { "$ref": "#/components/schemas/aroundPrecisionFromValue" } - ], - "x-categories": ["Geo-Search"] - }, - "insideBoundingBox": { - "type": "array", - "items": { - "type": "array", - "minItems": 4, - "maxItems": 4, - "items": { "type": "number", "format": "double" } - }, - "description": "Coordinates for a rectangular area in which to search.\n\nEach bounding box is defined by the two opposite points of its diagonal, and expressed as latitude and longitude pair:\n`[p1 lat, p1 long, p2 lat, p2 long]`.\nProvide multiple bounding boxes as nested arrays.\nFor more information, see [rectangular area](https://www.algolia.com/doc/guides/managing-results/refine-results/geolocation/#filtering-inside-rectangular-or-polygonal-areas).\n", - "example": [ - [47.3165, 4.9665, 47.3424, 5.0201], - [40.9234, 2.1185, 38.643, 1.9916] - ], - "x-categories": ["Geo-Search"] - }, - "insidePolygon": { - "type": "array", - "items": { - "type": "array", - "minItems": 6, - "maxItems": 20000, - "items": { "type": "number", "format": "double" } - }, - "description": "Coordinates of a polygon in which to search.\n\nPolygons are defined by 3 to 10,000 points. Each point is represented by its latitude and longitude.\nProvide multiple polygons as nested arrays.\nFor more information, see [filtering inside polygons](https://www.algolia.com/doc/guides/managing-results/refine-results/geolocation/#filtering-inside-rectangular-or-polygonal-areas).\nThis parameter is ignored if you also specify `insideBoundingBox`.\n", - "example": [ - [47.3165, 4.9665, 47.3424, 5.0201, 47.32, 4.9], - [40.9234, 2.1185, 38.643, 1.9916, 39.2587, 2.0104] - ], - "x-categories": ["Geo-Search"] - }, - "supportedLanguage": { - "type": "string", - "description": "ISO code for a supported language.", - "enum": [ - "af", - "ar", - "az", - "bg", - "bn", - "ca", - "cs", - "cy", - "da", - "de", - "el", - "en", - "eo", - "es", - "et", - "eu", - "fa", - "fi", - "fo", - "fr", - "ga", - "gl", - "he", - "hi", - "hu", - "hy", - "id", - "is", - "it", - "ja", - "ka", - "kk", - "ko", - "ku", - "ky", - "lt", - "lv", - "mi", - "mn", - "mr", - "ms", - "mt", - "nb", - "nl", - "no", - "ns", - "pl", - "ps", - "pt", - "pt-br", - "qu", - "ro", - "ru", - "sk", - "sq", - "sv", - "sw", - "ta", - "te", - "th", - "tl", - "tn", - "tr", - "tt", - "uk", - "ur", - "uz", - "zh" - ] - }, - "userToken": { - "type": "string", - "description": "Unique pseudonymous or anonymous user identifier.\n\nThis helps with analytics and click and conversion events.\nFor more information, see [user token](https://www.algolia.com/doc/guides/sending-events/concepts/usertoken/).\n", - "example": "test-user-123", - "x-categories": ["Personalization"] - }, - "baseSearchParamsWithoutQuery": { - "type": "object", - "additionalProperties": false, - "properties": { - "similarQuery": { - "type": "string", - "description": "Keywords to be used instead of the search query to conduct a more broader search.\n\nUsing the `similarQuery` parameter changes other settings:\n\n- `queryType` is set to `prefixNone`.\n- `removeStopWords` is set to true.\n- `words` is set as the first ranking criterion.\n- All remaining words are treated as `optionalWords`.\n\nSince the `similarQuery` is supposed to do a broad search, they usually return many results.\nCombine it with `filters` to narrow down the list of results.\n", - "default": "", - "example": "comedy drama crime Macy Buscemi", - "x-categories": ["Search"] - }, - "filters": { "$ref": "#/components/schemas/filters" }, - "facetFilters": { "$ref": "#/components/schemas/facetFilters" }, - "optionalFilters": { "$ref": "#/components/schemas/optionalFilters" }, - "numericFilters": { "$ref": "#/components/schemas/numericFilters" }, - "tagFilters": { "$ref": "#/components/schemas/tagFilters" }, - "sumOrFiltersScores": { - "type": "boolean", - "description": "Whether to sum all filter scores.\n\nIf true, all filter scores are summed.\nOtherwise, the maximum filter score is kept.\nFor more information, see [filter scores](https://www.algolia.com/doc/guides/managing-results/refine-results/filtering/in-depth/filter-scoring/#accumulating-scores-with-sumorfiltersscores).\n", - "default": false, - "x-categories": ["Filtering"] - }, - "restrictSearchableAttributes": { - "type": "array", - "items": { "type": "string" }, - "example": ["title", "author"], - "description": "Restricts a search to a subset of your searchable attributes.\nAttribute names are case-sensitive.\n", - "default": [], - "x-categories": ["Filtering"] - }, - "facets": { - "type": "array", - "items": { "type": "string" }, - "description": "Facets for which to retrieve facet values that match the search criteria and the number of matching facet values.\n\nTo retrieve all facets, use the wildcard character `*`.\nFor more information, see [facets](https://www.algolia.com/doc/guides/managing-results/refine-results/faceting/#contextual-facet-values-and-counts).\n", - "default": [], - "example": ["*"], - "x-categories": ["Faceting"] - }, - "facetingAfterDistinct": { - "type": "boolean", - "description": "Whether faceting should be applied after deduplication with `distinct`.\n\nThis leads to accurate facet counts when using faceting in combination with `distinct`.\nIt's usually better to use `afterDistinct` modifiers in the `attributesForFaceting` setting,\nas `facetingAfterDistinct` only computes correct facet counts if all records have the same facet values for the `attributeForDistinct`.\n", - "default": false, - "x-categories": ["Faceting"] - }, - "page": { "$ref": "#/components/schemas/page" }, - "offset": { - "type": "integer", - "description": "Position of the first hit to retrieve.", - "x-categories": ["Pagination"] - }, - "length": { - "type": "integer", - "description": "Number of hits to retrieve (used in combination with `offset`).", - "minimum": 0, - "maximum": 1000, - "x-categories": ["Pagination"] - }, - "aroundLatLng": { "$ref": "#/components/schemas/aroundLatLng" }, - "aroundLatLngViaIP": { "$ref": "#/components/schemas/aroundLatLngViaIP" }, - "aroundRadius": { "$ref": "#/components/schemas/aroundRadius" }, - "aroundPrecision": { "$ref": "#/components/schemas/aroundPrecision" }, - "minimumAroundRadius": { - "type": "integer", - "description": "Minimum radius (in meters) for a search around a location when `aroundRadius` isn't set.", - "minimum": 1, - "x-categories": ["Geo-Search"] - }, - "insideBoundingBox": { "$ref": "#/components/schemas/insideBoundingBox" }, - "insidePolygon": { "$ref": "#/components/schemas/insidePolygon" }, - "naturalLanguages": { - "type": "array", - "items": { "$ref": "#/components/schemas/supportedLanguage" }, - "description": "ISO language codes that adjust settings that are useful for processing natural language queries (as opposed to keyword searches):\n\n- Sets `removeStopWords` and `ignorePlurals` to the list of provided languages.\n- Sets `removeWordsIfNoResults` to `allOptional`.\n- Adds a `natural_language` attribute to `ruleContexts` and `analyticsTags`.\n", - "default": [], - "x-categories": ["Languages"] - }, - "ruleContexts": { - "type": "array", - "items": { "type": "string" }, - "description": "Assigns a rule context to the search query.\n\n[Rule contexts](https://www.algolia.com/doc/guides/managing-results/rules/rules-overview/how-to/customize-search-results-by-platform/#whats-a-context) are strings that you can use to trigger matching rules.\n", - "default": [], - "example": ["mobile"], - "x-categories": ["Rules"] - }, - "personalizationImpact": { - "type": "integer", - "description": "Impact that Personalization should have on this search.\n\nThe higher this value is, the more Personalization determines the ranking compared to other factors.\nFor more information, see [Understanding Personalization impact](https://www.algolia.com/doc/guides/personalization/personalizing-results/in-depth/configuring-personalization/#understanding-personalization-impact).\n", - "default": 100, - "minimum": 0, - "maximum": 100, - "x-categories": ["Personalization"] - }, - "userToken": { "$ref": "#/components/schemas/userToken" }, - "getRankingInfo": { - "type": "boolean", - "description": "Whether the search response should include detailed ranking information.", - "default": false, - "x-categories": ["Advanced"] - }, - "synonyms": { - "type": "boolean", - "description": "Whether to take into account an index's synonyms for this search.", - "default": true, - "x-categories": ["Advanced"] - }, - "clickAnalytics": { - "type": "boolean", - "description": "Whether to include a `queryID` attribute in the response.\n\nThe query ID is a unique identifier for a search query and is required for tracking [click and conversion events](https://www.algolia.com/guides/sending-events/getting-started/).\n", - "default": false, - "x-categories": ["Analytics"] - }, - "analytics": { - "type": "boolean", - "description": "Whether this search will be included in Analytics.", - "default": true, - "x-categories": ["Analytics"] - }, - "analyticsTags": { - "type": "array", - "items": { "type": "string" }, - "description": "Tags to apply to the query for [segmenting analytics data](https://www.algolia.com/doc/guides/search-analytics/guides/segments/).", - "default": [], - "x-categories": ["Analytics"] - }, - "percentileComputation": { - "type": "boolean", - "description": "Whether to include this search when calculating processing-time percentiles.", - "default": true, - "x-categories": ["Advanced"] - }, - "enableABTest": { - "type": "boolean", - "description": "Whether to enable A/B testing for this search.", - "default": true, - "x-categories": ["Advanced"] - } - } - }, - "baseSearchParams": { - "allOf": [ - { "$ref": "#/components/schemas/searchParamsQuery" }, - { "$ref": "#/components/schemas/baseSearchParamsWithoutQuery" } - ] - }, - "hitsPerPage": { - "type": "integer", - "description": "Number of hits per page.", - "default": 20, - "minimum": 1, - "maximum": 1000, - "x-categories": ["Pagination"] - }, - "typoToleranceEnum": { - "type": "string", - "title": "typo tolerance", - "description": "- `min`. Return matches with the lowest number of typos.\n For example, if you have matches without typos, only include those.\n But if there are no matches without typos (with 1 typo), include matches with 1 typo (2 typos).\n- `strict`. Return matches with the two lowest numbers of typos.\n With `strict`, the Typo ranking criterion is applied first in the `ranking` setting.\n", - "enum": ["min", "strict"] - }, - "typoTolerance": { - "description": "Whether [typo tolerance](https://www.algolia.com/doc/guides/managing-results/optimize-search-results/typo-tolerance/) is enabled and how it is applied.\n\nIf typo tolerance is true, `min`, or `strict`, [word splitting and concetenation](https://www.algolia.com/doc/guides/managing-results/optimize-search-results/handling-natural-languages-nlp/in-depth/splitting-and-concatenation/) is also active.\n", - "oneOf": [ - { - "type": "boolean", - "default": true, - "description": "Whether typo tolerance is active. If true, matches with typos are included in the search results and rank after exact matches." - }, - { "$ref": "#/components/schemas/typoToleranceEnum" } - ], - "x-categories": ["Typos"] - }, - "booleanString": { "type": "string", "enum": ["true", "false"] }, - "ignorePlurals": { - "description": "Treat singular, plurals, and other forms of declensions as equivalent.\nYou should only use this feature for the languages used in your index.\n", - "example": ["ca", "es"], - "oneOf": [ - { - "type": "array", - "description": "ISO code for languages for which this feature should be active.\nThis overrides languages you set with `queryLanguages`.\n", - "items": { "$ref": "#/components/schemas/supportedLanguage" } - }, - { "$ref": "#/components/schemas/booleanString" }, - { - "type": "boolean", - "description": "If true, `ignorePlurals` is active for all languages included in `queryLanguages`, or for all supported languages, if `queryLanguges` is empty.\nIf false, singulars, plurals, and other declensions won't be considered equivalent.\n", - "default": false - } - ], - "x-categories": ["Languages"] - }, - "removeStopWords": { - "description": "Removes stop words from the search query.\n\nStop words are common words like articles, conjunctions, prepositions, or pronouns that have little or no meaning on their own.\nIn English, \"the\", \"a\", or \"and\" are stop words.\n\nYou should only use this feature for the languages used in your index.\n", - "example": ["ca", "es"], - "oneOf": [ - { - "type": "array", - "description": "ISO code for languages for which stop words should be removed. This overrides languages you set in `queryLanguges`.", - "items": { "$ref": "#/components/schemas/supportedLanguage" } - }, - { - "type": "boolean", - "default": false, - "description": "If true, stop words are removed for all languages you included in `queryLanguages`, or for all supported languages, if `queryLanguages` is empty.\nIf false, stop words are not removed.\n" - } - ], - "x-categories": ["Languages"] - }, - "queryType": { - "type": "string", - "enum": ["prefixLast", "prefixAll", "prefixNone"], - "description": "Determines if and how query words are interpreted as prefixes.\n\nBy default, only the last query word is treated as prefix (`prefixLast`).\nTo turn off prefix search, use `prefixNone`.\nAvoid `prefixAll`, which treats all query words as prefixes.\nThis might lead to counterintuitive results and makes your search slower.\n\nFor more information, see [Prefix searching](https://www.algolia.com/doc/guides/managing-results/optimize-search-results/override-search-engine-defaults/in-depth/prefix-searching/).\n", - "default": "prefixLast", - "x-categories": ["Query strategy"] - }, - "removeWordsIfNoResults": { - "type": "string", - "enum": ["none", "lastWords", "firstWords", "allOptional"], - "example": "firstWords", - "description": "Strategy for removing words from the query when it doesn't return any results.\nThis helps to avoid returning empty search results.\n\n- `none`.\n No words are removed when a query doesn't return results.\n\n- `lastWords`.\n Treat the last (then second to last, then third to last) word as optional,\n until there are results or at most 5 words have been removed.\n\n- `firstWords`.\n Treat the first (then second, then third) word as optional,\n until there are results or at most 5 words have been removed.\n\n- `allOptional`.\n Treat all words as optional.\n\nFor more information, see [Remove words to improve results](https://www.algolia.com/doc/guides/managing-results/optimize-search-results/empty-or-insufficient-results/in-depth/why-use-remove-words-if-no-results/).\n", - "default": "none", - "x-categories": ["Query strategy"] - }, - "mode": { - "type": "string", - "enum": ["neuralSearch", "keywordSearch"], - "description": "Search mode the index will use to query for results.\n\nThis setting only applies to indices, for which Algolia enabled NeuralSearch for you.\n", - "default": "keywordSearch", - "x-categories": ["Query strategy"] - }, - "semanticSearch": { - "type": "object", - "description": "Settings for the semantic search part of NeuralSearch.\nOnly used when `mode` is `neuralSearch`.\n", - "properties": { - "eventSources": { - "oneOf": [ - { - "type": "array", - "description": "Indices from which to collect click and conversion events.\n\nIf null, the current index and all its replicas are used.\n", - "items": { "type": "string" } - }, - { "type": "null" } - ] - } - } - }, - "exactOnSingleWordQuery": { - "type": "string", - "enum": ["attribute", "none", "word"], - "description": "Determines how the [Exact ranking criterion](https://www.algolia.com/doc/guides/managing-results/optimize-search-results/override-search-engine-defaults/in-depth/adjust-exact-settings/#turn-off-exact-for-some-attributes) is computed when the search query has only one word.\n\n- `attribute`.\n The Exact ranking criterion is 1 if the query word and attribute value are the same.\n For example, a search for \"road\" will match the value \"road\", but not \"road trip\".\n\n- `none`.\n The Exact ranking criterion is ignored on single-word searches.\n\n- `word`.\n The Exact ranking criterion is 1 if the query word is found in the attribute value.\n The query word must have at least 3 characters and must not be a stop word.\n Only exact matches will be highlighted,\n partial and prefix matches won't.\n", - "default": "attribute", - "x-categories": ["Query strategy"] - }, - "alternativesAsExact": { - "type": "string", - "enum": ["ignorePlurals", "singleWordSynonym", "multiWordsSynonym"], - "x-categories": ["Query strategy"] - }, - "advancedSyntaxFeatures": { - "type": "string", - "enum": ["exactPhrase", "excludeWords"], - "x-categories": ["Query strategy"] - }, - "distinct": { - "description": "Determines how many records of a group are included in the search results.\n\nRecords with the same value for the `attributeForDistinct` attribute are considered a group.\nThe `distinct` setting controls how many members of the group are returned.\nThis is useful for [deduplication and grouping](https://www.algolia.com/doc/guides/managing-results/refine-results/grouping/#introducing-algolias-distinct-feature).\n\nThe `distinct` setting is ignored if `attributeForDistinct` is not set.\n", - "example": 1, - "oneOf": [ - { - "type": "boolean", - "description": "Whether deduplication is turned on. If true, only one member of a group is shown in the search results." - }, - { - "type": "integer", - "description": "Number of members of a group of records to include in the search results.\n\n- Don't use `distinct > 1` for records that might be [promoted by rules](https://www.algolia.com/doc/guides/managing-results/rules/merchandising-and-promoting/how-to/promote-hits/).\n The number of hits won't be correct and faceting won't work as expected.\n- With `distinct > 1`, the `hitsPerPage` parameter controls the number of returned groups.\n For example, with `hitsPerPage: 10` and `distinct: 2`, up to 20 records are returned.\n Likewise, the `nbHits` response attribute contains the number of returned groups.\n", - "minimum": 0, - "maximum": 4, - "default": 0 - } - ], - "x-categories": ["Advanced"] - }, - "maxFacetHits": { - "type": "integer", - "description": "Maximum number of facet values to return when [searching for facet values](https://www.algolia.com/doc/guides/managing-results/refine-results/faceting/#search-for-facet-values).", - "maximum": 100, - "default": 10, - "x-categories": ["Advanced"] - }, - "order": { - "description": "Explicit order of facets or facet values.\n\nThis setting lets you always show specific facets or facet values at the top of the list.\n", - "type": "array", - "items": { "type": "string" } - }, - "facets": { - "description": "Order of facet names.", - "type": "object", - "additionalProperties": false, - "properties": { "order": { "$ref": "#/components/schemas/order" } } - }, - "sortRemainingBy": { - "description": "Order of facet values that aren't explicitly positioned with the `order` setting.\n\n- `count`.\n Order remaining facet values by decreasing count.\n The count is the number of matching records containing this facet value.\n\n- `alpha`.\n Sort facet values alphabetically.\n\n- `hidden`.\n Don't show facet values that aren't explicitly positioned.\n", - "type": "string", - "enum": ["count", "alpha", "hidden"] - }, - "hide": { - "description": "Hide facet values.", - "type": "array", - "items": { "type": "string" } - }, - "value": { - "type": "object", - "additionalProperties": false, - "properties": { - "order": { "$ref": "#/components/schemas/order" }, - "sortRemainingBy": { "$ref": "#/components/schemas/sortRemainingBy" }, - "hide": { "$ref": "#/components/schemas/hide" } - } - }, - "values": { - "description": "Order of facet values. One object for each facet.", - "type": "object", - "additionalProperties": { - "x-additionalPropertiesName": "facet", - "$ref": "#/components/schemas/value" - } - }, - "facetOrdering": { - "description": "Order of facet names and facet values in your UI.", - "type": "object", - "additionalProperties": false, - "properties": { - "facets": { "$ref": "#/components/schemas/facets" }, - "values": { "$ref": "#/components/schemas/values" } - } - }, - "redirectURL": { - "description": "The redirect rule container.", - "type": "object", - "additionalProperties": false, - "properties": { "url": { "type": "string" } } - }, - "renderingContent": { - "description": "Extra data that can be used in the search UI.\n\nYou can use this to control aspects of your search UI, such as, the order of facet names and values\nwithout changing your frontend code.\n", - "type": "object", - "additionalProperties": false, - "properties": { - "facetOrdering": { "$ref": "#/components/schemas/facetOrdering" }, - "redirect": { "$ref": "#/components/schemas/redirectURL" } - }, - "x-categories": ["Advanced"] - }, - "reRankingApplyFilter": { - "description": "Restrict [Dynamic Re-Ranking](https://www.algolia.com/doc/guides/algolia-ai/re-ranking/) to records that match these filters.\n", - "oneOf": [ - { "type": "array", "items": { "$ref": "#/components/schemas/reRankingApplyFilter" } }, - { "type": "string", "x-categories": ["Filtering"] } - ] - }, - "indexSettingsAsSearchParams": { - "type": "object", - "additionalProperties": false, - "properties": { - "attributesToRetrieve": { - "type": "array", - "items": { "type": "string" }, - "example": ["author", "title", "content"], - "description": "Attributes to include in the API response.\n\nTo reduce the size of your response, you can retrieve only some of the attributes.\nAttribute names are case-sensitive.\n\n- `*` retrieves all attributes, except attributes included in the `customRanking` and `unretrievableAttributes` settings.\n- To retrieve all attributes except a specific one, prefix the attribute with a dash and combine it with the `*`: `[\"*\", \"-ATTRIBUTE\"]`.\n- The `objectID` attribute is always included.\n", - "default": ["*"], - "x-categories": ["Attributes"] - }, - "ranking": { - "type": "array", - "items": { "type": "string" }, - "description": "Determines the order in which Algolia returns your results.\n\nBy default, each entry corresponds to a [ranking criteria](https://www.algolia.com/doc/guides/managing-results/relevance-overview/in-depth/ranking-criteria/).\nThe tie-breaking algorithm sequentially applies each criterion in the order they're specified.\nIf you configure a replica index for [sorting by an attribute](https://www.algolia.com/doc/guides/managing-results/refine-results/sorting/how-to/sort-by-attribute/),\nyou put the sorting attribute at the top of the list.\n\n**Modifiers**\n\n- `asc(\"ATTRIBUTE\")`.\n Sort the index by the values of an attribute, in ascending order.\n- `desc(\"ATTRIBUTE\")`.\n Sort the index by the values of an attribute, in descending order.\n\nBefore you modify the default setting,\nyou should test your changes in the dashboard,\nand by [A/B testing](https://www.algolia.com/doc/guides/ab-testing/what-is-ab-testing/).\n", - "default": [ - "typo", - "geo", - "words", - "filters", - "proximity", - "attribute", - "exact", - "custom" - ], - "x-categories": ["Ranking"] - }, - "customRanking": { - "type": "array", - "items": { "type": "string" }, - "example": ["desc(popularity)", "asc(price)"], - "description": "Attributes to use as [custom ranking](https://www.algolia.com/doc/guides/managing-results/must-do/custom-ranking/).\nAttribute names are case-sensitive.\n\nThe custom ranking attributes decide which items are shown first if the other ranking criteria are equal.\n\nRecords with missing values for your selected custom ranking attributes are always sorted last.\nBoolean attributes are sorted based on their alphabetical order.\n\n**Modifiers**\n\n- `asc(\"ATTRIBUTE\")`.\n Sort the index by the values of an attribute, in ascending order.\n\n- `desc(\"ATTRIBUTE\")`.\n Sort the index by the values of an attribute, in descending order.\n\nIf you use two or more custom ranking attributes,\n[reduce the precision](https://www.algolia.com/doc/guides/managing-results/must-do/custom-ranking/how-to/controlling-custom-ranking-metrics-precision/) of your first attributes,\nor the other attributes will never be applied.\n", - "default": [], - "x-categories": ["Ranking"] - }, - "relevancyStrictness": { - "type": "integer", - "example": 90, - "description": "Relevancy threshold below which less relevant results aren't included in the results.\n\nYou can only set `relevancyStrictness` on [virtual replica indices](https://www.algolia.com/doc/guides/managing-results/refine-results/sorting/in-depth/replicas/#what-are-virtual-replicas).\nUse this setting to strike a balance between the relevance and number of returned results.\n", - "default": 100, - "x-categories": ["Ranking"] - }, - "attributesToHighlight": { - "type": "array", - "items": { "type": "string" }, - "example": ["author", "title", "conten", "content"], - "description": "Attributes to highlight.\n\nBy default, all searchable attributes are highlighted.\nUse `*` to highlight all attributes or use an empty array `[]` to turn off highlighting.\nAttribute names are case-sensitive.\n\nWith highlighting, strings that match the search query are surrounded by HTML tags defined by `highlightPreTag` and `highlightPostTag`.\nYou can use this to visually highlight matching parts of a search query in your UI.\n\nFor more information, see [Highlighting and snippeting](https://www.algolia.com/doc/guides/building-search-ui/ui-and-ux-patterns/highlighting-snippeting/js/).\n", - "x-categories": ["Highlighting and Snippeting"] - }, - "attributesToSnippet": { - "type": "array", - "items": { "type": "string" }, - "example": ["content:80", "description"], - "description": "Attributes for which to enable snippets.\nAttribute names are case-sensitive.\n\nSnippets provide additional context to matched words.\nIf you enable snippets, they include 10 words, including the matched word.\nThe matched word will also be wrapped by HTML tags for highlighting.\nYou can adjust the number of words with the following notation: `ATTRIBUTE:NUMBER`,\nwhere `NUMBER` is the number of words to be extracted.\n", - "default": [], - "x-categories": ["Highlighting and Snippeting"] - }, - "highlightPreTag": { - "type": "string", - "description": "HTML tag to insert before the highlighted parts in all highlighted results and snippets.", - "default": "", - "x-categories": ["Highlighting and Snippeting"] - }, - "highlightPostTag": { - "type": "string", - "description": "HTML tag to insert after the highlighted parts in all highlighted results and snippets.", - "default": "", - "x-categories": ["Highlighting and Snippeting"] - }, - "snippetEllipsisText": { - "type": "string", - "description": "String used as an ellipsis indicator when a snippet is truncated.", - "default": "…", - "x-categories": ["Highlighting and Snippeting"] - }, - "restrictHighlightAndSnippetArrays": { - "type": "boolean", - "description": "Whether to restrict highlighting and snippeting to items that at least partially matched the search query.\nBy default, all items are highlighted and snippeted.\n", - "default": false, - "x-categories": ["Highlighting and Snippeting"] - }, - "hitsPerPage": { "$ref": "#/components/schemas/hitsPerPage" }, - "minWordSizefor1Typo": { - "type": "integer", - "description": "Minimum number of characters a word in the search query must contain to accept matches with [one typo](https://www.algolia.com/doc/guides/managing-results/optimize-search-results/typo-tolerance/in-depth/configuring-typo-tolerance/#configuring-word-length-for-typos).", - "default": 4, - "x-categories": ["Typos"] - }, - "minWordSizefor2Typos": { - "type": "integer", - "description": "Minimum number of characters a word in the search query must contain to accept matches with [two typos](https://www.algolia.com/doc/guides/managing-results/optimize-search-results/typo-tolerance/in-depth/configuring-typo-tolerance/#configuring-word-length-for-typos).", - "default": 8, - "x-categories": ["Typos"] - }, - "typoTolerance": { "$ref": "#/components/schemas/typoTolerance" }, - "allowTyposOnNumericTokens": { - "type": "boolean", - "description": "Whether to allow typos on numbers in the search query.\n\nTurn off this setting to reduce the number of irrelevant matches\nwhen searching in large sets of similar numbers.\n", - "default": true, - "x-categories": ["Typos"] - }, - "disableTypoToleranceOnAttributes": { - "type": "array", - "items": { "type": "string" }, - "example": ["sku"], - "description": "Attributes for which you want to turn off [typo tolerance](https://www.algolia.com/doc/guides/managing-results/optimize-search-results/typo-tolerance/).\nAttribute names are case-sensitive.\n\nReturning only exact matches can help when:\n\n- [Searching in hyphenated attributes](https://www.algolia.com/doc/guides/managing-results/optimize-search-results/typo-tolerance/how-to/how-to-search-in-hyphenated-attributes/).\n- Reducing the number of matches when you have too many.\n This can happen with attributes that are long blocks of text, such as product descriptions.\n\nConsider alternatives such as `disableTypoToleranceOnWords` or adding synonyms if your attributes have intentional unusual spellings that might look like typos.\n", - "default": [], - "x-categories": ["Typos"] - }, - "ignorePlurals": { "$ref": "#/components/schemas/ignorePlurals" }, - "removeStopWords": { "$ref": "#/components/schemas/removeStopWords" }, - "keepDiacriticsOnCharacters": { - "type": "string", - "example": "øé", - "description": "Characters for which diacritics should be preserved.\n\nBy default, Algolia removes diacritics from letters.\nFor example, `é` becomes `e`. If this causes issues in your search,\nyou can specify characters that should keep their diacritics.\n", - "default": "", - "x-categories": ["Languages"] - }, - "queryLanguages": { - "type": "array", - "items": { "$ref": "#/components/schemas/supportedLanguage" }, - "example": ["es"], - "description": "Languages for language-specific query processing steps such as plurals, stop-word removal, and word-detection dictionaries.\n\nThis setting sets a default list of languages used by the `removeStopWords` and `ignorePlurals` settings.\nThis setting also sets a dictionary for word detection in the logogram-based [CJK](https://www.algolia.com/doc/guides/managing-results/optimize-search-results/handling-natural-languages-nlp/in-depth/normalization/#normalization-for-logogram-based-languages-cjk) languages.\nTo support this, you must place the CJK language **first**.\n\n**You should always specify a query language.**\nIf you don't specify an indexing language, the search engine uses all [supported languages](https://www.algolia.com/doc/guides/managing-results/optimize-search-results/handling-natural-languages-nlp/in-depth/supported-languages/),\nor the languages you specified with the `ignorePlurals` or `removeStopWords` parameters.\nThis can lead to unexpected search results.\nFor more information, see [Language-specific configuration](https://www.algolia.com/doc/guides/managing-results/optimize-search-results/handling-natural-languages-nlp/in-depth/language-specific-configurations/).\n", - "default": [], - "x-categories": ["Languages"] - }, - "decompoundQuery": { - "type": "boolean", - "description": "Whether to split compound words in the query into their building blocks.\n\nFor more information, see [Word segmentation](https://www.algolia.com/doc/guides/managing-results/optimize-search-results/handling-natural-languages-nlp/in-depth/language-specific-configurations/#splitting-compound-words).\nWord segmentation is supported for these languages: German, Dutch, Finnish, Swedish, and Norwegian.\nDecompounding doesn't work for words with [non-spacing mark Unicode characters](https://www.charactercodes.net/category/non-spacing_mark).\nFor example, `Gartenstühle` won't be decompounded if the `ü` consists of `u` (U+0075) and `◌̈` (U+0308).\n", - "default": true, - "x-categories": ["Languages"] - }, - "enableRules": { - "type": "boolean", - "description": "Whether to enable rules.", - "default": true, - "x-categories": ["Rules"] - }, - "enablePersonalization": { - "type": "boolean", - "description": "Whether to enable Personalization.", - "default": false, - "x-categories": ["Personalization"] - }, - "queryType": { "$ref": "#/components/schemas/queryType" }, - "removeWordsIfNoResults": { "$ref": "#/components/schemas/removeWordsIfNoResults" }, - "mode": { "$ref": "#/components/schemas/mode" }, - "semanticSearch": { "$ref": "#/components/schemas/semanticSearch" }, - "advancedSyntax": { - "type": "boolean", - "description": "Whether to support phrase matching and excluding words from search queries.\n\nUse the `advancedSyntaxFeatures` parameter to control which feature is supported.\n", - "default": false, - "x-categories": ["Query strategy"] - }, - "optionalWords": { - "type": "array", - "items": { "type": "string" }, - "example": ["blue", "iphone case"], - "description": "Words that should be considered optional when found in the query.\n\nBy default, records must match all words in the search query to be included in the search results.\nAdding optional words can help to increase the number of search results by running an additional search query that doesn't include the optional words.\nFor example, if the search query is \"action video\" and \"video\" is an optional word,\nthe search engine runs two queries. One for \"action video\" and one for \"action\".\nRecords that match all words are ranked higher.\n\nFor a search query with 4 or more words **and** all its words are optional,\nthe number of matched words required for a record to be included in the search results increases for every 1,000 records:\n\n- If `optionalWords` has less than 10 words, the required number of matched words increases by 1:\n results 1 to 1,000 require 1 matched word, results 1,001 to 2000 need 2 matched words.\n- If `optionalWords` has 10 or more words, the number of required matched words increases by the number of optional words dividied by 5 (rounded down).\n For example, with 18 optional words: results 1 to 1,000 require 1 matched word, results 1,001 to 2000 need 4 matched words.\n\nFor more information, see [Optional words](https://www.algolia.com/doc/guides/managing-results/optimize-search-results/empty-or-insufficient-results/#creating-a-list-of-optional-words).\n", - "default": [], - "x-categories": ["Query strategy"] - }, - "disableExactOnAttributes": { - "type": "array", - "items": { "type": "string" }, - "example": ["description"], - "description": "Searchable attributes for which you want to [turn off the Exact ranking criterion](https://www.algolia.com/doc/guides/managing-results/optimize-search-results/override-search-engine-defaults/in-depth/adjust-exact-settings/#turn-off-exact-for-some-attributes).\nAttribute names are case-sensitive.\n\nThis can be useful for attributes with long values, where the likelyhood of an exact match is high,\nsuch as product descriptions.\nTurning off the Exact ranking criterion for these attributes favors exact matching on other attributes.\nThis reduces the impact of individual attributes with a lot of content on ranking.\n", - "default": [], - "x-categories": ["Query strategy"] - }, - "exactOnSingleWordQuery": { "$ref": "#/components/schemas/exactOnSingleWordQuery" }, - "alternativesAsExact": { - "type": "array", - "items": { "$ref": "#/components/schemas/alternativesAsExact" }, - "description": "Alternatives of query words that should be considered as exact matches by the Exact ranking criterion.\n\n- `ignorePlurals`.\n Plurals and similar declensions added by the `ignorePlurals` setting are considered exact matches.\n\n- `singleWordSynonym`.\n Single-word synonyms, such as \"NY/NYC\" are considered exact matches.\n\n- `multiWordsSynonym`.\n Multi-word synonyms, such as \"NY/New York\" are considered exact matches.\n", - "default": ["ignorePlurals", "singleWordSynonym"], - "x-categories": ["Query strategy"] - }, - "advancedSyntaxFeatures": { - "type": "array", - "items": { "$ref": "#/components/schemas/advancedSyntaxFeatures" }, - "description": "Advanced search syntax features you want to support.\n\n- `exactPhrase`.\n Phrases in quotes must match exactly.\n For example, `sparkly blue \"iPhone case\"` only returns records with the exact string \"iPhone case\".\n\n- `excludeWords`.\n Query words prefixed with a `-` must not occur in a record.\n For example, `search -engine` matches records that contain \"search\" but not \"engine\".\n\nThis setting only has an effect if `advancedSyntax` is true.\n", - "default": ["exactPhrase", "excludeWords"], - "x-categories": ["Query strategy"] - }, - "distinct": { "$ref": "#/components/schemas/distinct" }, - "replaceSynonymsInHighlight": { - "type": "boolean", - "description": "Whether to replace a highlighted word with the matched synonym.\n\nBy default, the original words are highlighted even if a synonym matches.\nFor example, with `home` as a synonym for `house` and a search for `home`,\nrecords matching either \"home\" or \"house\" are included in the search results,\nand either \"home\" or \"house\" are highlighted.\n\nWith `replaceSynonymsInHighlight` set to `true`, a search for `home` still matches the same records,\nbut all occurences of \"house\" are replaced by \"home\" in the highlighted response.\n", - "default": false, - "x-categories": ["Highlighting and Snippeting"] - }, - "minProximity": { - "type": "integer", - "minimum": 1, - "maximum": 7, - "description": "Minimum proximity score for two matching words.\n\nThis adjusts the [Proximity ranking criterion](https://www.algolia.com/doc/guides/managing-results/relevance-overview/in-depth/ranking-criteria/#proximity)\nby equally scoring matches that are farther apart.\n\nFor example, if `minProximity` is 2, neighboring matches and matches with one word between them would have the same score.\n", - "default": 1, - "x-categories": ["Advanced"] - }, - "responseFields": { - "type": "array", - "items": { "type": "string" }, - "description": "Properties to include in the API response of `search` and `browse` requests.\n\nBy default, all response properties are included.\nTo reduce the response size, you can select, which attributes should be included.\n\nYou can't exclude these properties:\n`message`, `warning`, `cursor`, `serverUsed`, `indexUsed`,\n`abTestVariantID`, `parsedQuery`, or any property triggered by the `getRankingInfo` parameter.\n\nDon't exclude properties that you might need in your search UI.\n", - "default": ["*"], - "x-categories": ["Advanced"] - }, - "maxFacetHits": { "$ref": "#/components/schemas/maxFacetHits" }, - "maxValuesPerFacet": { - "type": "integer", - "description": "Maximum number of facet values to return for each facet.", - "default": 100, - "maximum": 1000, - "x-categories": ["Faceting"] - }, - "sortFacetValuesBy": { - "type": "string", - "description": "Order in which to retrieve facet values.\n\n- `count`.\n Facet values are retrieved by decreasing count.\n The count is the number of matching records containing this facet value.\n\n- `alpha`.\n Retrieve facet values alphabetically.\n\nThis setting doesn't influence how facet values are displayed in your UI (see `renderingContent`).\nFor more information, see [facet value display](https://www.algolia.com/doc/guides/building-search-ui/ui-and-ux-patterns/facet-display/js/).\n", - "default": "count", - "x-categories": ["Faceting"] - }, - "attributeCriteriaComputedByMinProximity": { - "type": "boolean", - "description": "Whether the best matching attribute should be determined by minimum proximity.\n\nThis setting only affects ranking if the Attribute ranking criterion comes before Proximity in the `ranking` setting.\nIf true, the best matching attribute is selected based on the minimum proximity of multiple matches.\nOtherwise, the best matching attribute is determined by the order in the `searchableAttributes` setting.\n", - "default": false, - "x-categories": ["Advanced"] - }, - "renderingContent": { "$ref": "#/components/schemas/renderingContent" }, - "enableReRanking": { - "type": "boolean", - "description": "Whether this search will use [Dynamic Re-Ranking](https://www.algolia.com/doc/guides/algolia-ai/re-ranking/).\n\nThis setting only has an effect if you activated Dynamic Re-Ranking for this index in the Algolia dashboard.\n", - "default": true, - "x-categories": ["Filtering"] - }, - "reRankingApplyFilter": { - "oneOf": [{ "$ref": "#/components/schemas/reRankingApplyFilter" }, { "type": "null" }] - } - } - }, - "searchParamsObject": { - "title": "Search parameters as object", - "description": "Each parameter value, including the `query` must not be larger than 512 bytes.", - "allOf": [ - { "$ref": "#/components/schemas/baseSearchParams" }, - { "$ref": "#/components/schemas/indexSettingsAsSearchParams" } - ] - }, - "searchParams": { - "title": "queryParameters", - "allOf": [ - { "$ref": "#/components/schemas/searchParamsObject" }, - { - "type": "object", - "description": "Search parameters for filtering the recommendations." - } - ] - }, - "baseRecommendRequest": { - "type": "object", - "additionalProperties": false, - "properties": { - "indexName": { "$ref": "#/components/schemas/indexName" }, - "threshold": { - "type": "number", - "format": "double", - "minimum": 0, - "maximum": 100, - "description": "Minimum score a recommendation must have to be included in the response." - }, - "maxRecommendations": { - "type": "integer", - "minimum": 1, - "maximum": 1000, - "default": 30, - "description": "Maximum number of recommendations to retrieve.\nBy default, all recommendations are returned and no fallback request is made.\nDepending on the available recommendations and the other request parameters,\nthe actual number of recommendations may be lower than this value.\n" - }, - "queryParameters": { "$ref": "#/components/schemas/searchParams" } - }, - "required": ["indexName", "threshold"] - }, - "fbtModel": { - "type": "string", - "description": "Frequently bought together model.\n\nThis model recommends items that have been purchased within 1 day with the item with the ID `objectID`.\n", - "enum": ["bought-together"] - }, - "objectID": { - "type": "string", - "description": "Unique record identifier.", - "example": "test-record-123" - }, - "frequentlyBoughtTogether": { - "type": "object", - "properties": { - "model": { "$ref": "#/components/schemas/fbtModel" }, - "objectID": { "$ref": "#/components/schemas/objectID" } - }, - "required": ["model", "objectID"] - }, - "boughtTogetherQuery": { - "title": "Frequently bought together", - "allOf": [ - { "$ref": "#/components/schemas/baseRecommendRequest" }, - { "$ref": "#/components/schemas/frequentlyBoughtTogether" } - ] - }, - "relatedModel": { - "type": "string", - "description": "Related products or similar content model.\n\nThis model recommends items that are similar to the item with the ID `objectID`.\nSimilarity is determined from the user interactions and attributes.\n", - "enum": ["related-products"] - }, - "fallbackParams": { - "title": "fallbackParameters", - "allOf": [ - { "$ref": "#/components/schemas/searchParamsObject" }, - { - "type": "object", - "description": "Search parameters to use for a fallback request if there aren't enough recommendations." - } - ] - }, - "relatedProducts": { - "type": "object", - "properties": { - "model": { "$ref": "#/components/schemas/relatedModel" }, - "objectID": { "$ref": "#/components/schemas/objectID" }, - "fallbackParameters": { "$ref": "#/components/schemas/fallbackParams" } - }, - "required": ["model", "objectID"] - }, - "relatedQuery": { - "title": "Related products", - "allOf": [ - { "$ref": "#/components/schemas/baseRecommendRequest" }, - { "$ref": "#/components/schemas/relatedProducts" } - ] - }, - "facetName": { - "type": "string", - "description": "Facet attribute. To be used in combination with `facetValue`.\nIf specified, only recommendations matching the facet filter will be returned.\n" - }, - "facetValue": { - "type": "string", - "description": "Facet value. To be used in combination with `facetName`.\nIf specified, only recommendations matching the facet filter will be returned.\n" - }, - "trendingItemsModel": { - "description": "Trending items model.\n\nTrending items are determined from the number of conversion events collected on them.\n", - "type": "string", - "enum": ["trending-items"] - }, - "trendingItems": { - "type": "object", - "properties": { - "facetName": { "$ref": "#/components/schemas/facetName" }, - "facetValue": { "$ref": "#/components/schemas/facetValue" }, - "model": { "$ref": "#/components/schemas/trendingItemsModel" }, - "fallbackParameters": { "$ref": "#/components/schemas/searchParamsObject" } - }, - "required": ["model"] - }, - "trendingItemsQuery": { - "title": "Trending items", - "allOf": [ - { "$ref": "#/components/schemas/baseRecommendRequest" }, - { "$ref": "#/components/schemas/trendingItems" } - ] - }, - "trendingFacetsModel": { - "type": "string", - "description": "Trending facet values model.\n\nThis model recommends trending facet values for the specified facet attribute.\n", - "enum": ["trending-facets"] - }, - "trendingFacets": { - "type": "object", - "properties": { - "facetName": { - "description": "Facet attribute for which to retrieve trending facet values." - }, - "model": { "$ref": "#/components/schemas/trendingFacetsModel" }, - "fallbackParameters": { "$ref": "#/components/schemas/fallbackParams" } - }, - "required": ["facetName", "model"] - }, - "trendingFacetsQuery": { - "title": "Trending facet values", - "allOf": [ - { "$ref": "#/components/schemas/baseRecommendRequest" }, - { "$ref": "#/components/schemas/trendingFacets" } - ] - }, - "lookingSimilarModel": { - "type": "string", - "description": "Looking similar model.\n\nThis model recommends items that look similar to the item with the ID `objectID` based on image attributes in your index.\n", - "enum": ["looking-similar"] - }, - "lookingSimilar": { - "type": "object", - "properties": { - "model": { "$ref": "#/components/schemas/lookingSimilarModel" }, - "objectID": { "$ref": "#/components/schemas/objectID" }, - "fallbackParameters": { "$ref": "#/components/schemas/fallbackParams" } - }, - "required": ["model", "objectID"] - }, - "lookingSimilarQuery": { - "title": "Looking similar", - "allOf": [ - { "$ref": "#/components/schemas/baseRecommendRequest" }, - { "$ref": "#/components/schemas/lookingSimilar" } - ] - }, - "recommendedForYouModel": { - "type": "string", - "description": "\"Recommened for you\" model.\n", - "enum": ["recommended-for-you"] - }, - "recommendedForYou": { - "type": "object", - "properties": { - "model": { "$ref": "#/components/schemas/recommendedForYouModel" }, - "fallbackParameters": { "$ref": "#/components/schemas/fallbackParams" } - }, - "required": ["model"] - }, - "recommendedForYouQuery": { - "title": "Recommended for you", - "allOf": [ - { "$ref": "#/components/schemas/baseRecommendRequest" }, - { "$ref": "#/components/schemas/recommendedForYou" } - ] - }, - "recommendationsRequest": { - "oneOf": [ - { "$ref": "#/components/schemas/boughtTogetherQuery" }, - { "$ref": "#/components/schemas/relatedQuery" }, - { "$ref": "#/components/schemas/trendingItemsQuery" }, - { "$ref": "#/components/schemas/trendingFacetsQuery" }, - { "$ref": "#/components/schemas/lookingSimilarQuery" }, - { "$ref": "#/components/schemas/recommendedForYouQuery" } - ] - }, - "processingTimeMS": { - "type": "integer", - "description": "Time the server took to process the request, in milliseconds.", - "example": 20 - }, - "RedirectRuleIndexMetadata": { - "type": "object", - "properties": { - "source": { "type": "string", "description": "Source index for the redirect rule." }, - "dest": { "type": "string", "description": "Destination index for the redirect rule." }, - "reason": { "type": "string", "description": "Reason for the redirect rule." }, - "succeed": { "type": "boolean", "description": "Redirect rule status." }, - "data": { - "title": "redirectRuleIndexData", - "type": "object", - "description": "Redirect rule data.", - "required": ["ruleObjectID"], - "properties": { "ruleObjectID": { "type": "string" } } - } - }, - "required": ["data", "succeed", "reason", "dest", "source"] - }, - "userData": { - "type": "object", - "example": { "settingID": "f2a7b51e3503acc6a39b3784ffb84300", "pluginVersion": "1.6.0" }, - "description": "An object with custom data.\n\nYou can store up to 32kB as custom data.\n", - "default": {}, - "x-categories": ["Advanced"] - }, - "baseSearchResponse": { - "type": "object", - "additionalProperties": true, - "required": ["processingTimeMS"], - "properties": { - "abTestID": { - "type": "integer", - "description": "A/B test ID. This is only included in the response for indices that are part of an A/B test." - }, - "abTestVariantID": { - "type": "integer", - "minimum": 1, - "description": "Variant ID. This is only included in the response for indices that are part of an A/B test." - }, - "aroundLatLng": { - "type": "string", - "description": "Computed geographical location.", - "example": "40.71,-74.01", - "pattern": "^(-?\\d+(\\.\\d+)?),\\s*(-?\\d+(\\.\\d+)?)$" - }, - "automaticRadius": { - "type": "string", - "description": "Distance from a central coordinate provided by `aroundLatLng`." - }, - "exhaustive": { - "title": "exhaustive", - "type": "object", - "description": "Whether certain properties of the search response are calculated exhaustive (exact) or approximated.", - "properties": { - "facetsCount": { - "type": "boolean", - "title": "facetsCount", - "description": "Whether the facet count is exhaustive (`true`) or approximate (`false`). See the [related discussion](https://support.algolia.com/hc/en-us/articles/4406975248145-Why-are-my-facet-and-hit-counts-not-accurate-)." - }, - "facetValues": { - "type": "boolean", - "title": "facetValues", - "description": "The value is `false` if not all facet values are retrieved." - }, - "nbHits": { - "type": "boolean", - "title": "nbHits", - "description": "Whether the `nbHits` is exhaustive (`true`) or approximate (`false`). When the query takes more than 50ms to be processed, the engine makes an approximation. This can happen when using complex filters on millions of records, when typo-tolerance was not exhaustive, or when enough hits have been retrieved (for example, after the engine finds 10,000 exact matches). `nbHits` is reported as non-exhaustive whenever an approximation is made, even if the approximation didn’t, in the end, impact the exhaustivity of the query." - }, - "rulesMatch": { - "type": "boolean", - "title": "rulesMatch", - "description": "Rules matching exhaustivity. The value is `false` if rules were enable for this query, and could not be fully processed due a timeout. This is generally caused by the number of alternatives (such as typos) which is too large." - }, - "typo": { - "type": "boolean", - "title": "typo", - "description": "Whether the typo search was exhaustive (`true`) or approximate (`false`). An approximation is done when the typo search query part takes more than 10% of the query budget (ie. 5ms by default) to be processed (this can happen when a lot of typo alternatives exist for the query). This field will not be included when typo-tolerance is entirely disabled." - } - } - }, - "exhaustiveFacetsCount": { - "type": "boolean", - "description": "See the `facetsCount` field of the `exhaustive` object in the response.", - "deprecated": true - }, - "exhaustiveNbHits": { - "type": "boolean", - "description": "See the `nbHits` field of the `exhaustive` object in the response.", - "deprecated": true - }, - "exhaustiveTypo": { - "type": "boolean", - "description": "See the `typo` field of the `exhaustive` object in the response.", - "deprecated": true - }, - "facets": { - "title": "facets", - "type": "object", - "additionalProperties": { - "x-additionalPropertiesName": "facet", - "type": "object", - "additionalProperties": { - "x-additionalPropertiesName": "facet count", - "type": "integer" - } - }, - "description": "Facet counts.", - "example": { "category": { "food": 1, "tech": 42 } } - }, - "facets_stats": { - "type": "object", - "description": "Statistics for numerical facets.", - "additionalProperties": { - "title": "facetStats", - "type": "object", - "properties": { - "min": { - "type": "number", - "format": "double", - "description": "Minimum value in the results." - }, - "max": { - "type": "number", - "format": "double", - "description": "Maximum value in the results." - }, - "avg": { - "type": "number", - "format": "double", - "description": "Average facet value in the results." - }, - "sum": { - "type": "number", - "format": "double", - "description": "Sum of all values in the results." - } - } - } - }, - "index": { - "type": "string", - "example": "indexName", - "description": "Index name used for the query." - }, - "indexUsed": { - "type": "string", - "description": "Index name used for the query. During A/B testing, the targeted index isn't always the index used by the query.", - "example": "indexNameAlt" - }, - "message": { "type": "string", "description": "Warnings about the query." }, - "nbSortedHits": { - "type": "integer", - "description": "Number of hits selected and sorted by the relevant sort algorithm.", - "example": 20 - }, - "parsedQuery": { - "type": "string", - "description": "Post-[normalization](https://www.algolia.com/doc/guides/managing-results/optimize-search-results/handling-natural-languages-nlp/#what-does-normalization-mean) query string that will be searched.", - "example": "george clo" - }, - "processingTimeMS": { "$ref": "#/components/schemas/processingTimeMS" }, - "processingTimingsMS": { - "type": "object", - "description": "Experimental. List of processing steps and their times, in milliseconds. You can use this list to investigate performance issues." - }, - "queryAfterRemoval": { - "type": "string", - "description": "Markup text indicating which parts of the original query have been removed to retrieve a non-empty result set." - }, - "redirect": { - "title": "redirect", - "type": "object", - "description": "[Redirect results to a URL](https://www.algolia.com/doc/guides/managing-results/rules/merchandising-and-promoting/how-to/redirects/), this this parameter is for internal use only.\n", - "properties": { - "index": { - "type": "array", - "items": { "$ref": "#/components/schemas/RedirectRuleIndexMetadata" } - } - } - }, - "renderingContent": { "$ref": "#/components/schemas/renderingContent" }, - "serverTimeMS": { - "type": "integer", - "description": "Time the server took to process the request, in milliseconds.", - "example": 20 - }, - "serverUsed": { - "type": "string", - "description": "Host name of the server that processed the request.", - "example": "c2-uk-3.algolia.net" - }, - "userData": { "$ref": "#/components/schemas/userData" }, - "queryID": { - "type": "string", - "description": "Unique identifier for the query. This is used for [click analytics](https://www.algolia.com/doc/guides/analytics/click-analytics/).", - "example": "a00dbc80a8d13c4565a442e7e2dca80a" - } - } - }, - "nbHits": { "type": "integer", "description": "Number of results (hits).", "example": 20 }, - "nbPages": { "type": "integer", "description": "Number of pages of results.", "example": 1 }, - "SearchPagination": { - "type": "object", - "additionalProperties": false, - "properties": { - "page": { "$ref": "#/components/schemas/page" }, - "nbHits": { "$ref": "#/components/schemas/nbHits" }, - "nbPages": { "$ref": "#/components/schemas/nbPages" }, - "hitsPerPage": { "$ref": "#/components/schemas/hitsPerPage" } - }, - "required": ["page", "nbHits", "nbPages", "hitsPerPage"] - }, - "highlightResultMap": { - "type": "object", - "description": "Surround words that match the query with HTML tags for highlighting.", - "additionalProperties": { - "x-additionalPropertiesName": "attribute", - "$ref": "#/components/schemas/highlightResult" - } - }, - "highlightedValue": { - "type": "string", - "description": "Highlighted attribute value, including HTML tags.", - "example": "George Clooney" - }, - "matchLevel": { - "type": "string", - "description": "Whether the whole query string matches or only a part.", - "enum": ["none", "partial", "full"] - }, - "highlightResultOption": { - "type": "object", - "description": "Surround words that match the query with HTML tags for highlighting.", - "additionalProperties": false, - "properties": { - "value": { "$ref": "#/components/schemas/highlightedValue" }, - "matchLevel": { "$ref": "#/components/schemas/matchLevel" }, - "matchedWords": { - "type": "array", - "description": "List of matched words from the search query.", - "example": ["action"], - "items": { "type": "string" } - }, - "fullyHighlighted": { - "type": "boolean", - "description": "Whether the entire attribute value is highlighted." - } - }, - "required": ["value", "matchLevel", "matchedWords"], - "x-discriminator-fields": ["matchLevel", "matchedWords"] - }, - "highlightResultOptionMap": { - "type": "object", - "description": "Surround words that match the query with HTML tags for highlighting.", - "additionalProperties": { - "x-additionalPropertiesName": "attribute", - "$ref": "#/components/schemas/highlightResultOption" - } - }, - "highlightResultOptionArray": { - "type": "array", - "description": "Surround words that match the query with HTML tags for highlighting.", - "items": { "$ref": "#/components/schemas/highlightResultOption" } - }, - "highlightResult": { - "oneOf": [ - { "$ref": "#/components/schemas/highlightResultMap" }, - { "$ref": "#/components/schemas/highlightResultOption" }, - { "$ref": "#/components/schemas/highlightResultOptionMap" }, - { "$ref": "#/components/schemas/highlightResultOptionArray" } - ] - }, - "snippetResultMap": { - "type": "object", - "description": "Snippets that show the context around a matching search query.", - "additionalProperties": { - "x-additionalPropertiesName": "attribute", - "$ref": "#/components/schemas/snippetResult" - } - }, - "snippetResultOption": { - "type": "object", - "description": "Snippets that show the context around a matching search query.", - "additionalProperties": false, - "properties": { - "value": { "$ref": "#/components/schemas/highlightedValue" }, - "matchLevel": { "$ref": "#/components/schemas/matchLevel" } - }, - "required": ["value", "matchLevel"], - "x-discriminator-fields": ["matchLevel"] - }, - "snippetResultOptionMap": { - "type": "object", - "description": "Snippets that show the context around a matching search query.", - "additionalProperties": { - "x-additionalPropertiesName": "attribute", - "$ref": "#/components/schemas/snippetResultOption" - } - }, - "snippetResultOptionArray": { - "type": "array", - "description": "Snippets that show the context around a matching search query.", - "items": { "$ref": "#/components/schemas/snippetResultOption" } - }, - "snippetResult": { - "oneOf": [ - { "$ref": "#/components/schemas/snippetResultMap" }, - { "$ref": "#/components/schemas/snippetResultOption" }, - { "$ref": "#/components/schemas/snippetResultOptionMap" }, - { "$ref": "#/components/schemas/snippetResultOptionArray" } - ] - }, - "matchedGeoLocation": { - "type": "object", - "properties": { - "lat": { - "type": "number", - "format": "double", - "description": "Latitude of the matched location." - }, - "lng": { - "type": "number", - "format": "double", - "description": "Longitude of the matched location." - }, - "distance": { - "type": "integer", - "description": "Distance between the matched location and the search location (in meters)." - } - } - }, - "personalization": { - "type": "object", - "properties": { - "filtersScore": { "type": "integer", "description": "The score of the filters." }, - "rankingScore": { "type": "integer", "description": "The score of the ranking." }, - "score": { "type": "integer", "description": "The score of the event." } - } - }, - "rankingInfo": { - "type": "object", - "description": "Object with detailed information about the record's ranking.", - "additionalProperties": false, - "properties": { - "filters": { - "type": "integer", - "minimum": 0, - "description": "Whether a filter matched the query." - }, - "firstMatchedWord": { - "type": "integer", - "minimum": 0, - "description": "Position of the first matched word in the best matching attribute of the record." - }, - "geoDistance": { - "type": "integer", - "minimum": 0, - "description": "Distance between the geo location in the search query and the best matching geo location in the record, divided by the geo precision (in meters)." - }, - "geoPrecision": { - "type": "integer", - "minimum": 1, - "description": "Precision used when computing the geo distance, in meters." - }, - "matchedGeoLocation": { "$ref": "#/components/schemas/matchedGeoLocation" }, - "personalization": { "$ref": "#/components/schemas/personalization" }, - "nbExactWords": { - "type": "integer", - "minimum": 0, - "description": "Number of exactly matched words." - }, - "nbTypos": { - "type": "integer", - "minimum": 0, - "description": "Number of typos encountered when matching the record." - }, - "promoted": { - "type": "boolean", - "description": "Whether the record was promoted by a rule." - }, - "proximityDistance": { - "type": "integer", - "minimum": 0, - "description": "Number of words between multiple matches in the query plus 1. For single word queries, `proximityDistance` is 0." - }, - "userScore": { - "type": "integer", - "description": "Overall ranking of the record, expressed as a single integer. This attribute is internal." - }, - "words": { "type": "integer", "minimum": 1, "description": "Number of matched words." }, - "promotedByReRanking": { - "type": "boolean", - "description": "Whether the record is re-ranked." - } - }, - "required": ["nbTypos", "firstMatchedWord", "geoDistance", "nbExactWords", "userScore"] - }, - "distinctSeqID": { "type": "integer" }, - "recommendScore": { - "type": "number", - "format": "double", - "minimum": 0, - "maximum": 100, - "description": "Recommendation score." - }, - "recommendHit": { - "type": "object", - "description": "Recommend hit.", - "additionalProperties": true, - "required": ["objectID", "_score"], - "properties": { - "objectID": { "$ref": "#/components/schemas/objectID" }, - "_highlightResult": { "$ref": "#/components/schemas/highlightResultMap" }, - "_snippetResult": { "$ref": "#/components/schemas/snippetResultMap" }, - "_rankingInfo": { "$ref": "#/components/schemas/rankingInfo" }, - "_distinctSeqID": { "$ref": "#/components/schemas/distinctSeqID" }, - "_score": { "$ref": "#/components/schemas/recommendScore" } - } - }, - "trendingFacetHit": { - "type": "object", - "description": "Trending facet hit.", - "required": ["_score", "facetName", "facetValue"], - "properties": { - "_score": { "$ref": "#/components/schemas/recommendScore" }, - "facetName": { "$ref": "#/components/schemas/facetName" }, - "facetValue": { "$ref": "#/components/schemas/facetValue" } - } - }, - "recommendationsHit": { - "oneOf": [ - { "$ref": "#/components/schemas/recommendHit" }, - { "$ref": "#/components/schemas/trendingFacetHit" } - ] - }, - "recommendationsHits": { - "type": "object", - "additionalProperties": false, - "properties": { - "hits": { - "type": "array", - "items": { "$ref": "#/components/schemas/recommendationsHit" } - } - }, - "required": ["hits"] - }, - "recommendationsResults": { - "allOf": [ - { "$ref": "#/components/schemas/baseSearchResponse" }, - { "$ref": "#/components/schemas/SearchPagination" }, - { "$ref": "#/components/schemas/recommendationsHits" } - ] - }, - "recommendModels": { - "type": "string", - "enum": ["related-products", "bought-together", "trending-facets", "trending-items"] - }, - "updatedAt": { - "type": "string", - "example": "2023-07-04T12:49:15Z", - "description": "Date and time when the object was updated, in RFC 3339 format." - }, - "ruleID": { - "title": "objectID", - "type": "string", - "description": "Unique identifier of a rule object." - }, - "context": { - "type": "string", - "pattern": "[A-Za-z0-9_-]+", - "description": "An additional restriction that only triggers the rule, when the search has the same value as `ruleContexts` parameter.\nFor example, if `context: mobile`, the rule is only triggered when the search request has a matching `ruleContexts: mobile`.\nA rule context must only contain alphanumeric characters.\n", - "example": "mobile" - }, - "Condition": { - "type": "object", - "description": "Condition that triggers the rule.\nIf not specified, the rule is triggered for all recommendations.\n", - "properties": { - "filters": { "$ref": "#/components/schemas/filters" }, - "context": { "$ref": "#/components/schemas/context" } - } - }, - "HideConsequenceObject": { - "type": "object", - "description": "Object ID of the recommendation you want to exclude.", - "properties": { "objectID": { "$ref": "#/components/schemas/objectID" } } - }, - "HideConsequence": { - "type": "array", - "description": "Exclude items from recommendations.", - "minItems": 1, - "items": { "$ref": "#/components/schemas/HideConsequenceObject" } - }, - "PromoteConsequenceObject": { - "type": "object", - "description": "Object ID and position of the recommendation you want to pin.", - "properties": { - "objectID": { "$ref": "#/components/schemas/objectID" }, - "position": { - "type": "integer", - "description": "Index in the list of recommendations where to place this item.", - "minimum": 0 - } - } - }, - "PromoteConsequence": { - "type": "array", - "description": "Place items at specific positions in the list of recommendations.", - "minItems": 1, - "items": { "$ref": "#/components/schemas/PromoteConsequenceObject" } - }, - "AutoFacetFilter": { - "type": "object", - "description": "Facet attribute. Only recommendations with the same value (or only recommendations with a different value) as the original viewed item are included.", - "properties": { - "facet": { "type": "string", "description": "Facet attribute." }, - "negative": { - "type": "boolean", - "description": "Whether the filter is negative.\nIf true, recommendations must not have the same value for the `facet` attribute.\nIf false, recommendations must have the same value for the `facet` attribute.\n" - } - } - }, - "ParamsConsequence": { - "type": "object", - "description": "Filter or boost recommendations matching a facet filter.", - "properties": { - "automaticFacetFilters": { - "type": "array", - "description": "Filter recommendations that match or don't match the same `facet:facet_value` combination as the viewed item.", - "items": { "$ref": "#/components/schemas/AutoFacetFilter" } - }, - "filters": { "$ref": "#/components/schemas/filters" }, - "optionalFilters": { - "type": "array", - "description": "Filters to promote or demote records in the search results.\n\nOptional filters work like facet filters, but they don't exclude records from the search results.\nRecords that match the optional filter rank before records that don't match.\nMatches with higher weights (``) rank before matches with lower weights.\nIf you're using a negative filter `facet:-value`, matching records rank after records that don't match.\n", - "items": { "type": "string" }, - "example": ["category:books", "category:-movies"] - } - } - }, - "Consequence": { - "type": "object", - "description": "Effect of the rule.", - "properties": { - "hide": { "$ref": "#/components/schemas/HideConsequence" }, - "promote": { "$ref": "#/components/schemas/PromoteConsequence" }, - "params": { "$ref": "#/components/schemas/ParamsConsequence" } - } - }, - "RecommendRule": { - "type": "object", - "description": "Recommend rule.", - "additionalProperties": false, - "properties": { - "_metadata": { - "title": "ruleMetadata", - "type": "object", - "description": "Rule metadata.", - "properties": { "lastUpdate": { "$ref": "#/components/schemas/updatedAt" } } - }, - "objectID": { "$ref": "#/components/schemas/ruleID" }, - "condition": { "$ref": "#/components/schemas/Condition" }, - "consequence": { "$ref": "#/components/schemas/Consequence" }, - "description": { - "type": "string", - "description": "Description of the rule's purpose. This can be helpful for display in the Algolia dashboard.", - "example": "Display a promotional banner" - }, - "enabled": { - "type": "boolean", - "default": true, - "description": "Indicates whether to enable the rule. If it isn't enabled, it isn't applied at query time." - } - } - }, - "taskID": { - "type": "integer", - "format": "int64", - "example": 1514562690001, - "description": "Unique identifier of a task.\n\nA successful API response means that a task was added to a queue. It might not run immediately. You can check the task's progress with the [`task` operation](#tag/Indices/operation/getTask) and this `taskID`.\n" - }, - "deletedAt": { - "type": "string", - "example": "2023-06-27T14:42:38.831Z", - "description": "Date and time when the object was deleted, in RFC 3339 format." - }, - "taskStatus": { - "type": "string", - "enum": ["published", "notPublished"], - "description": "Task status, `published` if the task is completed, `notPublished` otherwise." - }, - "parameters_query": { "type": "string", "description": "Search query.", "default": "" }, - "parameters_page": { - "type": "integer", - "minimum": 0, - "description": "Requested page of the API response." - }, - "parameters_hitsPerPage": { - "type": "integer", - "default": 20, - "minimum": 1, - "maximum": 1000, - "description": "Maximum number of hits per page." - } - }, - "responses": { - "BadRequest": { - "description": "Bad request or request arguments.", - "content": { - "application/json": { "schema": { "$ref": "#/components/schemas/ErrorBase" } } - } - }, - "FeatureNotEnabled": { - "description": "This feature is not enabled on your Algolia account.", - "content": { - "application/json": { "schema": { "$ref": "#/components/schemas/ErrorBase" } } - } - }, - "MethodNotAllowed": { - "description": "Method not allowed with this API key.", - "content": { - "application/json": { "schema": { "$ref": "#/components/schemas/ErrorBase" } } - } - }, - "IndexNotFound": { - "description": "Index not found.", - "content": { - "application/json": { "schema": { "$ref": "#/components/schemas/ErrorBase" } } - } - }, - "DeletedAt": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "title": "deletedAtResponse", - "description": "Response, taskID, and deletion timestamp.", - "additionalProperties": false, - "type": "object", - "required": ["taskID", "deletedAt"], - "properties": { - "taskID": { "$ref": "#/components/schemas/taskID" }, - "deletedAt": { "$ref": "#/components/schemas/deletedAt" } - } - } - } - } - } - } - } -} +openapi: 3.0.2 +info: + title: Recommend API + description: > + The Recommend API lets you retrieve recommendations from one of Algolia's AI recommendation models that you previously trained on your data. + + + ## Client libraries + + + Use Algolia's API clients and libraries to reliably integrate Algolia's APIs with your apps. + + The official API clients are covered by Algolia's [Service Level Agreement](https://www.algolia.com/policies/sla/). + + + See: [Algolia's ecosystem](https://www.algolia.com/doc/guides/getting-started/how-algolia-works/in-depth/ecosystem/) + + + ## Base URLs + + + The base URLs for requests to the Recommend API are: + + + - `https://{APPLICATION_ID}.algolia.net` + + - `https://{APPLICATION_ID}-dsn.algolia.net`. + If your subscription includes a [Distributed Search Network](https://dashboard.algolia.com/infra), + this ensures that requests are sent to servers closest to users. + + Both URLs provide high availability by distributing requests with load balancing. + + + **All requests must use HTTPS.** + + + ## Retry strategy + + + To guarantee a high availability, implement a retry strategy for all API requests using the URLs of your servers as fallbacks: + + + - `https://{APPLICATION_ID}-1.algolianet.com` + + - `https://{APPLICATION_ID}-2.algolianet.com` + + - `https://{APPLICATION_ID}-3.algolianet.com` + + + These URLs use a different DNS provider than the primary URLs. + + You should randomize this list to ensure an even load across the three servers. + + + All Algolia API clients implement this retry strategy. + + + ## Authentication + + + To authenticate your API requests, add these headers: + + + - `x-algolia-application-id`. Your Algolia application ID. + + - `x-algolia-api-key`. An API key with the necessary permissions to make the request. + The required access control list (ACL) to make a request is listed in each endpoint's reference. + + You can find your application ID and API key in the [Algolia dashboard](https://dashboard.algolia.com/account). + + + ## Request format + + + Request bodies must be JSON objects. + + + ## Response status and errors + + + The Recommend API returns JSON responses. + + Since JSON doesn't guarantee any specific ordering, don't rely on the order of attributes in the API response. + + + Successful responses return a `2xx` status. Client errors return a `4xx` status. Server errors are indicated by a `5xx` status. + + Error responses have a `message` property with more information. + + + ## Version + + + The current version of the Recommend API is version 1, as indicated by the `/1/` in each endpoint's URL. + + version: 1.0.0 +paths: + /1/indexes/*/recommendations: + post: + tags: + - recommend + operationId: getRecommendations + x-use-read-transporter: true + x-cacheable: true + x-acl: + - search + x-legacy-signature-recommend: true + summary: Retrieve recommendations + description: | + Retrieves recommendations from selected AI models. + requestBody: + required: true + content: + application/json: + schema: + title: getRecommendationsParams + description: Recommend request body. + type: object + additionalProperties: false + properties: + requests: + type: array + description: >- + Recommendation request with parameters depending on the requested model. + + items: + $ref: '#/components/schemas/recommendationsRequest' + required: + - requests + responses: + '200': + description: OK + content: + application/json: + schema: + title: getRecommendationsResponse + type: object + additionalProperties: false + properties: + results: + type: array + items: + $ref: '#/components/schemas/recommendationsResults' + required: + - results + '400': + $ref: '#/components/responses/BadRequest' + '402': + $ref: '#/components/responses/FeatureNotEnabled' + '403': + $ref: '#/components/responses/MethodNotAllowed' + '404': + $ref: '#/components/responses/IndexNotFound' +components: + securitySchemes: + appId: + type: apiKey + in: header + name: x-algolia-application-id + description: Your Algolia application ID. + apiKey: + type: apiKey + in: header + name: x-algolia-api-key + description: > + Your Algolia API key with the necessary permissions to make the request. + + Permissions are controlled through access control lists (ACL) and access restrictions. + + The required ACL to make a request is listed in each endpoint's reference. + + parameters: + PathInPath: + name: path + in: path + description: Path of the endpoint, anything after "/1" must be specified. + required: true + schema: + type: string + example: /keys + Parameters: + name: parameters + in: query + description: Query parameters to apply to the current query. + schema: + type: object + additionalProperties: true + IndexName: + name: indexName + in: path + description: Name of the index on which to perform the operation. + required: true + schema: + type: string + example: YourIndexName + Models: + in: path + name: model + required: true + description: > + [Recommend model](https://www.algolia.com/doc/guides/algolia-recommend/overview/#recommend-models). + + schema: + $ref: '#/components/schemas/recommendModels' + ObjectID: + name: objectID + in: path + description: Unique record identifier. + required: true + schema: + $ref: '#/components/schemas/objectID' + schemas: + ErrorBase: + description: Error. + type: object + x-keep-model: true + additionalProperties: true + properties: + message: + type: string + example: Invalid Application-Id or API-Key + indexName: + type: string + example: products + description: Index name (case-sensitive). + query: + type: string + description: Search query. + default: '' + x-categories: + - Search + searchParamsQuery: + type: object + additionalProperties: false + properties: + query: + $ref: '#/components/schemas/query' + filters: + type: string + description: > + Filter expression to only include items that match the filter criteria in the response. + + + You can use these filter expressions: + + + - **Numeric filters.** ` `, where `` is one of `<`, `<=`, `=`, `!=`, `>`, `>=`. + + - **Ranges.** `: TO ` where `` and `` are the lower and upper limits of the range (inclusive). + + - **Facet filters.** `:` where `` is a facet attribute (case-sensitive) and `` a facet value. + + - **Tag filters.** `_tags:` or just `` (case-sensitive). + + - **Boolean filters.** `: true | false`. + + + You can combine filters with `AND`, `OR`, and `NOT` operators with the following restrictions: + + + - You can only combine filters of the same type with `OR`. + **Not supported:** `facet:value OR num > 3`. + - You can't use `NOT` with combinations of filters. + **Not supported:** `NOT(facet:value OR facet:value)` + - You can't combine conjunctions (`AND`) with `OR`. + **Not supported:** `facet:value OR (facet:value AND facet:value)` + + Use quotes around your filters, if the facet attribute name or facet value has spaces, keywords (`OR`, `AND`, `NOT`), or quotes. + + If a facet attribute is an array, the filter matches if it matches at least one element of the array. + + + For more information, see [Filters](https://www.algolia.com/doc/guides/managing-results/refine-results/filtering/). + + example: (category:Book OR category:Ebook) AND _tags:published + x-categories: + - Filtering + facetFilters: + description: > + Filter the search by facet values, so that only records with the same facet values are retrieved. + + + **Prefer using the `filters` parameter, which supports all filter types and combinations with boolean operators.** + + + - `[filter1, filter2]` is interpreted as `filter1 AND filter2`. + + - `[[filter1, filter2], filter3]` is interpreted as `filter1 OR filter2 AND filter3`. + + - `facet:-value` is interpreted as `NOT facet:value`. + + + While it's best to avoid attributes that start with a `-`, you can still filter them by escaping with a backslash: + + `facet:\-value`. + + example: + - - category:Book + - category:-Movie + - author:John Doe + oneOf: + - type: array + items: + $ref: '#/components/schemas/facetFilters' + - type: string + x-categories: + - Filtering + optionalFilters: + description: > + Filters to promote or demote records in the search results. + + + Optional filters work like facet filters, but they don't exclude records from the search results. + + Records that match the optional filter rank before records that don't match. + + If you're using a negative filter `facet:-value`, matching records rank after records that don't match. + + + - Optional filters don't work on virtual replicas. + + - Optional filters are applied _after_ sort-by attributes. + + - Optional filters don't work with numeric attributes. + + example: + - category:Book + - author:John Doe + oneOf: + - type: array + items: + $ref: '#/components/schemas/optionalFilters' + - type: string + x-categories: + - Filtering + numericFilters: + description: > + Filter by numeric facets. + + + **Prefer using the `filters` parameter, which supports all filter types and combinations with boolean operators.** + + + You can use numeric comparison operators: `<`, `<=`, `=`, `!=`, `>`, `>=`. Comparsions are precise up to 3 decimals. + + You can also provide ranges: `facet: TO `. The range includes the lower and upper boundaries. + + The same combination rules apply as for `facetFilters`. + + example: + - - inStock = 1 + - deliveryDate < 1441755506 + - price < 1000 + oneOf: + - type: array + items: + $ref: '#/components/schemas/numericFilters' + - type: string + x-categories: + - Filtering + tagFilters: + description: > + Filter the search by values of the special `_tags` attribute. + + + **Prefer using the `filters` parameter, which supports all filter types and combinations with boolean operators.** + + + Different from regular facets, `_tags` can only be used for filtering (including or excluding records). + + You won't get a facet count. + + The same combination and escaping rules apply as for `facetFilters`. + + example: + - - Book + - Movie + - SciFi + oneOf: + - type: array + items: + $ref: '#/components/schemas/tagFilters' + - type: string + x-categories: + - Filtering + page: + type: integer + description: Page of search results to retrieve. + default: 0 + minimum: 0 + x-categories: + - Pagination + aroundLatLng: + type: string + description: > + Coordinates for the center of a circle, expressed as a comma-separated string of latitude and longitude. + + + Only records included within circle around this central location are included in the results. + + The radius of the circle is determined by the `aroundRadius` and `minimumAroundRadius` settings. + + This parameter is ignored if you also specify `insidePolygon` or `insideBoundingBox`. + + example: 40.71,-74.01 + default: '' + x-categories: + - Geo-Search + aroundLatLngViaIP: + type: boolean + description: Whether to obtain the coordinates from the request's IP address. + default: false + x-categories: + - Geo-Search + aroundRadiusAll: + title: all + type: string + description: >- + Return all records with a valid `_geoloc` attribute. Don't filter by distance. + + enum: + - all + aroundRadius: + description: > + Maximum radius for a search around a central location. + + + This parameter works in combination with the `aroundLatLng` and `aroundLatLngViaIP` parameters. + + By default, the search radius is determined automatically from the density of hits around the central location. + + The search radius is small if there are many hits close to the central coordinates. + + oneOf: + - type: integer + minimum: 1 + description: Maximum search radius around a central location in meters. + - $ref: '#/components/schemas/aroundRadiusAll' + x-categories: + - Geo-Search + aroundPrecisionFromValue: + title: range objects + type: array + items: + title: range + type: object + description: >- + Range object with lower and upper values in meters to define custom ranges. + + properties: + from: + type: integer + description: >- + Lower boundary of a range in meters. The Geo ranking criterion considers all records within the range to be equal. + + example: 20 + value: + type: integer + description: >- + Upper boundary of a range in meters. The Geo ranking criterion considers all records within the range to be equal. + + aroundPrecision: + description: > + Precision of a coordinate-based search in meters to group results with similar distances. + + + The Geo ranking criterion considers all matches within the same range of distances to be equal. + + oneOf: + - type: integer + default: 10 + description: > + Distance in meters to group results by similar distances. + + + For example, if you set `aroundPrecision` to 100, records wihin 100 meters to the central coordinate are considered to have the same distance, + + as are records between 100 and 199 meters. + + - $ref: '#/components/schemas/aroundPrecisionFromValue' + x-categories: + - Geo-Search + insideBoundingBox: + type: array + items: + type: array + minItems: 4 + maxItems: 4 + items: + type: number + format: double + description: > + Coordinates for a rectangular area in which to search. + + + Each bounding box is defined by the two opposite points of its diagonal, and expressed as latitude and longitude pair: + + `[p1 lat, p1 long, p2 lat, p2 long]`. + + Provide multiple bounding boxes as nested arrays. + + For more information, see [rectangular area](https://www.algolia.com/doc/guides/managing-results/refine-results/geolocation/#filtering-inside-rectangular-or-polygonal-areas). + + example: + - - 47.3165 + - 4.9665 + - 47.3424 + - 5.0201 + - - 40.9234 + - 2.1185 + - 38.643 + - 1.9916 + x-categories: + - Geo-Search + insidePolygon: + type: array + items: + type: array + minItems: 6 + maxItems: 20000 + items: + type: number + format: double + description: > + Coordinates of a polygon in which to search. + + + Polygons are defined by 3 to 10,000 points. Each point is represented by its latitude and longitude. + + Provide multiple polygons as nested arrays. + + For more information, see [filtering inside polygons](https://www.algolia.com/doc/guides/managing-results/refine-results/geolocation/#filtering-inside-rectangular-or-polygonal-areas). + + This parameter is ignored if you also specify `insideBoundingBox`. + + example: + - - 47.3165 + - 4.9665 + - 47.3424 + - 5.0201 + - 47.32 + - 4.9 + - - 40.9234 + - 2.1185 + - 38.643 + - 1.9916 + - 39.2587 + - 2.0104 + x-categories: + - Geo-Search + supportedLanguage: + type: string + description: ISO code for a supported language. + enum: + - af + - ar + - az + - bg + - bn + - ca + - cs + - cy + - da + - de + - el + - en + - eo + - es + - et + - eu + - fa + - fi + - fo + - fr + - ga + - gl + - he + - hi + - hu + - hy + - id + - is + - it + - ja + - ka + - kk + - ko + - ku + - ky + - lt + - lv + - mi + - mn + - mr + - ms + - mt + - nb + - nl + - 'no' + - ns + - pl + - ps + - pt + - pt-br + - qu + - ro + - ru + - sk + - sq + - sv + - sw + - ta + - te + - th + - tl + - tn + - tr + - tt + - uk + - ur + - uz + - zh + userToken: + type: string + description: > + Unique pseudonymous or anonymous user identifier. + + + This helps with analytics and click and conversion events. + + For more information, see [user token](https://www.algolia.com/doc/guides/sending-events/concepts/usertoken/). + + example: test-user-123 + x-categories: + - Personalization + baseSearchParamsWithoutQuery: + type: object + additionalProperties: false + properties: + similarQuery: + type: string + description: > + Keywords to be used instead of the search query to conduct a more broader search. + + + Using the `similarQuery` parameter changes other settings: + + + - `queryType` is set to `prefixNone`. + + - `removeStopWords` is set to true. + + - `words` is set as the first ranking criterion. + + - All remaining words are treated as `optionalWords`. + + + Since the `similarQuery` is supposed to do a broad search, they usually return many results. + + Combine it with `filters` to narrow down the list of results. + + default: '' + example: comedy drama crime Macy Buscemi + x-categories: + - Search + filters: + $ref: '#/components/schemas/filters' + facetFilters: + $ref: '#/components/schemas/facetFilters' + optionalFilters: + $ref: '#/components/schemas/optionalFilters' + numericFilters: + $ref: '#/components/schemas/numericFilters' + tagFilters: + $ref: '#/components/schemas/tagFilters' + sumOrFiltersScores: + type: boolean + description: > + Whether to sum all filter scores. + + + If true, all filter scores are summed. + + Otherwise, the maximum filter score is kept. + + For more information, see [filter scores](https://www.algolia.com/doc/guides/managing-results/refine-results/filtering/in-depth/filter-scoring/#accumulating-scores-with-sumorfiltersscores). + + default: false + x-categories: + - Filtering + restrictSearchableAttributes: + type: array + items: + type: string + example: + - title + - author + description: | + Restricts a search to a subset of your searchable attributes. + Attribute names are case-sensitive. + default: [] + x-categories: + - Filtering + facets: + type: array + items: + type: string + description: > + Facets for which to retrieve facet values that match the search criteria and the number of matching facet values. + + + To retrieve all facets, use the wildcard character `*`. + + For more information, see [facets](https://www.algolia.com/doc/guides/managing-results/refine-results/faceting/#contextual-facet-values-and-counts). + + default: [] + example: + - '*' + x-categories: + - Faceting + facetingAfterDistinct: + type: boolean + description: > + Whether faceting should be applied after deduplication with `distinct`. + + + This leads to accurate facet counts when using faceting in combination with `distinct`. + + It's usually better to use `afterDistinct` modifiers in the `attributesForFaceting` setting, + + as `facetingAfterDistinct` only computes correct facet counts if all records have the same facet values for the `attributeForDistinct`. + + default: false + x-categories: + - Faceting + page: + $ref: '#/components/schemas/page' + offset: + type: integer + description: Position of the first hit to retrieve. + x-categories: + - Pagination + length: + type: integer + description: Number of hits to retrieve (used in combination with `offset`). + minimum: 0 + maximum: 1000 + x-categories: + - Pagination + aroundLatLng: + $ref: '#/components/schemas/aroundLatLng' + aroundLatLngViaIP: + $ref: '#/components/schemas/aroundLatLngViaIP' + aroundRadius: + $ref: '#/components/schemas/aroundRadius' + aroundPrecision: + $ref: '#/components/schemas/aroundPrecision' + minimumAroundRadius: + type: integer + description: >- + Minimum radius (in meters) for a search around a location when `aroundRadius` isn't set. + + minimum: 1 + x-categories: + - Geo-Search + insideBoundingBox: + $ref: '#/components/schemas/insideBoundingBox' + insidePolygon: + $ref: '#/components/schemas/insidePolygon' + naturalLanguages: + type: array + items: + $ref: '#/components/schemas/supportedLanguage' + description: > + ISO language codes that adjust settings that are useful for processing natural language queries (as opposed to keyword searches): + + + - Sets `removeStopWords` and `ignorePlurals` to the list of provided languages. + + - Sets `removeWordsIfNoResults` to `allOptional`. + + - Adds a `natural_language` attribute to `ruleContexts` and `analyticsTags`. + + default: [] + x-categories: + - Languages + ruleContexts: + type: array + items: + type: string + description: > + Assigns a rule context to the search query. + + + [Rule contexts](https://www.algolia.com/doc/guides/managing-results/rules/rules-overview/how-to/customize-search-results-by-platform/#whats-a-context) are strings that you can use to trigger matching rules. + + default: [] + example: + - mobile + x-categories: + - Rules + personalizationImpact: + type: integer + description: > + Impact that Personalization should have on this search. + + + The higher this value is, the more Personalization determines the ranking compared to other factors. + + For more information, see [Understanding Personalization impact](https://www.algolia.com/doc/guides/personalization/personalizing-results/in-depth/configuring-personalization/#understanding-personalization-impact). + + default: 100 + minimum: 0 + maximum: 100 + x-categories: + - Personalization + userToken: + $ref: '#/components/schemas/userToken' + getRankingInfo: + type: boolean + description: >- + Whether the search response should include detailed ranking information. + + default: false + x-categories: + - Advanced + synonyms: + type: boolean + description: Whether to take into account an index's synonyms for this search. + default: true + x-categories: + - Advanced + clickAnalytics: + type: boolean + description: > + Whether to include a `queryID` attribute in the response. + + + The query ID is a unique identifier for a search query and is required for tracking [click and conversion events](https://www.algolia.com/guides/sending-events/getting-started/). + + default: false + x-categories: + - Analytics + analytics: + type: boolean + description: Whether this search will be included in Analytics. + default: true + x-categories: + - Analytics + analyticsTags: + type: array + items: + type: string + description: >- + Tags to apply to the query for [segmenting analytics data](https://www.algolia.com/doc/guides/search-analytics/guides/segments/). + + default: [] + x-categories: + - Analytics + percentileComputation: + type: boolean + description: >- + Whether to include this search when calculating processing-time percentiles. + + default: true + x-categories: + - Advanced + enableABTest: + type: boolean + description: Whether to enable A/B testing for this search. + default: true + x-categories: + - Advanced + baseSearchParams: + allOf: + - $ref: '#/components/schemas/searchParamsQuery' + - $ref: '#/components/schemas/baseSearchParamsWithoutQuery' + hitsPerPage: + type: integer + description: Number of hits per page. + default: 20 + minimum: 1 + maximum: 1000 + x-categories: + - Pagination + typoToleranceEnum: + type: string + title: typo tolerance + description: | + - `min`. Return matches with the lowest number of typos. + For example, if you have matches without typos, only include those. + But if there are no matches without typos (with 1 typo), include matches with 1 typo (2 typos). + - `strict`. Return matches with the two lowest numbers of typos. + With `strict`, the Typo ranking criterion is applied first in the `ranking` setting. + enum: + - min + - strict + typoTolerance: + description: > + Whether [typo tolerance](https://www.algolia.com/doc/guides/managing-results/optimize-search-results/typo-tolerance/) is enabled and how it is applied. + + + If typo tolerance is true, `min`, or `strict`, [word splitting and concetenation](https://www.algolia.com/doc/guides/managing-results/optimize-search-results/handling-natural-languages-nlp/in-depth/splitting-and-concatenation/) is also active. + + oneOf: + - type: boolean + default: true + description: >- + Whether typo tolerance is active. If true, matches with typos are included in the search results and rank after exact matches. + + - $ref: '#/components/schemas/typoToleranceEnum' + x-categories: + - Typos + booleanString: + type: string + enum: + - 'true' + - 'false' + ignorePlurals: + description: | + Treat singular, plurals, and other forms of declensions as equivalent. + You should only use this feature for the languages used in your index. + example: + - ca + - es + oneOf: + - type: array + description: | + ISO code for languages for which this feature should be active. + This overrides languages you set with `queryLanguages`. + items: + $ref: '#/components/schemas/supportedLanguage' + - $ref: '#/components/schemas/booleanString' + - type: boolean + description: > + If true, `ignorePlurals` is active for all languages included in `queryLanguages`, or for all supported languages, if `queryLanguges` is empty. + + If false, singulars, plurals, and other declensions won't be considered equivalent. + + default: false + x-categories: + - Languages + removeStopWords: + description: > + Removes stop words from the search query. + + + Stop words are common words like articles, conjunctions, prepositions, or pronouns that have little or no meaning on their own. + + In English, "the", "a", or "and" are stop words. + + + You should only use this feature for the languages used in your index. + + example: + - ca + - es + oneOf: + - type: array + description: >- + ISO code for languages for which stop words should be removed. This overrides languages you set in `queryLanguges`. + + items: + $ref: '#/components/schemas/supportedLanguage' + - type: boolean + default: false + description: > + If true, stop words are removed for all languages you included in `queryLanguages`, or for all supported languages, if `queryLanguages` is empty. + + If false, stop words are not removed. + + x-categories: + - Languages + queryType: + type: string + enum: + - prefixLast + - prefixAll + - prefixNone + description: > + Determines if and how query words are interpreted as prefixes. + + + By default, only the last query word is treated as prefix (`prefixLast`). + + To turn off prefix search, use `prefixNone`. + + Avoid `prefixAll`, which treats all query words as prefixes. + + This might lead to counterintuitive results and makes your search slower. + + + For more information, see [Prefix searching](https://www.algolia.com/doc/guides/managing-results/optimize-search-results/override-search-engine-defaults/in-depth/prefix-searching/). + + default: prefixLast + x-categories: + - Query strategy + removeWordsIfNoResults: + type: string + enum: + - none + - lastWords + - firstWords + - allOptional + example: firstWords + description: > + Strategy for removing words from the query when it doesn't return any results. + + This helps to avoid returning empty search results. + + + - `none`. + No words are removed when a query doesn't return results. + + - `lastWords`. + Treat the last (then second to last, then third to last) word as optional, + until there are results or at most 5 words have been removed. + + - `firstWords`. + Treat the first (then second, then third) word as optional, + until there are results or at most 5 words have been removed. + + - `allOptional`. + Treat all words as optional. + + For more information, see [Remove words to improve results](https://www.algolia.com/doc/guides/managing-results/optimize-search-results/empty-or-insufficient-results/in-depth/why-use-remove-words-if-no-results/). + + default: none + x-categories: + - Query strategy + mode: + type: string + enum: + - neuralSearch + - keywordSearch + description: > + Search mode the index will use to query for results. + + + This setting only applies to indices, for which Algolia enabled NeuralSearch for you. + + default: keywordSearch + x-categories: + - Query strategy + semanticSearch: + type: object + description: | + Settings for the semantic search part of NeuralSearch. + Only used when `mode` is `neuralSearch`. + properties: + eventSources: + oneOf: + - type: array + description: | + Indices from which to collect click and conversion events. + + If null, the current index and all its replicas are used. + items: + type: string + - type: 'null' + exactOnSingleWordQuery: + type: string + enum: + - attribute + - none + - word + description: > + Determines how the [Exact ranking criterion](https://www.algolia.com/doc/guides/managing-results/optimize-search-results/override-search-engine-defaults/in-depth/adjust-exact-settings/#turn-off-exact-for-some-attributes) is computed when the search query has only one word. + + + - `attribute`. + The Exact ranking criterion is 1 if the query word and attribute value are the same. + For example, a search for "road" will match the value "road", but not "road trip". + + - `none`. + The Exact ranking criterion is ignored on single-word searches. + + - `word`. + The Exact ranking criterion is 1 if the query word is found in the attribute value. + The query word must have at least 3 characters and must not be a stop word. + Only exact matches will be highlighted, + partial and prefix matches won't. + + default: attribute + x-categories: + - Query strategy + alternativesAsExact: + type: string + enum: + - ignorePlurals + - singleWordSynonym + - multiWordsSynonym + x-categories: + - Query strategy + advancedSyntaxFeatures: + type: string + enum: + - exactPhrase + - excludeWords + x-categories: + - Query strategy + distinct: + description: > + Determines how many records of a group are included in the search results. + + + Records with the same value for the `attributeForDistinct` attribute are considered a group. + + The `distinct` setting controls how many members of the group are returned. + + This is useful for [deduplication and grouping](https://www.algolia.com/doc/guides/managing-results/refine-results/grouping/#introducing-algolias-distinct-feature). + + + The `distinct` setting is ignored if `attributeForDistinct` is not set. + + example: 1 + oneOf: + - type: boolean + description: >- + Whether deduplication is turned on. If true, only one member of a group is shown in the search results. + + - type: integer + description: > + Number of members of a group of records to include in the search results. + + + - Don't use `distinct > 1` for records that might be [promoted by rules](https://www.algolia.com/doc/guides/managing-results/rules/merchandising-and-promoting/how-to/promote-hits/). + The number of hits won't be correct and faceting won't work as expected. + - With `distinct > 1`, the `hitsPerPage` parameter controls the number of returned groups. + For example, with `hitsPerPage: 10` and `distinct: 2`, up to 20 records are returned. + Likewise, the `nbHits` response attribute contains the number of returned groups. + + minimum: 0 + maximum: 4 + default: 0 + x-categories: + - Advanced + maxFacetHits: + type: integer + description: >- + Maximum number of facet values to return when [searching for facet values](https://www.algolia.com/doc/guides/managing-results/refine-results/faceting/#search-for-facet-values). + + maximum: 100 + default: 10 + x-categories: + - Advanced + order: + description: > + Explicit order of facets or facet values. + + + This setting lets you always show specific facets or facet values at the top of the list. + + type: array + items: + type: string + facets: + description: Order of facet names. + type: object + additionalProperties: false + properties: + order: + $ref: '#/components/schemas/order' + sortRemainingBy: + description: > + Order of facet values that aren't explicitly positioned with the `order` setting. + + + - `count`. + Order remaining facet values by decreasing count. + The count is the number of matching records containing this facet value. + + - `alpha`. + Sort facet values alphabetically. + + - `hidden`. + Don't show facet values that aren't explicitly positioned. + + type: string + enum: + - count + - alpha + - hidden + hide: + description: Hide facet values. + type: array + items: + type: string + value: + type: object + additionalProperties: false + properties: + order: + $ref: '#/components/schemas/order' + sortRemainingBy: + $ref: '#/components/schemas/sortRemainingBy' + hide: + $ref: '#/components/schemas/hide' + values: + description: Order of facet values. One object for each facet. + type: object + additionalProperties: + x-additionalPropertiesName: facet + $ref: '#/components/schemas/value' + facetOrdering: + description: Order of facet names and facet values in your UI. + type: object + additionalProperties: false + properties: + facets: + $ref: '#/components/schemas/facets' + values: + $ref: '#/components/schemas/values' + redirectURL: + description: The redirect rule container. + type: object + additionalProperties: false + properties: + url: + type: string + renderingContent: + description: > + Extra data that can be used in the search UI. + + + You can use this to control aspects of your search UI, such as, the order of facet names and values + + without changing your frontend code. + + type: object + additionalProperties: false + properties: + facetOrdering: + $ref: '#/components/schemas/facetOrdering' + redirect: + $ref: '#/components/schemas/redirectURL' + x-categories: + - Advanced + reRankingApplyFilter: + description: > + Restrict [Dynamic Re-Ranking](https://www.algolia.com/doc/guides/algolia-ai/re-ranking/) to records that match these filters. + + oneOf: + - type: array + items: + $ref: '#/components/schemas/reRankingApplyFilter' + - type: string + x-categories: + - Filtering + indexSettingsAsSearchParams: + type: object + additionalProperties: false + properties: + attributesToRetrieve: + type: array + items: + type: string + example: + - author + - title + - content + description: > + Attributes to include in the API response. + + + To reduce the size of your response, you can retrieve only some of the attributes. + + Attribute names are case-sensitive. + + + - `*` retrieves all attributes, except attributes included in the `customRanking` and `unretrievableAttributes` settings. + + - To retrieve all attributes except a specific one, prefix the attribute with a dash and combine it with the `*`: `["*", "-ATTRIBUTE"]`. + + - The `objectID` attribute is always included. + + default: + - '*' + x-categories: + - Attributes + ranking: + type: array + items: + type: string + description: > + Determines the order in which Algolia returns your results. + + + By default, each entry corresponds to a [ranking criteria](https://www.algolia.com/doc/guides/managing-results/relevance-overview/in-depth/ranking-criteria/). + + The tie-breaking algorithm sequentially applies each criterion in the order they're specified. + + If you configure a replica index for [sorting by an attribute](https://www.algolia.com/doc/guides/managing-results/refine-results/sorting/how-to/sort-by-attribute/), + + you put the sorting attribute at the top of the list. + + + **Modifiers** + + + - `asc("ATTRIBUTE")`. + Sort the index by the values of an attribute, in ascending order. + - `desc("ATTRIBUTE")`. + Sort the index by the values of an attribute, in descending order. + + Before you modify the default setting, + + you should test your changes in the dashboard, + + and by [A/B testing](https://www.algolia.com/doc/guides/ab-testing/what-is-ab-testing/). + + default: + - typo + - geo + - words + - filters + - proximity + - attribute + - exact + - custom + x-categories: + - Ranking + customRanking: + type: array + items: + type: string + example: + - desc(popularity) + - asc(price) + description: > + Attributes to use as [custom ranking](https://www.algolia.com/doc/guides/managing-results/must-do/custom-ranking/). + + Attribute names are case-sensitive. + + + The custom ranking attributes decide which items are shown first if the other ranking criteria are equal. + + + Records with missing values for your selected custom ranking attributes are always sorted last. + + Boolean attributes are sorted based on their alphabetical order. + + + **Modifiers** + + + - `asc("ATTRIBUTE")`. + Sort the index by the values of an attribute, in ascending order. + + - `desc("ATTRIBUTE")`. + Sort the index by the values of an attribute, in descending order. + + If you use two or more custom ranking attributes, + + [reduce the precision](https://www.algolia.com/doc/guides/managing-results/must-do/custom-ranking/how-to/controlling-custom-ranking-metrics-precision/) of your first attributes, + + or the other attributes will never be applied. + + default: [] + x-categories: + - Ranking + relevancyStrictness: + type: integer + example: 90 + description: > + Relevancy threshold below which less relevant results aren't included in the results. + + + You can only set `relevancyStrictness` on [virtual replica indices](https://www.algolia.com/doc/guides/managing-results/refine-results/sorting/in-depth/replicas/#what-are-virtual-replicas). + + Use this setting to strike a balance between the relevance and number of returned results. + + default: 100 + x-categories: + - Ranking + attributesToHighlight: + type: array + items: + type: string + example: + - author + - title + - conten + - content + description: > + Attributes to highlight. + + + By default, all searchable attributes are highlighted. + + Use `*` to highlight all attributes or use an empty array `[]` to turn off highlighting. + + Attribute names are case-sensitive. + + + With highlighting, strings that match the search query are surrounded by HTML tags defined by `highlightPreTag` and `highlightPostTag`. + + You can use this to visually highlight matching parts of a search query in your UI. + + + For more information, see [Highlighting and snippeting](https://www.algolia.com/doc/guides/building-search-ui/ui-and-ux-patterns/highlighting-snippeting/js/). + + x-categories: + - Highlighting and Snippeting + attributesToSnippet: + type: array + items: + type: string + example: + - content:80 + - description + description: > + Attributes for which to enable snippets. + + Attribute names are case-sensitive. + + + Snippets provide additional context to matched words. + + If you enable snippets, they include 10 words, including the matched word. + + The matched word will also be wrapped by HTML tags for highlighting. + + You can adjust the number of words with the following notation: `ATTRIBUTE:NUMBER`, + + where `NUMBER` is the number of words to be extracted. + + default: [] + x-categories: + - Highlighting and Snippeting + highlightPreTag: + type: string + description: >- + HTML tag to insert before the highlighted parts in all highlighted results and snippets. + + default: + x-categories: + - Highlighting and Snippeting + highlightPostTag: + type: string + description: >- + HTML tag to insert after the highlighted parts in all highlighted results and snippets. + + default: + x-categories: + - Highlighting and Snippeting + snippetEllipsisText: + type: string + description: String used as an ellipsis indicator when a snippet is truncated. + default: … + x-categories: + - Highlighting and Snippeting + restrictHighlightAndSnippetArrays: + type: boolean + description: > + Whether to restrict highlighting and snippeting to items that at least partially matched the search query. + + By default, all items are highlighted and snippeted. + + default: false + x-categories: + - Highlighting and Snippeting + hitsPerPage: + $ref: '#/components/schemas/hitsPerPage' + minWordSizefor1Typo: + type: integer + description: >- + Minimum number of characters a word in the search query must contain to accept matches with [one typo](https://www.algolia.com/doc/guides/managing-results/optimize-search-results/typo-tolerance/in-depth/configuring-typo-tolerance/#configuring-word-length-for-typos). + + default: 4 + x-categories: + - Typos + minWordSizefor2Typos: + type: integer + description: >- + Minimum number of characters a word in the search query must contain to accept matches with [two typos](https://www.algolia.com/doc/guides/managing-results/optimize-search-results/typo-tolerance/in-depth/configuring-typo-tolerance/#configuring-word-length-for-typos). + + default: 8 + x-categories: + - Typos + typoTolerance: + $ref: '#/components/schemas/typoTolerance' + allowTyposOnNumericTokens: + type: boolean + description: | + Whether to allow typos on numbers in the search query. + + Turn off this setting to reduce the number of irrelevant matches + when searching in large sets of similar numbers. + default: true + x-categories: + - Typos + disableTypoToleranceOnAttributes: + type: array + items: + type: string + example: + - sku + description: > + Attributes for which you want to turn off [typo tolerance](https://www.algolia.com/doc/guides/managing-results/optimize-search-results/typo-tolerance/). + + Attribute names are case-sensitive. + + + Returning only exact matches can help when: + + + - [Searching in hyphenated attributes](https://www.algolia.com/doc/guides/managing-results/optimize-search-results/typo-tolerance/how-to/how-to-search-in-hyphenated-attributes/). + + - Reducing the number of matches when you have too many. + This can happen with attributes that are long blocks of text, such as product descriptions. + + Consider alternatives such as `disableTypoToleranceOnWords` or adding synonyms if your attributes have intentional unusual spellings that might look like typos. + + default: [] + x-categories: + - Typos + ignorePlurals: + $ref: '#/components/schemas/ignorePlurals' + removeStopWords: + $ref: '#/components/schemas/removeStopWords' + keepDiacriticsOnCharacters: + type: string + example: øé + description: | + Characters for which diacritics should be preserved. + + By default, Algolia removes diacritics from letters. + For example, `é` becomes `e`. If this causes issues in your search, + you can specify characters that should keep their diacritics. + default: '' + x-categories: + - Languages + queryLanguages: + type: array + items: + $ref: '#/components/schemas/supportedLanguage' + example: + - es + description: > + Languages for language-specific query processing steps such as plurals, stop-word removal, and word-detection dictionaries. + + + This setting sets a default list of languages used by the `removeStopWords` and `ignorePlurals` settings. + + This setting also sets a dictionary for word detection in the logogram-based [CJK](https://www.algolia.com/doc/guides/managing-results/optimize-search-results/handling-natural-languages-nlp/in-depth/normalization/#normalization-for-logogram-based-languages-cjk) languages. + + To support this, you must place the CJK language **first**. + + + **You should always specify a query language.** + + If you don't specify an indexing language, the search engine uses all [supported languages](https://www.algolia.com/doc/guides/managing-results/optimize-search-results/handling-natural-languages-nlp/in-depth/supported-languages/), + + or the languages you specified with the `ignorePlurals` or `removeStopWords` parameters. + + This can lead to unexpected search results. + + For more information, see [Language-specific configuration](https://www.algolia.com/doc/guides/managing-results/optimize-search-results/handling-natural-languages-nlp/in-depth/language-specific-configurations/). + + default: [] + x-categories: + - Languages + decompoundQuery: + type: boolean + description: > + Whether to split compound words in the query into their building blocks. + + + For more information, see [Word segmentation](https://www.algolia.com/doc/guides/managing-results/optimize-search-results/handling-natural-languages-nlp/in-depth/language-specific-configurations/#splitting-compound-words). + + Word segmentation is supported for these languages: German, Dutch, Finnish, Swedish, and Norwegian. + + Decompounding doesn't work for words with [non-spacing mark Unicode characters](https://www.charactercodes.net/category/non-spacing_mark). + + For example, `Gartenstühle` won't be decompounded if the `ü` consists of `u` (U+0075) and `◌̈` (U+0308). + + default: true + x-categories: + - Languages + enableRules: + type: boolean + description: Whether to enable rules. + default: true + x-categories: + - Rules + enablePersonalization: + type: boolean + description: Whether to enable Personalization. + default: false + x-categories: + - Personalization + queryType: + $ref: '#/components/schemas/queryType' + removeWordsIfNoResults: + $ref: '#/components/schemas/removeWordsIfNoResults' + mode: + $ref: '#/components/schemas/mode' + semanticSearch: + $ref: '#/components/schemas/semanticSearch' + advancedSyntax: + type: boolean + description: > + Whether to support phrase matching and excluding words from search queries. + + + Use the `advancedSyntaxFeatures` parameter to control which feature is supported. + + default: false + x-categories: + - Query strategy + optionalWords: + type: array + items: + type: string + example: + - blue + - iphone case + description: > + Words that should be considered optional when found in the query. + + + By default, records must match all words in the search query to be included in the search results. + + Adding optional words can help to increase the number of search results by running an additional search query that doesn't include the optional words. + + For example, if the search query is "action video" and "video" is an optional word, + + the search engine runs two queries. One for "action video" and one for "action". + + Records that match all words are ranked higher. + + + For a search query with 4 or more words **and** all its words are optional, + + the number of matched words required for a record to be included in the search results increases for every 1,000 records: + + + - If `optionalWords` has less than 10 words, the required number of matched words increases by 1: + results 1 to 1,000 require 1 matched word, results 1,001 to 2000 need 2 matched words. + - If `optionalWords` has 10 or more words, the number of required matched words increases by the number of optional words dividied by 5 (rounded down). + For example, with 18 optional words: results 1 to 1,000 require 1 matched word, results 1,001 to 2000 need 4 matched words. + + For more information, see [Optional words](https://www.algolia.com/doc/guides/managing-results/optimize-search-results/empty-or-insufficient-results/#creating-a-list-of-optional-words). + + default: [] + x-categories: + - Query strategy + disableExactOnAttributes: + type: array + items: + type: string + example: + - description + description: > + Searchable attributes for which you want to [turn off the Exact ranking criterion](https://www.algolia.com/doc/guides/managing-results/optimize-search-results/override-search-engine-defaults/in-depth/adjust-exact-settings/#turn-off-exact-for-some-attributes). + + Attribute names are case-sensitive. + + + This can be useful for attributes with long values, where the likelyhood of an exact match is high, + + such as product descriptions. + + Turning off the Exact ranking criterion for these attributes favors exact matching on other attributes. + + This reduces the impact of individual attributes with a lot of content on ranking. + + default: [] + x-categories: + - Query strategy + exactOnSingleWordQuery: + $ref: '#/components/schemas/exactOnSingleWordQuery' + alternativesAsExact: + type: array + items: + $ref: '#/components/schemas/alternativesAsExact' + description: > + Alternatives of query words that should be considered as exact matches by the Exact ranking criterion. + + + - `ignorePlurals`. + Plurals and similar declensions added by the `ignorePlurals` setting are considered exact matches. + + - `singleWordSynonym`. + Single-word synonyms, such as "NY/NYC" are considered exact matches. + + - `multiWordsSynonym`. + Multi-word synonyms, such as "NY/New York" are considered exact matches. + + default: + - ignorePlurals + - singleWordSynonym + x-categories: + - Query strategy + advancedSyntaxFeatures: + type: array + items: + $ref: '#/components/schemas/advancedSyntaxFeatures' + description: | + Advanced search syntax features you want to support. + + - `exactPhrase`. + Phrases in quotes must match exactly. + For example, `sparkly blue "iPhone case"` only returns records with the exact string "iPhone case". + + - `excludeWords`. + Query words prefixed with a `-` must not occur in a record. + For example, `search -engine` matches records that contain "search" but not "engine". + + This setting only has an effect if `advancedSyntax` is true. + default: + - exactPhrase + - excludeWords + x-categories: + - Query strategy + distinct: + $ref: '#/components/schemas/distinct' + replaceSynonymsInHighlight: + type: boolean + description: > + Whether to replace a highlighted word with the matched synonym. + + + By default, the original words are highlighted even if a synonym matches. + + For example, with `home` as a synonym for `house` and a search for `home`, + + records matching either "home" or "house" are included in the search results, + + and either "home" or "house" are highlighted. + + + With `replaceSynonymsInHighlight` set to `true`, a search for `home` still matches the same records, + + but all occurences of "house" are replaced by "home" in the highlighted response. + + default: false + x-categories: + - Highlighting and Snippeting + minProximity: + type: integer + minimum: 1 + maximum: 7 + description: > + Minimum proximity score for two matching words. + + + This adjusts the [Proximity ranking criterion](https://www.algolia.com/doc/guides/managing-results/relevance-overview/in-depth/ranking-criteria/#proximity) + + by equally scoring matches that are farther apart. + + + For example, if `minProximity` is 2, neighboring matches and matches with one word between them would have the same score. + + default: 1 + x-categories: + - Advanced + responseFields: + type: array + items: + type: string + description: > + Properties to include in the API response of `search` and `browse` requests. + + + By default, all response properties are included. + + To reduce the response size, you can select, which attributes should be included. + + + You can't exclude these properties: + + `message`, `warning`, `cursor`, `serverUsed`, `indexUsed`, + + `abTestVariantID`, `parsedQuery`, or any property triggered by the `getRankingInfo` parameter. + + + Don't exclude properties that you might need in your search UI. + + default: + - '*' + x-categories: + - Advanced + maxFacetHits: + $ref: '#/components/schemas/maxFacetHits' + maxValuesPerFacet: + type: integer + description: Maximum number of facet values to return for each facet. + default: 100 + maximum: 1000 + x-categories: + - Faceting + sortFacetValuesBy: + type: string + description: > + Order in which to retrieve facet values. + + + - `count`. + Facet values are retrieved by decreasing count. + The count is the number of matching records containing this facet value. + + - `alpha`. + Retrieve facet values alphabetically. + + This setting doesn't influence how facet values are displayed in your UI (see `renderingContent`). + + For more information, see [facet value display](https://www.algolia.com/doc/guides/building-search-ui/ui-and-ux-patterns/facet-display/js/). + + default: count + x-categories: + - Faceting + attributeCriteriaComputedByMinProximity: + type: boolean + description: > + Whether the best matching attribute should be determined by minimum proximity. + + + This setting only affects ranking if the Attribute ranking criterion comes before Proximity in the `ranking` setting. + + If true, the best matching attribute is selected based on the minimum proximity of multiple matches. + + Otherwise, the best matching attribute is determined by the order in the `searchableAttributes` setting. + + default: false + x-categories: + - Advanced + renderingContent: + $ref: '#/components/schemas/renderingContent' + enableReRanking: + type: boolean + description: > + Whether this search will use [Dynamic Re-Ranking](https://www.algolia.com/doc/guides/algolia-ai/re-ranking/). + + + This setting only has an effect if you activated Dynamic Re-Ranking for this index in the Algolia dashboard. + + default: true + x-categories: + - Filtering + reRankingApplyFilter: + oneOf: + - $ref: '#/components/schemas/reRankingApplyFilter' + - type: 'null' + searchParamsObject: + title: Search parameters as object + description: >- + Each parameter value, including the `query` must not be larger than 512 bytes. + + allOf: + - $ref: '#/components/schemas/baseSearchParams' + - $ref: '#/components/schemas/indexSettingsAsSearchParams' + searchParams: + title: queryParameters + allOf: + - $ref: '#/components/schemas/searchParamsObject' + - type: object + description: Search parameters for filtering the recommendations. + baseRecommendRequest: + type: object + additionalProperties: false + properties: + indexName: + $ref: '#/components/schemas/indexName' + threshold: + type: number + format: double + minimum: 0 + maximum: 100 + description: >- + Minimum score a recommendation must have to be included in the response. + + maxRecommendations: + type: integer + minimum: 1 + maximum: 1000 + default: 30 + description: > + Maximum number of recommendations to retrieve. + + By default, all recommendations are returned and no fallback request is made. + + Depending on the available recommendations and the other request parameters, + + the actual number of recommendations may be lower than this value. + + queryParameters: + $ref: '#/components/schemas/searchParams' + required: + - indexName + - threshold + fbtModel: + type: string + description: > + Frequently bought together model. + + + This model recommends items that have been purchased within 1 day with the item with the ID `objectID`. + + enum: + - bought-together + objectID: + type: string + description: Unique record identifier. + example: test-record-123 + frequentlyBoughtTogether: + type: object + properties: + model: + $ref: '#/components/schemas/fbtModel' + objectID: + $ref: '#/components/schemas/objectID' + required: + - model + - objectID + boughtTogetherQuery: + title: Frequently bought together + allOf: + - $ref: '#/components/schemas/baseRecommendRequest' + - $ref: '#/components/schemas/frequentlyBoughtTogether' + relatedModel: + type: string + description: > + Related products or similar content model. + + + This model recommends items that are similar to the item with the ID `objectID`. + + Similarity is determined from the user interactions and attributes. + + enum: + - related-products + fallbackParams: + title: fallbackParameters + allOf: + - $ref: '#/components/schemas/searchParamsObject' + - type: object + description: >- + Search parameters to use for a fallback request if there aren't enough recommendations. + + relatedProducts: + type: object + properties: + model: + $ref: '#/components/schemas/relatedModel' + objectID: + $ref: '#/components/schemas/objectID' + fallbackParameters: + $ref: '#/components/schemas/fallbackParams' + required: + - model + - objectID + relatedQuery: + title: Related products + allOf: + - $ref: '#/components/schemas/baseRecommendRequest' + - $ref: '#/components/schemas/relatedProducts' + facetName: + type: string + description: > + Facet attribute. To be used in combination with `facetValue`. + + If specified, only recommendations matching the facet filter will be returned. + + facetValue: + type: string + description: > + Facet value. To be used in combination with `facetName`. + + If specified, only recommendations matching the facet filter will be returned. + + trendingItemsModel: + description: > + Trending items model. + + + Trending items are determined from the number of conversion events collected on them. + + type: string + enum: + - trending-items + trendingItems: + type: object + properties: + facetName: + $ref: '#/components/schemas/facetName' + facetValue: + $ref: '#/components/schemas/facetValue' + model: + $ref: '#/components/schemas/trendingItemsModel' + fallbackParameters: + $ref: '#/components/schemas/searchParamsObject' + required: + - model + trendingItemsQuery: + title: Trending items + allOf: + - $ref: '#/components/schemas/baseRecommendRequest' + - $ref: '#/components/schemas/trendingItems' + trendingFacetsModel: + type: string + description: > + Trending facet values model. + + + This model recommends trending facet values for the specified facet attribute. + + enum: + - trending-facets + trendingFacets: + type: object + properties: + facetName: + description: Facet attribute for which to retrieve trending facet values. + model: + $ref: '#/components/schemas/trendingFacetsModel' + fallbackParameters: + $ref: '#/components/schemas/fallbackParams' + required: + - facetName + - model + trendingFacetsQuery: + title: Trending facet values + allOf: + - $ref: '#/components/schemas/baseRecommendRequest' + - $ref: '#/components/schemas/trendingFacets' + lookingSimilarModel: + type: string + description: > + Looking similar model. + + + This model recommends items that look similar to the item with the ID `objectID` based on image attributes in your index. + + enum: + - looking-similar + lookingSimilar: + type: object + properties: + model: + $ref: '#/components/schemas/lookingSimilarModel' + objectID: + $ref: '#/components/schemas/objectID' + fallbackParameters: + $ref: '#/components/schemas/fallbackParams' + required: + - model + - objectID + lookingSimilarQuery: + title: Looking similar + allOf: + - $ref: '#/components/schemas/baseRecommendRequest' + - $ref: '#/components/schemas/lookingSimilar' + recommendedForYouModel: + type: string + description: | + "Recommened for you" model. + enum: + - recommended-for-you + recommendedForYou: + type: object + properties: + model: + $ref: '#/components/schemas/recommendedForYouModel' + fallbackParameters: + $ref: '#/components/schemas/fallbackParams' + required: + - model + recommendedForYouQuery: + title: Recommended for you + allOf: + - $ref: '#/components/schemas/baseRecommendRequest' + - $ref: '#/components/schemas/recommendedForYou' + recommendationsRequest: + oneOf: + - $ref: '#/components/schemas/boughtTogetherQuery' + - $ref: '#/components/schemas/relatedQuery' + - $ref: '#/components/schemas/trendingItemsQuery' + - $ref: '#/components/schemas/trendingFacetsQuery' + - $ref: '#/components/schemas/lookingSimilarQuery' + - $ref: '#/components/schemas/recommendedForYouQuery' + processingTimeMS: + type: integer + description: Time the server took to process the request, in milliseconds. + example: 20 + RedirectRuleIndexMetadata: + type: object + properties: + source: + type: string + description: Source index for the redirect rule. + dest: + type: string + description: Destination index for the redirect rule. + reason: + type: string + description: Reason for the redirect rule. + succeed: + type: boolean + description: Redirect rule status. + data: + title: redirectRuleIndexData + type: object + description: Redirect rule data. + required: + - ruleObjectID + properties: + ruleObjectID: + type: string + required: + - data + - succeed + - reason + - dest + - source + userData: + type: object + example: + settingID: f2a7b51e3503acc6a39b3784ffb84300 + pluginVersion: 1.6.0 + description: | + An object with custom data. + + You can store up to 32kB as custom data. + default: {} + x-categories: + - Advanced + baseSearchResponse: + type: object + additionalProperties: true + required: + - processingTimeMS + properties: + abTestID: + type: integer + description: >- + A/B test ID. This is only included in the response for indices that are part of an A/B test. + + abTestVariantID: + type: integer + minimum: 1 + description: >- + Variant ID. This is only included in the response for indices that are part of an A/B test. + + aroundLatLng: + type: string + description: Computed geographical location. + example: 40.71,-74.01 + pattern: ^(-?\d+(\.\d+)?),\s*(-?\d+(\.\d+)?)$ + automaticRadius: + type: string + description: Distance from a central coordinate provided by `aroundLatLng`. + exhaustive: + title: exhaustive + type: object + description: >- + Whether certain properties of the search response are calculated exhaustive (exact) or approximated. + + properties: + facetsCount: + type: boolean + title: facetsCount + description: >- + Whether the facet count is exhaustive (`true`) or approximate (`false`). See the [related discussion](https://support.algolia.com/hc/en-us/articles/4406975248145-Why-are-my-facet-and-hit-counts-not-accurate-). + + facetValues: + type: boolean + title: facetValues + description: The value is `false` if not all facet values are retrieved. + nbHits: + type: boolean + title: nbHits + description: >- + Whether the `nbHits` is exhaustive (`true`) or approximate (`false`). When the query takes more than 50ms to be processed, the engine makes an approximation. This can happen when using complex filters on millions of records, when typo-tolerance was not exhaustive, or when enough hits have been retrieved (for example, after the engine finds 10,000 exact matches). `nbHits` is reported as non-exhaustive whenever an approximation is made, even if the approximation didn’t, in the end, impact the exhaustivity of the query. + + rulesMatch: + type: boolean + title: rulesMatch + description: >- + Rules matching exhaustivity. The value is `false` if rules were enable for this query, and could not be fully processed due a timeout. This is generally caused by the number of alternatives (such as typos) which is too large. + + typo: + type: boolean + title: typo + description: >- + Whether the typo search was exhaustive (`true`) or approximate (`false`). An approximation is done when the typo search query part takes more than 10% of the query budget (ie. 5ms by default) to be processed (this can happen when a lot of typo alternatives exist for the query). This field will not be included when typo-tolerance is entirely disabled. + + exhaustiveFacetsCount: + type: boolean + description: >- + See the `facetsCount` field of the `exhaustive` object in the response. + + deprecated: true + exhaustiveNbHits: + type: boolean + description: See the `nbHits` field of the `exhaustive` object in the response. + deprecated: true + exhaustiveTypo: + type: boolean + description: See the `typo` field of the `exhaustive` object in the response. + deprecated: true + facets: + title: facets + type: object + additionalProperties: + x-additionalPropertiesName: facet + type: object + additionalProperties: + x-additionalPropertiesName: facet count + type: integer + description: Facet counts. + example: + category: + food: 1 + tech: 42 + facets_stats: + type: object + description: Statistics for numerical facets. + additionalProperties: + title: facetStats + type: object + properties: + min: + type: number + format: double + description: Minimum value in the results. + max: + type: number + format: double + description: Maximum value in the results. + avg: + type: number + format: double + description: Average facet value in the results. + sum: + type: number + format: double + description: Sum of all values in the results. + index: + type: string + example: indexName + description: Index name used for the query. + indexUsed: + type: string + description: >- + Index name used for the query. During A/B testing, the targeted index isn't always the index used by the query. + + example: indexNameAlt + message: + type: string + description: Warnings about the query. + nbSortedHits: + type: integer + description: Number of hits selected and sorted by the relevant sort algorithm. + example: 20 + parsedQuery: + type: string + description: >- + Post-[normalization](https://www.algolia.com/doc/guides/managing-results/optimize-search-results/handling-natural-languages-nlp/#what-does-normalization-mean) query string that will be searched. + + example: george clo + processingTimeMS: + $ref: '#/components/schemas/processingTimeMS' + processingTimingsMS: + type: object + description: >- + Experimental. List of processing steps and their times, in milliseconds. You can use this list to investigate performance issues. + + queryAfterRemoval: + type: string + description: >- + Markup text indicating which parts of the original query have been removed to retrieve a non-empty result set. + + redirect: + title: redirect + type: object + description: > + [Redirect results to a URL](https://www.algolia.com/doc/guides/managing-results/rules/merchandising-and-promoting/how-to/redirects/), this this parameter is for internal use only. + + properties: + index: + type: array + items: + $ref: '#/components/schemas/RedirectRuleIndexMetadata' + renderingContent: + $ref: '#/components/schemas/renderingContent' + serverTimeMS: + type: integer + description: Time the server took to process the request, in milliseconds. + example: 20 + serverUsed: + type: string + description: Host name of the server that processed the request. + example: c2-uk-3.algolia.net + userData: + $ref: '#/components/schemas/userData' + queryID: + type: string + description: >- + Unique identifier for the query. This is used for [click analytics](https://www.algolia.com/doc/guides/analytics/click-analytics/). + + example: a00dbc80a8d13c4565a442e7e2dca80a + _automaticInsights: + type: boolean + description: Whether automatic events collection is enabled for the application. + nbHits: + type: integer + description: Number of results (hits). + example: 20 + nbPages: + type: integer + description: Number of pages of results. + example: 1 + SearchPagination: + type: object + additionalProperties: false + properties: + page: + $ref: '#/components/schemas/page' + nbHits: + $ref: '#/components/schemas/nbHits' + nbPages: + $ref: '#/components/schemas/nbPages' + hitsPerPage: + $ref: '#/components/schemas/hitsPerPage' + required: + - page + - nbHits + - nbPages + - hitsPerPage + highlightResultMap: + type: object + description: Surround words that match the query with HTML tags for highlighting. + additionalProperties: + x-additionalPropertiesName: attribute + $ref: '#/components/schemas/highlightResult' + highlightedValue: + type: string + description: Highlighted attribute value, including HTML tags. + example: George Clooney + matchLevel: + type: string + description: Whether the whole query string matches or only a part. + enum: + - none + - partial + - full + highlightResultOption: + type: object + description: Surround words that match the query with HTML tags for highlighting. + additionalProperties: false + properties: + value: + $ref: '#/components/schemas/highlightedValue' + matchLevel: + $ref: '#/components/schemas/matchLevel' + matchedWords: + type: array + description: List of matched words from the search query. + example: + - action + items: + type: string + fullyHighlighted: + type: boolean + description: Whether the entire attribute value is highlighted. + required: + - value + - matchLevel + - matchedWords + x-discriminator-fields: + - matchLevel + - matchedWords + highlightResultOptionMap: + type: object + description: Surround words that match the query with HTML tags for highlighting. + additionalProperties: + x-additionalPropertiesName: attribute + $ref: '#/components/schemas/highlightResultOption' + highlightResultOptionArray: + type: array + description: Surround words that match the query with HTML tags for highlighting. + items: + $ref: '#/components/schemas/highlightResultOption' + highlightResult: + oneOf: + - $ref: '#/components/schemas/highlightResultMap' + - $ref: '#/components/schemas/highlightResultOption' + - $ref: '#/components/schemas/highlightResultOptionMap' + - $ref: '#/components/schemas/highlightResultOptionArray' + snippetResultMap: + type: object + description: Snippets that show the context around a matching search query. + additionalProperties: + x-additionalPropertiesName: attribute + $ref: '#/components/schemas/snippetResult' + snippetResultOption: + type: object + description: Snippets that show the context around a matching search query. + additionalProperties: false + properties: + value: + $ref: '#/components/schemas/highlightedValue' + matchLevel: + $ref: '#/components/schemas/matchLevel' + required: + - value + - matchLevel + x-discriminator-fields: + - matchLevel + snippetResultOptionMap: + type: object + description: Snippets that show the context around a matching search query. + additionalProperties: + x-additionalPropertiesName: attribute + $ref: '#/components/schemas/snippetResultOption' + snippetResultOptionArray: + type: array + description: Snippets that show the context around a matching search query. + items: + $ref: '#/components/schemas/snippetResultOption' + snippetResult: + oneOf: + - $ref: '#/components/schemas/snippetResultMap' + - $ref: '#/components/schemas/snippetResultOption' + - $ref: '#/components/schemas/snippetResultOptionMap' + - $ref: '#/components/schemas/snippetResultOptionArray' + matchedGeoLocation: + type: object + properties: + lat: + type: number + format: double + description: Latitude of the matched location. + lng: + type: number + format: double + description: Longitude of the matched location. + distance: + type: integer + description: >- + Distance between the matched location and the search location (in meters). + + personalization: + type: object + properties: + filtersScore: + type: integer + description: The score of the filters. + rankingScore: + type: integer + description: The score of the ranking. + score: + type: integer + description: The score of the event. + rankingInfo: + type: object + description: Object with detailed information about the record's ranking. + additionalProperties: false + properties: + filters: + type: integer + minimum: 0 + description: Whether a filter matched the query. + firstMatchedWord: + type: integer + minimum: 0 + description: >- + Position of the first matched word in the best matching attribute of the record. + + geoDistance: + type: integer + minimum: 0 + description: >- + Distance between the geo location in the search query and the best matching geo location in the record, divided by the geo precision (in meters). + + geoPrecision: + type: integer + minimum: 1 + description: Precision used when computing the geo distance, in meters. + matchedGeoLocation: + $ref: '#/components/schemas/matchedGeoLocation' + personalization: + $ref: '#/components/schemas/personalization' + nbExactWords: + type: integer + minimum: 0 + description: Number of exactly matched words. + nbTypos: + type: integer + minimum: 0 + description: Number of typos encountered when matching the record. + promoted: + type: boolean + description: Whether the record was promoted by a rule. + proximityDistance: + type: integer + minimum: 0 + description: >- + Number of words between multiple matches in the query plus 1. For single word queries, `proximityDistance` is 0. + + userScore: + type: integer + description: >- + Overall ranking of the record, expressed as a single integer. This attribute is internal. + + words: + type: integer + minimum: 1 + description: Number of matched words. + promotedByReRanking: + type: boolean + description: Whether the record is re-ranked. + required: + - nbTypos + - firstMatchedWord + - geoDistance + - nbExactWords + - userScore + distinctSeqID: + type: integer + recommendScore: + type: number + format: double + minimum: 0 + maximum: 100 + description: Recommendation score. + recommendHit: + type: object + description: Recommend hit. + additionalProperties: true + required: + - objectID + - _score + properties: + objectID: + $ref: '#/components/schemas/objectID' + _highlightResult: + $ref: '#/components/schemas/highlightResultMap' + _snippetResult: + $ref: '#/components/schemas/snippetResultMap' + _rankingInfo: + $ref: '#/components/schemas/rankingInfo' + _distinctSeqID: + $ref: '#/components/schemas/distinctSeqID' + _score: + $ref: '#/components/schemas/recommendScore' + trendingFacetHit: + type: object + description: Trending facet hit. + required: + - _score + - facetName + - facetValue + properties: + _score: + $ref: '#/components/schemas/recommendScore' + facetName: + $ref: '#/components/schemas/facetName' + facetValue: + $ref: '#/components/schemas/facetValue' + recommendationsHit: + oneOf: + - $ref: '#/components/schemas/recommendHit' + - $ref: '#/components/schemas/trendingFacetHit' + recommendationsHits: + type: object + additionalProperties: false + properties: + hits: + type: array + items: + $ref: '#/components/schemas/recommendationsHit' + required: + - hits + recommendationsResults: + allOf: + - $ref: '#/components/schemas/baseSearchResponse' + - $ref: '#/components/schemas/SearchPagination' + - $ref: '#/components/schemas/recommendationsHits' + recommendModels: + type: string + enum: + - related-products + - bought-together + - trending-facets + - trending-items + updatedAt: + type: string + example: '2023-07-04T12:49:15Z' + description: Date and time when the object was updated, in RFC 3339 format. + ruleID: + title: objectID + type: string + description: Unique identifier of a rule object. + context: + type: string + pattern: '[A-Za-z0-9_-]+' + description: > + An additional restriction that only triggers the rule, when the search has the same value as `ruleContexts` parameter. + + For example, if `context: mobile`, the rule is only triggered when the search request has a matching `ruleContexts: mobile`. + + A rule context must only contain alphanumeric characters. + + example: mobile + Condition: + type: object + description: | + Condition that triggers the rule. + If not specified, the rule is triggered for all recommendations. + properties: + filters: + $ref: '#/components/schemas/filters' + context: + $ref: '#/components/schemas/context' + HideConsequenceObject: + type: object + description: Object ID of the recommendation you want to exclude. + properties: + objectID: + $ref: '#/components/schemas/objectID' + HideConsequence: + type: array + description: Exclude items from recommendations. + minItems: 1 + items: + $ref: '#/components/schemas/HideConsequenceObject' + PromoteConsequenceObject: + type: object + description: Object ID and position of the recommendation you want to pin. + properties: + objectID: + $ref: '#/components/schemas/objectID' + position: + type: integer + description: Index in the list of recommendations where to place this item. + minimum: 0 + PromoteConsequence: + type: array + description: Place items at specific positions in the list of recommendations. + minItems: 1 + items: + $ref: '#/components/schemas/PromoteConsequenceObject' + AutoFacetFilter: + type: object + description: >- + Facet attribute. Only recommendations with the same value (or only recommendations with a different value) as the original viewed item are included. + + properties: + facet: + type: string + description: Facet attribute. + negative: + type: boolean + description: > + Whether the filter is negative. + + If true, recommendations must not have the same value for the `facet` attribute. + + If false, recommendations must have the same value for the `facet` attribute. + + ParamsConsequence: + type: object + description: Filter or boost recommendations matching a facet filter. + properties: + automaticFacetFilters: + type: array + description: >- + Filter recommendations that match or don't match the same `facet:facet_value` combination as the viewed item. + + items: + $ref: '#/components/schemas/AutoFacetFilter' + filters: + $ref: '#/components/schemas/filters' + optionalFilters: + type: array + description: > + Filters to promote or demote records in the search results. + + + Optional filters work like facet filters, but they don't exclude records from the search results. + + Records that match the optional filter rank before records that don't match. + + Matches with higher weights (``) rank before matches with lower weights. + + If you're using a negative filter `facet:-value`, matching records rank after records that don't match. + + items: + type: string + example: + - category:books + - category:-movies + Consequence: + type: object + description: Effect of the rule. + properties: + hide: + $ref: '#/components/schemas/HideConsequence' + promote: + $ref: '#/components/schemas/PromoteConsequence' + params: + $ref: '#/components/schemas/ParamsConsequence' + RecommendRule: + type: object + description: Recommend rule. + additionalProperties: false + properties: + _metadata: + title: ruleMetadata + type: object + description: Rule metadata. + properties: + lastUpdate: + $ref: '#/components/schemas/updatedAt' + objectID: + $ref: '#/components/schemas/ruleID' + condition: + $ref: '#/components/schemas/Condition' + consequence: + $ref: '#/components/schemas/Consequence' + description: + type: string + description: >- + Description of the rule's purpose. This can be helpful for display in the Algolia dashboard. + + example: Display a promotional banner + enabled: + type: boolean + default: true + description: >- + Indicates whether to enable the rule. If it isn't enabled, it isn't applied at query time. + + taskID: + type: integer + format: int64 + example: 1514562690001 + description: > + Unique identifier of a task. + + + A successful API response means that a task was added to a queue. It might not run immediately. You can check the task's progress with the [`task` operation](#tag/Indices/operation/getTask) and this `taskID`. + + deletedAt: + type: string + example: '2023-06-27T14:42:38.831Z' + description: Date and time when the object was deleted, in RFC 3339 format. + taskStatus: + type: string + enum: + - published + - notPublished + description: >- + Task status, `published` if the task is completed, `notPublished` otherwise. + + parameters_query: + type: string + description: Search query. + default: '' + parameters_page: + type: integer + minimum: 0 + description: Requested page of the API response. + parameters_hitsPerPage: + type: integer + default: 20 + minimum: 1 + maximum: 1000 + description: Maximum number of hits per page. + responses: + BadRequest: + description: Bad request or request arguments. + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorBase' + FeatureNotEnabled: + description: This feature is not enabled on your Algolia account. + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorBase' + MethodNotAllowed: + description: Method not allowed with this API key. + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorBase' + IndexNotFound: + description: Index not found. + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorBase' + DeletedAt: + description: OK + content: + application/json: + schema: + title: deletedAtResponse + description: Response, taskID, and deletion timestamp. + additionalProperties: false + type: object + required: + - taskID + - deletedAt + properties: + taskID: + $ref: '#/components/schemas/taskID' + deletedAt: + $ref: '#/components/schemas/deletedAt' From bb3801c33b28a0322b1792e299377e98db64743a Mon Sep 17 00:00:00 2001 From: Paul Hachmang Date: Wed, 11 Sep 2024 14:00:34 +0200 Subject: [PATCH 32/70] Updated mesh spec --- packages/algolia-mesh/algolia-spec.yaml | 343 +++++++++++++----------- 1 file changed, 185 insertions(+), 158 deletions(-) diff --git a/packages/algolia-mesh/algolia-spec.yaml b/packages/algolia-mesh/algolia-spec.yaml index 6f1be536c2..879e8396ff 100644 --- a/packages/algolia-mesh/algolia-spec.yaml +++ b/packages/algolia-mesh/algolia-spec.yaml @@ -405,128 +405,17 @@ components: The required ACL to make a request is listed in each endpoint's reference. - parameters: - PathInPath: - name: path - in: path - description: Path of the endpoint, anything after "/1" must be specified. - required: true - schema: - type: string - example: /keys - Parameters: - name: parameters - in: query - description: Query parameters to apply to the current query. - schema: - type: object - additionalProperties: true - IndexName: - name: indexName - in: path - description: Name of the index on which to perform the operation. - required: true - schema: - type: string - example: YourIndexName - ObjectID: - name: objectID - in: path - description: Unique record identifier. - required: true - schema: - $ref: '#/components/schemas/objectID' - ForwardToReplicas: - in: query - name: forwardToReplicas - required: false - description: Whether changes are applied to replica indices. - schema: - type: boolean - parameters_ObjectID: - name: objectID - in: path - description: Unique identifier of a synonym object. - required: true - schema: - type: string - example: synonymID - ReplaceExistingSynonyms: - in: query - name: replaceExistingSynonyms - schema: - type: boolean - description: >- - Whether to replace all synonyms in the index with the ones sent with this request. - - KeyString: - in: path - name: key - required: true - schema: - type: string - example: YourAPIKey - description: API key. - ObjectIDRule: - in: path - name: objectID - description: Unique identifier of a rule object. - required: true - schema: - $ref: '#/components/schemas/ruleID' - ClearExistingRules: - in: query - name: clearExistingRules - required: false - schema: - type: boolean - description: Whether existing rules should be deleted before adding this batch. - DictionaryName: - in: path - name: dictionaryName - description: Dictionary type in which to search. - required: true - schema: - $ref: '#/components/schemas/dictionaryType' - Page: - in: query - name: page - description: | - Requested page of the API response. - If `null`, the API response is not paginated. - required: false - schema: - oneOf: - - type: integer - minimum: 0 - - type: 'null' - default: null - HitsPerPage: - in: query - name: hitsPerPage - description: Number of hits per page. - required: false - schema: - type: integer - default: 100 - UserIDInHeader: - name: X-Algolia-User-ID - description: Unique identifier of the user who makes the search request. - in: header - required: true - schema: - $ref: '#/components/schemas/userID' - UserIDInPath: - name: userID - description: Unique identifier of the user who makes the search request. - in: path - required: true - schema: - $ref: '#/components/schemas/userID' schemas: + attributeToUpdate: + x-keep-model: true + deprecated: true + oneOf: + - type: string + - $ref: '#/components/schemas/builtInOperation' ErrorBase: description: Error. type: object + x-keep-model: true additionalProperties: true properties: message: @@ -538,6 +427,7 @@ components: type: string searchParamsString: type: object + title: Search parameters as query string. description: Search parameters as query string. additionalProperties: false x-discriminator-fields: @@ -1047,7 +937,7 @@ components: length: type: integer description: Number of hits to retrieve (used in combination with `offset`). - minimum: 1 + minimum: 0 maximum: 1000 x-categories: - Pagination @@ -1204,6 +1094,11 @@ components: - $ref: '#/components/schemas/typoToleranceEnum' x-categories: - Typos + booleanString: + type: string + enum: + - 'true' + - 'false' ignorePlurals: description: | Treat singular, plurals, and other forms of declensions as equivalent. @@ -1218,6 +1113,7 @@ components: This overrides languages you set with `queryLanguages`. items: $ref: '#/components/schemas/supportedLanguage' + - $ref: '#/components/schemas/booleanString' - type: boolean description: > If true, `ignorePlurals` is active for all languages included in `queryLanguages`, or for all supported languages, if `queryLanguges` is empty. @@ -2221,7 +2117,6 @@ components: type: integer description: Facet counts. facets_stats: - title: facetsStats type: object description: Statistics for numerical facets. additionalProperties: @@ -2308,6 +2203,9 @@ components: Unique identifier for the query. This is used for [click analytics](https://www.algolia.com/doc/guides/analytics/click-analytics/). example: a00dbc80a8d13c4565a442e7e2dca80a + _automaticInsights: + type: boolean + description: Whether automatic events collection is enabled for the application. nbHits: type: integer description: Number of results (hits). @@ -2377,6 +2275,9 @@ components: - value - matchLevel - matchedWords + x-discriminator-fields: + - matchLevel + - matchedWords highlightResultOptionMap: type: object description: Surround words that match the query with HTML tags for highlighting. @@ -2412,6 +2313,8 @@ components: required: - value - matchLevel + x-discriminator-fields: + - matchLevel snippetResultOptionMap: type: object description: Snippets that show the context around a matching search query. @@ -2798,44 +2701,6 @@ components: $ref: '#/components/schemas/taskID' updatedAt: $ref: '#/components/schemas/updatedAt' - attribute: - type: string - description: Value of the attribute to update. - builtInOperationType: - type: string - enum: - - Increment - - Decrement - - Add - - Remove - - AddUnique - - IncrementFrom - - IncrementSet - description: How to change the attribute. - builtInOperationValue: - oneOf: - - type: string - description: >- - A string to append or remove for the `Add`, `Remove`, and `AddUnique` operations. - - - type: integer - description: A number to add, remove, or append, depending on the operation. - builtInOperation: - type: object - description: Update to perform on the attribute. - additionalProperties: false - properties: - _operation: - $ref: '#/components/schemas/builtInOperationType' - value: - $ref: '#/components/schemas/builtInOperationValue' - required: - - _operation - - value - attributeToUpdate: - oneOf: - - $ref: '#/components/schemas/attribute' - - $ref: '#/components/schemas/builtInOperation' action: type: string enum: @@ -3730,6 +3595,8 @@ components: required: - position - objectIDs + x-discriminator-fields: + - objectIDs promoteObjectID: title: objectID description: Record to promote. @@ -3743,6 +3610,8 @@ components: required: - position - objectID + x-discriminator-fields: + - objectID promote: oneOf: - $ref: '#/components/schemas/promoteObjectIDs' @@ -4094,6 +3963,15 @@ components: description: >- Task status, `published` if the task is completed, `notPublished` otherwise. + GetTaskResponse: + title: getTaskResponse + type: object + additionalProperties: false + properties: + status: + $ref: '#/components/schemas/taskStatus' + required: + - status operationType: type: string enum: @@ -4289,6 +4167,155 @@ components: - copyOperationResponse - batchResponses - moveOperationResponse + builtInOperationType: + type: string + enum: + - Increment + - Decrement + - Add + - Remove + - AddUnique + - IncrementFrom + - IncrementSet + description: How to change the attribute. + builtInOperationValue: + oneOf: + - type: string + description: >- + A string to append or remove for the `Add`, `Remove`, and `AddUnique` operations. + + - type: integer + description: A number to add, remove, or append, depending on the operation. + builtInOperation: + type: object + description: Update to perform on the attribute. + additionalProperties: false + properties: + _operation: + $ref: '#/components/schemas/builtInOperationType' + value: + $ref: '#/components/schemas/builtInOperationValue' + required: + - _operation + - value + parameters: + PathInPath: + name: path + in: path + description: Path of the endpoint, anything after "/1" must be specified. + required: true + schema: + type: string + example: /keys + Parameters: + name: parameters + in: query + description: Query parameters to apply to the current query. + schema: + type: object + additionalProperties: true + IndexName: + name: indexName + in: path + description: Name of the index on which to perform the operation. + required: true + schema: + type: string + example: YourIndexName + ObjectID: + name: objectID + in: path + description: Unique record identifier. + required: true + schema: + $ref: '#/components/schemas/objectID' + ForwardToReplicas: + in: query + name: forwardToReplicas + required: false + description: Whether changes are applied to replica indices. + schema: + type: boolean + parameters_ObjectID: + name: objectID + in: path + description: Unique identifier of a synonym object. + required: true + schema: + type: string + example: synonymID + ReplaceExistingSynonyms: + in: query + name: replaceExistingSynonyms + schema: + type: boolean + description: >- + Whether to replace all synonyms in the index with the ones sent with this request. + + KeyString: + in: path + name: key + required: true + schema: + type: string + example: YourAPIKey + description: API key. + ObjectIDRule: + in: path + name: objectID + description: Unique identifier of a rule object. + required: true + schema: + $ref: '#/components/schemas/ruleID' + ClearExistingRules: + in: query + name: clearExistingRules + required: false + schema: + type: boolean + description: Whether existing rules should be deleted before adding this batch. + DictionaryName: + in: path + name: dictionaryName + description: Dictionary type in which to search. + required: true + schema: + $ref: '#/components/schemas/dictionaryType' + Page: + in: query + name: page + description: | + Requested page of the API response. + If `null`, the API response is not paginated. + required: false + schema: + oneOf: + - type: integer + minimum: 0 + - type: 'null' + default: null + HitsPerPage: + in: query + name: hitsPerPage + description: Number of hits per page. + required: false + schema: + type: integer + default: 100 + UserIDInHeader: + name: X-Algolia-User-ID + description: Unique identifier of the user who makes the search request. + in: header + required: true + schema: + $ref: '#/components/schemas/userID' + UserIDInPath: + name: userID + description: Unique identifier of the user who makes the search request. + in: path + required: true + schema: + $ref: '#/components/schemas/userID' responses: BadRequest: description: Bad request or request arguments. From 5212ac85bf3156082de6db78d522dfd4c204ec0d Mon Sep 17 00:00:00 2001 From: Paul Hachmang Date: Wed, 11 Sep 2024 14:28:50 +0200 Subject: [PATCH 33/70] =?UTF-8?q?Cleaned=20up=20algolia=20schema.=20Remove?= =?UTF-8?q?d=20indexSettingsAsSearchParams=20from=20searchParamsObject,=20?= =?UTF-8?q?so=20we=E2=80=99ve=20got=20less=20options=20to=20worrt=20about?= =?UTF-8?q?=20when=20querying?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/algolia-mesh/algolia-spec.yaml | 1 - .../scripts/base-schema-filter.mts | 58 +++++++++++++++++++ .../algolia-mesh/scripts/generate-spec.mts | 41 +------------ 3 files changed, 60 insertions(+), 40 deletions(-) create mode 100644 packages/algolia-mesh/scripts/base-schema-filter.mts diff --git a/packages/algolia-mesh/algolia-spec.yaml b/packages/algolia-mesh/algolia-spec.yaml index 879e8396ff..f3be00d142 100644 --- a/packages/algolia-mesh/algolia-spec.yaml +++ b/packages/algolia-mesh/algolia-spec.yaml @@ -1984,7 +1984,6 @@ components: allOf: - $ref: '#/components/schemas/baseSearchParams' - - $ref: '#/components/schemas/indexSettingsAsSearchParams' searchParams: $ref: '#/components/schemas/searchParamsObject' processingTimeMS: diff --git a/packages/algolia-mesh/scripts/base-schema-filter.mts b/packages/algolia-mesh/scripts/base-schema-filter.mts new file mode 100644 index 0000000000..0fb090ac33 --- /dev/null +++ b/packages/algolia-mesh/scripts/base-schema-filter.mts @@ -0,0 +1,58 @@ +import type { OpenAPIV3 } from 'openapi-types' + +function isRef(value: any): value is OpenAPIV3.ReferenceObject { + return typeof value === 'object' && '$ref' in value +} + +export function algoliaSchemaBaseFilter( + schemaKey: string, + schema: OpenAPIV3.ReferenceObject | OpenAPIV3.SchemaObject, +): [string, OpenAPIV3.ReferenceObject | OpenAPIV3.SchemaObject] { + if (isRef(schema)) return [schemaKey, schema] + + if ( + schema.oneOf && + schema.oneOf[0]?.['$ref'] === '#/components/schemas/searchParamsString' && + schema.oneOf[1]?.['$ref'] === '#/components/schemas/searchParamsObject' + ) { + return [schemaKey, { $ref: '#/components/schemas/searchParamsObject' }] + } + + if (schemaKey === 'searchParamsObject') { + return [ + schemaKey, + { + ...schema, + allOf: schema.allOf?.filter( + (item) => + !isRef(item) || item.$ref !== '#/components/schemas/indexSettingsAsSearchParams', + ), + }, + ] + } + + if (schemaKey === 'facetFilters') { + return [schemaKey, { ...schema, example: undefined, oneOf: undefined, type: 'object' }] + } + + return [ + schemaKey, + { + ...schema, + default: undefined, + properties: schema.properties + ? Object.fromEntries( + Object.entries(schema.properties).map(([propertyKey, property]) => { + if (isRef(property)) return [propertyKey, property] + + if (propertyKey === 'customNormalization' || propertyKey === 'facets') { + return [propertyKey, { ...property, example: undefined }] + } + + return [propertyKey, { ...property, default: undefined }] + }), + ) + : undefined, + }, + ] +} diff --git a/packages/algolia-mesh/scripts/generate-spec.mts b/packages/algolia-mesh/scripts/generate-spec.mts index 27a9109bbe..bf4919daac 100644 --- a/packages/algolia-mesh/scripts/generate-spec.mts +++ b/packages/algolia-mesh/scripts/generate-spec.mts @@ -3,10 +3,7 @@ import { writeFile } from 'node:fs/promises' import type { OpenAPIV3 } from 'openapi-types' import prettier from 'prettier' import conf from '@graphcommerce/prettier-config-pwa' - -function isRef(value: any): value is OpenAPIV3.ReferenceObject { - return typeof value === 'object' && '$ref' in value -} +import { algoliaSchemaBaseFilter } from './base-schema-filter.mjs' const response = await fetch( 'https://raw.githubusercontent.com/algolia/api-clients-automation/main/specs/bundled/search.yml', @@ -22,47 +19,13 @@ const newSchema: OpenAPIV3.Document = { ...openApiSchema.components, schemas: Object.fromEntries( Object.entries(openApiSchema.components?.schemas ?? {}).map(([schemaKey, schema]) => { - if (isRef(schema)) return [schemaKey, schema] - - if ( - schema.oneOf && - schema.oneOf[0]?.['$ref'] === '#/components/schemas/searchParamsString' && - schema.oneOf[1]?.['$ref'] === '#/components/schemas/searchParamsObject' - ) { - return [schemaKey, { $ref: '#/components/schemas/searchParamsObject' }] - } - - if (schemaKey === 'facetFilters') { - return [schemaKey, { ...schema, example: undefined, oneOf: undefined, type: 'object' }] - } - - return [ - schemaKey, - { - ...schema, - default: undefined, - properties: schema.properties - ? Object.fromEntries( - Object.entries(schema.properties).map(([propertyKey, property]) => { - if (isRef(property)) return [propertyKey, property] - - if (propertyKey === 'customNormalization' || propertyKey === 'facets') { - return [propertyKey, { ...property, example: undefined }] - } - - return [propertyKey, { ...property, default: undefined }] - }), - ) - : undefined, - }, - ] + return algoliaSchemaBaseFilter(schemaKey, schema) }), ), }, paths: { ...Object.fromEntries( Object.entries(openApiSchema.paths) - .map(([path, pathItem]) => { if (!pathItem) return [path, pathItem] const newValue = pathItem From 6587d4d3b56d54bb0908232f94009f479cae8561 Mon Sep 17 00:00:00 2001 From: Paul Hachmang Date: Wed, 11 Sep 2024 14:30:03 +0200 Subject: [PATCH 34/70] Apply the schema filters used for algolia-mesh in algolia-recommend. And made the queryParameters and fallbackParameters working --- .../algolia-insights-spec.yaml | 6 +- .../algolia-recommend-spec.yaml | 106 +----------------- .../scripts/generate-recommend-spec.mts | 66 ++++++++--- 3 files changed, 60 insertions(+), 118 deletions(-) diff --git a/packages/algolia-personalization/algolia-insights-spec.yaml b/packages/algolia-personalization/algolia-insights-spec.yaml index be57f002db..c24d45d06c 100644 --- a/packages/algolia-personalization/algolia-insights-spec.yaml +++ b/packages/algolia-personalization/algolia-insights-spec.yaml @@ -2,7 +2,7 @@ "openapi": "3.0.2", "info": { "title": "Insights API", - "description": "The Insights API lets you collect events related to your search and discovery experience.\nEvents represent user interactions with your app or website.\nThey unlock powerful features, such as recommendations, personalization, smarter search results,\nand analytics that help you optimize your user experience.\n\n## Client libraries\n\nUse Algolia's API clients, libraries, and integrations to collect events from your UI and send them to the Insights API.\nSee: [Algolia's ecosystem](https://www.algolia.com/doc/guides/getting-started/how-algolia-works/in-depth/ecosystem/)\n\n## Base URLs\n\nThe base URLs for making requests to the Insights API are:\n\n- `https://insights.us.algolia.io` (`https://insights.algolia.io` is an alias)\n- `https://insights.de.algolia.io`\n\n**All requests must use HTTPS.**\n\n## Authentication\n\nTo authenticate your API requests, add these headers:\n\n- `x-algolia-application-id`. Your Algolia application ID.\n- `x-algolia-api-key`. An API key with the necessary permissions to make the request.\n The required access control list (ACL) to make a request is listed in each endpoint's reference.\n\nYou can find your application ID and API key in the [Algolia dashboard](https://dashboard.algolia.com/account).\n\n## Request format\n\nRequest bodies must be JSON objects.\n\n## Response status and errors\n\nResponse bodies are JSON objects.\nDeleting a user token returns an empty response body with rate-limiting information as headers.\n\nSuccessful responses return a `2xx` status. Client errors return a `4xx` status. Server errors are indicated by a `5xx` status.\nError responses have a `message` property with more information.\n\nThe Insights API doesn't validate if the event parameters such as `indexName`, `objectIDs`, or `userToken`,\ncorrespond to anything in the Search API. It justs checks if they're formatted correctly.\nCheck the [Events](https://dashboard.algolia.com/events/health) health section,\nwhether your events can be used for Algolia features such as Analytics, or Dynamic Re-Ranking.\n\n## Version\n\nThe current version of the Insights API is version 1, as indicated by the `/1/` in each endpoint's URL.\n", + "description": "The Insights API lets you collect events related to your search and discovery experience.\nEvents represent user interactions with your app or website.\nThey unlock powerful features, such as recommendations, personalization, smarter search results,\nand analytics that help you optimize your user experience.\n\n## Client libraries\n\nUse Algolia's API clients, libraries, and integrations to collect events from your UI and send them to the Insights API.\nSee: [Algolia's ecosystem](https://www.algolia.com/doc/guides/getting-started/how-algolia-works/in-depth/ecosystem/)\n\n## Base URLs\n\nThe base URLs for making requests to the Insights API are:\n\n- `https://insights.us.algolia.io`\n- `https://insights.de.algolia.io`\n- `https//insights.algolia.io` (routes requests to the closest of the above servers, based on your geographical location)\n\n**All requests must use HTTPS.**\n\n## Authentication\n\nTo authenticate your API requests, add these headers:\n\n- `x-algolia-application-id`. Your Algolia application ID.\n- `x-algolia-api-key`. An API key with the necessary permissions to make the request.\n The required access control list (ACL) to make a request is listed in each endpoint's reference.\n\nYou can find your application ID and API key in the [Algolia dashboard](https://dashboard.algolia.com/account).\n\n## Request format\n\nRequest bodies must be JSON objects.\n\n## Response status and errors\n\nResponse bodies are JSON objects.\nDeleting a user token returns an empty response body with rate-limiting information as headers.\n\nSuccessful responses return a `2xx` status. Client errors return a `4xx` status. Server errors are indicated by a `5xx` status.\nError responses have a `message` property with more information.\n\nThe Insights API doesn't validate if the event parameters such as `indexName`, `objectIDs`, or `userToken`,\ncorrespond to anything in the Search API. It justs checks if they're formatted correctly.\nCheck the [Events](https://dashboard.algolia.com/events/health) health section,\nwhether your events can be used for Algolia features such as Analytics, or Dynamic Re-Ranking.\n\n## Version\n\nThe current version of the Insights API is version 1, as indicated by the `/1/` in each endpoint's URL.\n", "version": "1.0.0" }, "paths": { @@ -521,6 +521,7 @@ "ErrorBase": { "description": "Error.", "type": "object", + "x-keep-model": true, "additionalProperties": true, "properties": { "message": { "type": "string", "example": "Invalid Application-Id or API-Key" } @@ -531,8 +532,7 @@ "minLength": 1, "maxLength": 64, "pattern": "[\\x20-\\x7E]{1,64}", - "description": "Event name, up to 64 ASCII characters.\n\nConsider naming events consistently—for example, by adopting Segment's\n[object-action](https://segment.com/academy/collecting-data/naming-conventions-for-clean-data/#the-object-action-framework)\nframework.\n", - "example": "Product Clicked" + "description": "Event name, up to 64 ASCII characters.\n\nConsider naming events consistently—for example, by adopting Segment's\n[object-action](https://segment.com/academy/collecting-data/naming-conventions-for-clean-data/#the-object-action-framework)\nframework.\n" }, "ClickEvent": { "type": "string", "enum": ["click"] }, "index": { diff --git a/packages/algolia-recommend/algolia-recommend-spec.yaml b/packages/algolia-recommend/algolia-recommend-spec.yaml index fbbb03eab5..4c2fa6b053 100644 --- a/packages/algolia-recommend/algolia-recommend-spec.yaml +++ b/packages/algolia-recommend/algolia-recommend-spec.yaml @@ -226,7 +226,6 @@ components: query: type: string description: Search query. - default: '' x-categories: - Search searchParamsQuery: @@ -294,17 +293,9 @@ components: `facet:\-value`. - example: - - - category:Book - - category:-Movie - - author:John Doe - oneOf: - - type: array - items: - $ref: '#/components/schemas/facetFilters' - - type: string x-categories: - Filtering + type: object optionalFilters: description: > Filters to promote or demote records in the search results. @@ -386,7 +377,6 @@ components: page: type: integer description: Page of search results to retrieve. - default: 0 minimum: 0 x-categories: - Pagination @@ -403,13 +393,11 @@ components: This parameter is ignored if you also specify `insidePolygon` or `insideBoundingBox`. example: 40.71,-74.01 - default: '' x-categories: - Geo-Search aroundLatLngViaIP: type: boolean description: Whether to obtain the coordinates from the request's IP address. - default: false x-categories: - Geo-Search aroundRadiusAll: @@ -659,7 +647,6 @@ components: Combine it with `filters` to narrow down the list of results. - default: '' example: comedy drama crime Macy Buscemi x-categories: - Search @@ -685,7 +672,6 @@ components: For more information, see [filter scores](https://www.algolia.com/doc/guides/managing-results/refine-results/filtering/in-depth/filter-scoring/#accumulating-scores-with-sumorfiltersscores). - default: false x-categories: - Filtering restrictSearchableAttributes: @@ -698,7 +684,6 @@ components: description: | Restricts a search to a subset of your searchable attributes. Attribute names are case-sensitive. - default: [] x-categories: - Filtering facets: @@ -714,8 +699,6 @@ components: For more information, see [facets](https://www.algolia.com/doc/guides/managing-results/refine-results/faceting/#contextual-facet-values-and-counts). default: [] - example: - - '*' x-categories: - Faceting facetingAfterDistinct: @@ -730,7 +713,6 @@ components: as `facetingAfterDistinct` only computes correct facet counts if all records have the same facet values for the `attributeForDistinct`. - default: false x-categories: - Faceting page: @@ -781,7 +763,6 @@ components: - Adds a `natural_language` attribute to `ruleContexts` and `analyticsTags`. - default: [] x-categories: - Languages ruleContexts: @@ -794,7 +775,6 @@ components: [Rule contexts](https://www.algolia.com/doc/guides/managing-results/rules/rules-overview/how-to/customize-search-results-by-platform/#whats-a-context) are strings that you can use to trigger matching rules. - default: [] example: - mobile x-categories: @@ -809,7 +789,6 @@ components: For more information, see [Understanding Personalization impact](https://www.algolia.com/doc/guides/personalization/personalizing-results/in-depth/configuring-personalization/#understanding-personalization-impact). - default: 100 minimum: 0 maximum: 100 x-categories: @@ -821,13 +800,11 @@ components: description: >- Whether the search response should include detailed ranking information. - default: false x-categories: - Advanced synonyms: type: boolean description: Whether to take into account an index's synonyms for this search. - default: true x-categories: - Advanced clickAnalytics: @@ -838,13 +815,11 @@ components: The query ID is a unique identifier for a search query and is required for tracking [click and conversion events](https://www.algolia.com/guides/sending-events/getting-started/). - default: false x-categories: - Analytics analytics: type: boolean description: Whether this search will be included in Analytics. - default: true x-categories: - Analytics analyticsTags: @@ -854,7 +829,6 @@ components: description: >- Tags to apply to the query for [segmenting analytics data](https://www.algolia.com/doc/guides/search-analytics/guides/segments/). - default: [] x-categories: - Analytics percentileComputation: @@ -862,13 +836,11 @@ components: description: >- Whether to include this search when calculating processing-time percentiles. - default: true x-categories: - Advanced enableABTest: type: boolean description: Whether to enable A/B testing for this search. - default: true x-categories: - Advanced baseSearchParams: @@ -878,7 +850,6 @@ components: hitsPerPage: type: integer description: Number of hits per page. - default: 20 minimum: 1 maximum: 1000 x-categories: @@ -992,7 +963,6 @@ components: For more information, see [Prefix searching](https://www.algolia.com/doc/guides/managing-results/optimize-search-results/override-search-engine-defaults/in-depth/prefix-searching/). - default: prefixLast x-categories: - Query strategy removeWordsIfNoResults: @@ -1025,7 +995,6 @@ components: For more information, see [Remove words to improve results](https://www.algolia.com/doc/guides/managing-results/optimize-search-results/empty-or-insufficient-results/in-depth/why-use-remove-words-if-no-results/). - default: none x-categories: - Query strategy mode: @@ -1039,7 +1008,6 @@ components: This setting only applies to indices, for which Algolia enabled NeuralSearch for you. - default: keywordSearch x-categories: - Query strategy semanticSearch: @@ -1081,7 +1049,6 @@ components: Only exact matches will be highlighted, partial and prefix matches won't. - default: attribute x-categories: - Query strategy alternativesAsExact: @@ -1141,7 +1108,6 @@ components: Maximum number of facet values to return when [searching for facet values](https://www.algolia.com/doc/guides/managing-results/refine-results/faceting/#search-for-facet-values). maximum: 100 - default: 10 x-categories: - Advanced order: @@ -1274,8 +1240,6 @@ components: - The `objectID` attribute is always included. - default: - - '*' x-categories: - Attributes ranking: @@ -1309,15 +1273,6 @@ components: and by [A/B testing](https://www.algolia.com/doc/guides/ab-testing/what-is-ab-testing/). - default: - - typo - - geo - - words - - filters - - proximity - - attribute - - exact - - custom x-categories: - Ranking customRanking: @@ -1356,7 +1311,6 @@ components: or the other attributes will never be applied. - default: [] x-categories: - Ranking relevancyStrictness: @@ -1370,7 +1324,6 @@ components: Use this setting to strike a balance between the relevance and number of returned results. - default: 100 x-categories: - Ranking attributesToHighlight: @@ -1425,7 +1378,6 @@ components: where `NUMBER` is the number of words to be extracted. - default: [] x-categories: - Highlighting and Snippeting highlightPreTag: @@ -1433,7 +1385,6 @@ components: description: >- HTML tag to insert before the highlighted parts in all highlighted results and snippets. - default: x-categories: - Highlighting and Snippeting highlightPostTag: @@ -1441,13 +1392,11 @@ components: description: >- HTML tag to insert after the highlighted parts in all highlighted results and snippets. - default: x-categories: - Highlighting and Snippeting snippetEllipsisText: type: string description: String used as an ellipsis indicator when a snippet is truncated. - default: … x-categories: - Highlighting and Snippeting restrictHighlightAndSnippetArrays: @@ -1457,7 +1406,6 @@ components: By default, all items are highlighted and snippeted. - default: false x-categories: - Highlighting and Snippeting hitsPerPage: @@ -1467,7 +1415,6 @@ components: description: >- Minimum number of characters a word in the search query must contain to accept matches with [one typo](https://www.algolia.com/doc/guides/managing-results/optimize-search-results/typo-tolerance/in-depth/configuring-typo-tolerance/#configuring-word-length-for-typos). - default: 4 x-categories: - Typos minWordSizefor2Typos: @@ -1475,7 +1422,6 @@ components: description: >- Minimum number of characters a word in the search query must contain to accept matches with [two typos](https://www.algolia.com/doc/guides/managing-results/optimize-search-results/typo-tolerance/in-depth/configuring-typo-tolerance/#configuring-word-length-for-typos). - default: 8 x-categories: - Typos typoTolerance: @@ -1487,7 +1433,6 @@ components: Turn off this setting to reduce the number of irrelevant matches when searching in large sets of similar numbers. - default: true x-categories: - Typos disableTypoToleranceOnAttributes: @@ -1512,7 +1457,6 @@ components: Consider alternatives such as `disableTypoToleranceOnWords` or adding synonyms if your attributes have intentional unusual spellings that might look like typos. - default: [] x-categories: - Typos ignorePlurals: @@ -1528,7 +1472,6 @@ components: By default, Algolia removes diacritics from letters. For example, `é` becomes `e`. If this causes issues in your search, you can specify characters that should keep their diacritics. - default: '' x-categories: - Languages queryLanguages: @@ -1558,7 +1501,6 @@ components: For more information, see [Language-specific configuration](https://www.algolia.com/doc/guides/managing-results/optimize-search-results/handling-natural-languages-nlp/in-depth/language-specific-configurations/). - default: [] x-categories: - Languages decompoundQuery: @@ -1575,19 +1517,16 @@ components: For example, `Gartenstühle` won't be decompounded if the `ü` consists of `u` (U+0075) and `◌̈` (U+0308). - default: true x-categories: - Languages enableRules: type: boolean description: Whether to enable rules. - default: true x-categories: - Rules enablePersonalization: type: boolean description: Whether to enable Personalization. - default: false x-categories: - Personalization queryType: @@ -1606,7 +1545,6 @@ components: Use the `advancedSyntaxFeatures` parameter to control which feature is supported. - default: false x-categories: - Query strategy optionalWords: @@ -1643,7 +1581,6 @@ components: For more information, see [Optional words](https://www.algolia.com/doc/guides/managing-results/optimize-search-results/empty-or-insufficient-results/#creating-a-list-of-optional-words). - default: [] x-categories: - Query strategy disableExactOnAttributes: @@ -1666,7 +1603,6 @@ components: This reduces the impact of individual attributes with a lot of content on ranking. - default: [] x-categories: - Query strategy exactOnSingleWordQuery: @@ -1688,9 +1624,6 @@ components: - `multiWordsSynonym`. Multi-word synonyms, such as "NY/New York" are considered exact matches. - default: - - ignorePlurals - - singleWordSynonym x-categories: - Query strategy advancedSyntaxFeatures: @@ -1709,9 +1642,6 @@ components: For example, `search -engine` matches records that contain "search" but not "engine". This setting only has an effect if `advancedSyntax` is true. - default: - - exactPhrase - - excludeWords x-categories: - Query strategy distinct: @@ -1735,7 +1665,6 @@ components: but all occurences of "house" are replaced by "home" in the highlighted response. - default: false x-categories: - Highlighting and Snippeting minProximity: @@ -1753,7 +1682,6 @@ components: For example, if `minProximity` is 2, neighboring matches and matches with one word between them would have the same score. - default: 1 x-categories: - Advanced responseFields: @@ -1778,8 +1706,6 @@ components: Don't exclude properties that you might need in your search UI. - default: - - '*' x-categories: - Advanced maxFacetHits: @@ -1787,7 +1713,6 @@ components: maxValuesPerFacet: type: integer description: Maximum number of facet values to return for each facet. - default: 100 maximum: 1000 x-categories: - Faceting @@ -1808,7 +1733,6 @@ components: For more information, see [facet value display](https://www.algolia.com/doc/guides/building-search-ui/ui-and-ux-patterns/facet-display/js/). - default: count x-categories: - Faceting attributeCriteriaComputedByMinProximity: @@ -1823,7 +1747,6 @@ components: Otherwise, the best matching attribute is determined by the order in the `searchableAttributes` setting. - default: false x-categories: - Advanced renderingContent: @@ -1836,7 +1759,6 @@ components: This setting only has an effect if you activated Dynamic Re-Ranking for this index in the Algolia dashboard. - default: true x-categories: - Filtering reRankingApplyFilter: @@ -1850,13 +1772,10 @@ components: allOf: - $ref: '#/components/schemas/baseSearchParams' - - $ref: '#/components/schemas/indexSettingsAsSearchParams' searchParams: title: queryParameters allOf: - $ref: '#/components/schemas/searchParamsObject' - - type: object - description: Search parameters for filtering the recommendations. baseRecommendRequest: type: object additionalProperties: false @@ -1875,7 +1794,6 @@ components: type: integer minimum: 1 maximum: 1000 - default: 30 description: > Maximum number of recommendations to retrieve. @@ -1886,7 +1804,7 @@ components: the actual number of recommendations may be lower than this value. queryParameters: - $ref: '#/components/schemas/searchParams' + $ref: '#/components/schemas/searchParamsObject' required: - indexName - threshold @@ -1935,10 +1853,6 @@ components: title: fallbackParameters allOf: - $ref: '#/components/schemas/searchParamsObject' - - type: object - description: >- - Search parameters to use for a fallback request if there aren't enough recommendations. - relatedProducts: type: object properties: @@ -1947,7 +1861,7 @@ components: objectID: $ref: '#/components/schemas/objectID' fallbackParameters: - $ref: '#/components/schemas/fallbackParams' + $ref: '#/components/schemas/searchParamsObject' required: - model - objectID @@ -2016,7 +1930,7 @@ components: model: $ref: '#/components/schemas/trendingFacetsModel' fallbackParameters: - $ref: '#/components/schemas/fallbackParams' + $ref: '#/components/schemas/searchParamsObject' required: - facetName - model @@ -2043,7 +1957,7 @@ components: objectID: $ref: '#/components/schemas/objectID' fallbackParameters: - $ref: '#/components/schemas/fallbackParams' + $ref: '#/components/schemas/searchParamsObject' required: - model - objectID @@ -2064,7 +1978,7 @@ components: model: $ref: '#/components/schemas/recommendedForYouModel' fallbackParameters: - $ref: '#/components/schemas/fallbackParams' + $ref: '#/components/schemas/searchParamsObject' required: - model recommendedForYouQuery: @@ -2123,7 +2037,6 @@ components: An object with custom data. You can store up to 32kB as custom data. - default: {} x-categories: - Advanced baseSearchResponse: @@ -2210,10 +2123,6 @@ components: x-additionalPropertiesName: facet count type: integer description: Facet counts. - example: - category: - food: 1 - tech: 42 facets_stats: type: object description: Statistics for numerical facets. @@ -2729,7 +2638,6 @@ components: example: Display a promotional banner enabled: type: boolean - default: true description: >- Indicates whether to enable the rule. If it isn't enabled, it isn't applied at query time. @@ -2758,14 +2666,12 @@ components: parameters_query: type: string description: Search query. - default: '' parameters_page: type: integer minimum: 0 description: Requested page of the API response. parameters_hitsPerPage: type: integer - default: 20 minimum: 1 maximum: 1000 description: Maximum number of hits per page. diff --git a/packages/algolia-recommend/scripts/generate-recommend-spec.mts b/packages/algolia-recommend/scripts/generate-recommend-spec.mts index b4699e9b75..448cc20db1 100644 --- a/packages/algolia-recommend/scripts/generate-recommend-spec.mts +++ b/packages/algolia-recommend/scripts/generate-recommend-spec.mts @@ -3,6 +3,7 @@ import { writeFile, readFile } from 'node:fs/promises' import { OpenAPIV3 } from 'openapi-types' import prettier from 'prettier' import conf from '@graphcommerce/prettier-config-pwa' +import { algoliaSchemaBaseFilter } from '@graphcommerce/algolia-mesh/scripts/base-schema-filter.mjs' const response = await fetch( 'https://raw.githubusercontent.com/algolia/api-clients-automation/main/specs/bundled/recommend.yml', @@ -69,27 +70,62 @@ const newSchema: OpenAPIV3.Document = { components: { ...openApiSchema.components, schemas: Object.fromEntries( - Object.entries(openApiSchema.components?.schemas ?? {}).map(([schemaKey, schema]) => { - if (isRef(schema) || schemaKey !== 'recommendedForYouQuery') return [schemaKey, schema] - - return [ - schemaKey, - { - ...schema, - oneOf: schema.oneOf?.filter( - (item) => !isRef(item) || item.$ref !== '#/components/schemas/recommendedForYouQuery', - ), - }, - ] - }), + Object.entries(openApiSchema.components?.schemas ?? {}).map( + ([incomingKey, incomingSchema]) => { + const [schemaKey, schema] = algoliaSchemaBaseFilter(incomingKey, incomingSchema) + if (isRef(schema)) return [schemaKey, schema] + + // Some object have an addition type 'object' which removes all types of the object, we only add known properties here. + const ref = schema.allOf?.find((item) => isRef(item)) + const obj = schema.allOf?.find((item) => !isRef(item) && item.type === 'object') + if (ref && obj) { + return [schemaKey, { ...schema, allOf: [ref satisfies OpenAPIV3.ReferenceObject] }] + } + + if (schemaKey === 'recommendedForYouQuery') { + return [ + schemaKey, + { + ...schema, + oneOf: schema.oneOf?.filter( + (item) => + !isRef(item) || item.$ref !== '#/components/schemas/recommendedForYouQuery', + ), + }, + ] + } + + if (schemaKey === 'fallbackParams') { + return [schemaKey, undefined] + } + + return [ + schemaKey, + { + ...schema, + properties: schema.properties + ? { + ...schema.properties, + ...(schema?.properties?.fallbackParameters + ? { fallbackParameters: { $ref: '#/components/schemas/searchParamsObject' } } + : {}), + ...(schema?.properties?.queryParameters + ? { queryParameters: { $ref: '#/components/schemas/searchParamsObject' } } + : {}), + } + : undefined, + }, + ] + }, + ), ), }, } await writeFile( './algolia-recommend-spec.yaml', - await prettier.format(JSON.stringify(newSchema), { - parser: 'json', + await prettier.format(yaml.dump(newSchema), { + parser: 'yaml', ...conf, }), ) From 3b198d13312ccd3233e9181e095f66d68cb33e4a Mon Sep 17 00:00:00 2001 From: Paul Hachmang Date: Wed, 11 Sep 2024 14:39:28 +0200 Subject: [PATCH 35/70] Add back indexSettingsAsSearchParams this breaks the number of items per page --- packages/algolia-mesh/algolia-spec.yaml | 1 + .../algolia-mesh/scripts/base-schema-filter.mts | 13 ------------- .../algolia-recommend/algolia-recommend-spec.yaml | 1 + 3 files changed, 2 insertions(+), 13 deletions(-) diff --git a/packages/algolia-mesh/algolia-spec.yaml b/packages/algolia-mesh/algolia-spec.yaml index f3be00d142..879e8396ff 100644 --- a/packages/algolia-mesh/algolia-spec.yaml +++ b/packages/algolia-mesh/algolia-spec.yaml @@ -1984,6 +1984,7 @@ components: allOf: - $ref: '#/components/schemas/baseSearchParams' + - $ref: '#/components/schemas/indexSettingsAsSearchParams' searchParams: $ref: '#/components/schemas/searchParamsObject' processingTimeMS: diff --git a/packages/algolia-mesh/scripts/base-schema-filter.mts b/packages/algolia-mesh/scripts/base-schema-filter.mts index 0fb090ac33..82bdfab5bd 100644 --- a/packages/algolia-mesh/scripts/base-schema-filter.mts +++ b/packages/algolia-mesh/scripts/base-schema-filter.mts @@ -18,19 +18,6 @@ export function algoliaSchemaBaseFilter( return [schemaKey, { $ref: '#/components/schemas/searchParamsObject' }] } - if (schemaKey === 'searchParamsObject') { - return [ - schemaKey, - { - ...schema, - allOf: schema.allOf?.filter( - (item) => - !isRef(item) || item.$ref !== '#/components/schemas/indexSettingsAsSearchParams', - ), - }, - ] - } - if (schemaKey === 'facetFilters') { return [schemaKey, { ...schema, example: undefined, oneOf: undefined, type: 'object' }] } diff --git a/packages/algolia-recommend/algolia-recommend-spec.yaml b/packages/algolia-recommend/algolia-recommend-spec.yaml index 4c2fa6b053..f5f95a675e 100644 --- a/packages/algolia-recommend/algolia-recommend-spec.yaml +++ b/packages/algolia-recommend/algolia-recommend-spec.yaml @@ -1772,6 +1772,7 @@ components: allOf: - $ref: '#/components/schemas/baseSearchParams' + - $ref: '#/components/schemas/indexSettingsAsSearchParams' searchParams: title: queryParameters allOf: From a7a23259587e3241f1138b9c2efb21ab680c79b5 Mon Sep 17 00:00:00 2001 From: Paul Hachmang Date: Thu, 12 Sep 2024 09:26:31 +0200 Subject: [PATCH 36/70] Make filters working --- .../mesh/getSearchResultsInput.ts | 4 +- packages/algolia-recommend/README.md | 7 ++ .../mesh/getRecommendationArgs.ts | 51 ++++++++ packages/algolia-recommend/mesh/resolvers.ts | 119 +++++++++++++----- ...=> AlgoliaProductRecommendations.graphqls} | 72 ++++++----- .../schema/AlgoliaTrending.graphqls | 74 +++++++++++ 6 files changed, 263 insertions(+), 64 deletions(-) create mode 100644 packages/algolia-recommend/mesh/getRecommendationArgs.ts rename packages/algolia-recommend/schema/{AlgoliaRecommendations.graphqls => AlgoliaProductRecommendations.graphqls} (72%) create mode 100644 packages/algolia-recommend/schema/AlgoliaTrending.graphqls diff --git a/packages/algolia-mesh/mesh/getSearchResultsInput.ts b/packages/algolia-mesh/mesh/getSearchResultsInput.ts index 729c2bbf14..295b060741 100644 --- a/packages/algolia-mesh/mesh/getSearchResultsInput.ts +++ b/packages/algolia-mesh/mesh/getSearchResultsInput.ts @@ -1,6 +1,6 @@ import type { + AlgoliasearchParamsObject_Input, MeshContext, - Queryalgolia_searchSingleIndexArgs, QueryproductsArgs, } from '@graphcommerce/graphql-mesh' import { getStoreConfig } from './getStoreConfig' @@ -12,7 +12,7 @@ import { export async function getSearchResultsInput( args: QueryproductsArgs, context: MeshContext, -): Promise { +): Promise { const { engine, ...filters } = args.filter ?? {} return { diff --git a/packages/algolia-recommend/README.md b/packages/algolia-recommend/README.md index 42762d6c51..b885e18a10 100644 --- a/packages/algolia-recommend/README.md +++ b/packages/algolia-recommend/README.md @@ -1,3 +1,10 @@ # Algolia Recommend This package contains the Algolia Recommend API client and mesh resolvers. + +See [Config.graphqls](./Config.graphqls) for details and see the +[AlgoliaRecommendations.graphqls](./schema/AlgoliaRecommendations.graphqls) for +the extensions made on the schema. + +When configuring and using the recommendations, make sure the recommendations +are build first. diff --git a/packages/algolia-recommend/mesh/getRecommendationArgs.ts b/packages/algolia-recommend/mesh/getRecommendationArgs.ts new file mode 100644 index 0000000000..384f100691 --- /dev/null +++ b/packages/algolia-recommend/mesh/getRecommendationArgs.ts @@ -0,0 +1,51 @@ +import { getSearchResultsInput } from '@graphcommerce/algolia-mesh' +import { + QueryproductsArgs, + MeshContext, + AlgoliasearchParamsObject_Input, + AlgoliaLookingSimilarInput, +} from '@graphcommerce/graphql-mesh' + +export async function getRecommendationQueryInput( + args: QueryproductsArgs, + context: MeshContext, +): Promise { + const queryParameters = await getSearchResultsInput(args, context) + + if (queryParameters?.facets) delete queryParameters.facets + if (queryParameters?.hitsPerPage) delete queryParameters.hitsPerPage + if (queryParameters?.page) { + console.log('jajaja') + delete queryParameters.page + } + + console.log('balbla', queryParameters) + return queryParameters +} + +export async function getRecommendationsArgs( + root: { uid?: string }, + args: { input?: AlgoliaLookingSimilarInput | null }, + context: MeshContext, +) { + const { fallback, filter, maxRecommendations = 8, search, threshold = 75 } = args.input ?? {} + + return { + objectID: atob(root.uid ?? ''), + threshold, + maxRecommendations, + queryParameters: await getRecommendationQueryInput( + { filter, pageSize: maxRecommendations, search }, + context, + ), + fallbackParameters: await getRecommendationQueryInput( + { + filter: fallback?.filter, + pageSize: maxRecommendations, + search: fallback?.search, + sort: fallback?.sort, + }, + context, + ), + } +} diff --git a/packages/algolia-recommend/mesh/resolvers.ts b/packages/algolia-recommend/mesh/resolvers.ts index 7d4daf5f53..cb13c627a6 100644 --- a/packages/algolia-recommend/mesh/resolvers.ts +++ b/packages/algolia-recommend/mesh/resolvers.ts @@ -2,15 +2,13 @@ import fragments from '@graphcommerce/graphql/generated/fragments.json' import type { AlgoliaLookingSimilarInput, + AlgoliaRelatedProductsInput, MeshContext, ProductInterfaceResolvers, - RequireFields, ResolverFn, Resolvers, ResolversParentTypes, ResolversTypes, - SimpleProductalgolia_looking_similarArgs, - VirtualProductalgolia_looking_similarArgs, } from '@graphcommerce/graphql-mesh' import { GraphCommerceAlgoliaRecommendationLocation, @@ -19,6 +17,7 @@ import { } from '@graphcommerce/next-config' import { createProductMapper } from './createProductMapper' import { createFacetValueMapper } from './createValueFacetMapper' +import { getRecommendationsArgs } from './getRecommendationArgs' import { getRecommendations } from './getRecommendations' type ProductTypes = NonNullable>> @@ -26,22 +25,42 @@ const productInterfaceTypes = fragments.possibleTypes.ProductInterface as Produc const resolvers: Resolvers = { Query: { - trendingProducts: async (root, args, context, info) => - getRecommendations( + trendingProducts: async (root, args, context, info) => { + const { facetName, facetValue } = args.input + const { threshold, fallbackParameters, maxRecommendations, queryParameters } = + await getRecommendationsArgs(root, args, context) + return getRecommendations( 'Trending_items_Input', - { threshold: 75, ...args.input }, + { + threshold, + facetName, + facetValue, + fallbackParameters, + maxRecommendations, + queryParameters, + }, context, info, await createProductMapper(context), - ), - trendingFacetValues: (root, args, context, info) => - getRecommendations( + ) + }, + trendingFacetValues: async (root, args, context, info) => { + const { threshold, fallbackParameters, maxRecommendations, queryParameters } = + await getRecommendationsArgs(root, args, context) + return getRecommendations( 'Trending_facet_values_Input', - { threshold: 75, ...args.input }, + { + facetName: args.input.facetName, + threshold, + fallbackParameters, + maxRecommendations, + queryParameters, + }, context, info, createFacetValueMapper(), - ), + ) + }, }, } @@ -66,14 +85,18 @@ type ProductResolver = ResolverFn< > if (isEnabled(import.meta.graphCommerce.algolia.relatedProducts)) { - const resolve: ProductResolver = async (root, args, context, info) => - getRecommendations( + const resolve: ProductResolver = async (root, args, context, info) => { + const { objectID, threshold, fallbackParameters, maxRecommendations, queryParameters } = + await getRecommendationsArgs(root, args, context) + + return getRecommendations( 'Related_products_Input', - { objectID: atob(root.uid), threshold: 75 }, + { objectID, threshold, fallbackParameters, maxRecommendations, queryParameters }, context, info, await createProductMapper(context), ) + } productInterfaceTypes.forEach((productType) => { if (!resolvers[productType]) resolvers[productType] = {} @@ -85,14 +108,17 @@ if (isEnabled(import.meta.graphCommerce.algolia.relatedProducts)) { } if (isEnabled(import.meta.graphCommerce.algolia.lookingSimilar)) { - const resolve: ProductResolver = async (root, args, context, info) => - getRecommendations( + const resolve: ProductResolver = async (root, args, context, info) => { + const { objectID, threshold, fallbackParameters, maxRecommendations, queryParameters } = + await getRecommendationsArgs(root, args, context) + return getRecommendations( 'Looking_similar_Input', - { objectID: atob(root.uid), threshold: 75 }, + { objectID, threshold, fallbackParameters, maxRecommendations, queryParameters }, context, info, await createProductMapper(context), ) + } productInterfaceTypes.forEach((productType) => { if (!resolvers[productType]) resolvers[productType] = {} @@ -104,14 +130,18 @@ if (isEnabled(import.meta.graphCommerce.algolia.lookingSimilar)) { } if (isEnabled(import.meta.graphCommerce.algolia.frequentlyBoughtTogether)) { - const resolver: ProductResolver = async (root, args, context, info) => - getRecommendations( + const resolver: ProductResolver = async (root, args, context, info) => { + const { objectID, threshold, fallbackParameters, maxRecommendations, queryParameters } = + await getRecommendationsArgs(root, args, context) + + return getRecommendations( 'Frequently_bought_together_Input', - { objectID: atob(root.uid), threshold: 75 }, + { objectID, threshold, maxRecommendations, queryParameters }, context, info, await createProductMapper(context), ) + } productInterfaceTypes.forEach((productType) => { if (!resolvers[productType]) resolvers[productType] = {} @@ -121,39 +151,66 @@ if (isEnabled(import.meta.graphCommerce.algolia.frequentlyBoughtTogether)) { }) } -type AlgoliaRecommendationResolver = ResolverFn< +const similar: ResolverFn< Maybe>>, ResolversParentTypes['ProductInterface'], MeshContext, { input?: AlgoliaLookingSimilarInput | null } -> +> = async (root, args, context, info) => { + const { objectID, threshold, fallbackParameters, maxRecommendations, queryParameters } = + await getRecommendationsArgs(root, args, context) -const similar: AlgoliaRecommendationResolver = async (root, args, context, info) => - getRecommendations( + return getRecommendations( 'Looking_similar_Input', - { objectID: atob(root.uid), threshold: 75, maxRecommendations: 8, ...args.input }, + { objectID, threshold, fallbackParameters, maxRecommendations, queryParameters }, context, info, await createProductMapper(context), ) +} -const related: AlgoliaRecommendationResolver = async (root, args, context, info) => - getRecommendations( +const related: ResolverFn< + Maybe>>, + ResolversParentTypes['ProductInterface'], + MeshContext, + { input?: AlgoliaRelatedProductsInput | null } +> = async (root, args, context, info) => { + const { objectID, threshold, fallbackParameters, maxRecommendations, queryParameters } = + await getRecommendationsArgs(root, args, context) + + console.log(queryParameters) + + return getRecommendations( 'Related_products_Input', - { objectID: atob(root.uid), threshold: 75, maxRecommendations: 8, ...args.input }, + { + objectID, + threshold, + // fallbackParameters, + maxRecommendations, + queryParameters, + }, context, info, await createProductMapper(context), ) +} -const together: AlgoliaRecommendationResolver = async (root, args, context, info) => - getRecommendations( +const together: ResolverFn< + Maybe>>, + ResolversParentTypes['ProductInterface'], + MeshContext, + { input?: AlgoliaRelatedProductsInput | null } +> = async (root, args, context, info) => { + const { objectID, threshold, fallbackParameters, maxRecommendations, queryParameters } = + await getRecommendationsArgs(root, args, context) + return getRecommendations( 'Frequently_bought_together_Input', - { objectID: atob(root.uid), threshold: 75, maxRecommendations: 8, ...args.input }, + { objectID, threshold, maxRecommendations, queryParameters }, context, info, await createProductMapper(context), ) +} productInterfaceTypes.forEach((productType) => { if (!resolvers[productType]) resolvers[productType] = {} diff --git a/packages/algolia-recommend/schema/AlgoliaRecommendations.graphqls b/packages/algolia-recommend/schema/AlgoliaProductRecommendations.graphqls similarity index 72% rename from packages/algolia-recommend/schema/AlgoliaRecommendations.graphqls rename to packages/algolia-recommend/schema/AlgoliaProductRecommendations.graphqls index 7559c1cae1..8f1e8b4d82 100644 --- a/packages/algolia-recommend/schema/AlgoliaRecommendations.graphqls +++ b/packages/algolia-recommend/schema/AlgoliaProductRecommendations.graphqls @@ -1,64 +1,59 @@ -input TrendingProductsInput { - maxRecommendations: Int = 8 - +input AlgoliaFallbackParams { """ - Minimum score a recommendation must have to be included in the response. + One or more keywords to use in a full-text search. """ - threshold: Float! = 75 - + search: String """ - Facet attribute. To be used in combination with `facetValue`. - If specified, only recommendations matching the facet filter will be returned. + The product attributes to search for and return. """ - facetName: String - + filter: ProductAttributeFilterInput """ - Facet value. To be used in combination with `facetName`. - If specified, only recommendations matching the facet filter will be returned. + Specifies which attributes to sort on, and whether to return the results in ascending or descending order. """ - facetValue: String + sort: ProductAttributeSortInput } -input TrendingFacetValuesInput { +input AlgoliaLookingSimilarInput { maxRecommendations: Int = 8 """ Minimum score a recommendation must have to be included in the response. """ threshold: Float! = 75 + """ - Facet attribute for which to retrieve trending facet values. + One or more keywords to use in a full-text search. """ - facetName: String! -} + search: String -type TrendingFacetValue { - facetName: String! - facetValue: String! -} - -extend type Query { - trendingProducts(input: TrendingProductsInput!): [ProductInterface] + """ + The product attributes to search for and return. + """ + filter: ProductAttributeFilterInput - trendingFacetValues(input: TrendingFacetValuesInput!): [TrendingFacetValue] + """ + When there are no related products, use this fallback query + """ + fallback: AlgoliaFallbackParams } -input AlgoliaLookingSimilarInput { +input AlgoliaFrequentlyBoughtTogetherInput { maxRecommendations: Int = 8 """ Minimum score a recommendation must have to be included in the response. """ threshold: Float! = 75 -} -input AlgoliaFrequentlyBoughtTogetherInput { - maxRecommendations: Int = 8 + """ + One or more keywords to use in a full-text search. + """ + search: String """ - Minimum score a recommendation must have to be included in the response. + The product attributes to search for and return. """ - threshold: Float! = 75 + filter: ProductAttributeFilterInput } input AlgoliaRelatedProductsInput { @@ -68,6 +63,21 @@ input AlgoliaRelatedProductsInput { Minimum score a recommendation must have to be included in the response. """ threshold: Float! = 75 + + """ + One or more keywords to use in a full-text search. + """ + search: String + + """ + The product attributes to search for and return. + """ + filter: ProductAttributeFilterInput + + """ + When there are no related products, use this fallback query + """ + fallback: AlgoliaFallbackParams } interface ProductInterface { diff --git a/packages/algolia-recommend/schema/AlgoliaTrending.graphqls b/packages/algolia-recommend/schema/AlgoliaTrending.graphqls new file mode 100644 index 0000000000..bfc3d7fde2 --- /dev/null +++ b/packages/algolia-recommend/schema/AlgoliaTrending.graphqls @@ -0,0 +1,74 @@ +input TrendingProductsInput { + maxRecommendations: Int = 8 + + """ + Minimum score a recommendation must have to be included in the response. + """ + threshold: Float! = 75 + + """ + Facet attribute. To be used in combination with `facetValue`. + If specified, only recommendations matching the facet filter will be returned. + """ + facetName: String! + + """ + Facet value. To be used in combination with `facetName`. + If specified, only recommendations matching the facet filter will be returned. + """ + facetValue: String! + + """ + One or more keywords to use in a full-text search. + """ + search: String + + """ + The product attributes to search for and return. + """ + filter: ProductAttributeFilterInput + + """ + When there are no related products, use this fallback query + """ + fallback: AlgoliaFallbackParams +} + +input TrendingFacetValuesInput { + maxRecommendations: Int = 8 + + """ + Minimum score a recommendation must have to be included in the response. + """ + threshold: Float! = 75 + """ + Facet attribute for which to retrieve trending facet values. + """ + facetName: String! + + """ + One or more keywords to use in a full-text search. + """ + search: String + + """ + The product attributes to search for and return. + """ + filter: ProductAttributeFilterInput + + """ + When there are no related products, use this fallback query + """ + fallback: AlgoliaFallbackParams +} + +type TrendingFacetValue { + facetName: String! + facetValue: String! +} + +extend type Query { + trendingProducts(input: TrendingProductsInput!): [ProductInterface] + + trendingFacetValues(input: TrendingFacetValuesInput!): [TrendingFacetValue] +} From ed2c312f08644ee5d49a1eb77b84c580ae5b1cd6 Mon Sep 17 00:00:00 2001 From: Renzo van Hootegem Date: Thu, 12 Sep 2024 10:10:50 +0200 Subject: [PATCH 37/70] Removed logs --- packages/algolia-recommend/mesh/getRecommendationArgs.ts | 2 -- packages/algolia-recommend/mesh/resolvers.ts | 2 -- 2 files changed, 4 deletions(-) diff --git a/packages/algolia-recommend/mesh/getRecommendationArgs.ts b/packages/algolia-recommend/mesh/getRecommendationArgs.ts index 384f100691..967de9003d 100644 --- a/packages/algolia-recommend/mesh/getRecommendationArgs.ts +++ b/packages/algolia-recommend/mesh/getRecommendationArgs.ts @@ -15,11 +15,9 @@ export async function getRecommendationQueryInput( if (queryParameters?.facets) delete queryParameters.facets if (queryParameters?.hitsPerPage) delete queryParameters.hitsPerPage if (queryParameters?.page) { - console.log('jajaja') delete queryParameters.page } - console.log('balbla', queryParameters) return queryParameters } diff --git a/packages/algolia-recommend/mesh/resolvers.ts b/packages/algolia-recommend/mesh/resolvers.ts index cb13c627a6..f929268863 100644 --- a/packages/algolia-recommend/mesh/resolvers.ts +++ b/packages/algolia-recommend/mesh/resolvers.ts @@ -178,8 +178,6 @@ const related: ResolverFn< const { objectID, threshold, fallbackParameters, maxRecommendations, queryParameters } = await getRecommendationsArgs(root, args, context) - console.log(queryParameters) - return getRecommendations( 'Related_products_Input', { From 0efac41dc4aea936af6ac31d93c62ee34c1df8e2 Mon Sep 17 00:00:00 2001 From: Renzo van Hootegem Date: Tue, 24 Sep 2024 10:45:23 +0200 Subject: [PATCH 38/70] Corrected import type --- packages/algolia-recommend/mesh/createValueFacetMapper.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/algolia-recommend/mesh/createValueFacetMapper.ts b/packages/algolia-recommend/mesh/createValueFacetMapper.ts index 1289a7f164..1b82210ee0 100644 --- a/packages/algolia-recommend/mesh/createValueFacetMapper.ts +++ b/packages/algolia-recommend/mesh/createValueFacetMapper.ts @@ -1,13 +1,13 @@ import type { AlgoliarecommendationsHit, AlgoliatrendingFacetHit, - TrendingFacet, + TrendingFacetValue, } from '@graphcommerce/graphql-mesh' export function createFacetValueMapper() { const isAlgoliaRecommendHit = (hit: AlgoliarecommendationsHit): hit is AlgoliatrendingFacetHit => !!hit && '__typename' in hit && hit.__typename === 'AlgoliatrendingFacetHit' - return (hit: AlgoliarecommendationsHit): TrendingFacet | null => + return (hit: AlgoliarecommendationsHit): TrendingFacetValue | null => isAlgoliaRecommendHit(hit) ? hit : null } From 29d8f02063bb6d174d9ca567749f8e345b040dcb Mon Sep 17 00:00:00 2001 From: Renzo van Hootegem Date: Tue, 24 Sep 2024 17:10:45 +0200 Subject: [PATCH 39/70] Added check for when page is 0 --- packages/algolia-recommend/mesh/getRecommendationArgs.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/algolia-recommend/mesh/getRecommendationArgs.ts b/packages/algolia-recommend/mesh/getRecommendationArgs.ts index 967de9003d..82caa6cc0d 100644 --- a/packages/algolia-recommend/mesh/getRecommendationArgs.ts +++ b/packages/algolia-recommend/mesh/getRecommendationArgs.ts @@ -14,7 +14,7 @@ export async function getRecommendationQueryInput( if (queryParameters?.facets) delete queryParameters.facets if (queryParameters?.hitsPerPage) delete queryParameters.hitsPerPage - if (queryParameters?.page) { + if (queryParameters?.page || queryParameters?.page === 0) { delete queryParameters.page } From 0ca9e27d0db68cd2c323303a293a0be94908dafa Mon Sep 17 00:00:00 2001 From: Renzo van Hootegem Date: Fri, 27 Sep 2024 11:45:56 +0200 Subject: [PATCH 40/70] Fixed recommendations bug, where always the same items are returned --- packages/algolia-mesh/mesh/getStoreConfig.ts | 4 +- .../mesh/getRecommendations.ts | 39 +++++++++++-------- packages/algolia-recommend/mesh/resolvers.ts | 25 +++++++++--- 3 files changed, 43 insertions(+), 25 deletions(-) diff --git a/packages/algolia-mesh/mesh/getStoreConfig.ts b/packages/algolia-mesh/mesh/getStoreConfig.ts index fb4bc3796c..8fee341eff 100644 --- a/packages/algolia-mesh/mesh/getStoreConfig.ts +++ b/packages/algolia-mesh/mesh/getStoreConfig.ts @@ -14,9 +14,9 @@ export type GetStoreConfigReturn = let configCache: Promise | undefined -export function getStoreConfig(context: MeshContext): Promise { +export async function getStoreConfig(context: MeshContext): Promise { if (!configCache) { - configCache = context.m2.Query.storeConfig({ + configCache = await context.m2.Query.storeConfig({ context, selectionSet: /* GraphQL */ ` { diff --git a/packages/algolia-recommend/mesh/getRecommendations.ts b/packages/algolia-recommend/mesh/getRecommendations.ts index a27480629e..0774eb7a97 100644 --- a/packages/algolia-recommend/mesh/getRecommendations.ts +++ b/packages/algolia-recommend/mesh/getRecommendations.ts @@ -15,12 +15,32 @@ const inputToModel = { Looking_similar_Input: 'looking_similar' as const, Related_products_Input: 'related_products' as const, } +function argsFromKeysInput(keys, args, context) { + const body = keys + .map( + (key) => + ({ + [key.keyInput]: { + model: inputToModel[key.keyInput as string], + indexName: getIndexName(context), + ...args, + objectID: key.objectId, + }, + }) as unknown as AlgoliarecommendationsRequest_Input, + ) + .filter(nonNullable) + + const returnObject = { input: { requests: body } } + + return returnObject +} export async function getRecommendations< K extends keyof AlgoliarecommendationsRequest_Input, Input extends AlgoliarecommendationsRequest_Input[K], R, >( + root, keyInput: K, args: Simplify, 'indexName' | 'model'>>, context: MeshContext, @@ -29,23 +49,8 @@ export async function getRecommendations< ) { return ( (await context.algoliaRecommend.Query.algolia_getRecommendations({ - key: keyInput, - argsFromKeys: (keys) => ({ - input: { - requests: keys - .map( - (key) => - ({ - [key]: { - model: inputToModel[key as string], - indexName: getIndexName(context), - ...args, - }, - }) as unknown as AlgoliarecommendationsRequest_Input, - ) - .filter(nonNullable), - }, - }), + key: { keyInput, objectId: atob(root.uid) }, + argsFromKeys: (keys) => argsFromKeysInput(keys, args, context), valuesFromResults: (res, keys) => keys .map((_key, index) => res?.results[index]) diff --git a/packages/algolia-recommend/mesh/resolvers.ts b/packages/algolia-recommend/mesh/resolvers.ts index f929268863..339dbf2274 100644 --- a/packages/algolia-recommend/mesh/resolvers.ts +++ b/packages/algolia-recommend/mesh/resolvers.ts @@ -30,6 +30,7 @@ const resolvers: Resolvers = { const { threshold, fallbackParameters, maxRecommendations, queryParameters } = await getRecommendationsArgs(root, args, context) return getRecommendations( + root, 'Trending_items_Input', { threshold, @@ -48,6 +49,7 @@ const resolvers: Resolvers = { const { threshold, fallbackParameters, maxRecommendations, queryParameters } = await getRecommendationsArgs(root, args, context) return getRecommendations( + root, 'Trending_facet_values_Input', { facetName: args.input.facetName, @@ -90,6 +92,7 @@ if (isEnabled(import.meta.graphCommerce.algolia.relatedProducts)) { await getRecommendationsArgs(root, args, context) return getRecommendations( + root, 'Related_products_Input', { objectID, threshold, fallbackParameters, maxRecommendations, queryParameters }, context, @@ -112,6 +115,7 @@ if (isEnabled(import.meta.graphCommerce.algolia.lookingSimilar)) { const { objectID, threshold, fallbackParameters, maxRecommendations, queryParameters } = await getRecommendationsArgs(root, args, context) return getRecommendations( + root, 'Looking_similar_Input', { objectID, threshold, fallbackParameters, maxRecommendations, queryParameters }, context, @@ -131,10 +135,11 @@ if (isEnabled(import.meta.graphCommerce.algolia.lookingSimilar)) { if (isEnabled(import.meta.graphCommerce.algolia.frequentlyBoughtTogether)) { const resolver: ProductResolver = async (root, args, context, info) => { - const { objectID, threshold, fallbackParameters, maxRecommendations, queryParameters } = + const { objectID, threshold, maxRecommendations, queryParameters } = await getRecommendationsArgs(root, args, context) return getRecommendations( + root, 'Frequently_bought_together_Input', { objectID, threshold, maxRecommendations, queryParameters }, context, @@ -161,6 +166,7 @@ const similar: ResolverFn< await getRecommendationsArgs(root, args, context) return getRecommendations( + root, 'Looking_similar_Input', { objectID, threshold, fallbackParameters, maxRecommendations, queryParameters }, context, @@ -175,10 +181,13 @@ const related: ResolverFn< MeshContext, { input?: AlgoliaRelatedProductsInput | null } > = async (root, args, context, info) => { - const { objectID, threshold, fallbackParameters, maxRecommendations, queryParameters } = - await getRecommendationsArgs(root, args, context) - + const { objectID, threshold, maxRecommendations, queryParameters } = await getRecommendationsArgs( + root, + args, + context, + ) return getRecommendations( + root, 'Related_products_Input', { objectID, @@ -199,9 +208,13 @@ const together: ResolverFn< MeshContext, { input?: AlgoliaRelatedProductsInput | null } > = async (root, args, context, info) => { - const { objectID, threshold, fallbackParameters, maxRecommendations, queryParameters } = - await getRecommendationsArgs(root, args, context) + const { objectID, threshold, maxRecommendations, queryParameters } = await getRecommendationsArgs( + root, + args, + context, + ) return getRecommendations( + root, 'Frequently_bought_together_Input', { objectID, threshold, maxRecommendations, queryParameters }, context, From 5d271e44ea26d75e3e1df6ae92e7c4360b0422f4 Mon Sep 17 00:00:00 2001 From: Renzo van Hootegem Date: Fri, 27 Sep 2024 11:58:12 +0200 Subject: [PATCH 41/70] Fixed storeconfig typing --- packages/algolia-mesh/mesh/getStoreConfig.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/algolia-mesh/mesh/getStoreConfig.ts b/packages/algolia-mesh/mesh/getStoreConfig.ts index 8fee341eff..c583027cdf 100644 --- a/packages/algolia-mesh/mesh/getStoreConfig.ts +++ b/packages/algolia-mesh/mesh/getStoreConfig.ts @@ -14,9 +14,9 @@ export type GetStoreConfigReturn = let configCache: Promise | undefined -export async function getStoreConfig(context: MeshContext): Promise { +export function getStoreConfig(context: MeshContext): Promise | undefined { if (!configCache) { - configCache = await context.m2.Query.storeConfig({ + configCache = context.m2.Query.storeConfig({ context, selectionSet: /* GraphQL */ ` { From 0f710ec9370f1ec536bb1d65e6216e2cfe34bd85 Mon Sep 17 00:00:00 2001 From: Renzo van Hootegem Date: Fri, 27 Sep 2024 12:16:20 +0200 Subject: [PATCH 42/70] Typing fixes --- packages/algolia-mesh/mesh/getStoreConfig.ts | 5 +++-- packages/algolia-recommend/mesh/getRecommendations.ts | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/packages/algolia-mesh/mesh/getStoreConfig.ts b/packages/algolia-mesh/mesh/getStoreConfig.ts index c583027cdf..05390c3ae1 100644 --- a/packages/algolia-mesh/mesh/getStoreConfig.ts +++ b/packages/algolia-mesh/mesh/getStoreConfig.ts @@ -12,9 +12,10 @@ export type GetStoreConfigReturn = > | undefined -let configCache: Promise | undefined +let configCache: Promise -export function getStoreConfig(context: MeshContext): Promise | undefined { +export function getStoreConfig(context: MeshContext): Promise { + // eslint-disable-next-line @typescript-eslint/no-misused-promises if (!configCache) { configCache = context.m2.Query.storeConfig({ context, diff --git a/packages/algolia-recommend/mesh/getRecommendations.ts b/packages/algolia-recommend/mesh/getRecommendations.ts index 0774eb7a97..8921e6e5f8 100644 --- a/packages/algolia-recommend/mesh/getRecommendations.ts +++ b/packages/algolia-recommend/mesh/getRecommendations.ts @@ -40,7 +40,7 @@ export async function getRecommendations< Input extends AlgoliarecommendationsRequest_Input[K], R, >( - root, + root: { uid: string }, keyInput: K, args: Simplify, 'indexName' | 'model'>>, context: MeshContext, From 77231e564c23dbb7a75943d323ae68e02d62e827 Mon Sep 17 00:00:00 2001 From: Renzo van Hootegem Date: Fri, 27 Sep 2024 13:55:05 +0200 Subject: [PATCH 43/70] Improved algolia typing --- packages/algolia-recommend/mesh/getRecommendations.ts | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/packages/algolia-recommend/mesh/getRecommendations.ts b/packages/algolia-recommend/mesh/getRecommendations.ts index 8921e6e5f8..5f592d34fe 100644 --- a/packages/algolia-recommend/mesh/getRecommendations.ts +++ b/packages/algolia-recommend/mesh/getRecommendations.ts @@ -1,3 +1,4 @@ +import { ProductsItemsItem } from '@graphcommerce/algolia-mesh' import { getIndexName } from '@graphcommerce/algolia-mesh/mesh/getIndexName' import type { AlgoliarecommendationsRequest_Input, @@ -15,6 +16,9 @@ const inputToModel = { Looking_similar_Input: 'looking_similar' as const, Related_products_Input: 'related_products' as const, } +function isAlgoliaResponse(root: T): root is T & { uid: string } { + return 'uid' in root +} function argsFromKeysInput(keys, args, context) { const body = keys .map( @@ -40,13 +44,16 @@ export async function getRecommendations< Input extends AlgoliarecommendationsRequest_Input[K], R, >( - root: { uid: string }, + root: ProductsItemsItem, keyInput: K, args: Simplify, 'indexName' | 'model'>>, context: MeshContext, info: GraphQLResolveInfo, mapper: (hit: AlgoliarecommendationsHit) => R, ) { + if (!isAlgoliaResponse(root)) { + return [] + } return ( (await context.algoliaRecommend.Query.algolia_getRecommendations({ key: { keyInput, objectId: atob(root.uid) }, From 6a252c6a7c7809b39e5ab9ae8e32cc01af1e02ab Mon Sep 17 00:00:00 2001 From: Renzo van Hootegem Date: Fri, 27 Sep 2024 14:39:52 +0200 Subject: [PATCH 44/70] Typing fixes --- packages/algolia-recommend/mesh/getRecommendations.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/algolia-recommend/mesh/getRecommendations.ts b/packages/algolia-recommend/mesh/getRecommendations.ts index 5f592d34fe..43ebf0445c 100644 --- a/packages/algolia-recommend/mesh/getRecommendations.ts +++ b/packages/algolia-recommend/mesh/getRecommendations.ts @@ -1,4 +1,3 @@ -import { ProductsItemsItem } from '@graphcommerce/algolia-mesh' import { getIndexName } from '@graphcommerce/algolia-mesh/mesh/getIndexName' import type { AlgoliarecommendationsRequest_Input, @@ -44,7 +43,7 @@ export async function getRecommendations< Input extends AlgoliarecommendationsRequest_Input[K], R, >( - root: ProductsItemsItem, + root: object, keyInput: K, args: Simplify, 'indexName' | 'model'>>, context: MeshContext, From 49c3d3b20cca7ffc47d0fc94c3685b954d18b976 Mon Sep 17 00:00:00 2001 From: Renzo van Hootegem Date: Mon, 30 Sep 2024 14:29:44 +0200 Subject: [PATCH 45/70] Moved the category search to its own folder --- .../mesh/algoliaHitToMagentoProduct.ts | 2 +- .../algoliaHitToMagentoCategory.ts | 4 +- .../{ => categories}/getCategoryResults.ts | 2 +- .../getCategoryResultsInput.ts | 0 .../algolia-mesh/mesh/categories/resolvers.ts | 43 +++++++++++++++++++ packages/algolia-mesh/mesh/resolvers.ts | 36 ---------------- ...gentoSearchApplyAlgoliaCategoriesEngine.ts | 26 +++++++++++ .../algolia-mesh/plugins/meshConfigAlgolia.ts | 1 + .../schema/AlgoliaSchema.graphqls | 16 +++++++ 9 files changed, 90 insertions(+), 40 deletions(-) rename packages/algolia-mesh/mesh/{ => categories}/algoliaHitToMagentoCategory.ts (92%) rename packages/algolia-mesh/mesh/{ => categories}/getCategoryResults.ts (94%) rename packages/algolia-mesh/mesh/{ => categories}/getCategoryResultsInput.ts (100%) create mode 100644 packages/algolia-mesh/mesh/categories/resolvers.ts create mode 100644 packages/algolia-mesh/plugins/magentoSearchApplyAlgoliaCategoriesEngine.ts diff --git a/packages/algolia-mesh/mesh/algoliaHitToMagentoProduct.ts b/packages/algolia-mesh/mesh/algoliaHitToMagentoProduct.ts index 33dc528e56..c222070bfb 100644 --- a/packages/algolia-mesh/mesh/algoliaHitToMagentoProduct.ts +++ b/packages/algolia-mesh/mesh/algoliaHitToMagentoProduct.ts @@ -81,7 +81,7 @@ function mapPriceRange( } } -function algoliaUrlToUrlKey(url?: string | null, base?: string | null): string | null { +export function algoliaUrlToUrlKey(url?: string | null, base?: string | null): string | null { if (!url || !base) return null return url.replace(base, '') } diff --git a/packages/algolia-mesh/mesh/algoliaHitToMagentoCategory.ts b/packages/algolia-mesh/mesh/categories/algoliaHitToMagentoCategory.ts similarity index 92% rename from packages/algolia-mesh/mesh/algoliaHitToMagentoCategory.ts rename to packages/algolia-mesh/mesh/categories/algoliaHitToMagentoCategory.ts index 5a269714ad..ac145daccd 100644 --- a/packages/algolia-mesh/mesh/algoliaHitToMagentoCategory.ts +++ b/packages/algolia-mesh/mesh/categories/algoliaHitToMagentoCategory.ts @@ -8,8 +8,8 @@ import { Algoliahit, AlgoliaProductHitAdditionalProperties, } from '@graphcommerce/graphql-mesh' -import { algoliaUrlToUrlKey } from './algoliaHitToMagentoProduct' -import { GetStoreConfigReturn } from './getStoreConfig' +import { algoliaUrlToUrlKey } from '../algoliaHitToMagentoProduct' +import { GetStoreConfigReturn } from '../getStoreConfig' export type AlgoliaCategoryHitAddiotonalProperties = AlgoliaProductHitAdditionalProperties & { path: string diff --git a/packages/algolia-mesh/mesh/getCategoryResults.ts b/packages/algolia-mesh/mesh/categories/getCategoryResults.ts similarity index 94% rename from packages/algolia-mesh/mesh/getCategoryResults.ts rename to packages/algolia-mesh/mesh/categories/getCategoryResults.ts index 8f4fa5211b..3633745876 100644 --- a/packages/algolia-mesh/mesh/getCategoryResults.ts +++ b/packages/algolia-mesh/mesh/categories/getCategoryResults.ts @@ -1,7 +1,7 @@ import { MeshContext, QueryproductsArgs } from '@graphcommerce/graphql-mesh' import type { GraphQLResolveInfo } from 'graphql' +import { getIndexName } from '../getIndexName' import { getCategoryResultsInput } from './getCategoryResultsInput' -import { getIndexName } from './getIndexName' export async function getCategoryResults( args: QueryproductsArgs, diff --git a/packages/algolia-mesh/mesh/getCategoryResultsInput.ts b/packages/algolia-mesh/mesh/categories/getCategoryResultsInput.ts similarity index 100% rename from packages/algolia-mesh/mesh/getCategoryResultsInput.ts rename to packages/algolia-mesh/mesh/categories/getCategoryResultsInput.ts diff --git a/packages/algolia-mesh/mesh/categories/resolvers.ts b/packages/algolia-mesh/mesh/categories/resolvers.ts new file mode 100644 index 0000000000..82ba6e349d --- /dev/null +++ b/packages/algolia-mesh/mesh/categories/resolvers.ts @@ -0,0 +1,43 @@ +import { type Resolvers } from '@graphcommerce/graphql-mesh' +import { getStoreConfig } from '../getStoreConfig' +import { algoliaHitToMagentoCategory, CategoriesItemsItem } from './algoliaHitToMagentoCategory' +import { getCategoryResults } from './getCategoryResults' + +export const resolvers: Resolvers = { + Query: { + categories: async (root, args, context, info) => { + const todo = { algoliaCategories: true } + if (!todo.algoliaCategories) return context.m2.Query.categories({ root, args, context, info }) + const algoliaResponse = await getCategoryResults(args, context, info) + const items: (CategoriesItemsItem | null)[] = [] + const storeConfig = await getStoreConfig(context) + if (!algoliaResponse?.hits) { + return { + items: [], + page_info: { + current_page: 1, + page_size: 20, + total_pages: 1, + }, + total_count: 0, + } + } + for (const hit of algoliaResponse.hits) { + if (hit?.objectID) { + const category = algoliaHitToMagentoCategory(hit, storeConfig) + items.push(category) + } + } + + return { + items, + page_info: { + current_page: 1, + page_size: 20, + total_pages: 1, + }, + total_count: 0, + } + }, + }, +} diff --git a/packages/algolia-mesh/mesh/resolvers.ts b/packages/algolia-mesh/mesh/resolvers.ts index ec6460f21e..acfcd04704 100644 --- a/packages/algolia-mesh/mesh/resolvers.ts +++ b/packages/algolia-mesh/mesh/resolvers.ts @@ -5,11 +5,9 @@ import { } from '@graphcommerce/graphql-mesh' import type { GraphQLResolveInfo } from 'graphql' import { algoliaFacetsToAggregations, getCategoryList } from './algoliaFacetsToAggregations' -import { algoliaHitToMagentoCategory, CategoriesItemsItem } from './algoliaHitToMagentoCategory' import { algoliaHitToMagentoProduct, ProductsItemsItem } from './algoliaHitToMagentoProduct' import { getAlgoliaSettings } from './getAlgoliaSettings' import { getAttributeList } from './getAttributeList' -import { getCategoryResults } from './getCategoryResults' import { getGroupId } from './getGroupId' import { getSearchResults } from './getSearchResults' import { getSearchSuggestions } from './getSearchSuggestions' @@ -117,39 +115,5 @@ export const resolvers: Resolvers = { algolia_queryID: (await searchResults)?.queryID, } }, - categories: async (root, args, context, info) => { - const todo = { algoliaCategories: true } - if (!todo.algoliaCategories) return context.m2.Query.categories({ root, args, context, info }) - const algoliaResponse = await getCategoryResults(args, context, info) - const items: (CategoriesItemsItem | null)[] = [] - const storeConfig = await getStoreConfig(context) - if (!algoliaResponse?.hits) { - return { - items: [], - page_info: { - current_page: 1, - page_size: 20, - total_pages: 1, - }, - total_count: 0, - } - } - for (const hit of algoliaResponse.hits) { - if (hit?.objectID) { - const category = algoliaHitToMagentoCategory(hit, storeConfig) - items.push(category) - } - } - - return { - items, - page_info: { - current_page: 1, - page_size: 20, - total_pages: 1, - }, - total_count: 0, - } - }, }, } diff --git a/packages/algolia-mesh/plugins/magentoSearchApplyAlgoliaCategoriesEngine.ts b/packages/algolia-mesh/plugins/magentoSearchApplyAlgoliaCategoriesEngine.ts new file mode 100644 index 0000000000..e8fac28fe9 --- /dev/null +++ b/packages/algolia-mesh/plugins/magentoSearchApplyAlgoliaCategoriesEngine.ts @@ -0,0 +1,26 @@ +import type { + useProductListApplySearchDefaults as useProductListApplyDefaults, + productListApplySearchDefaults as productListApplyDefaults, + searchDefaultsToProductListFilters as defaultsToProductListFilters, +} from '@graphcommerce/magento-search' +import { FunctionPlugin, PluginConfig } from '@graphcommerce/next-config' +import { applyEngineVariables } from '../utils/applyEngineVariable' + +export const config: PluginConfig = { + module: '@graphcommerce/magento-search', + type: 'function', +} + +export const useProductListApplySearchDefaults: FunctionPlugin< + typeof useProductListApplyDefaults +> = (prev, params) => applyEngineVariables(prev(params)) + +export const productListApplySearchDefaults: FunctionPlugin = ( + prev, + params, + conf, +) => applyEngineVariables(prev(params, conf)) + +export const searchDefaultsToProductListFilters: FunctionPlugin< + typeof defaultsToProductListFilters +> = (prev, variables) => applyEngineVariables(prev(variables)) diff --git a/packages/algolia-mesh/plugins/meshConfigAlgolia.ts b/packages/algolia-mesh/plugins/meshConfigAlgolia.ts index 1a0bf8a572..f06370f221 100644 --- a/packages/algolia-mesh/plugins/meshConfigAlgolia.ts +++ b/packages/algolia-mesh/plugins/meshConfigAlgolia.ts @@ -60,6 +60,7 @@ export const meshConfig: FunctionPlugin = ( additionalResolvers: [ ...(baseConfig.additionalResolvers ?? []), '@graphcommerce/algolia-mesh/mesh/resolvers.ts', + '@graphcommerce/algolia-mesh/mesh/categories/resolvers.ts', ], }, graphCommerceConfig, diff --git a/packages/algolia-mesh/schema/AlgoliaSchema.graphqls b/packages/algolia-mesh/schema/AlgoliaSchema.graphqls index c0037d88b1..a280870cbc 100644 --- a/packages/algolia-mesh/schema/AlgoliaSchema.graphqls +++ b/packages/algolia-mesh/schema/AlgoliaSchema.graphqls @@ -44,6 +44,22 @@ input ProductAttributeFilterInput { This is faster, but is only recommended for search and category pages as the data could be out of date and many fields of a product aren't supported. """ engine: FilterEqualTypeInput + + """ + When set to true, the categories query is passed to Algolia + + This is faster, but is only recommended for search and category pages as the data could be out of date and many fields of a product aren't supported. + """ + categories_engine: FilterEqualTypeInput +} + +input CategoryFilterInput { + """ + When set to true, the categories query is passed to Algolia + + This is faster, but is only recommended for search and category pages as the data could be out of date and many fields of a product aren't supported. + """ + categories_engine: FilterEqualTypeInput } type Products { From 16354b90f472d6af97d4016426dc15725edd020f Mon Sep 17 00:00:00 2001 From: Renzo van Hootegem Date: Mon, 30 Sep 2024 14:38:47 +0200 Subject: [PATCH 46/70] commented code --- ...gentoSearchApplyAlgoliaCategoriesEngine.ts | 44 +++++++++---------- 1 file changed, 22 insertions(+), 22 deletions(-) diff --git a/packages/algolia-mesh/plugins/magentoSearchApplyAlgoliaCategoriesEngine.ts b/packages/algolia-mesh/plugins/magentoSearchApplyAlgoliaCategoriesEngine.ts index e8fac28fe9..6c7e2aeb69 100644 --- a/packages/algolia-mesh/plugins/magentoSearchApplyAlgoliaCategoriesEngine.ts +++ b/packages/algolia-mesh/plugins/magentoSearchApplyAlgoliaCategoriesEngine.ts @@ -1,26 +1,26 @@ -import type { - useProductListApplySearchDefaults as useProductListApplyDefaults, - productListApplySearchDefaults as productListApplyDefaults, - searchDefaultsToProductListFilters as defaultsToProductListFilters, -} from '@graphcommerce/magento-search' -import { FunctionPlugin, PluginConfig } from '@graphcommerce/next-config' -import { applyEngineVariables } from '../utils/applyEngineVariable' +// import type { +// useProductListApplySearchDefaults as useProductListApplyDefaults, +// productListApplySearchDefaults as productListApplyDefaults, +// searchDefaultsToProductListFilters as defaultsToProductListFilters, +// } from '@graphcommerce/magento-search' +// import { FunctionPlugin, PluginConfig } from '@graphcommerce/next-config' +// import { applyEngineVariables } from '../utils/applyEngineVariable' -export const config: PluginConfig = { - module: '@graphcommerce/magento-search', - type: 'function', -} +// export const config: PluginConfig = { +// module: '@graphcommerce/magento-search', +// type: 'function', +// } -export const useProductListApplySearchDefaults: FunctionPlugin< - typeof useProductListApplyDefaults -> = (prev, params) => applyEngineVariables(prev(params)) +// export const useProductListApplySearchDefaults: FunctionPlugin< +// typeof useProductListApplyDefaults +// > = (prev, params) => applyEngineVariables(prev(params)) -export const productListApplySearchDefaults: FunctionPlugin = ( - prev, - params, - conf, -) => applyEngineVariables(prev(params, conf)) +// export const productListApplySearchDefaults: FunctionPlugin = ( +// prev, +// params, +// conf, +// ) => applyEngineVariables(prev(params, conf)) -export const searchDefaultsToProductListFilters: FunctionPlugin< - typeof defaultsToProductListFilters -> = (prev, variables) => applyEngineVariables(prev(variables)) +// export const searchDefaultsToProductListFilters: FunctionPlugin< +// typeof defaultsToProductListFilters +// > = (prev, variables) => applyEngineVariables(prev(variables)) From daf3e3c00995a9078dc2e8b8644d709e74949332 Mon Sep 17 00:00:00 2001 From: Renzo van Hootegem Date: Mon, 30 Sep 2024 15:13:16 +0200 Subject: [PATCH 47/70] Added check if engine filter is available --- packages/algolia-mesh/mesh/categories/resolvers.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/packages/algolia-mesh/mesh/categories/resolvers.ts b/packages/algolia-mesh/mesh/categories/resolvers.ts index 82ba6e349d..50720bbbd9 100644 --- a/packages/algolia-mesh/mesh/categories/resolvers.ts +++ b/packages/algolia-mesh/mesh/categories/resolvers.ts @@ -6,8 +6,11 @@ import { getCategoryResults } from './getCategoryResults' export const resolvers: Resolvers = { Query: { categories: async (root, args, context, info) => { - const todo = { algoliaCategories: true } - if (!todo.algoliaCategories) return context.m2.Query.categories({ root, args, context, info }) + const isAgolia = + (args.filters?.categories_engine?.in ?? [args.filters?.categories_engine?.eq])[0] === + 'algolia' + + if (!isAgolia) return context.m2.Query.categories({ root, args, context, info }) const algoliaResponse = await getCategoryResults(args, context, info) const items: (CategoriesItemsItem | null)[] = [] const storeConfig = await getStoreConfig(context) From 42982958249c0bd33242ca6c18620b24b5ed4f06 Mon Sep 17 00:00:00 2001 From: Renzo van Hootegem Date: Tue, 1 Oct 2024 10:32:01 +0200 Subject: [PATCH 48/70] Now returns the complete dataset from algolia. Renamed engine --- .../mesh/categories/algoliaHitToMagentoCategory.ts | 8 ++++++++ .../algolia-mesh/mesh/categories/getCategoryResults.ts | 4 ++-- .../mesh/categories/getCategoryResultsInput.ts | 9 ++++++--- packages/algolia-mesh/mesh/categories/resolvers.ts | 4 +--- packages/algolia-mesh/schema/AlgoliaSchema.graphqls | 9 +-------- 5 files changed, 18 insertions(+), 16 deletions(-) diff --git a/packages/algolia-mesh/mesh/categories/algoliaHitToMagentoCategory.ts b/packages/algolia-mesh/mesh/categories/algoliaHitToMagentoCategory.ts index ac145daccd..5190c95ad2 100644 --- a/packages/algolia-mesh/mesh/categories/algoliaHitToMagentoCategory.ts +++ b/packages/algolia-mesh/mesh/categories/algoliaHitToMagentoCategory.ts @@ -14,6 +14,10 @@ import { GetStoreConfigReturn } from '../getStoreConfig' export type AlgoliaCategoryHitAddiotonalProperties = AlgoliaProductHitAdditionalProperties & { path: string url: string + product_count: number + meta_description: string + meta_title: string + level: number } export function assertAdditional( @@ -60,5 +64,9 @@ export function algoliaHitToMagentoCategory( url_key: '', url_path: algoliaUrlToUrlKey(additionalProperties.url, storeConfig?.base_link_url), breadcrumbs: mapBreadcrumbs(additionalProperties?.path), + product_count: additionalProperties?.product_count, + meta_description: additionalProperties?.meta_description, + meta_title: additionalProperties?.meta_title, + level: additionalProperties?.level, } } diff --git a/packages/algolia-mesh/mesh/categories/getCategoryResults.ts b/packages/algolia-mesh/mesh/categories/getCategoryResults.ts index 3633745876..e6afe0c674 100644 --- a/packages/algolia-mesh/mesh/categories/getCategoryResults.ts +++ b/packages/algolia-mesh/mesh/categories/getCategoryResults.ts @@ -1,10 +1,10 @@ -import { MeshContext, QueryproductsArgs } from '@graphcommerce/graphql-mesh' +import { MeshContext, QuerycategoryListArgs } from '@graphcommerce/graphql-mesh' import type { GraphQLResolveInfo } from 'graphql' import { getIndexName } from '../getIndexName' import { getCategoryResultsInput } from './getCategoryResultsInput' export async function getCategoryResults( - args: QueryproductsArgs, + args: QuerycategoryListArgs, context: MeshContext, info: GraphQLResolveInfo, ) { diff --git a/packages/algolia-mesh/mesh/categories/getCategoryResultsInput.ts b/packages/algolia-mesh/mesh/categories/getCategoryResultsInput.ts index 6bcc731ec3..b7a025e574 100644 --- a/packages/algolia-mesh/mesh/categories/getCategoryResultsInput.ts +++ b/packages/algolia-mesh/mesh/categories/getCategoryResultsInput.ts @@ -1,11 +1,14 @@ -import { Queryalgolia_searchSingleIndexArgs, QueryproductsArgs } from '@graphcommerce/graphql-mesh' +import { + Queryalgolia_searchSingleIndexArgs, + QuerycategoryListArgs, +} from '@graphcommerce/graphql-mesh' // eslint-disable-next-line @typescript-eslint/require-await export async function getCategoryResultsInput( - args: QueryproductsArgs, + args: QuerycategoryListArgs, ): Promise { return { - query: args.search ?? '', + query: args.filters?.name?.match ?? '', hitsPerPage: args.pageSize ? args.pageSize : 10, page: args.currentPage ? args.currentPage - 1 : 0, } diff --git a/packages/algolia-mesh/mesh/categories/resolvers.ts b/packages/algolia-mesh/mesh/categories/resolvers.ts index 50720bbbd9..2c3544c0ed 100644 --- a/packages/algolia-mesh/mesh/categories/resolvers.ts +++ b/packages/algolia-mesh/mesh/categories/resolvers.ts @@ -6,9 +6,7 @@ import { getCategoryResults } from './getCategoryResults' export const resolvers: Resolvers = { Query: { categories: async (root, args, context, info) => { - const isAgolia = - (args.filters?.categories_engine?.in ?? [args.filters?.categories_engine?.eq])[0] === - 'algolia' + const isAgolia = (args.filters?.engine?.in ?? [args.filters?.engine?.eq])[0] === 'algolia' if (!isAgolia) return context.m2.Query.categories({ root, args, context, info }) const algoliaResponse = await getCategoryResults(args, context, info) diff --git a/packages/algolia-mesh/schema/AlgoliaSchema.graphqls b/packages/algolia-mesh/schema/AlgoliaSchema.graphqls index a280870cbc..0c8e9cc367 100644 --- a/packages/algolia-mesh/schema/AlgoliaSchema.graphqls +++ b/packages/algolia-mesh/schema/AlgoliaSchema.graphqls @@ -44,13 +44,6 @@ input ProductAttributeFilterInput { This is faster, but is only recommended for search and category pages as the data could be out of date and many fields of a product aren't supported. """ engine: FilterEqualTypeInput - - """ - When set to true, the categories query is passed to Algolia - - This is faster, but is only recommended for search and category pages as the data could be out of date and many fields of a product aren't supported. - """ - categories_engine: FilterEqualTypeInput } input CategoryFilterInput { @@ -59,7 +52,7 @@ input CategoryFilterInput { This is faster, but is only recommended for search and category pages as the data could be out of date and many fields of a product aren't supported. """ - categories_engine: FilterEqualTypeInput + engine: FilterEqualTypeInput } type Products { From f0b482a3af150f50672e241cd338218be9f81e13 Mon Sep 17 00:00:00 2001 From: Renzo van Hootegem Date: Tue, 1 Oct 2024 10:52:13 +0200 Subject: [PATCH 49/70] Put calls into a promise to improve performance --- .../algolia-mesh/mesh/categories/resolvers.ts | 29 ++++++++----------- 1 file changed, 12 insertions(+), 17 deletions(-) diff --git a/packages/algolia-mesh/mesh/categories/resolvers.ts b/packages/algolia-mesh/mesh/categories/resolvers.ts index 2c3544c0ed..7221be97ea 100644 --- a/packages/algolia-mesh/mesh/categories/resolvers.ts +++ b/packages/algolia-mesh/mesh/categories/resolvers.ts @@ -9,20 +9,15 @@ export const resolvers: Resolvers = { const isAgolia = (args.filters?.engine?.in ?? [args.filters?.engine?.eq])[0] === 'algolia' if (!isAgolia) return context.m2.Query.categories({ root, args, context, info }) - const algoliaResponse = await getCategoryResults(args, context, info) + const items: (CategoriesItemsItem | null)[] = [] - const storeConfig = await getStoreConfig(context) - if (!algoliaResponse?.hits) { - return { - items: [], - page_info: { - current_page: 1, - page_size: 20, - total_pages: 1, - }, - total_count: 0, - } - } + + const [algoliaResponse, storeConfig] = await Promise.all([ + await getCategoryResults(args, context, info), + await getStoreConfig(context), + ]) + + if (!algoliaResponse?.hits) return context.m2.Query.categories({ root, args, context, info }) for (const hit of algoliaResponse.hits) { if (hit?.objectID) { const category = algoliaHitToMagentoCategory(hit, storeConfig) @@ -33,11 +28,11 @@ export const resolvers: Resolvers = { return { items, page_info: { - current_page: 1, - page_size: 20, - total_pages: 1, + current_page: algoliaResponse.page + 1, + page_size: algoliaResponse.hitsPerPage, + total_pages: algoliaResponse.nbPages, }, - total_count: 0, + total_count: algoliaResponse.nbHits, } }, }, From 165e786ca83972d90a69c13a23480ea5c9958e54 Mon Sep 17 00:00:00 2001 From: Renzo van Hootegem Date: Tue, 1 Oct 2024 10:58:18 +0200 Subject: [PATCH 50/70] Created seperate getIndexname function for categories --- .../mesh/categories/getCategoryResults.ts | 4 ++-- packages/algolia-mesh/mesh/categories/getIndexName.ts | 11 +++++++++++ packages/algolia-mesh/mesh/getIndexName.ts | 4 ++-- 3 files changed, 15 insertions(+), 4 deletions(-) create mode 100644 packages/algolia-mesh/mesh/categories/getIndexName.ts diff --git a/packages/algolia-mesh/mesh/categories/getCategoryResults.ts b/packages/algolia-mesh/mesh/categories/getCategoryResults.ts index e6afe0c674..ce0a0680e8 100644 --- a/packages/algolia-mesh/mesh/categories/getCategoryResults.ts +++ b/packages/algolia-mesh/mesh/categories/getCategoryResults.ts @@ -1,7 +1,7 @@ import { MeshContext, QuerycategoryListArgs } from '@graphcommerce/graphql-mesh' import type { GraphQLResolveInfo } from 'graphql' -import { getIndexName } from '../getIndexName' import { getCategoryResultsInput } from './getCategoryResultsInput' +import { getIndexName } from './getIndexName' export async function getCategoryResults( args: QuerycategoryListArgs, @@ -10,7 +10,7 @@ export async function getCategoryResults( ) { return context.algolia.Query.algolia_searchSingleIndex({ args: { - indexName: getIndexName(context, 'categories'), + indexName: getIndexName(context), input: await getCategoryResultsInput(args), }, selectionSet: /* GraphQL */ ` diff --git a/packages/algolia-mesh/mesh/categories/getIndexName.ts b/packages/algolia-mesh/mesh/categories/getIndexName.ts new file mode 100644 index 0000000000..031caff6ab --- /dev/null +++ b/packages/algolia-mesh/mesh/categories/getIndexName.ts @@ -0,0 +1,11 @@ +import { MeshContext } from '@graphcommerce/graphql-mesh' +import { storefrontConfigDefault } from '@graphcommerce/next-ui' + +function getStoreHeader(context: MeshContext) { + return (context as MeshContext & { headers: Record }).headers.store +} + +export function getIndexName(context: MeshContext) { + const storeCode = getStoreHeader(context) ?? storefrontConfigDefault().magentoStoreCode + return `${import.meta.graphCommerce.algolia.indexNamePrefix}${storeCode}_categories` +} diff --git a/packages/algolia-mesh/mesh/getIndexName.ts b/packages/algolia-mesh/mesh/getIndexName.ts index a50337ee07..4678635a25 100644 --- a/packages/algolia-mesh/mesh/getIndexName.ts +++ b/packages/algolia-mesh/mesh/getIndexName.ts @@ -5,7 +5,7 @@ function getStoreHeader(context: MeshContext) { return (context as MeshContext & { headers: Record }).headers.store } -export function getIndexName(context: MeshContext, indexType: string = 'products') { +export function getIndexName(context: MeshContext) { const storeCode = getStoreHeader(context) ?? storefrontConfigDefault().magentoStoreCode - return `${import.meta.graphCommerce.algolia.indexNamePrefix}${storeCode}_${indexType}` + return `${import.meta.graphCommerce.algolia.indexNamePrefix}${storeCode}_products` } From 0f00e8ef56a4403c682dfeffd9a865a110cc26fd Mon Sep 17 00:00:00 2001 From: Renzo van Hootegem Date: Tue, 1 Oct 2024 11:06:24 +0200 Subject: [PATCH 51/70] Removed some personalisation plugins, while that is enabled in algolia dashboard --- .../plugins/getSearchResultsAnalytics.ts | 2 +- .../plugins/getSearchResultsPersonalization.ts | 18 ------------------ .../getSearchSuggestionsPersonalization.ts | 16 ---------------- 3 files changed, 1 insertion(+), 35 deletions(-) delete mode 100644 packages/algolia-personalization/plugins/getSearchResultsPersonalization.ts delete mode 100644 packages/algolia-personalization/plugins/getSearchSuggestionsPersonalization.ts diff --git a/packages/algolia-personalization/plugins/getSearchResultsAnalytics.ts b/packages/algolia-personalization/plugins/getSearchResultsAnalytics.ts index 082866dd89..fa4c57ca37 100644 --- a/packages/algolia-personalization/plugins/getSearchResultsAnalytics.ts +++ b/packages/algolia-personalization/plugins/getSearchResultsAnalytics.ts @@ -5,7 +5,7 @@ import { getUserToken } from '../mesh/getUserToken' export const config: PluginConfig = { type: 'function', module: '@graphcommerce/algolia-mesh', - ifConfig: 'algolia.personalizationEnabled', + ifConfig: 'algolia.analyticsEnabled', } export const getSearchResultsInput: FunctionPlugin = async ( diff --git a/packages/algolia-personalization/plugins/getSearchResultsPersonalization.ts b/packages/algolia-personalization/plugins/getSearchResultsPersonalization.ts deleted file mode 100644 index 4427a91edf..0000000000 --- a/packages/algolia-personalization/plugins/getSearchResultsPersonalization.ts +++ /dev/null @@ -1,18 +0,0 @@ -import type { getSearchResultsInput as getSearchResultsInputType } from '@graphcommerce/algolia-mesh' -import { FunctionPlugin, PluginConfig } from '@graphcommerce/next-config' - -export const config: PluginConfig = { - type: 'function', - module: '@graphcommerce/algolia-mesh', - ifConfig: 'algolia.personalizationEnabled', -} - -export const getSearchResultsInput: FunctionPlugin = async ( - prev, - args, - context, -) => ({ - ...(await prev(args, context)), - // enablePersonalization: true, - // personalizationImpact: 50, -}) diff --git a/packages/algolia-personalization/plugins/getSearchSuggestionsPersonalization.ts b/packages/algolia-personalization/plugins/getSearchSuggestionsPersonalization.ts deleted file mode 100644 index 58300e4964..0000000000 --- a/packages/algolia-personalization/plugins/getSearchSuggestionsPersonalization.ts +++ /dev/null @@ -1,16 +0,0 @@ -import type { getSearchSuggestionsInput as getSearchSuggestionsInputType } from '@graphcommerce/algolia-mesh' -import { FunctionPlugin, PluginConfig } from '@graphcommerce/next-config' - -export const config: PluginConfig = { - type: 'function', - module: '@graphcommerce/algolia-mesh', - ifConfig: 'algolia.personalizationEnabled', -} - -export const getSearchSuggestionsInput: FunctionPlugin< - typeof getSearchSuggestionsInputType -> = async (prev, search, context) => ({ - ...(await prev(search, context)), - // enablePersonalization: true, - // personalizationImpact: 50, -}) From 7c3d984c12d0225f2fd1781e41662b6683ac8ff6 Mon Sep 17 00:00:00 2001 From: Renzo van Hootegem Date: Tue, 1 Oct 2024 11:06:35 +0200 Subject: [PATCH 52/70] Removed empty file --- ...gentoSearchApplyAlgoliaCategoriesEngine.ts | 26 ------------------- 1 file changed, 26 deletions(-) delete mode 100644 packages/algolia-mesh/plugins/magentoSearchApplyAlgoliaCategoriesEngine.ts diff --git a/packages/algolia-mesh/plugins/magentoSearchApplyAlgoliaCategoriesEngine.ts b/packages/algolia-mesh/plugins/magentoSearchApplyAlgoliaCategoriesEngine.ts deleted file mode 100644 index 6c7e2aeb69..0000000000 --- a/packages/algolia-mesh/plugins/magentoSearchApplyAlgoliaCategoriesEngine.ts +++ /dev/null @@ -1,26 +0,0 @@ -// import type { -// useProductListApplySearchDefaults as useProductListApplyDefaults, -// productListApplySearchDefaults as productListApplyDefaults, -// searchDefaultsToProductListFilters as defaultsToProductListFilters, -// } from '@graphcommerce/magento-search' -// import { FunctionPlugin, PluginConfig } from '@graphcommerce/next-config' -// import { applyEngineVariables } from '../utils/applyEngineVariable' - -// export const config: PluginConfig = { -// module: '@graphcommerce/magento-search', -// type: 'function', -// } - -// export const useProductListApplySearchDefaults: FunctionPlugin< -// typeof useProductListApplyDefaults -// > = (prev, params) => applyEngineVariables(prev(params)) - -// export const productListApplySearchDefaults: FunctionPlugin = ( -// prev, -// params, -// conf, -// ) => applyEngineVariables(prev(params, conf)) - -// export const searchDefaultsToProductListFilters: FunctionPlugin< -// typeof defaultsToProductListFilters -// > = (prev, variables) => applyEngineVariables(prev(variables)) From 3ab8a39e757b195d5441e23d4f91c117b99e7179 Mon Sep 17 00:00:00 2001 From: Renzo van Hootegem Date: Tue, 1 Oct 2024 11:13:32 +0200 Subject: [PATCH 53/70] Removed personalization config, while this is enabled in algolia dashboard --- packages/algolia-personalization/Config.graphqls | 4 ---- 1 file changed, 4 deletions(-) diff --git a/packages/algolia-personalization/Config.graphqls b/packages/algolia-personalization/Config.graphqls index d7c30d3ec6..9465ef498f 100644 --- a/packages/algolia-personalization/Config.graphqls +++ b/packages/algolia-personalization/Config.graphqls @@ -3,8 +3,4 @@ extend input GraphCommerceAlgoliaConfig { Send algolia analytics events. """ analyticsEnabled: Boolean - """ - Enable personalized queries. - """ - personalizationEnabled: Boolean } From 435f0e92b0d6c8e83df1f258d43099e0f44f2e56 Mon Sep 17 00:00:00 2001 From: Renzo van Hootegem Date: Tue, 1 Oct 2024 12:04:34 +0200 Subject: [PATCH 54/70] Renamed graphql-mesh to graphql products and added own package for categories --- .../mesh}/algoliaHitToMagentoCategory.ts | 0 .../mesh}/getCategoryResults.ts | 0 .../mesh}/getCategoryResultsInput.ts | 0 .../mesh}/getIndexName.ts | 0 .../mesh}/resolvers.ts | 0 packages/algolia-categories/package.json | 33 +++++++++ .../plugins/meshConfigAlgoliaCategories.ts | 22 ++++++ .../hooks/useSendAlgoliaEvent.ts | 2 +- packages/algolia-personalization/package.json | 2 +- .../CHANGELOG.md | 0 .../Config.graphqls | 0 .../README.md | 0 .../algolia-spec.yaml | 0 .../graphql/CustomerGroupId.graphql | 0 .../graphql/GetAlgoliaSettings.graphql | 0 .../graphql/ProductListItems_Algolia.graphql | 0 .../hooks/useAlgoliaIndexName.ts | 0 .../hooks/useAlgoliaQueryContext.ts | 0 .../index.ts | 0 .../link/customerGroupIdLink.ts | 0 .../mesh/algoliaFacetsToAggregations.ts | 0 .../mesh/algoliaHitToMagentoProduct.ts | 0 .../categories/algoliaHitToMagentoCategory.ts | 72 +++++++++++++++++++ .../mesh/categories/getCategoryResults.ts | 34 +++++++++ .../categories/getCategoryResultsInput.ts | 15 ++++ .../mesh/categories/getIndexName.ts | 11 +++ .../mesh/categories/resolvers.ts | 39 ++++++++++ .../mesh/getAlgoliaSettings.ts | 0 .../mesh/getAttributeList.ts | 0 .../mesh/getGroupId.ts | 0 .../mesh/getIndexName.ts | 0 .../mesh/getSearchResults.ts | 0 .../mesh/getSearchResultsInput.ts | 0 .../mesh/getSearchSuggestions.ts | 0 .../mesh/getSearchSuggestionsInput.ts | 0 .../mesh/getStoreConfig.ts | 0 ...ctFilterInputToAlgoliafacetFiltersInput.ts | 0 .../mesh/resolvers.ts | 0 .../mesh/sortOptions.ts | 0 .../mesh/utils.ts | 0 .../next-env.d.ts | 0 .../package.json | 2 +- .../GraphQLProviderAlgoliaCustomerGroupId.tsx | 0 .../plugins/ProductListItemsBaseAlgolia.tsx | 0 .../magentoProductApplyAlgoliaEngine.ts | 0 .../magentoSearchApplyAlgoliaEngine.ts | 0 .../plugins/meshConfigAlgolia.ts | 5 +- .../schema/AlgoliaSchema.graphqls | 0 .../schema/CustomerAlgoliaGroupId.graphqls | 0 .../scripts/generate-spec.mts | 0 .../tsconfig.json | 0 .../utils/applyCategoryEngineVariable.ts} | 0 .../utils/applyEngineVariable.ts | 11 +++ 53 files changed, 242 insertions(+), 6 deletions(-) rename packages/{algolia-mesh/mesh/categories => algolia-categories/mesh}/algoliaHitToMagentoCategory.ts (100%) rename packages/{algolia-mesh/mesh/categories => algolia-categories/mesh}/getCategoryResults.ts (100%) rename packages/{algolia-mesh/mesh/categories => algolia-categories/mesh}/getCategoryResultsInput.ts (100%) rename packages/{algolia-mesh/mesh/categories => algolia-categories/mesh}/getIndexName.ts (100%) rename packages/{algolia-mesh/mesh/categories => algolia-categories/mesh}/resolvers.ts (100%) create mode 100644 packages/algolia-categories/package.json create mode 100644 packages/algolia-categories/plugins/meshConfigAlgoliaCategories.ts rename packages/{algolia-mesh => algolia-products}/CHANGELOG.md (100%) rename packages/{algolia-mesh => algolia-products}/Config.graphqls (100%) rename packages/{algolia-mesh => algolia-products}/README.md (100%) rename packages/{algolia-mesh => algolia-products}/algolia-spec.yaml (100%) rename packages/{algolia-mesh => algolia-products}/graphql/CustomerGroupId.graphql (100%) rename packages/{algolia-mesh => algolia-products}/graphql/GetAlgoliaSettings.graphql (100%) rename packages/{algolia-mesh => algolia-products}/graphql/ProductListItems_Algolia.graphql (100%) rename packages/{algolia-mesh => algolia-products}/hooks/useAlgoliaIndexName.ts (100%) rename packages/{algolia-mesh => algolia-products}/hooks/useAlgoliaQueryContext.ts (100%) rename packages/{algolia-mesh => algolia-products}/index.ts (100%) rename packages/{algolia-mesh => algolia-products}/link/customerGroupIdLink.ts (100%) rename packages/{algolia-mesh => algolia-products}/mesh/algoliaFacetsToAggregations.ts (100%) rename packages/{algolia-mesh => algolia-products}/mesh/algoliaHitToMagentoProduct.ts (100%) create mode 100644 packages/algolia-products/mesh/categories/algoliaHitToMagentoCategory.ts create mode 100644 packages/algolia-products/mesh/categories/getCategoryResults.ts create mode 100644 packages/algolia-products/mesh/categories/getCategoryResultsInput.ts create mode 100644 packages/algolia-products/mesh/categories/getIndexName.ts create mode 100644 packages/algolia-products/mesh/categories/resolvers.ts rename packages/{algolia-mesh => algolia-products}/mesh/getAlgoliaSettings.ts (100%) rename packages/{algolia-mesh => algolia-products}/mesh/getAttributeList.ts (100%) rename packages/{algolia-mesh => algolia-products}/mesh/getGroupId.ts (100%) rename packages/{algolia-mesh => algolia-products}/mesh/getIndexName.ts (100%) rename packages/{algolia-mesh => algolia-products}/mesh/getSearchResults.ts (100%) rename packages/{algolia-mesh => algolia-products}/mesh/getSearchResultsInput.ts (100%) rename packages/{algolia-mesh => algolia-products}/mesh/getSearchSuggestions.ts (100%) rename packages/{algolia-mesh => algolia-products}/mesh/getSearchSuggestionsInput.ts (100%) rename packages/{algolia-mesh => algolia-products}/mesh/getStoreConfig.ts (100%) rename packages/{algolia-mesh => algolia-products}/mesh/productFilterInputToAlgoliafacetFiltersInput.ts (100%) rename packages/{algolia-mesh => algolia-products}/mesh/resolvers.ts (100%) rename packages/{algolia-mesh => algolia-products}/mesh/sortOptions.ts (100%) rename packages/{algolia-mesh => algolia-products}/mesh/utils.ts (100%) rename packages/{algolia-mesh => algolia-products}/next-env.d.ts (100%) rename packages/{algolia-mesh => algolia-products}/package.json (95%) rename packages/{algolia-mesh => algolia-products}/plugins/GraphQLProviderAlgoliaCustomerGroupId.tsx (100%) rename packages/{algolia-mesh => algolia-products}/plugins/ProductListItemsBaseAlgolia.tsx (100%) rename packages/{algolia-mesh => algolia-products}/plugins/magentoProductApplyAlgoliaEngine.ts (100%) rename packages/{algolia-mesh => algolia-products}/plugins/magentoSearchApplyAlgoliaEngine.ts (100%) rename packages/{algolia-mesh => algolia-products}/plugins/meshConfigAlgolia.ts (90%) rename packages/{algolia-mesh => algolia-products}/schema/AlgoliaSchema.graphqls (100%) rename packages/{algolia-mesh => algolia-products}/schema/CustomerAlgoliaGroupId.graphqls (100%) rename packages/{algolia-mesh => algolia-products}/scripts/generate-spec.mts (100%) rename packages/{algolia-mesh => algolia-products}/tsconfig.json (100%) rename packages/{algolia-mesh/utils/applyEngineVariable.ts => algolia-products/utils/applyCategoryEngineVariable.ts} (100%) create mode 100644 packages/algolia-products/utils/applyEngineVariable.ts diff --git a/packages/algolia-mesh/mesh/categories/algoliaHitToMagentoCategory.ts b/packages/algolia-categories/mesh/algoliaHitToMagentoCategory.ts similarity index 100% rename from packages/algolia-mesh/mesh/categories/algoliaHitToMagentoCategory.ts rename to packages/algolia-categories/mesh/algoliaHitToMagentoCategory.ts diff --git a/packages/algolia-mesh/mesh/categories/getCategoryResults.ts b/packages/algolia-categories/mesh/getCategoryResults.ts similarity index 100% rename from packages/algolia-mesh/mesh/categories/getCategoryResults.ts rename to packages/algolia-categories/mesh/getCategoryResults.ts diff --git a/packages/algolia-mesh/mesh/categories/getCategoryResultsInput.ts b/packages/algolia-categories/mesh/getCategoryResultsInput.ts similarity index 100% rename from packages/algolia-mesh/mesh/categories/getCategoryResultsInput.ts rename to packages/algolia-categories/mesh/getCategoryResultsInput.ts diff --git a/packages/algolia-mesh/mesh/categories/getIndexName.ts b/packages/algolia-categories/mesh/getIndexName.ts similarity index 100% rename from packages/algolia-mesh/mesh/categories/getIndexName.ts rename to packages/algolia-categories/mesh/getIndexName.ts diff --git a/packages/algolia-mesh/mesh/categories/resolvers.ts b/packages/algolia-categories/mesh/resolvers.ts similarity index 100% rename from packages/algolia-mesh/mesh/categories/resolvers.ts rename to packages/algolia-categories/mesh/resolvers.ts diff --git a/packages/algolia-categories/package.json b/packages/algolia-categories/package.json new file mode 100644 index 0000000000..3ed81e9283 --- /dev/null +++ b/packages/algolia-categories/package.json @@ -0,0 +1,33 @@ +{ + "name": "@graphcommerce/algolia-categories", + "homepage": "https://www.graphcommerce.org/", + "repository": "github:graphcommerce-org/graphcommerce", + "version": "9.0.0-canary.76", + "sideEffects": false, + "prettier": "@graphcommerce/prettier-config-pwa", + "eslintConfig": { + "extends": "@graphcommerce/eslint-config-pwa", + "parserOptions": { + "project": "./tsconfig.json" + } + }, + "scripts": { + "generate": "tsx scripts/generate-spec.mts" + }, + "peerDependencies": { + "@graphcommerce/algolia-products": "9.0.0-canary.82", + "@graphcommerce/google-datalayer": "^9.0.0-canary.76", + "@graphcommerce/graphql": "^9.0.0-canary.76", + "@graphcommerce/graphql-mesh": "^9.0.0-canary.76", + "@graphcommerce/magento-customer": "^9.0.0-canary.76", + "@graphcommerce/magento-product": "^9.0.0-canary.76", + "@graphcommerce/magento-search": "^9.0.0-canary.76", + "@graphcommerce/next-config": "^9.0.0-canary.76", + "@graphcommerce/next-ui": "^9.0.0-canary.76", + "react": "^18.2.0" + }, + "devDependencies": { + "graphql": "^16.0.0", + "tsx": "^4.16.2" + } +} diff --git a/packages/algolia-categories/plugins/meshConfigAlgoliaCategories.ts b/packages/algolia-categories/plugins/meshConfigAlgoliaCategories.ts new file mode 100644 index 0000000000..be9e460bf9 --- /dev/null +++ b/packages/algolia-categories/plugins/meshConfigAlgoliaCategories.ts @@ -0,0 +1,22 @@ +import type { meshConfig as meshConfigBase } from '@graphcommerce/graphql-mesh/meshConfig' +import type { FunctionPlugin, PluginConfig } from '@graphcommerce/next-config' + +export const config: PluginConfig = { + module: '@graphcommerce/graphql-mesh/meshConfig', + type: 'function', +} + +export const meshConfig: FunctionPlugin = ( + prev, + baseConfig, + graphCommerceConfig, +) => + prev( + { + additionalResolvers: [ + ...(baseConfig.additionalResolvers ?? []), + '@graphcommerce/algolia-categories/mesh/categories/resolvers.ts', + ], + }, + graphCommerceConfig, + ) diff --git a/packages/algolia-personalization/hooks/useSendAlgoliaEvent.ts b/packages/algolia-personalization/hooks/useSendAlgoliaEvent.ts index 5c3416c6b1..4cde2b7602 100644 --- a/packages/algolia-personalization/hooks/useSendAlgoliaEvent.ts +++ b/packages/algolia-personalization/hooks/useSendAlgoliaEvent.ts @@ -1,5 +1,5 @@ /* eslint-disable arrow-body-style */ -import { useAlgoliaIndexName, useAlgoliaQuery } from '@graphcommerce/algolia-mesh' +import { useAlgoliaIndexName, useAlgoliaQuery } from '@graphcommerce/algolia-products' import { GoogleEventTypes, sendEvent } from '@graphcommerce/google-datalayer' import { useApolloClient } from '@graphcommerce/graphql' import type { AlgoliaEventsItems_Input } from '@graphcommerce/graphql-mesh' diff --git a/packages/algolia-personalization/package.json b/packages/algolia-personalization/package.json index faa5428d6d..9528578c73 100644 --- a/packages/algolia-personalization/package.json +++ b/packages/algolia-personalization/package.json @@ -15,7 +15,7 @@ "generate-insights": "tsx scripts/generate-insights-spec.mts" }, "peerDependencies": { - "@graphcommerce/algolia-mesh": "^9.0.0-canary.76", + "@graphcommerce/algolia-products": "^9.0.0-canary.76", "@graphcommerce/google-datalayer": "^9.0.0-canary.76", "@graphcommerce/graphql": "^9.0.0-canary.76", "@graphcommerce/graphql-mesh": "^9.0.0-canary.76", diff --git a/packages/algolia-mesh/CHANGELOG.md b/packages/algolia-products/CHANGELOG.md similarity index 100% rename from packages/algolia-mesh/CHANGELOG.md rename to packages/algolia-products/CHANGELOG.md diff --git a/packages/algolia-mesh/Config.graphqls b/packages/algolia-products/Config.graphqls similarity index 100% rename from packages/algolia-mesh/Config.graphqls rename to packages/algolia-products/Config.graphqls diff --git a/packages/algolia-mesh/README.md b/packages/algolia-products/README.md similarity index 100% rename from packages/algolia-mesh/README.md rename to packages/algolia-products/README.md diff --git a/packages/algolia-mesh/algolia-spec.yaml b/packages/algolia-products/algolia-spec.yaml similarity index 100% rename from packages/algolia-mesh/algolia-spec.yaml rename to packages/algolia-products/algolia-spec.yaml diff --git a/packages/algolia-mesh/graphql/CustomerGroupId.graphql b/packages/algolia-products/graphql/CustomerGroupId.graphql similarity index 100% rename from packages/algolia-mesh/graphql/CustomerGroupId.graphql rename to packages/algolia-products/graphql/CustomerGroupId.graphql diff --git a/packages/algolia-mesh/graphql/GetAlgoliaSettings.graphql b/packages/algolia-products/graphql/GetAlgoliaSettings.graphql similarity index 100% rename from packages/algolia-mesh/graphql/GetAlgoliaSettings.graphql rename to packages/algolia-products/graphql/GetAlgoliaSettings.graphql diff --git a/packages/algolia-mesh/graphql/ProductListItems_Algolia.graphql b/packages/algolia-products/graphql/ProductListItems_Algolia.graphql similarity index 100% rename from packages/algolia-mesh/graphql/ProductListItems_Algolia.graphql rename to packages/algolia-products/graphql/ProductListItems_Algolia.graphql diff --git a/packages/algolia-mesh/hooks/useAlgoliaIndexName.ts b/packages/algolia-products/hooks/useAlgoliaIndexName.ts similarity index 100% rename from packages/algolia-mesh/hooks/useAlgoliaIndexName.ts rename to packages/algolia-products/hooks/useAlgoliaIndexName.ts diff --git a/packages/algolia-mesh/hooks/useAlgoliaQueryContext.ts b/packages/algolia-products/hooks/useAlgoliaQueryContext.ts similarity index 100% rename from packages/algolia-mesh/hooks/useAlgoliaQueryContext.ts rename to packages/algolia-products/hooks/useAlgoliaQueryContext.ts diff --git a/packages/algolia-mesh/index.ts b/packages/algolia-products/index.ts similarity index 100% rename from packages/algolia-mesh/index.ts rename to packages/algolia-products/index.ts diff --git a/packages/algolia-mesh/link/customerGroupIdLink.ts b/packages/algolia-products/link/customerGroupIdLink.ts similarity index 100% rename from packages/algolia-mesh/link/customerGroupIdLink.ts rename to packages/algolia-products/link/customerGroupIdLink.ts diff --git a/packages/algolia-mesh/mesh/algoliaFacetsToAggregations.ts b/packages/algolia-products/mesh/algoliaFacetsToAggregations.ts similarity index 100% rename from packages/algolia-mesh/mesh/algoliaFacetsToAggregations.ts rename to packages/algolia-products/mesh/algoliaFacetsToAggregations.ts diff --git a/packages/algolia-mesh/mesh/algoliaHitToMagentoProduct.ts b/packages/algolia-products/mesh/algoliaHitToMagentoProduct.ts similarity index 100% rename from packages/algolia-mesh/mesh/algoliaHitToMagentoProduct.ts rename to packages/algolia-products/mesh/algoliaHitToMagentoProduct.ts diff --git a/packages/algolia-products/mesh/categories/algoliaHitToMagentoCategory.ts b/packages/algolia-products/mesh/categories/algoliaHitToMagentoCategory.ts new file mode 100644 index 0000000000..5190c95ad2 --- /dev/null +++ b/packages/algolia-products/mesh/categories/algoliaHitToMagentoCategory.ts @@ -0,0 +1,72 @@ +import { + RequireFields, + ResolverFn, + ResolversParentTypes, + ResolversTypes, + MeshContext, + QuerycategoriesArgs, + Algoliahit, + AlgoliaProductHitAdditionalProperties, +} from '@graphcommerce/graphql-mesh' +import { algoliaUrlToUrlKey } from '../algoliaHitToMagentoProduct' +import { GetStoreConfigReturn } from '../getStoreConfig' + +export type AlgoliaCategoryHitAddiotonalProperties = AlgoliaProductHitAdditionalProperties & { + path: string + url: string + product_count: number + meta_description: string + meta_title: string + level: number +} + +export function assertAdditional( + additional: unknown, +): additional is AlgoliaCategoryHitAddiotonalProperties { + return true +} + +export type CategoriesItemsItem = NonNullable< + Awaited< + ReturnType< + ResolverFn< + ResolversTypes['CategoryResult'], + ResolversParentTypes['Query'], + MeshContext, + RequireFields + > + > + >['items'] +>[number] + +function mapBreadcrumbs(algoliaPath) { + const pathArray = algoliaPath.split(' / ') + return pathArray.map((item) => ({ + category_name: item, + category_uid: 0, + })) +} + +export function algoliaHitToMagentoCategory( + hit: Algoliahit, + storeConfig: GetStoreConfigReturn, +): CategoriesItemsItem { + const { objectID, additionalProperties } = hit + + if (!assertAdditional(additionalProperties)) return null + return { + name: additionalProperties?.name, + children: null, + products: null, + image: additionalProperties?.image_url, + uid: objectID, + redirect_code: 0, + url_key: '', + url_path: algoliaUrlToUrlKey(additionalProperties.url, storeConfig?.base_link_url), + breadcrumbs: mapBreadcrumbs(additionalProperties?.path), + product_count: additionalProperties?.product_count, + meta_description: additionalProperties?.meta_description, + meta_title: additionalProperties?.meta_title, + level: additionalProperties?.level, + } +} diff --git a/packages/algolia-products/mesh/categories/getCategoryResults.ts b/packages/algolia-products/mesh/categories/getCategoryResults.ts new file mode 100644 index 0000000000..ce0a0680e8 --- /dev/null +++ b/packages/algolia-products/mesh/categories/getCategoryResults.ts @@ -0,0 +1,34 @@ +import { MeshContext, QuerycategoryListArgs } from '@graphcommerce/graphql-mesh' +import type { GraphQLResolveInfo } from 'graphql' +import { getCategoryResultsInput } from './getCategoryResultsInput' +import { getIndexName } from './getIndexName' + +export async function getCategoryResults( + args: QuerycategoryListArgs, + context: MeshContext, + info: GraphQLResolveInfo, +) { + return context.algolia.Query.algolia_searchSingleIndex({ + args: { + indexName: getIndexName(context), + input: await getCategoryResultsInput(args), + }, + selectionSet: /* GraphQL */ ` + { + nbPages + hitsPerPage + page + queryID + nbHits + hits { + __typename + objectID + additionalProperties + } + facets + } + `, + context, + info, + }) +} diff --git a/packages/algolia-products/mesh/categories/getCategoryResultsInput.ts b/packages/algolia-products/mesh/categories/getCategoryResultsInput.ts new file mode 100644 index 0000000000..b7a025e574 --- /dev/null +++ b/packages/algolia-products/mesh/categories/getCategoryResultsInput.ts @@ -0,0 +1,15 @@ +import { + Queryalgolia_searchSingleIndexArgs, + QuerycategoryListArgs, +} from '@graphcommerce/graphql-mesh' + +// eslint-disable-next-line @typescript-eslint/require-await +export async function getCategoryResultsInput( + args: QuerycategoryListArgs, +): Promise { + return { + query: args.filters?.name?.match ?? '', + hitsPerPage: args.pageSize ? args.pageSize : 10, + page: args.currentPage ? args.currentPage - 1 : 0, + } +} diff --git a/packages/algolia-products/mesh/categories/getIndexName.ts b/packages/algolia-products/mesh/categories/getIndexName.ts new file mode 100644 index 0000000000..031caff6ab --- /dev/null +++ b/packages/algolia-products/mesh/categories/getIndexName.ts @@ -0,0 +1,11 @@ +import { MeshContext } from '@graphcommerce/graphql-mesh' +import { storefrontConfigDefault } from '@graphcommerce/next-ui' + +function getStoreHeader(context: MeshContext) { + return (context as MeshContext & { headers: Record }).headers.store +} + +export function getIndexName(context: MeshContext) { + const storeCode = getStoreHeader(context) ?? storefrontConfigDefault().magentoStoreCode + return `${import.meta.graphCommerce.algolia.indexNamePrefix}${storeCode}_categories` +} diff --git a/packages/algolia-products/mesh/categories/resolvers.ts b/packages/algolia-products/mesh/categories/resolvers.ts new file mode 100644 index 0000000000..7221be97ea --- /dev/null +++ b/packages/algolia-products/mesh/categories/resolvers.ts @@ -0,0 +1,39 @@ +import { type Resolvers } from '@graphcommerce/graphql-mesh' +import { getStoreConfig } from '../getStoreConfig' +import { algoliaHitToMagentoCategory, CategoriesItemsItem } from './algoliaHitToMagentoCategory' +import { getCategoryResults } from './getCategoryResults' + +export const resolvers: Resolvers = { + Query: { + categories: async (root, args, context, info) => { + const isAgolia = (args.filters?.engine?.in ?? [args.filters?.engine?.eq])[0] === 'algolia' + + if (!isAgolia) return context.m2.Query.categories({ root, args, context, info }) + + const items: (CategoriesItemsItem | null)[] = [] + + const [algoliaResponse, storeConfig] = await Promise.all([ + await getCategoryResults(args, context, info), + await getStoreConfig(context), + ]) + + if (!algoliaResponse?.hits) return context.m2.Query.categories({ root, args, context, info }) + for (const hit of algoliaResponse.hits) { + if (hit?.objectID) { + const category = algoliaHitToMagentoCategory(hit, storeConfig) + items.push(category) + } + } + + return { + items, + page_info: { + current_page: algoliaResponse.page + 1, + page_size: algoliaResponse.hitsPerPage, + total_pages: algoliaResponse.nbPages, + }, + total_count: algoliaResponse.nbHits, + } + }, + }, +} diff --git a/packages/algolia-mesh/mesh/getAlgoliaSettings.ts b/packages/algolia-products/mesh/getAlgoliaSettings.ts similarity index 100% rename from packages/algolia-mesh/mesh/getAlgoliaSettings.ts rename to packages/algolia-products/mesh/getAlgoliaSettings.ts diff --git a/packages/algolia-mesh/mesh/getAttributeList.ts b/packages/algolia-products/mesh/getAttributeList.ts similarity index 100% rename from packages/algolia-mesh/mesh/getAttributeList.ts rename to packages/algolia-products/mesh/getAttributeList.ts diff --git a/packages/algolia-mesh/mesh/getGroupId.ts b/packages/algolia-products/mesh/getGroupId.ts similarity index 100% rename from packages/algolia-mesh/mesh/getGroupId.ts rename to packages/algolia-products/mesh/getGroupId.ts diff --git a/packages/algolia-mesh/mesh/getIndexName.ts b/packages/algolia-products/mesh/getIndexName.ts similarity index 100% rename from packages/algolia-mesh/mesh/getIndexName.ts rename to packages/algolia-products/mesh/getIndexName.ts diff --git a/packages/algolia-mesh/mesh/getSearchResults.ts b/packages/algolia-products/mesh/getSearchResults.ts similarity index 100% rename from packages/algolia-mesh/mesh/getSearchResults.ts rename to packages/algolia-products/mesh/getSearchResults.ts diff --git a/packages/algolia-mesh/mesh/getSearchResultsInput.ts b/packages/algolia-products/mesh/getSearchResultsInput.ts similarity index 100% rename from packages/algolia-mesh/mesh/getSearchResultsInput.ts rename to packages/algolia-products/mesh/getSearchResultsInput.ts diff --git a/packages/algolia-mesh/mesh/getSearchSuggestions.ts b/packages/algolia-products/mesh/getSearchSuggestions.ts similarity index 100% rename from packages/algolia-mesh/mesh/getSearchSuggestions.ts rename to packages/algolia-products/mesh/getSearchSuggestions.ts diff --git a/packages/algolia-mesh/mesh/getSearchSuggestionsInput.ts b/packages/algolia-products/mesh/getSearchSuggestionsInput.ts similarity index 100% rename from packages/algolia-mesh/mesh/getSearchSuggestionsInput.ts rename to packages/algolia-products/mesh/getSearchSuggestionsInput.ts diff --git a/packages/algolia-mesh/mesh/getStoreConfig.ts b/packages/algolia-products/mesh/getStoreConfig.ts similarity index 100% rename from packages/algolia-mesh/mesh/getStoreConfig.ts rename to packages/algolia-products/mesh/getStoreConfig.ts diff --git a/packages/algolia-mesh/mesh/productFilterInputToAlgoliafacetFiltersInput.ts b/packages/algolia-products/mesh/productFilterInputToAlgoliafacetFiltersInput.ts similarity index 100% rename from packages/algolia-mesh/mesh/productFilterInputToAlgoliafacetFiltersInput.ts rename to packages/algolia-products/mesh/productFilterInputToAlgoliafacetFiltersInput.ts diff --git a/packages/algolia-mesh/mesh/resolvers.ts b/packages/algolia-products/mesh/resolvers.ts similarity index 100% rename from packages/algolia-mesh/mesh/resolvers.ts rename to packages/algolia-products/mesh/resolvers.ts diff --git a/packages/algolia-mesh/mesh/sortOptions.ts b/packages/algolia-products/mesh/sortOptions.ts similarity index 100% rename from packages/algolia-mesh/mesh/sortOptions.ts rename to packages/algolia-products/mesh/sortOptions.ts diff --git a/packages/algolia-mesh/mesh/utils.ts b/packages/algolia-products/mesh/utils.ts similarity index 100% rename from packages/algolia-mesh/mesh/utils.ts rename to packages/algolia-products/mesh/utils.ts diff --git a/packages/algolia-mesh/next-env.d.ts b/packages/algolia-products/next-env.d.ts similarity index 100% rename from packages/algolia-mesh/next-env.d.ts rename to packages/algolia-products/next-env.d.ts diff --git a/packages/algolia-mesh/package.json b/packages/algolia-products/package.json similarity index 95% rename from packages/algolia-mesh/package.json rename to packages/algolia-products/package.json index 2aafcc452f..76f75e7c07 100644 --- a/packages/algolia-mesh/package.json +++ b/packages/algolia-products/package.json @@ -1,5 +1,5 @@ { - "name": "@graphcommerce/algolia-mesh", + "name": "@graphcommerce/algolia-products", "homepage": "https://www.graphcommerce.org/", "repository": "github:graphcommerce-org/graphcommerce", "version": "9.0.0-canary.76", diff --git a/packages/algolia-mesh/plugins/GraphQLProviderAlgoliaCustomerGroupId.tsx b/packages/algolia-products/plugins/GraphQLProviderAlgoliaCustomerGroupId.tsx similarity index 100% rename from packages/algolia-mesh/plugins/GraphQLProviderAlgoliaCustomerGroupId.tsx rename to packages/algolia-products/plugins/GraphQLProviderAlgoliaCustomerGroupId.tsx diff --git a/packages/algolia-mesh/plugins/ProductListItemsBaseAlgolia.tsx b/packages/algolia-products/plugins/ProductListItemsBaseAlgolia.tsx similarity index 100% rename from packages/algolia-mesh/plugins/ProductListItemsBaseAlgolia.tsx rename to packages/algolia-products/plugins/ProductListItemsBaseAlgolia.tsx diff --git a/packages/algolia-mesh/plugins/magentoProductApplyAlgoliaEngine.ts b/packages/algolia-products/plugins/magentoProductApplyAlgoliaEngine.ts similarity index 100% rename from packages/algolia-mesh/plugins/magentoProductApplyAlgoliaEngine.ts rename to packages/algolia-products/plugins/magentoProductApplyAlgoliaEngine.ts diff --git a/packages/algolia-mesh/plugins/magentoSearchApplyAlgoliaEngine.ts b/packages/algolia-products/plugins/magentoSearchApplyAlgoliaEngine.ts similarity index 100% rename from packages/algolia-mesh/plugins/magentoSearchApplyAlgoliaEngine.ts rename to packages/algolia-products/plugins/magentoSearchApplyAlgoliaEngine.ts diff --git a/packages/algolia-mesh/plugins/meshConfigAlgolia.ts b/packages/algolia-products/plugins/meshConfigAlgolia.ts similarity index 90% rename from packages/algolia-mesh/plugins/meshConfigAlgolia.ts rename to packages/algolia-products/plugins/meshConfigAlgolia.ts index f06370f221..d6cee9e341 100644 --- a/packages/algolia-mesh/plugins/meshConfigAlgolia.ts +++ b/packages/algolia-products/plugins/meshConfigAlgolia.ts @@ -21,7 +21,7 @@ export const meshConfig: FunctionPlugin = ( handler: { openapi: { endpoint: `https://${graphCommerceConfig.algolia.applicationId}.algolia.net/`, - source: '@graphcommerce/algolia-mesh/algolia-spec.yaml', + source: '@graphcommerce/algolia-products/algolia-spec.yaml', ignoreErrorResponses: true, schemaHeaders: { 'X-Algolia-Application-Id': graphCommerceConfig.algolia.applicationId, @@ -59,8 +59,7 @@ export const meshConfig: FunctionPlugin = ( ], additionalResolvers: [ ...(baseConfig.additionalResolvers ?? []), - '@graphcommerce/algolia-mesh/mesh/resolvers.ts', - '@graphcommerce/algolia-mesh/mesh/categories/resolvers.ts', + '@graphcommerce/algolia-products/mesh/resolvers.ts', ], }, graphCommerceConfig, diff --git a/packages/algolia-mesh/schema/AlgoliaSchema.graphqls b/packages/algolia-products/schema/AlgoliaSchema.graphqls similarity index 100% rename from packages/algolia-mesh/schema/AlgoliaSchema.graphqls rename to packages/algolia-products/schema/AlgoliaSchema.graphqls diff --git a/packages/algolia-mesh/schema/CustomerAlgoliaGroupId.graphqls b/packages/algolia-products/schema/CustomerAlgoliaGroupId.graphqls similarity index 100% rename from packages/algolia-mesh/schema/CustomerAlgoliaGroupId.graphqls rename to packages/algolia-products/schema/CustomerAlgoliaGroupId.graphqls diff --git a/packages/algolia-mesh/scripts/generate-spec.mts b/packages/algolia-products/scripts/generate-spec.mts similarity index 100% rename from packages/algolia-mesh/scripts/generate-spec.mts rename to packages/algolia-products/scripts/generate-spec.mts diff --git a/packages/algolia-mesh/tsconfig.json b/packages/algolia-products/tsconfig.json similarity index 100% rename from packages/algolia-mesh/tsconfig.json rename to packages/algolia-products/tsconfig.json diff --git a/packages/algolia-mesh/utils/applyEngineVariable.ts b/packages/algolia-products/utils/applyCategoryEngineVariable.ts similarity index 100% rename from packages/algolia-mesh/utils/applyEngineVariable.ts rename to packages/algolia-products/utils/applyCategoryEngineVariable.ts diff --git a/packages/algolia-products/utils/applyEngineVariable.ts b/packages/algolia-products/utils/applyEngineVariable.ts new file mode 100644 index 0000000000..f032188876 --- /dev/null +++ b/packages/algolia-products/utils/applyEngineVariable.ts @@ -0,0 +1,11 @@ +import type { ProductListQueryVariables } from '@graphcommerce/magento-product' + +export function applyEngineVariables(variables: ProductListQueryVariables | undefined) { + return { + ...variables, + filters: { + ...variables?.filters, + engine: { eq: 'algolia' }, + }, + } +} From 96ec76b245329571d9e6999a6381e65dc81cb573 Mon Sep 17 00:00:00 2001 From: Renzo van Hootegem Date: Tue, 1 Oct 2024 12:05:23 +0200 Subject: [PATCH 55/70] Import fixes --- .../algolia-categories/mesh/algoliaHitToMagentoCategory.ts | 4 ++-- packages/algolia-categories/mesh/resolvers.ts | 2 +- .../algolia-categories/plugins/meshConfigAlgoliaCategories.ts | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/algolia-categories/mesh/algoliaHitToMagentoCategory.ts b/packages/algolia-categories/mesh/algoliaHitToMagentoCategory.ts index 5190c95ad2..22dd11db5e 100644 --- a/packages/algolia-categories/mesh/algoliaHitToMagentoCategory.ts +++ b/packages/algolia-categories/mesh/algoliaHitToMagentoCategory.ts @@ -1,3 +1,5 @@ +import { algoliaUrlToUrlKey } from '@graphcommerce/algolia-products/mesh/algoliaHitToMagentoProduct' +import { GetStoreConfigReturn } from '@graphcommerce/algolia-products/mesh/getStoreConfig' import { RequireFields, ResolverFn, @@ -8,8 +10,6 @@ import { Algoliahit, AlgoliaProductHitAdditionalProperties, } from '@graphcommerce/graphql-mesh' -import { algoliaUrlToUrlKey } from '../algoliaHitToMagentoProduct' -import { GetStoreConfigReturn } from '../getStoreConfig' export type AlgoliaCategoryHitAddiotonalProperties = AlgoliaProductHitAdditionalProperties & { path: string diff --git a/packages/algolia-categories/mesh/resolvers.ts b/packages/algolia-categories/mesh/resolvers.ts index 7221be97ea..24b012afa7 100644 --- a/packages/algolia-categories/mesh/resolvers.ts +++ b/packages/algolia-categories/mesh/resolvers.ts @@ -1,5 +1,5 @@ +import { getStoreConfig } from '@graphcommerce/algolia-products/mesh/getStoreConfig' import { type Resolvers } from '@graphcommerce/graphql-mesh' -import { getStoreConfig } from '../getStoreConfig' import { algoliaHitToMagentoCategory, CategoriesItemsItem } from './algoliaHitToMagentoCategory' import { getCategoryResults } from './getCategoryResults' diff --git a/packages/algolia-categories/plugins/meshConfigAlgoliaCategories.ts b/packages/algolia-categories/plugins/meshConfigAlgoliaCategories.ts index be9e460bf9..5588adb6dc 100644 --- a/packages/algolia-categories/plugins/meshConfigAlgoliaCategories.ts +++ b/packages/algolia-categories/plugins/meshConfigAlgoliaCategories.ts @@ -15,7 +15,7 @@ export const meshConfig: FunctionPlugin = ( { additionalResolvers: [ ...(baseConfig.additionalResolvers ?? []), - '@graphcommerce/algolia-categories/mesh/categories/resolvers.ts', + '@graphcommerce/algolia-categories/mesh/resolvers.ts', ], }, graphCommerceConfig, From 206457cdec4b185bd9e4254390e3f94ed4428f77 Mon Sep 17 00:00:00 2001 From: Renzo van Hootegem Date: Tue, 1 Oct 2024 12:07:01 +0200 Subject: [PATCH 56/70] Updated algolia products readme --- packages/algolia-products/README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/algolia-products/README.md b/packages/algolia-products/README.md index 59675af764..e607360d6b 100644 --- a/packages/algolia-products/README.md +++ b/packages/algolia-products/README.md @@ -1,6 +1,6 @@ # Algolia Magento 2 -An implementation of Algolia through the GraphQL Mesh. +An implementation of Algolia Products through the GraphQL Mesh. ## Prerequisites @@ -9,8 +9,8 @@ Make sure the Magento 2 module is correctly installed and working. ## Installation 1. Find current version of your `@graphcommerce/next-ui` in your package.json. -2. `yarn add @graphcommerce/algolia-mesh@9.0.0` (replace 9.0.0 with the version - of the step above) +2. `yarn add @graphcommerce/algolia-products@9.0.0` (replace 9.0.0 with the + version of the step above) 3. Configure the following ([configuration values](./Config.graphqls)) in your graphcommerce.config.js: From 503251ff0b769c595ab38ef180393e478773e563 Mon Sep 17 00:00:00 2001 From: Renzo van Hootegem Date: Tue, 1 Oct 2024 12:11:44 +0200 Subject: [PATCH 57/70] Removed todos --- packages/algolia-products/README.md | 5 ----- 1 file changed, 5 deletions(-) diff --git a/packages/algolia-products/README.md b/packages/algolia-products/README.md index e607360d6b..441bf19786 100644 --- a/packages/algolia-products/README.md +++ b/packages/algolia-products/README.md @@ -77,8 +77,3 @@ new index which will be used for suggestions Don't mind to use the suggestions setting in magento admin. It will create a suggestions index but it will not record suggestions. This requires instantSearch - -## Todo - -- [] Category search / Additional indexes -- [] Highlighting From 6eda4328e01ecda57266f68561f830198aa0ff1e Mon Sep 17 00:00:00 2001 From: Renzo van Hootegem Date: Tue, 1 Oct 2024 12:18:00 +0200 Subject: [PATCH 58/70] Added sources to mesh config, so it doesnt break --- .../algolia-categories/plugins/meshConfigAlgoliaCategories.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/algolia-categories/plugins/meshConfigAlgoliaCategories.ts b/packages/algolia-categories/plugins/meshConfigAlgoliaCategories.ts index 5588adb6dc..ed6ce69bc1 100644 --- a/packages/algolia-categories/plugins/meshConfigAlgoliaCategories.ts +++ b/packages/algolia-categories/plugins/meshConfigAlgoliaCategories.ts @@ -13,6 +13,7 @@ export const meshConfig: FunctionPlugin = ( ) => prev( { + sources: [...baseConfig.sources], additionalResolvers: [ ...(baseConfig.additionalResolvers ?? []), '@graphcommerce/algolia-categories/mesh/resolvers.ts', From 3033a096524b30dad30cbc23b85cdb2dce8cdfee Mon Sep 17 00:00:00 2001 From: Renzo van Hootegem Date: Tue, 1 Oct 2024 12:21:45 +0200 Subject: [PATCH 59/70] Removed unused code --- packages/algolia-products/hooks/useAlgoliaIndexName.ts | 5 ----- 1 file changed, 5 deletions(-) delete mode 100644 packages/algolia-products/hooks/useAlgoliaIndexName.ts diff --git a/packages/algolia-products/hooks/useAlgoliaIndexName.ts b/packages/algolia-products/hooks/useAlgoliaIndexName.ts deleted file mode 100644 index d4fbc1a812..0000000000 --- a/packages/algolia-products/hooks/useAlgoliaIndexName.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { useStorefrontConfig } from '@graphcommerce/next-ui' - -export function useAlgoliaIndexName() { - return `${import.meta.graphCommerce.algolia.indexNamePrefix}${useStorefrontConfig().magentoStoreCode}_products` -} From b0a96d5557dcf594397cd39e7f482094518583ca Mon Sep 17 00:00:00 2001 From: Renzo van Hootegem Date: Tue, 1 Oct 2024 12:28:07 +0200 Subject: [PATCH 60/70] Removed personalization enabled from readme --- packages/algolia-personalization/README.md | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/algolia-personalization/README.md b/packages/algolia-personalization/README.md index e05a075213..cee0d409a3 100644 --- a/packages/algolia-personalization/README.md +++ b/packages/algolia-personalization/README.md @@ -24,7 +24,6 @@ An implementation of Algolia personalization/Analytics through the GraphQL Mesh. algolia: { // Even if you do not use personalization, enabling analytics still allows you to track events in Algolia. algoliaEnableAnalytics: true, - algoliaPersonalizationEnabled: true, }, } ``` From c6c54399a4a693d018a4e66b941d870abff1185f Mon Sep 17 00:00:00 2001 From: Renzo van Hootegem Date: Tue, 1 Oct 2024 13:11:15 +0200 Subject: [PATCH 61/70] Updated readme --- packages/algolia-personalization/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/algolia-personalization/README.md b/packages/algolia-personalization/README.md index cee0d409a3..3d2361b778 100644 --- a/packages/algolia-personalization/README.md +++ b/packages/algolia-personalization/README.md @@ -28,7 +28,7 @@ An implementation of Algolia personalization/Analytics through the GraphQL Mesh. } ``` -2. In your algolia dashboard make sure, you have personalization enabled. +2. In your algolia dashboard make sure, you have (ai)-personalization enabled. 3. Continue to browse the site, and make sure your events are logged in de event debugger in your algolia dashboard. In the under left corner. `Data Sources > Events > Debugger`. Once you've collected several events, you From 349c92936dc2025cdb586be25f9ecb413a8df002 Mon Sep 17 00:00:00 2001 From: Renzo van Hootegem Date: Tue, 1 Oct 2024 13:20:49 +0200 Subject: [PATCH 62/70] Updated readmes and readded useAlgoliaIndexName --- packages/algolia-categories/README.md | 14 ++++++++++++++ packages/algolia-personalization/README.md | 2 +- .../algolia-products/hooks/useAlgoliaIndexName.ts | 5 +++++ 3 files changed, 20 insertions(+), 1 deletion(-) create mode 100644 packages/algolia-categories/README.md create mode 100644 packages/algolia-products/hooks/useAlgoliaIndexName.ts diff --git a/packages/algolia-categories/README.md b/packages/algolia-categories/README.md new file mode 100644 index 0000000000..bed10c81f4 --- /dev/null +++ b/packages/algolia-categories/README.md @@ -0,0 +1,14 @@ +# Algolia Magento 2 + +An implementation of Algolia categories through the GraphQL Mesh. This will only +apply to the search + +## Prerequisites + +1. Make sure you have configured @graphcommerce/algolia-products. + +## Installation + +1. Find current version of your `@graphcommerce/next-ui` in your package.json. +2. `yarn add @graphcommerce/algolia-categories@9.0.0` (replace 9.0.0 with the + version of the step above) diff --git a/packages/algolia-personalization/README.md b/packages/algolia-personalization/README.md index 3d2361b778..7762ced8e4 100644 --- a/packages/algolia-personalization/README.md +++ b/packages/algolia-personalization/README.md @@ -4,7 +4,7 @@ An implementation of Algolia personalization/Analytics through the GraphQL Mesh. ## Prerequisites -1. Make sure you have configured @graphcommerce/algolia-mesh. +1. Make sure you have configured @graphcommerce/algolia-products. 2. As this is an extension of the google-datalayer. Make sure you have installed @graphcommerce/google-datalayer diff --git a/packages/algolia-products/hooks/useAlgoliaIndexName.ts b/packages/algolia-products/hooks/useAlgoliaIndexName.ts new file mode 100644 index 0000000000..d4fbc1a812 --- /dev/null +++ b/packages/algolia-products/hooks/useAlgoliaIndexName.ts @@ -0,0 +1,5 @@ +import { useStorefrontConfig } from '@graphcommerce/next-ui' + +export function useAlgoliaIndexName() { + return `${import.meta.graphCommerce.algolia.indexNamePrefix}${useStorefrontConfig().magentoStoreCode}_products` +} From 10a50ca7456990e1b018841cdd5c75506bb725d0 Mon Sep 17 00:00:00 2001 From: Renzo van Hootegem Date: Tue, 1 Oct 2024 15:27:58 +0200 Subject: [PATCH 63/70] replaced algolia-mesh with algolia-products in recommend package --- .../plugins/getSearchResultsAnalytics.ts | 4 ++-- .../plugins/getSearchSuggestionsAnalytics.ts | 4 ++-- .../plugins/meshConfigAlgoliaInsights.ts | 4 ---- packages/algolia-products/CHANGELOG.md | 2 +- packages/algolia-recommend/mesh/createProductMapper.ts | 6 +++--- packages/algolia-recommend/mesh/getRecommendationArgs.ts | 2 +- packages/algolia-recommend/mesh/getRecommendations.ts | 2 +- packages/algolia-recommend/package.json | 2 +- .../algolia-recommend/scripts/generate-recommend-spec.mts | 2 +- packages/algolia-search/README.md | 2 +- 10 files changed, 13 insertions(+), 17 deletions(-) diff --git a/packages/algolia-personalization/plugins/getSearchResultsAnalytics.ts b/packages/algolia-personalization/plugins/getSearchResultsAnalytics.ts index fa4c57ca37..09f38f69d3 100644 --- a/packages/algolia-personalization/plugins/getSearchResultsAnalytics.ts +++ b/packages/algolia-personalization/plugins/getSearchResultsAnalytics.ts @@ -1,10 +1,10 @@ -import type { getSearchResultsInput as getSearchResultsInputType } from '@graphcommerce/algolia-mesh' +import type { getSearchResultsInput as getSearchResultsInputType } from '@graphcommerce/algolia-products' import { FunctionPlugin, PluginConfig } from '@graphcommerce/next-config' import { getUserToken } from '../mesh/getUserToken' export const config: PluginConfig = { type: 'function', - module: '@graphcommerce/algolia-mesh', + module: '@graphcommerce/algolia-products', ifConfig: 'algolia.analyticsEnabled', } diff --git a/packages/algolia-personalization/plugins/getSearchSuggestionsAnalytics.ts b/packages/algolia-personalization/plugins/getSearchSuggestionsAnalytics.ts index fc224aa8b2..ae7cb4ba80 100644 --- a/packages/algolia-personalization/plugins/getSearchSuggestionsAnalytics.ts +++ b/packages/algolia-personalization/plugins/getSearchSuggestionsAnalytics.ts @@ -1,10 +1,10 @@ -import type { getSearchSuggestionsInput as getSearchSuggestionsInputType } from '@graphcommerce/algolia-mesh' +import type { getSearchSuggestionsInput as getSearchSuggestionsInputType } from '@graphcommerce/algolia-products' import { FunctionPlugin, PluginConfig } from '@graphcommerce/next-config' import { getUserToken } from '../mesh/getUserToken' export const config: PluginConfig = { type: 'function', - module: '@graphcommerce/algolia-mesh', + module: '@graphcommerce/algolia-products', ifConfig: 'algolia.analyticsEnabled', } diff --git a/packages/algolia-personalization/plugins/meshConfigAlgoliaInsights.ts b/packages/algolia-personalization/plugins/meshConfigAlgoliaInsights.ts index 24b1f2be7a..990983e20b 100644 --- a/packages/algolia-personalization/plugins/meshConfigAlgoliaInsights.ts +++ b/packages/algolia-personalization/plugins/meshConfigAlgoliaInsights.ts @@ -54,10 +54,6 @@ export const meshConfig: FunctionPlugin = ( ], }, ], - // additionalResolvers: [ - // ...(baseConfig.additionalResolvers ?? []), - // '@graphcommerce/algolia-mesh/mesh/resolvers.ts', - // ], }, graphCommerceConfig, ) diff --git a/packages/algolia-products/CHANGELOG.md b/packages/algolia-products/CHANGELOG.md index 63ee8f8904..05615c8ec2 100644 --- a/packages/algolia-products/CHANGELOG.md +++ b/packages/algolia-products/CHANGELOG.md @@ -1,4 +1,4 @@ -# @graphcommerce/algolia-mesh +# @graphcommerce/algolia-products ## 9.0.0-canary.82 diff --git a/packages/algolia-recommend/mesh/createProductMapper.ts b/packages/algolia-recommend/mesh/createProductMapper.ts index 0c3e54dc9f..52482b04e4 100644 --- a/packages/algolia-recommend/mesh/createProductMapper.ts +++ b/packages/algolia-recommend/mesh/createProductMapper.ts @@ -1,6 +1,6 @@ -import { algoliaHitToMagentoProduct } from '@graphcommerce/algolia-mesh' -import { getGroupId } from '@graphcommerce/algolia-mesh/mesh/getGroupId' -import { getStoreConfig } from '@graphcommerce/algolia-mesh/mesh/getStoreConfig' +import { algoliaHitToMagentoProduct } from '@graphcommerce/algolia-products' +import { getGroupId } from '@graphcommerce/algolia-products/mesh/getGroupId' +import { getStoreConfig } from '@graphcommerce/algolia-products/mesh/getStoreConfig' import type { MeshContext, AlgoliarecommendationsHit, diff --git a/packages/algolia-recommend/mesh/getRecommendationArgs.ts b/packages/algolia-recommend/mesh/getRecommendationArgs.ts index 82caa6cc0d..81cefa92cc 100644 --- a/packages/algolia-recommend/mesh/getRecommendationArgs.ts +++ b/packages/algolia-recommend/mesh/getRecommendationArgs.ts @@ -1,4 +1,4 @@ -import { getSearchResultsInput } from '@graphcommerce/algolia-mesh' +import { getSearchResultsInput } from '@graphcommerce/algolia-products' import { QueryproductsArgs, MeshContext, diff --git a/packages/algolia-recommend/mesh/getRecommendations.ts b/packages/algolia-recommend/mesh/getRecommendations.ts index 43ebf0445c..48b328b800 100644 --- a/packages/algolia-recommend/mesh/getRecommendations.ts +++ b/packages/algolia-recommend/mesh/getRecommendations.ts @@ -1,4 +1,4 @@ -import { getIndexName } from '@graphcommerce/algolia-mesh/mesh/getIndexName' +import { getIndexName } from '@graphcommerce/algolia-products/mesh/getIndexName' import type { AlgoliarecommendationsRequest_Input, MeshContext, diff --git a/packages/algolia-recommend/package.json b/packages/algolia-recommend/package.json index 5366e11aae..4a788cda8f 100644 --- a/packages/algolia-recommend/package.json +++ b/packages/algolia-recommend/package.json @@ -15,7 +15,7 @@ "generate": "tsx scripts/generate-recommend-spec.mts" }, "peerDependencies": { - "@graphcommerce/algolia-mesh": "^9.0.0-canary.76", + "@graphcommerce/algolia-products": "^9.0.0-canary.76", "@graphcommerce/graphql": "^9.0.0-canary.76", "@graphcommerce/graphql-mesh": "^9.0.0-canary.76", "@graphcommerce/next-config": "^9.0.0-canary.76", diff --git a/packages/algolia-recommend/scripts/generate-recommend-spec.mts b/packages/algolia-recommend/scripts/generate-recommend-spec.mts index 448cc20db1..8e1f6c89a5 100644 --- a/packages/algolia-recommend/scripts/generate-recommend-spec.mts +++ b/packages/algolia-recommend/scripts/generate-recommend-spec.mts @@ -3,7 +3,7 @@ import { writeFile, readFile } from 'node:fs/promises' import { OpenAPIV3 } from 'openapi-types' import prettier from 'prettier' import conf from '@graphcommerce/prettier-config-pwa' -import { algoliaSchemaBaseFilter } from '@graphcommerce/algolia-mesh/scripts/base-schema-filter.mjs' +import { algoliaSchemaBaseFilter } from '@graphcommerce/algolia-products/scripts/base-schema-filter.mjs' const response = await fetch( 'https://raw.githubusercontent.com/algolia/api-clients-automation/main/specs/bundled/recommend.yml', diff --git a/packages/algolia-search/README.md b/packages/algolia-search/README.md index a4e83d7235..86b6b37f85 100644 --- a/packages/algolia-search/README.md +++ b/packages/algolia-search/README.md @@ -2,7 +2,7 @@ ❗️❗️ NOT SUPPORTED: Does not work with productFiltersPro and does not work with ProductListLayout/\* components. No further development will be done. Please use -[@graphcommerce/algolia-mesh](../algolia-mesh/README.md) ❗️❗️ +[@graphcommerce/algolia-products](../algolia-products/README.md) ❗️❗️ Implementation of Algolia Instant Search inside Graphcommerce. Add client or server side product, category and pages search to your project! From ed35db24a5d0473dcaeac60cb56ee19c62171212 Mon Sep 17 00:00:00 2001 From: Renzo van Hootegem Date: Tue, 1 Oct 2024 15:28:50 +0200 Subject: [PATCH 64/70] removed awaits in promise --- packages/algolia-categories/mesh/resolvers.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/algolia-categories/mesh/resolvers.ts b/packages/algolia-categories/mesh/resolvers.ts index 24b012afa7..775cd50bb6 100644 --- a/packages/algolia-categories/mesh/resolvers.ts +++ b/packages/algolia-categories/mesh/resolvers.ts @@ -13,8 +13,8 @@ export const resolvers: Resolvers = { const items: (CategoriesItemsItem | null)[] = [] const [algoliaResponse, storeConfig] = await Promise.all([ - await getCategoryResults(args, context, info), - await getStoreConfig(context), + getCategoryResults(args, context, info), + getStoreConfig(context), ]) if (!algoliaResponse?.hits) return context.m2.Query.categories({ root, args, context, info }) From 5977732146de698a89b4548ee06a9ddc1df0353f Mon Sep 17 00:00:00 2001 From: Renzo van Hootegem Date: Tue, 1 Oct 2024 15:32:16 +0200 Subject: [PATCH 65/70] Added changelog and update package.json with newest GC versions --- packages/algolia-categories/CHANGELOG.md | 0 packages/algolia-categories/package.json | 18 +++++++++--------- 2 files changed, 9 insertions(+), 9 deletions(-) create mode 100644 packages/algolia-categories/CHANGELOG.md diff --git a/packages/algolia-categories/CHANGELOG.md b/packages/algolia-categories/CHANGELOG.md new file mode 100644 index 0000000000..e69de29bb2 diff --git a/packages/algolia-categories/package.json b/packages/algolia-categories/package.json index 3ed81e9283..b507813b0d 100644 --- a/packages/algolia-categories/package.json +++ b/packages/algolia-categories/package.json @@ -15,15 +15,15 @@ "generate": "tsx scripts/generate-spec.mts" }, "peerDependencies": { - "@graphcommerce/algolia-products": "9.0.0-canary.82", - "@graphcommerce/google-datalayer": "^9.0.0-canary.76", - "@graphcommerce/graphql": "^9.0.0-canary.76", - "@graphcommerce/graphql-mesh": "^9.0.0-canary.76", - "@graphcommerce/magento-customer": "^9.0.0-canary.76", - "@graphcommerce/magento-product": "^9.0.0-canary.76", - "@graphcommerce/magento-search": "^9.0.0-canary.76", - "@graphcommerce/next-config": "^9.0.0-canary.76", - "@graphcommerce/next-ui": "^9.0.0-canary.76", + "@graphcommerce/algolia-products": "^9.0.0-canary.82", + "@graphcommerce/google-datalayer": "^9.0.0-canary.82", + "@graphcommerce/graphql": "^9.0.0-canary.82", + "@graphcommerce/graphql-mesh": "^9.0.0-canary.82", + "@graphcommerce/magento-customer": "^9.0.0-canary.82", + "@graphcommerce/magento-product": "^9.0.0-canary.82", + "@graphcommerce/magento-search": "^9.0.0-canary.82", + "@graphcommerce/next-config": "^9.0.0-canary.82", + "@graphcommerce/next-ui": "^9.0.0-canary.82", "react": "^18.2.0" }, "devDependencies": { From a948267051206eb98eef0acaba2da7823116151a Mon Sep 17 00:00:00 2001 From: Renzo van Hootegem Date: Tue, 1 Oct 2024 15:34:25 +0200 Subject: [PATCH 66/70] Deleted categories from the algolia-products package --- .../categories/algoliaHitToMagentoCategory.ts | 72 ------------------- .../mesh/categories/getCategoryResults.ts | 34 --------- .../categories/getCategoryResultsInput.ts | 15 ---- .../mesh/categories/getIndexName.ts | 11 --- .../mesh/categories/resolvers.ts | 39 ---------- 5 files changed, 171 deletions(-) delete mode 100644 packages/algolia-products/mesh/categories/algoliaHitToMagentoCategory.ts delete mode 100644 packages/algolia-products/mesh/categories/getCategoryResults.ts delete mode 100644 packages/algolia-products/mesh/categories/getCategoryResultsInput.ts delete mode 100644 packages/algolia-products/mesh/categories/getIndexName.ts delete mode 100644 packages/algolia-products/mesh/categories/resolvers.ts diff --git a/packages/algolia-products/mesh/categories/algoliaHitToMagentoCategory.ts b/packages/algolia-products/mesh/categories/algoliaHitToMagentoCategory.ts deleted file mode 100644 index 5190c95ad2..0000000000 --- a/packages/algolia-products/mesh/categories/algoliaHitToMagentoCategory.ts +++ /dev/null @@ -1,72 +0,0 @@ -import { - RequireFields, - ResolverFn, - ResolversParentTypes, - ResolversTypes, - MeshContext, - QuerycategoriesArgs, - Algoliahit, - AlgoliaProductHitAdditionalProperties, -} from '@graphcommerce/graphql-mesh' -import { algoliaUrlToUrlKey } from '../algoliaHitToMagentoProduct' -import { GetStoreConfigReturn } from '../getStoreConfig' - -export type AlgoliaCategoryHitAddiotonalProperties = AlgoliaProductHitAdditionalProperties & { - path: string - url: string - product_count: number - meta_description: string - meta_title: string - level: number -} - -export function assertAdditional( - additional: unknown, -): additional is AlgoliaCategoryHitAddiotonalProperties { - return true -} - -export type CategoriesItemsItem = NonNullable< - Awaited< - ReturnType< - ResolverFn< - ResolversTypes['CategoryResult'], - ResolversParentTypes['Query'], - MeshContext, - RequireFields - > - > - >['items'] ->[number] - -function mapBreadcrumbs(algoliaPath) { - const pathArray = algoliaPath.split(' / ') - return pathArray.map((item) => ({ - category_name: item, - category_uid: 0, - })) -} - -export function algoliaHitToMagentoCategory( - hit: Algoliahit, - storeConfig: GetStoreConfigReturn, -): CategoriesItemsItem { - const { objectID, additionalProperties } = hit - - if (!assertAdditional(additionalProperties)) return null - return { - name: additionalProperties?.name, - children: null, - products: null, - image: additionalProperties?.image_url, - uid: objectID, - redirect_code: 0, - url_key: '', - url_path: algoliaUrlToUrlKey(additionalProperties.url, storeConfig?.base_link_url), - breadcrumbs: mapBreadcrumbs(additionalProperties?.path), - product_count: additionalProperties?.product_count, - meta_description: additionalProperties?.meta_description, - meta_title: additionalProperties?.meta_title, - level: additionalProperties?.level, - } -} diff --git a/packages/algolia-products/mesh/categories/getCategoryResults.ts b/packages/algolia-products/mesh/categories/getCategoryResults.ts deleted file mode 100644 index ce0a0680e8..0000000000 --- a/packages/algolia-products/mesh/categories/getCategoryResults.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { MeshContext, QuerycategoryListArgs } from '@graphcommerce/graphql-mesh' -import type { GraphQLResolveInfo } from 'graphql' -import { getCategoryResultsInput } from './getCategoryResultsInput' -import { getIndexName } from './getIndexName' - -export async function getCategoryResults( - args: QuerycategoryListArgs, - context: MeshContext, - info: GraphQLResolveInfo, -) { - return context.algolia.Query.algolia_searchSingleIndex({ - args: { - indexName: getIndexName(context), - input: await getCategoryResultsInput(args), - }, - selectionSet: /* GraphQL */ ` - { - nbPages - hitsPerPage - page - queryID - nbHits - hits { - __typename - objectID - additionalProperties - } - facets - } - `, - context, - info, - }) -} diff --git a/packages/algolia-products/mesh/categories/getCategoryResultsInput.ts b/packages/algolia-products/mesh/categories/getCategoryResultsInput.ts deleted file mode 100644 index b7a025e574..0000000000 --- a/packages/algolia-products/mesh/categories/getCategoryResultsInput.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { - Queryalgolia_searchSingleIndexArgs, - QuerycategoryListArgs, -} from '@graphcommerce/graphql-mesh' - -// eslint-disable-next-line @typescript-eslint/require-await -export async function getCategoryResultsInput( - args: QuerycategoryListArgs, -): Promise { - return { - query: args.filters?.name?.match ?? '', - hitsPerPage: args.pageSize ? args.pageSize : 10, - page: args.currentPage ? args.currentPage - 1 : 0, - } -} diff --git a/packages/algolia-products/mesh/categories/getIndexName.ts b/packages/algolia-products/mesh/categories/getIndexName.ts deleted file mode 100644 index 031caff6ab..0000000000 --- a/packages/algolia-products/mesh/categories/getIndexName.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { MeshContext } from '@graphcommerce/graphql-mesh' -import { storefrontConfigDefault } from '@graphcommerce/next-ui' - -function getStoreHeader(context: MeshContext) { - return (context as MeshContext & { headers: Record }).headers.store -} - -export function getIndexName(context: MeshContext) { - const storeCode = getStoreHeader(context) ?? storefrontConfigDefault().magentoStoreCode - return `${import.meta.graphCommerce.algolia.indexNamePrefix}${storeCode}_categories` -} diff --git a/packages/algolia-products/mesh/categories/resolvers.ts b/packages/algolia-products/mesh/categories/resolvers.ts deleted file mode 100644 index 7221be97ea..0000000000 --- a/packages/algolia-products/mesh/categories/resolvers.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { type Resolvers } from '@graphcommerce/graphql-mesh' -import { getStoreConfig } from '../getStoreConfig' -import { algoliaHitToMagentoCategory, CategoriesItemsItem } from './algoliaHitToMagentoCategory' -import { getCategoryResults } from './getCategoryResults' - -export const resolvers: Resolvers = { - Query: { - categories: async (root, args, context, info) => { - const isAgolia = (args.filters?.engine?.in ?? [args.filters?.engine?.eq])[0] === 'algolia' - - if (!isAgolia) return context.m2.Query.categories({ root, args, context, info }) - - const items: (CategoriesItemsItem | null)[] = [] - - const [algoliaResponse, storeConfig] = await Promise.all([ - await getCategoryResults(args, context, info), - await getStoreConfig(context), - ]) - - if (!algoliaResponse?.hits) return context.m2.Query.categories({ root, args, context, info }) - for (const hit of algoliaResponse.hits) { - if (hit?.objectID) { - const category = algoliaHitToMagentoCategory(hit, storeConfig) - items.push(category) - } - } - - return { - items, - page_info: { - current_page: algoliaResponse.page + 1, - page_size: algoliaResponse.hitsPerPage, - total_pages: algoliaResponse.nbPages, - }, - total_count: algoliaResponse.nbHits, - } - }, - }, -} From bca47b81061ea8608753cfc8940ce4db65ab27e3 Mon Sep 17 00:00:00 2001 From: Renzo van Hootegem Date: Tue, 1 Oct 2024 16:16:57 +0200 Subject: [PATCH 67/70] Added changeset for algolia categories --- .changeset/fast-eyes-fail.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/fast-eyes-fail.md diff --git a/.changeset/fast-eyes-fail.md b/.changeset/fast-eyes-fail.md new file mode 100644 index 0000000000..5060f8e7f2 --- /dev/null +++ b/.changeset/fast-eyes-fail.md @@ -0,0 +1,5 @@ +--- +'@graphcommerce/algolia-categories': minor +--- + +Enables algolia category search in graphql mesh. Integrated algolias category search into categories type within the mesh. This will only be used on search pages From 56fcc45b60e43574c64fcdd7b02f8062d677e250 Mon Sep 17 00:00:00 2001 From: Renzo van Hootegem Date: Tue, 1 Oct 2024 16:24:29 +0200 Subject: [PATCH 68/70] Added changesets --- .changeset/six-geese-look.md | 5 +++++ .changeset/strange-tools-kiss.md | 5 +++++ .changeset/wild-news-notice.md | 5 +++++ 3 files changed, 15 insertions(+) create mode 100644 .changeset/six-geese-look.md create mode 100644 .changeset/strange-tools-kiss.md create mode 100644 .changeset/wild-news-notice.md diff --git a/.changeset/six-geese-look.md b/.changeset/six-geese-look.md new file mode 100644 index 0000000000..c373dc230e --- /dev/null +++ b/.changeset/six-geese-look.md @@ -0,0 +1,5 @@ +--- +'@graphcommerce/algolia-personalization': minor +--- + +Integrated algolia personalisation and algolia analytics into graphql mesh. Built user profiles and gives user personalised results. Keep track of the user data and view in algolia analytics dashboard diff --git a/.changeset/strange-tools-kiss.md b/.changeset/strange-tools-kiss.md new file mode 100644 index 0000000000..2479956e96 --- /dev/null +++ b/.changeset/strange-tools-kiss.md @@ -0,0 +1,5 @@ +--- +'@graphcommerce/algolia-products': minor +--- + +Integrated algolia product queries into graphql-mesh. Provides fast and accurate searches, that can be personalised via magento-algolia and its algolia dashboard diff --git a/.changeset/wild-news-notice.md b/.changeset/wild-news-notice.md new file mode 100644 index 0000000000..e87ce8111d --- /dev/null +++ b/.changeset/wild-news-notice.md @@ -0,0 +1,5 @@ +--- +'@graphcommerce/algolia-recommend': minor +--- + +Integrated algolia recommend queries into graphql mesh. Provide accurate upsells and related products on pdp pages From f3cb518793954ee3603f9b9ab87a2439dac254cc Mon Sep 17 00:00:00 2001 From: Paul Hachmang Date: Wed, 2 Oct 2024 10:00:38 +0200 Subject: [PATCH 69/70] Updated tests --- package.json | 2 +- .../__tests__/interceptors/findPlugins.ts | 58 +++++++++---------- .../utils/resolveDependenciesSync.ts | 4 +- yarn.lock | 42 +++++++++++++- 4 files changed, 71 insertions(+), 35 deletions(-) diff --git a/package.json b/package.json index c2e1d27162..7049fae367 100644 --- a/package.json +++ b/package.json @@ -35,7 +35,7 @@ "packages:7": "yarn workspace @graphcommerce/graphql-codegen-markdown-docs dev", "create-patch": "patch-package --exclude 'package.json$|gql.ts$|interceptor.tsx$'", "jest-watch": "yarn test --watch", - "test": "PRIVATE_ADDITIONAL_DEPENDENCIES=\"@graphcommerce/magento-cart-pickup,@graphcommerce/magento-payment-braintree,@graphcommerce/mollie-magento-payment,@graphcommerce/magento-payment-paypal,@graphcommerce/algolia-mesh,@graphcommerce/algolia-personalization\" NODE_OPTIONS=\"--experimental-vm-modules\" jest" + "test": "PRIVATE_ADDITIONAL_DEPENDENCIES=\"@graphcommerce/magento-cart-pickup,@graphcommerce/magento-payment-braintree,@graphcommerce/mollie-magento-payment,@graphcommerce/magento-payment-paypal,@graphcommerce/algolia-categories,@graphcommerce/algolia-products,@graphcommerce/algolia-personalization,@graphcommerce/algolia-recommend\" NODE_OPTIONS=\"--experimental-vm-modules\" jest" }, "sideEffects": false, "prettier": "@graphcommerce/prettier-config-pwa", diff --git a/packagesDev/next-config/__tests__/interceptors/findPlugins.ts b/packagesDev/next-config/__tests__/interceptors/findPlugins.ts index 27995a947c..809f947939 100644 --- a/packagesDev/next-config/__tests__/interceptors/findPlugins.ts +++ b/packagesDev/next-config/__tests__/interceptors/findPlugins.ts @@ -235,6 +235,14 @@ it('finds plugins', () => { "targetModule": "@graphcommerce/magento-cart-payment-method", "type": "component", }, + { + "enabled": true, + "sourceExport": "meshConfig", + "sourceModule": "@graphcommerce/algolia-categories/plugins/meshConfigAlgoliaCategories", + "targetExport": "meshConfig", + "targetModule": "@graphcommerce/graphql-mesh/meshConfig", + "type": "function", + }, { "enabled": true, "sourceExport": "meshConfig", @@ -246,7 +254,15 @@ it('finds plugins', () => { { "enabled": true, "sourceExport": "meshConfig", - "sourceModule": "@graphcommerce/algolia-mesh/plugins/meshConfigAlgolia", + "sourceModule": "@graphcommerce/algolia-recommend/plugins/meshConfigAlgoliaRecommend", + "targetExport": "meshConfig", + "targetModule": "@graphcommerce/graphql-mesh/meshConfig", + "type": "function", + }, + { + "enabled": true, + "sourceExport": "meshConfig", + "sourceModule": "@graphcommerce/algolia-products/plugins/meshConfigAlgolia", "targetExport": "meshConfig", "targetModule": "@graphcommerce/graphql-mesh/meshConfig", "type": "function", @@ -254,7 +270,7 @@ it('finds plugins', () => { { "enabled": true, "sourceExport": "useProductListApplySearchDefaults", - "sourceModule": "@graphcommerce/algolia-mesh/plugins/magentoSearchApplyAlgoliaEngine", + "sourceModule": "@graphcommerce/algolia-products/plugins/magentoSearchApplyAlgoliaEngine", "targetExport": "useProductListApplySearchDefaults", "targetModule": "@graphcommerce/magento-search", "type": "function", @@ -262,7 +278,7 @@ it('finds plugins', () => { { "enabled": true, "sourceExport": "productListApplySearchDefaults", - "sourceModule": "@graphcommerce/algolia-mesh/plugins/magentoSearchApplyAlgoliaEngine", + "sourceModule": "@graphcommerce/algolia-products/plugins/magentoSearchApplyAlgoliaEngine", "targetExport": "productListApplySearchDefaults", "targetModule": "@graphcommerce/magento-search", "type": "function", @@ -270,7 +286,7 @@ it('finds plugins', () => { { "enabled": true, "sourceExport": "searchDefaultsToProductListFilters", - "sourceModule": "@graphcommerce/algolia-mesh/plugins/magentoSearchApplyAlgoliaEngine", + "sourceModule": "@graphcommerce/algolia-products/plugins/magentoSearchApplyAlgoliaEngine", "targetExport": "searchDefaultsToProductListFilters", "targetModule": "@graphcommerce/magento-search", "type": "function", @@ -278,7 +294,7 @@ it('finds plugins', () => { { "enabled": true, "sourceExport": "ProductListItemsBase", - "sourceModule": "@graphcommerce/algolia-mesh/plugins/ProductListItemsBaseAlgolia", + "sourceModule": "@graphcommerce/algolia-products/plugins/ProductListItemsBaseAlgolia", "targetExport": "ProductListItemsBase", "targetModule": "@graphcommerce/magento-product", "type": "component", @@ -613,40 +629,22 @@ it('finds plugins', () => { "targetModule": "@graphcommerce/google-datalayer", "type": "function", }, - { - "enabled": false, - "ifConfig": "algolia.personalizationEnabled", - "sourceExport": "getSearchSuggestionsInput", - "sourceModule": "@graphcommerce/algolia-personalization/plugins/getSearchSuggestionsPersonalization", - "targetExport": "getSearchSuggestionsInput", - "targetModule": "@graphcommerce/algolia-mesh", - "type": "function", - }, { "enabled": false, "ifConfig": "algolia.analyticsEnabled", "sourceExport": "getSearchSuggestionsInput", "sourceModule": "@graphcommerce/algolia-personalization/plugins/getSearchSuggestionsAnalytics", "targetExport": "getSearchSuggestionsInput", - "targetModule": "@graphcommerce/algolia-mesh", + "targetModule": "@graphcommerce/algolia-products", "type": "function", }, { "enabled": false, - "ifConfig": "algolia.personalizationEnabled", - "sourceExport": "getSearchResultsInput", - "sourceModule": "@graphcommerce/algolia-personalization/plugins/getSearchResultsPersonalization", - "targetExport": "getSearchResultsInput", - "targetModule": "@graphcommerce/algolia-mesh", - "type": "function", - }, - { - "enabled": false, - "ifConfig": "algolia.personalizationEnabled", + "ifConfig": "algolia.analyticsEnabled", "sourceExport": "getSearchResultsInput", "sourceModule": "@graphcommerce/algolia-personalization/plugins/getSearchResultsAnalytics", "targetExport": "getSearchResultsInput", - "targetModule": "@graphcommerce/algolia-mesh", + "targetModule": "@graphcommerce/algolia-products", "type": "function", }, { @@ -671,7 +669,7 @@ it('finds plugins', () => { "enabled": false, "ifConfig": "algolia.catalogEnabled", "sourceExport": "useProductListApplyCategoryDefaults", - "sourceModule": "@graphcommerce/algolia-mesh/plugins/magentoProductApplyAlgoliaEngine", + "sourceModule": "@graphcommerce/algolia-products/plugins/magentoProductApplyAlgoliaEngine", "targetExport": "useProductListApplyCategoryDefaults", "targetModule": "@graphcommerce/magento-product", "type": "function", @@ -680,7 +678,7 @@ it('finds plugins', () => { "enabled": false, "ifConfig": "algolia.catalogEnabled", "sourceExport": "productListApplyCategoryDefaults", - "sourceModule": "@graphcommerce/algolia-mesh/plugins/magentoProductApplyAlgoliaEngine", + "sourceModule": "@graphcommerce/algolia-products/plugins/magentoProductApplyAlgoliaEngine", "targetExport": "productListApplyCategoryDefaults", "targetModule": "@graphcommerce/magento-product", "type": "function", @@ -689,7 +687,7 @@ it('finds plugins', () => { "enabled": false, "ifConfig": "algolia.catalogEnabled", "sourceExport": "categoryDefaultsToProductListFilters", - "sourceModule": "@graphcommerce/algolia-mesh/plugins/magentoProductApplyAlgoliaEngine", + "sourceModule": "@graphcommerce/algolia-products/plugins/magentoProductApplyAlgoliaEngine", "targetExport": "categoryDefaultsToProductListFilters", "targetModule": "@graphcommerce/magento-product", "type": "function", @@ -698,7 +696,7 @@ it('finds plugins', () => { "enabled": false, "ifConfig": "algolia.customerGroupPricingEnabled", "sourceExport": "GraphQLProvider", - "sourceModule": "@graphcommerce/algolia-mesh/plugins/GraphQLProviderAlgoliaCustomerGroupId", + "sourceModule": "@graphcommerce/algolia-products/plugins/GraphQLProviderAlgoliaCustomerGroupId", "targetExport": "GraphQLProvider", "targetModule": "@graphcommerce/graphql", "type": "component", diff --git a/packagesDev/next-config/__tests__/utils/resolveDependenciesSync.ts b/packagesDev/next-config/__tests__/utils/resolveDependenciesSync.ts index 5cb799ae47..7df78af557 100644 --- a/packagesDev/next-config/__tests__/utils/resolveDependenciesSync.ts +++ b/packagesDev/next-config/__tests__/utils/resolveDependenciesSync.ts @@ -41,8 +41,10 @@ it('resolves dependences', () => { "@graphcommerce/magento-cart-items" => "packages/magento-cart-items", "@graphcommerce/lingui-next" => "packages/lingui-next", "@graphcommerce/magento-payment-paypal" => "packages/magento-payment-paypal", + "@graphcommerce/algolia-categories" => "packages/algolia-categories", "@graphcommerce/algolia-personalization" => "packages/algolia-personalization", - "@graphcommerce/algolia-mesh" => "packages/algolia-mesh", + "@graphcommerce/algolia-recommend" => "packages/algolia-recommend", + "@graphcommerce/algolia-products" => "packages/algolia-products", "@graphcommerce/next-config" => "packagesDev/next-config", "@graphcommerce/magento-search" => "packages/magento-search", "@graphcommerce/google-datalayer" => "packages/google-datalayer", diff --git a/yarn.lock b/yarn.lock index c91dfd8953..90543d750b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2819,13 +2819,14 @@ __metadata: languageName: unknown linkType: soft -"@graphcommerce/algolia-mesh@workspace:packages/algolia-mesh": +"@graphcommerce/algolia-categories@workspace:packages/algolia-categories": version: 0.0.0-use.local - resolution: "@graphcommerce/algolia-mesh@workspace:packages/algolia-mesh" + resolution: "@graphcommerce/algolia-categories@workspace:packages/algolia-categories" dependencies: graphql: "npm:^16.0.0" tsx: "npm:^4.16.2" peerDependencies: + "@graphcommerce/algolia-products": ^9.0.0-canary.82 "@graphcommerce/google-datalayer": ^9.0.0-canary.82 "@graphcommerce/graphql": ^9.0.0-canary.82 "@graphcommerce/graphql-mesh": ^9.0.0-canary.82 @@ -2845,18 +2846,53 @@ __metadata: graphql: "npm:^16.0.0" tsx: "npm:^4.16.2" peerDependencies: - "@graphcommerce/algolia-mesh": ^9.0.0-canary.82 + "@graphcommerce/algolia-products": ^9.0.0-canary.76 + "@graphcommerce/google-datalayer": ^9.0.0-canary.76 + "@graphcommerce/graphql": ^9.0.0-canary.76 + "@graphcommerce/graphql-mesh": ^9.0.0-canary.76 + "@graphcommerce/magento-customer": ^9.0.0-canary.76 + "@graphcommerce/magento-product": ^9.0.0-canary.76 + "@graphcommerce/next-config": ^9.0.0-canary.76 + "@graphcommerce/next-ui": ^9.0.0-canary.76 + react: ^18.2.0 + languageName: unknown + linkType: soft + +"@graphcommerce/algolia-products@workspace:packages/algolia-products": + version: 0.0.0-use.local + resolution: "@graphcommerce/algolia-products@workspace:packages/algolia-products" + dependencies: + graphql: "npm:^16.0.0" + tsx: "npm:^4.16.2" + peerDependencies: "@graphcommerce/google-datalayer": ^9.0.0-canary.82 "@graphcommerce/graphql": ^9.0.0-canary.82 "@graphcommerce/graphql-mesh": ^9.0.0-canary.82 "@graphcommerce/magento-customer": ^9.0.0-canary.82 "@graphcommerce/magento-product": ^9.0.0-canary.82 + "@graphcommerce/magento-search": ^9.0.0-canary.82 "@graphcommerce/next-config": ^9.0.0-canary.82 "@graphcommerce/next-ui": ^9.0.0-canary.82 react: ^18.2.0 languageName: unknown linkType: soft +"@graphcommerce/algolia-recommend@workspace:packages/algolia-recommend": + version: 0.0.0-use.local + resolution: "@graphcommerce/algolia-recommend@workspace:packages/algolia-recommend" + dependencies: + graphql: "npm:^16.0.0" + tsx: "npm:^4.16.2" + peerDependencies: + "@graphcommerce/algolia-products": ^9.0.0-canary.76 + "@graphcommerce/graphql": ^9.0.0-canary.76 + "@graphcommerce/graphql-mesh": ^9.0.0-canary.76 + "@graphcommerce/next-config": ^9.0.0-canary.76 + "@graphcommerce/next-ui": ^9.0.0-canary.76 + react: ^18.2.0 + languageName: unknown + linkType: soft + "@graphcommerce/algolia-search@workspace:packages/algolia-search": version: 0.0.0-use.local resolution: "@graphcommerce/algolia-search@workspace:packages/algolia-search" From bbc7d69595dbf2c01c2b99d62452c707ab39f67e Mon Sep 17 00:00:00 2001 From: Paul Hachmang Date: Wed, 2 Oct 2024 12:12:55 +0200 Subject: [PATCH 70/70] Renamed packages and created REAMDE --- docs/magento/algolia.md | 298 ++++++++++++++++-- packages/algolia-categories/README.md | 13 +- .../CHANGELOG.md | 0 packages/algolia-insights/README.md | 3 + .../algolia-insights-spec.yaml | 0 .../graphql}/AlgoliaSendEvent.graphql | 0 .../hooks/useSendAlgoliaEvent.ts | 10 +- packages/algolia-insights/index.ts | 2 + packages/algolia-insights/package.json | 34 ++ .../plugins/getSearchResultsInputInsights.ts | 16 + .../getSearchSuggestionsInputInsights.ts | 14 + .../plugins/meshConfigAlgoliaInsights.ts | 2 +- .../plugins/useSendEventAlgolia.ts | 1 - .../scripts/generate-insights-spec.mts | 0 packages/algolia-insights/tsconfig.json | 5 + .../algolia-personalization/Config.graphqls | 6 - packages/algolia-personalization/README.md | 36 +-- packages/algolia-personalization/index.ts | 1 + packages/algolia-personalization/package.json | 1 + .../plugins/InContextInputAlgoliaUserToken.ts | 3 +- ...> getSearchResultsInputPersonalization.ts} | 2 - ...tSearchSuggestionsInputPersonalization.ts} | 2 - packages/algolia-products/Config.graphqls | 7 +- .../mesh/getSearchSuggestionsInput.ts | 3 - .../__tests__/interceptors/findPlugins.ts | 103 +++--- .../utils/resolveDependenciesSync.ts | 1 + 26 files changed, 419 insertions(+), 144 deletions(-) rename packages/{algolia-personalization => algolia-insights}/CHANGELOG.md (100%) create mode 100644 packages/algolia-insights/README.md rename packages/{algolia-personalization => algolia-insights}/algolia-insights-spec.yaml (100%) rename packages/{algolia-personalization/mutations => algolia-insights/graphql}/AlgoliaSendEvent.graphql (100%) rename packages/{algolia-personalization => algolia-insights}/hooks/useSendAlgoliaEvent.ts (97%) create mode 100644 packages/algolia-insights/index.ts create mode 100644 packages/algolia-insights/package.json create mode 100644 packages/algolia-insights/plugins/getSearchResultsInputInsights.ts create mode 100644 packages/algolia-insights/plugins/getSearchSuggestionsInputInsights.ts rename packages/{algolia-personalization => algolia-insights}/plugins/meshConfigAlgoliaInsights.ts (95%) rename packages/{algolia-personalization => algolia-insights}/plugins/useSendEventAlgolia.ts (94%) rename packages/{algolia-personalization => algolia-insights}/scripts/generate-insights-spec.mts (100%) create mode 100644 packages/algolia-insights/tsconfig.json delete mode 100644 packages/algolia-personalization/Config.graphqls create mode 100644 packages/algolia-personalization/index.ts rename packages/algolia-personalization/plugins/{getSearchResultsAnalytics.ts => getSearchResultsInputPersonalization.ts} (90%) rename packages/algolia-personalization/plugins/{getSearchSuggestionsAnalytics.ts => getSearchSuggestionsInputPersonalization.ts} (90%) diff --git a/docs/magento/algolia.md b/docs/magento/algolia.md index 9c2506b705..2b8dbf8175 100644 --- a/docs/magento/algolia.md +++ b/docs/magento/algolia.md @@ -1,31 +1,34 @@ -# Algolia Magento 2 +# Implementing Advanced Search with Algolia in GraphCommerce -An implementation of Algolia through the GraphQL Mesh. +The need for fast, relevant, and personalized search experiences has become +paramount. GraphCommerce, This integration offers developers a robust solution +to enhance search capabilities while maintaining the flexibility and performance +that GraphCommerce is known for. -## Prerequisites +In this article, we'll dive deep into the technical aspects of implementing +Algolia in GraphCommerce, exploring the architecture, key components, and best +practices for leveraging this powerful integration. -Make sure the Magento 2 module is correctly installed and working. +The Algolia integration in GraphCommerce is designed with a clear separation +between the GraphQL layer and the frontend implementation. This architecture +allows for seamless integration with existing GraphCommerce setups while +providing the flexibility to customize and extend functionality as needed. -## Installation +The is composed of five packages: -1. Find current version of your `@graphcommerce/next-ui` in your package.json. -2. `yarn add @graphcommerce/address-fields-nl@9.0.0` (replace 9.0.0 with the - version of the step above) -3. Configure the following ([configuration values](./Config.graphqls)) in your - graphcommerce.config.js: +- `@graphcommerce/algolia-products` +- `@graphcommerce/algolia-categories` +- `@graphcommerce/algolia-insights` +- `@graphcommerce/algolia-personalization` +- `@graphcommerce/algolia-recommend` - ```js - const config = { - algolia: { - applicationId: 'your-algolia-application-id', // Stores > Configuration > Algolia Search > Credentials and Basic Setup > Application ID - indexNamePrefix: 'default_', // Stores > Configuration > Algolia Search > Credentials and Basic Setup > Index name prefix - searchOnlyApiKey: 'blabla', // Stores > Configuration > Algolia Search > Credentials and Basic Setup > Search-only (public) API key - catalogEnabled: false, // start with search and if everything works as expected, you can move on to the catalog. - }, - } - ``` +## Installation and configuration of Algolia for GraphCommerce -### Configuration +### Preparation + +To integrate Magento's product catalog with GraphCommerce, you need to set up +your Algolia indexes. This is typically done using the Magento 2 Algolia module, +which handles the synchronization of product and category data. 1. Setup the fields that you want to index in algolia `Stores > configuration > Algolia Search > Products > Products` @@ -38,11 +41,256 @@ Make sure the Magento 2 module is correctly installed and working. 4. To configure sorting options, enable Virtual Replica's. `Stores > Configuration > Algolia Search > Instant Search Results Page > Use Virtual Replica` +### @graphcommerce/algolia-products + +After this package is installed and configured the search is automatically +replaced by Algolia's search. + +This package is responsible for integrating Algolia's search functionality into +GraphCommerce's GraphQL layer. It includes several key components: + +- **Search Result Mapping**: Converts Algolia search hits to Magento-compatible + product objects. +- **Facet Handling**: Transforms Algolia facets into Magento-style aggregations + for consistent filtering. +- **Query Input Preparation**: Prepares search inputs to align with Algolia's + query structure. + +Find current version of your `@graphcommerce/next-ui` in your package.json. And +run `yarn add @graphcommerce/algolia-products@9.0.0` (replace 9.0.0 with the +same version as the `@graphcommerce/next-ui`) + +To enable Algolia in your GraphCommerce project, you'll need to add the +necessary configuration to your `graphcommerce.config.js` file: + +```ts +module.exports = { + // Other configuration... + algolia: { + // Configure your Algolia application ID. + // Stores > Configuration > Algolia Search > Credentials and Basic Setup > Application ID + applicationId: 'SAME_AS_MAGENTO', + + // Configure your Algolia Search Only API Key. + // Stores > Configuration > Algolia Search > Credentials and Basic Setup > Search-only (public) API key + searchOnlyApiKey: 'SAME_AS_MAGENTO', + + // Stores > Configuration > Algolia Search > Credentials and Basic Setup > Index name prefix + indexNamePrefix: 'SAME_AS_MAGENTO', + + // By default the catalog will not use algolia. Set this to true to enable Algolia for the catalog. + catalogEnabled: true, + + // Enable Algolia customer group pricing. + // Please be aware that personalization needs to be enabled as well to make this work. + customerGroupPricingEnabled: true, + }, +} +``` + +When writing your own product queries, you can set the engine to `algolia` to +use Algolia's search. + +```graphql +query AlgoliaProducts { + products(filter: { engine: { eq: "algolia" } }) { + items { + name + } + } +} +``` + +See the +[algoliaHitToMagentoProduct](https://github.com/graphcommerce-org/graphcommerce/blob/main/packages/algolia-products/mesh/algoliaHitToMagentoProduct.ts) +for all the fields that are returned. + +### @graphcommerce/algolia-categories + +This package is responsible for integrating Algolia's category search +functionality into GraphCommerce's GraphQL layer. + +We currenly do not support replacing the whole category tree with Algolia's +categories, because only a reduced set of fields are indexed to algolia. + +When writing your own category queries, you can set the engine to `algolia` to +use Algolia's search. + +```graphql +query AlgoliaCategories { + categories(filters: { engine: { eq: "algolia" } }) { + items { + name + breadcrumbs { + category_name + } + } + } +} +``` + +See the +[algoliaHitToMagentoCategory](https://github.com/graphcommerce-org/graphcommerce/blob/main/packages/algolia-categories/mesh/algoliaHitToMagentoCategory.ts) +for all the fields that are returned. + +### @graphcommerce/algolia-insights + +This package implements Algolia's insights features, which are used to track +user interactions and improve search relevance. These events are crucial for +enabling features like NeuralSearch, Dynamic Re-Ranking, Query Categorization, +Analytics, A/B Testing, Recommend, and Personalization. + +1. When installed the feature is automatically enabled. +2. Browse the site and verify that events are being logged in the Algolia + dashboard. You can check this in the Event Debugger: + `Data Sources > Events > Debugger`. + +The events as described in +[Algolia's event types page](https://www.algolia.com/doc/guides/sending-events/concepts/event-types) +are sent to Algolia from the codebase. + +The event flow implemented is as follows: + +1. When a user sees a product on the screen (IntersectionObserver) the + `viewedObjectIDs`/`viewedObjectIDsAfterSearch` is send. +2. When a user views a list of products the `viewedFilters`/`clickedFilters` are + send. +3. When a user clicks on a product the + `clickedObjectIDs`/`clickedObjectIDsAfterSearch` is send. +4. When a user adds a product to the cart the + `addedToCartObjectIDs`/`addedToCartObjectIDsAfterSearch` is send. We + automatically track where each item in the cart originated form. We track + every item in the cart and match them to the original query that was used to + find them. +5. When a user purchases a product the + `purchasedObjectIDs`/`purchasedObjectIDsAfterSearch` is send. We + automatically track where each item in the purchase originated form. We track + every item in the purchase and match them to the original query that was used + to find them. + +Note: The `convertedObjectIds`/`convertedObjectIDsAfterSearch` are not +implemented, they are a simplified version of the +`addedToCartObjectIDs`/`addedToCartObjectIDsAfterSearch` + +The `useSendAlgoliaEvent` hook in `@graphcommerce/algolia-insights` is +responsible for sending these events to Algolia. + +### @graphcommerce/algolia-personalization + +This package requires insights to be sent to Algolia for them to be able to +create personalized queries. Once you've collected several events, set up +personalization strategies in the Algolia dashboard under +`Enhance > Personalization`. + +This package implements Algolia's personalization features, including: +Personalized search results and User token management for consistent +identification + +Note: This package is optional because this will increase the amount of API +calls send to Algolia which can increase your costs when viewing lists of +products that otherwise are static like related products, category queries etc. + +### @graphcommerce/algolia-recommend + +This package requires insights to be sent to Algolia for them to be able to +create recommendations. Once you've collected several events, set up +recommendation strategies in the Algolia dashboard under `Recommend > Models`. + +This package implements Algolia's recommendation features, including: + +- Related products +- Frequently bought together +- Similar looking items + +The recommend package can replace various sections of Magento's default fields. +So this can be a drop-in replacement. You can configure which fields to replace +by updating your `graphcommerce.config.js`: + +```ts +module.exports = { + algolia: { + lookingSimilar: 'UPSELL_PRODUCTS', + frequentlyBoughtTogether: 'CROSSSELL_PRODUCTS', + relatedProducts: 'RELATED_PRODUCTS', + }, +} +``` + +Besides this the features are also available as separate fields on products: + +```graphql +query AlgoliaProducts { + products(filter: { engine: { eq: "algolia" } }) { + items { + name + algolia_looking_similar { + name + } + algolia_related_products { + name + } + algolia_frequently_bought_together { + name + } + } + } +} +``` + +## Multi-store and Multi-language Support + +The Algolia integration in GraphCommerce supports multi-store and multi-language +setups out of the box. It uses the indexes created by the Magento 2 Algolia +module. + +## Additional Configuration + +### Sorting Options + +To configure sorting options for your Algolia-powered search, you need to enable +Virtual Replicas in your Magento 2 Algolia module configuration: + +1. Navigate to + `Stores > Configuration > Algolia Search > Instant Search Results Page` +2. Enable the "Use Virtual Replica" option + +This allows you to define different sorting options, each of which will be +treated as a separate (virtual) index in Algolia. + +### Customer Group Pricing + +To enable customer group pricing, make sure customers groups prices are mapped +to algolia. +`Stores > Configuration > Algolia Search > Advanced > Enable Customer Groups`. + +⚠️ Warning: Catalog price rules for a specific customer group do not seem to be +indexed.It seems only: `[Product] > Advanced Pricing > Customer Group Price` +gets indexed. + +Note: The GraphQL API does not expose the customer group_id by default. We're +doing an additional REST API call to get the value. This means a somewhat slower +(few hundred ms) when the Customer is loaded. + ### Customization -Customise the code by creating plugins on the functions you want to adjust +By default `algoliaFacetsToAggregations` and `algoliaHitToMagentoProduct` are +split into it's own functions so plugins can be easily written. + +## Conclusion + +The Algolia integration in GraphCommerce offers a powerful toolset for +implementing advanced search and discovery features in your e-commerce +application. By leveraging the separation of concerns between the GraphQL layer +and frontend implementation, you can create a flexible, performant, and +feature-rich search experience. + +As you implement and customize the Algolia integration, remember to focus on +performance, user experience, and data privacy. Regularly analyze search +analytics and user behavior to continually refine and improve your search +implementation. -## Algolia for Catalog +For more information and detailed documentation, visit the GraphCommerce GitHub +repository and Algolia's developer documentation. Happy coding! -Sorting is defined by Algolia and can not be configured per category as each -sorting option is a separate (virtual) index. +TODO: INSERT GRAPHIC - Diagram illustrating the architecture of Algolia +integration in GraphCommerce diff --git a/packages/algolia-categories/README.md b/packages/algolia-categories/README.md index bed10c81f4..24d4b9de3e 100644 --- a/packages/algolia-categories/README.md +++ b/packages/algolia-categories/README.md @@ -1,14 +1,3 @@ # Algolia Magento 2 -An implementation of Algolia categories through the GraphQL Mesh. This will only -apply to the search - -## Prerequisites - -1. Make sure you have configured @graphcommerce/algolia-products. - -## Installation - -1. Find current version of your `@graphcommerce/next-ui` in your package.json. -2. `yarn add @graphcommerce/algolia-categories@9.0.0` (replace 9.0.0 with the - version of the step above) +[See algolia docs](../../docs/magento/algolia.md) diff --git a/packages/algolia-personalization/CHANGELOG.md b/packages/algolia-insights/CHANGELOG.md similarity index 100% rename from packages/algolia-personalization/CHANGELOG.md rename to packages/algolia-insights/CHANGELOG.md diff --git a/packages/algolia-insights/README.md b/packages/algolia-insights/README.md new file mode 100644 index 0000000000..47a4fd9346 --- /dev/null +++ b/packages/algolia-insights/README.md @@ -0,0 +1,3 @@ +# Algolia Insights + +[See algolia docs](../../docs/magento/algolia.md) diff --git a/packages/algolia-personalization/algolia-insights-spec.yaml b/packages/algolia-insights/algolia-insights-spec.yaml similarity index 100% rename from packages/algolia-personalization/algolia-insights-spec.yaml rename to packages/algolia-insights/algolia-insights-spec.yaml diff --git a/packages/algolia-personalization/mutations/AlgoliaSendEvent.graphql b/packages/algolia-insights/graphql/AlgoliaSendEvent.graphql similarity index 100% rename from packages/algolia-personalization/mutations/AlgoliaSendEvent.graphql rename to packages/algolia-insights/graphql/AlgoliaSendEvent.graphql diff --git a/packages/algolia-personalization/hooks/useSendAlgoliaEvent.ts b/packages/algolia-insights/hooks/useSendAlgoliaEvent.ts similarity index 97% rename from packages/algolia-personalization/hooks/useSendAlgoliaEvent.ts rename to packages/algolia-insights/hooks/useSendAlgoliaEvent.ts index 4cde2b7602..68a11214d6 100644 --- a/packages/algolia-personalization/hooks/useSendAlgoliaEvent.ts +++ b/packages/algolia-insights/hooks/useSendAlgoliaEvent.ts @@ -9,7 +9,7 @@ import { cookie } from '@graphcommerce/next-ui' import { useDebounce } from '@graphcommerce/react-hook-form' import { useEventCallback } from '@mui/material' import { useRef } from 'react' -import { AlgoliaSendEventDocument } from '../mutations/AlgoliaSendEvent.gql' +import { AlgoliaSendEventDocument } from '../graphql/AlgoliaSendEvent.gql' const getSHA256Hash = async (input: string) => { const textAsBuffer = new TextEncoder().encode(input) @@ -75,7 +75,7 @@ type AlgoliaEventCommon = { queryID?: string } -let prevFilters: string[] = [] +const prevFilters: Record = {} const dataLayerToAlgoliaMap: { [K in keyof Partial]: ( @@ -98,8 +98,10 @@ const dataLayerToAlgoliaMap: { ) { const filters = mapSelectedFiltersToAlgoliaEvent(eventData.filter_params.filters) - const newlyAppliedFilters = filters.filter((filter) => !prevFilters.includes(filter)) - prevFilters = filters + const newlyAppliedFilters = filters.filter( + (filter) => !prevFilters[eventData.item_list_name]?.includes(filter), + ) + prevFilters[eventData.item_list_name] = filters if (newlyAppliedFilters.length > 0) { events.push({ diff --git a/packages/algolia-insights/index.ts b/packages/algolia-insights/index.ts new file mode 100644 index 0000000000..125f690c09 --- /dev/null +++ b/packages/algolia-insights/index.ts @@ -0,0 +1,2 @@ +export * from './hooks/useSendAlgoliaEvent' +export * from './graphql/AlgoliaSendEvent.gql' diff --git a/packages/algolia-insights/package.json b/packages/algolia-insights/package.json new file mode 100644 index 0000000000..8994e566fa --- /dev/null +++ b/packages/algolia-insights/package.json @@ -0,0 +1,34 @@ +{ + "name": "@graphcommerce/algolia-insights", + "homepage": "https://www.graphcommerce.org/", + "repository": "github:graphcommerce-org/graphcommerce", + "version": "9.0.0-canary.83", + "sideEffects": false, + "prettier": "@graphcommerce/prettier-config-pwa", + "eslintConfig": { + "extends": "@graphcommerce/eslint-config-pwa", + "parserOptions": { + "project": "./tsconfig.json" + } + }, + "scripts": { + "generate": "tsx scripts/generate-insights-spec.mts" + }, + "peerDependencies": { + "@graphcommerce/algolia-products": "^9.0.0-canary.83", + "@graphcommerce/google-datalayer": "^9.0.0-canary.83", + "@graphcommerce/graphql": "^9.0.0-canary.83", + "@graphcommerce/graphql-mesh": "^9.0.0-canary.83", + "@graphcommerce/magento-customer": "^9.0.0-canary.83", + "@graphcommerce/magento-product": "^9.0.0-canary.83", + "@graphcommerce/next-config": "^9.0.0-canary.83", + "@graphcommerce/next-ui": "^9.0.0-canary.83", + "@graphcommerce/react-hook-form": "^9.0.0-canary.83", + "@mui/material": "*", + "react": "^18.2.0" + }, + "devDependencies": { + "graphql": "^16.0.0", + "tsx": "^4.16.2" + } +} diff --git a/packages/algolia-insights/plugins/getSearchResultsInputInsights.ts b/packages/algolia-insights/plugins/getSearchResultsInputInsights.ts new file mode 100644 index 0000000000..08012eff21 --- /dev/null +++ b/packages/algolia-insights/plugins/getSearchResultsInputInsights.ts @@ -0,0 +1,16 @@ +import type { getSearchResultsInput as getSearchResultsInputType } from '@graphcommerce/algolia-products' +import { FunctionPlugin, PluginConfig } from '@graphcommerce/next-config' + +export const config: PluginConfig = { + type: 'function', + module: '@graphcommerce/algolia-products', +} + +export const getSearchResultsInput: FunctionPlugin = async ( + prev, + args, + context, +) => ({ + ...(await prev(args, context)), + analytics: true, +}) diff --git a/packages/algolia-insights/plugins/getSearchSuggestionsInputInsights.ts b/packages/algolia-insights/plugins/getSearchSuggestionsInputInsights.ts new file mode 100644 index 0000000000..281e6ed047 --- /dev/null +++ b/packages/algolia-insights/plugins/getSearchSuggestionsInputInsights.ts @@ -0,0 +1,14 @@ +import type { getSearchSuggestionsInput as getSearchSuggestionsInputType } from '@graphcommerce/algolia-products' +import { FunctionPlugin, PluginConfig } from '@graphcommerce/next-config' + +export const config: PluginConfig = { + type: 'function', + module: '@graphcommerce/algolia-products', +} + +export const getSearchSuggestionsInput: FunctionPlugin< + typeof getSearchSuggestionsInputType +> = async (prev, search, context) => ({ + ...(await prev(search, context)), + analytics: true, +}) diff --git a/packages/algolia-personalization/plugins/meshConfigAlgoliaInsights.ts b/packages/algolia-insights/plugins/meshConfigAlgoliaInsights.ts similarity index 95% rename from packages/algolia-personalization/plugins/meshConfigAlgoliaInsights.ts rename to packages/algolia-insights/plugins/meshConfigAlgoliaInsights.ts index 990983e20b..d2206b4725 100644 --- a/packages/algolia-personalization/plugins/meshConfigAlgoliaInsights.ts +++ b/packages/algolia-insights/plugins/meshConfigAlgoliaInsights.ts @@ -21,7 +21,7 @@ export const meshConfig: FunctionPlugin = ( handler: { openapi: { endpoint: `https://insights.algolia.io/`, - source: '@graphcommerce/algolia-personalization/algolia-insights-spec.yaml', + source: '@graphcommerce/algolia-insights/algolia-insights-spec.yaml', ignoreErrorResponses: true, schemaHeaders: { 'X-Algolia-Application-Id': graphCommerceConfig.algolia.applicationId, diff --git a/packages/algolia-personalization/plugins/useSendEventAlgolia.ts b/packages/algolia-insights/plugins/useSendEventAlgolia.ts similarity index 94% rename from packages/algolia-personalization/plugins/useSendEventAlgolia.ts rename to packages/algolia-insights/plugins/useSendEventAlgolia.ts index bf5aff73ca..a7fb1fd9c3 100644 --- a/packages/algolia-personalization/plugins/useSendEventAlgolia.ts +++ b/packages/algolia-insights/plugins/useSendEventAlgolia.ts @@ -9,7 +9,6 @@ import { useSendAlgoliaEvent } from '../hooks/useSendAlgoliaEvent' export const config: PluginConfig = { module: '@graphcommerce/google-datalayer', type: 'function', - ifConfig: 'algolia.analyticsEnabled', } export const useSendEvent: FunctionPlugin = (prev) => { diff --git a/packages/algolia-personalization/scripts/generate-insights-spec.mts b/packages/algolia-insights/scripts/generate-insights-spec.mts similarity index 100% rename from packages/algolia-personalization/scripts/generate-insights-spec.mts rename to packages/algolia-insights/scripts/generate-insights-spec.mts diff --git a/packages/algolia-insights/tsconfig.json b/packages/algolia-insights/tsconfig.json new file mode 100644 index 0000000000..6595dc2c81 --- /dev/null +++ b/packages/algolia-insights/tsconfig.json @@ -0,0 +1,5 @@ +{ + "exclude": ["**/node_modules", "**/.*/"], + "include": ["**/*.ts", "**/*.tsx"], + "extends": "@graphcommerce/typescript-config-pwa/nextjs.json", +} diff --git a/packages/algolia-personalization/Config.graphqls b/packages/algolia-personalization/Config.graphqls deleted file mode 100644 index 9465ef498f..0000000000 --- a/packages/algolia-personalization/Config.graphqls +++ /dev/null @@ -1,6 +0,0 @@ -extend input GraphCommerceAlgoliaConfig { - """ - Send algolia analytics events. - """ - analyticsEnabled: Boolean -} diff --git a/packages/algolia-personalization/README.md b/packages/algolia-personalization/README.md index 7762ced8e4..24d4b9de3e 100644 --- a/packages/algolia-personalization/README.md +++ b/packages/algolia-personalization/README.md @@ -1,37 +1,3 @@ # Algolia Magento 2 -An implementation of Algolia personalization/Analytics through the GraphQL Mesh. - -## Prerequisites - -1. Make sure you have configured @graphcommerce/algolia-products. -2. As this is an extension of the google-datalayer. Make sure you have installed - @graphcommerce/google-datalayer - -## Installation - -1. Find current version of your `@graphcommerce/next-ui` in your package.json. -2. `yarn add @graphcommerce/algolia-personalization@9.0.0` (replace 9.0.0 with - the version of the step above) - -## Configuration - -1. See [Config](./Config.graphqls) for the configuration values. Add the - following to your graphcommerce.config.js: - - ```js - const config = { - algolia: { - // Even if you do not use personalization, enabling analytics still allows you to track events in Algolia. - algoliaEnableAnalytics: true, - }, - } - ``` - -2. In your algolia dashboard make sure, you have (ai)-personalization enabled. -3. Continue to browse the site, and make sure your events are logged in de event - debugger in your algolia dashboard. In the under left corner. - `Data Sources > Events > Debugger`. Once you've collected several events, you - can start configuring personalization -4. in `Enchance > personalization` setup the strategies. Note: if you can't find - some events, make sure you have send several. +[See algolia docs](../../docs/magento/algolia.md) diff --git a/packages/algolia-personalization/index.ts b/packages/algolia-personalization/index.ts new file mode 100644 index 0000000000..782f7ef3fa --- /dev/null +++ b/packages/algolia-personalization/index.ts @@ -0,0 +1 @@ +export * from './mesh/getUserToken' diff --git a/packages/algolia-personalization/package.json b/packages/algolia-personalization/package.json index e5a6aa9811..5d44bc4314 100644 --- a/packages/algolia-personalization/package.json +++ b/packages/algolia-personalization/package.json @@ -15,6 +15,7 @@ "generate": "tsx scripts/generate-insights-spec.mts" }, "peerDependencies": { + "@graphcommerce/algolia-insights": "^9.0.0-canary.83", "@graphcommerce/algolia-products": "^9.0.0-canary.83", "@graphcommerce/google-datalayer": "^9.0.0-canary.83", "@graphcommerce/graphql": "^9.0.0-canary.83", diff --git a/packages/algolia-personalization/plugins/InContextInputAlgoliaUserToken.ts b/packages/algolia-personalization/plugins/InContextInputAlgoliaUserToken.ts index 3bd4a3e63c..d70f8e7e6a 100644 --- a/packages/algolia-personalization/plugins/InContextInputAlgoliaUserToken.ts +++ b/packages/algolia-personalization/plugins/InContextInputAlgoliaUserToken.ts @@ -1,3 +1,4 @@ +import { ALGOLIA_USER_TOKEN_COOKIE_NAME } from '@graphcommerce/algolia-insights' import { type getInContextInput as getInContextInputType, type useInContextInput as useInContextInputType, @@ -5,12 +6,10 @@ import { import type { InContextInput } from '@graphcommerce/graphql-mesh' import type { FunctionPlugin, PluginConfig } from '@graphcommerce/next-config' import { cookie } from '@graphcommerce/next-ui' -import { ALGOLIA_USER_TOKEN_COOKIE_NAME } from '../hooks/useSendAlgoliaEvent' export const config: PluginConfig = { type: 'function', module: '@graphcommerce/graphql', - ifConfig: 'algolia.personalizationEnabled', } export const getInContextInput: FunctionPlugin = ( diff --git a/packages/algolia-personalization/plugins/getSearchResultsAnalytics.ts b/packages/algolia-personalization/plugins/getSearchResultsInputPersonalization.ts similarity index 90% rename from packages/algolia-personalization/plugins/getSearchResultsAnalytics.ts rename to packages/algolia-personalization/plugins/getSearchResultsInputPersonalization.ts index 09f38f69d3..97d4bd0fd8 100644 --- a/packages/algolia-personalization/plugins/getSearchResultsAnalytics.ts +++ b/packages/algolia-personalization/plugins/getSearchResultsInputPersonalization.ts @@ -5,7 +5,6 @@ import { getUserToken } from '../mesh/getUserToken' export const config: PluginConfig = { type: 'function', module: '@graphcommerce/algolia-products', - ifConfig: 'algolia.analyticsEnabled', } export const getSearchResultsInput: FunctionPlugin = async ( @@ -15,6 +14,5 @@ export const getSearchResultsInput: FunctionPlugin ({ ...(await prev(args, context)), clickAnalytics: true, - analytics: true, userToken: getUserToken(context), }) diff --git a/packages/algolia-personalization/plugins/getSearchSuggestionsAnalytics.ts b/packages/algolia-personalization/plugins/getSearchSuggestionsInputPersonalization.ts similarity index 90% rename from packages/algolia-personalization/plugins/getSearchSuggestionsAnalytics.ts rename to packages/algolia-personalization/plugins/getSearchSuggestionsInputPersonalization.ts index ae7cb4ba80..bb897ddc09 100644 --- a/packages/algolia-personalization/plugins/getSearchSuggestionsAnalytics.ts +++ b/packages/algolia-personalization/plugins/getSearchSuggestionsInputPersonalization.ts @@ -5,7 +5,6 @@ import { getUserToken } from '../mesh/getUserToken' export const config: PluginConfig = { type: 'function', module: '@graphcommerce/algolia-products', - ifConfig: 'algolia.analyticsEnabled', } export const getSearchSuggestionsInput: FunctionPlugin< @@ -13,6 +12,5 @@ export const getSearchSuggestionsInput: FunctionPlugin< > = async (prev, search, context) => ({ ...(await prev(search, context)), clickAnalytics: true, - analytics: true, userToken: getUserToken(context), }) diff --git a/packages/algolia-products/Config.graphqls b/packages/algolia-products/Config.graphqls index 080d3631ca..6a4d3972af 100644 --- a/packages/algolia-products/Config.graphqls +++ b/packages/algolia-products/Config.graphqls @@ -26,13 +26,10 @@ input GraphCommerceAlgoliaConfig { """ catalogEnabled: Boolean - """ - By default fetching categories will not use algolia. Set this to true to enable Algolia for categories. - """ - categoryEnabled: Boolean - """ Enable Algolia customer group pricing. + + Please be aware that personalization needs to be enabled to make this work. """ customerGroupPricingEnabled: Boolean diff --git a/packages/algolia-products/mesh/getSearchSuggestionsInput.ts b/packages/algolia-products/mesh/getSearchSuggestionsInput.ts index 9d295557c4..fa320272c7 100644 --- a/packages/algolia-products/mesh/getSearchSuggestionsInput.ts +++ b/packages/algolia-products/mesh/getSearchSuggestionsInput.ts @@ -1,6 +1,5 @@ import type { MeshContext, Queryalgolia_searchSingleIndexArgs } from '@graphcommerce/graphql-mesh' import { getIndexName } from './getIndexName' -import { getUserToken } from '@graphcommerce/algolia-personalization/mesh/getUserToken' export function isSuggestionsEnabled() { return Boolean(import.meta.graphCommerce.algolia.suggestionsSuffix) @@ -18,7 +17,5 @@ export async function getSearchSuggestionsInput( return { query: search, hitsPerPage: 5, - analytics: true, - userToken: getUserToken(context), } } diff --git a/packagesDev/next-config/__tests__/interceptors/findPlugins.ts b/packagesDev/next-config/__tests__/interceptors/findPlugins.ts index 809f947939..3effd87ce3 100644 --- a/packagesDev/next-config/__tests__/interceptors/findPlugins.ts +++ b/packagesDev/next-config/__tests__/interceptors/findPlugins.ts @@ -243,14 +243,70 @@ it('finds plugins', () => { "targetModule": "@graphcommerce/graphql-mesh/meshConfig", "type": "function", }, + { + "enabled": true, + "sourceExport": "getSearchSuggestionsInput", + "sourceModule": "@graphcommerce/algolia-personalization/plugins/getSearchSuggestionsInputPersonalization", + "targetExport": "getSearchSuggestionsInput", + "targetModule": "@graphcommerce/algolia-products", + "type": "function", + }, + { + "enabled": true, + "sourceExport": "getSearchResultsInput", + "sourceModule": "@graphcommerce/algolia-personalization/plugins/getSearchResultsInputPersonalization", + "targetExport": "getSearchResultsInput", + "targetModule": "@graphcommerce/algolia-products", + "type": "function", + }, + { + "enabled": true, + "sourceExport": "getInContextInput", + "sourceModule": "@graphcommerce/algolia-personalization/plugins/InContextInputAlgoliaUserToken", + "targetExport": "getInContextInput", + "targetModule": "@graphcommerce/graphql", + "type": "function", + }, + { + "enabled": true, + "sourceExport": "useInContextInput", + "sourceModule": "@graphcommerce/algolia-personalization/plugins/InContextInputAlgoliaUserToken", + "targetExport": "useInContextInput", + "targetModule": "@graphcommerce/graphql", + "type": "function", + }, + { + "enabled": true, + "sourceExport": "useSendEvent", + "sourceModule": "@graphcommerce/algolia-insights/plugins/useSendEventAlgolia", + "targetExport": "useSendEvent", + "targetModule": "@graphcommerce/google-datalayer", + "type": "function", + }, { "enabled": true, "sourceExport": "meshConfig", - "sourceModule": "@graphcommerce/algolia-personalization/plugins/meshConfigAlgoliaInsights", + "sourceModule": "@graphcommerce/algolia-insights/plugins/meshConfigAlgoliaInsights", "targetExport": "meshConfig", "targetModule": "@graphcommerce/graphql-mesh/meshConfig", "type": "function", }, + { + "enabled": true, + "sourceExport": "getSearchSuggestionsInput", + "sourceModule": "@graphcommerce/algolia-insights/plugins/getSearchSuggestionsInputInsights", + "targetExport": "getSearchSuggestionsInput", + "targetModule": "@graphcommerce/algolia-products", + "type": "function", + }, + { + "enabled": true, + "sourceExport": "getSearchResultsInput", + "sourceModule": "@graphcommerce/algolia-insights/plugins/getSearchResultsInputInsights", + "targetExport": "getSearchResultsInput", + "targetModule": "@graphcommerce/algolia-products", + "type": "function", + }, { "enabled": true, "sourceExport": "meshConfig", @@ -620,51 +676,6 @@ it('finds plugins', () => { "targetModule": "@graphcommerce/magento-product", "type": "component", }, - { - "enabled": false, - "ifConfig": "algolia.analyticsEnabled", - "sourceExport": "useSendEvent", - "sourceModule": "@graphcommerce/algolia-personalization/plugins/useSendEventAlgolia", - "targetExport": "useSendEvent", - "targetModule": "@graphcommerce/google-datalayer", - "type": "function", - }, - { - "enabled": false, - "ifConfig": "algolia.analyticsEnabled", - "sourceExport": "getSearchSuggestionsInput", - "sourceModule": "@graphcommerce/algolia-personalization/plugins/getSearchSuggestionsAnalytics", - "targetExport": "getSearchSuggestionsInput", - "targetModule": "@graphcommerce/algolia-products", - "type": "function", - }, - { - "enabled": false, - "ifConfig": "algolia.analyticsEnabled", - "sourceExport": "getSearchResultsInput", - "sourceModule": "@graphcommerce/algolia-personalization/plugins/getSearchResultsAnalytics", - "targetExport": "getSearchResultsInput", - "targetModule": "@graphcommerce/algolia-products", - "type": "function", - }, - { - "enabled": false, - "ifConfig": "algolia.personalizationEnabled", - "sourceExport": "getInContextInput", - "sourceModule": "@graphcommerce/algolia-personalization/plugins/InContextInputAlgoliaUserToken", - "targetExport": "getInContextInput", - "targetModule": "@graphcommerce/graphql", - "type": "function", - }, - { - "enabled": false, - "ifConfig": "algolia.personalizationEnabled", - "sourceExport": "useInContextInput", - "sourceModule": "@graphcommerce/algolia-personalization/plugins/InContextInputAlgoliaUserToken", - "targetExport": "useInContextInput", - "targetModule": "@graphcommerce/graphql", - "type": "function", - }, { "enabled": false, "ifConfig": "algolia.catalogEnabled", diff --git a/packagesDev/next-config/__tests__/utils/resolveDependenciesSync.ts b/packagesDev/next-config/__tests__/utils/resolveDependenciesSync.ts index 7df78af557..d2fb4a4707 100644 --- a/packagesDev/next-config/__tests__/utils/resolveDependenciesSync.ts +++ b/packagesDev/next-config/__tests__/utils/resolveDependenciesSync.ts @@ -43,6 +43,7 @@ it('resolves dependences', () => { "@graphcommerce/magento-payment-paypal" => "packages/magento-payment-paypal", "@graphcommerce/algolia-categories" => "packages/algolia-categories", "@graphcommerce/algolia-personalization" => "packages/algolia-personalization", + "@graphcommerce/algolia-insights" => "packages/algolia-insights", "@graphcommerce/algolia-recommend" => "packages/algolia-recommend", "@graphcommerce/algolia-products" => "packages/algolia-products", "@graphcommerce/next-config" => "packagesDev/next-config",