Searchdoc | ess-ltr-legacy
BM25 (단어 분포 기반) → Embedding (벡터 유사도) → Hybrid (수동 조합) → LTR (자동 학습)
Feature 활용의 복잡도: 단일 Feature → 다중 Feature, 수동 설계 → 자동 학습
단어의 출현 빈도(TF)와 희소성(IDF)을 수치화
Feature: 단어 분포 통계
score(D,Q) = Σ IDF(qᵢ) × TF(qᵢ, D) × (k₁ + 1) / (TF + k₁ × (1 - b + b × |D|/avgdl))
텍스트를 벡터로 변환하고 Cosine Similarity로 비교
Feature: 벡터 간 코사인 유사도
sim(q, d) = cos(θ) = (q · d) / (||q|| × ||d||)
사람이 직접 두 Feature의 비율과 조합 방식을 설계
Linear Combination (수동 가중치):
score(d) = α × BM25(d) + (1-α) × KNN(d)
N개 Feature의 최적 조합을 ML 모델이 데이터로부터 학습
ML Model이 Feature 가중치 학습:
score(q, d) = f(x₁, x₂, ..., xₙ)
| 접근법 | 사용 Feature | 조합 방식 | 핵심 아이디어 |
|---|---|---|---|
| BM25 | 단어 분포 (TF-IDF) | 수식 고정 | 단어 빈도 + 희소성 |
| Embedding | 벡터 유사도 | Cosine Sim | 의미적 유사성 |
| Hybrid | BM25 + KNN | 사람이 설계 | 수동 가중치 조합 |
| LTR | N개 Feature | ML이 학습 | 데이터 기반 자동 최적화 |
Feature 복잡도 증가 + 조합 방식 자동화 = LTR
Pointwise → Pairwise → Listwise
"순위"를 어떻게 학습할 것인가? - 학습 단위의 차이
각 문서의 절대적 관련도 점수를 예측
학습 방식: 회귀(Regression) 또는 분류(Classification)
Query "파이썬 입문" → Doc A: 4점, Doc B: 2점, Doc C: 1점
두 문서 중 어느 것이 더 관련있는지 학습
학습 방식: 쌍(Pair) 비교 → 이진 분류
Doc A vs Doc B → A가 더 관련 (1) or B가 더 관련 (0)
쿼리에 대한 전체 문서 리스트의 순서를 최적화
학습 방식: 전체 리스트의 랭킹 품질(NDCG 등) 직접 최적화
[A, B, C, D] 순서 자체를 평가 → NDCG@10 = 0.85
Pairwise + Listwise의 장점을 결합한 알고리즘
핵심 아이디어:
λ = Pairwise Loss의 기울기 × NDCG 변화량
| 방법 | 학습 단위 | Loss 함수 | 대표 알고리즘 |
|---|---|---|---|
| Pointwise | 문서 1개 | MSE, Cross-entropy | Linear Reg, Random Forest |
| Pairwise | 문서 쌍 | Pairwise Loss | RankNet, RankSVM |
| Listwise | 전체 리스트 | NDCG-like | LambdaMART, ListNet |
실무에서는 LambdaMART 또는 XGBoost (Pairwise/Listwise) 가장 많이 사용
Learning to Rank는 머신러닝을 사용하여
검색 결과의 순위(Ranking)를 최적화하는 기법
TF-IDF, BM25 등
정적인 점수 계산
여러 피처를 ML 모델이
학습하여 동적 랭킹
| 단계 | Task | 설명 |
|---|---|---|
| 1 | create-featureset |
OpenSearch에 Feature 정의 등록 |
| 2 | generate-dataset |
LLM으로 Judgement + Feature 추출 |
| 3 | train-model |
XGBoost 모델 학습 & 업로드 |
# 1. Featureset 생성
python main.py --task create-featureset --env .env.local
# 2. Dataset 생성 (Judgement + Features)
python main.py --task generate-dataset --env .env.local
# 3. 모델 학습
python main.py --task train-model --env .env.local
환경변수: OpenSearch 연결정보, Bedrock 설정, 경로 등
OpenSearch LTR Plugin에 Feature 정의 등록
Featureset = 검색 시 추출할 Feature들의 템플릿 집합
txt_en_1: chunk_texttxt_en_3: absolute_titletxt_en_4: qr (질의응답)txt_en_5: keywordstxt_en_6: chunk_summarytxt_en_7: node_summaryemb_en_1: chunk_embeddingemb_en_2: qr_1_embeddingemb_en_5: chunk_summary_embedding{
"featureset": {
"name": "my_featureset",
"features": [
{
"name": "txt_en_1",
"params": ["query_text"],
"template": {
"match": { "chunk_text": "{{query_text}}" }
}
},
{
"name": "emb_en_1",
"params": ["query_embedding_str"],
"template": {
"knn": { "chunk_embedding": { "vector": ..., "k": 16 }}
}
}
]
}
}
학습 데이터 생성 - 3개 서브태스크
Claude Sonnet이 Query-Document 관련도 평가
Query: "AWS Lambda 콜드스타트 최적화"
Document: "Lambda 함수의 콜드스타트를 줄이는 방법..."
→ Claude 평가: 4 (매우 관련있음)
Judgement Scale:
0: 관련없음
1: 약간 관련
2: 관련있음
3: 많이 관련
4: 매우 관련 (정답)
문서에서 검색 가능한 메타데이터 추출
OpenSearch SLTR로 Feature 값 추출
# LTR 포맷 (LibSVM style)
# Label qid:N feat1:val1 feat2:val2 ...
4 qid:1 1:0.85 2:0.72 3:0.91 4:0.45 5:0.88 ...
0 qid:1 1:0.12 2:0.08 3:0.15 4:0.03 5:0.11 ...
3 qid:2 1:0.78 2:0.65 3:0.82 4:0.55 5:0.71 ...
Query마다 여러 Document의 Feature값 + Judgement Label
XGBoost로 랭킹 모델 학습
Feature Dataset → XGBoost Classifier → OpenSearch 모델 업로드
모델이 학습 전에 사람이 정해주는 설정값
| Parameter (학습됨) | Hyperparameter (설정) |
|---|---|
| 트리의 분기 기준값 | max_depth (트리 깊이) |
| 각 Feature의 가중치 | learning_rate (학습 속도) |
| 분류 경계선 | n_estimators (트리 개수) |
📝 비유: 요리 레시피(Parameter) vs 오븐 온도(Hyperparameter)
레시피는 재료로 만들어지고, 오븐 온도는 요리 전에 설정
베이지안 최적화 기반 효율적 탐색
Grid Search vs Optuna:
🎯 100번 시도로 1000번 Grid Search보다 나은 결과
# 1. 데이터 로드 & 파싱
train_X, train_y, train_qid = generate_dataset(train_data)
# train_X: 10개 Feature 컬럼
# train_y: Judgement Label (0-4)
# train_qid: Query ID (그룹핑용)
# 2. HPO with Optuna (100 trials)
study = optuna.create_study(direction='maximize')
study.optimize(hpo_objective, n_trials=100)
# 3. Final Model Training
model = XGBClassifier(**best_params)
model.fit(X_train, y_train, eval_set=[(X_val, y_val)])
Optuna가 매 Trial마다 호출하는 함수
def hpo_objective(trial):
# 1. Optuna가 제안하는 Hyperparameter
params = {
'max_depth': trial.suggest_int('max_depth', 1, 5),
'learning_rate': trial.suggest_float('learning_rate', 1e-4, 1e-2, log=True),
'n_estimators': trial.suggest_int('n_estimators', 150, 1500),
'subsample': trial.suggest_float('subsample', 0.4, 1.0),
'early_stopping_rounds': trial.suggest_int('early_stopping_rounds', 15, 40),
}
# 2. 해당 Hyperparameter로 모델 학습
model = XGBClassifier(**params)
model.fit(train_X, train_y, eval_set=[(val_X, val_y)])
# 3. 평가 지표 반환 → Optuna가 최대화
return recall_score(val_y, model.predict(val_X))
| Parameter | Range | 설명 |
|---|---|---|
max_depth |
1 ~ 5 | 트리 깊이 |
n_estimators |
150 ~ 1500 | 트리 개수 |
learning_rate |
1e-4 ~ 1e-2 | 학습률 (log scale) |
subsample |
0.4 ~ 1.0 | 샘플링 비율 |
early_stopping |
15 ~ 40 | 조기 종료 라운드 |
* Recall 최적화: 관련 문서를 놓치지 않는 것이 핵심
# XGBoost → JSON dump
reg_booster = model.get_booster()
dump_tree_list = reg_booster.get_dump(
dump_format='json',
with_stats=False
)
# OpenSearch LTR Plugin에 등록
model_payload = {
"model": {
"name": "my-model-xgbr-tree-107-depth-2",
"model": {
"type": "model/xgboost+json",
"definition": json.dumps(json_tree)
}
}
}
# POST /_ltr/_featureset/{name}/_createmodel
| Feature | Type | Field | 설명 |
|---|---|---|---|
| txt_en_1 | Text | chunk_text | 본문 텍스트 매칭 |
| txt_en_3 | Text | absolute_title | 문서 제목 매칭 |
| txt_en_4 | Text | qr | 예상 Q&A 매칭 |
| txt_en_5 | Text | keywords | 키워드 매칭 |
| txt_en_6 | Text | chunk_summary | 청크 요약 매칭 |
| txt_en_7 | Text | node_summary | 노드 요약 매칭 |
| emb_en_1 | KNN | chunk_embedding | 청크 벡터 유사도 |
| emb_en_2 | KNN | qr_1_embedding | Q&A 벡터 유사도 |
| emb_en_5 | KNN | chunk_summary_embedding | 요약 벡터 유사도 |
ess-ltr-legacy/
├── main.py # CLI 진입점
├── app/
│ ├── bedrock/ # LLM Wrapper
│ │ ├── claude_sonnet.py # Judgement, QR, Summary
│ │ └── titan_embedding.py # Vector Embedding
│ ├── core/ # Config, Task 정의
│ ├── prompt/ # 18개 프롬프트 템플릿
│ │ ├── generate_qr.txt
│ │ ├── judgement_scale.txt
│ │ └── ...
│ ├── service/ # 핵심 서비스
│ │ ├── opensearch_featureset_service.py
│ │ ├── judgement_scale_dataset_service.py
│ │ ├── metadata_generate_service.py
│ │ ├── feature_dataset_service.py
│ │ └── train_ranking_model_service.py
│ └── utils/ # OpenSearch Helper
└── dataset/ # 생성된 데이터셋
Query → Base Ranking → Reranking → Final Results
2-Stage Ranking: 속도와 정확도의 균형
빠른 후보 추출 - 수백만 → 수백 개
목적: 전체 문서에서 관련 후보군 빠르게 추출
방식:
⚡ 속도 우선: 수백만 문서를 ms 단위로 필터링
정밀 재정렬 - 상위 N개만 정교하게
목적: Base Ranking 결과를 더 정교하게 재정렬
방식:
🎯 정확도 우선: 상위 100~1000개만 대상
Base Ranking (Recall 확보) + Reranking (Precision 향상)
| 방식 | 속도 | 정확도 | 특징 |
|---|---|---|---|
| LTR (XGBoost) | ⚡ 빠름 | ★★★☆ | Feature 기반, OpenSearch 내장 |
| Cross-Encoder | 보통 | ★★★★ | Query-Doc 쌍 직접 비교 |
| LLM Reranker | 느림 | ★★★★★ | Cohere, GPT 기반 |
| ColBERT | 빠름 | ★★★★ | Token 레벨 매칭 |
LTR = 속도와 정확도의 균형점, 검색엔진 내장 가능
Query → Base Retrieval (BM25 + KNN) → LTR Rescore → Final Ranking
{
"query": { ... },
"rescore": {
"query": {
"rescore_query": {
"sltr": {
"model": "my-model-xgbr-tree-107-depth-2",
"params": {
"query_text": "검색어",
"query_embedding_str": "[0.1, 0.2, ...]"
}
}
}
}
}
}
| 구성요소 | 기술 |
|---|---|
| 검색 엔진 | OpenSearch + LTR Plugin |
| Feature | Text Match (7) + KNN (3) |
| Judgement | Claude Sonnet (5-scale) |
| Embedding | Amazon Titan |
| ML Model | XGBoost + Optuna HPO |
| 평가 지표 | Recall (primary) |
감사합니다