http://cislmu.org 와 학교 수업을 토대로 정리합니다.






4장에서 배울 핵심 개념은 아래와 같다.
정보 검색의 많은 설계 과정은 HW 제약 조건에 따라 이루어진다.
정보 검색을 설계하면서 제일 먼저, 필요한 하드웨어 기본 사항을 검토해야 한다.
🔔 왜 하나의 큰 chunk를 전송하는 게 여러 개의 작은 chunk를 전송하는 것보다 빠를까?
=> 작은 chunk들을 여러 번 읽을 때마다 디스크 헤드를 이동시켜야 하므로 seek time이 반복적으로 발생하는데, 하나의 큰 chunk를 읽을 때는 seek time이 한 번만 발생한다.

정보 검색 시스템에서 사용되는 HW의 주요 특성들에 대한 일반적인 값들
RCV1: 정보 검색 및 텍스트 분류 연구에 사용되는 벤치마크 데이터셋
확장 가능한 index 구축 알고리즘을 적용하는 예시로서 Reuters RCV1 컬렉션을 사용할 것이다.
RCV1은 1995~1996년 Reuters에서 배포한 영어 뉴스 기사들을 수집한 것이다.

이런 영어 뉴스 기사들을 모아둔 것이다.

term의 평균 빈도는? 4.5
term 당 바이트 수는 7.5인데, 왜 word type 당 바이트 수는 4.5인가? => 각 용어가 여러 문서에서 반복적으로 등장하기 떄문
positional posting은 몇 개인가? =>
Claude가 계산해주었다.
전체 non-positional posting의 수 (T) = 1억 개 (100,000,000)
문서의 수 (N) = 800,000
문서 당 평균 토큰 수 (L) = 200
positional posting의 수는 각 문서 내에서 용어의 위치 정보까지 포함하므로, 대략 문서 당 평균 토큰 수에 비례할 것입니다.
positional posting의 수 = non-positional posting의 수 (T) × 문서 당 평균 토큰 수 (L)
= 100,000,000 × 200
= 20,000,000,000 (200억 개)
따라서, positional posting의 수는 대략 200억 개 정도로 예상할 수 있습니다. 이는 non-positional posting에 비해 용어 위치 정보를 추가로 저장해야 하므로 인덱스의 크기가 상당히 커짐을 의미합니다.
🔔 Non-positional posting vs Positional posting
전자는 특정 term을 포함한 문서의 ID만을 반환한다.
후자는 ID와 함께 문서 내에서의 위치 정보까지 저장한다.
저 알고리즘이 매우 큰 컬렉션으로 확장되지 않는 이유는 무엇인가?
3가지 정도의 이유가 있다.
인덱스 크기의 급격한 증가
: 문서 수가 증가함에 따라 inverted index의 크기도 빠르게 증가한다.
특히 positional posting을 사용할 경우, 인덱스 크기는 non-positional posting에 비해 문서 당 평균 토큰 수에 비례하여 증가한다.
질의 처리 시간 증가: 인덱스 크기가 커짐에 따라 질의 처리 시간도 증가한다.
인덱스 구축 시간 증가: 문서 수가 많아질수록 인덱스를 구축하는 데 걸리는 시간도 증가한다.

1장에서부터 계속 봤던 inverted index이다.
BSBI 알고리즘을 공부하여 inverted index를 구축하는 것이 목표이다.

