ElasticSearch - 기본 개념

Sungjin·2022년 12월 12일
0

ELK Stack

목록 보기
1/4

ElasticSearch 의 모든 기능은 REST API 의 형태이다.
이번 페이지에서 주로 다룰 내용은 다음과 같다.

  • 인덱스 생성
  • 인덱스 관리
    - 확인 작업
    - 삭제 작업
  • ElasticSearch 가 제공하는 샘플 예제를 불러오는 방법

🌈 준비 작업

🔎 Kibana Console 사용법

예제를 진행하면서, 특별한 프로그램이 아닌 키바나 Dev Tools 에 있는 콘솔을 이용하여 Rest API 를 호출할 것이다.

키바나의 왼쪽 토글로부터 Management → Dev Tools 를 선택하면 된다.

재생 버튼을 눌러 엘라스틱서치와 바로 통신이 가능하다. 또한, 엘라스틱서치 API 자동 완성 기능이 지원된다. 자동완성은 사용 가능한 API 들을 미리보기 형태로 제공하기 때문에 API 사용방법을 완벽히 외우거나 매번 도큐먼트를 뒤져보지 않아도 된다.

🔎 시스템 상태 확인

엘라스틱 서치의 현재 상태를 빠르게 확인할 수 있는 방법으로 일반적으로 cat API 를 사용한다.

cat 은 “compact and aligned text” 의 약자로, 콘솔에서 시스템 상태를 확인할 때 가독성을 높일 목적으로 만들어졌다.

nodes, shards 등을 확인할 수 있다.

그럼, cat 을 통해 내부 인덱스 목록을 확인해 보자.

GET _cat/indices?v 를 요청하면 된다.

  1. ? 뒤에는 몇 가지 파라미터를 사용할 수 있다.

  2. v - 컬럼 헤더를 확인할 수 있다.

  3. s - 정렬

h - 헤더

엘라스틱 스택을 설명하기 전에 cat API 를 설명하는 이유는 현재 클러스터 상태를 빠르게 확인 가능하기 때문이다. 실습을 하면서 많은 인덱스를 만들고 지울 텐데 이러한 과정에서 빠르게 상태를 점검할 수 있기 때문에 알아두는 것이 좋다.

🔎 샘플 데이터 불러오기

엘라스틱 스택은 세가지 샘플 데이터를 기본으로 제공한다. 샘플 데이터를 불러오도록 하자. 키바나의 홈화면에 try sample data 를 클릭해보자.

총 3 개의 샘플 데이터가 있고, 각 샘플 데이터 마다 Add data 버튼을 클릭해서 샘플을 추가할 수 있다. 샘플 데이터가 추가된 후 나타나는 Remove 버튼을 누르면 샘플 데이터가 삭제된다는 점을 참고하자.

샘플 데이터를 추가하면 키바나의 Visualize 와 Dashboard 등에도 샘플들이 추가된다.

🌈 인덱스와 도큐먼트

엘라스틱서치를 이해하기 위해서는 인덱스와 도큐먼트가 무척 중요하다. 인덱스는 도큐먼트를 저장하는 논리적 구분자이며, 도큐먼트는 실제 데이터를 저장하는 단위이다.

일반적으로 엘라스틱을 이용해 시스템을 개발하면 하나의 프로젝트에 하나의 클러스터를 생성한다.

그리고, 클러스터 내부는 데이터 성격에 따라 여러 개의 인덱스를 생성한다. 인덱스 내부에는 JSON 형태로 된 다수의 도큐먼트가 존재하고 도큐먼트는 복수의 필드들을 갖는다.

이제부터 인덱스와 도큐먼트에 관해 자세히 알아보자.

🔎 도큐먼트

도큐먼트는 엘라스틱서치에서 데이터가 저장되는 기본 단위로 JSON 형태며, 하나의 도큐먼트는 여러 필드 값을 갖는다. 엘라스틱 서기차 도큐먼트에 데이터를 어떻게 저장하는지 알아보도록하자.

다음과 같은 JSON 형태의 데이터를 도큐먼트라고 한다.

{
  "name": "Hong",
  "age": 27,
  "gender": "male"
}

여기서 name, age, gender 를 필드라고 하며 “Hong”, 27, “male” 을 값이라고 한다.

🔎 인덱스

인덱스는 도큐먼트를 저장하는 논리적 단위로, 관계형 데이터베이스의 테이블과 유사한 개념이다.

하나의 인덱스에 다수의 도큐먼트가 포함되는 구조인데, 동일한 인덱스에 있는 도큐먼트는 동일한 스키마를 갖는다. 그리고 모든 도큐먼트는 반드시 하나의 인덱스에 저장되어 있어야 한다.

  • 스키마에 따른 그루핑.

예를 들어, 회원 정보 도큐먼트와 상품 정보 도큐먼트는 성격이 다르기 때문에 스키마도 다르다. 이렇게 서로 다른 스키마를 가진 도큐먼트를 하나의 인덱스에 저장하는 방법은 바람직하지 않다. 스키마에 인덱스를 구분하는 것은 기본이며 필수적인 사항이다.

  • 관리 목적의 그루핑.

기본적인 이론으로는 인덱스에는 용량, 숫자 제한 없이 무한의 도큐먼트를 포함할 수 있다. 하지만 인덱스가 커지면 검색 시 많은 도큐먼트를 참조해야 하기 때문에 성능이 나빠진다. 그래서 엘라스틱 서치를 운용하면 인덱스 용량을 제한 하게 한다. 기본적으로 특정 도큐먼트 개수에 도달하거나 특정 용량을 넘어서면 인덱스를 분리한다.

🌈 도큐먼트 CRUD

