20. 검색 시스템

20. 검색 시스템

난이도: ⭐⭐⭐⭐

개요

검색(Search)은 대부분의 애플리케이션에서 기본적인 구성 요소입니다. 이 레슨에서는 검색 엔진이 내부적으로 어떻게 작동하는지 다룹니다 — 역색인(inverted index)과 텍스트 분석부터 분산 검색 아키텍처 및 랭킹 알고리즘까지. 가장 널리 채택된 솔루션인 Elasticsearch를 중심으로 설명합니다.


목차

  1. 검색 엔진의 기초
  2. 역색인
  3. Elasticsearch 아키텍처
  4. 인덱싱과 매핑
  5. 검색 쿼리
  6. 랭킹과 관련성
  7. 검색 시스템 확장
  8. 연습 문제

1. 검색 엔진의 기초

1.1 검색 시스템 구성 요소

┌─────────────────────────────────────────────────────────────────┐
│              Search System Architecture                          │
│                                                                 │
│  ┌────────────┐     ┌────────────┐     ┌────────────────────┐   │
│  │  Crawling / │     │  Indexing   │     │  Query Processing  │   │
│  │  Ingestion  │────▶│  Pipeline   │────▶│  & Retrieval       │   │
│  └────────────┘     └────────────┘     └────────────────────┘   │
│       │                   │                      │              │
│       ▼                   ▼                      ▼              │
│  ┌────────────┐     ┌────────────┐     ┌────────────────────┐   │
│  │ Fetch docs  │     │ Tokenize   │     │ Parse query        │   │
│  │ Extract text│     │ Analyze    │     │ Match documents    │   │
│  │ Normalize   │     │ Build index│     │ Rank results       │   │
│  │ Detect lang │     │ Store      │     │ Highlight          │   │
│  └────────────┘     └────────────┘     └────────────────────┘   │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

1.2 전문 검색 vs 데이터베이스 검색

┌──────────────────────┬────────────────────┬──────────────────────┐
                       Database (SQL)      Search Engine (ES)   
├──────────────────────┼────────────────────┼──────────────────────┤
 Primary use           Transactional       Search & analytics   
 Data model            Structured rows     Semi-structured docs 
 Query language        SQL                 DSL (JSON)           
 Text matching         LIKE / FTS          Full-text + facets   
 Relevance scoring     Limited             Advanced (BM25)      
 Horizontal scaling    Complex (sharding)  Native (shards)      
 Real-time updates     ACID guarantees     Near real-time       
 Aggregations          GROUP BY            Rich aggregations    
 Consistency           Strong              Eventual             
└──────────────────────┴────────────────────┴──────────────────────┘

2. 역색인

2.1 작동 원리

┌─────────────────────────────────────────────────────────────────┐
              Inverted Index                                      
                                                                 
  Documents:                                                     
  Doc1: "the quick brown fox"                                    
  Doc2: "the lazy brown dog"                                     
  Doc3: "the quick dog jumps"                                    
                                                                 
  Forward Index:                    Inverted Index:              
  Doc1  [the, quick, brown, fox]  brown  [Doc1, Doc2]         
  Doc2  [the, lazy, brown, dog]   dog    [Doc2, Doc3]         
  Doc3  [the, quick, dog, jumps]  fox    [Doc1]               
                                   jumps  [Doc3]               
                                   lazy   [Doc2]               
                                   quick  [Doc1, Doc3]         
                                   the    [Doc1, Doc2, Doc3]   
                                                                 
  Query: "quick brown"                                           
  quick  {Doc1, Doc3}                                           
  brown  {Doc1, Doc2}                                           
  Intersection: {Doc1}   AND query result                      
  Union: {Doc1, Doc2, Doc3}  OR query result                   
                                                                 
└─────────────────────────────────────────────────────────────────┘

2.2 텍스트 분석 파이프라인

