fromis_9/backend/docs/suggestion-improvement-plan.md
caadiq c201de203e feat: 추천 검색어 시스템 구현 (kiwi-nlp 형태소 분석)
- kiwi-nlp 기반 한국어 형태소 분석기 추가
- 추천 검색어 API 구현 (/api/schedules/suggestions)
  - Prefix 매칭, Bi-gram 다음 단어 예측
  - 초성 검색 지원, 영문→한글 자동 변환 (Inko)
- 사용자 사전 추가 (멤버/그룹명, 프로그램명 등)
- DB 테이블: suggestion_queries, suggestion_word_pairs, suggestion_chosung

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-18 13:01:29 +09:00

253 lines
6.7 KiB
Markdown

# 추천 검색어 시스템 개선 계획서
## 1. 현재 시스템 분석
### 1.1 기존 동작 방식
```
검색어 입력 → 띄어쓰기 분리 → Unigram/Bi-gram 저장 → prefix 매칭으로 추천
```
### 1.2 기존 코드의 문제점
| 문제 | 설명 | 예시 |
|------|------|------|
| **띄어쓰기 기준 분리** | 조사/어미가 포함된 형태로 저장됨 | "콘서트가", "열립니다" |
| **무의미한 검색어 저장** | 검색 결과 없어도 저장됨 | 오타, 의미없는 문자열 |
| **Redis 캐시 미활용** | prefix 검색은 매번 DB 조회 | 성능 저하 |
| **서브쿼리 반복** | `MAX(count)` 매 쿼리마다 실행 | 성능 저하 |
| **초성 검색 미지원** | "ㅍㅁㅅ"로 검색 불가 | 사용성 저하 |
### 1.3 기존 테이블 구조
```sql
-- Unigram (전체 검색어)
CREATE TABLE search_queries (
query VARCHAR(255) PRIMARY KEY,
count INT DEFAULT 1,
last_searched_at TIMESTAMP
);
-- Bi-gram (단어 쌍)
CREATE TABLE word_pairs (
word1 VARCHAR(100),
word2 VARCHAR(100),
count INT DEFAULT 1,
PRIMARY KEY (word1, word2)
);
```
---
## 2. 개선 목표
1. **형태소 분석기 도입**: 의미있는 단어(명사, 고유명사)만 추출
2. **검색 결과 기반 필터링**: 실제 검색 결과가 있는 검색어만 저장
3. **캐시 성능 개선**: Redis 활용 강화
4. **초성 검색 지원**: "ㅍㅁㅅ" → "프로미스나인"
5. **코드 구조 개선**: Fastify 플러그인 구조에 맞게 재작성
---
## 3. 개선 설계
### 3.1 형태소 분석기 도입 (koalanlp)
**설치 요구사항:**
- Java 8 이상 (Docker에 추가 필요)
- `npm install koalanlp`
**사용할 분석기:**
- **KMR (코모란)**: 속도 빠름, 정확도 양호
**추출 대상 품사:**
| 태그 | 설명 | 예시 |
|------|------|------|
| NNP | 고유명사 | 프로미스나인, 지원 |
| NNG | 일반명사 | 콘서트, 생일 |
| NNB | 의존명사 | (필요시) |
| SL | 외국어 | fromis_9 |
**예시:**
```
입력: "프로미스나인 콘서트가 열립니다"
기존: ["프로미스나인", "콘서트가", "열립니다"]
개선: ["프로미스나인", "콘서트"]
```
### 3.2 검색어 저장 로직 개선
```
검색 실행
Meilisearch 검색 결과 확인
↓ (결과 있음)
형태소 분석으로 명사 추출
Unigram 저장 (전체 검색어)
Bi-gram 저장 (명사 쌍)
Redis 캐시 업데이트
```
### 3.3 테이블 구조 개선
```sql
-- 검색어 테이블 (변경 없음)
CREATE TABLE search_queries (
id INT AUTO_INCREMENT PRIMARY KEY,
query VARCHAR(255) UNIQUE NOT NULL,
count INT DEFAULT 1,
has_results BOOLEAN DEFAULT TRUE, -- 추가: 검색 결과 유무
last_searched_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
INDEX idx_prefix (query(50)),
INDEX idx_count (count DESC)
);
-- 단어 쌍 테이블 (변경 없음)
CREATE TABLE word_pairs (
word1 VARCHAR(100) NOT NULL,
word2 VARCHAR(100) NOT NULL,
count INT DEFAULT 1,
PRIMARY KEY (word1, word2),
INDEX idx_word1 (word1, count DESC)
);
-- 추가: 초성 인덱스 테이블
CREATE TABLE chosung_index (
chosung VARCHAR(50) NOT NULL, -- 초성 (예: "ㅍㅁㅅㄴㅇ")
word VARCHAR(100) NOT NULL, -- 원본 단어 (예: "프로미스나인")
count INT DEFAULT 1,
PRIMARY KEY (chosung, word),
INDEX idx_chosung (chosung)
);
```
### 3.4 Redis 캐시 전략
| 키 패턴 | 용도 | TTL |
|---------|------|-----|
| `suggest:prefix:{query}` | prefix 검색 결과 캐시 | 1시간 |
| `suggest:bigram:{word}` | Bi-gram 다음 단어 | 24시간 |
| `suggest:popular` | 인기 검색어 Top 100 | 10분 |
| `suggest:max_count` | 최대 검색 횟수 (임계값용) | 1시간 |
### 3.5 초성 검색 구현
```javascript
// 한글 → 초성 변환
function getChosung(text) {
const CHOSUNG = ['ㄱ','ㄲ','ㄴ','ㄷ','ㄸ','ㄹ','ㅁ','ㅂ','ㅃ','ㅅ','ㅆ','ㅇ','ㅈ','ㅉ','ㅊ','ㅋ','ㅌ','ㅍ','ㅎ'];
let result = '';
for (const char of text) {
const code = char.charCodeAt(0) - 0xAC00;
if (code >= 0 && code <= 11171) {
result += CHOSUNG[Math.floor(code / 588)];
}
}
return result;
}
// "프로미스나인" → "ㅍㄹㅁㅅㄴㅇ"
```
### 3.6 API 엔드포인트
```
GET /api/schedules/suggestions
?q=검색어
&limit=10
Response:
{
"suggestions": ["프로미스나인", "프로미스나인 콘서트", ...]
}
```
---
## 4. 파일 구조
```
backend/src/
├── services/
│ └── suggestions/
│ ├── index.js # 메인 서비스 (저장/조회)
│ ├── morpheme.js # 형태소 분석 (koalanlp)
│ ├── chosung.js # 초성 변환/검색
│ └── cache.js # Redis 캐시 관리
├── routes/
│ └── schedules/
│ └── suggestions.js # API 라우트
└── plugins/
└── koalanlp.js # koalanlp 초기화 플러그인
```
---
## 5. 구현 순서
### Phase 1: 기본 마이그레이션
1. [ ] Docker에 Java 설치 추가
2. [ ] koalanlp 패키지 설치 및 초기화
3. [ ] 기존 suggestions.js를 Fastify 구조로 마이그레이션
4. [ ] 기본 API 동작 확인
### Phase 2: 형태소 분석 적용
5. [ ] morpheme.js 구현 (명사 추출)
6. [ ] saveSearchQuery에 형태소 분석 적용
7. [ ] 검색 결과 있을 때만 저장하도록 수정
### Phase 3: 캐시 및 성능 개선
8. [ ] Redis 캐시 로직 강화
9. [ ] MAX(count) 캐싱
10. [ ] 인기 검색어 캐시
### Phase 4: 초성 검색
11. [ ] chosung.js 구현
12. [ ] chosung_index 테이블 생성
13. [ ] 초성 검색 API 통합
### Phase 5: 테스트 및 정리
14. [ ] 기존 데이터 마이그레이션 (형태소 재분석)
15. [ ] 성능 테스트
16. [ ] 문서화
---
## 6. Docker 변경사항
```dockerfile
# Dockerfile에 Java 추가
FROM node:20-alpine
# Java 설치 (koalanlp 필요)
RUN apk add --no-cache openjdk11-jre
# JAVA_HOME 설정
ENV JAVA_HOME=/usr/lib/jvm/java-11-openjdk
ENV PATH="$JAVA_HOME/bin:$PATH"
```
---
## 7. 예상 효과
| 항목 | 기존 | 개선 후 |
|------|------|---------|
| 단어 추출 정확도 | 낮음 (띄어쓰기 기준) | 높음 (형태소 기준) |
| 불필요한 데이터 | 많음 | 적음 (결과 있는 것만) |
| 검색 응답 속도 | 보통 | 빠름 (캐시 활용) |
| 초성 검색 | 불가 | 가능 |
| 사용자 경험 | 보통 | 향상 |
---
## 8. 리스크 및 대응
| 리스크 | 영향 | 대응 방안 |
|--------|------|-----------|
| Java 설치로 이미지 크기 증가 | ~100MB | Alpine + JRE만 설치 |
| koalanlp 초기 로딩 시간 | 첫 요청 지연 | 서버 시작 시 미리 로드 |
| 형태소 분석 오류 | 단어 추출 실패 | fallback으로 기존 방식 유지 |