리스트를 만들면 그만인 게 아니라, 정렬을 무조건 해야 한다.
index를 만들 때, 문서를 한 번에 하나씩 파싱한다.
그리고 어떤 term에 대한 최종 postings는 전체 문서 세트를 처리하기 전까지는 완전하지 못하다. (어떤 용어에 대한 모든 출현 정보를 알 수 없기 때문)
모든 게시물을 메모리에 보관한 다음 마지막에 메모리 내에서 정렬할 수 있을까?
-> 대규모 컬렉션이면 당연히 불가능하다.
그래서 중간 결과를 디스크에 저장해야 한다.
메모리 대신 디스크를 쓰면, 위같은 index construction 알고리즘을 똑같이 사용할 수 있을까?
이것도 안된다. 왜냐하면 매우 큰 레코드 세트를 디스크에서 정렬하는 건 너무 느리기 때문이다. (디스크 검색 횟수가 너무 많다.)
따라서 외부 정렬 알고리즘이 필요하다.
외부 정렬 알고리즘은 대량의 데이터를 정렬할 때 사용되며, 메모리에 한 번에 로드할 수 없을 만큼 큰 데이터를 정렬해야 할 때 쓰인다.
예를 들어, T = 100,000,000 개의 non-positional postings를 정렬해야 한다고 하자. 각각의 posting은 12 bytes의 크기를 가진다. (termID 4 + docID 4 + term frequency 4)
그리고 저 1억 개를 나누어 담을 block을 정의해야 한다.
각각의 block은 10,000,000 개의 posting으로 구성된다.
이는 메모리에 쉽게 로드할 수 있는 양이고, RCV1에 대해 총 10개의 블록이 있다고 한다.
알고리즘의 기본 아이디어는 아래와 같다.
각 블록에 대해 아래 단계를 수행한다.
1. postings를 메모리에 축적한다.
2. 메모리에서 postings를 정렬한다.
3. 정렬된 block을 디스크에 쓴다.
그 후 이 블록들을 하나의 긴 정렬된 순서로 병합한다.

각 블록들을 메모리상에서 정렬한 후, 병합하여 disk에 쓴다.

알고리즘은 사진과 같다.
각 블록에 대해 BSBI-INVERT를 수행하고, DISK에 쓰는 작업을 반복한 후, 정렬된 블록을 병합한다.
그런데 BSBI에도 문제가 있다.
딕셔너리를 메모리에 유지해야 한다는 가정이 있어야 한다.
term-termID 매핑을 구현하려면 동적으로 확장 가능한 사전이 필요하다.
사실, 우리는 termID, docID postings 대신 term, docID postings를 활용하고 있다.
그런데 이렇게 하면 중간 파일이 정말 커지게 된다.
=> 확장은 하지만, 매우 느린 인덱스 구성 방법을 사용하게 된다.
줄여서 SPIMI라고 부른다.
핵심 아이디어가 2가지 있다.
1. 각 블록에 대해 별도의 사전을 생성하면, 블록 간 termID 매핑을 유지할 필요가 없다.
2. 정렬하지 않는다. 게시글 목록에 게시글이 발생하는 대로 누적한다.
이 두 가지 아이디어로 각 블록에 대해 완전한 inverted index를 생성할 수 있다.
그런 다음 이러한 개별 인덱스를 하나의 큰 인덱스로 병합할 수 있다.
여기까지는 무슨 소리인지 싶다.

