BE-commerce 프로젝트
약 4분 분량 검색·추천

점수가 같은 상품의 순서가 검색의 쪽 넘김과 두 엔진의 차이를 흔들고 있었다

Search Lucene Elasticsearch 페이지네이션디버깅
목차

점수가 같은 상품을 엔진 내부의 문서 순서대로 줄 세운 것이 두 가지 문제를 만들고 있었습니다. 하나는 검색 결과의 쪽을 넘기는 사이 상품이 빠지거나 두 번 나온 것이고 다른 하나는 Elasticsearch와 OpenSearch의 상위 10개가 25% 달랐던 것입니다. 동점을 상품 id로 끊자 쪽 넘김 누락이 888건에서 9건으로 줄었고 두 엔진의 결과는 545개 쿼리 모두에서 같아졌습니다.

개요

H&M 카탈로그에는 이름이 같은 상품이 색상별로 여럿 있습니다. 그래서 검색 점수가 같은 동점이 많습니다. 10위 자리의 동점 무리가 상위 50개 중 중앙값 45개일 정도입니다. 검색 엔진은 동점을 내부 문서 번호 순서로 둡니다. 이 번호는 색인을 만든 절차와 문서를 갈아 끼운 이력이 정합니다.

검색 색인이 상품 변경을 1초 안에 반영하게 되면서(ch46) 이 순서가 계속 바뀌게 됐습니다. 그리고 이전에 원인을 모른 채 적어 둔 숫자가 하나 있었습니다. 같은 매핑과 같은 질의인데 Elasticsearch와 OpenSearch의 상위 10개 겹침이 0.753이었습니다.

동점을 내부 문서 순서로 두면 가격만 바뀌어도 쪽 경계가 밀리고 두 엔진의 순서가 색인 절차에 따라 달라진다. 동점을 상품 id로 끊었다

쪽을 넘기는 사이 결과가 흔들린다

검색은 오프셋(page) 방식입니다. 측정 전에 예측을 적었습니다. Lucene은 문서를 갈아 끼우면 번호가 바뀌니, 검색어와 무관한 가격 변경만으로도 동점 무리의 순서가 바뀌어 쪽 사이에 중복과 누락이 생길 것이라고요.

결과가 3쪽(20개씩) 이상인 쿼리 50개로 쟀습니다. 쿼리마다 1~3쪽 스냅숏을 먼저 찍고 1쪽을 본 뒤 3초 동안 변경을 넣고 2쪽, 다시 3초 뒤 3쪽을 봤습니다. 변경은 초당 20건입니다.

변경중복누락문제 있는 쿼리
없음000/50
카탈로그 무작위 상품의 가격000/50
그 쿼리 결과 상품의 가격8888850/50
새 상품 추가(이름이 검색어와 같음)682,00050/50

예측이 맞았습니다. 검색어와 무관한 가격 변경도 결과 상품에 몰리면 스냅숏 3,000개 중 888개(30%)가 끝내 나오지 않았습니다.

무작위 변경으로만 쟀다면 0이 나와 “문제 없음”으로 적었을 겁니다. 10만 건에서 무작위로 고른 변경은 쿼리 하나의 결과에 거의 걸리지 않습니다. 그래서 측정 전에 “결과 상품에 몰기” 조건을 이슈에 덧붙였습니다.

동점을 상품 id로 끊었다

정렬을 “점수 내림차순, 같으면 상품 id 오름차순”으로 바꿨습니다.

변경이전(중복 · 누락)동점 = 상품 id(중복 · 누락)
결과 상품의 가격88 · 8880 · 9
새 상품 추가68 · 2,00042 · 2,000

측정 전에 정한 채택 기준은 “가격 변경 아래 중복·누락 0”이었고 누락 9건이 남아 엄밀히는 넘지 못했습니다. 남은 9건의 원인은 동점이 아니었습니다. Lucene은 문서를 갈아 끼울 때 옛 판을 지웠다고 표시만 하고 병합되기 전까지 그 판도 단어 통계에 남깁니다. 가격만 바꿔도 그 상품 단어들의 IDF가 조금씩 내려가 점수 자체가 움직입니다. 이 성질을 단위 테스트로 확인하고 고정해 두었습니다.

그래도 채택했습니다. 이 정렬이 잃는 것을 찾지 못했기 때문입니다. 검색 지연은 흔들림 범위 안이었습니다(동시성 10 p95 19.7·26.2ms 대 18.6·20.4ms). 색인을 처음 만들 때는 문서 번호가 상품 id 순이라 동점 순서도 같습니다. 기준을 0으로 적은 것은 원인이 동점 하나라고 봤기 때문입니다. 원인은 둘이었습니다.

새 상품이 위로 끼어드는 경우는 동점 정렬로 막을 수 없습니다. 오프셋 방식의 한계라서 커서 방식 검색이 필요하고 그래도 위로 끼어든 상품은 안 보입니다.

두 엔진의 차이도 동점이었다

Elasticsearch와 OpenSearch를 같은 카탈로그로 색인하고 545개 쿼리의 상위 50개와 점수를 받아 비교했습니다. 처음 수집에서는 겹침이 1.000이라 0.753이 재현되지 않았습니다. 이유를 찾아보니 규모 실험 때 적재 중 refresh를 끄도록 색인 스크립트를 바꿨는데 그게 두 엔진의 문서 순서를 우연히 맞춰 놓았습니다.

처음 비교 때의 절차(적재 중 1초마다 refresh)로 다시 색인하자 겹침이 0.799로 떨어졌습니다. 다른 쿼리 197개를 가르면 점수가 다른 쿼리는 0개였습니다. 모두 동점 순서만 달랐습니다. 적재 중에 refresh가 돌면 세그먼트가 엔진마다 다른 시점에 생기고 하나로 병합한 뒤의 문서 순서도 달라집니다.

색인 절차정렬겹침
적재 중 refresh 1초점수0.799
같은 절차 + 상품 id 필드점수0.912
같은 절차 + 상품 id 필드점수, 상품 id1.000

Elasticsearch와 OpenSearch 질의에도 같은 동점 정렬을 넣었습니다. 색인 문서에 상품 id 필드가 있어야 해서 기존 색인은 다시 만들어야 합니다.

flip flop 한 쿼리만은 점수가 실제로 달랐습니다. explain API로 보니 상품명의 flip 단어 가중치가 두 배(7.70 대 3.85) 차이 났는데 같은 엔진도 색인을 다시 만들 때마다 이 값이 바뀌었습니다. 원인은 확인하지 못했습니다. 동점 정렬 뒤에는 이 쿼리의 상위 10개도 두 엔진에서 같았습니다.

정리

  • 검색 결과는 점수 내림차순, 같으면 상품 id 오름차순입니다. Lucene과 Elasticsearch, OpenSearch 모두 같습니다
  • 여러 단어 쿼리에서는 가격 변경만으로도 쪽 경계의 상품이 가끔 빠집니다(50개 중 4개 쿼리, 9건). 옛 판이 통계에 남아 점수가 움직이는 탓입니다
  • 원인을 모른 채 적어 두었던 0.753은 엔진 차이가 아닌 색인 절차의 차이였습니다
프로필 사진
작성자 @범수

오늘의 노력이 내일의 전문성을 만든다고 믿습니다.

댓글

댓글 수정/삭제는 GitHub Discussions에서 가능합니다.