11 minute read

8월말 내가 소방청 프로젝트 (신고 전화내에서 대화를 분석해 긴급출동을 보조하는)에 투입되고,

STT 와 TA 그리고 API 간의 파이프라인을 만들고 띵가띵가? 하려 할 때쯤,

‘승우씨 주소 검색은 어떻게 할꺼에요?’ 라는 질문을 받게 되었다. 난 파이프라인과 STT메세지를 전처리 정도로 생각하고 맘이 편했는데?..

이말을 전해 들은 후, 일을 맡아서가 아닌, 주소를 어떻게 처리할까에 대한 고민으로 하루를 머리를 꽁꽁 싸매고 있었던것 같다.

이게 간단히 생각하면 굉장히 간단해 보인다. STT CER도 굉장히 높고, 주소 키워드를 잡아주는것 까지 되는데, 이걸 그냥 검색하면 되지 않나? 라고 생각할수 있을것이다.

또 카카오, 네이버 네비게이션을 키고 서울시청 찾아줘 을 말하면 굉장히 잘 찾아주는걸 볼수 있다.

자 이렇게 주변에서 답을 다 알려주었는데 왜 온종일 머리를 싸매고 있었는가???

전사데이터를 이곳에 적을수 없으니 내가 간단한 예를 들어 보도록 하겠다, 우리가 출동시켜야할 위치는 어딜까? 한번 찾아보자

’’ 안에 있는 단어는 주소 키워드이다.

EXAMPLE1

RX : 일일구 입니다
TX : 네 지금 '한강' 산책중에 여기 사람이 쓰러져 있어요 빨리와주세요
RX : '한강' 어디신데요?
TX : 여기가 지금 '예시동' 인데요
RX : '예시동 한강공원' 으로 가면 되나요
TX : 아 여기 '한대 병원' 이 보여요
RX : '병원' 이요
TX : 네 '병원' 맞아요
RX : 네 알겠습니다 지금 출발하겠습니다

EXAMPLE2

RX : 일일구 입니다
TX : 여기 친구랑 대화중에 친구가 쓰러진것 같아요. 말이 없어요
RX : 친구가 어디살아요?
TX : 친구 집이 `예시로` 아 아 어디였더라
RX : 천천히 말해주세요 `예시로`
TX : 아 `일일일 다시 이번지`요
RX : `일일일 다시 일번지` 맞나요
RX : 아니요 일말고 이요

굉장히 쉽다.

  • EXAMPLE1 은 ‘한대 병원’
  • EXAMPLE2 는 ‘예시로 111-2 번지’ 로 출동 시키면된다.

근데 이걸 컴퓨터가 어떻게 알게 할까?… EXAMPLE1 에서는 주소 대상물이 7개 EXAPLE2에서는 대상물이 4개 심지어 마지막 RX : 아니요 일말고 이요 는 대상물 키워드에 잡히지도 않았다.

또한 EXAMPLE1의 ‘한대 병원’ 의 정식명칭은 ‘한국대학교 부속병원’ 이였다고 생각해보면 이제 어떻게 접근해야 할지 좀 의문이 생길것이다.

나는 이번 ES 포스팅에선 EXAMPLE2와 같이 행정동/도로명 번지 검색은 룰로 처리를 하였기에, (이것도 ES로 할 방법은 있다.) 이번 포스팅에선 깊게 다루진 않겠다.

내가 다룰 내용은 EXAMPLE1을 어떻게 처리하였는지를 포스팅하고, 어떻게 하면 더 개선할수 있을지를 공유하고자 한다.

00. ES를 사용하였는가

Example1 중요 포인트는 뭘까?

  1. 이전 주소 정보가 추후 나와야할 주소 정보에 영향을 미쳐야 한다.
    • ‘한대 병원’이 예시동에만 있는것이 아닌, 타 대학병원 처럼 여러곳에 위치한다,
  2. ‘한대 병원’은 한국대학교 부속병원’과 동의어이다.
  3. 모든걸 동의어에 정리하는것은 힘들다
    • 한국대학교 부속병원을 토크나이징 하면 [‘한국’, ‘대학교’, ‘부속’, ‘병원’] 이 된다.

ES를 사용하면 이러한 포인트들을 해결할수 있었다.

01. Elasticsearch 개념

