1. 오프라인 평가 프레임워크 구축
테크포크에는 추천 시스템이 있습니다.
기존의 평가 방식은 사실 시간문제로 게시글이 저장되어있는 MySQL과 Elasticsearch에 직접 연결한 상태로 진행했고,
이에 따라 테스트 데이터들이 DB에 남는 문제가 발생했습니다.
즉, 테스트가 실제 DB에 영향을 주는 구조였고 이는 큰 문제였습니다.
이를 개선하는 방향으로 추천 평가를 독립적인 테스트 환경에서 진행하기 위해
원격 DB에 있는 게시글의 데이터를 json 형태로 추출하면 어떨까 하는 생각이 들었습니다.
또한 추천에 필요한 사용자 데이터는 따로 없으므로 이것도 임의로 생성한 뒤 json 형태로 추출하는 계획을 세웠습니다.
추출 자체는 꽤나 쉬웠으나 문제는 JPA의 ID auto-increment에서 발생했습니다.
일단 추출한 json을 테스트에 쓰려고 로드 후 테스트 컨테이너에 저장하면
저장된 ID값과 기존 ID 값이 일치하지 않아 테스트 진행이 어려웠습니다.
결국 우여곡절 끝에 기존의 ID값과 저장된 ID값을 매핑하는 Map을 활용하여 이를 해결하였습니다만...
이게 최선이었나 하는 생각이 듭니다.
예를 들어 JPA 대신 JDBC Template를 활용한다면 ID 값을 그대로 활용할 수 있었을 거 같다는 생각이 드네요...
2. 판단 지표 생성 방식의 변화
기존의 판단 지표는 아무래도 사용자 데이터가 없다보니
게시글 키워드 기반으로 읽은 게시글들을 선정한 뒤 단순 키워드 매칭으로 판단 지표의 점수를 부여했었습니다.
하지만 추후에 추천에서도 키워드 기반 매칭을 할 의향이 있으므로
키워드 기반의 점수 부여 방식은 키워드 기반 매칭이 많이 일어난 게시글이 높은 순위를 차지하는 문제가 발생할 수 있습니다.
이에 따라 완전히 별개의 점수 부여 방식이 필요해졌습니다.
사용자가 될 수 있는 제가 임의로 부여할 수도 있지만 LLM을 판단 지표 제작에 사용한다면 편리할 거 같다는 생각이 들었습니다.
String userPrompt = String.format("""
다음 사용자가 해당 게시글을 추천받았을 때 얼마나 만족할지 평가해주세요.
## 사용자 프로필
%s
## 게시글 정보
- 제목: %s
- 회사/블로그: %s
- 요약: %s
- 본문 내용(일부):
%s
## 평가 기준
5점 (매우 강한 추천): 사용자의 핵심 관심사(주력 기술, 해결하려는 문제)와 정확히 일치하며, 반드시 읽어야 할 글.
4점 (추천): 사용자의 관심사와 밀접하게 관련되어 있으며, 흥미를 느낄 만한 글.
3점 (보통): 사용자의 관심사와 관련은 있으나, 핵심 분야가 아니거나 너무 일반적인 내용.
2점 (약간 관련): 키워드는 일부 겹치지만, 사용자의 주된 관심사와 거리가 먼 글.
1점 (관련 없음): 사용자의 관심사와 전혀 무관한 글.
## 응답 형식
반드시 점수(숫자 1~5)만 출력하세요. 설명은 필요 없습니다.
""",
userProfile.getProfileText(),
post.getTitle(),
post.getCompany(),
postSummary,
contentContext.length() > 0 ? contentContext.toString() : "(본문 데이터 없음)"
);
위와 같이 프롬프트를 작성하여 점수를 부여했고 확인해본 결과 꽤나 합리적이라고 생각이 들어 이 방법을 채택하였습니다.
3. 네이티브 kNN 전환
기존의 kNN 방식은 script_score를 사용하여 모든 게시글에 대해 kNN을 작동한 뒤,
후보군으로 조회된 100개의 게시글 중 읽은 게시글은 배제하는 걸 자바 코드에서 진행했었습니다.
하지만 이는 Retrieval 결과가 의도와 다르게 100개보다 적게 추출되었으며,
모든 게시글에 대해 kNN을 진행하므로 시간이 많이 소모되었습니다.
그래서 script_score 방식 대신 네이티브 kNN을 도입하여
mustNot 쿼리를 통해 후보군 조회 과정에서 읽은 게시글은 배제하도록 하였고,
kNN 대신 ANN을 사용하도록 하였습니다.
/**
* 읽은 글 제외를 위한 필터 쿼리 생성
*/
private Query createExcludeFilter(Set<Long> readPostIds) {
if (readPostIds == null || readPostIds.isEmpty()) {
return null;
}
List<FieldValue> excludeValues = readPostIds.stream()
.map(FieldValue::of)
.toList();
return Query.of(q -> q
.bool(b -> b
.mustNot(mn -> mn
.terms(t -> t
.field("postId")
.terms(v -> v.value(excludeValues))
)
)
)
);
}
public List<KnnSearch> createKnnSearches(
String titleField,
String summaryField,
String contentField,
float[] queryVector,
float titleWeight,
float summaryWeight,
float contentWeight,
int k,
int numCandidates,
Query filter
) {
List<KnnSearch> knnSearches = new ArrayList<>();
List<Float> vectorList = new ArrayList<>();
for (float v : queryVector) {
vectorList.add(v);
}
if (titleWeight > 0) {
knnSearches.add(KnnSearch.of(ks -> {
ks.field(titleField)
.queryVector(vectorList)
.k(k)
.numCandidates(numCandidates)
.boost(titleWeight);
if (filter != null) {
ks.filter(filter);
}
return ks;
}));
}
if (summaryWeight > 0) {
knnSearches.add(KnnSearch.of(ks -> {
ks.field(summaryField)
.queryVector(vectorList)
.k(k)
.numCandidates(numCandidates)
.boost(summaryWeight);
if (filter != null) {
ks.filter(filter);
}
return ks;
}));
}
if (contentWeight > 0 && contentField != null) {
knnSearches.add(KnnSearch.of(ks -> {
ks.field(contentField)
.queryVector(vectorList)
.k(k)
.numCandidates(numCandidates)
.boost(contentWeight);
if (filter != null) {
ks.filter(filter);
}
return ks;
}));
}
return knnSearches;
}
이를 통해 Latency를 1400ms에서 1000ms로 성능 최적화를 할 수 있었습니다.
4. 가중치 변경
이번에 평가 방법을 변경하고 나니 콘텐츠 임베딩이 오히려 추천에서는 노이즈임을 파악하여
콘텐츠 임베딩은 추천에서 배재하였습니다.
또한 제목과 요약 사이의 가중치는 0.5:0.5가 최적으로 판단되었고,
Mmr의 lambda값은 0.95로 선정하였습니다.
이 수치는 매우 유사한 글이 계속 반복되는 안정장치 역할을 수행합니다.
Recall@8는 0.1267
nDCG@8는 0.3550를 달성하였습니다.
아 Recall과 nDCG의 k값을 8로 설정한건 웹 UI에서 8개의 게시글이 메인 뷰에 보이기에
이 지표를 최우선으로 높여야 사용자를 후킹할 수 있을 것이라 판단했습니다.
'프로젝트 > Techfork' 카테고리의 다른 글
| [26/02/21] 오늘의 개발 일지 - Cloudflare + Nginx + Docker Blue-Green 무중단 배포 구축기 (0) | 2026.02.21 |
|---|---|
| [26/02/18] 오늘의 개발 일지 - Cloudflare 환경에서 Nginx 프록시 헤더 잘 사용하기 (0) | 2026.02.21 |
| [26/01/28] 오늘의 개발 일지 - 웹 Apple 소셜 로그인 구현 (0) | 2026.01.29 |
| [26/01/27] 오늘의 개발 일지 - 검색 API 엔드포인트 통합 & 회원탈퇴 API 구현 & Activity 도메인 테스트 코드 작성 (0) | 2026.01.28 |
| [26/01/26] 오늘의 개발 일지 - iOS 전용 카카오 로그인 API 구현 및 사용자 API 구현 (0) | 2026.01.28 |