|
| 1 | +검색창에 단어를 입력하다 보면 입력 중인 글자에 맞는 검색어가 자동으로 완성되어 표시되는 것을 볼 수 있다. 이런 기능을 보통 검색어 자동완성이라 부른다. |
| 2 | + |
| 3 | +### 요구사항 |
| 4 | + |
| 5 | +- 빠른 응답 속도: 사용자가 검색어를 입력함에 따라 자동완성 검색어도 충분히 빨리 표시되어야 한다. 페이스북 문서 기준 시스템 응답속도는 100밀초 이내여야 한다. |
| 6 | +- 자동완성되어 출력되는 검색어는 사용자가 입력한 단어와 연관된 것이여야 한다. |
| 7 | +- 계산 결과는 인기도 등의 순위 모델에 의해 정렬 |
| 8 | +- 규모 확장성, 고가용성 |
| 9 | + |
| 10 | +### 규모 추정 |
| 11 | + |
| 12 | +- DAU 천만 명 |
| 13 | +- 평균적으로 한 사용자는 매일 10건의 검색을 수행 |
| 14 | +- 질의할 때마다 평균적으로 20바이트의 데이터를 입력한다고 가정 |
| 15 | + - 문자 인코딩 방법으로는 ASCII → 1문자 = 1바이트 |
| 16 | + - 질의문으로 평균적으로 4개 단어, 각 단어는 평균적으로 다섯 글자 |
| 17 | + - 질의 당 평균 20바이트 |
| 18 | +- 검색창에 글자를 입력할 때마다 클라이언트는 검색어 자동완성 백엔드에 요청을 보냄. 평균적으로 1회 검색당 20건의 요청이 백엔드로 전달. |
| 19 | +- 대략 초당 24,000건의 QPS, 최대 QPS 48,000 |
| 20 | +- 질의 가운데 20% 정도는 신규 검색어라고 가정. 대략 0.4GB(= 천만 * 10 질의/일 * 20자 * 20%)만큼 매일 신규 데이터 추가 |
| 21 | + |
| 22 | +## 설계안 |
| 23 | + |
| 24 | +### 개략적 설계안 |
| 25 | + |
| 26 | +시스템은 두 부분으로 나뉨 |
| 27 | + |
| 28 | +- 데이터 수집 서비스: 사용자가 입력한 질의를 실시간으로 수집하는 시스템. |
| 29 | +- 질의 서비스: 주어진 질의에 다섯 개의 인기 검색어를 정렬해 내놓는 서비스 |
| 30 | + |
| 31 | +질의 빈도 테이블이 있다고 가정하면 SQL 질의문을 아래와 같이 사용 |
| 32 | + |
| 33 | +```sql |
| 34 | +SELECT * FROM frequency_table |
| 35 | +WHERE query Like 'prefix%' |
| 36 | +ORDER BY frequency DESC |
| 37 | +LIMIT 5; |
| 38 | +``` |
| 39 | + |
| 40 | +그러나 데이터가 많아지면 DB가 병목이 될 수 있음. (인덱스 효율 X) |
| 41 | + |
| 42 | +### 트라이 자료 구조 |
| 43 | + |
| 44 | +RDB는 다섯 개의 인기 검색어를 골라내는 방안으로는 효율적이지 않다. 트라이(접두어 트리라고 한다)를 사용해 해결 할 수 있다. |
| 45 | + |
| 46 | +트라이는 문자열들을 간략하게 저장할 수 있는 자료구조다. 이용 빈도에 따라 정렬된 결과를 내놓기 위해서는 노드에 빈도정보까지 저장해야 한다. |
| 47 | + |
| 48 | +용어 정의 후 알아보자. |
| 49 | + |
| 50 | +- p: 접두어의 길이 |
| 51 | +- n: 트라이 안에 있는 노드 개수 |
| 52 | +- c: 주어진 노드의 자식 노드 개수 |
| 53 | + |
| 54 | +가장 많이 사용된 질의어 k개는 다음과 같이 찾을 수 있다. |
| 55 | + |
| 56 | +- 해당 접두어를 표현하는 노드를 찾는다. 시간 복잡도는 O(p)이다. |
| 57 | +- 해당 노드부터 시작하는 하위 트리를 탐색하여 모든 유효 노드를 찾는다. 유효한 검색 문자열을 구성하는 노드가 유효 노드다. 시간 복잡도는 O(c)이다. |
| 58 | +- 유효 노드들을 정렬하여 가장 인기 있는 검색어 k개를 찾는다. 시간 복잡도는 O(c*logc)이다. |
| 59 | + |
| 60 | +이 알고리즘의 최악의 경우에는 k개 결과를 얻으려고 전체 트라이를 다 검색해야 하는 일이 생길 수 있다. 이때 해결법은 다음과 같다. |
| 61 | + |
| 62 | +1. 접두어의 최대 길이 제한 |
| 63 | + |
| 64 | + 사용자가 검색창에 긴 검색어를 입력하는 일은 거의 없다. 따라서 p값을 작은 정수값이라 가정해도 안전하다. 검색어의 최대 길이를 제한할 수 있다면 접두어 노드를 찾는 단계의 시간 복잡도는 O(p)에서 O(1)로 바뀐다. (최대 탐색 길이가 일정해지기 때문) |
| 65 | + |
| 66 | +2. 각 노드에 인기 검색어를 캐시 |
| 67 | + |
| 68 | + 각 노드에 k개의 인기 검색어를 저장해 두면 전체 트라이를 검색하는 일 방지. 5~10개 정도의 자동완성 제안을 표시하면 되기에 k는 충분히 작은 값이다. 그러나 각 노드에 질의어를 저장할 공간이 많이 필요하게 된다. |
| 69 | + |
| 70 | + |
| 71 | +앞의 두 가지 최적회 기법을 적용하면 시간 복잡도는 아래와 같다. |
| 72 | + |
| 73 | +1. 접두어 노드를 찾는 시간 O(1) |
| 74 | +2. 최고 인기 검색어 5개를 찾는 질의의 시간 복잡도 O(1) |
| 75 | + |
| 76 | +### 데이터 수집 서비스 |
| 77 | + |
| 78 | +사용자가 검색창에 뭔가 타이핑을 할 때마다 실시간으로 데이터를 수정하는 것은 다음과 같은 이유로 실용적이지 않음. |
| 79 | + |
| 80 | +- 매일 수천만 건의 질의가 입력되면 그때마다 트라이를 갱신해야 함. |
| 81 | +- 일단 트라이가 만들어지고 나면 인기 검색어는 그다지 자주 바뀌지 않을 것이라 트라이를 자주 갱신할 필요가 없다. |
| 82 | + |
| 83 | +트위터와 같은 서비스는 검색어를 신선하게 유지할 필요가 있지만 구글 검색은 자주 바꿀 필요가 없다. |
| 84 | + |
| 85 | +용례가 달라지더라도 데이터 수집 서비스의 토대인 트라이를 만드는 데 쓰는 데이터는 보통 데이터 분석 서비스나 로깅 서비스로부터 온다. |
| 86 | + |
| 87 | +따라서 이를 바탕으로 재설계한다. |
| 88 | + |
| 89 | +- 데이터 분석 서비스 로그 |
| 90 | + - 검색창에 입력된 질의에 관한 원본 데이터가 보관되며, 수정은 이루어지지 않고 인덱스도 걸지 않는다. |
| 91 | +- 로그 취합 서버 |
| 92 | + - 양이 엄청나고 데이터 형식이 제각각인 데이터들을 취합하여 소비할 수 있도록 하는 서버. |
| 93 | + - 취합 주기는 실시간성의 중요도에 따라 다르게 설정 가능. |
| 94 | + - 본문에는 일주일 기준 |
| 95 | +- 작업 서버 |
| 96 | + - 주기적으로 비동기적 작업을 실행해 트라이 자료구조를 만들고 트라이 데이터베이스에 저장하는 역할을 담당 |
| 97 | +- 트라이 캐시 |
| 98 | + - 분산 캐시 시스템으로 트라이 데이터를 메모리에 유지하여 읽기 연산 성능을 높이는 구실. 매주 트라이 데이터베이스의 스냅샷을 떠서 갱신 |
| 99 | +- 트라이 데이터베이스 |
| 100 | + - 지속성 저장소로 사용 가능한 DB는 아래와 같다. |
| 101 | + 1. 문서 저장서(document store): 주기적으로 트라이를 직렬화해 DB에 저장할 수 있음. 몽고 DB 같은 문서 저장소를 활용하면 편리하게 가능.(유연한 스키마 제공으로 동적 데이터 저장 용이, JSON/BSON 포맷, …) |
| 102 | + 2. 키-값 저장소: 트라이에 아래 로직을 적용해 해시테이블 형태로 변환 가능. |
| 103 | + 1. 트라이에 보관된 모든 접두어를 해시 테이블 값으로 변환 |
| 104 | + 2. 각 트라이 노드에 보관된 모든 데이터를 해시 테이블 값으로 변환 |
| 105 | +- 검색어 삭제 |
| 106 | + - 위험한 질의어를 자동완성 결과에서 제거 하기 위해 트라이 캐시 앞에 필터 계층을 두고 필터 규척에 따라 검색 결과를 거른다. |
| 107 | + |
| 108 | +### 질의 서비스 |
| 109 | + |
| 110 | +새 설계를 바탕으로 아래와 같이 작동한다. |
| 111 | + |
| 112 | +1. 검색 질의가 로드밸런서로 전송된다. |
| 113 | +2. 로드밸런서는 해당 질의를 API 서버로 보낸다. |
| 114 | +3. API 서버는 트라이 캐시에서 데이터를 가져와 해당 요청에 대한 자동완성 검색어 제안 응답을 구성한다. |
| 115 | +4. 데이터가 트라이 캐시에 없는 경우에는 데이터를 데이터베이스에 가져와 캐시에 채운다. |
| 116 | + |
| 117 | +더욱 최적화를 위한 방안은 아래와 같다. |
| 118 | + |
| 119 | +- AJAX 요청: AJAX 요청을 통해 페이지 새로고침을 할 필요가 없어짐. |
| 120 | +- 브라우저 캐싱: 제안된 검색어를 브라우저 캐시에 넣어서 해당 캐시에서 바로 가져갈 수 있다. 구글 검색 엔진이 이런 캐시 매커니즘을 사용한다. |
| 121 | +- 데이터 샘플링: 모든 질의 결과를 로깅하면 CPU 자원과 저장공간을 엄청나게 소진함. N개의 요청 가운데 1개만 로깅하도록 샘플링 |
| 122 | + |
| 123 | +### 저장소 규모 확장 |
| 124 | + |
| 125 | +영어만 지원되기 때문에, 첫 글자를 기준으로 샤딩 할 수 있다. |
| 126 | + |
| 127 | +이 방법을 쓰는 경우 알파벳이 26자기 때문에 서버가 26대로 제한된다. 더 늘리고 싶다면 샤딩을 계층적으로 해야한다. (첫 번째 글자는 첫 번째 레벨의 샤딩에, 두 번째 글자는 두 번째 레벨의 샤딩에) 그러나 단어에 시작하는 알파벳의 빈도에 따라 서버에 분배가 되기 때문에 부하가 균등하지 않다. |
| 128 | + |
| 129 | +이 문제를 해결하기 위해서는 과거 질의 데이터 패턴을 분석하여 샤딩하는 방법도 있다. 어느 알파벳이 많이 나왔는지 분석해서 샤드를 분배하는 것이다. |
| 130 | + |
| 131 | +## 추가적으로 생각해볼 점 |
| 132 | + |
| 133 | +- 다국어 기능을 지원하도록 트라이에 유니코드 데이터 저장 |
| 134 | +- 국가별로 다른 검색어 순위를 지원하기 위해 국가별로 다른 트라이를 사용하고 트라이를 CDN에 저장해 응답시간 최소화 |
| 135 | +- 실시간으로 변하는 검색어 추이를 반영하기. (현재 설계에는 트라이 갱신이 일주일이고 트라이 구성에 많은 시간 소요) |
| 136 | + - 샤딩을 통해 작업 대상 데이터를 줄인다. |
| 137 | + - 순위 모델을 바꾸어 최근 검색어에 높은 가중치 부여. |
| 138 | + - 데이터가 스트림 형태로 와서 한번에 모든 데이터를 동시에 사용할 수 없을 가능성이 있다는 점. |
| 139 | + |
| 140 | +[How We Built Prefixy: A Scalable Prefix Search Service for Powering Autocomplete](https://medium.com/@prefixyteam/how-we-built-prefixy-a-scalable-prefix-search-service-for-powering-autocomplete-c20f98e2eff1) |
| 141 | + |
| 142 | +https://engineering.fb.com/2010/05/17/web/the-life-of-a-typeahead-query/ |
0 commit comments