diff --git a/packages/core/src/plugin/default-search-plugin/search-strategy/mysql-search-strategy.ts b/packages/core/src/plugin/default-search-plugin/search-strategy/mysql-search-strategy.ts index 0db7e682bf..f885270544 100644 --- a/packages/core/src/plugin/default-search-plugin/search-strategy/mysql-search-strategy.ts +++ b/packages/core/src/plugin/default-search-plugin/search-strategy/mysql-search-strategy.ts @@ -7,6 +7,7 @@ import { RequestContext } from '../../../api/common/request-context'; import { SearchIndexItem } from '../search-index-item.entity'; import { SearchStrategy } from './search-strategy'; +import { fieldsToSelect } from './search-strategy-common'; import { createFacetIdCountMap, mapToSearchResult } from './search-strategy-utils'; /** @@ -47,7 +48,10 @@ export class MysqlSearchStrategy implements SearchStrategy { const take = input.take || 25; const skip = input.skip || 0; const sort = input.sort; - const qb = this.connection.getRepository(SearchIndexItem).createQueryBuilder('si'); + const qb = this.connection + .getRepository(SearchIndexItem) + .createQueryBuilder('si') + .select(this.createMysqlsSelect(!!input.groupByProduct)); if (input.groupByProduct) { qb.addSelect('MIN(price)', 'minPrice') .addSelect('MAX(price)', 'maxPrice') @@ -75,13 +79,16 @@ export class MysqlSearchStrategy implements SearchStrategy { .take(take) .skip(skip) .getRawMany() - .then(res => res.map(r => mapToSearchResult(r, ctx.channel.currencyCode))); + .then((res) => res.map((r) => mapToSearchResult(r, ctx.channel.currencyCode))); } async getTotalCount(ctx: RequestContext, input: SearchInput, enabledOnly: boolean): Promise { const innerQb = this.applyTermAndFilters( ctx, - this.connection.getRepository(SearchIndexItem).createQueryBuilder('si'), + this.connection + .getRepository(SearchIndexItem) + .createQueryBuilder('si') + .select(this.createMysqlsSelect(!!input.groupByProduct)), input, ); if (enabledOnly) { @@ -93,7 +100,7 @@ export class MysqlSearchStrategy implements SearchStrategy { .select('COUNT(*) as total') .from(`(${innerQb.getQuery()})`, 'inner') .setParameters(innerQb.getParameters()); - return totalItemsQb.getRawOne().then(res => res.total); + return totalItemsQb.getRawOne().then((res) => res.total); } private applyTermAndFilters( @@ -115,7 +122,7 @@ export class MysqlSearchStrategy implements SearchStrategy { 'score', ) .andWhere( - new Brackets(qb1 => { + new Brackets((qb1) => { qb1.where('sku LIKE :like_term') .orWhere('MATCH (productName) AGAINST (:term)') .orWhere('MATCH (productVariantName) AGAINST (:term)') @@ -141,4 +148,33 @@ export class MysqlSearchStrategy implements SearchStrategy { } return qb; } + /** + * When a select statement includes a GROUP BY clause, + * then all selected columns must be aggregated. So we just apply the + * "MIN" function in this case to all other columns than the productId. + */ + private createMysqlsSelect(groupByProduct: boolean): string { + return fieldsToSelect + .map((col) => { + const qualifiedName = `si.${col}`; + const alias = `si_${col}`; + if (groupByProduct && col !== 'productId') { + if ( + col === 'facetIds' || + col === 'facetValueIds' || + col === 'collectionIds' || + col === 'channelIds' + ) { + return `GROUP_CONCAT(${qualifiedName}) as "${alias}"`; + } else if (col === 'enabled') { + return `MAX(${qualifiedName}) as "${alias}"`; + } else { + return `MIN(${qualifiedName}) as "${alias}"`; + } + } else { + return `${qualifiedName} as "${alias}"`; + } + }) + .join(', '); + } } diff --git a/packages/core/src/plugin/default-search-plugin/search-strategy/postgres-search-strategy.ts b/packages/core/src/plugin/default-search-plugin/search-strategy/postgres-search-strategy.ts index 16960255e2..4939b1c8d0 100644 --- a/packages/core/src/plugin/default-search-plugin/search-strategy/postgres-search-strategy.ts +++ b/packages/core/src/plugin/default-search-plugin/search-strategy/postgres-search-strategy.ts @@ -1,12 +1,12 @@ import { SearchInput, SearchResult } from '@vendure/common/lib/generated-types'; import { ID } from '@vendure/common/lib/shared-types'; -import { unique } from '@vendure/common/lib/unique'; import { Brackets, Connection, SelectQueryBuilder } from 'typeorm'; import { RequestContext } from '../../../api/common/request-context'; import { SearchIndexItem } from '../search-index-item.entity'; import { SearchStrategy } from './search-strategy'; +import { fieldsToSelect } from './search-strategy-common'; import { createFacetIdCountMap, mapToSearchResult } from './search-strategy-utils'; /** @@ -81,7 +81,7 @@ export class PostgresSearchStrategy implements SearchStrategy { .take(take) .skip(skip) .getRawMany() - .then(res => res.map(r => mapToSearchResult(r, ctx.channel.currencyCode))); + .then((res) => res.map((r) => mapToSearchResult(r, ctx.channel.currencyCode))); } async getTotalCount(ctx: RequestContext, input: SearchInput, enabledOnly: boolean): Promise { @@ -101,7 +101,7 @@ export class PostgresSearchStrategy implements SearchStrategy { .select('COUNT(*) as total') .from(`(${innerQb.getQuery()})`, 'inner') .setParameters(innerQb.getParameters()); - return totalItemsQb.getRawOne().then(res => res.total); + return totalItemsQb.getRawOne().then((res) => res.total); } private applyTermAndFilters( @@ -130,7 +130,7 @@ export class PostgresSearchStrategy implements SearchStrategy { 'score', ) .andWhere( - new Brackets(qb1 => { + new Brackets((qb1) => { qb1.where('to_tsvector(si.sku) @@ to_tsquery(:term)') .orWhere('to_tsvector(si.productName) @@ to_tsquery(:term)') .orWhere('to_tsvector(si.productVariantName) @@ to_tsquery(:term)') @@ -164,30 +164,8 @@ export class PostgresSearchStrategy implements SearchStrategy { * "MIN" function in this case to all other columns than the productId. */ private createPostgresSelect(groupByProduct: boolean): string { - return [ - 'sku', - 'enabled', - 'slug', - 'price', - 'priceWithTax', - 'productVariantId', - 'languageCode', - 'productId', - 'productName', - 'productVariantName', - 'description', - 'facetIds', - 'facetValueIds', - 'collectionIds', - 'channelIds', - 'productAssetId', - 'productPreview', - 'productPreviewFocalPoint', - 'productVariantAssetId', - 'productVariantPreview', - 'productVariantPreviewFocalPoint', - ] - .map(col => { + return fieldsToSelect + .map((col) => { const qualifiedName = `si.${col}`; const alias = `si_${col}`; if (groupByProduct && col !== 'productId') { diff --git a/packages/core/src/plugin/default-search-plugin/search-strategy/search-strategy-common.ts b/packages/core/src/plugin/default-search-plugin/search-strategy/search-strategy-common.ts new file mode 100644 index 0000000000..9628eb8487 --- /dev/null +++ b/packages/core/src/plugin/default-search-plugin/search-strategy/search-strategy-common.ts @@ -0,0 +1,23 @@ +export const fieldsToSelect = [ + 'sku', + 'enabled', + 'slug', + 'price', + 'priceWithTax', + 'productVariantId', + 'languageCode', + 'productId', + 'productName', + 'productVariantName', + 'description', + 'facetIds', + 'facetValueIds', + 'collectionIds', + 'channelIds', + 'productAssetId', + 'productPreview', + 'productPreviewFocalPoint', + 'productVariantAssetId', + 'productVariantPreview', + 'productVariantPreviewFocalPoint', +];