ElasticSearchSearch EngineDatabase

엘라스틱 서치 개념 잡기

·8 min read

검색엔진좀 만지고 싶어서 개념부터 다시 잡고있다.

검색 엔진이 빠른 이유

Inverted Index(역색인)

우리는 보통 DB에서 중요한 키에 인덱스를 건다.

예를 들어 USER_ID에 인덱스를 걸어서,

특정 유저에 정보를 ID로 빠르게 참조하기 위해서다.

만약에 ID가 아닌, 나이로 유저를 찾고 싶다면?

age에다가 색인을 걸어야한다.

중요 데이터가 아닌 찾고자 하는 데이터를 기준으로 역으로 색인을 건다는 것이다.

엘라스틱 서치 또한 텍스트를 분해하면서 단어->문서ID 형태로 사전을 만든다.

특정 단어가 포함된 문서 ID를 빠르게 찾아내기 위해서다.

Shard & Replica(샤드와 레플리카)

샤드와 레플리카는 지금까지 여러 글에서 다뤄왔다.

Shard는 데이터를 쪼개 여러 노드에 분산 저장해 성능을 향상하고

Replica는 데이터를 복제해 장애에 대비하고 읽기도 분산시킨다.

데이터를 어떻게 다루는가
Text vs Keyword

과거에 한번 다룬 내용이니 빠르게 넘어가겠다.

Text: 단어 쪼갬(분석 함) -> 본문 검색 또는 전문 검색

Keyword: 원문 그대로 저장 (분석 안함) -> 필터링, 정렬, 집계

Multi-field: 두 개 동시에 사용

Nested(중첩 객체)

엘라스틱 서치는 기본적으로 객체 배열을 평탄화해버려 데이터 간의 관계가 깨진다.

예시를 들자면 그룹이 있고, 그룹안에 멤버가 있다.

{ group: "A", members: [ {name: "Kim", age: 20}, {name: "Lee", age: 30} ] }

일반 저장시 객체 배열이 평탄화 되면 다음과 같이 된다.

names: ["Kim", "Lee"], ages: [20, 30]

원래 의도상으로는 Kim은 20살인데, 30살 이냐고 물어보면 그렇다고 답변이 돌아오는 것이다.

names가 "Kim"이 맞고, ages가 30이 맞으니깐

다만 Nested를 사용하면 {name: "Kim", age: 20} 이라는 객체 자체를 독립된 문서처럼 저장해서 관리한다.

물론 검색도 별도 방식을 써야하니 type:"nested"가 보이면 nested query를 사용해야한다.

데이터를 어떻게 처리하고 분석하는가

우리 텍스트를 쪼개는걸 생각하면 대표적으로 등장하는 놈이 있다.

바로 Tokenizer,

물론 자르는거 말고도 소문자 변환, 불용어 제거 등의 후처리가 들어가야한다.

엘라스틱 서치에서는 주로 Nori 라고 한국어 형태소에 특화된 처리기를 주로 쓴다.

"아버지가"에서 "아버지(명사) + 가(조사)" 분리 하는 등

한국어 문법을 이애하고 의미 단위(명사, 동사 등)으로 쪼개는 역할을 한다.

데이터를 어떻게 검색하는가

처음 볼 때 느낀건, 괄호가 무진장 많다는 것이다.

다만 대충 문법좀 보고 나면 DB Query랑 비슷하고,

약간 반복되는 틀 같은게 있는 것 같다.

전반적인 뼈대

엘라스틱 검색 요청은 보통 검색(Query), 정렬(Sort), 페이징(Size,From), 필드 선택(Source), 집계(Aggs)의 구역으로 나뉜다.

GET /index_name/_search
{
  "query": { ... },    // 1. 검색 조건 (WHERE)
  "sort": [ ... ],     // 2. 정렬 조건 (ORDER BY)
  "from": 0,           // 3. 페이징 시작점 (OFFSET)
  "size": 20,          // 4. 가져올 개수 (LIMIT)
  "_source": [ ... ],  // 5. 가져올 필드 지정 (SELECT)
  "aggs": { ... }      // 6. 통계/집계 (GROUP BY)
}
where 논리 조합

결국 쿼리란 조건문의 조합 아니겠는가

복잡해지는 조건문에 조건 별로 태그를 열고 닫고 조합을 해대니 복잡성을 올리는 주된 원인이 아닐까 싶다.

"query": {
  "bool": {
    "must": [      // AND (점수 계산 O) - "정확도 중요"
      { ... } 
    ],
    "must_not": [  // NOT (점수 계산 X) - "제외"
      { ... }
    ],
    "should": [    // OR (점수 가산) - "있으면 점수 UP"
      { ... }
    ],
    "filter": [    // AND (점수 계산 X) - "단순 필터링(빠름)"
      { ... }
    ]
  }
}

검색엔진 결과는 결국 스코어링이다.

score가 어떻게 나오느냐는 계산에 따라 달라지는데,

당연히 계산 안하는 filter와 must_not을 자주 써주는게 성능 측면에서는 좋을 것이다.

상세 조건

아까 where bool 조건에 들어갈 실제 조건들이다.

처리 방식은 대상이 **Text(분석)**이냐 **Keyword(정확)**이냐에 따라서 나뉜다.

Text 처리

풀검색, 내용 검색을 해야하니 당연히 문장을 쪼개서 검색을 해야한다.

이때 주로 쓰는게 바로 match인데, 검색한다면 기본은 이걸로 생각하면 된다.

{ "match": { "title": "서울 맛집" } } 
// "서울" OR "맛집"이 포함된 문서 검색

만약에 여러개 필드에서 검색하고 싶으면?

multi_match를 쓰면 된다.

{
  "multi_match": {
    "query": "맛집",
    "fields": ["title", "description", "brand^2"]
  }
}

brand 항목 보면 ^2을 붙여놨는데 분석은 결국 스코어링 즉 점수가 환산 되기에,

^2 등의 수식으로 중요도 즉 가중치를 2배로 만드는 등의 동작이 가능하다.

Keyword 처리

정확히 일치해야하기에 일반적인 우리 DB 검색과 비슷하다.

단일 값 일치에 경우 term을, SQL IN 처럼 여러개 검색하면 terms를 쓰고,

범위 검색에서는 range를 쓴다.

{ "term": { "status": "active" } }
{ "terms": { "category_id": [10, 20, 30] } }
{
  "range": {
    "price": { "gte": 10000, "lte": 50000 }
  }
}
nested 계층 구조 검색

type이 nested면 반드시 계층 방식대로 검색을 해야한다.

{
  "nested": {
    "path": "options", // nested 필드명
    "query": {
      "bool": {
        "must": [
          { "match": { "options.color": "red" } }
        ]
      }
    }
  }
}

자세한 설명은 생략한다.

당장은 복잡한 집계 까지는 필요없고,

Nested 객체 필터링만 하면 되기에,

나중에 기회가 되면 다뤄보겠다.

← Previous
OpenSearch
Next →
엘라스틱 서치 필터링