모든 주제와 태그

검색·추천

14개의 글

GenPage는 순위 모델에 다섯 번 지고 비교 규칙을 맞춘 뒤 순위 점수와 합쳐 이기고 기본값이 됐다

Netflix GenPage 구성을 H&M 구매 데이터로 옮겨 업계 표준(후보 생성 + LightGBM 순위 모델)과 겨뤘다. 상품 고르기, 줄 구성, Taobao 노출 데이터, 통계 곁채널, 행 점수 결합까지 다섯 번 모두 졌다. 비교 규칙을 두 쪽에 같게 걸고 순위 모델 점수 위에서 GenPage가 줄만 고르게 하자 홀드아웃 68,984명에서 규칙 줄 구성을 페이지 nDCG +2.0%로 이겼다. 그 방식을 모델 서버와 앱 홈 2쪽 기능 플래그로 붙였고 지연은 지금 방식보다 짧았다. 표본이 작아 판정을 내리지 못한 가상 사용자 A/B를 같은 사용자 68,984명 짝 설계로 다시 재자 구간이 16분의 1로 줄며 이겼다. 앱이 실제로 보여 주는 구성으로 재도 이겨서 홈 2쪽 줄 구성의 기본값을 결합 방식으로 켰다.

추천 모델을 학습해 보고 넣지 않았다: 넘을 선을 먼저 정했고 생성 중 차단도 재 보고 껐다

학습한 ALS 모델의 최고 점수(MAP@12 0.0076)는 측정 전에 정한 선(마지막 구매 재추천 0.0234)의 3분의 1이었고 인기 상품보다도 낮았다. 품절 상품을 생성 중에 막는 방식은 막으려던 문제가 실제 카탈로그에 없었고 서빙 중앙값을 62ms에서 369ms로 늘려 개인화를 못 받는 요청을 만들었다. 둘 다 코드는 남기고 켜지 않았다.

검색 엔진을 넣을지 지연으로 먼저 재 봤다: 지연 축은 답했고 품질 축은 남았다

상품 목록 페이지 지연의 82.5%가 패싯 집계였다. 검색 엔진 도입 기준을 측정 전에 정해 두고 쟀더니 300ms를 넘긴 원인은 집계가 아니라 포화였고 패싯을 60초 캐시하자 패싯 p95가 90.9ms에서 5.7ms로 줄어 엔진을 검토할 조건이 사라졌다. 그런데 이커머스가 검색 엔진을 들이는 큰 이유는 지연이 아니라 품질이고 그 축은 아직 재지 않았다.

과부하에서 무엇을 포기하나: 정책은 지연을 정하고 개인화 비율은 모델 용량이 정한다

추천 모델이 감당하는 양보다 요청이 많을 때 넘친 요청을 어떻게 포기할지 세 정책을 비교했다. 개인화를 받은 요청 비율(coverage)은 세 정책이 ±0.3%p 안에서 같았고 갈린 것은 p95(160ms·320ms·454ms)였다. 아무것도 거절하지 않는 정책은 4배 부하에서 coverage가 31.4%로 더 좋아 보였는데 요청을 받지도 못해 분모에서 빠진 생존자 편향이었다.

검색 품질을 쟀더니 MySQL 이 따라오지 못했다: Lucene, Elasticsearch, OpenSearch 중 앱 안 Lucene 을 고른 이유

오타와 어형 변화가 섞인 검색어에서 nDCG@10이 지금 검색(LIKE) 0.071, MySQL FULLTEXT 0.324, Lucene 0.808, Elasticsearch 0.814, OpenSearch 0.798이었다. 측정 전에 정한 기준대로 엔진을 넣었고 세 엔진의 품질 차이가 0.02 안이라 운영 비용으로 앱 안 Lucene을 골랐다. 1차에서는 오타 허용이 blazer를 lazer에 밀리게 했다.

홈을 여러 쪽으로 나눠 만들었다: 커서가 보여 준 것을 들고 다니고, 다음 쪽 순서는 방금 본 상품이 정한다

GenPage처럼 홈을 몇 행씩 나눠 만들고 다음 쪽이 앞 쪽과 그 사이의 세션 활동을 입력으로 받게 했다. 쪽 상태는 서버에 두지 않고 커서에 실었다. 사용자 30명 실측에서 쪽 사이 중복은 0(커서가 없으면 2쪽 평균 8.6개)이었고 세션 반영은 96.7~100%였다. 1차에서는 인기 표에 아동복이 0개라 세션 신호가 조용히 버려지고 있었다.