이번 포스트를 시작하기전에 es 개념에 대해 다시한번 집고 넘어가자

소개

ES는 Json 기반의 비정형 데이터 분산 검색 및 분석을 지원한다. 실시간 검색 서비스, 분산 및 병렬처리, 그리고 멀티테넌시 기능을 제공하고 다양한 기능을 플러그인 형태로 구현하여 적용할 수 있는것이 특징이다. (플러그인 개발도 다음 포스팅에서 다루어보자) 또한 클러스터로 구성할 수 있기 때문에 검색 대상의 용량이 증가했을 때 대응하기가 매우 수월하다.

특징
  • 분산/확장/병령처리 Elasticsearch 구성시 보통 3개 이상의 노드와 클러스터로 구성하며, 데이터를 샤드로 저장시 클러스터 내 다른 호스트에 복사본을 저장해 놓기 때문에 하나의 노드가 죽거나 샤드가 깨져도 복제되어 있는 다른 샤드를 활용하기 때문에 데이터의 안정성이 보장된다. 또한 데이터의 분산과 병렬처리가 되므로 실시간 검색 및 분석이 가능하다. 노드를 수평적으로 늘리는것또한 가능하여 더많은 용량이 필요한 경우 노드를 클러스터에 추가하는것이 가능하다.
  • 고가용성 Elasticsearch는 동작중 죽은 노드를 감지 하고 삭제하며 사용자의 데이터를 안전하고 접근 가능하도록 유지하기 때문에, 동작중에 일부 노드에 문제가 생기더라도 문제없이 서비스가 가능하도록 한다.
  • 멀티 테넌시 클러스터는 여래개의 인덱스들을 저장하고 관리하며, 하나의 쿼리나 그룹 쿼리로 여러 인덱스의 데이터를 검색하는것이 가능하다.
  • 스키마의 미존재
  • 플러그인 형태로 구현 검색 엔진을 직접 수행하지 않고, 필요한 기능에 대한 플러그인을 적용하여 기능을 확장하는것이 가능하다. 예를 들면 외부에서 제공하는 형태소 분석기나 REST API를 구현하여 적용하는것이 가능하다.
Elasticsearch 구조

논리적 구조

  • 도큐먼트(Document) 엘라스틱서치 데이터 최소단위(RDBMS의 Row) Json 오브젝트 하나를 가르켜 Document라 한다. 하나의 Documents는 다양한 필드로 구성되며, 이 필드에는 데이터 필드에 해당하는 데이터 타입이 들어간다.
  • 필드
  • 매핑 매핑은 필드와 필드의 속성을 정의하고 색인 방법을 정의한다. 매핑 정보에 여러가지 데이터 타입이 지정가능하며, 필드명 자체는 중복이 불가능하다

물리적 구조

