Design Search Autocomplete System
요구사항 및 개략적 추정
빠른 응답 속도: 사용자가 검색어를 입력함에 따라 자동완성 검색어가 100ms 이내로 노출
표시 건수: 사용자가 입력한 검색어에 대해 5개의 자동완성 검색어를 제공
연관성: 자동완성 출력 검색어는 사용자 입력 단어와 연관성이 있어야 함
정렬: 시스템 계산 결과는 인기도 및 순위 모델에 의해 정렬되어야 함
규모 확장성: 많은 트래픽을 감당할 수 있도록 확장 가능한 시스템
고가용성: 시스템 일부 장애 발생시에도 서비스 중단이 없어야 함
DAU: 1,000만명으로 가정
검색수: 사용자당 매일 10건의 검색을 수행한다고 가정
검색 입력 데이터: 평균적으로 4개의 단어로 검색어를 입력한다고 가정
신규 검색 여부: 검색 가운데 20% 정도를 신규 검색어라 가정
검색어 자동완성 시스템에서 위와 같은 요구사항을 만족해야할 때, 아래와 같이 추정할 수 있다.
검색 입력 데이터: 각 단어를 평균적으로 5글자로 가정하면 4 * 5 = 20byte(문자 인코딩 방법 ASCII로 가정)
검색당 서버 요청 수: 검색 1회당 20건(한 글자 입력할 때마다 매번 요청)
초당 서버 요청 수(QPS): 1,000만명 * 10검 검색 * 20번 요청 / 24 / 3600 = 23,148
최대 QPS: 23,148 * 2(부하를 고려하여) = 46,296
데이터 저장량: 1,000만명 * 10검 검색 * 20byte * 20% = 400MB
시스템 설계
검색어 자동완성 시스템을 구현하기 위해 크게 두 부분으로 나눌 수 있다.
데이터 수집 서비스: 사용자가 입력한 질의를 수집하는 시스템
질의 서비스: 주어진 질의에 다섯 개의 인기 검색어를 정렬하여 반환하는 시스템
데이터베이스 자료구조 - 트라이 자료구조
SELECT *
FROM TB
WHERE keyword LIKE 'prefix%'
ORDER BY popularity DESC
LIMIT 5
관계형 데이터베이스로 위와 같은 쿼리를 수행할 수 있지만, 많은 데이터를 처리할 때는 효율적이지 않기 때문에 트라이 자료구조를 사용하는 것이 좋다. 트라이는 문자열들을 간략하게 저장할 수 있는 자료구조로 아래와 같은 특징을 가진다.
트리 형태의 자료구조
트리의 루트 노드는 빈 문자열을 나타냄
각 노드는 글자 하나를 저장하며, 해당 글자 다음에 등장할 수 있는 글자 수(알파벳의 경우 26개)만큼의 자식 노드를 가질 수 있음
각 트리 노드는 하나의 단어 혹은 접두어 문자열을 나타냄
이러한 특징에 더해 이용 빈도가 높은 단어를 빠르게 찾기 위해 노드에 빈도 정보를 저장할 수도 있다.