이제 도큐먼트를 Create, Read, Update, Delete 해보자.

🔎 인덱스 생성/확인/삭제

도큐먼트는 반드시 하나의 인덱스에 포함 되어있어야 하므로 인덱스를 먼저 생성해보도록하자.

  • PUT test-index

test-index 라는 이름의 인덱스를 생성하는 API 이다. PUT은 생성이나 수정을 위한 HTTP 메소드이며, 대신 POST 를 사용하여도 된다.

  • GET test-index

인덱스를 확인하는 API 이다. 인덱스 설정이나 매핑값들을 확인할 수 있다.

  • DELETE test-index

인덱스를 삭제하는 API 이다. 만약, 인덱스에 도큐먼트가 존재한다면, 도큐먼트 또한 모두 삭제되니 사용에 주의하도록 하자.

🔎 도큐먼트 생성
엘라스틱서치에서 도큐먼트를 인덱스에 포함시키는 것을 인덱싱이라고 하는데, 도큐먼트를 인덱싱 해보자.

PUT test-index/_doc/1
{
  "name": "Hong",
  "age": 27,
  "gender": "male"
}

_doc 는 엔드포인트 구분을 위한 예약어, 숫자 1은 인덱싱할 도큐먼트의 고유 아이디다. 정상적으로 도큐먼트가 인덱싱 되었는지 확인해보자.

GET test-index

mappings에 age는 long 타입, gender 와 name 은 text 타입으로 필드가 지정되었다. 우리가 데이터 타입을 지정하지 않아도 엘라스틱서치는 도큐먼트의 필드와 값을 보고 자동으로 지정하는데, 이런 기능을 다이내믹 매핑 이라고 한다.

이제, “country”필드가 포함된 또다른 도큐먼트를 인덱싱 해보자.

PUT test-index/_doc/2
{
  "name": "Kim",
  "age": 2,
  "country": "korea"
}

country 필드가 추가 되며, 문제 없이 인덱싱 되는 것을 확인할 수 있다.

이번에는 데이터 타입을 잘못 입력한 도큐먼트를 인덱싱 해보자.

PUT test-index/_doc/3
{
  "name": "Jane",
  "age": "100",
  "country": "korea"
}

age 가 text 타입으로 잘못 지정되었더라도, 엘라스틱 서치는 유연하게 실수타입으로 변환하여 저장한다. JSON 파일에서는 데이터 타입을 크게 신경쓰지 않고 표현하는 경우가 흔한데, 엘라스틱서치는 혹시 모를 사용자 입력 실수를 고려해 자동으로 데이터 형변환을 진행 한다.

🔎 도큐먼트 읽기

도큐먼트를 읽는 방법은 크게 도큐먼트 아이디를 이용해 조회하는 방법과 DSL 이라는 엘라스틱 서치가 제공하는 쿼리문을 이용해 검색하는 방법이 있다.

  • 아이디를 이용해 조회하는 방법

GET test-index/_doc/1

  • DSL 을 통해 조회하는 방법

GET test-index/_search

search 라는 DSL 쿼리를 이용해 도큐먼트를 읽어올 수 있다.

DSL 쿼리는 다음에 자세히 다뤄보도록하자.

🔎 도큐먼트 수정

이번에는 도큐먼트를 수정하는 방법에 대해서 알아보자. test-index 의 1번 도큐먼트의 특정 필드 값을 변경해보자.

PUT test-index/_doc/1
{
  "name": "Change",
  "age": 34,
  "gender": "male"
}

결과를 보면 updated 된 것을 알 수 있다. 이렇게 응답 결과의 result 를 통해 도큐먼트가 생성되고 업데이트 되고 삭제되는 상태를 알 수 있다.

이러한 방식 말고도, update API 를 통해서 도큐먼트의 값을 업데이트 할 수 있다.

PUT test-index/_update/1
{
  "doc": {
    "name": "Hong"
  }
}

_update 라는 엔드포인트를 추가해 특정 필드의 값만 업데이트할 수 있다. 여기서는 1번 도큐먼트의 name을 “Hong”으로 바꿨다.

엘라스틱서치 도큐먼트 수정 작업은 비용이 많이 들기 때문에 권장하지 않는다. 특히 엘라스틱서치를 로그 수집 용도로 사용한다면 개별 도큐먼트를 수정할 일은 거의 없다.

개별 도큐먼트 수정이 많은 작업이라면 엘라스틱서치 대신 다른 데이터베이스를 고려하는 편이 더 좋다.

🔎 도큐먼트 삭제

특정 도큐먼트를 삭제하기 위해서는 인덱스명과 도큐먼트 아이디를 알고 있어야 한다.

DELETE test-index/_doc/2

삭제 과정 또한 수정과 마찬가지로 비용이 많이 들어가는 작업인 만큼 사용에 주의하여야 한다.

🌈 벌크 데이터

데이터 CRUD 동작을 할 때는 REST API 를 호출해 하나하나 도큐먼트를 요청하는 것보다 벌크로 한번에 요청하는 것이 효율적이다. API 를 20번 호출해 20개의 도큐먼트를 인덱싱하는 경우 HTTP 통신이 20번 발생하게 되는데, API를 한 번 호출해 20개의 도큐먼트를 인덱싱한다면 훨씬 빠르고 경제적이다.

POST _bulk
{"index": {"_index": "test", "_id": "1"}}
{"field1": "value1"}
{"create": {"_index": "test", "_id": "3"}}
{"field1": "value3"}
{"update": {"_id": "1", "_index": "test"}}
{"doc": {"field2" : "value2"}}
{"delete": {"_index": "test", "_id": "2"}}