물리적구조

  • 노드 노드는 Elasticsearch 클러스터에 포함된 단일 서버 로서 데이터를 저장하고 클러스터의 색인화 및 검색 기능에 참여한다. 노드는 클러스터처럼 이름으로 식별이 되며, 원한다면 특정 노드 이름으로 정의하는 것이 가능하다. RDBMS 와는 다르게 여러개의 노드로 구성된 엘라스틱서치 클러스터는 관리하는 데이터가 커져도 노드를 추가시켜 대용량 처리를 가능하게 해준다. 개발 및 테스트 환경서는 Elasticsearch 의 단일 노드 구성으로도 충분하다, 실제 운영환경에서는 목적에 맞게 노드를 적절히 설정해 운영해야 한다. 노드의 종류
    • 마스터노드 클러스터 관리 노드. 노드추가/제거, 인덱스 생성/삭제 등 클러스터의 전반적 관리 담당. 여러개의 마스터 노드를 설정하면 하나의 마스터 노드로 작동된다.
    • 데이터노드 도큐먼트가 저장되는 노드, 데이터가 분산 저장되는 물리적 공간인 샤드가 배치되는 노드, 색인/검색/통계 등 데이터 작업 수행(리소스가 많이 필요) 마서터와는 분리가 필요
    • 코디네이팅 노드 사용자의 요청을 받고 라운드로빈 방식으로 분산시켜주는 노드, 클러스터에 관련된 것은 마스터 노드로 넘기고 데이터 관련된 것은 데이터 노드로 넘긴다.
    • 인제스트 노드 문서 전처리 작업 수행. 인덱스 생성 전 문서의 형식 변경을 다양하게 할수가 있다.
  • 샤드 인덱스 내부에는 색인된 데이터들이 존재한다. 이 데이터들은 하나로 뭉쳐서 존재하지 않고 물리적인 공간에 여러개의 부분들로 나뉘어 존재한다. 이러한 부분을 샤드 라 한다. 엘라스틱서치는 기본적으로 인덱스를 5개의 샤드로 나누어 저장한다. 인덱스를 여러 샤드로 나누어 저장하기 때문에 콘텐츠 볼륨의 수평/분할/확장 이 가능하다. 작업이 여러 샤드에서 수행하기 때문에 처리량을 늘릴수 있다. 샤드는 프라이머리 샤드와 레플리카 샤드로 나뉜다.
    • 프라이머리 샤드: 데이터의 원본, 엘라스틱 서치에서 데이터 업데이트 요청을 날리면 반드시 프라이머리 샤드에 요청을 하고, 해당 내용은 레플리카에 복제가 된다. 검색 성능 향상을 위해 클러스터의 샤드 갯수를 조정하는 튜닝을 한다.
    • 레플리카 샤드 : 프라이머리 샤드의 복제본이다. 기존 원본 데이터가 무너졌을 때 그 대신 쓰면서 장애 극복 역할을 수행한다. 기본적으로 원본 프라이머리 샤드와 동이랗ㄴ 노드에 배정되지 않는다.
  • 세그먼트 세그먼트란 엘라스틱에서 문서의 빠른 검색을 위해 설계된 자료구조 이다. 각 샤드는 다수의 세그먼트로 구성된다. 엘라스틱서치에서 데이터를 저장하면, 엘라스틱서치는 이것을 메모리에 모아둔다. 그후 새로운 세그먼트를 디스크에 기록하여 검색을 refresh한다. 이로 인해 새로운 검색 가능한 세그먼트가 만들어지게된다. 세그먼트는 불변의 성질을 가지고 있기 때문에 데이터가 업데이트되면 실제로는 삭제되었다고 마크하고 새로운 데이터를 가르킨다. 그리고 삭제되었다고 마크된 데이터는 디스크에 남아있다가 백그라운드에서 주기적으로 또는 특정 임계치를 넘기면 더이상 필요가 없어진 데이터들을 정리하고 새로운 세그먼트로 병합을 하게된다. 이를 세그먼트 병합(segment Merge) 라 한다.

02. ElasticSearch Setting과 Mapping

es Setting과 Mapping 은 이전 es01 포스트에서 다룬적이 있다. 하지만 여기선 nori anlayzer 를 알아보기 위한 수단이였을분 이곳에서 자세히 다루어 보자.

settings 와 mappings 는 인덱스의 정보 단위이다.

인덱스를 처음 생성한뒤 GET 인덱스를 하면 설정 settings 그리고 mappings 정보 확인이 가능하다.

settings 또는 mappings 정보를 따로 보고 싶다면 <인덱스>/_settings 또는 _mappings 를 추가해 확인이 가능하다.

처음 인덱스를 정의하면 샤드수나 복제본 수 같은 정보는 settings 아래 설정이 자동으로 생성이 된다. (7점대 버전부턴 두개 모두 디폴트가 1이다.)

대부분의 설정값들은 한번 지정되면 변경이 되지 않는다.

number_of_shards, number_of_replicas

샤드와 레플리카 https://esbook.kimjmin.net/03-cluster/3.2-index-and-shards

number_of_shards 설정은 인덱스 처음 설정시 한번 지정하면 변경이 불가하다. 샤드 수를 변경하려면 인덱스를 새로 정의하고 기존 인덱스 데이터를 재색인 해야 한다.

number_of_replica 는 다이나믹하게 변경이 가능하다. 이미 선언된 <인덱스>의 복제본 카피 개수를 1에서 2로 변경하려면 인덱스명 뒤에 _settings api 로 접근해서 변경할 설정만 입력해서 변경이 가능하다.

refresh_interval 자주 사용되는 settings 에서 refresh_interval 이 있다. refresh_interval는 Elasticsearch 에서 세그먼트가 만들어지는 refresh 타임을 설정하는 값이다. defalut는 1s 이며 setting의 index아래 설정하며 number_of_replica와 마찬가지로 자유롭게 설정 변경이 가능하다.

