Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

향수검색에 opensearch 도입 #520

Merged
merged 15 commits into from
Jun 27, 2023
Merged
572 changes: 508 additions & 64 deletions package-lock.json

Large diffs are not rendered by default.

3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
"private": true,
"dependencies": {
"@babel/plugin-proposal-decorators": "^7.22.3",
"@opensearch-project/opensearch": "^1.2.0",
"@types/swagger-ui-express": "^4.1.3",
"@types/winston": "^2.4.4",
"aws-sdk": "^2.1101.0",
Expand Down Expand Up @@ -74,6 +75,7 @@
"@types/morgan": "^1.9.3",
"@types/node": "^17.0.45",
"@types/node-cron": "^3.0.2",
"@types/sinon": "^10.0.15",
"@types/supertest": "^2.0.12",
"@types/swagger-jsdoc": "^6.0.1",
"@types/validator": "^13.7.17",
Expand All @@ -84,6 +86,7 @@
"mocha": "^10.2.0",
"nodemon": "^2.0.22",
"sequelize-cli": "^6.4.1",
"sinon": "^15.2.0",
"supertest": "^6.2.2",
"swagger-jsdoc": "^6.1.0",
"swagger-ui-express": "^4.3.0",
Expand Down
57 changes: 23 additions & 34 deletions src/controllers/Perfume.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,11 +36,9 @@ import { ResponseDTO, SimpleResponseDTO } from '@response/index';
import {
PerfumeIntegralDTO,
ListAndCountDTO,
PerfumeSearchResultDTO,
PerfumeThumbDTO,
PerfumeThumbWithReviewDTO,
PerfumeThumbKeywordDTO,
PerfumeSearchDTO,
PagingDTO,
} from '@dto/index';
import { GenderMap } from '@src/utils/enumType';
Expand Down Expand Up @@ -194,41 +192,32 @@ const getPerfume: RequestHandler = (
* description: Token is missing or invalid
* x-swagger-router-controller: Perfume
* */
const searchPerfume: RequestHandler = (
const searchPerfume: RequestHandler = async (
req: Request | any,
res: Response,
next: NextFunction
): any => {
const loginUserIdx: number = req.middlewareToken.loginUserIdx || -1;
const perfumeSearchRequest: PerfumeSearchRequest =
PerfumeSearchRequest.createByJson(req.body);
const pagingRequestDTO: PagingRequestDTO = PagingRequestDTO.createByJson(
req.query
);
logger.debug(
`${LOG_TAG} likePerfume(userIdx = ${loginUserIdx}, query = ${JSON.stringify(
req.query
)}, body = ${JSON.stringify(req.body)})`
);
const perfumeSearchDTO: PerfumeSearchDTO =
perfumeSearchRequest.toPerfumeSearchDTO(loginUserIdx);
Perfume.searchPerfume(perfumeSearchDTO, pagingRequestDTO.toPageDTO())
.then((result: ListAndCountDTO<PerfumeSearchResultDTO>) => {
return result.convertType(PerfumeResponse.createByJson);
})
.then((response: ListAndCountDTO<PerfumeResponse>) => {
LoggerHelper.logTruncated(
logger.debug,
`${LOG_TAG} searchPerfume's result = ${response}`
);
res.status(StatusCode.OK).json(
new ResponseDTO<ListAndCountDTO<PerfumeResponse>>(
MSG_GET_SEARCH_PERFUME_SUCCESS,
response
)
);
})
.catch((err: Error) => next(err));
) => {
const loginUserIdx = req.middlewareToken.loginUserIdx;
const perfumeSearchDTO = PerfumeSearchRequest.createByJson(
req.body
).toPerfumeSearchDTO(loginUserIdx);
const pagingRequestDTO = PagingRequestDTO.createByJson(req.query);

try {
const result = await Perfume.searchPerfume(
perfumeSearchDTO,
pagingRequestDTO.toPageDTO()
);
const response = result.convertType(PerfumeResponse.createByJson);
res.status(StatusCode.OK).json(
new ResponseDTO<ListAndCountDTO<PerfumeResponse>>(
MSG_GET_SEARCH_PERFUME_SUCCESS,
response
)
);
} catch (err: Error | any) {
next(err);
}
};

/**
Expand Down
12 changes: 6 additions & 6 deletions src/controllers/definitions/request/perfume.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,14 +23,14 @@ import { PerfumeSearchDTO } from '@src/data/dto';
* type: integer
* */
class PerfumeSearchRequest {
readonly keywordList: number[];
readonly brandList: number[];
readonly ingredientList: number[];
readonly keywordList?: number[];
readonly brandList?: number[];
readonly ingredientList?: number[];
readonly searchText: string;
constructor(
keywordList: number[],
brandList: number[],
ingredientList: number[],
keywordList: number[] | undefined,
brandList: number[] | undefined,
ingredientList: number[] | undefined,
searchText: string
) {
this.keywordList = keywordList;
Expand Down
141 changes: 0 additions & 141 deletions src/dao/PerfumeDao.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ import {
PagingDTO,
PerfumeDTO,
PerfumeInquireHistoryDTO,
PerfumeSearchResultDTO,
PerfumeThumbDTO,
} from '@dto/index';

Expand Down Expand Up @@ -38,36 +37,6 @@ const PERFUME_THUMB_COLUMNS: string[] = [

import redis from '@utils/db/redis';

const SQL_SEARCH_PERFUME_SELECT: string =
'SELECT ' +
'p.perfume_idx AS perfumeIdx, p.brand_idx AS brandIdx, p.name, p.english_name AS englishName, p.image_url AS imageUrl, p.created_at AS createdAt, p.updated_at AS updatedAt, ' +
'b.brand_idx AS "Brand.brandIdx", ' +
'b.name AS "Brand.name", ' +
'b.english_name AS "Brand.englishName", ' +
'b.first_initial AS "Brand.firstInitial", ' +
'b.image_url AS "Brand.imageUrl", ' +
'b.description AS "Brand.description", ' +
'b.created_at AS "Brand.createdAt", ' +
'b.updated_at AS "Brand.updatedAt" ' +
'FROM perfumes p ' +
'INNER JOIN brands b ON p.brand_idx = b.brand_idx ' +
'WHERE p.deleted_at IS NULL AND (:whereCondition) ' +
'ORDER BY :orderCondition ' +
'LIMIT :limit ' +
'OFFSET :offset';
const SQL_SEARCH_BRAND_CONDITION: string = ' p.brand_idx IN (:brands)';
const SQL_SEARCH_KEYWORD_CONDITION: string =
'(SELECT COUNT(DISTINCT(jpk.keyword_idx)) FROM join_perfume_keywords jpk WHERE jpk.perfume_idx = p.perfume_idx AND jpk.keyword_idx IN (:keywords) GROUP BY jpk.perfume_idx) = (:keywordCount) ';
const SQL_SEARCH_INGREDIENT_CONDITION: string =
'(SELECT COUNT(DISTINCT(i.category_idx)) FROM notes n INNER JOIN ingredients i ON i.ingredient_idx = n.ingredient_idx WHERE n.perfume_idx = p.perfume_idx AND n.ingredient_idx IN (:ingredients) GROUP BY n.perfume_idx) = (:categoryCount) ';

const SQL_SEARCH_PERFUME_SELECT_COUNT: string =
'SELECT ' +
'COUNT(p.perfume_idx) as count ' +
'FROM perfumes p ' +
'INNER JOIN brands b ON p.brand_idx = b.brand_idx ' +
'WHERE p.deleted_at IS NULL AND (:whereCondition) ';

const SQL_SEARCH_RANDOM_PERFUME_WITH_MIN_REVIEW_COUNT: string =
'SELECT ' +
'`Perfume`.perfume_idx AS perfumeIdx, `Perfume`.brand_idx AS brandIdx, `Perfume`.name, `Perfume`.english_name AS englishName, `Perfume`.image_url AS imageUrl, `Perfume`.created_at AS createdAt, `Perfume`.updated_at AS updatedAt, ' +
Expand Down Expand Up @@ -97,116 +66,6 @@ const defaultOption: { [key: string]: any } = {
};

class PerfumeDao {
/**
* 향수 검색
*
* @param {number[]} brandIdxList
* @param {number[]} ingredientIdxList
* @param {number[]} categoryIdxList
* @param {number[]} keywordIdxList
* @param {string} searchText
* @param {PagingDTO} pagingDTO
* @returns {Promise<Perfume[]>} perfumeList
*/
async search(
brandIdxList: number[],
ingredientIdxList: number[],
categoryIdxList: number[],
keywordIdxList: number[],
searchText: string,
pagingDTO: PagingDTO
): Promise<ListAndCountDTO<PerfumeSearchResultDTO>> {
logger.debug(
`${LOG_TAG} search(brandIdxList = ${brandIdxList}, ` +
`ingredientIdxList = ${ingredientIdxList}, ` +
`keywordList = ${keywordIdxList}, ` +
`searchText = ${searchText}, ` +
`pagingDTO = ${pagingDTO}`
);
let orderCondition = pagingDTO.sqlOrderQuery('p.perfume_idx ASC');

let whereCondition: string = '';
if (
ingredientIdxList.length +
keywordIdxList.length +
brandIdxList.length >
0
) {
const arr: number[][] = [
ingredientIdxList,
keywordIdxList,
brandIdxList,
];
const conditionSQL: string[] = [
SQL_SEARCH_INGREDIENT_CONDITION,
SQL_SEARCH_KEYWORD_CONDITION,
SQL_SEARCH_BRAND_CONDITION,
];
whereCondition = arr
.reduce((prev: string[], cur: number[], index: number) => {
if (cur.length > 0) {
prev.push(conditionSQL[index]);
}
return prev;
}, [])
.join(' AND ');
}
if (searchText && searchText.length > 0) {
if (whereCondition.length) {
whereCondition = `${whereCondition} AND `;
}
whereCondition = `${whereCondition} (MATCH(p.name, p.english_name) AGAINST('${searchText}*' IN BOOLEAN MODE)`;
if (brandIdxList.length == 0) {
whereCondition = `${whereCondition} OR (MATCH(b.name, b.english_name) AGAINST ('${searchText}*' IN BOOLEAN MODE))`;
}
whereCondition = `${whereCondition} )`;
orderCondition =
`case when MATCH(p.name, p.english_name) AGAINST('${searchText}') then 0 ` +
`when MATCH(p.name, p.english_name) AGAINST('${searchText}*' IN BOOLEAN MODE) then 1 ` +
`else 2 end, ${orderCondition}`;
}
const countSQL: string = SQL_SEARCH_PERFUME_SELECT_COUNT.replace(
':whereCondition',
whereCondition.length > 0 ? whereCondition : 'TRUE'
);
const selectSQL: string = SQL_SEARCH_PERFUME_SELECT.replace(
':whereCondition',
whereCondition.length > 0 ? whereCondition : 'TRUE'
).replace(':orderCondition', orderCondition);

if (ingredientIdxList.length == 0) ingredientIdxList.push(-1);
if (brandIdxList.length == 0) brandIdxList.push(-1);
if (keywordIdxList.length == 0) keywordIdxList.push(-1);
const [{ count }] = await sequelize.query<{ count: number }>(countSQL, {
replacements: {
keywords: keywordIdxList,
brands: brandIdxList,
ingredients: ingredientIdxList,
categoryCount: categoryIdxList.length,
keywordCount: keywordIdxList.length,
},
type: QueryTypes.SELECT,
raw: true,
});
const rows: PerfumeSearchResultDTO[] = (
await sequelize.query(selectSQL, {
replacements: {
keywords: keywordIdxList,
brands: brandIdxList,
ingredients: ingredientIdxList,
categoryCount: categoryIdxList.length,
keywordCount: keywordIdxList.length,
limit: pagingDTO.limit,
offset: pagingDTO.offset,
},
type: QueryTypes.SELECT,
raw: true,
nest: true,
})
).map(PerfumeSearchResultDTO.createByJson);
return new ListAndCountDTO(count, rows);
}

/**
* 향수 조회
*
Expand Down
12 changes: 6 additions & 6 deletions src/data/dto/PerfumeSearchDTO.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
class PerfumeSearchDTO {
readonly keywordIdxList: number[];
readonly brandIdxList: number[];
readonly ingredientCategoryList: number[];
readonly keywordIdxList?: number[];
readonly brandIdxList?: number[];
readonly ingredientCategoryList?: number[];
readonly searchText: string;
readonly userIdx: number;
constructor(
keywordIdxList: number[],
brandIdxList: number[],
ingredientCategoryList: number[],
keywordIdxList: number[] | undefined,
brandIdxList: number[] | undefined,
ingredientCategoryList: number[] | undefined,
searchText: string,
userIdx: number
) {
Expand Down
56 changes: 0 additions & 56 deletions src/data/dto/PerfumeSearchResultDTO.ts

This file was deleted.

1 change: 0 additions & 1 deletion src/data/dto/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ export * from './PerfumeDTO';
export * from './PerfumeIntegralDTO';
export * from './PerfumeSearchDTO';
export * from './PerfumeInquireHistoryDTO';
export * from './PerfumeSearchResultDTO';
export * from './PerfumeSummaryDTO';
export * from './PerfumeThumbDTO';
export * from './PerfumeThumbWithReviewDTO';
Expand Down
4 changes: 2 additions & 2 deletions src/models/tables/Perfume.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ export class Perfume extends Model {
onUpdate: 'CASCADE',
onDelete: 'CASCADE',
})
Notes: Note;
Notes: Note[];

@BelongsToMany(() => User, {
foreignKey: 'userIdx',
Expand Down Expand Up @@ -139,5 +139,5 @@ export class Perfume extends Model {
onUpdate: 'CASCADE',
onDelete: 'CASCADE',
})
perfumeKeywords: JoinPerfumeKeyword[];
JoinPerfumeKeywords: JoinPerfumeKeyword[];
}
Loading