
설계에 앞서 다음 사항을 먼저 확인해야 함.
빠른 응답 속도
연관성
정렬
규모 확장성
고가용성
현재 데이터가 없으므로 몇 가지 가정을 세우고 시작함.
일간 능동 사용자(DAU): 1000만 명
한 사용자가 하루에 검색하는 횟수: 평균 10회
질의당 평균 입력 데이터 크기: 20바이트
신규 검색어 비율: 전체의 20%
검색어를 입력할 때 글자 하나마다 요청이 발생함.
"dinner" 입력
→ search?q=d
→ search?q=di
→ search?q=din
→ search?q=dinn
→ search?q=dinne
→ search?q=dinner
따라서 검색 1회당 평균 20건의 요청이 백엔드로 전달됨.
QPS
= 1000만 × 10회 × 20건 ÷ 86,400초
≈ 24,000
최대 QPS
= 24,000 × 2
= 48,000
하루 질의 데이터
= 1000만 × 10회 × 20바이트
= 2GB
신규 데이터
= 2GB × 20%
= 0.4GB / 일
시스템은 크게 두 부분으로 나눌 수 있음.
데이터 수집 서비스
질의 서비스
질의문과 사용 빈도를 저장하는 빈도 테이블이 있다고 가정함.
데이터 수집 서비스는 사용자가 입력한 질의문을 저장하면서 사용 빈도를 늘림.

질의 서비스는 빈도가 높은 순으로 상위 k개의 검색어를 SQL 질의문으로 계산하여 보여줌.
사용하는 질의문은 다음과 같음.
SELECT * FROM frequency_table
WHERE query LIKE 'prefix%'
ORDER BY frequency DESC
LIMIT k
데이터가 적을 때는 괜찮은 설계안이지만, 데이터가 많아지면 DB가 병목이 됨.
따라서 상세 설계안으로 넘어감.
관계형 DB로 가장 인기 있는 k개의 질의문을 골라내는 방식은 효율적이지 않음.
따라서 트라이 자료구조를 사용하여 응답 시간을 줄임.
트라이(trie)라는 이름은 retrieval의 가운데 부분에서 따온 것임.
자식 노드의 개수는 지원하는 문자 집합에 따라 달라짐.
영문 소문자만 지원한다면 노드마다 최대 26개의 자식을 가질 수 있고, 한글이나 유니코드 전반을 지원한다면 훨씬 커짐.
따라서 요구사항에서 지원 언어를 먼저 확인해야 함.
빈도 테이블의 정보를 트라이 노드에 저장하면 다음과 같음.

기호는 다음과 같이 정의함.
p: 접두어의 길이n: 트라이 안에 있는 노드 개수c: 주어진 노드의 자식 노드 개수해당 접두어를 표현하는 노드를 찾음 → O(p)
그 노드부터 하위 트리를 탐색하여 모든 유효 노드를 찾음 → O(c)
유효 노드를 정렬하여 가장 인기 있는 검색어 k개를 찾음 → O(c log c)
전체 시간 복잡도는 다음과 같음.
O(p) + O(c) + O(c log c)
= O(p + c log c)
최악의 경우 전체 트라이를 모두 검색해야 하는 상황이 생길 수 있음.
이를 해결하는 방법은 두 가지임.
접두어의 최대 길이 제한
p를 상수로 만들 수 있으므로 1단계가 O(1)이 됨각 노드에 인기 검색어 캐시
저장 공간이 많이 필요해지지만, 빠른 응답 속도가 더 중요하므로 이 기법들을 사용함.
두 기법을 함께 적용하면 접두어를 찾아 인기 검색어를 얻는 과정의 전체 시간 복잡도가 O(1)이 됨.
접두어 길이 제한 → O(p)를 상수로
노드별 top k 캐시 → 탐색과 정렬 제거
= O(1)
여기서 O(1)은 접두어 길이가 상수로 제한된다는 전제 위에서 성립함.
앞선 설계안에서는 실시간으로 데이터를 갱신했으나, 매일 수천만 건의 질의가 발생하는 대형 서비스에서는 적합하지 않음.
트라이가 한 번 만들어지면 인기 검색어는 자주 바뀌지 않으므로 자주 갱신할 필요가 없음.
트라이를 만드는 데 쓰이는 데이터는 보통 데이터 분석 서비스나 로깅 서비스로부터 옴.

데이터 분석 로그
로그 취합 서버
작업 서버
트라이 캐시
트라이 DB
영속성 저장장치로는 두 가지 선택지가 있음.
문서 저장소
키-값 저장소
접두어 "din" → 키
["dinner", "dinosaur", ...] → 값
질의 서비스는 다음 과정으로 이루어짐.
AJAX 요청
브라우저 캐싱
데이터 샘플링
작업 서버가 담당하며, 데이터 분석 서비스의 로그나 데이터베이스로부터 취합된 데이터를 이용함.
두 가지 방법이 있음.
주기마다 전체 갱신
각 노드를 개별적으로 갱신
혐오 조장, 폭력성, 성적 불쾌감, 위험 단어 등은 연산 결과에서 제거해야 함.
캐시 앞에 필터 계층을 두고 부적절한 질의어를 걸러내는 방법이 좋음.
트라이 캐시
→ 필터 계층
→ API 서버
필터 계층을 두면 필터 규칙에 따라 검색 결과를 자유롭게 변경할 수 있기 때문임.
트라이가 한 서버에 담기지 않을 만큼 커지면 샤딩이 필요함.
첫 글자를 기준으로 나누는 방식은 데이터가 균등하게 분배되지 않음.
a로 시작하는 검색어 → 매우 많음
x, z로 시작하는 검색어 → 매우 적음
과거 질의 데이터의 분포를 분석하여, 각 샤드가 비슷한 양의 데이터를 담도록 구간을 나눔.
분포 분석 결과 s로 시작하는 검색어가 많다면
→ s 하나만으로 한 샤드를 구성
u, v, w, x, y, z는 각각 적다면
→ 묶어서 한 샤드로 구성
이 매핑 정보, 즉 어떤 검색어가 어느 저장소 서버에 저장되는지를 관리하는 것이 검색어 대응 샤드 관리자임.

검색어 자동완성 시스템의 전체 흐름은 다음과 같음.
수집
→ 데이터 분석 로그
→ 로그 취합 서버
→ 작업 서버
→ 트라이 DB
→ 트라이 캐시
질의
→ 로드밸런서
→ API 서버
→ 트라이 캐시(미스 시 DB)
→ 응답
핵심은 다음과 같음.
O(1)에 가까워짐