벌크는 도큐먼트 읽기는 지원하지 않고 도큐먼트 생성/수정/삭제만 지원한다. 벌크 데이터 포맷을 보면 삭제만 한 줄로 작성하고 나머지 작업들은 두 줄로 작성 된다. 각 줄 사이에는 쉼표 등 별도의 구분자가 없고 라인 사이 공백을 허용하지 않는다.

JSON 문법처럼 보이지만 복수의 JSON 구조를 줄바꿈 문자열로 구분하는 NDJSON 형태다. JSON 과 비슷하지만 문법이 조금 다르니 라인이나 쉼표 사이에 주의 해야 한다.

POST _bulk
{"index": {"_index": "index2", "_id": "1"}}
{"name": "park", "age": 30, "gender": "female"}
{"index": {"_index": "index2", "_id": "2"}}
{"name": "Hong", "age": 20, "gender": "male"}

위의 코드는 벌크로 2개의 도큐먼트를 한 번에 인덱싱한다.

GET index2/_search 를 통해 인덱스를 조회해보도록 하자.

index2에 2개의 도큐먼트가 추가된 것을 확인할 수 있다. 벌크 데이터를 파일 형태로 만들어서 적용하는 방법도 있다. 실제 현업에서는 파일로 만들어서 사용하는 방식이 더 실용적이다.

  • 잘못된 벌크 데이터 포맷
{
  "index":
    {
      "_index": "index2",
      "_type": "_doc",
      "_id": "5"
    }
}

bulk 요청 데이터 포맷은 JSON 이 아닌 NdJSON 이다. 도큐먼트 필드가 많아지면 가독성 때문에 JSON 으로 작성하는 경우가 많은데 주의 하여아한다.

🌈 매핑

관계형 데이터베이스는 테이블을 만들 때 반드시 스키마 설계가 필요하다. 스키마는 테이블을 구성하는 구성요소 간의 논리적인 관계와 정의를 의미한다. 구조를 명확히 하지 않고는 테이블이 생성되지 않고, 추후에 인덱싱 컬럼이나 조인 컬럼 등에서도 문제가 발생하기 때문이다.

엘라스틱서치에서도 스키마와 비슷한 역할을 하는 것이 있는데 바로 매핑이다. JSON 형태의 데이터를 루씬이 이해할 수 있도록 바꿔주는 작업이다.

엘라스틱서치가 검색 엔진으로 전문 검색과 대용량 데이터를 빠르게 실시간 검색할 수 있는 이유는 매핑이 있기 때문이다.

매핑은 두 가지로 나뉜다.

  • 다이내믹 매핑 : 엘라스틱서치가 자동으로 한다.

  • 명시적 매핑 : 사용자가 직접 설정한다.

매핑 설정 방법과 좋은 매핑이 무엇인지 배워보고, 매핑할 때 사용하는 데이터의 타입도 알아보자.

🔎 다이내믹 매핑

엘라스틱 서치의 모든 인덱스는 매핑 정보를 갖고 있지만 유연한 활용을 위해 인덱스 생성 시 매핑 정의를 강제하지 않는다. 앞에서의 예제를 생각해보자. 우리는 test-index 의 인덱스에 매핑 정보를 설정하지 않았지만, 도큐먼트가 인덱싱 되었다. 이 이유는 엘라스틱서치에서의 다이내믹 매핑 기능 때문이다.

특별히 데이터 타입이나 스키마에 고민하지 않아도 JSON 도큐먼트의 데이터 타입에 맞춰 엘라스틱서치가 자동으로 인덱스 매핑을 해주는 것이다.

JSON (원본 소스 데이터 타입 )다이내믹 매핑으로 변환된 데이터 타입
null필드를 추가하지 않음
booleanboolean
floatfloat
integerlong
objectobject
stringstring 데이터 형태에 따라 date, text/keyword 필드

위의 표는 원본 데이터가 다이내믹 매핑에 의해 어떤 타입으로 변환 되는지 보여준다.

예를들어, JSON 도큐먼트에 "age" : 27 이라는 필드와 값이 있다면 엘라스틱서치는 20을 숫자(integer)로 인식한다. 원본 소스가 integer 타입이기 때문에 엘라스틱서치는 인덱스 생성 시 age 필드를 long 타입의 필드로 매핑한다.

앞서 활용했던 test-index 의 매핑값을 확인해보자. 엘라스틱서치는 mapping API를 제공한다.

GET test-index/_mapping

지금은 간단한 예제를 작성하는 수준이라서 특별히 문제는 없지만, 대량의 도큐먼트를 갖고 있는 인덱스에 mapping 을 사용한다면 효율이 떨어질 수 있다.

다이내믹 매핑 결과를 확인해보자. age 필드는 사람의 나이 이므로, long 형보다는 short 형이 유리할 수 있다. country 나 gender 같은 범주형 데이터는 전문 검색 보다는 일반적으로 집계나 정렬, 필터링을 위해서 사용되기 때문에 keyword 타입으로 지정하는 것이 좋다. text/keyword의 차이점은 나중에 자세히 살펴보도록 하자.

🔎 명시적 매핑

인덱스 매핑을 직접 정의하는 것을 명시적 매핑이라고 한다. 인덱스를 생성할 때 mappings 정의를 설정하거나 mapping API를 이용해 매핑을 지정할 수 있다.

PUT "인덱스명"
{
  "mappings": {
    "properties": {
      "필드명": "필드타입"
    }
  }
}

인덱스를 생성할 때 mappings, properties 아래에 필드명과 다른 필드 타입을 지정해주면 된다.

PUT index3
{
  "mappings": {
    "properties": {
      "age": {"type": "short"},
      "name": {"type": "text"},
      "gender": {"type": "keyword"}
    }
  }
}

