하테나 서비스는 키워드를 포함하는 블로그
를 검색하는 기능을 포함하고 있습니다.
하테나는 이를 구현하기 위해 RDB
를 활용했습니다. 누군가가 블로그에 글을 적게되면 글에 포함되어 있는 키워드를 모두 추출합니다. 그럼 '이 블로그는 ㅇㅇ과 ㅁㅁ라는 단어를 포함하고 있어'라고 알게 됩니다. 이 단어와 블로그의 연관성을 데이터베이스의 레코드로서 저장해 둡니다.
다만 이 방식은 확장성 측면에서 낙제점을 받았습니다. 레코드 수가 너무 많아져 검색 시간이 점점 늘어나는 문제를 겪게 됩니다.
그래서 취한 방법은 검색엔진을 만들어 검색함으로 문제를 회피한다. 라는 것이였습니다.
검색 엔진은 다음과 같은 기능을 제공합니다.
하테나의 검색 엔진은 개인이 북마크한 개인 데이터로부터 검색하는 시스템입니다.
사용자별로 검색 인덱스를 따로 준비해두고 이를 갱신합니다. 검색할 때는 해당 사용자의 인덱스에서만 검색합니다. 이렇게 인덱스를 타면 검색 시스템을 컴팩트하게 구현할 수 있고, 원하는 요구도 만족할 수 있습니다.
위의 검색엔진의 기능을 구현하기 위해서 해야할 것은 매우 많습니다.
처음에 검색할 대상 문서를 가져와야합니다. 이르 일반적으로 크롤링
이라고 합니다. 대상 문서가 웹에 있다면 웹 크롤러를 만들어서 대량 문서를 가져오는 작업이 필요합니다.
그 다음에는 가져온 문서를 어떻게 저장
할 것인가 라는 문제가 있습니다. 데이터 베이스의 분산도 생각해야 합니다.
그리고 가져온 문서로부터 인덱스
를 구축합니다. 인덱스를 타야지 검색이 빨라지기에 검색엔진에서 인덱스는 필수입니다.
검색에서는 검색 쿼리를 포함하는 문서가 검색결과로 반환되는데, 그 순서를 지정하는 스코어링
도 중요합니다.
전문 검색의 아키텍처에는 종류가 꽤나 많습니다. 저자는 책에서 3가지의 유형을 소개합니다.
grep
형은 검색 대상 문서를 처음부터 전부 읽어가는, 말하자면 가장 단순한 구조입니다.
전부 읽어가면 어딘가에서는 해당 문서가 발견되기 때문입니다. 검색 대상인 텍스트 길이를 m
, 검색어의 길이를 n
이라고 했을 때 이는 O(mn)
만큼 걸리게 됩니다.
물론 KMP
법, BM(Boyer-Moore)
법 등 어느정도 계산량을 개선한 방법을 활용하면 이보다 줄어들 수 있지만, 데이터가 늘어나면 힘겨워 질 것이라 상상할 수 있을 것입니다.
O(m+n)
O(mn)
, 최선 O(n/m)
단순 무식하지만 사실 좋은점도 많이 있습니다. 이는 즉시성이 좋으며 검색 누락이 없습니다. 또한 병렬화하기가 매우 간단합니다.
정리하자면 이는 검색 대상을 처음부터 전부 읽으며, 즉시성이 좋고, 검색누락이 없으며, 병렬화나 쿼리 확장이 용이합니다.
Suffix형
은 검색 대상 전문을 검색 가능한 형태로 가지고 있는 것입니다. 데이터 구조로써는Trie
,Suffix Array/Tree
를 생각할 수 있습니다.
Suffix Array란??
이는 접미사 배열이라고도 하며 어떤 문자열 S의 모든 접미사를 사전순으로 정렬해 둔 것입니다.
물론 이 말이 곧이 곧대로 접미사들을 문자열 배열에 저장하면 문자열 길이의 제곱에 비례하는 메모리가 필요하기 때문에, 대개 접미사 배열은 각 접미사의 시작 위치를 담는 정수 배열로 구현됩니다.
아래는 'alohomora'라는 단어의 Suffix Array
를 나타냅니다.
i | A[i] | S[A[i]···] |
0 | 8 | a |
1 | 0 | a l o h o m o r a |
2 | 3 | h o m o r a |
3 | 1 | l o h o m o r a |
4 | 5 | m o r a |
5 | 2 | o h o m o r a |
6 | 4 | o m o r a |
7 | 6 | o r a |
8 | 7 | r a |
alohomora
의 Suffix Array
는 803152467
을 의미합니다.
접미사 배열을 이용하면 문자열 검색을 최적화할 수 있습니다.
접미사 배열을 이용한 문자열 검색은 짚더미 H가 바늘 N을 포함한다면 항상 N은 H의 어떤 접미사의 접두사라는 점을 이용합니다.
모든 부분 문자열에 대해 이 속성이 성립함을 알 수 있습니다. 이 속성을 이용하면 H의 접미사 배열을 이진 탐색해서 각 문자열이 출현하는 위치를 찾을 수 있다.
접미사 배열의 길이는 항상 |H|
이므로 이진 탐색의 내부는 O(log|H|)
번 수행됩니다. 각 문자열 비교에 O(|N|)
시간이 걸리기 때문에 이 이진 탐색의 수행 시간은 O(|N|lg|H|)
이 됩니다.
Suffix형
은 문서를 전부 메모리에 올릴 수 있는 형태로 만들고 이를 통해 빠르게 검색합니다. 이 또한 데이터가 커지며 이론적으로는 검색이 가능해진다는 것을 알지만, 실제 이 구조를 가진 엔진은 좀처럼 구현하기 흼듭니다.
역 인덱스형은 주류로 사용되는 검색 기법이며, 간단히 말하자면 단어와 문서를 연관짓는 인덱스를 활용하는 것 입니다.
역 인덱스 방식의 특징으로는 역 인덱스를 문서와는 별개로 만들어야 한다는 점입니다. 즉, 검색하기 전에 인덱스를 전처리로 만들어야 합니다.
이러한 특징 때문에 즉시성의 측면에서는 뛰어나지 못합니다. 구현방법에 따라서는 검색누락이 생길 수도 있습니다.
하지만 이는 실제로 인덱스를 압축함으로써 컴패트하게 가져갈 수 있고, 대규모화하기도 쉽습니다.
Inverted index
를 그대로 번역하여 역 인덱스라고 부릅니다.참고) Elastic Search
의 경우도 내부에 역인덱스를 구성하여 검색 속도를 최적화합니다.
역 인덱스의 내부구조는 크게 Dictionary
와 Postings
라는 두 파트로 나뉩니다.
예제를 통해 무엇을 의미하는지 알아봅시다.
doc1 : 하테나의 마스코트인 시나몬은 도쿄에 없다.
doc2 : 홋카이도에 살았던 kurain
doc3 : 하테나교토의 시나몬에는 식숙한 kurain
doc4 : 하테나는 창업 9년째인 벤처다.
doc5 : 도치키현의 명물은 딸기다.
하테나 -> 1, 3, 4
시나몬 -> 1, 3
교토 -> 3
도쿄 -> 1
홋카이도 -> 2
kurain -> 2, 3
단어의 집합을 Dictionary
라고 합니다. 그리고 각 단어를 포함하고 있는 문서는 몇 번인지를 나타낸 것이 바로 Postings
입니다.
이것이 바로 역 인덱스 입니다. = Dictionary
+ Postings
'교토'로 검색하면 3번만 포함되어 있다거나, 인덱스를 보면 바로 어떤 문서에 어떤 단어가 포함되어 있는지 알 수 있습니다.
모든 단어에 대해 Dictionary
를 만드는 방법은 효율적이지 않습니다. 이에 따라 어떤 단어를 선택하는지에 대한 문제가 발생합니다.
또한 영어같은 경우는 공백으로 구분해서 문장이 쓰이므로 공백으로 구분하면 문서를 단어로 분해할 수 있습니다. 그러나 일본어의 경우 공백이 없고 단어의 분기점이 있는지 모른다는 문제가 있는데, 어떻게 활용하면 좋을까요?
사전을 활용하면 사전이 곧 검색 시스템의 단어공간이 됩니다. 즉 사전에 들어 있는 단어만 검색할 수 있습니다. 하테나의 경우는 자체키워드 40만개
+ 위키피디아의 단어 60만개
를 활용해 약 100만개
정도의 단어 검색을 지원합니다.
복숭아나 자두나 모두 복숭아과입니다.(일본의 말장난)
スモモモモモモモモモノウチ
라는 일본어 문장이 있을때 이 때 スモモ
, モモ
등으로 품사를 분해하는 것을 유형파악과 분리
라고 합니다. 이 원리에 따라 세세하게 나눈 각 단어를 형태소
라고 합니다.
유형파악과 분리
에 의해 형태소로 분할하는 것이 형태소 분석기의 주된 기능입니다.
형태소 분석기에 의해 분리된 형태소는 다음과 같습니다.
スモモモモモモモモノウチ
スモモ(자두) - 명사
モ(나) - 조사
モモ(복숭아도) - 명사
モ(도)) - 조사
モモ(복숭아과) - 명사
ノ(의) - 조사
ウチ(일부) - 명사
이와 같이 형태소 분석기는 문장을 형태소로 나눠서 그 품사를 추정합니다. 이런 분석기는 관련 패턴을 기계학습을 통해 학습시킵니다.
위와 같은 방식으로 단어를 지정할 수 있지만 그럼에도 검색누락이 발생할 수 있습니다.
예를 들어 Gears of War
이라는 게임 타이틀이 있습니다. 이에 따라 Gears 발매일
이라고 And
검색해보면 정확하게 Gears of War
발매일이 검색되게 됩니다.
그러나 Gear 발매일
이라고 검색했을 시 Metal Gear
라는 게임의 발매일이 검색되게 됩니다. 이는 Gear
라는 단어가 사전에 없고, Gears
라는 단어는 역인덱스로 Gears of War
로만 이어져 있음으로 검색되지 않는게 당연했습니다.
이러한 방식은 검색엔진의 설계나 사상에 좀 맞지 않는 것 같습니다.
집, 주택, 건물, 주거지
와 같은 동의어의 경우 또한 같이 검색되지 않으면 검색 누락이 발생할 수 있습니다.
n-gram
이란 텍스트를 n
자씩 잘라낸 것을 의미합니다.
abracadabra
의 3-gramabr
, bra
, rac
... 등등3글자 씩 잘라낸 것을 트라이그램
, 2글자 씩 잘라낸 것을 바이그램
이라고 하기도 합니다.
n-gram
이용해 연인덱스를 구성한다면 검색 누락을 방지할 수 있습니다.
하테나의 마스코트인 시나몬
-> '하테', '테나', '나의', '마스', '스코', '코트' ....
사용자가 '하테나'를 입력하면 '하테', '테나'로 분할하여 검색을 수행하고, 양쪽에 포함된 공통문서를 교집합(intersection
)하여 검색을 진행합니다.
이러한 방법은 지금까지도 이어지며 ElasticSearch - N-gram tokenizer에서도 확인 가능합니다.
동경소녀
를 검색했을 때 도쿄타워
가 검색된다면 난처할 것입니다. 따라서 보통 검색겨로가가 나온 후에 필터링
을 수행합니다. 실제 검색결과를 조사해서 동경소녀
라는 검색어가 포함되어 있는지 확인하는 것입니다.
grep
형과 동일한 검색시간이 소요되게 되므로 하테나에서는 제목, 코멘트, URL을 대상으로만 필터링을 수행하고, 본문은 n-gram
을 활용하지 않고 단어 기반을 사용합니다.
검색 엔진의 척도를 파악할 떄 쓰이는 단위를 의미합니다.
Postings
란 해당 단어를 포함하는 문서 번호 또는ID
를 지니고 있는 배열입니다.
Dictionary | Postings |
하테나 | 1, 3, 4 |
시나몬 | 1, 3 |
교토 | 3 |
도쿄 | 1 |
홋카이도 | 2 |
kurain | 2, 3 |
위의 예에서는 단순하게 문서ID
만을 보유하고 있는데, 단어가 해당 문서 내의 어느 위치에 출현하는지 그 출현위치도 저장하는 경우도 있습니다. 이것을 Full Inverted Index
라고 합니다.
출현 위치를 저장하지 않으면 처음에 본 것처럼 단어에 대응하는 문서ID
가 나열된 배열에 불가하므로 VB Code
를 활용해 압축할 수 있습니다.
더해 역 인덱스라는 것은 결국엔 key-value
스토어로 저장하기에 적합합니다. 따라서 O(1)
로 인덱스에 접근할 수 있습니다.
이상으로 전문 검색기술을 만들기 위한 이론과 구조를 알아봤습니다.
본 챕터에서 알아가면 좋을 것은
정도가 있을 것 같으며 책에서 설명한 대부분의 이론들 또한 ElaticSearch
에도 사용되고 있음으로 이또한 같이 알아두면 좋을 듯 합니다.