엘라스틱 서치 세그먼트
엘라스틱 서치는 documnet를 엘라스틱 인덱스로 만든뒤 샤드로 분리하여 보관한다.
각 엘라스틱 서치 샤드는 루씬의 인덱스이기도 하다.
루씬은 새로운 데이터를 엘라스틱 서치 인덱스에 저장할 때 세그먼트를 생성하는데, 루씬의 인덱스 조각인 이 세그먼트를 조합하여 저장한 데이터의 검색이 이루어진다. 
루씬은 순차적으로 세그먼트를 검색하므로 세그먼트 수가 많아지면 속도가 느려진다. 

루씬 세그먼트 설명 https://icarus8050.tistory.com/51

루씬의 flush = 엘라스틱의 refresh 세그먼트 생성시 커널 시스템 개시에 세그먼트가 캐시돼 읽기가 가능하다. refresh가 되어야 읽을수 있는 상태가 된다. 즉 인덱스를 새로고침 함으로써 새로 추가한 데이터의 검색이 가능해진다.

refresh 타임이 1초라면 es 클러스터에 존재하는 모든 샤드가 기본적으로 1초마다 한번씩 refresh 작업이 수행된다.

analyzer, tokenizer, filter

settings 에서는 애널라이저, 토크나이저, 토큰 필터 역시 setting 내부에서 정의한다. 나중에 만들 plugin 도 이러한 기능들을 위함이다. 그러므로 각각의 역할을 반드시 알고 넘어가도록 하자

Analayzer

Elasticsearch 에서는 분석된 문장을 _analyzer API를 이용해서 확인할 수 있다. 토크나이저는 tokenizer, 토큰 필터는 filter 항목의 값으로 입력하면 된다. 토크나이저는 하나만 적용되기 때문에 바로 입력하고, 토큰필터는 여러개가 적용이 가능하기 때문에 [] 안에 배열 형식으로 입력한다..

Elasticsearch에 텍스트 입력시 필드를 인덱싱하고, Documents화 할때 루씬엔진에 의해 텍스트가 분석되어 입력이 된다.

이때 텍스트를 분석하는 엔진을 Analyzer라고 한다. 루씬 에서 제공하는 Analyzer는 하나의 Tokenizer와 다수의 Filter로 구성된다. Filter는 CharFiltd와 TokenFilter가 있다.

  • CharFilter는 입력된 문자열에서 불필요한 문자를 normalization하기위해 사용된다. 기본적으로 CharFilter에 의해 공백 콤마 등의 문자가 삭제 된다.
  • TokenFilter는 tokenizer에 의해 분리된 token에 대한 Filter 처리를 하게 된다 .

Tokenizer는 CharFitler와 비슷한 일을 하지만 입력값이 CharFitler가 입력이 character stream에 반해 Tokenizer는 token stream을 사용하게 된다

fitler 와 tokenizer

처음 Elasticsearch 를 설치하면 standard analyzer를 사용하게 되는데 여기에는 CharFilter가 없다. 따라서 모든 값이 바로 Tokenizer로 전달되게 된다,

fitler 와 tokenizer

TokenFilter : Text를 Token 으로 분리하는 필터이다. Analyzer : Filter와 Tokenizer를 포함하고 있는 컴포넌트이다.

PUT my_index
{
  "settings": {
    "analysis": {
      "analyzer": {
        "my_analyzer": {
          "type": "custom",
          "char_flter": [ "...", "..." ... ]
          "tokenizer": "...",
          "filter": [ "...", "..." ... ]
        }
      },
      "char_filter":{
        "my_char_filter":{
          "type": "…"
          ... 
        }
      }
      "tokenizer": {
        "my_tokenizer":{
          "type": "…"
          ...
        }
      },
      "filter": {
        "my_token_filter": {
          "type": "…"
          ...
        }
      }
    }
  }
}

analysis 내부에 analyzer, char_filter tokenizer, filter 를 입력하고 각자의 내부에 임의의 이름을 주어 각 기능들을 정의한다. 각 내부에 하나 이상을 생성할 수도 있으며 애널라이저에서는 사용자가 정의한 토크나이저 토큰필터를 사용하거나 es 내부의 것을 사용하는것이 가능하다.