index3이라는 새로운 인덱스를 생성하면서 명시적으로 매핑을 지정하고 있다. 지금 생성한 인덱스의 매핑정보를 확인해 보도록 하자.

GET index3/_mapping

index3은 우리가 지정한 필드명과 필드 타입으로 인덱스가 매핑되었다. 저장할 데이터를 확실히 알고 있다면, 다이내믹 매핑보다는 명시적 매핑을 사용하는 것이 좋다.

인덱스 매핑이 정해지면 새로운 필드를 추가할 수는 있으나, 이미 정의된 필드를 수정하거나 삭제할 수 는 없다. 필드 이름을 변경하거나 데이터 타입을 변경하기 위해서는 새로운 인덱스를 만들거나 reindex API 를 이용해야 하니 매핑 작업은 신중하게 하는 것이 좋다.

매핑에서 다루는 파라미터들도 알아두면 좋다. properties 외에도 분석기나 포맷 등을 설정하는 파라미터도 존재한다.

🔎 매핑 타입

명시적 매핑을 위해서는 엘라스틱서치에서 사용하는 데이터 타입에 대한 이해가 필요하다.

데이터 타입은 다음의 표와 같다.

추가로, 좋은 스키마가 관계형 데이터베이스의 성능을 끌어올리는 것처럼 매핑을 잘 활용하면 엘라스틱서치의 인덱스 성능을 올릴 수 있다.

데이터 형태데이터 타입설명
텍스트text전문 검색이 필요한 데이터로 텍스트 분석기가 텍스트를 작은 단위로 분리한다.
keyword정렬이나 집계에 사용되는 텍스트 데이터로 분석을 하지 않고 원문을 통째로 인덱싱한다.
날짜date날짜/ 시간 데이터
정수byte, short, integer, longbyte : 부호 있는 8비트 데이터
short: 부호 있는 16비트 데이터
integer: 부호 있는 32비트 데이터
long: 부호 있는 64비트 데이터
실수scaled_float, half_float, double, floatscaled_float : float 데이터에 특정 값을 곱해서 정수형으로 바꾼 데이터. 정확도는 떨어지나 필요에 따라 집계 등에서 효율적으로 사용 가능
half_float : 16비트 부동소수점 실수 데이터
float : 32비트 부동소수점 실수 데이터
double : 64비트 부동소수점 실수 데이터
불린boolean참/ 거짓 데이터로 true/ false 만을 값으로 갖는다.
IP 주소ipipv4, ipv6 타입 ip 주소를 입력할 수 있다.
위치 정보geo-point, geo-shapegeo-point : 위도, 경도 값을 갖는다.
geo-shap : 하나의 위치 포인트가 아닌 임의의 지형
범위 값integer_range, long-range, float_range, double_range, ip_range, date_range범위를 설정할 수 있는 데이터, 정수, 실수, ip, 날짜 등 범위 값을 저장하고 검색할 수 있다. 최솟값과 최댓값을 통해 범위를 입력한다.
객체형object계층 구조를 갖는 형태로 필드 안에 다른 필드들이 들어갈 수 있다. name: {“first”: “Kim”, “last”: “tony”} 로 타입을 정의하면 name.first, name.last 형태로 접근할 수 있다.
배열형nested배열형 객체를 저장한다. 객체를 따로 인덱싱하여 객체가 하나로 합쳐지는 것을 막고, 배열 내부의 객체에 쿼리로 접근할 수 있다.
join부모/ 자식 관계를 표현할 수 있다.

🔎 멀티 필드를 활용한 문자열 처리

엘라스틱서치 5.x 버전부터 문자열 타입이 텍스트와 키워드라는 타입으로 분리되었다. 이 두 가지 타입이 어떤 용도로 사용되는지 알아보고, 멀티 필드를 사용해 두 타입을 동시에 활용하는 방법도 알아보자.

💡 텍스트 타입

텍스트 타입은 일반적으로 문장을 저장하는 매핑 타입으로 사용된다. 강제성은 없지만 일반적으로 문장이나 여러 단어가 나열된 문자열을 텍스트 타입으로 지정한다.

텍스트 타입으로 지정된 문자열은 분석기에 의해 토큰으로 분리되고, 이렇게 분리된 토큰들은 인덱싱되는데 이를 역인덱싱이라고 한다. 이때 역인덱스에 저장된 토큰들을 용어라고 한다.

직접 텍스트 타입을 매핑해보자.

PUT test_index
{
  "mappings": {
    "properties": {
      "contents": {"type": "text"}
    }
  }
}

test-index 라는 이름의 인덱스를 생성하고 contents필드의 타입을 텍스트 타입으로 매핑하였다. 다음의 도큐먼트를 인덱싱하자.

PUT test_index/_doc/1
{
  "contents": "sunny day"
}

“sunny day” 는 텍스트이기 때문에 분석기에 의해 [“sunny”, “day”] 와 같은 형태로 역인덱스에 저장된다. 이제 DSL 쿼리를 이용하여 검색을 해보자.

GET test_index/_search
{
  "query": {
    "match": {
      "contents": "day"
    }
  }
}

match 는 전문 검색을 할 수 있는 쿼리이며, contents 필드에 있는 역인덱싱된 용어 중 일치하는 용어가 있는 도큐먼트를 찾는 쿼리문이다.

보이는 것과 같이 1번 도큐먼트를 찾아낸 것을 볼 수 있다. 즉, 역인덱싱된 용어 중 하나라도 포함되면 도큐먼트를 찾는다.

