@vendure/core
Version:
A modern, headless ecommerce framework
267 lines • 13.3 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.MysqlSearchStrategy = void 0;
const generated_types_1 = require("@vendure/common/lib/generated-types");
const typeorm_1 = require("typeorm");
const errors_1 = require("../../../common/error/errors");
const transactional_connection_1 = require("../../../connection/transactional-connection");
const constants_1 = require("../constants");
const search_index_item_entity_1 = require("../entities/search-index-item.entity");
const search_strategy_common_1 = require("./search-strategy-common");
const search_strategy_utils_1 = require("./search-strategy-utils");
/**
* @description A weighted fulltext search for MySQL / MariaDB.
*
* @docsCategory DefaultSearchPlugin
*/
class MysqlSearchStrategy {
constructor() {
this.minTermLength = 2;
}
async init(injector) {
this.connection = injector.get(transactional_connection_1.TransactionalConnection);
this.options = injector.get(constants_1.PLUGIN_INIT_OPTIONS);
}
async getFacetValueIds(ctx, input, enabledOnly) {
const facetValuesQb = this.connection
.getRepository(ctx, search_index_item_entity_1.SearchIndexItem)
.createQueryBuilder('si')
.select(['MIN(si.productId)', 'MIN(si.productVariantId)'])
.addSelect('GROUP_CONCAT(si.facetValueIds)', 'facetValues');
this.applyTermAndFilters(ctx, facetValuesQb, Object.assign(Object.assign({}, input), { groupByProduct: true }));
if (!input.groupByProduct) {
facetValuesQb.groupBy('si.productVariantId');
}
if (enabledOnly) {
facetValuesQb.andWhere('si.enabled = :enabled', { enabled: true });
}
const facetValuesResult = await facetValuesQb.getRawMany();
return (0, search_strategy_utils_1.createFacetIdCountMap)(facetValuesResult);
}
async getCollectionIds(ctx, input, enabledOnly) {
const collectionsQb = this.connection
.getRepository(ctx, search_index_item_entity_1.SearchIndexItem)
.createQueryBuilder('si')
.select(['MIN(si.productId)', 'MIN(si.productVariantId)'])
.addSelect('GROUP_CONCAT(si.collectionIds)', 'collections');
this.applyTermAndFilters(ctx, collectionsQb, input);
if (!input.groupByProduct) {
collectionsQb.groupBy('si.productVariantId');
}
if (enabledOnly) {
collectionsQb.andWhere('si.enabled = :enabled', { enabled: true });
}
const collectionsResult = await collectionsQb.getRawMany();
return (0, search_strategy_utils_1.createCollectionIdCountMap)(collectionsResult);
}
async getSearchResults(ctx, input, enabledOnly) {
const take = input.take || 25;
const skip = input.skip || 0;
const sort = input.sort;
const qb = this.connection
.getRepository(ctx, search_index_item_entity_1.SearchIndexItem)
.createQueryBuilder('si')
.select(this.createMysqlSelect(!!input.groupByProduct));
if (input.groupByProduct) {
qb.addSelect('MIN(si.price)', 'minPrice')
.addSelect('MAX(si.price)', 'maxPrice')
.addSelect('MIN(si.priceWithTax)', 'minPriceWithTax')
.addSelect('MAX(si.priceWithTax)', 'maxPriceWithTax');
}
this.applyTermAndFilters(ctx, qb, input);
if (sort) {
if (sort.name) {
qb.addOrderBy('si_productName', sort.name);
}
if (sort.price) {
qb.addOrderBy('si_price', sort.price);
}
}
else if (input.term && input.term.length > this.minTermLength) {
qb.addOrderBy('score', 'DESC');
}
// Required to ensure deterministic sorting.
// E.g. in case of sorting products with duplicate name, price or score results.
qb.addOrderBy('si_productVariantId', 'ASC');
if (enabledOnly) {
qb.andWhere('si.enabled = :enabled', { enabled: true });
}
return qb
.limit(take)
.offset(skip)
.getRawMany()
.then(res => res.map(r => (0, search_strategy_utils_1.mapToSearchResult)(r, this.options.indexCurrencyCode ? r.si_currencyCode : ctx.channel.defaultCurrencyCode)));
}
async getTotalCount(ctx, input, enabledOnly) {
const innerQb = this.applyTermAndFilters(ctx, this.connection
.getRepository(ctx, search_index_item_entity_1.SearchIndexItem)
.createQueryBuilder('si')
.select(this.createMysqlSelect(!!input.groupByProduct)), input);
if (enabledOnly) {
innerQb.andWhere('si.enabled = :enabled', { enabled: true });
}
const totalItemsQb = this.connection.rawConnection
.createQueryBuilder()
.select('COUNT(*) as total')
.from(`(${innerQb.getQuery()})`, 'inner')
.setParameters(innerQb.getParameters());
return totalItemsQb.getRawOne().then(res => res.total);
}
applyTermAndFilters(ctx, qb, input) {
const { term, facetValueFilters, facetValueIds, facetValueOperator, collectionId, collectionSlug, collectionIds, collectionSlugs, } = input;
if (term && term.length > this.minTermLength) {
const safeTerm = term
.replace(/"/g, '')
.replace(/@/g, ' ')
.trim()
.replace(/[+\-*~<>]/g, ' ')
.trim();
const termScoreQuery = this.connection
.getRepository(ctx, search_index_item_entity_1.SearchIndexItem)
.createQueryBuilder('si_inner')
.select('si_inner.productId', 'inner_productId')
.addSelect('si_inner.productVariantId', 'inner_productVariantId')
.addSelect('IF (si_inner.sku LIKE :like_term, 10, 0)', 'sku_score')
.addSelect(`(SELECT sku_score) +
MATCH (si_inner.productName) AGAINST (:term IN BOOLEAN MODE) * 2 +
MATCH (si_inner.productVariantName) AGAINST (:term IN BOOLEAN MODE) * 1.5 +
MATCH (si_inner.description) AGAINST (:term IN BOOLEAN MODE) * 1`, 'score')
.where(new typeorm_1.Brackets(qb1 => {
qb1.where('si_inner.sku LIKE :like_term')
.orWhere('MATCH (si_inner.productName) AGAINST (:term IN BOOLEAN MODE)')
.orWhere('MATCH (si_inner.productVariantName) AGAINST (:term IN BOOLEAN MODE)')
.orWhere('MATCH (si_inner.description) AGAINST (:term IN BOOLEAN MODE)');
}))
.andWhere('si_inner.channelId = :channelId')
.setParameters({
term: `${safeTerm}*`,
like_term: `%${safeTerm}%`,
channelId: ctx.channelId,
});
qb.innerJoin(`(${termScoreQuery.getQuery()})`, 'term_result', 'inner_productId = si.productId')
.addSelect(input.groupByProduct ? 'MAX(term_result.score)' : 'term_result.score', 'score')
.andWhere('term_result.inner_productVariantId = si.productVariantId')
.setParameters(termScoreQuery.getParameters());
}
else {
qb.addSelect('1 as score');
}
if (input.inStock != null) {
if (input.groupByProduct) {
qb.andWhere('si.productInStock = :inStock', { inStock: input.inStock });
}
else {
qb.andWhere('si.inStock = :inStock', { inStock: input.inStock });
}
}
if (facetValueIds === null || facetValueIds === void 0 ? void 0 : facetValueIds.length) {
qb.andWhere(new typeorm_1.Brackets(qb1 => {
for (const id of facetValueIds) {
const placeholder = (0, search_strategy_utils_1.createPlaceholderFromId)(id);
const clause = `FIND_IN_SET(:${placeholder}, si.facetValueIds)`;
const params = { [placeholder]: id };
if (facetValueOperator === generated_types_1.LogicalOperator.AND) {
qb1.andWhere(clause, params);
}
else {
qb1.orWhere(clause, params);
}
}
}));
}
if (facetValueFilters === null || facetValueFilters === void 0 ? void 0 : facetValueFilters.length) {
qb.andWhere(new typeorm_1.Brackets(qb1 => {
for (const facetValueFilter of facetValueFilters) {
qb1.andWhere(new typeorm_1.Brackets(qb2 => {
var _a, _b;
if (facetValueFilter.and && ((_a = facetValueFilter.or) === null || _a === void 0 ? void 0 : _a.length)) {
throw new errors_1.UserInputError('error.facetfilterinput-invalid-input');
}
if (facetValueFilter.and) {
const placeholder = (0, search_strategy_utils_1.createPlaceholderFromId)(facetValueFilter.and);
const clause = `FIND_IN_SET(:${placeholder}, si.facetValueIds)`;
const params = { [placeholder]: facetValueFilter.and };
qb2.where(clause, params);
}
if ((_b = facetValueFilter.or) === null || _b === void 0 ? void 0 : _b.length) {
for (const id of facetValueFilter.or) {
const placeholder = (0, search_strategy_utils_1.createPlaceholderFromId)(id);
const clause = `FIND_IN_SET(:${placeholder}, si.facetValueIds)`;
const params = { [placeholder]: id };
qb2.orWhere(clause, params);
}
}
}));
}
}));
}
if (collectionId) {
qb.andWhere('FIND_IN_SET (:collectionId, si.collectionIds)', { collectionId });
}
if (collectionSlug) {
qb.andWhere('FIND_IN_SET (:collectionSlug, si.collectionSlugs)', { collectionSlug });
}
if (collectionIds === null || collectionIds === void 0 ? void 0 : collectionIds.length) {
qb.andWhere(new typeorm_1.Brackets(qb1 => {
for (const id of Array.from(new Set(collectionIds))) {
const placeholder = (0, search_strategy_utils_1.createPlaceholderFromId)(id);
qb1.orWhere(`FIND_IN_SET(:${placeholder}, si.collectionIds)`, {
[placeholder]: id,
});
}
}));
}
if (collectionSlugs === null || collectionSlugs === void 0 ? void 0 : collectionSlugs.length) {
qb.andWhere(new typeorm_1.Brackets(qb1 => {
for (const slug of Array.from(new Set(collectionSlugs))) {
const placeholder = (0, search_strategy_utils_1.createPlaceholderFromId)(slug);
qb1.orWhere(`FIND_IN_SET(:${placeholder}, si.collectionSlugs)`, {
[placeholder]: slug,
});
}
}));
}
qb.andWhere('si.channelId = :channelId', { channelId: ctx.channelId });
(0, search_strategy_utils_1.applyLanguageConstraints)(qb, ctx.languageCode, ctx.channel.defaultLanguageCode);
if (this.options.indexCurrencyCode) {
qb.andWhere('si.currencyCode = :currencyCode', { currencyCode: ctx.currencyCode });
}
if (input.groupByProduct === true) {
qb.groupBy('si.productId');
qb.addSelect('BIT_OR(si.enabled)', 'productEnabled');
}
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.
*/
createMysqlSelect(groupByProduct) {
return (0, search_strategy_common_1.getFieldsToSelect)(this.options.indexStockStatus, this.options.indexCurrencyCode)
.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' || col === 'inStock' || col === 'productInStock') {
return `MAX(${qualifiedName}) as "${alias}"`;
}
else {
return `MIN(${qualifiedName}) as "${alias}"`;
}
}
else {
return `${qualifiedName} as "${alias}"`;
}
})
.join(', ');
}
}
exports.MysqlSearchStrategy = MysqlSearchStrategy;
//# sourceMappingURL=mysql-search-strategy.js.map