알고리즘은 위와 같다.
여기서 블록을 병합하는 과정은 BSBI와도 유사하다.
Compression(압축)은 SPIMI를 더욱 효율적으로 만든다.
자세한 건 5장에서 다룬다.
웹 스케일 인덱싱의 경우, 분산 컴퓨터 클러스터를 사용해야 한다.
개별 컴퓨터는 결함이 발생하기 쉽다. -> 예기치 않게 속도가 느려지거나 실패할 수 있다.
이러한 컴퓨터의 풀을 어떻게 활용할 수 있을까?
=> 인덱싱 작업을 지시하는 마스터 머신을 유지한다.
이는 안전한 것으로 간주된다.
인덱싱을 parallel tasks(병렬 작업) 세트로 나눈다.
마스터 머신은 풀에서 idle machine(유휴 머신)에 각 작업을 할당한다.
예시로 구글 데이터 센터를 들어보자.
구글 데이터 센터에는 상용 머신들이 있다.
그리고 이러한 데이터 센터는 전 세계에 분산되어 있다.
1백만 대의 서버와 3백만 개의 프로세서/코어가 존재한다고 한다. 지금은 2024년이니 당연히 배로 있을 것이다.
구글은 매 분기마다 100,000대의 서버를 설치한다고 한다.
이는 연간 2억~2억 5천만 달러의 지출을 차지한다고 하고, 전 세계 컴퓨팅 용량의 10%를 차지한다.
1000개의 노드가 있는 무중단 시스템에서 각 노드의 가동률이 99.9%인 경우, 시스템의 가동률은 얼마인가? (고장을 허용하지 않는다고 할 때.)
-> 37%이다. 99.9^1000 하면 0.37이 나온다.
서버가 1백만 대 설치되어 있는 경우, 3년마다 한 대의 서버가 고장난다고 할 때, 시스템 장애 간격은 얼마나 되는가?
-> 2분 미만이다.
이번에도 클로드가 계산해주었다.
서버의 수명이 3년이라는 것은 한 대의 서버가 고장 나는 데 평균 3년이 걸린다는 의미입니다.
100만 대의 서버가 있으므로, 전체 시스템에서는 (1,000,000 / 3) 대의 서버가 매년 고장 납니다.
연간 고장 나는 서버 수 = 1,000,000 / 3 = 333,333대
일 년은 약 525,600분입니다. 따라서 서버 고장 간격은 다음과 같이 계산할 수 있습니다.
서버 고장 간격 = 525,600분 / 333,333대 ≈ 1.58분
따라서 100만 대의 서버가 설치된 경우, 평균적으로 2분 미만의 간격으로 서버 고장이 발생하게 됩니다.
두 세트의 parallel task를 정의하고 이를 해결하기 위해 두 가지 유형의 머신을 배포할 것이다.
우선 입력 문서 컬렉션을 분할로 나눈다.
(BSBI/SPIMI의 블록에 해당)
각 split(분할)은 문서의 하위 집합이다.
Master의 역할:
마스터 머신은 한 split을 하나의 유휴 상태 parser 머신에 할당한다.
Parser의 처리 과정:
할당받은 Parser(구문 분석기)는 한 번에 문서 하나를 읽고, 그 문서 내에서 (term, docID) 쌍을 생성한다. 이 쌍은 검색 색인에 사용될 수 있는 기본 데이터 요소이다.
Parser의 출력 분할:
생성된 (term, docID) 쌍은 'j'개의 용어 파티션으로 작성된다.
각 파티션은 특정 범위의 용어의 첫 글자를 기준으로 분할된다.
예를 들어 j=3이라고 하면, a-f, g-p, q-z 이렇게 3 개의 파티션으로 나누어진다.
parser에 의해 생성된 term-partition 쌍을 수집하고, 특정 용어 파티션에 대해 이들을 정렬하고, postings lists로 작성한다.
한 term partition은 특정 알파벳 범위의 용어들을 의미한다. (a-f 등)

지금까지 설명한 index construction 알고리즘은 MapReduce의 인스턴스이다.
MepReduce는 분산 컴퓨팅을 위한 강력하고 개념적으로 간단한 프레임워크이다. 배포 시 코드를 더 작성할 필요도 없다.
당장 Google indexing system(2002년경)의 각 단계도 MapReduce로 구현되었다.
그리고 Index construction 말고, term-partitioned 인덱스를 document-partitioned 인덱스로 변환하는 단계도 있다.
왜 document-partitioned index가 더 나을까?

