[Elasticsearch] 매핑과 데이터 타입
Document에 텍스트가 저장될 때, 엘라스틱서치의 analyzer가 작동해 텍스트가 인덱싱되기 전에 analyze, tokenize 과정을 거치게 된다.
인덱스를 생성할 때 각 필드에 대한 analyzer를 설정할 수 있다.
Document가 저장될 때 마다 설정된 analyzer는 텍스트를 토큰화하고 지정된 token filter와 character filter를 통해 텍스트를 처리한 후 인덱싱이 가능한 형태로 저장한다.
character filter - 텍스트의 특정 문자나 패턴을 변경하거나 제거하는 역할을 수행한다. (HTML태그 제거)
tokenizer - 텍스트를 토큰 단위로 분할하는 역할을 수행한다. (단어 단위로 구분, 구분자 단위로 구분)
token filter - 분할된 토큰에 대해 변환을 수행한다. (소문자 변환, 어간 추출)
엘라스틱서치는 기본적으로 여러 analyzer를 제공하지만 필요에 따라 analyzer를 커스텀해서 사용할 수 있다.
PUT /my_index
{
"settings": {
"analysis": {
"analyzer": {
"my_custom_analyzer": {
"type": "custom",
"tokenizer": "whitespace",
"filter": ["lowercase", "stop", "asciifolding"]
}
}
}
}
}
GET /my_index/_search
{
"query": {
"match": {
"field_name": {
"query": "your search text",
"analyzer": "my_custom_analyzer"
}
}
}
}
시스템에서 한국어를 다룰 때는 한국어에 특화된 분석기인 Nori analyzer를 사용한다.
Nori analyzer는 Apache Lucene의 analyzer로, 문장을 형태소 단위로 나눠 어근 / 접사 / 조사 등을 인식하고 분리하는 역할을 수행해 한국어 문장을 형태소 단위로 구분해 저장할 수 있다.
각 필드별로 서로 다른 analyzer를 설정하는 것도 가능하다.
한국어가 입력되는 title 필드에는 Nori analyzer를, 영어가 입력되는 subtitle 필드에는 standard analyzer를..
이런식으로 해당 필드에 적합한 analyzer를 지정해서 사용할 수 있다.
Document가 인덱싱 될 때는 해당 Document의 텍스트 필드가 analyze되고, tokenize 된 결과는 역색인 구조로 저장된다.
RDB의 인덱스와는 다르게 검색 속도를 최적화하기 위해 설게된 구조로, tokenize 된 각 단어는 단어가 포함된 문서 ID와 연결된 구조로 저장된다.
일반적인 인덱스는 문서에서 해당 단어를 찾는 방식으로 동작하지만, 역색인은 단어에서 문서를 찾는 방식으로 동작한다.
단어와 문서 ID만 저장할 뿐만 아니라 관련성 점수 계산에 필요한 추가 정보를 저장해 검색된 문서들이 사용자의 검색어와 얼마나 관련되는지 평가할 수 있고, 평가를 통해 순위를 매겨 의미있는 검색을 구현할 수 있다.
사용자가 입력한 키워드를 포함하는 문서를 역색인 구조를 통해 빠르게 탐색할 수 있다.
분석된 각 토큰은 역색인의 키로 사용되고, 각 토큰에 대해 토큰이 포함된 문서의 ID 리스트가 저장된다.
Document의 각 필드별로 독립된 역색인이 생성되고, 문서 내 각 필드들은 자체적으로 별도의 역색인을 가진다.
해당 필드에 포함된 텍스트를 기준으로 고유한 토큰 집합과 문서 ID를 저장하며, 이 값을 통해 문서를 탐색한다.
역색인이 저장될 때 역색인 내부에서는 단어가 중복되지 않아 저장 공간이 절약된다.
mapping은 엘라스틱서치에서 인덱스 내 각 필드가 어떤 데이터 타입이고 어떻게 다뤄져야하는지를 정의한다.
RDB의 스키마 정도로 생각하면 된다. 매핑을 통해 데이터를 인덱싱하고 검색한다.
엘라스틱서치는 기본적으로 자동 매핑 기능을 제공해 새로운 인덱스에 데이터를 넣을 때 데이터 구조를 기반으로 적절한 매핑을 자동으로 만들어주지만, 자동 매핑이 제대로 이루어지지 않은 경우 수동으로 매핑을 지정할 수 있다.
PUT /my_index
{
"mappings": {
"properties": {
"title": {
"type": "text"
},
"views": {
"type": "integer"
},
"created_at": {
"type": "date",
"format": "yyyy-MM-dd HH:mm:ss"
}
}
}
}
text : analyzer로 텍스트가 토큰화되어 인덱싱되는 필드로, Full Text Scan에 사용된다.
keyword : text 타입처럼 분석되지 않고 전체 값이 하나의 단위로 인덱싱되는 필드이다.
numeric : 자바와 유사하게 integer long float 등 여러 숫자 타입을 지원한다.
object : JSON 객체 구조를 표현하는 필드로 하위 필드가 함께 저장된다.
nested : 중첩된 객체 구조를 개별 문서처럼 인식해 독립적으로 검색할 수 있다.
document의 object 타입은 인덱싱 될 때 평탄화되어 계층적으로 구성된 필드가 점으로 구분된 필드로 인덱싱된다.
부모 문서의 일부로 저장되고 각 하위 필드는 독립적으로 인덱싱된다.
// 매핑
PUT /object_example
{
"mappings": {
"properties": {
"user": {
"type": "object",
"properties": {
"name": { "type": "text" },
"hobbies": { "type": "text" }
}
}
}
}
}
// 문서
POST /object_example/_doc/1
{
"user": {
"name": "John",
"hobbies": ["reading", "swimming"]
}
}
반면 nested 타입은 중첩된 객체를 개별 문서로 인식하고 인덱싱해서 중첩된 객체를 부모의 일부가 아닌 별도의 서브 문서처럼 다룬다.
// 매핑
PUT /nested_example
{
"mappings": {
"properties": {
"user": {
"type": "nested",
"properties": {
"name": { "type": "text" },
"hobby": { "type": "text" }
}
}
}
}
}
// 문서
POST /nested_example/_doc/1
{
"user": [
{
"name": "John",
"hobby": "reading"
},
{
"name": "Jane",
"hobby": "swimming"
}
]
}
PUT /recipes
{
"mappings": {
"properties": {
"description": { "type": "text" },
"ingredients": {
"type": "text",
"fields": {
"keyword": { "type": "keyword" }
}
}
}
}
}
웬만하면 자동 매핑을 피하고 명시적으로 매핑을 정의하자. (strict 사용)
RDB에서 스키마를 작성할 때 많은 시간을 들이는 것 처럼, 엘라스틱서치에서 매핑을 작성할 때도 충분한 설계 시간이 필요하다.
매핑 설계 시 저장되는 데이터의 특성에 맞춰 데이터의 타입과 doc_values / norms / index / analyzer 등 필드 속성을 결정하자.
엘라스틱서치는 한 필드에 여러 타입을 매핑하는 멀티 필드 매핑도 지원하니, 특정 필드를 정확하게 비교해야 하면서 Tokenize 작업도 수행해야 할 때 멀티 필드 매핑 기능을 사용해 매핑을 설계하자.
이미 만들어진 매핑을 수정할 때는 새로운 필드를 추가하거나 메타 정보 정도만 수정할 수 있고, 기존 필드의 데이터 타입을 변경하거나 속성을 변경하는건 불가능하다.
매핑을 미리 정의한 후 Documnet를 색인할 때, 매핑에 정의된 필드를 모두 사용하는건 아니다.
RDB에서는 NULL 값으로 처리되지만, 엘라스틱서치에서는 아예 해당 필드를 저장하지 않는 방식으로 작동해 데이터를 좀 더 유연하게 다룰 수 있다.
불가피하게 매핑을 변경해야 할 때는 기존 데이터들을 다시 인덱싱 해야 한다.
엘라스틱서치는 reindex api를 제공하니 /_reindex 엔드포인트로 기존 데이터를 새로운 인덱스로 쉽게 복사할 수 있다.
POST /_reindex
{
"source": {
"index": "old_index",
"_source": ["field1", "field2"]
},
"dest": {
"index": "new_index"
},
"script": {
"source": """
if (ctx._source.field1 != null) {
ctx._source.field1 = ctx._source.field1.toUpperCase();
}
ctx._source.new_field = 'processed';
"""
}
}
_reindex api에서 _source 필드로 필요한 필드만 선택해서 새로운 인덱스로 복사하거나, script 필드에 직접 스크립트를 작성해 reindex 비즈니스 로직을 구현할 수 있다.
reindex 수행 시 데이터를 새로운 인덱스로 복사하며 기존 인덱스 데이터를 유지하기에 디스크 공간이 부족한 경우 엘라스틱서버 클러스터가 불안정해 질 수 있다.
수행 전 디스크 공간을 꼭 확인하고, 샤드 수를 조정해 디스크 사용량을 최적화하자.
RDBMS도 테이블에 저장되는 데이터를 해당 서버 컴퓨터의 파일 형태로 저장하듯, 엘라스틱서치에서도 실제로 저장되는 데이터는 파일 형태로 관리한다. (.fdt, .fdx, .tim, .tip 확장자 사용)
analyzer 설정 시 검색 정확도를 높이기 위해 Synonym을 설정하기도 한다.
Tokenizer와 Token Filter 설정으로 Synonym을 처리한다.
PUT /synonym_index
{
"settings": {
"analysis": {
"filter": {
"synonym_filter": {
"type": "synonym",
"synonyms": [
"car, automobile",
"tv, television"
]
}
},
"analyzer": {
"synonym_analyzer": {
"tokenizer": "standard",
"filter": [
"lowercase",
"synonym_filter"
]
}
}
}
}
}
설정할 동의어가 많은 경우 별도의 파일에 정리한 후 synonyms 옵션에 파일 경로를 입력해도 된다.
텍스트를 저장하기 전에 Tokenizing - Token Filtering - Indexing 단계를 수행하는데, 동의어 처리는 Token Filtering 단계에서 발생한다.
필터 설정에서 정의된 동의어 목록을 로드해 메모리에 저장하고, 텍스트가 Tokenizer를 통해 분리되면 각 토큰은 Synonym Token Filter를 거친다.
각 토큰에 대해 동의어 사전을 참고해서 Synonym Token Filter가 수행되고 수정된 토큰 목록이 다음 필터나 인덱싱 단계로 전달된다.
.expand 속성을 통해 사용자로부터 받은 입력값을 확장하거나 대체한다.
대체한 후 car로 검색한 경우 automobile 데이터는 검색되지 않는다.
'Database > Elasticsearch' 카테고리의 다른 글
[Elasticsearch] 정보 검색과 검색 쿼리 (0) | 2024.12.11 |
---|---|
[Elasticsearch] 아키텍처와 동작 원리 (0) | 2024.11.07 |
댓글
이 글 공유하기
다른 글
-
[Elasticsearch] 정보 검색과 검색 쿼리
[Elasticsearch] 정보 검색과 검색 쿼리
2024.12.11 -
[Elasticsearch] 아키텍처와 동작 원리
[Elasticsearch] 아키텍처와 동작 원리
2024.11.07