텍스트 타입의 경우 기본적으로 집계나 정렬을 지원하지 않으며, 매핑 파라미터로 집계나 정렬을 지원할 수는 있으나 메모리를 많이 사용한다는 단점이 있다.

텍스트 타입으로 지정된 필드를 정렬할 경우 문장의 첫 문자열이 아닌 분해된 용어를 기준으로 정렬을 수행하므로 예상과는 다른 결과를 얻게 된다. 전문 검색이 아닌 집계나 정렬은 키워드 타입을 사용해야 한다.

💡 키워드 타입

키워드 타입은 카테고리나 사람 이름, 브랜드 등 규칙성이 있거나 유의미한 값들의 집합, 즉 범주형 데이터에 주로 사용된다. 앞서 텍스트 타입에서는 “sunny day” 를 [“sunny”, “day”]와 같은 형태로 역인덱싱 하였는데, 키워드 타입은 [sunny day] 라는 1개의 용어로 만든다.

따라서 부분 일치 검색은 어렵지만 대신 완전 일치 검색을 위해 사용할 수 있으며 집계나 정렬에 사용할 수 있다. 범주형 데이터의 경우 데이터 형태가 몇 가지로 고정되기 때문에 문자열 집계나 정렬 작업에 활용 가치가 더 높다.

PUT keyword_index
{
  "mappings": {
    "properties": {
      "contents": {"type": "keyword"}
    }
  }
}

keyword_index 라는 이름의 인덱스를 생성했다. 다음으로 도큐먼트를 하나 인덱싱하자.

PUT keyword_index/_doc/1
{
  "contents": "sunny day"
}

이제 DSL 쿼리를 이용해 검색을 해보자.

GET keyword_index/_search
{
  "query": {
    "match": {
      "contents": "sunny"
    }
  }
}

결과를 보면, 키워드 타입의 경우 용어로 분리되지 않기 때문에 도큐먼트를 찾을 수 없다. 도큐먼트를 찾기 위해서는 정확히 전문을 다 입력해야한다.

GET keyword_index/_search
{
  "query": {
    "match": {
      "contents": "sunny day"
    }
  }
}

💡 멀티 필드

멀티 필드는 단일 필드 입력에 대해 여러 하위 필드를 정의하는 기능으로, 이를 위해 fields 라는 매핑 파라미터가 사용된다. fields 는 하나의 필드를 여러 용도로 사용할 수 있게 만들어준다. 문자열의 경우 전문 검색이 필요하면서 정렬도 필요한 경우가 있다. 또한 처음 데이터 스키마를 잡는 시점에서는 키워드 타입으로 충분히 처리가 가능한 범주형 데이터였지만 데이터가 늘어나면서 전문 검색이 필요해지는 경우도 생긴다. 이런 경우 두 개의 필드를 다 지원해야한다.

PUT multifield_index
{
  "mappings": {
    "properties": {
      "message": {"type": "text"},
      "contents": {
        "type": "text",
        "fields": {
          "keyword": {"type": "keyword"}
        }
      }
    }
  }
}

message, contents 라는 2개의 필드를 가진 multifield_index 인덱스를 생성했다. 그중 contents 필드는 멀티 타입으로 설정했다. contents 필드는 텍스트 타입이면서 키워드 타입을 갖는다.

이제 도큐먼트를 인덱싱해보자.

PUT multifield_index/_doc/1
{
  "message": "1 document",
  "contents": "sunny day"
}

PUT multifield_index/_doc/2
{
  "message": "2 document",
  "contents": "sunny day"
}

PUT multifield_index/_doc/3
{
  "message": "3 document",
  "contents": "sunny day"
}

3개의 도큐먼트를 인덱싱 하였다. 이제 match 쿼리를 이용해 도큐먼트를 찾아보자.

GET multifield_index/_search
{
  "query": {
    "match": {
      "contents": "day"
    }
  }
}

3개의 도큐먼트가 모두 검색된다. contents는 멀티 필드이지만 기본적으로 텍스트 타입으로 매핑되어 있다.

이번에는 contents 필드의 키워드 타입으로 검색해보자.

GET multifield_index/_search
{
  "query": {
    "match": {
      "contents.keyword": "day"
    }
  }
}

contents 뒤에 keyword를 추가해 하위 필드를 팜조할 수 있다. 여기서 하위 필드로 사용한 keyword는 매핑을 하면서 사용자가 지정한 명칭이다. keyword 대신 다른 이름을 사용해도 무관하지만 통상적으로 많이 사용한다.

키워드 타입은 집계에서 활용도가 큰데 집계 쿼리를 한번 실행해보자.

GET multifield_index/_search
{
  "size": 0,
  "aggs": {
    "contents": {
      "terms": {
        "field": "contents.keyword"
      }
    }
  }
}

aggs 는 집계를 하기 위한 쿼리이다.

contents.keyword 값이 같은 도큐먼트끼리 그룹핑이된다. 문자열 데이터가 있는데 전문 검색도 해야 하고 집계나 정렬도 필요하다면 멀티 필드가 좋은 수단이 된다.

🌈 인덱스 템플릿

인덱스 템플릿은 주로 설정이 동일한 복수의 인덱스를 만들 때 사용한다. 관리 편의성, 성능 등을 위해 인덱스를 파티셔닝하는 일이 많은데 이때 파티셔닝되는 인덱스들은 설정이 같아야 한다. 설정이 동일한 인덱스를 매번 일일히 작성하는 것은 비효율적일 뿐 아니라 실수를 유발할 수 있다.

🔎 템플릿 확인

템플릿 API를 사용해 현재 등록된 인덱스 템플릿을 확인해보자.

GET _index_template

