- 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>
253 lines
6.7 KiB
Markdown
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으로 기존 방식 유지 |
|