위 사진 처럼 트리 형태로 저장되며, 자세한 설명을 위한 용어를 정의하면 다음과 같다.
p: 접두어(prefix)의 길이
n: 트라이 안에 있는 노드 개수
c: 주어진 노드의 자식 노드 개수
트라이 구조로 자동완성을 구현하기 위해 아래와 같은 방법을 사용한다.
1
해당 접두어를 표현하는 노드 탐색
O(p)
2
해당 노드부터 시작하는 하위 트리를 탐색하여 모든 유효 노드 탐색
O(c)
3
유효 노드들을 정렬하여 인기있는 검색어 탐색
O(c log c)
만약 'tr' 입력했다고 가정하면 위 알고리즘은 다음과 같이 동작하게 된다.
접두어 노드 'tr' 찾음
해당 노드부터 시작하는 하위 트리를 탐색하여 모든 유효 노드 탐색
tree: 10 / true: 35 / try: 29가 유효 노드
유효 노드를 정렬
true: 35 / try: 29 / tree: 10
이 알고리즘은 직관적이지만 최악의 경우 전체 트라이를 다 검색해야 하는 일이 생길 수 있기 때문에 다음과 같은 방법을 사용하여 방지할 수 있다.
접두어 최대 길이 제한: 사용자가 검색창에 긴 검색어를 입력하는 일이 거의 없으므로, 작은 정숫값(예를 들어 50)으로 제한하여 O(1) 시간 복잡도를 유지
노드에 인기 검색어 캐시: 각 노드에 k개의 인기 검색어를 저장하면 하위 트리를 탐색하는 시간을 줄일 수 있음(이미 캐싱되어 있는 검색어는 O(1) 시간에 반환 가능)
데이터 수집 서비스
데이터 수집을 실시간으로 하는 방법도 있지만, 아래 두 가지 문제가 존재한다.
매일 수천만 건의 검색어에 대해 트라이가 갱신되기 때문에 부하가 커짐
일단 트라이가 만들어지고 나면 인기 검색어는 자주 바뀌지 않아 자주 갱신할 필요가 없음
실시간 수집 대신에 데이터 분석 서비스나 로깅 서비스를 통해 데이터를 수집하고, 트라이 자료구조를 주기적으로 갱신하는 방법을 사용할 수 있다.
데이터 분석 서비스 로그: 사용자가 검색한 검색어를 로깅
로그 취합 서버: 수집된 로그를 취합하여 저장
취합된 데이터: 검색어 / 수집 시간 날짜 / 검색 횟수의 형태로 저장
작업 서버: 취합된 데이터를 주기적으로 읽어 트라이 자료구조를 만들어 트라이 데이터베이스에 저장
트라이 데이터베이스: 트라이 자료구조를 저장하는 데이터베이스, 두 가지 데이터베이스 형태로 구현 가능
문서 저장소: 트라이를 직렬화하여 저장
키-값 저장소: 트라이에 보관된 모든 접두어를 해시 테이블 키로, 각 트라이 노드의 데이터를 값으로 저장
트라이 캐시: 트라이 데이터를 메모리에 유지하여 조회 성능을 향상(일정 주기로 트라이 데이터베이스의 스냅샷을 가져와 갱신)
질의 서비스
질의 서비스를 구현하기 위해 아래의 순서로 진행할 수 있다.
검색 질의가 로드밸런서로 전달
로드밸런서는 요청을 질의 API 서버로 전달
질의 API 서버는 트라이 캐시에서 데이터를 가져와 해당 요청에 대한 자동완성 검색어 제안 응답 구성
데이터가 트라이 캐시에 없는 경우 데이터를 트라이 데이터베이스에서 가져와 트라이 캐시에 저장
추가적으로 아래 최적화 방안으로 성능을 향상할 수 있다.
브라우저 캐싱: 대부분 애플리케이션의 경우 자동완성 검색어 제안 결과는 자주 바뀌지 않기 때문에 브라우저 캐싱을 통해 성능 향상
데이터 샘플링: 모든 질의 결과를 로깅하면 CPU 자원과 저장 공간을 많이 사용하게 되므로, N개의 요청중 하나만 로깅하도록 샘플링하여 로깅
트라이 연산
트라이는 검색어 자동완성의 핵심 컴포넌트로, 다음과 같이 동작한다.
트라이 생성: 트라이 생성을 담당하는 서버가 생성하며, 데이터 분석 서비스의 로그나 데이터베이스로부터 취합된 데이터를 이용하여 생성
트라이 갱신: 트라이를 갱신하는 데는 두 가지 방법 존재
매주 한 번 갱신: 새로운 트라이를 만든 후, 기존 트라이를 대체
트라이의 각 노드를 개별적으로 갱신: 각 노드를 개별적으로 갱신(하나의 노드가 갱신될 때 상위 노드들을 모두 갱신해야하므로, 트라이가 작을 때 고려해볼 수 있음)
검색어 삭제: 트라이 캐시에서 데이터를 가져올 때 필터 계층을 추가하여 부적절한 검색어를 제거(혐오성/폭력적/성적인 단어 등)
저장소 규모 확장
트라이의 크기가 한 서버에 넣기에 너무 커지는 경우 샤딩을 통해 규모 확장을 할 수 있다.
prefix 기반으로 샤딩('aa' - 'ag': 1번 샤드, 'ah' - 'az': 2번 샤드, ...)
각 prefix를 균등하게 분배하여 샤드에 저장
검색어 대응 샤드 관리자를 두어 어떤 샤드에 저장되어 있는지 확인
추가적인 고려사항
다국어 지원: 트라이에 유니코드 문자로 저장하여 다국어 지원
국가별 인기 검색어 순위 지원: 국가별로 다른 트라이 사용 및 트라이를 CDN에 저장하여 지연 시간을 줄임
참고자료
Last updated
Was this helpful?