검색 키워드 품질 에이전트
검색 키워드 사전에 등록돼 있는데 검색 결과가 0건인 키워드를 매일 찾아, 고치는 SQL과 되돌리는 SQL을 만들어 주는 에이전트를 만들었습니다. 에이전트는 DB에 직접 쓰지 않고, 사람이 SQL을 검토한 뒤 실행합니다.
풀려던 문제
검색어가 필터가 되는 방식
댄블 검색은 검색어를 발골사전(search_keywords 테이블)에서 찾아 필터로 바꿉니다. 사전의 키워드는 저마다 타입이 있고, 타입마다 실제로 걸 필터 값이 따로 연결돼 있습니다.
| 타입 | 연결된 값 | 예시 |
|---|---|---|
| 브랜드 | 브랜드 | "폴로 랄프로렌" → 폴로 랄프로렌 브랜드 상품만 |
| 카테고리 | 상품 카테고리 | "반팔티" → 반팔 티셔츠 카테고리 상품만 |
| 색상 | 상품의 색상 옵션 | "블랙" → 블랙 색상 옵션이 있는 상품만 |
| 스타일 | 없음 | "하객룩" → 필터 없이 상품명·설명 텍스트로 찾기 |
"폴로 랄프로렌 반팔티"를 검색하면 "폴로 랄프로렌"은 브랜드 필터로, "반팔티"는 카테고리 필터로 바뀌고, 두 조건을 모두 만족하는 상품이 나옵니다. 그래서 키워드는 사전에 등록돼 있는데 연결된 값이 비어 있거나 잘못돼 있으면, 검색어를 알아보고도 제대로 된 필터를 걸지 못합니다. 걸 필터가 하나도 없으면 검색은 그 자리에서 결과 없이 끝납니다.
등록돼 있는데 결과가 0건
가장 먼저 풀어야 했던 건 사전에 등록돼 있는데 결과가 0건인 키워드였습니다. 등록이 안 된 키워드는 눈에 띄지만, 등록된 키워드가 0건을 내면 담당자는 "등록했으니 됐다"고 생각하는 동안 사용자만 빈 화면을 보게 됩니다. 원인은 대부분 위의 연결 데이터에 있었습니다.
| 검색어 | 증상 | 원인 |
|---|---|---|
| 로고 | 0건 | "로고"가 색상 타입으로 등록돼 있는데 연결된 색상 옵션이 하나도 없음. 검색어는 알아봤지만 걸 필터가 없어 검색이 결과 없이 끝남 |
| 체크셔츠 | 0건 | 무늬인 "체크"가 색상 타입으로 등록돼, "체크 색상이면서 셔츠인 상품"을 찾게 됨. 그런 상품은 없음 |
| 원피스 | 카테고리 필터가 안 걸림 | 카테고리 타입으로 등록돼 있는데 연결된 카테고리가 없어, 원피스 카테고리로 거르지 못하고 텍스트로만 찾음 |
| 트레이닝 | 하의만 노출 | 카테고리 타입인데 연결된 카테고리가 없고, 같은 뜻으로 묶인 "트레이닝 팬츠"의 카테고리만 따라가 상의·세트가 빠짐 |
키워드가 수천 개라 사람이 하나씩 확인하기는 어려웠습니다. 그래서 매일 데이터를 모아 이런 키워드를 찾고, 빠진 연결 값을 채우거나 필터 대신 텍스트로 찾도록 스타일 타입으로 바꾸는 SQL까지 만들어 주는 에이전트를 만들었습니다.
목표 지표는 등록 키워드 중 검색의 절반 이상이 0건인 키워드의 비율로 잡고, 0.5% 아래를 목표로 했습니다. 남는 0.5%는 형태소 분석기 한계처럼 데이터로 고칠 수 없는 경우입니다.
무엇부터 찾는지
| 순위 | 대상 |
|---|---|
| 1 | 등록됐는데 결과가 망가진 키워드 (0건, 필터 미적용, 범위가 좁아짐, 인기 상품으로 대체 노출) |
| 2 | 등록되지 않았고 사실상 0건인 키워드 |
| 3 | 등록됐는데 아무도 클릭하지 않는 키워드 |
| 4 | 깨진 연결 데이터 |
| 5 | 등록해 두면 좋은 미등록 키워드 |
1순위 문제가 있는 날은 1순위만 다루고, 없을 때만 다음 순위로 내려갑니다.
설계 원칙
- DB에 직접 쓰지 않습니다. 결과물은 고치는 SQL과 되돌리는 SQL 파일이고, 실행은 사람이 검토한 뒤에 합니다.
- 운영 DB는 하루 한 번만 조회합니다. PostgreSQL과 ClickHouse에서 필요한 데이터를 CSV로 한 번에 받아 두고, 이후 분석은 전부 로컬 DuckDB로 합니다. 분석 중에 쿼리를 수십 번 해도 운영 DB에는 부담이 없습니다.
- 찾는 역할, SQL을 쓰는 역할, 검증하는 역할을 나눴습니다. 한 역할이 찾고 고치고 검증까지 하면 자기 실수를 놓치기 쉽기 때문입니다.
실행 흐름
0. 준비 (병렬) 백엔드 코드에서 키워드 등록·분류 로직 읽기 / DB 스키마 수집 / 30일 검색 추이 집계
1. 수집 PostgreSQL·ClickHouse → CSV 스냅샷 (하루 1회)
2. Analyst DuckDB로 CSV를 조회해 문제 키워드 탐지 → issues.json
3. Generator 이슈별로 고치는 SQL과 되돌리는 SQL 작성
TestRunner 외래키·중복 키·되돌리기 짝·빠진 이슈를 코드로 검사 → 실패하면 Generator가 다시 작성
4. Reviewer 이슈마다 5개 기준으로 채점 → 통과 / SQL 다시 / 탐지 다시 / 사람에게 넘김
Analyst와 Reviewer는 Claude Opus, 코드 읽기와 Generator는 Claude Sonnet으로 돌렸습니다. Reviewer가 같은 피드백을 두 번 연속 주면 사람에게 넘겨, 고치고 반려하는 반복이 끝없이 돌지 않게 했습니다.
검증은 두 번
TestRunner는 코드라서 빠르고 확실합니다. 외래키가 깨지거나, 중복 키가 생기거나, 되돌리는 SQL이 빠진 것처럼 규칙으로 판단할 수 있는 오류를 잡습니다. Reviewer는 키워드 타입을 맞게 골랐는지, 다른 검색어 결과까지 바꾸지 않는지처럼 도메인을 알아야 판단할 수 있는 부분을 봅니다.
| 기준 | 보는 것 | 배점 | 통과 필수 |
|---|---|---|---|
| 타입 분류 | 브랜드·카테고리·색상·스타일 중 맞는 타입인지 | 30 | 예 |
| 해결력 | 이 SQL로 실제 문제가 풀리는지 | 25 | 예 |
| 안전성 | 다른 키워드·관계에 영향이 없는지 | 20 | 예 |
| 정합성 | 외래키·중복·배열 형식·NULL 처리 | 15 | 예 |
| 영향도 | 수정 전후 검색 결과 변화 예측, 되돌리기 완전성 | 10 |
통과 필수 기준 중 하나라도 걸리면 그 이슈는 반려됩니다.
한 번 검토한 키워드는 기억
"폴로"처럼 브랜드와 카테고리가 섞인 키워드는 매일 이슈로 다시 올라옵니다. 담당자가 한 번 판단한 키워드는 reviewed_keywords.json에 남겨 다음 실행부터 반영합니다.
- 제외: 이슈로 올리지 않음 (예: 폴로, 남겨 두기로 결정)
- 확정: 방향이 정해졌으니 바로 처리 (예: ck → Calvin Klein 브랜드)
- 정상: 잘 등록된 키워드. 클릭률이 급락하거나 0건 비율이 급등할 때만 다시 검사
돌려 보며 알아낸 것
에이전트를 돌리고, 검색 API를 직접 호출하고, 백엔드 코드를 읽어 가며 확인한 내용입니다. 확인한 내용은 탐지 조건과 프롬프트에 다시 반영했습니다.
- 색상인데 패턴 값이 연결된 키워드: 체크·레오파드·글렌체크는 색상 키워드로 등록돼 있었지만, 연결된 값의 실제 타입은 상의 패턴이었습니다. 검색 인덱스의 색상 필드에는 색상 타입만 들어가서 이 필터는 항상 0건이었습니다. 연결 값이 비어 있지 않아 기존 탐지 조건에도 걸리지 않았기 때문에, 연결된 값의 타입까지 확인하도록 조건을 추가했습니다.
- 카테고리 연결이 빠진 키워드: "남방"과 트레이닝 계열 키워드는 카테고리로 등록돼 있었지만 카테고리 연결 데이터가 없었습니다. 검색 API를 호출해 보니 카테고리 필터 대신 텍스트로 대체 검색되고 있었습니다("남방" 3,180건).
- 오류처럼 보이지만 설계인 값: 검색 API 응답의 카테고리 id는 항상 비어 있습니다. 백엔드 코드를 읽어 보니 일부러 넣지 않는 값이어서, 에이전트가 이 값을 근거로 SQL을 만들지 않도록 규칙에 넣었습니다.
- 판매 중인 상품이 없는 브랜드: 루이비통은 브랜드 데이터는 있지만 판매 중인 상품이 없어서, 브랜드로 등록하면 오히려 0건이 됩니다. 브랜드 등록을 제안하기 전에 판매 중인 상품 수를 먼저 세도록 했고, 품절·판매 종료 상품은 빼고 셉니다.
- 수집 범위 때문에 생긴 오판: 노출이 꺼진 브랜드를 빼고 수집했더니 실제로 있는 브랜드를 없다고 판단하는 일이 생겨, 브랜드는 전부 수집하도록 바꿨습니다.
리포트
매일 실행이 끝나면 담당자가 볼 파일이 남습니다.
- 하루 요약: 그날 실행한 모든 시도의 판정, 점수, 이슈 수를 한 화면에서 비교
- 시도별 리포트: 이슈마다 채점표와 판정, 수정 전후 예상 효과(검색 수, 0건 비율 변화), 사람에게 넘긴 이슈는 이유와 다음에 할 일
- SQL 두 개: 고치는 SQL과 되돌리는 SQL. 담당자가 검토한 뒤 직접 실행
실제로 돌려 본 결과
2026년 5월 11일부터 24일까지 10일 동안 37번 실행했습니다. SQL 작성과 검증은 모두 103번 반복됐고, 그중 47번은 TestRunner가 규칙 위반으로 돌려보냈습니다. Reviewer 판정은 통과 36번, SQL 다시 쓰기 13번, 탐지 다시 하기 6번, 사람에게 넘김 1번이었습니다. 규칙으로 잡히는 오류는 Reviewer에게 가기 전에 걸러진 셈입니다.
5월 18일 다섯 번째 실행 리포트의 일부입니다. 효과는 Reviewer가 데이터로 예측한 값입니다.
| 검색어 | 판정 | 점수 | 내용 |
|---|---|---|---|
| 블레이저자켓 | 통과 | 95 | 카테고리로 등록. 최근 189회 검색, 인기 상품으로 대체 노출되던 73건 해소 예상 |
| 우영미 | 통과 | 100 | 브랜드로 등록. 최근 72회 검색, 해당 브랜드 상품 89개로 정확히 연결 |
| 하객 | 사람에게 넘김 | 상품명에 "하객"이 없어 SQL로는 해결 불가. 상품 태깅 검토 필요 | |
| 루이비통 | 사람에게 넘김 | 브랜드는 있지만 판매 중인 상품이 0개라 브랜드로 등록하면 오히려 0건 |
SQL로 풀 수 없는 문제는 억지로 고치지 않고 이유와 함께 담당자에게 넘기도록 했습니다.
설계 과정과 탐지하는 케이스 10가지는 Lab 글에 더 자세히 적었습니다.