VidDish의 레시피 검색에는 검색어에 맞는 문서가 있어도 결과에서 빠지는 문제가 있었습니다. 원인은 검색 결과의 순위를 매기기 전에 있었습니다. 단어별로 인기 있는 레시피를 일정 개수만 가져오고, 그 목록에 공통으로 들어 있는 문서만 남기는 과정에서 찾으려던 레시피를 버리고 있었습니다.
사용자는 검색에 나오지 않는 레시피가 앱에 없는 것인지, 검색이 놓친 것인지 구분할 수 없습니다. 결과가 몇 개라도 나오면 개발하는 쪽에서도 정상으로 보기 쉽습니다. 그래서 이 문제를 고칠 때는 화면에 무엇이 나타나는지만 볼 수 없었습니다. 나와야 할 레시피가 어느 단계에서 빠지는지를 먼저 살펴봐야 했습니다.
왜 처음부터 모든 레시피를 읽지 않았나
VidDish는 제목과 재료 등에 들어 있는 검색용 단어 조각을 따로 저장합니다. 검색 요청이 오면 그 조각을 가진 문서를 후보로 가져오고, 서버가 검색어와 얼마나 잘 맞는지 판단해 순서를 정합니다. 여기서 후보는 최종 결과를 고르기 위해 먼저 살펴보는 목록입니다.
처음 방식은 검색어의 첫 토큰으로 인기 상위 60개를 가져오는 것이었습니다. 후보가 60개에 도달하고 검색어가 여러 조각이면 두 번째 토큰으로도 상위 60개를 가져와 공통 문서를 남겼습니다. 검색마다 모든 레시피를 읽지 않도록 조회 범위를 제한하고, 두 단어에 함께 걸리는 문서로 범위를 좁히려는 구조였습니다.
이때 중요한 것은 ‘상위’의 기준입니다. 후보를 가져올 때는 인기도 순서였고, 검색어와의 관련도는 가져온 뒤에 계산했습니다. Firestore의 limit은 그 쿼리에서 가져올 문서 수를 제한합니다. 앱이 나중에 적용할 관련도 판단까지 대신하지는 않습니다. Firestore 정렬과 조회 수 제한 안내
인기 있는 레시피를 먼저 보여주는 선택은 탐색 화면에서는 자연스럽습니다. 하지만 특정 재료를 입력한 검색에서는 인기가 낮아도 그 재료에 정확히 맞는 레시피를 찾을 수 있어야 합니다. 후보 단계에서 빠지면 뒤의 관련도 계산에는 참여할 기회조차 없습니다.
두 목록의 공통 항목만 남겼는데 왜 놓쳤을까
‘돼지고기 김치’를 검색한다고 가정해 보겠습니다. 아래는 원리를 설명하기 위한 가상 자료입니다. 실제 구현의 60개 제한을 세 개로 줄였습니다.
| 인기순 후보 | ‘김치’로 가져온 목록 | ‘돼지고기’로 가져온 목록 |
|---|---|---|
| 첫 번째 | 김치전 | 제육볶음 |
| 두 번째 | 김치볶음밥 | 돼지고기 김치찌개 |
| 세 번째 | 돼지고기 김치찌개 | 돼지고기 김치볶음 |
두 목록의 공통 항목은 ‘돼지고기 김치찌개’ 하나입니다. 그런데 ‘돼지고기 김치볶음’도 검색어 두 단어에 모두 맞습니다. ‘김치’ 목록에서 인기순 세 개 안에 들지 못했다는 이유로 제외됐을 뿐입니다.
기존 방식은 공통 항목이 하나라도 있으면 그 목록을 채택했습니다. 그래서 처음 가져온 후보에 이미 들어 있던 김치볶음까지 버릴 수 있었습니다. 공통 항목이 아예 없으면 첫 목록을 유지하는 예외가 있었지만, 위처럼 한 개만 겹치는 상황은 해결하지 못했습니다.
전체 목록끼리의 교집합과, 각각 잘라 낸 일부 목록끼리의 교집합은 다릅니다. 이 차이를 놓치면 ‘두 조건을 함께 확인했으니 더 정확해졌다’고 생각하기 쉽습니다. 실제로는 조건을 확인할 문서부터 잃어버릴 수 있습니다.
더 적게 걸리는 단어에서 후보를 시작했다
수정 후에는 검색용 토큰별로 몇 개의 문서가 걸리는지 먼저 셉니다. 현재는 최대 네 개의 토큰을 비교하고, 그중 한 건 이상 걸리는 토큰에서 매치 수가 가장 적은 것을 고릅니다. 그 토큰으로 최대 300개의 후보를 가져온 뒤 검색어 전체와의 관련도를 판단합니다.
예를 들어 ‘김치’에 120개, ‘돼지고기’에 18개가 걸린다고 가정하면 돼지고기 쪽 18개를 먼저 살펴보는 식입니다. 검색 정보가 제대로 만들어져 있다면 두 단어를 모두 포함하는 레시피는 그 18개 안에도 있어야 합니다. 두 목록을 각각 잘라 공통 항목을 찾을 필요가 줄어듭니다. 이 숫자 역시 설명용 예시입니다.
앞의 표에 있던 ‘돼지고기 김치볶음’도 이 18개에 포함됩니다. 이제는 별도로 잘라 온 ‘김치’ 목록에 없다는 이유로 버리지 않고, 그 레시피의 검색용 본문에 두 단어가 있는지 직접 확인할 수 있습니다. 예전에는 교집합 단계에서 사라졌던 레시피가 수정 후에는 관련도 평가까지 도달하는 것입니다. 이 예시에서 달라진 결과는 바로 그 지점입니다.
이 선택의 핵심은 모든 검색어에 맞는 레시피를 포함하면서, 먼저 살펴볼 범위를 좁히는 것입니다. 후보를 모은 다음에는 검색어 조각이 얼마나 맞는지 등을 기준으로 순서를 정합니다.
기존 제한을 단순히 60개에서 더 큰 수로 바꾸는 방법도 생각할 수 있습니다. 하지만 두 목록을 각각 자른 뒤 교집합을 구하는 구조가 그대로라면, 자료가 더 늘었을 때 같은 문제가 다시 생깁니다. 이번 수정에서는 후보 상한을 늘리는 것과 함께 후보를 선택하는 기준을 바꿨습니다.
두 후보 목록을 교집합 대신 합집합으로 합치는 방법도 있습니다. 그러면 어느 한쪽에 들어 있던 레시피는 살릴 수 있습니다. 다만 양쪽 인기순 상한 밖에 있는 문서는 여전히 가져오지 못합니다. 반면 더 적게 걸리는 토큰의 후보를 전부 읽을 수 있다면, 그 범위 안에서 검색어 전체를 확인할 수 있습니다. 현재 방식이 유리한 조건은 이렇게 조회 상한 안에 들어오는 후보 집합을 고를 수 있을 때입니다.
이 변경에는 추가 작업도 생깁니다. 먼저 토큰별 매치 수를 조회해야 하고, 예전보다 많은 후보를 읽는 검색도 있습니다. 이번 선택은 누락을 줄이기 위해 조회 범위를 넓힌 것이므로, 읽기 수나 응답 속도까지 함께 개선됐다고 볼 수는 없습니다.
희귀한 단어 하나가 검색 의도를 빼앗지 않도록
후보 수가 적다는 이유만으로 검색 품질이 좋아지지는 않습니다. 예를 들어 ‘쉬운 김치찌개’에서 ‘쉬운’이라는 표현이 들어간 레시피가 몇 개 없고, 그 안에 김치찌개는 하나도 없을 수 있습니다. 희귀한 표현만 따라가면 사용자가 찾던 음식과 멀어집니다.
그래서 고른 후보에 검색어 조각을 모두 포함한 문서가 없으면, 조건에 따라 가장 긴 검색어 조각에서 만든 토큰의 인기 상위 60개도 가져와 합칩니다. 이 추가 조회가 폴백입니다. 이미 그 토큰으로 조회했거나 해당 토큰에 걸리는 문서가 없다면 반복해서 읽지 않습니다.
또 아무 문서에도 걸리지 않는 토큰은 첫 후보를 고를 때 제외합니다. ‘zzzzz 김치’처럼 불필요한 글자가 섞였다는 이유로 김치 후보까지 전부 사라지는 일을 줄이기 위해서입니다. 이것은 오타를 올바른 단어로 고쳐 주는 기능은 아닙니다. 남아 있는 검색어를 바탕으로 관련 결과를 보여주는 선택입니다.
따라서 이 검색은 언제나 모든 단어가 정확히 맞는 결과만 보여주는 엄격한 필터와는 동작이 다릅니다. 여러 단어에 맞는 결과를 우선하되, 그런 결과가 없을 때도 일부 관련된 레시피를 보여줄 수 있습니다. 검색을 기획할 때는 이런 부분 일치를 허용할지까지 함께 정해야 합니다.
300개를 가져오면 누락이 모두 없어질까
고른 토큰에 걸리는 문서가 300개 이하이고 검색 정보가 최신이며 빠짐없이 만들어져 있다면, 그 토큰의 후보를 모두 살펴볼 수 있습니다. 하지만 300개를 넘으면 다시 인기순 상위에서 잘립니다. 낮은 인기도의 레시피가 후보 밖으로 밀릴 가능성은 남아 있습니다.
후보에 포함되는 것과 첫 화면에 표시되는 것도 다릅니다. 현재 검색은 기본적으로 상위 20개를 반환합니다. 후보에 들어왔더라도 관련도 순서에서 밀리면 첫 결과에는 나타나지 않을 수 있습니다. 또한 검색용 정보 자체가 오래됐거나 일부 단어가 저장되지 않았다면 후보 선택 방식을 고치는 것만으로 해결되지 않습니다.
300은 이 앱에서 선택한 조회 상한입니다. 모든 앱에 적절한 값이라는 뜻은 아닙니다. 더 많이 읽으면 검색할 수 있는 범위가 넓어지지만, 처리할 자료와 응답에 걸리는 시간도 함께 살펴야 합니다. 개발 기록에는 후보 선택을 바꾼 전후의 비교가 남아 있고 관련 판단 로직의 테스트도 있습니다. 다만 이 글에서 운영 환경의 검색 누락이 모두 해소됐다거나 실제 사용자 만족도가 올랐다고 단정할 근거는 없습니다.
자료가 계속 늘어난다면 상한에 걸리는 검색이 얼마나 자주 생기는지, 찾아야 할 레시피가 실제로 후보에 들어오는지 확인해야 합니다. 두 단어가 모두 흔해서 더 적게 걸리는 쪽도 계속 300개를 넘는다면, 현재 방식의 장점이 줄어듭니다. 그런 검색에서 누락이 반복되는지가 후보 조회 방식을 더 바꾸거나 검색 기능에 맞는 다른 방식을 도입할 판단 근거가 됩니다.
같은 증상을 만났다면 찾으려던 문서 하나를 기준으로 따라가 보세요. 먼저 검색용 정보에 필요한 단어가 들어 있는지 봅니다. 정보가 맞는데 최초 후보에 없다면 조회 조건과 인기순 상한을 확인합니다. 최초 후보에는 있지만 중간에 사라졌다면 목록을 합치거나 좁히는 과정을 봐야 합니다. 마지막까지 남아 있는데 화면에 없다면 관련도 순서와 반환 개수를 살펴볼 차례입니다.
수정 확인에 쓸 자료도 유명한 레시피만 골라서는 부족합니다. 두 단어에 모두 맞지만 인기도가 낮은 레시피, 어느 한 단어에만 맞는 레시피, 아무 결과에도 걸리지 않는 단어를 섞어야 후보 누락과 부분 일치 동작을 함께 볼 수 있습니다. 특히 앞의 예시처럼 교집합이 한 개는 남는데 다른 정답을 버리는 경우를 포함해야 합니다. VidDish에서 문제가 됐던 것은 바로 그 단계였고, 결과가 몇 개 나왔다는 사실만으로는 이 누락을 발견하기 어려웠습니다.