┌─────────────────────────────────────────────────────────────────┐
│              Text Analysis Pipeline                              │
│                                                                 │
│  Input: "The Quick Brown Foxes are JUMPING!"                    │
│                                                                 │
│  1. Character Filters                                           │
│     ├── HTML strip: remove <tags>                               │
│     └── Pattern replace: normalize chars                        │
│     Result: "The Quick Brown Foxes are JUMPING!"                │
│                                                                 │
│  2. Tokenizer                                                   │
│     ├── Standard: split on word boundaries                      │
│     └── Result: ["The", "Quick", "Brown", "Foxes", "are",      │
│                  "JUMPING"]                                     │
│                                                                 │
│  3. Token Filters                                               │
│     ├── Lowercase: "the", "quick", "brown", "foxes", ...       │
│     ├── Stop words: remove "the", "are"                         │
│     ├── Stemmer: "foxes" → "fox", "jumping" → "jump"           │
│     └── Result: ["quick", "brown", "fox", "jump"]              │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

2.3 위치를 포함한 포스팅 리스트

┌─────────────────────────────────────────────────────────────────┐
│              Posting List with Term Frequency & Positions        │
│                                                                 │
│  Term: "database"                                               │
│  ┌─────────────────────────────────────────────────────┐        │
│  │ Doc1: tf=3, positions=[5, 12, 45]                   │        │
│  │ Doc3: tf=1, positions=[8]                           │        │
│  │ Doc7: tf=5, positions=[2, 10, 15, 30, 42]          │        │
│  └─────────────────────────────────────────────────────┘        │
│                                                                 │
│  Uses:                                                          │
│  • tf (term frequency) → relevance scoring                     │
│  • positions → phrase queries, proximity queries                │
│  • doc frequency → IDF calculation                              │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

3. Elasticsearch 아키텍처

3.1 클러스터 아키텍처

┌─────────────────────────────────────────────────────────────────┐
│              Elasticsearch Cluster                               │
│                                                                 │
│  Cluster: "production"                                          │
│  ┌────────────────────────────────────────────────────────┐     │
│  │  Node 1 (Master + Data)                                │     │
│  │  ┌──────────┐ ┌──────────┐ ┌──────────┐               │     │
│  │  │ Shard 0  │ │ Shard 2  │ │Replica 1 │               │     │
│  │  │ (Primary)│ │ (Primary)│ │          │               │     │
│  │  └──────────┘ └──────────┘ └──────────┘               │     │
│  └────────────────────────────────────────────────────────┘     │
│  ┌────────────────────────────────────────────────────────┐     │
│  │  Node 2 (Data)                                         │     │
│  │  ┌──────────┐ ┌──────────┐ ┌──────────┐               │     │
│  │  │ Shard 1  │ │Replica 0 │ │Replica 2 │               │     │
│  │  │ (Primary)│ │          │ │          │               │     │
│  │  └──────────┘ └──────────┘ └──────────┘               │     │
│  └────────────────────────────────────────────────────────┘     │
│  ┌────────────────────────────────────────────────────────┐     │
│  │  Node 3 (Data)                                         │     │
│  │  ┌──────────┐ ┌──────────┐ ┌──────────┐               │     │
│  │  │Replica 0 │ │Replica 1 │ │Replica 2 │               │     │
│  │  └──────────┘ └──────────┘ └──────────┘               │     │
│  └────────────────────────────────────────────────────────┘     │
│                                                                 │
│  Index: "products" → 3 primary shards, 1 replica each          │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

3.2 인덱스, 샤드, 세그먼트

┌─────────────────────────────────────────────────────────────────┐
│              Index → Shard → Segment                             │
│                                                                 │
│  Index (logical)                                                │
│  ├── Shard 0 (Lucene index)                                    │
│  │   ├── Segment 1 (immutable)                                 │
│  │   │   ├── Inverted index                                    │
│  │   │   ├── Stored fields                                     │
│  │   │   └── Doc values (columnar)                             │
│  │   ├── Segment 2 (immutable)                                 │
│  │   └── Segment 3 (immutable)                                 │
│  ├── Shard 1                                                   │
│  │   └── ...                                                   │
│  └── Shard 2                                                   │
│      └── ...                                                   │
│                                                                 │
│  New docs → in-memory buffer → refresh (1s) → new segment      │
│  Segments merge periodically (background)                      │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

3.3 쓰기 및 읽기 경로