analysis 내용은 한번 선언후에는 변경이 불가하다. 이미 만들어진 인덱스에 애널라이저나 토크나이저 등을 추가하거나 사전을 변경하려면 인덱스를 먼저 _close한후 추가하고 다시 _open 하여 적용할수 있다.

03. mappings

mappings는 버전마다 변경된게 많은것 같다. 이번 mappings의 경우 7 버전에 맞추어 설명하도록 한다.

동적 매핑

mappings는 es를 활용하면서 가장 손이 많이가는 작업중 하나이다. es 는 동적 매핑을 지원하기 때문에 미리 정의하지 않아도 인덱스에 새로 추가하면 자동으로 매핑이 생성된다. (솔직히 type을 미리 지정하고 가는게 좋다고 본다.)

es의 매핑이 동적으로 생성 될 때는 필드의 값을 보고 타입을 예상하는데, 그 필드가 포함될 수 있는 가장 넓은 범위 형태의 데이터 타입을 선택한다. 즉 int 가 아닌 long 이 된다는 말이다. 또한 iso date type 까지 지원하고 있다.

매핑 정의

데이터가 입력되어 자동으로 매핑이 생성되기 전에 미리 인덱스 매핑을 정의해 놓으면 정의 해 놓은 매핑에 맞추어 데이터가 입력이 된다. (권장)

PUT <인덱스명>
{
  "mappings": {
    "properties": {
      "<필드명>":{
        "type": "<필드 타입>"
        … <필드 설정>
      }
      …
    }
  }
}

이미 만들어진 매핑에 필드를 추가하는것은 가능하지만 이미 만들어진 필들르 삭제 하거나 필드의 타입을 변경하는 것은 불가하다. 필드 변경이 필요한 경우 인덱스를 새로 정의해야 한다.

PUT <인덱스명>/_mapping
{
  "properties": {
    "<추가할 필드명>": { 
      "type": "<필드 타입>"
      … <필드 설정>
    }
  }
}

당연히 필드명 중복은 불가하다.

필드 추가는 최상위 필드와 object 타입의 내부필드, 그리고 다중필드 (multi-feild) 역시 추가가 가능하다.

es 필드에 설정 가능한 타입으로는, 일반적으로 자바 언어 레벨에서 지원하는 기본 타입 들과 es 루씬 레벨에서 추상화된 확장 타입 들이 존재한다.

문자열 text and keyword

text 와 keyword 의 차이에 대해 알아보자, es 에서 선언이 가능한 문자열 타입에는 text 와 keyword 두가지가 존재한다. 이 둘은 텍스트 분석의 적용 여부에 따라 달라진다. 인덱스를 생성할 때 매핑에 필드를 미리 정의 하지 않으면 동적 문자열 필드가 생성될때 text, keyword 필드가 다중필드로 같이 생성이 된다.

text

text 타입은 문자열을 텀 단위 로 쪼개 역 색인 (inverted index) 구조를 만든다. 보통은 풀텍스트 검색에 사용할 문자열 필드들을 text 타입 으로 지정한다. text 필드에 설정 가능한 옵션들은 다음과 같은것들이 있다.

  • analyzer
    • 색인에 사용할 애널라이저를 입력한다
    • 디폴트는 standard 애널라이저
    • 토크나이저, 토큰 필터들을 따로 지정할수는 없다. 필요시에는 settings 에 따로 정의를 해 두고 사용이 가능하다.
  • search_analyzer
    • 기본적으로 text 필드는 match 쿼리로 검색 할 때 색인에 사용한 동일한 애널라이저로 검색 쿼리를 분석하게 된다.
    • search_analyzer 를 따로 지정하게 되면 검색시에는 색인에 사용한 애널라이저가 아닌 다른 애널라이저를 사용하여 검색을 한다.
  • index
    • 디폴트는 true
    • false 로 설정하게 되면 해당필드는 색인을 만들지 않아 검색이 불가능하다
  • boost
    • 디폴트는 1
    • 값이 1보다 높다면 풀텍스트 검색시 해당 필드 스코어 점수에 가중치를 부여 한다.
    • 값이 1보다 낮으면 풀텍스트 검색시 해당 필드 스코어 점수에 가중치가 내려간다.
  • fielddata
    • 디폴트는 false
    • true로 설정하면 해당필드 색인된 텀들을 가지고 집계(aggression) 또는 정렬(sorting) 이 가능해진다.
    • 다이나믹 설정이 가능 하다.
    • true 설정시 쿼리에 메모리 사용량이 많아져 권장하지는 않는 옵션
    • 집계와 정렬은 항상 keyword 필드를 사용하는것을 권장