특정 인덱스 템플릿만 확인할 수도 있다. _index_template 뒤에 템플릿 이름을 적거나 와일드 카드 표현식을 이용해 특정 인덱스 템플릿을 확인할 수 있다.

🔎 템플릿 설정

인덱스 템플릿을 생성할 때 다양한 설정을 할 수 있지만, 일반적으로 매핑과 세팅 설정을 가장 많이 한다. 인덱스 템플릿 기술을 이용해 인덱스 매핑과 세팅을 효율적으로 설정해보자.

💡 템플릿 생성

인덱스 템플릿을 하나 만들어보자. 템플릿 이름은 test_template으로 하자.

PUT _index_template/test_template
{
  "index_patterns": ["test_*"],
  "priority": 1,
  "template": {
    "settings": {
      "number_of_shards": 3,
      "number_of_replicas": 1
    }
  },
  "mappings": {
    "properties": {
      "name": {"ttpe": "text"},
      "age": {"type": "short"},
      "gender": {"type": "keyword"}
    }
  }
}

템플릿을 생성했다. 다음은 템플릿에서 자주 사용하는 파라미터를 정리한 내용이다.

파라미터설명
index_patterns새로 만들어지는 인덱스 중에 인덱스 이름이 인덱스 패턴과 매칭되는 경우 이 템플릿이 적용된다. 여기서는 test_로 시작되는 이름을 가진 인덱스들은 모두 test_template에 있는 매칭, 세팅등이 적용된다.
priority인덱스 생성 시 이름에 매칭되는 템플릿이 둘 이상일 때 템플릿이 적용되는 우선순위를 정할 수 있다. 숫자가 가장 높은 템플릿이 먼저 적용된다.
template새로 생성되는 인덱스에 적용되는 settings, mappings 같은 인덱스 설정을 정의한다.

💡 템플릿 적용

이제 test_template 템플릿이 적용될 수 있도록 인덱스 패턴에 맞는 인덱스를 하나 만들어보자. 한 가지 주의해야할 점은 템플릿을 만들기 전에 이미 존재하는 인덱스는 비록 템플릿 패턴과 일치하더라도 템플릿이 적용되지 않는다.

PUT test_index1/_doc/1
{
  "name": "kim",
  "age": 10,
  "gender": "male"
}

이제 인덱스가 잘 매핑 되었는지 확인해보자.

GET test_index1/_mapping

잘 적용된 것을 확인할 수 있다. 비교를 위해서 test_로 시작하는 인덱스가 아닌 다른 인덱스를 만들어 보자.

PUT index4/_doc/1
{
  "name": "kim",
  "age": 10,
  "gender": "male"
}

GET index4/_mapping

보이는 것과 같이 템플릿 적용이 되지 않은 모습이다.

이제, 템플릿과 매핑값이 다른 도큐먼트를 인덱싱해보도록 하자.

PUT test_index2/_doc/1
{
  "name": "Hong",
  "age": "thirteen"
}

400번대의 오류가 발생하는 것을 볼 수 있다. 무엇 때문에 이러한 오류가 났는지도 알려주는데 숫자형 데이터에 문자형 데이터가 들어왔기 때문이라는 내용이다.

💡 템플릿 삭제

템플릿 생성 시 기존 인덱스들은 영향을 받지 않는다고 했다. 삭제 역시 마찬가지다. 템플릿을 지워도 기존 인덱스들은 영향을 받지 않는다.

DELETE _index_template/test_template

🔎 다이내믹 템플릿

다이내믹 템플릿은 매핑을 다이내믹하게 지정하는 템플릿 기술이다. 왜 다이내믹 매핑이 필요할까? 로그 시스템이나 비정형화된 데이터를 인덱싱하는 경우를 생각해보자. 우리는 로그 시스템의 구조를 알지 못하기 때문에 필드 타입을 정확히 정의하기 힘들고, 필드 개수를 정할 수 없는 경우도 있다. 따라서 다이내믹 템플릿은 매핑을 정확하게 정할 수 없거나 대략적인 데이터 구조만을 알고 있을 때 사용할 수 있는 방법이다.

PUT dynamin_index1
{
  "mappings": {
    "dynamic_templates": [
      {
        "my_string_fields": {
          "match_mapping_type": "string",
          "mapping": {"type": "keyword"}
        }
      }
      ]
  }
}

dynamic_index1 은 다이내믹 템플릿을 사용한다. my_string_fields는 임의로 정의한 다이내믹 템플릿의 이름이다.

여기서 match_mapping_type 은 조건문 혹은 매핑 트리거이다. 조건에 만족할 경우 트리거링이 된다. 여기서는 문자열 타입 데이터가 있으면 조건에 만족한다. 매핑은 실제 매핑을 적용하는 부분이다. 위의 경우 문자열 타입의 데이터가 들어오면 키워드 타입으로 매핑한다.

도큐먼트를 인덱싱하여 매핑을 확인해보도록하자.

PUT dynamin_index1/_doc/1
{
  "name": "abc",
  "age": 1
}

GET dynamin_index1/_mapping

보이는 것과 같이 name 필드가 keyword 타입으로 매핑된 것을 확인할 수 있다.

하나 더 확인해보자. 이번엔 matchunmatch 조건문을 이용해보자.

PUT dynamic_index2
{
  "mappings": {
    "dynamic_templates": [
      {
        "my_long_fields": {
            "match": "long_*",
            "unmatch": "*_text",
            "mapping": {"type": "long"}
        }
      }
    ]
  }
}

match는 조건에 맞는 경우 mapping에 의해 필드들은 모두 숫자 타입을 갖는다. unmatch는 조건에 맞는 경우 mapping에서 제외한다. 도큐먼트를 추가하고 매핑결과를 확인해 보자.

