fix(suggestions): 동적 임계값으로 추천 검색어 필터링

- 최대 검색 횟수의 1% 또는 최소 10회 중 더 큰 값 적용
- GREATEST(MAX(count) * 0.01, 10) 사용
- 데이터가 적을 때도 오타 필터링 가능

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
caadiq 2026-01-13 22:41:15 +09:00
parent 88f15a3ec1
commit 02fe9314e4

View file

@ -9,6 +9,12 @@ const inko = new Inko();
const SUGGESTION_PREFIX = "suggestions:"; const SUGGESTION_PREFIX = "suggestions:";
const CACHE_TTL = 86400; // 24시간 const CACHE_TTL = 86400; // 24시간
// 추천 검색어로 노출되기 위한 최소 비율 (최대 검색 횟수 대비)
// 예: 0.01 = 최대 검색 횟수의 1% 이상만 노출
const MIN_COUNT_RATIO = 0.01;
// 최소 임계값 (데이터가 적을 때 오타 방지)
const MIN_COUNT_FLOOR = 10;
/** /**
* 영문만 포함된 검색어인지 확인 * 영문만 포함된 검색어인지 확인
*/ */
@ -166,38 +172,21 @@ export async function getSuggestions(query, limit = 10) {
/** /**
* 다음 단어 예측 (Bi-gram 기반) * 다음 단어 예측 (Bi-gram 기반)
* 동적 임계값을 사용하므로 Redis 캐시를 사용하지 않음
*/ */
async function getNextWordSuggestions(lastWord, prefix, limit) { async function getNextWordSuggestions(lastWord, prefix, limit) {
try { try {
// 1. Redis 캐시 확인
const cacheKey = `${SUGGESTION_PREFIX}${lastWord}`;
let nextWords = await redis.zrevrange(cacheKey, 0, limit - 1);
// 2. 캐시 미스 시 DB 조회 후 Redis 채우기
if (nextWords.length === 0) {
const [rows] = await pool.query( const [rows] = await pool.query(
`SELECT word2, count FROM word_pairs `SELECT word2, count FROM word_pairs
WHERE word1 = ? WHERE word1 = ?
AND count >= GREATEST((SELECT MAX(count) * ? FROM word_pairs), ?)
ORDER BY count DESC ORDER BY count DESC
LIMIT ?`, LIMIT ?`,
[lastWord, limit * 2] // 여유있게 가져오기 [lastWord, MIN_COUNT_RATIO, MIN_COUNT_FLOOR, limit]
); );
if (rows.length > 0) { // prefix + 다음 단어 조합으로 반환
// Redis에 캐싱 return rows.map((r) => `${prefix} ${r.word2}`);
const multi = redis.multi();
for (const row of rows) {
multi.zadd(cacheKey, row.count, row.word2);
}
multi.expire(cacheKey, CACHE_TTL);
await multi.exec();
nextWords = rows.map((r) => r.word2);
}
}
// 3. prefix + 다음 단어 조합으로 반환
return nextWords.slice(0, limit).map((word) => `${prefix} ${word}`);
} catch (error) { } catch (error) {
console.error("[SearchSuggestion] Bi-gram 조회 오류:", error.message); console.error("[SearchSuggestion] Bi-gram 조회 오류:", error.message);
return []; return [];
@ -218,19 +207,21 @@ async function getPrefixSuggestions(prefix, koreanPrefix, limit) {
// 영어 원본과 한글 변환 둘 다 검색 // 영어 원본과 한글 변환 둘 다 검색
[rows] = await pool.query( [rows] = await pool.query(
`SELECT query FROM search_queries `SELECT query FROM search_queries
WHERE query LIKE ? OR query LIKE ? WHERE (query LIKE ? OR query LIKE ?)
AND count >= GREATEST((SELECT MAX(count) * ? FROM search_queries), ?)
ORDER BY count DESC, last_searched_at DESC ORDER BY count DESC, last_searched_at DESC
LIMIT ?`, LIMIT ?`,
[`${prefix}%`, `${koreanPrefix}%`, limit] [`${prefix}%`, `${koreanPrefix}%`, MIN_COUNT_RATIO, MIN_COUNT_FLOOR, limit]
); );
} else { } else {
// 단일 검색 // 단일 검색
[rows] = await pool.query( [rows] = await pool.query(
`SELECT query FROM search_queries `SELECT query FROM search_queries
WHERE query LIKE ? WHERE query LIKE ?
AND count >= GREATEST((SELECT MAX(count) * ? FROM search_queries), ?)
ORDER BY count DESC, last_searched_at DESC ORDER BY count DESC, last_searched_at DESC
LIMIT ?`, LIMIT ?`,
[`${prefix}%`, limit] [`${prefix}%`, MIN_COUNT_RATIO, MIN_COUNT_FLOOR, limit]
); );
} }