MapReduce는 map, reduce 함수로 구성된다.
MapReduce 색인 구축의 과정:
Map 함수: Parser에 해당
입력으로 웹 컬렉션(문서들의 집합)을 받습니다.
각 문서를 처리하며 (용어, 문서ID) 쌍을 생성합니다.
예를 들어, 문서 d1에서 "C CAME", 문서 d2에서 "C DIED"라는 텍스트가 있다면, 이를 ((C, d2), (DIED, d2), (C, d1), (CAME, d1))와 같이 (용어, 문서ID) 쌍으로 매핑합니다.
Shuffling:
Map 함수의 출력이 셔플링 과정을 거쳐 같은 용어를 가진 쌍들이 reduce 함수로 전달되도록 합니다.
Reduce 함수: Inverter에 해당
map 함수의 출력에서 같은 용어를 키로 가진 (용어, 문서ID) 쌍의 리스트를 받습니다.
이 리스트를 용어별로 정렬하고, 각 용어에 대한 게시물 목록(postings lists)으로 변환합니다.
예를 들어, ((C, (d2, d1)), (DIED, (d2)), (CAME, (d1)))과 같은 출력을 생성합니다. 여기서 C는 두 문서 d1, d2에서 발견되었고, DIED는 d2에서, CAME는 d1에서 발견되었습니다.
예시에서:
"C"라는 용어는 두 문서 d1, d2에 나타났으며, 이에 대한 postings list는 (d2, d1)이 됩니다.
"DIED"라는 용어는 문서 d2에 나타났으며, 이에 대한 postings list는 (d2)입니다.
"CAME"라는 용어는 문서 d1에 나타났으며, 이에 대한 postings list는 (d1)입니다.
지금까지는 컬렉션이 정적이라고 가정하였다.
그런데 사실 컬렉션은 정적인 경우가 거의 없다.
문서들이 삽입, 삭제, 수정되는 경우가 많기 때문이다.
즉, 딕셔너리와 postings lists는 동적으로 수정되어야 한다.
가장 간단한 접근법은 아래와 같다.
메인 인덱스와 보조 인덱스 간에는 당연히 문제가 생긴다.
잦은 병합도 문제가 될 수 있고,
병합 기간 동안의 낮은 검색 성능도 문제가 있다.
이를 해결하기 위해 Logarithmic merge라는, 좀 더 복잡한 것을 도입한다.
로그라믹 결합은 index를 효율적으로 관리하는 방법 중 하나이다.
비용 분산: index 병합의 비용을 시간에 걸쳐 분산하므로, 단일 병합 작업으로 인한 시스템 부담을 줄일 수 있다.
색인 시리즈 유지: 여러 개의 색인을 유지하고, 각 색인은 이전 색인보다 크기가 두 배인 시리즈를 형성한다.
메모리 내 최소 색인 유지: 가장 작은 색인(Z0)을 메모리에 유지하여 빠른 업데이트와 접근을 가능하게 한다.
만약 Z0이 너무 크면, 디스크에 I0 이런 식으로 저장한다.
이미 디스크에 I0가 존재한다면, Z0와 I0를 병합하여 I1을 만든다.

LMERGEADDTOKEN
Z0에 새 토큰을 병합합니다. Z0는 메모리 내의 인덱스입니다.
만약 Z0의 크기가 특정 임계값 n에 도달하면, 다음 단계로 넘어갑니다.
무한 루프를 시작하며, 모든 인덱스 Ii에 대해 다음 작업을 수행합니다.
만약 현재 인덱스 Ii가 존재한다면 (즉, indexes 집합에 있다면),
현재 인덱스 Ii와 Zi를 병합하여 새로운 임시 인덱스 Zi+1를 생성합니다. 이 인덱스는 디스크 상에 저장됩니다.
Zi+1가 생성되었으므로, 이 인덱스를 indexes 집합에서 제거합니다.
만약 현재 인덱스 Ii가 존재하지 않는다면 (즉, 처음 병합하는 경우),
Ii를 현재 메모리 내 인덱스 Zi로 대체합니다.
Ii를 indexes 집합에 추가합니다.
병합을 중단하고 루프를 탈출합니다.
Z0를 비웁니다 (모든 토큰을 처리했으므로).
LOGARITHMICMERGE
Z0를 비웁니다; 이것은 메모리 내 색인입니다.
indexes 집합을 비웁니다; 이것은 디스크 상의 모든 색인을 추적합니다.
계속해서 진행되는 메인 루프를 시작합니다.
LMergeAddToken 함수를 호출합니다; 이 함수는 새로운 토큰을 처리하고 필요한 병합을 수행합니다.
이 알고리즘은 메모리 내의 인덱스가 커질 때마다 디스크에 저장된 색인과 병합하여, 색인의 크기가 지수적으로 증가하는 것을 방지하고, 인덱스의 크기가 관리 가능한 수준으로 유지된다.
이 방식은 인덱스의 병합 비용을 시간에 걸쳐 분산시키고, 시스템의 응답성을 유지하는데 도움을 준다.

예를 들어 보조 색인이 크기 a를 가지고 있다면 사진같은 결론에 도달할 수도 있다.

그러므로 logarithmic merge가 보조 인덱스 방식보다 훨씬 더 효율적이다.
거대한 검색 엔진에서 동적 인덱싱은 어떻게 될까?
중간 데이터 구조가 크다는 점을 제외하면 기본적으로 동일한 문제이다.