PUT dynamic_index2/_doc/1
{
  "long_num": "2",
  "long_text": "3"
}

GET dynamic_index2/_mapping

위의 그림에서 볼 수 있듯이 long_num 필드는 match 조건에 의해 문자열이 숫자 타입으로 매핑되었다. long_text 는 match 조건에 부합하지만 unmatch 조건도 부합하여 다이내믹 템플릿에서 제외되었다.

다음은 다이내믹 템플릿에서 사용할 수 있는 조건문이다.
|조건문|설명|
|----|----|
|match_mapping_type|데이터 타입을 확인하고 타입들 중 일부를 지정한 매핑 타입으로 변경한다.|
|match, unmatch|match: 필드명이 패턴과 일치할 경우 매핑 타입으로 변경한다.|
||unmatch: match 패턴과 일치하는 경우에 제외할 패턴을 설정할 수 있다.|
|match_pattern|match 패턴에서 사용할 수 있는 파라미터를 조정한다. 예를 들어, 정규식이나 와일드 패턴 등을 지정한다.|
|path_match, path_unmatch|match, unmatch와 비슷하지만 .이 들어가는 필드명에서 사용한다.|

🌈 분석기

이번에는 분석기에 대해 알아보자. 엘라스틱서치는 전문 검색을 지원하기 위해 역인덱싱 기술을 사용한다. 전문 검색은 장문의 문자열에서 부분 검색을 수행하는 것이며, 역인덱싱은 장문의 문자열을 분석해 작은 단위로 쪼개어 인덱싱하는 기술이다.

역인덱싱을 이용한 전문 검색에서 양질의 결과를 얻기 위해서는 문자열을 나누는 기준이 중요하며, 이를 지원하기 위해 엘라스틱 서치는 캐릭터 필터, 토크나이저, 토큰 필터로 구성되어 있는 분석기 모듈을 갖고 있다.

분석기에는 반드시 하나의 토크나이저가 반드시 포함돼야 하며, 캐릭터 필터와 토큰 필터는 옵션이므로 없어도 되고 여러 개를 함께 사용해도 된다.

🔎 분석기 구성

구성요소설명
캐릭터 필터입력받은 문자열을 변경하거나 불필요한 문자들을 제거한다.
토크나이저문자열을 토큰으로 분리한다. 분리할 때 토큰의 순서나 시작, 끝 위치도 기록한다.
토큰 필터분리된 토큰들의 필터 작업을 한다. 대소문자 구분, 형태소 분석등의 작업이 가능하다.

💡 역인덱싱

분석기를 이해하려면 우선 역인덱싱에 대해 알아야 한다. 분석기는 문자열을 토큰화하고 이를 인덱싱하는데 이를 역인덱싱이라고 한다.

엘라스틱 서치는 도서의 색인 처럼 단어들을 역인덱싱하여 도큐먼트를 쉽게 찾을 수 있다.

💡 분석기 API

엘라스틱서치는 필터와 토크나이저를 테스트볼 수 있는 anlyze라는 이름의 REST API 를 제공하고 있다. 키바나 콘솔을 통해 분석기를 사용해보자.

POST _analyze
{
  "analyzer": "stop",
  "text": "The 10 most loving dog breeds."
}

다음은 분석기 결과를 보여준다.

스톱 분석기는 소문자 변경 토크나이저와 스톱 토큰 필터로 구성되어있다.

text에 적었던 문자열이 [most, loving, dog, breeds] 4개의 토큰으로 분리되었고 ‘the’, ‘10’등은 스톱 분석기에 의해 토큰으로 지정되지 않았다. 스톱 분석기는 불용어를 처리한다. 분석시에 대해서는 다음 주제에서 자세히 다뤄보자.

💡 분석기 종류

엘라스틱서치는 다양한 분석기를 제공한다. 여기서는 가장 대중적인 분석기에 대하여 알아보도록하자.
|분석기|설명|
|----|---|
|standard|특별한 설정이 없으면 엘라스틱서치가 기본적으로 사용하는 분석기다. 영문법을 기준으로 한 스탠다드 토크나이저와 소문자 변경 필터, 스톱 필터가 포함되어 있다.|
||[the, 10, most, loving, dog, breeds]|
|simple|문자만 토큰화한다. 공백, 숫자 하이픈이나 작은따옴표 같은 문자는 토큰화하지 않는다.|
||[the, most, loving, dog, breeds]|
|whitespace|공백을 기준으로 구분하여 토큰화한다.|
||[the, 10, most, loving, dog, breeds]|
|stop|심플 분석기와 비슷하지만 스톱 필터가 포함되었다. 스톱 필터에 의해 ‘the’가 제외 되었다.|
||[the, 10, most, loving, dog, breeds]|

🔎 토크나이저

앞서 분석기는 반드시 하나의 토크나이저를 포함해야 한다고 설명했다. 토크나이저는 문자열을 분리해 토큰화하는 역할을 한다. 자주 쓰이는 토크나이저 종류를 알아보자.

토크나이저설명
standard스탠다드 분석기가 사용하는 토크나이저로, 특별한 설정이 없으면 기본 토크나이저로 사용된다. 쉼표나 점 같은 기호를 제거하며 텍스트 기반으로 토큰화한다.
lowercase텍스트 기반으로 토큰화하며 모든 문자를 소문자로 변경해 토큰화 한다.
ngram원문으로 부터 N개의 연속된 글자 단위를 모두 토큰화한다. 예를 들어, 엘라스틱 서치를 2gram 토큰화 한다면 [엘라, 라스, 스틱, 틱서, 서치] 가 된다. 원문으로부터 검색할 수 있는 모든 조합을 얻어낼 수 있기 때문에 정밀하지만, N개 이하의 글자 수로는 검색이 불가능하며 모든 조합을 추출하기 때문에 저장공단을 많이 차지 한다.
uax_url_email스탠다드 분석기와 비슷하지만 URL이나 이메일을 토큰화하는데 강점이 있다.

