fix: 추천 검색어 동적 임계값 필터링 복원

- SQL GREATEST()로 동적 임계값 적용
- MAX(count) * 1% 또는 최소 10회 중 더 큰 값 사용
- Prefix, 초성, 인기 검색어 모두 필터링 적용
- 데이터가 적을 때도 오타 필터링 가능

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
caadiq 2026-01-19 09:44:02 +09:00
parent 108265b1fd
commit 72db9dcdc1

View file

@ -15,8 +15,8 @@ const inko = new Inko();
const CONFIG = { const CONFIG = {
// 추천 검색어 최소 검색 횟수 비율 (최대 대비) // 추천 검색어 최소 검색 횟수 비율 (최대 대비)
MIN_COUNT_RATIO: 0.01, MIN_COUNT_RATIO: 0.01,
// 최소 임계값 (데이터 적을 때) // 최소 임계값 (데이터 적을 때 오타 방지)
MIN_COUNT_FLOOR: 5, MIN_COUNT_FLOOR: 10,
// Redis 키 prefix // Redis 키 prefix
REDIS_PREFIX: 'suggest:', REDIS_PREFIX: 'suggest:',
// 캐시 TTL (초) // 캐시 TTL (초)
@ -24,7 +24,6 @@ const CONFIG = {
PREFIX: 3600, // prefix 검색: 1시간 PREFIX: 3600, // prefix 검색: 1시간
BIGRAM: 86400, // bi-gram: 24시간 BIGRAM: 86400, // bi-gram: 24시간
POPULAR: 600, // 인기 검색어: 10분 POPULAR: 600, // 인기 검색어: 10분
MAX_COUNT: 3600, // 최대 횟수: 1시간
}, },
}; };
@ -208,6 +207,7 @@ export class SuggestionService {
/** /**
* Prefix 매칭 * Prefix 매칭
* - GREATEST() 동적 임계값 적용: MAX(count) * 1% 또는 최소 10
*/ */
async getPrefixSuggestions(prefix, koreanPrefix, limit) { async getPrefixSuggestions(prefix, koreanPrefix, limit) {
try { try {
@ -216,19 +216,21 @@ export class SuggestionService {
if (koreanPrefix) { if (koreanPrefix) {
// 영어 + 한글 변환 둘 다 검색 // 영어 + 한글 변환 둘 다 검색
[rows] = await this.db.query( [rows] = await this.db.query(
`SELECT query, count FROM suggestion_queries `SELECT query FROM suggestion_queries
WHERE query LIKE ? OR query LIKE ? WHERE (query LIKE ? OR query LIKE ?)
AND count >= GREATEST((SELECT MAX(count) * ? FROM suggestion_queries), ?)
ORDER BY count DESC, last_searched_at DESC ORDER BY count DESC, last_searched_at DESC
LIMIT ?`, LIMIT ?`,
[`${prefix}%`, `${koreanPrefix}%`, limit] [`${prefix}%`, `${koreanPrefix}%`, CONFIG.MIN_COUNT_RATIO, CONFIG.MIN_COUNT_FLOOR, limit]
); );
} else { } else {
[rows] = await this.db.query( [rows] = await this.db.query(
`SELECT query, count FROM suggestion_queries `SELECT query FROM suggestion_queries
WHERE query LIKE ? WHERE query LIKE ?
AND count >= GREATEST((SELECT MAX(count) * ? FROM suggestion_queries), ?)
ORDER BY count DESC, last_searched_at DESC ORDER BY count DESC, last_searched_at DESC
LIMIT ?`, 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) { async getChosungSuggestions(chosung, limit) {
try { try {
const [rows] = await this.db.query( const [rows] = await this.db.query(
`SELECT word, count FROM suggestion_chosung `SELECT word FROM suggestion_chosung
WHERE chosung LIKE ? WHERE chosung LIKE ?
AND count >= GREATEST((SELECT MAX(count) * ? FROM suggestion_chosung), ?)
ORDER BY count DESC ORDER BY count DESC
LIMIT ?`, LIMIT ?`,
[`${chosung}%`, limit] [`${chosung}%`, CONFIG.MIN_COUNT_RATIO, CONFIG.MIN_COUNT_FLOOR, limit]
); );
return rows.map(r => r.word); return rows.map(r => r.word);
@ -261,6 +265,7 @@ export class SuggestionService {
/** /**
* 인기 검색어 조회 * 인기 검색어 조회
* - GREATEST() 동적 임계값 적용
*/ */
async getPopularQueries(limit = 10) { async getPopularQueries(limit = 10) {
try { try {
@ -272,12 +277,13 @@ export class SuggestionService {
return JSON.parse(cached); return JSON.parse(cached);
} }
// DB 조회 // DB 조회 (동적 임계값 이상만)
const [rows] = await this.db.query( 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 ORDER BY count DESC
LIMIT ?`, LIMIT ?`,
[limit] [CONFIG.MIN_COUNT_RATIO, CONFIG.MIN_COUNT_FLOOR, limit]
); );
const result = rows.map(r => r.query); const result = rows.map(r => r.query);