keyword

keyword 타입은 입력된 문자열을 하나의 토큰으로 저장한다. text 타입에 keyword 애널라이저를 적용한 것과 동일하다. 보통은 집계 또는 정렬에 사용할 문자열 필드를 keyword로 지정한다. keyword 필드에 설정 가능한 옵션들은 아래와 같다.

  • index, boost -text 와 동일
  • doc_values
    • 디폴트는 true
    • keyword 값들은 기본적으로 집계나 정렬에 메모리를 소모하지 않고 열기반 저장소 (columnar store) 를 만들어 저장한다. 이 값을 false 로 한다면 doc_values에 값을 저장하지 않아 집계 정렬이 불가능해진다.
  • ignore_above
    • 디폴트는 2,147,483,647
    • 동적 매핑으로 생성되면 256
    • 설정된 길이 이상의 문자열은 색인을 하지 않는다, 검색 집계가 불가능 하다.
    • source 에는 남아있기 때문에 다른 필드 값을 쿼리해서 가져와 사용한다.
  • normalizer
    • keyword 필드는 애널라이저를 사용하지 않고 노말라이저 적용이 가능하다.
    • 노말라이저는 애널라이저와 유사하게 settings 에서 정의한다.
    • 토크나이저는 정용이 불가능하고, 캐릭터 필터와 토큰 필더만 적용해서 사용이 가능하다.

04. 간단한 쿼리들 term, match, match_pharse

term 과 match, match_phrase 의 사용법을 알고 사용하도록 하자,

term

사전적 의미로 보면 term은 용언 이라는 뜻을 가지고 있다. 해당 content의 inverted index에 저장되는 token 들 중에서 쿼리의 키워드와 일치하는 녀석이 있는지 찾아 준다, 문서의 tag등을 검색할때 사용된다.

text 가 아래와 같이 토크나이징 된다면

curl -XPOST 'localhost:9200/my_index/_analyze?pretty' -H 'Content-Type: application/json' -d'
{
  "analyzer": "my_custom_analyzer",
  "text": "여러개의 물건들"
}

{
  "tokens" : [
    {
      "token" : "여러",
      "start_offset" : 0,
      "end_offset" : 2,
      "type" : "MM",
      "position" : 0
    },
    {
      "token" : "개",
      "start_offset" : 2,
      "end_offset" : 3,
      "type" : "NNB",
      "position" : 1
    },
    {
      "token" : "물건",
      "start_offset" : 5,
      "end_offset" : 7,
      "type" : "NNG",
      "position" : 2
    },
    {
      "token" : "물건들",
      "start_offset" : 5,
      "end_offset" : 8,
      "type" : "NNG",
      "position" : 2
    }
  ]
}

term 쿼리를 통해서 검색 결과에 포함하려면 위에 결과와 동일하게 검색을 해주어야 한다. example

{
  "query": {
    "bool": {
      "must": [
        {
          "term": {
            "content": "여러"
          }
        }
      ]
    }
  }
}
match

term과 마찬가지로 inverted index에 저장되는 token들 중에서 일치하는 녀석이 있는지 찾아준다. 위의 term 과의 차이점은 검색하는 keyword를 match는 analyze 한다. 이 analyze 한 결과의 token 들 중에서 하나라도 일치하면 결과 doc에 포함이 된다.

match_phrase

phrase 는 사전적으로 라는 의미를 가지고 있다. 구는 둘 또는 그이상 의 어절로 이루어져 한 덩어리로써 절이나 문장의 성분이 되는 동일한 말의 단위라고 한다. 둘이상이라는 말이 포인트이다. 즉 match 가 token들 중에 일치하는 keyword가 하나라도 존재한 document를 검색한다면, match_phrase 가 검색 match처럼 keyword 를 analyzer 하는 것은 동일하다. __ 그러나 그 결과 token 들이 모두 존재하고, 순서도 동일한 document만을 검색 결과에 포함한다는 차이__ 가 있다.