🔎 필터

분석기는 하나의 토크나이저와 다수의 필터로 조합된다고 했다. 분석기에서 필터는 옵션으로 하나 이상을 포함할 수 있지만, 없어도 분석기를 돌리는 데 문제는 없다. 필터가 없는 분석기는 토크나이저만 이용해 토큰화 작업을 진행하는데, 대부분 엘라스틱에서 제공하는 분석기들은 하나 이상의 필터를 포함하고 있다. 필터를 통해 더 세부적인 작업이 가능하기 때문이다. 필터 역시 analyze API 를 이용해 테스트해볼 수 있다.

필터는 단독으로 사용할 수 없고 반드시 토크나이저가 있어야 한다.

POST _analyze
{
  "tokenizer": "standard",
  "filter": ["uppercase"],
  "text": "The 10 most Loving dog breeds"
}

스탠다드 토크나이저에 uppercase 라는 토큰 필터를 적용해봤다. 필터는 []안에 복수 개를 적을 수 있는데 필터가 여러 개 있으면 순서대로 필터가 적용된다.

💡 캐릭터 필터

캐릭터 필터는 토크나이저 전에 위치하며 문자들을 전처리하는 역할을 하는데, HTML 문법을 제거/ 변경하거나 특정 문자가 왔을 때 다른 문자로 대체하는 일을 한다.

예를들어, HTML에서   문자가 오면 공백으로 바꾸는 작업등을 캐릭터 필터에서 진행하면 편하다. 엘라스틱서치가 제공하는 대부분의 분석기는 캐릭터 필터가 포함되어 있지 않다. 캐릭터 필터를 사용하기 위해서는 커스텀 분석기를 만들어 사용하는 것이 좋다.

💡 토큰 필터

토큰 필터는 토크나이저에 의해 토큰화 되어 있는 문자들에 필터를 적용한다. 토큰들을 변경하거나 삭제하고 푸가하는 작업들이 가능하다.

다음은 자주 사용하는 토큰 필터의 목록이다.
|필터|설명|
|lowercase|모든 문자를 소문자로 변환한다. 반대로 모든 문자를 대문자로 변환하는 필터는 uppercase 이다.|
|stemmer|영어 문법을 분석하는 필터다. 언어마다 고유한 문법이 있어서 필터 하나로 모든 언어에 대응하기는 힘들다. 한글의 경우 아리랑, 노리 같은 오픈소스가 있다.|
|stop|기본 필터에서 제거하지 못하는 특정한 단어를 제거할 수 있다.|

참고로, stop 과 stemmer 필터는 영어 문법에서만 적용 가능하다.

🔎 커스텀 분석기

커스텀 분석기에 대해 알아보자. 커스텀 분석기는 엘라스틱서치에서 제공하는 내장 분석기들 중 원하는 기능을 만족하는 분석기가 없을 때 사용자가 직접 토크나이저, 필터등을 조합해 사용할 수 있는 분석기다. 필터들의 조합이나 순서에 따라 특별한 형태의 분석기를 만들 수 있다.

PUT custom_analyzer
{
  "settings": {
    "analysis": {
      "filter": {
        "my_stopwords": {
          "type": "stop",
          "stopwords": ["lions"]
        }
      },
      "analyzer": {
        "my_analyzer": {
          "type": "custom",
          "char_filter": [],
          "tokenizer": "standard",
          "filter": ["lowercase", "my_stopwords"]
        }
      }
    }
  }
}

custom_analyzer 라는 새로운 인덱스를 만들었다.

인덱스 설정에 analysis 파라미터를 추가하고 파라미터 밑에 필터와 분석기를 만든 것이다.

보이는 것과 같이 "type": "custom" 을 지정하여 분석기를 커스텀형태로 만들었고, 캐릭터 필터는 지정하지 않았으며 토큰 필터는 기존의 lowercase 와 커스텀 필터인 my_stopwords 를 지정하였다.

추가로 my_stopword 의 필터에 대해서 설명하자면 기본 stop 필터에 불용어로서 “lions” 를 추가한 것이다.

이제 테스트를 해보자.

GET custom_analyzer/_analyze
{
  "analyzer": "my_analyzer",
  "text": "Lions Cats"
}

앞서, 설명했던 것과 같이 lowercase 와 my_stopwords 필터가 적용된 것을 확인해볼 수 있다.

💡 필터 적용 순서

커스텀 분석기에서 필터를 여러 개 사용한다면 필터의 순서에도 주의해야 한다. 필터 배열의 첫 번째 순서부터 필터가 적용된다. 가끔 이 순서가 잘못되면 원하지 않는 결과가 나오기도 한다.

그렇다면, 위에서 만든 custom_analyzer의 필터의 순서를 변경해서 실행해보도록하자.

GET custom_analyzer/_analyze
{
  "tokenizer": "standard",
  "filter": ["my_stopwords", "lowercase"],
  "text": "Lions Cats"
}

결과를 보면 위에서 나왔던 결과와 다른 것을 확인할 수 있다. my_stopwords 필터가 먼저 적용되었기 때문에 불용어인 “lions” 는 토큰에 존재하지 않는 상태이다. 다음으로 lowercase 가 적용되어서 토큰은 [“lions”, “cats”] 가 된다.

profile
WEB STUDY & etc.. HELLO!

0개의 댓글