┌─────────────────────────────────────────────────────────────────┐
│  Write Path:                                                    │
│                                                                 │
│  Client ──▶ Coordinating Node ──▶ Primary Shard                │
│                                       │                        │
│                                       ├──▶ In-memory buffer    │
│                                       ├──▶ Translog (WAL)      │
│                                       └──▶ Replica Shards      │
│                                                                 │
│  Refresh (every 1s): buffer → searchable segment               │
│  Flush (every 30m):  translog → disk, clear translog           │
│                                                                 │
├─────────────────────────────────────────────────────────────────┤
│  Read Path:                                                     │
│                                                                 │
│  Client ──▶ Coordinating Node                                  │
│                    │                                            │
│            ┌───────┼───────┐                                    │
│            ▼       ▼       ▼                                    │
│         Shard 0  Shard 1  Shard 2  (scatter)                   │
│            │       │       │                                    │
│            └───────┼───────┘                                    │
│                    ▼                                            │
│           Merge & rank results (gather)                         │
│                    │                                            │
│                    ▼                                            │
│                 Client                                          │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

4. 인덱싱과 매핑

4.1 인덱스 생성

PUT /products
{
  "settings": {
    "number_of_shards": 3,
    "number_of_replicas": 1,
    "analysis": {
      "analyzer": {
        "product_analyzer": {
          "type": "custom",
          "tokenizer": "standard",
          "filter": ["lowercase", "english_stemmer", "english_stop"]
        }
      },
      "filter": {
        "english_stemmer": {
          "type": "stemmer",
          "language": "english"
        },
        "english_stop": {
          "type": "stop",
          "stopwords": "_english_"
        }
      }
    }
  },
  "mappings": {
    "properties": {
      "name": {
        "type": "text",
        "analyzer": "product_analyzer",
        "fields": {
          "keyword": { "type": "keyword" },
          "suggest": {
            "type": "completion"
          }
        }
      },
      "description": {
        "type": "text",
        "analyzer": "product_analyzer"
      },
      "category": { "type": "keyword" },
      "price": { "type": "float" },
      "rating": { "type": "float" },
      "created_at": { "type": "date" },
      "in_stock": { "type": "boolean" },
      "tags": { "type": "keyword" }
    }
  }
}

4.2 필드 타입

┌────────────────┬──────────────────────────────────────────────────┐
 Type            Use Case                                         
├────────────────┼──────────────────────────────────────────────────┤
 text            Full-text search (analyzed, tokenized)            
 keyword         Exact match, sorting, aggregations                
 integer/long    Numeric values, range queries                     
 float/double    Decimal numbers                                   
 date            Timestamps, date math                             
 boolean         true/false filters                                
 object          Nested JSON (flattened internally)                
 nested          Array of objects (maintains relationships)        
 geo_point       Latitude/longitude                                
 completion      Auto-complete suggestions                         
 dense_vector    ML embeddings, similarity search                  
└────────────────┴──────────────────────────────────────────────────┘

5. 검색 쿼리

5.1 매치 쿼리

// Full-text match (analyzed)
GET /products/_search
{
  "query": {
    "match": {
      "description": {
        "query": "wireless bluetooth headphones",
        "operator": "and"
      }
    }
  }
}

// Multi-match across fields
GET /products/_search
{
  "query": {
    "multi_match": {
      "query": "sony headphones",
      "fields": ["name^3", "description", "tags^2"],
      "type": "best_fields"
    }
  }
}

5.2 불린 쿼리

// Compound boolean query
GET /products/_search
{
  "query": {
    "bool": {
      "must": [
        { "match": { "description": "wireless headphones" } }
      ],
      "filter": [
        { "term": { "category": "electronics" } },
        { "range": { "price": { "gte": 50, "lte": 200 } } },
        { "term": { "in_stock": true } }
      ],
      "should": [
        { "term": { "tags": "premium" } },
        { "range": { "rating": { "gte": 4.5 } } }
      ],
      "must_not": [
        { "term": { "tags": "refurbished" } }
      ],
      "minimum_should_match": 1
    }
  }
}

5.3 집계

// Faceted search with aggregations
GET /products/_search
{
  "size": 0,
  "aggs": {
    "categories": {
      "terms": { "field": "category", "size": 20 }
    },
    "price_ranges": {
      "range": {
        "field": "price",
        "ranges": [
          { "to": 50, "key": "budget" },
          { "from": 50, "to": 200, "key": "mid-range" },
          { "from": 200, "key": "premium" }
        ]
      }
    },
    "avg_rating": {
      "avg": { "field": "rating" }
    },
    "top_rated_by_category": {
      "terms": { "field": "category" },
      "aggs": {
        "avg_rating": { "avg": { "field": "rating" } },
        "top_products": {
          "top_hits": {
            "size": 3,
            "sort": [{ "rating": "desc" }],
            "_source": ["name", "rating", "price"]
          }
        }
      }
    }
  }
}

5.4 자동완성 / 제안

// Completion suggester
POST /products/_search
{
  "suggest": {
    "product-suggest": {
      "prefix": "wire",
      "completion": {
        "field": "name.suggest",
        "size": 5,
        "fuzzy": {
          "fuzziness": "AUTO"
        }
      }
    }
  }
}

// Search-as-you-type with match_phrase_prefix
GET /products/_search
{
  "query": {
    "match_phrase_prefix": {
      "name": {
        "query": "wireless blue",
        "max_expansions": 10
      }
    }
  }
}

6. 랭킹과 관련성

6.1 BM25 스코어링

┌─────────────────────────────────────────────────────────────────┐
│              BM25 Scoring (Elasticsearch default)                │
│                                                                 │
│  score(q, d) = Σ IDF(qi) × [f(qi,d) × (k1+1)]                │
│                              ────────────────────────           │
│                              f(qi,d) + k1×(1-b+b×|d|/avgdl)   │
│                                                                 │
│  Where:                                                         │
│   IDF(qi) = inverse document frequency of term qi              │
│    (rare terms  higher score)                                  │
│   f(qi,d) = term frequency of qi in document d                │
│    (more occurrences  higher, but with diminishing returns)   │
│   |d| = document length                                       │
│   avgdl = average document length                              │
│   k1 = term saturation parameter (default 1.2)                │
│   b = length normalization (default 0.75)                      │
│                                                                 │
│  Key insight: BM25 has diminishing returns for term frequency   │
│  (unlike TF-IDF which grows linearly)                          │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

6.2 부스팅과 커스텀 스코어링

// Field boosting
GET /products/_search
{
  "query": {
    "multi_match": {
      "query": "headphones",
      "fields": ["name^5", "description^2", "tags^3"]
    }
  }
}

// Function score for custom ranking
GET /products/_search
{
  "query": {
    "function_score": {
      "query": { "match": { "description": "headphones" } },
      "functions": [
        {
          "field_value_factor": {
            "field": "rating",
            "modifier": "log1p",
            "factor": 2
          }
        },
        {
          "gauss": {
            "created_at": {
              "origin": "now",
              "scale": "30d",
              "decay": 0.5
            }
          }
        }
      ],
      "score_mode": "multiply",
      "boost_mode": "multiply"
    }
  }
}

6.3 설명 API

// Understand why a document scored the way it did
GET /products/_explain/doc_id
{
  "query": {
    "match": { "description": "wireless headphones" }
  }
}

7. 검색 시스템 확장

7.1 샤드 크기 전략

┌─────────────────────────────────────────────────────────────────┐
│              Shard Sizing Guidelines                              │
│                                                                 │
│  Rules of thumb:                                                │
│   Target shard size: 10-50 GB                                  │
│   Max shards per node: ~20 per GB of heap                      │
│   Typical node heap: 32 GB  ~640 shards max                  │
│                                                                 │
│  Example calculation:                                           │
│   Data: 500 GB, 1 replica                                     │
│   Total: 1 TB (with replicas)                                  │
│   Shard size target: 25 GB                                     │
│   Primary shards: 500/25 = 20                                  │
│   Total shards: 20 × 2 = 40                                   │
│   Nodes needed: 40/640  1, but 3 for HA                     │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

7.2 인덱스 수명 주기 관리 (ILM)

┌─────────────────────────────────────────────────────────────────┐
│              Index Lifecycle (Hot-Warm-Cold-Delete)               │
│                                                                 │
│  Hot Phase (0-7 days)                                           │
│  ├── Fast SSDs, high write throughput                           │
│  ├── Rollover at 50GB or 7 days                                │
│  └── Primary for indexing + search                              │
│                                                                 │
│  Warm Phase (7-30 days)                                         │
│  ├── Standard disks, read-only                                  │
│  ├── Force merge to 1 segment per shard                         │
│  └── Shrink replica count                                       │
│                                                                 │
│  Cold Phase (30-90 days)                                        │
│  ├── Object storage (S3) via searchable snapshots               │
│  ├── Minimal resources                                          │
│  └── Slower queries acceptable                                  │
│                                                                 │
│  Delete Phase (> 90 days)                                       │
│  └── Automatically delete expired indices                       │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

7.3 검색 성능 최적화

Performance checklist:

1. Mapping optimization
   - Use keyword for exact match (not text)
   - Disable _source for write-heavy indices
   - Use doc_values for sorting/aggregations

2. Query optimization
   - Use filter context (cacheable, no scoring)
   - Avoid wildcard queries on large fields
   - Use routing for tenant-specific data

3. Index optimization
   - Appropriate shard count (avoid over-sharding)
   - Force merge read-only indices
   - Use index sorting for common sort patterns

4. Hardware
   - 50% memory for OS file cache
   - SSDs for hot data
   - Dedicated master nodes in production

8. 연습 문제

문제 1: 이커머스 검색

천만 개의 제품이 있는 이커머스 플랫폼을 위한 검색 시스템을 설계하세요.

요구 사항:
- 이름, 설명, 브랜드에 대한 전문 검색
- 카테고리 패싯(faceting)
- 가격 범위 필터링
- 관련성, 가격, 평점, 최신순으로 정렬
- 자동완성 제안
- 오타 허용

주요 결정 사항:
- 인덱스 설계: 5개의 프라이머리 샤드 (1000만 문서 ≈ 20GB)
- 동의어가 있는 커스텀 분석기 (예: "phone" = "smartphone")
- 자동완성을 위한 완성 제안기(Completion suggester)
- 인기 제품 부스팅을 위한 함수 스코어
- 오타 허용을 위한 Fuzziness

문제 2: 로그 검색 플랫폼

하루 50TB의 로그를 위한 중앙 집중식 로그 검색 시스템을 설계하세요.

주요 고려 사항:
- 쓰기 중심 워크로드 (50TB/일 = ~580MB/초)
- 시간 기반 인덱스 (일별 또는 시간별 롤오버)
- Hot-warm-cold 아키텍처
- 7일 검색 윈도우 (hot), 30일 아카이브 (cold)
- 구조화된 필드: timestamp, level, service, message

아키텍처:
- 수집 버퍼로서 Kafka
- 파싱을 위한 Logstash/Vector
- Hot 노드: 10 × (32GB RAM, 2TB NVMe SSD)
- Warm 노드: 6 × (32GB RAM, 8TB HDD)
- ILM 정책: 50GB에서 롤오버, 24시간 후 warm으로 이동
- ELK Cloud vs Loki 비교 및 예상 비용

문제 3: 자동완성 시스템

50ms 미만으로 결과를 반환하는 검색 자동완성을 설계하세요.

접근법:
1. 접두사 매칭을 위한 완성 제안기(Completion suggester)
2. 부분 단어 매칭을 위한 엣지 n-gram 분석기
3. 제안을 위한 별도의 경량 인덱스
4. Redis에 인기 쿼리 캐시 (TTL: 5)
5. 클라이언트  디바운싱 (300ms)

인덱스 설계:
{
  "suggest_text": {
    "type": "text",
    "analyzer": "edge_ngram_analyzer",
    "search_analyzer": "standard",
    "fields": {
      "suggest": { "type": "completion" }
    }
  },
  "popularity": { "type": "integer" }
}

쿼리 파이프라인:
사용자 입력  디바운스  edge-ngram 매치 + 완성 제안
 인기도로 부스트  상위 10 반환

다음 단계

참고 자료

to navigate between lessons