GenPage를 실제 홈에 붙여 보니 모델보다 입력과 과부하 계약이 먼저 흔들렸다

사용자 이력으로 홈 2쪽을 생성하는 작은 GenPage 모델을 만들고 실제 서빙 경로까지 연결했다. 모델 품질은 MAP@12 0.0209로 기준선 0.0234를 넘지 못했지만, 더 큰 문제는 빈 HTTP 본문·학습과 다른 입력·게이트 밖 2쪽 호출이었다. 네 실험을 한 흐름으로 묶어 무엇을 확인하고 기본값을 켜지 않았는지 정리한다.

검색어와 필터를 함께 걸고 앱 안 Lucene이 몇 건까지 버티는지 쟀다

검색어가 있으면 카테고리·가격·재고 필터가 모두 무시되던 검색에 필터와 패싯을 붙였다. 엔진 안에서 걸면 결과 수가 정답과 171개 쿼리 모두 같았고 상위 500개를 받아 DB에서 거르면 95.5%를 잃었다. 복제 카탈로그로 규모를 늘리자 앱 안 Lucene은 300만 건에서 p95 232ms로 지연 예산을 넘었다.

WikiEngine 총정리: 1,215만 건 검색 엔진의 설계부터 RAG까지

나무위키+한국어 위키백과+영어 위키백과+뉴스+웹텍스트+C4 한국어 코퍼스 1,215만 건 검색 엔진 프로젝트를 2개월간 26편의 기술 블로그로 기록하고 총정리한다. MySQL LIKE 5,000ms 타임아웃에서 시작하여 임베디드 Lucene + Nori 한국어 형태소 분석으로 전환하고, Caffeine+Redis 2계층 캐시(82% 히트율), MySQL Replication R/W 분리, Nginx 스케일아웃(에러율 13.25%→0%), Debezium+Kafka CDC, Redis 3노드 Consistent Hashing까지 분산 아키텍처를 완성한다. 검색 품질은 동의어 확장, 오타 교정, UnifiedHighlighter snippet, LTR(NDCG +4.8%p), 카테고리 28개 자동 분류, Aho-Corasick 금칙어 필터링으로 고도화하고, RAG(Gemini SSE 스트리밍)로 AI 검색 요약을 제공한다. 자동완성 시스템 설계(CQRS + MapReduce + CDC)의 이론과 실제 구현의 매핑, 26편 전체 시리즈 링크, 핵심 수치 총정리를 포함한다.

AI 검색 요약: RAG 파이프라인 + SSE 스트리밍 + 비용 모니터링

Lucene BM25 검색 결과 Top-5 문서를 LLM 컨텍스트에 주입하는 RAG(Retrieval-Augmented Generation) 파이프라인을 구축한다. Spring AI 2.0 + Gemini 2.0 Flash로 SSE 스트리밍 답변을 생성하고, 인라인 출처 배지를 파싱하여 게시글 링크로 연결한다. 할루시네이션 방지(문서 기반 답변 제한 + 인용 강제), AI 요약 트리거 조건(네비게이션 의도 스킵), Redis Token Bucket rate limiting(10 RPM 전역), 동일 쿼리 캐싱(TTL 30분, LLM 비용 40-60% 절감), Grafana 7패널 대시보드(RPM, 응답시간, 토큰, 피드백, 비용 추정)까지 포함한다. BM25가 이 프로젝트에서 Dense Retrieval보다 적합한 근거와, Hybrid Retrieval 전환 로드맵도 정리한다.

LTR 재랭킹 + 카테고리 자동 분류: XGBoost4J + LLM-as-a-Judge

BM25 수동 가중치(title:3, content:1)의 한계를 Learning to Rank(LTR)로 극복한다. 카테고리 28개 자동 분류(키워드 기반, 정확도 83%) → SortedSetDocValuesFacetCounts 네이티브 Facet 전환 → 태그 216만 건 인덱싱을 1회 재색인으로 통합 반영한다. LLM-as-a-Judge(Gemini)로 학습 데이터 900쌍을 생성하고(1차 실패 98% → 5초 딜레이+지수 백오프로 해결), XGBoost LambdaMART 14개 피처로 학습하여 NDCG@10을 0.6910 → 0.7387(+4.8%p) 개선한다. XGBoost4J ARM64 네이티브 추론, Rescorer Top-200 재랭킹, RefreshListener 기반 FacetState 캐싱, MultiCollectorManager 단일 패스 수집까지 구현하지만, 2코어 ARM Free Tier에서 LTR ON 시 CPU 포화(72배 악화)를 k6로 실측하여 LTR_ENABLED=false로 비활성화한다.