PMI RAG (Retrieval-Augmented Generation) 시스템으로, BM25와 Vector Search를 결합한 하이브리드 검색을 제공합니다.
- 하이브리드 검색: BM25 with Kiwi 형태소 분석기 + Vector Similarity Search(cosin 유사도 비교)
- RRF (Reciprocal Rank Fusion): 두 검색 결과를 효과적으로 통합
- 한국어 최적화: Kiwi 형태소 분석기 기반 전처리
- LLM 기반 필터링: Claude Sonnet을 사용한 쿼리 전처리 및 결과 필터링
- 고성능: 인덱스 캐싱으로 빠른 검색 속도
사용자 쿼리
│
▼
┌──────────────────────────────────┐
│ Claude Sonnet 쿼리 전처리 │
│ (오타 수정, 불필요한 단어 제거) │
└──────────┬───────────────────────┘
│
┌──────┴──────┐
│ │
▼ ▼
┌─────────┐ ┌─────────────┐
│ BM25 │ │ Vector │
│ Search │ │ Search │
│(형태소) │ │ (KURE-v1) │
└────┬────┘ └──────┬──────┘
│ │
└──────┬───────┘
│
▼
┌───────────────┐
│ RRF 융합 │
│ (k=60) │
└───────┬───────┘
│
▼
┌───────────────┐
│ Claude Sonnet │
│ 필터링 & 랭킹 │
└───────┬───────┘
│
▼
최종 결과
이 시스템은 하이브리드 검색(BM25 + Vector Search) + LLM 기반 필터링을 결합한 고도화된 패널 검색 엔진입니다. 사용자가 자연어로 질문하면 AI가 최적의 패널을 찾아주는 RAG(Retrieval-Augmented Generation) 기반 시스템입니다.skax+1
- 사용자 입력 → LLM이 검색에 최적화된 쿼리로 정제
- 결과 개수 추출 (예: "20명" →
result_count=20) - 출생년도, 거주지역, 성별 키워드 추출
BM25 검색 (키워드 기반)
- 한국어 형태소 분석 → 토큰화 → BM25 점수 계산
- 캐시된 인덱스 파일(
bm25_index.pkl) 사용
Vector 검색 (의미 기반)
- 쿼리 임베딩 벡터화 → PostgreSQL pgvector로 코사인 유사도 검색
- 동시 실행으로 검색 속도 최적화
pythonrrf_score = 1/(k + bm25_rank) + 1/(k + vector_rank)
- 두 검색 결과를 통합하여 더 정확한 순위 생성
- k=60 (하이퍼파라미터)
- 쿼리에서 "OTT", "음용경험" 등 도메인 키워드 감지
- JSON 필드의 정확한 키 매칭으로 관련 패널만 추출
result_count ≤ 100: Claude Sonnet이 각 패널의 적합도 판단result_count > 100: RRF 결과 직접 반환 (속도 우선)
- 정규화: 특수문자/이모티콘 제거, ㅋㅋㅋ 제거
- 형태소 분석: Kiwi로 한국어 토큰화
- 품사 필터링: 명사(NN*), 동사(VV), 형용사(VA)만 추출
- 불용어 제거: "이", "가", "을", "를" 등 제거
- 단일 문자 제거: 의미 없는 한글자 단어 제거
pythoncore_keywords = ['OTT', 'OTT구독', '음용경험', '음주경험'] kiwi.add_user_word(keyword, 'NNG', 1.0)
- 도메인 특화 복합명사를 단일 토큰으로 인식
- 출생년도 필터: JSON 필드 매칭 (
info_text::jsonb->>'출생년도') - 거주지역 필터: 17개 광역시도 OR 조건 검색
- 성별 필터: "남자"/"여자" 정규화 후 적용
| 라이브러리 | 용도 | 버전 권장 |
|---|---|---|
| rank-bm25 | BM25 알고리즘 구현 | 최신 |
| pgvector | PostgreSQL 벡터 검색 | 0.2.0+ |
| psycopg2 | PostgreSQL 연결 | 2.9+ |
| 라이브러리 | 용도 | 특징 |
|---|---|---|
| kiwipiepy | 한국어 형태소 분석 | 빠르고 정확, C++ 기반 |
| numpy | 벡터 연산 | 임베딩 처리 |
| 라이브러리 | 용도 | 비고 |
|---|---|---|
| FastAPI | REST API 서버 | 비동기 지원 |
| Claude Sonnet API | LLM 쿼리 전처리/필터링 | Anthropic |
| pydantic | 입력 검증 | FastAPI 내장 |
pythonThreadPoolExecutor *# Python 표준 라이브러리*
- BM25 + Vector 검색 병렬 실행으로 검색 속도 2배 향상
| 라이브러리 | 장점 | 단점 |
|---|---|---|
| Kiwi ⭐ (채택) | 매우 빠름, 정확도 높음, 한국어에 강점, 사용하기 좋음 | - |
| Mecab-ko | 오픈소스, 커뮤니티 큼 | 설치 복잡 |
| KoNLPy (Okt) | 사용 간편 | 느림, 정확도 낮음 |
| 솔루션 | 장점 | 단점 |
|---|---|---|
| pgvector ⭐ (채택) | PostgreSQL 통합, 안정적, | |
| 현재 데이터 규모에서 적합함 | 대규모에서 느림 | |
| Pinecone | 관리형, 확장성 좋음 | 비용 높음 |
| FAISS | 매우 빠름 | 별도 인프라 필요 |
| Milvus | 오픈소스, 확장성 | 복잡한 설정 |
| 라이브러리 | 선택 이유 |
|---|---|
| rank-bm25 ⭐ (채택) | 간단, 캐싱 지원, |
| 현재 데이터 규모에서 적합함 | |
| Elasticsearch | 프로덕션 급, |
| 메모리의 데이터가 1KB ~ 1GB 이상. |
키워드 필터링을 통해 보완
성별, 거주지역, 출생년도
| 모델 | 장점 | 단점 |
|---|---|---|
| Claude Sonnet ⭐ (채택) | 추론 능력 우수 | API 비용 |
| GPT-4 | 범용성 높음 | 비용 높음 |
| 오픈소스 LLM (Llama) | 무료 | 정확도 낮음, 인프라 필요 |
A: 각각의 장단점을 보완하기 위해서입니다.
- BM25: 정확한 키워드 매칭에 강함 (예: "OTT 구독자" → "OTT" 단어 포함 문서)
- Vector: 의미적 유사도 파악 (예: "넷플릭스 이용자" ≈ "OTT 구독자")
- RRF 융합으로 두 방식의 장점을 결합하여 정확도 향상skax
A: RRF 상수 k는 순위의 영향력을 조절하는 하이퍼파라미터입니다.
- k가 작을수록: 상위 랭크의 영향력 증가 (극단적 선호)
- k가 클수록: 전체 순위를 고르게 고려 (안정적)
- k=60은 정보 검색 분야의 경험적 기본값으로, 실험을 통해 최적화 가능합니다
A: 비용과 속도의 트레이드오프 때문입니다.
result_count > 100: 대량 결과가 필요한 경우 → LLM 호출 비용/시간이 과도하게 증가result_count ≤ 100: 소량 정밀 검색 → LLM으로 각 패널의 적합도를 세밀하게 평가- 실제 사용 시 95%는 100명 이하 요청이므로 대부분 LLM 필터링 적용
A: 한국어의 교착어 특성 때문입니다.mainlineit+1
- "음용경험이" → [음용경험(명사), 이(조사)] 분리
- "OTT를" → [OTT(명사), 를(조사)] 분리
- 불용어(조사) 제거 후 핵심 키워드만 추출하여 검색 정확도 향상
- 사용자 사전으로 "OTT구독"을 단일 토큰으로 인식하여 복합명사 검색 강화
A: 코드의 로그 구조로 측정 가능합니다.
- 검색 속도: (36,000개 문서 기준)
- 101개 이상 검색시 약 8-10초 , 최종적으로 llm_filter를 거치지 않고, RRF순위로 반환
- 100개 이하 검색시 약 20-60초,
- 정확도: RRF 상위 30개 중 LLM이 최종 선택 → 높은 정밀도
- 확장성: BM25 인덱스 캐싱 + 배치 처리로 10만 개 이상 문서도 처리 가능
- 메모리: 배치 크기 조절로 최적화 (기본 5,000개/배치)
A: 데이터 크기에 따라 다릅니다. 100개
101개 이상이면 8~9초,
↓
[Claude Sonnet] 쿼리 정제 + 필터 추출 ↓ ┌─────────────────┴─────────────────┐ │ [BM25 검색] [Vector 검색] │ (병렬) │ - Kiwi 형태소 분석 - 쿼리 임베딩 │ │ - 캐시 인덱스 사용 - pgvector 검색 cosin └─────────────────┬─────────────────┘`
[RRF 융합] 두 검색 결과 통합 ↓ [LLM 필터링] (조건부 result_count <= 100) 최종 적합도 판단 ↓ JSON 결과 반환`
이 시스템은 키워드 매칭의 정확성 + 의미 검색의 유연성 + LLM의 추론 능력을 결합한 패널 검색 솔루션입니다.
| 방법 | 기술 | 장점 | 단점 |
|---|---|---|---|
| FTS (현재 사용) | Python BM25 + Kiwi | 한국어 최적화, 형태소 분석, 빠름 | 인덱스 재구축 필요 |
| Vector Search | KURE-v1 embeddings | 의미론적 유사도, 동의어 처리 | 짧은 쿼리에 약함 |
| RRF | 랭크 융합 | 두 방법의 장점 결합 | - |
preprocess_text() 함수가 수행하는 단계:
-
정규화: 특수문자, 이모티콘 제거
"운동 좋아하구!! ㅋㅋㅋ ♥" → "운동 좋아하구" -
형태소 분석: Kiwi 사용
"운동 좋아하는" → [('운동', 'NNG'), ('좋아하', 'VV'), ('는', 'ETM')] -
품사 필터링: 명사, 동사, 형용사만 추출
[('운동', 'NNG'), ('좋아하', 'VV')] → ['운동', '좋아하'] -
불용어 제거: 조사, 접속사 등 제거
['이', '운동', '을', '좋아하'] → ['운동', '좋아하'] -
단일 문자 제거: 의미 없는 한글자 제거
['운', '동', '좋아하'] → ['좋아하']
하이브리드 검색 수행
요청:
{
"query": "운동 좋아하는 20대 여성",
"count": 300
}응답:
{
"original_query": "운동 좋아하는 20대 여성",
"clean_query": "운동 좋아하는 20대 여성",
"result": [
{
"id": "uuid-1234",
"info_text": {...}
}
],
"metrics": {
"total_time": "2.34s",
"final_count": 10
}
}헬스 체크
응답:
{
"message": "RRF Search API with Sonnet running"
}데이터베이스에 새 문서가 추가되거나 변경된 경우:
# 기존 인덱스 백업 (선택)
mv bm25_index.pkl bm25_index.pkl.backup
# 새 인덱스 구축
python build_index.py
# 서버 재시작 (FastAPI reload 모드라면 자동)# crontab -e
# 매일 새벽 3시에 인덱스 재구축
0 3 * * * cd /path/to/RRF_Search && python build_index.py| 문서 수 | 인덱스 로드 | 검색 시간 | 총 응답 시간 |
|---|---|---|---|
| 1만 | ~200ms | ~20ms | ~2-3초 |
| 5만 | ~1s | ~50ms | ~2-3초 |
| 10만 | ~2s | ~100ms | ~3-4초 |
총 응답 시간에는 LLM 호출 시간(~1-2초) 포함
| 문서 수 | 인덱스 크기 | 메모리 사용 |
|---|---|---|
| 1만 | ~30 MB | ~200 MB |
| 5만 | ~150 MB | ~800 MB |
| 10만 | ~300 MB | ~1.5 GB |
python test_bm25.py다음을 테스트합니다:
- BM25, FTS, Vector 검색 비교
- RRF 융합 결과
- Sonnet 전처리 + 검색 파이프라인
python test_fts_korean.py다음을 확인합니다:
- FTS 토큰화 방식
- 다양한 검색 방법 비교
- pg_trgm 사용 가능 여부
✗ BM25 인덱스 캐시 파일(bm25_index.pkl)을 찾을 수 없습니다.
해결: python build_index.py 실행
ModuleNotFoundError: No module named 'kiwipiepy'
해결: pip install -r requirements.txt
✗ 데이터베이스 연결 실패: connection refused
확인:
.env파일에 올바른 비밀번호 설정- 데이터베이스 서버 실행 중인지 확인
- 네트워크 연결 확인
해결:
- 서버 메모리 증설
- 배치 크기 조정 (build_index.py)
- Redis 캐시 사용 (추후 개선)
- FastAPI: REST API 프레임워크
- PostgreSQL: 메인 데이터베이스
- pgvector: 벡터 유사도 검색
- Kiwi (kiwipiepy): 한국어 형태소 분석
- rank-bm25: BM25 알고리즘 구현
- KURE-v1: 한국어 문장 임베딩 (1024차원)
- Claude Sonnet 4.5: 쿼리 전처리 및 결과 필터링
- AWS Bedrock: LLM 호스팅 (ap-southeast-2)
- AWS RDS PostgreSQL: 데이터 저장소 (ap-southeast-2)
- Redis 기반 인덱스 캐싱 (멀티프로세스 지원)
- 증분 인덱스 업데이트 (전체 재구축 불필요)
- 멀티프로세싱 기반 인덱스 구축 (속도 향상)
- 검색 결과 캐싱 (동일 쿼리 재사용)
- 로깅 및 모니터링 추가
- A/B 테스트를 위한 검색 방법 선택 옵션
- Kiwi: https://github.com/bab2min/kiwipiepy
- rank-bm25: https://github.com/dorianbrown/rank_bm25
- RRF 논문: Cormack et al. (2009) - Reciprocal Rank Fusion
- BM25 알고리즘: https://en.wikipedia.org/wiki/Okapi_BM25
- FastAPI: https://fastapi.tiangolo.com
이 프로젝트는 PMI 프로젝트의 일부입니다.
문제가 발생하면 다음 문서를 참고하세요: