logo
작성일

CH5. Index Compression

Authors
  • avatar
    Name
    seren-wib
    Twitter
Contents

5. Index compression

5.1 Compression

색인을 왜 압축하냐 → 공간도 줄고 속도도 빨라짐

  1. 압축하는 이유 (일반)

    • 디스크 공간 절약 (돈 절약)
    • 메모리에 더 많이 올림 (속도 ↑)
    • 디스크 → 메모리 전송도 빨라짐: 압축된 걸 읽고 메모리에서 푸는 게 그냥 읽는 것보다 빠름
    • 전제: 푸는(decompression) 알고리즘이 빠름. 이 장에서 쓰는 건 다 빠름
  2. IR에서 압축하는 이유

    대상목표
    사전메모리에 통째로 올릴 만큼 작게
    포스팅디스크 공간 줄이기, 디스크 읽는 시간 줄이기
    • 큰 검색엔진은 포스팅도 상당 부분 메모리에 둠
  3. lossy vs lossless

    • lossy: 정보 일부를 버리는 압축. 예: 소문자화, 불용어 제거, Porter 어간 추출, 숫자 제거 (ch2 전처리가 사실상 lossy 압축)
    • lossless: 정보를 하나도 안 버리는 압축. 색인 압축은 주로 이것

5.2 Term statistics

압축 전에 term이 몇 개쯤 나오고(Heaps) 어떻게 분포하는지(Zipf) 알아야 함

  1. Reuters RCV1 (ch4 4.1이랑 같은 표)

    기호항목값
    N문서 수800,000
    L문서당 token 수200
    Mterm 수 (word type)400,000
    token당 바이트 (공백·구두점 포함)6
    token당 바이트 (빼고)4.5
    term당 바이트7.5
    Tnon-positional 포스팅 수100,000,000
  2. 전처리가 크기에 주는 영향 (Reuters)

    • 숫자 = 그 단계에서 줄어든 % / 누적 %
    단계사전 (term 수)non-positional 포스팅positional 포스팅 (token)
    처리 안 함484,494109,971,179197,879,290
    숫자 제거473,723 (−2 / −2)100,680,242 (−8 / −8)179,158,204 (−9 / −9)
    대소문자 통일391,523 (−17 / −19)96,969,056 (−3 / −12)179,158,204 (−0 / −9)
    불용어 30개391,493 (−0 / −19)83,390,443 (−14 / −24)121,857,825 (−31 / −38)
    불용어 150개391,373 (−0 / −19)67,001,847 (−30 / −39)94,516,599 (−47 / −52)
    어간 추출322,383 (−17 / −33)63,812,300 (−4 / −42)94,516,599 (−0 / −52)
    • 연습: non-positional vs positional 숫자가 왜 다르냐
      • 대소문자 통일 −3 vs −0, 어간 추출 −4 vs −0: The와 the가 한 문서에 있으면 포스팅(docID) 2개 → 1개로 합쳐짐. 근데 token 개수는 그대로라 positional은 안 줄어듦
      • 불용어 −14 vs −31, −30 vs −47: 불용어는 한 문서에 수십 번 나옴. non-positional은 문서당 1개만 빠지고, positional은 나온 횟수만큼 다 빠짐
    • 불용어 빼도 사전은 거의 안 줄어듦 (30개, 150개 빼봤자 term 수에 비하면 0%)
  3. Heaps' law: 어휘 크기 계산

    • 어휘에 상한이 있나? 없음. 길이 20짜리 단어만 해도 70²⁰ ≈ 10³⁷개 가능 → 컬렉션이 커지면 어휘도 계속 커짐
    • Heaps' law: 컬렉션 token 수로 term 수를 예측하는 경험 법칙
    • M = k · T^b (M = term 수, T = token 수), 보통 30 ≤ k ≤ 100, b ≈ 0.5
    • log-log 그래프에서 직선: log M = b · log T + log k (컬렉션 크기와 어휘 크기 사이 제일 단순한 관계)
    • Reuters: log₁₀M = 0.49 · log₁₀T + 1.64 → k = 10^1.64 ≈ 44, b = 0.49
    • 예: T = 1,000,020 → 44 × 1,000,020^0.49 ≈ 38,323 (실제 38,365) → 잘 맞음
  4. Heaps' law 연습: k, b 구해서 M 계산

    • 처음 10,000 token에 3,000 term, 1,000,000 token에 30,000 term
    • 나누기: 30,000 / 3,000 = (1,000,000 / 10,000)^b → 10 = 100^b → b = 0.5
    • 대입: 3,000 = k × 10,000^0.5 = k × 100 → k = 30
    • 페이지 2 × 10¹⁰개 × 200 token = T = 4 × 10¹² → M = 30 × √(4 × 10¹²) = 30 × 2 × 10⁶ = 60,000,000
    • 연습 1 (오타 포함 vs 자동 교정): 오타 포함하면 새 term이 계속 생겨서 M이 더 커짐 (k 또는 b가 커짐)
  5. Zipf's law: 빈도 계산

    • Heaps = 어휘가 얼마나 커지냐 / Zipf = 자주 나오는 term vs 드문 term이 몇 개냐
    • Zipf's law: i번째로 많이 나오는 term의 빈도가 1/i에 비례. cf_i ∝ 1/i (cf = 컬렉션 전체 등장 횟수)
    • cf₂ = cf₁ / 2, cf₃ = cf₁ / 3 … 예: the가 cf₁이면 of는 절반, and는 1/3
    • cf_i = c · i^k (k = −1) → log cf_i = log c − log i → log-log에서 기울기 −1 직선 (멱법칙, power law)
    • Reuters에선 직선에 딱 맞진 않음. 중요한 건 결론: 자주 나오는 term 몇 개, 드문 term 엄청 많음

5.3 Dictionary compression

사전을 메모리에 올리려면 고정 길이 칸의 낭비부터 없애야 함

  1. 사전을 왜 압축하냐

    • 사전은 포스팅보다 훨씬 작음. 그래도 메모리에 둬야 함
    • 다른 프로그램이랑 메모리 경쟁, 휴대폰·차량용 컴퓨터처럼 메모리 작은 기기, 빠른 시작 시간
  2. 고정 길이 배열 (ch3 3.1)

    • 한 줄 = term 20 + 빈도 4 + 포스팅 포인터 4 = 28바이트 → (20 + 4 + 4) × 400,000 = 11.2 MB
    • 문제
      • term 칸 대부분이 낭비: 1글자 term도 20바이트
      • 20글자 넘는 단어는 못 넣음 (hydrochlorofluorocarbons, supercalifragilisticexpialidocious)
      • 영어 term 평균 길이는 8글자 정도 → term당 평균 8글자만 쓰게 하려면?
  3. 문자열 방식 (dictionary as a string)

    • term을 전부 이어 붙인 긴 문자열 하나 + 각 줄에 term 시작 위치 포인터
    • 예: ...systilesyzygeticsyzygialsyzygyszaibelyiteszecin... / 다음 term 포인터 위치 = 지금 term 끝
    • 한 줄 = 빈도 4 + 포스팅 포인터 4 + term 포인터 3 + term 평균 8바이트(문자열 안)
    • term 포인터가 3바이트인 이유: 문자열 길이 8 × 400,000 = 3,200,000 → log₂(3,200,000) ≈ 21.6 < 24비트 = 3바이트
    • 400,000 × (4 + 4 + 3 + 8) = 7.6 MB (고정 길이 11.2 MB보다 작음)
  4. blocking (k = 4)

    • term 4개마다 포인터 1개만 둠, 대신 term 앞에 길이 1바이트씩 붙임. 예: 7systile9syzygetic8syzygial6syzygy11szaibelyite6szecin
    • 블록당: 전엔 포인터 4 × 3 = 12바이트 → 이젠 포인터 3 + 길이 4 × 1 = 7바이트 → 블록당 5바이트 절약
    • 전체: 400,000 / 4 블록 × 5 = 0.5 MB → 7.6 → 7.1 MB
    • 대가: 찾기가 조금 느림
    blocking 없음blocking (k = 4)
    찾는 법term마다 이진 탐색블록까지 이진 탐색 → 블록 안은 앞에서부터 차례로
    예 (8 term)aid, box, ... 전부 트리 노드[aid box den ex] [job ox pit win] 블록 2개
    속도빠름조금 느림
    • k를 키우면 공간은 더 줄고, 블록 안에서 훑는 시간은 늘어남
  5. front coding

    • front coding: 블록 안 term들이 같은 접두사를 가지면 접두사를 한 번만 쓰는 것 (정렬돼 있어서 이웃끼리 접두사가 자주 같음)
    • 8automata8automate9automatic10automation → 8automat*a1◇e2◇ic3◇ion
    • 읽는 법: 8automat*a = 길이 8, 접두사 automat 끝은 * → automata / 1◇e = 접두사 뒤에 1글자 e → automate / 2◇ic → automatic / 3◇ion → automation
    • 연습: 어떤 접두사를 쓸지, 트레이드오프는?
      • 접두사가 길면 term마다 아끼는 글자가 많지만, 그 접두사를 공유하는 term은 적음
      • 접두사가 짧으면 많은 term이 공유하지만 아끼는 글자가 적음
  6. 사전 크기 정리 (Reuters, M = 400,000)

    방식계산크기
    고정 길이 배열(20 + 4 + 4) × 400,00011.2 MB
    문자열 + term 포인터400,000 × (4 + 4 + 3 + 8)7.6 MB
    + blocking (k = 4)7.6 − 400,000 / 4 × 5바이트 = 7.6 − 0.57.1 MB
    + blocking + front coding5.9 MB

5.4 Postings compression

docID 대신 차이(gap)를 저장하고, 작은 gap엔 비트를 적게 쓰는 가변 길이 코드로

  1. 포스팅을 왜 압축하냐 / docID 하나 몇 비트

    • 포스팅 파일이 사전보다 최소 10배 큼 → 포스팅 하나(= docID 하나)를 작게 저장하는 게 핵심
    • 4바이트 정수 = 32비트 / 필요한 만큼만: log₂ 800,000 ≈ 19.6 → 20비트
    • 목표: 20비트보다 훨씬 적게
  2. gap 저장

    • 포스팅은 docID 오름차순 → 첫 docID + 앞 것과의 차이(gap)만 저장
    • 예: computer 283154, 283159, 283202 → 283154, 5, 43
    termdocIDgap
    the283042, 283043, 283044, 2830451, 1, 1
    computer283047, 283154, 283159, 283202107, 5, 43
    arachnocentric252000, 500100252000, 248100
    • 자주 나오는 term(the)은 gap이 작음 / 드문 term(arachnocentric)은 큼
  3. 가변 길이 인코딩 (variable length encoding)

    • 작은 gap엔 비트 적게, 큰 gap엔 많이 쓰는 인코딩
    • 목표: arachnocentric 같은 드문 term은 gap당 20비트쯤, the 같은 흔한 term은 gap당 몇 비트만
    • 방법 2가지: 바이트 단위 VB 코드 / 비트 단위 γ 코드
  4. VB 코드 (variable byte)

    VB 코드: 7비트씩 나누고 마지막 바이트만 c=1 / 감마 코드: 13 → 1101 → offset 101, 길이 3 → 1110 → 1110101
    • 상용·연구 시스템에서 많이 씀. 가변 길이이면서 바이트 경계(alignment)에 맞음
    • 바이트 하나 = continuation bit c 1개(맨 앞) + 숫자 7비트
    • 인코딩: 128로 계속 나눠서 나머지를 뒤에서부터 채움 (7비트씩) → 마지막 바이트만 c = 1, 나머지는 c = 0
    • 예: 824 = 6 × 128 + 56 → 00000110 10111000 / 5 → 10000101 / 214577 = 13·128² + 12·128 + 49 → 00001101 00001100 10110001
    • 디코딩: n = 128 × n + (7비트 값), c = 1인 바이트 만나면 숫자 하나 끝 → n = 0으로 다시
    • 연습: 130 = 1 × 128 + 2 → 00000001 10000010
    • 그림에서: 위 = VB (주황 c = 1 = 마지막 바이트)
    VBEncodeNumber(n)
    1 bytes <- < >
    2 while true
    3 do Prepend(bytes, n mod 128)
    4    if n < 128
    5      then Break
    6    n <- n div 128
    7 bytes[Length(bytes)] += 128
    8 return bytes
    
    VBDecode(bytestream)
    1 numbers <- < >
    2 n <- 0
    3 for i <- 1 to Length(bytestream)
    4 do if bytestream[i] < 128
    5      then n <- 128 * n + bytestream[i]
    6      else n <- 128 * n + (bytestream[i] - 128)
    7           Append(numbers, n)
    8           n <- 0
    9 return numbers
    
    • VBEncodeNumber: 128로 나눈 나머지를 앞에 붙이기 반복 → 마지막 바이트에 128 더함(= c 비트 1)
    • VBEncode: 숫자마다 VBEncodeNumber 해서 이어 붙임
    • VBDecode: 128보다 작으면(c = 0) 계속 누적, 128 이상이면(c = 1) 128 빼고 누적 → 숫자 하나 완성
  5. 다른 가변 코드

    • 바이트 말고 다른 단위(alignment)도 가능: 32비트(word), 16비트, 4비트(nibble)
    • 작은 gap이 많으면 바이트 단위는 낭비 → nibble이 나음
    • gap 여러 개를 word 하나에 몰아넣는 word-aligned 코드도 있음
  6. 감마(γ) 코드

    • 비트 단위 코드(bitlevel code): VB보다 더 압축됨. 제일 유명한 게 γ
    • unary: n → 1을 n개 + 0. 예: 3 → 1110, 40 → 1 40개 + 0
    • γ = 길이(unary) + offset
      • offset = 2진수에서 맨 앞 1 뗀 것: 13 → 1101 → 101
      • 길이 = offset 비트 수를 unary로: 3 → 1110
      • γ(13) = 1110,101
    • 예: 1 → 0 (offset 없음) / 2 → 10,0 / 3 → 10,1 / 4 → 110,00 / 9 → 1110,001 / 24 → 11110,1000 / 511 → 111111110,11111111 / 1025 → 11111111110,0000000001
    • 0은 γ 코드 없음 (gap은 1 이상이라 상관없음)
    • 연습: 130 = 10000010 → offset 0000010 (7비트) → 길이 7 → 11111110 → γ(130) = 11111110,0000010
    • 디코딩: 0 나올 때까지 1을 셈(= offset 비트 수) → 그만큼 읽고 → 앞에 1 붙여서 2진수 → 10진수
    • 그림에서: 아래 = 13 → 1101 → 앞 1 떼기 → 길이 3을 unary → 붙이기
  7. γ 코드 길이

    • offset = ⌊log₂G⌋비트, 길이 = ⌊log₂G⌋ + 1비트
    • 전체 = 2⌊log₂G⌋ + 1비트 → 항상 홀수. 예: 13 → 2 × 3 + 1 = 7비트
    • 최적 길이 log₂G의 2배 안에 들어옴 (gap 분포에 대한 가정이 대략 맞을 때)
  8. γ 코드 성질

    • prefix-free: 어떤 코드도 다른 코드의 앞부분이 아님 → 구분자 없이 이어 붙여도 디코딩 가능 (VB도 같음)
    • 최적의 3배 안 (가정 더 하면 2배 안). gap 분포와 상관없이 성립
    • universal: 어떤 분포에도 쓸 수 있음
    • parameter-free: 정할 파라미터 없음
  9. γ vs VB: 정렬(alignment)

    • 컴퓨터는 8, 16, 32비트 경계로 다룸 → 비트 단위로 압축·조작하면 느릴 수 있음
    • VB는 바이트 경계에 맞아서 더 빠를 수 있음
    VBγ
    단위바이트비트
    크기116 MB (Reuters)101 MB (더 작음)
    속도빠름 (정렬됨)느릴 수 있음
    구현단순복잡
    • 공간 조금 더 쓰는 대신 VB가 개념적으로 단순함
  10. 크기 계산 (Reuters)

    자료계산크기 (MB)
    사전, 고정 길이11.2
    사전, 문자열 + term 포인터7.6
    + blocking (k = 4)7.1
    + blocking + front coding5.9
    컬렉션 (텍스트 + xml)3,600
    컬렉션 (텍스트만)960
    term-문서 incidence matrix400,000 × 800,000비트 ÷ 840,000
    포스팅, 압축 X (32비트)1억 × 4바이트400
    포스팅, 압축 X (20비트)1억 × 2.5바이트250
    포스팅, VB116
    포스팅, γ101
    • 색인 = 컬렉션 텍스트의 10~15% (위치·빈도 정보 빼고 계산한 거라 실제론 덜 줄어듦)