From 72db9dcdc1a591972f2fe46ef5dcbf563b0506da Mon Sep 17 00:00:00 2001 From: caadiq Date: Mon, 19 Jan 2026 09:44:02 +0900 Subject: [PATCH] =?UTF-8?q?fix:=20=EC=B6=94=EC=B2=9C=20=EA=B2=80=EC=83=89?= =?UTF-8?q?=EC=96=B4=20=EB=8F=99=EC=A0=81=20=EC=9E=84=EA=B3=84=EA=B0=92=20?= =?UTF-8?q?=ED=95=84=ED=84=B0=EB=A7=81=20=EB=B3=B5=EC=9B=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - SQL GREATEST()로 동적 임계값 적용 - MAX(count) * 1% 또는 최소 10회 중 더 큰 값 사용 - Prefix, 초성, 인기 검색어 모두 필터링 적용 - 데이터가 적을 때도 오타 필터링 가능 Co-Authored-By: Claude Opus 4.5 --- backend/src/services/suggestions/index.js | 32 ++++++++++++++--------- 1 file changed, 19 insertions(+), 13 deletions(-) diff --git a/backend/src/services/suggestions/index.js b/backend/src/services/suggestions/index.js index e909207..fcea5d2 100644 --- a/backend/src/services/suggestions/index.js +++ b/backend/src/services/suggestions/index.js @@ -15,8 +15,8 @@ const inko = new Inko(); const CONFIG = { // 추천 검색어 최소 검색 횟수 비율 (최대 대비) MIN_COUNT_RATIO: 0.01, - // 최소 임계값 (데이터 적을 때) - MIN_COUNT_FLOOR: 5, + // 최소 임계값 (데이터 적을 때 오타 방지) + MIN_COUNT_FLOOR: 10, // Redis 키 prefix REDIS_PREFIX: 'suggest:', // 캐시 TTL (초) @@ -24,7 +24,6 @@ const CONFIG = { PREFIX: 3600, // prefix 검색: 1시간 BIGRAM: 86400, // bi-gram: 24시간 POPULAR: 600, // 인기 검색어: 10분 - MAX_COUNT: 3600, // 최대 횟수: 1시간 }, }; @@ -208,6 +207,7 @@ export class SuggestionService { /** * Prefix 매칭 + * - GREATEST()로 동적 임계값 적용: MAX(count) * 1% 또는 최소 10 중 더 큰 값 */ async getPrefixSuggestions(prefix, koreanPrefix, limit) { try { @@ -216,19 +216,21 @@ export class SuggestionService { if (koreanPrefix) { // 영어 + 한글 변환 둘 다 검색 [rows] = await this.db.query( - `SELECT query, count FROM suggestion_queries - WHERE query LIKE ? OR query LIKE ? + `SELECT query FROM suggestion_queries + WHERE (query LIKE ? OR query LIKE ?) + AND count >= GREATEST((SELECT MAX(count) * ? FROM suggestion_queries), ?) ORDER BY count DESC, last_searched_at DESC LIMIT ?`, - [`${prefix}%`, `${koreanPrefix}%`, limit] + [`${prefix}%`, `${koreanPrefix}%`, CONFIG.MIN_COUNT_RATIO, CONFIG.MIN_COUNT_FLOOR, limit] ); } else { [rows] = await this.db.query( - `SELECT query, count FROM suggestion_queries + `SELECT query FROM suggestion_queries WHERE query LIKE ? + AND count >= GREATEST((SELECT MAX(count) * ? FROM suggestion_queries), ?) ORDER BY count DESC, last_searched_at DESC LIMIT ?`, - [`${prefix}%`, limit] + [`${prefix}%`, CONFIG.MIN_COUNT_RATIO, CONFIG.MIN_COUNT_FLOOR, limit] ); } @@ -241,15 +243,17 @@ export class SuggestionService { /** * 초성 검색 + * - GREATEST()로 동적 임계값 적용 */ async getChosungSuggestions(chosung, limit) { try { const [rows] = await this.db.query( - `SELECT word, count FROM suggestion_chosung + `SELECT word FROM suggestion_chosung WHERE chosung LIKE ? + AND count >= GREATEST((SELECT MAX(count) * ? FROM suggestion_chosung), ?) ORDER BY count DESC LIMIT ?`, - [`${chosung}%`, limit] + [`${chosung}%`, CONFIG.MIN_COUNT_RATIO, CONFIG.MIN_COUNT_FLOOR, limit] ); return rows.map(r => r.word); @@ -261,6 +265,7 @@ export class SuggestionService { /** * 인기 검색어 조회 + * - GREATEST()로 동적 임계값 적용 */ async getPopularQueries(limit = 10) { try { @@ -272,12 +277,13 @@ export class SuggestionService { return JSON.parse(cached); } - // DB 조회 + // DB 조회 (동적 임계값 이상만) const [rows] = await this.db.query( - `SELECT query, count FROM suggestion_queries + `SELECT query FROM suggestion_queries + WHERE count >= GREATEST((SELECT MAX(count) * ? FROM suggestion_queries), ?) ORDER BY count DESC LIMIT ?`, - [limit] + [CONFIG.MIN_COUNT_RATIO, CONFIG.MIN_COUNT_FLOOR, limit] ); const result = rows.map(r => r.query);