업데이트 한 번에 색인을 처음부터 다시 한 이유 — 그리고 다시는 조용히 못 일어나게 만든 방법
개발자가 받을 수 있는 가장 불쾌한 메시지 중 하나는, 앱이 이미 끝낸 일을 며칠에 걸쳐 다시 하고 있다는 사용자의 말입니다. 저희에게 그 일이 있었습니다. 업데이트를 하자 몇 주 전에 끝나 있던 의미 색인을 사용자 기기가 다시 계산하기 시작했습니다. 아무도 요청하지 않았고, 릴리즈 노트에도 그런 경고가 없었습니다.
흥미로운 대목은 저희가 실수를 했다는 사실이 아닙니다. 그 실수가 코드 리뷰에서는 명백히 옳은 변경으로 보였다는 점입니다.
의미 색인이 실제로 저장하는 것
키워드 검색은 단어를 저장합니다. 의미검색은 벡터를 저장합니다 — 어떤 대목이 무엇을 뜻하는지를 숫자 목록으로 담은 것이라, "계약서 검토 의견"이 단어 하나 겹치지 않는 "초안에 대한 피드백"을 찾아낼 수 있습니다.
결정적인 성질은 벡터 하나만으로는 아무 뜻도 없다는 것입니다. 같은 방식으로 만들어진 다른 벡터와의 관계에서만 의미가 생깁니다. 두 벡터가 비교 가능하려면 다음이 전부 같아야 합니다.
- 같은 임베딩 모델
- 같은 토크나이저 — 모델이 보기 전에 글을 어떻게 조각낼지 정하는 부분
- 그 둘을 둘러싼 같은 설정
하나라도 바뀌면 예전 벡터는 조금 틀린 게 아닙니다. 새 좌표계에서 아무 의미가 없습니다 — 나머지 데이터가 미터인데 혼자 피트로 잰 값 같은 것입니다. 부분 점수도, 변환도 없습니다. 유일하게 옳은 대응은 다시 계산하는 것입니다.
정확성 수정이 어떻게 데이터를 무효화하는가
일어난 일의 모양은 이렇습니다. 임베딩 전에 글을 조각내는 방식에 결함이 있었고, 그것을 고쳤습니다. 수정 자체는 옳았습니다 — 모델이 기대하는 방식과 맞아떨어졌고 결과도 좋아졌습니다.
그런데 글을 다르게 자른다는 것은 모델이 다른 입력을 받는다는 뜻입니다. 다른 입력은 다른 벡터를 냅니다. 이미 디스크에 있던 모든 벡터는 옛 방식으로 만들어졌으므로, 업데이트 이후에 계산되는 어떤 것과도 어긋나게 됐습니다.
이걸 코드 리뷰로 읽으면 작고 명백히 옳은 패치가 보입니다. 데이터 변경으로 읽으면 사용자가 저장한 색인의 100%를 무효화한다가 보입니다. 이 두 읽기는 서로 닮지 않았고, 리뷰 절차는 앞의 것만 보고 있었습니다.
이건 저희만의 함정이 아닙니다. 저장된 산출물이 어떤 파이프라인에 의존하는 곳이라면 어디든, 그 파이프라인 깊은 곳의 변경이 하류 전체를 무효화하면서도 국소적으로 보일 수 있습니다. 변경은 작고, 파급은 전부입니다.
"감지해서 변환하면 되지 않나"가 안 되는 이유
다시 계산하는 대신 옛 벡터를 변환하자는 생각이 자연스럽게 듭니다. 불가능합니다. 한 토크나이저로 만든 벡터를, 같은 글이 다른 토크나이저에서 만들었을 벡터로 바꾸는 함수는 없습니다. 그러려면 필요한 정보가 바로 잃어버린 그 정보이기 때문입니다. 재계산은 게으른 선택이 아니라 유일하게 옳은 선택입니다.
그러니 문제는 재계산이 일어나느냐가 아닙니다. 그것이 알고 일어나느냐입니다 — 릴리즈를 내보내는 사람과 설치하는 사람이 모두 그 비용을 미리 알았는가.
지금은 무엇이 막고 있나
해법은 "더 조심하자"가 아니었습니다. 조심은 작고 옳아 보이는 변경 앞에서 살아남지 못합니다. 대신 세 가지가 바뀌었습니다.
벡터 집합의 정체가 명시됩니다. 코드가 어쩌다 하는 동작으로 암시되는 대신, 저장된 벡터가 어느 모델·어느 설정에 속하는지를 상수가 이름 붙여 정합니다. 디스크의 벡터에는 어느 집합에서 왔는지가 찍혀 있어서, 앱이 시작 시점에 싸게 판단할 수 있습니다 — 지금 가진 것이 오늘 계산할 것과 여전히 같은가.
그 상수를 테스트가 붙들고 있습니다. 값이 바뀌는 순간 실패하는 것만이 유일한 임무인 회귀 테스트가 생겼습니다. 하나는 그 값을 바꾸지 않는 업그레이드가 아무것도 재계산하지 않음을 단언하고, 다른 하나는 바꾸는 업그레이드가 전량을 다시 큐에 넣는다는 것을 아무도 안 본 채 창발하도록 두지 않고 명시적으로 단언합니다. 축을 바꾸면서 빌드를 초록으로 유지할 수는 없습니다.
그 변경은 이제 선언된 행위입니다. 프로젝트 개발 규약을 고쳐서, 그 상수를 건드리는 것을 검색 랭킹을 바꾸는 것과 같은 급으로 다루게 했습니다. 테스트를 먼저 의도적으로 갱신해야 하고, 릴리즈 노트에 무엇이 재계산되며 대략 얼마가 드는지 적어야 합니다. 재색인이 다시는 없다는 뜻이 아닙니다 — 때로는 그게 옳습니다. 조용히 일어날 수 없다는 뜻입니다.
지금 LocalSynapse 를 쓰신다면
실질적인 결과는 둘입니다.
첫째, 평범한 업데이트는 색인을 건드리지 않습니다. 벡터 정체를 그대로 두는 업그레이드는 아무것도 재계산하지 않으며, 이것은 이제 가정이 아니라 테스트가 단언하는 사실입니다 — 보장의 성격이 다릅니다.
둘째, 앞으로 어떤 릴리즈가 정말로 재계산을 필요로 한다면, 설치 전에 릴리즈 노트에서 읽게 됩니다. 팬 소리로 알게 되지 않습니다. 그리고 재계산이 도는 동안에도 키워드 검색은 계속 동작합니다. 두 색인은 별개라서 검색이 없는 시간은 생기지 않습니다.
이 일에서 가져간 더 넓은 교훈이 있고, 이 글이 존재하는 이유이기도 합니다. 파생 데이터를 저장하는 시스템에서 "이 변경이 이미 디스크에 있는 데이터에 무슨 짓을 하는가"라는 질문은 사람의 기억이 아닌 곳에 있어야 합니다. 저희의 그 질문은 이제 실패하는 테스트 안에 있습니다. 릴리즈 직전 밤 열한 시에도 작동하는 알림은 그것뿐입니다.