728x90
엘라스틱서치 실무 가이드 2장을 요약한 내용입니다.
2.1 엘라스틱서치를 구성하는 개념
2.1.1 기본 용어
- 인덱스
- 인덱스는 데이터 저장 공간
- 하나의 인덱스는 하나의 타입
- 하나의 물리적인 노드에 여러 개의 논리적인 인덱스를 생성 가능
- 검색시 인덱스 이름으로 문서 데이터를 검색
- 여러 개의 인덱스를 동시에 검색 가능
- 인덱스 생성 시 기본적으로 5개의 프라이머리 샤드와 1개의 레플리카 샤드 셋트를 생성
- 인덱스 이름은 모두 소문자
- 추가, 수정, 삭제, 검색은 RESTful API로 수행
- 인덱스가 없는 상태에서 데이터가 추가된다면 데이터를 이용해 인덱스가 자동으로 생성
- 샤드
- 색인된 문서는 하나의 인덱스에 저장
- 인덱스 내부에 색인된 데이터는 물리적인 공간에 여러 개의 파티션으로 나뉘어 구성
- 파티션을 샤드라고 부른다.
- 샤드로 문서를 분산 저장하고 있어 데이터 손실 위험을 최소화
- 타입
- 인덱스의 논리적 구조를 의미
- 6.1버전부터는 인덱스당 하나의 타입만 사용
- 문서
- 데이터가 저장되는 최소의 단위
- JSON 포멧으로 데이터가 저장
- 문서는 다수의 필드로 구성돼 있음
- 데이터의 형태에 따라 용도에 맞는 데이터 타입을 정의 해야함
- 중첩 구조를 지원하기 때문에 문서 안에 문서를 지정하는 것이 가능
- 필드
- 문서를 구성하기 위한 속성
- 하나의 필드는 목적에 따라 다수의 데이터 타입을 가질 수 있음
- ex) 영화 제목을 검색할때 초성을 이용한 검색이 모두 지원되도록 제목 필드는 2개의 데이터 타입을 가져야 한다.
- 매핑
- 문서의 필드와 필드의 속성을 정의하고 그에 따른 색인 방법을 정의하는 프로세스
- 인덱스의 매핑 정보에는 여러 가지 데이터 타입을 지정할 수 있지만 필드명은 중복해서 사용할 수 없다.
2.1.2 노드의 종류
- 클러스터는 물리적인 노드 인스턴스들의 모임이라 할 수 있다.
- 클러스터는 모든 노드의 검색과 색인 작업을 과장하는 논리적인 개념
- 분산 처리를 위해 다양한 형태의 노드들을 조합해서 클러스터를 구성
- 마스터 노드가 전체적인 클러스터를 관리
- 데이터 노드가 실제 데이터를 관리
- 설정에 따라 4가지의 유형의 노드를 제공
마스터 노드
- 노드 추가와 제거 같은 클러스터 관리
- 네트워크 속도가 빠르고 지연이 없는 노드를 마스터 노드로 선정
- 다수의 노드를 마스터 노드로 설정할 수 있지만 결과적으로 하나의 노드만이 마스터 노드로 선출되어 동작
- conf/elasticsearch.yml 파일 설정 node.master: true node.data: false node.ingest: false search.remote.connect: false
데이터 노드
- 데이터가 실제로 분산 저장되는 물리적 공간인 샤드가 배치되는 노드
- 색인 작업은 CPU, 메모리, 스토리지 같은 컴퓨팅 리소스를 많이 소모하기 때문에 리소스 모니터링이 필요
- 데이터 노드는 가능한 마스터 노드와 분리해서 구성하는 것이 좋다
- conf/elasticsearch.yml 파일 설정 node.master: false node.data: true node.ingest: false search.remote.connect: false
코디네이팅 노드
- 데이터 노드, 마스터 노드, 인제스트 노드의 역활을 하지 않고 들어온 요청을 단순히 라운드로빈 방식으로 분산 시켜주는 노드
- 클러스터 관련 요청은 마스터 노드에 전달
- 데이터 관련 요청은 데이터 노드에 전달
- conf/elasticsearch.yml 파일 설정 node.master: false node.data: false node.ingest: false search.remote.connect: false
인제스트 노드
- 문서의 전처리 작업
- 스크립트로 전처리 파이프라인을 구성하고 실행 할 수 있다.
conf/elasticsearch.yml 파일 설정
node.master: false
node.data: false
node.ingest: true
search.remote.connect: false
2.1.3 클러스터, 노드, 샤드
[그림 2.2] 클러스터와 노드
- 하나의 엘라스틱서치 클러스에 노드#1, 노드#2로 총 2개의 물리적 노드가 존재
- 인덱스의 문서를 조합할 때 마스터 노드를 통해 2개의 노드를 모두 조회해서 각 데이터를 취합 후 결과를 하나로 합쳐서 제공
- 클러스터에 있는 노드는 실시간으로 추가, 제거가 가능
- 가용성이나 확장성 측면에서 유연
프라이머리 샤드 3개, 레플리카 샤드 0세트 구성
- 샤드는 분산된 데이터에 따라 순차적인 번호를 가진다
- 프라이머리 샤드는 안정성을 위해 하나의 노드에 하나씩 분산 저장
- 인덱스에 다수의 문서를 색인하면 문서는 3개의 샤드로 골고루 분산 저장
프라이머리 샤드 6개, 레플리카 샤드 0세트 구성
- 3개의 노드에 프라이머리 샤드 6개가 노드당 2개씩 배치된다.
- 색인시 6개의 샤드에 데이터가 분산
프라이머리 샤드 3개, 레플리카 샤드 1세트 구성
- 레플리카 샤드를 1세트를 설정했으므로 3개의 레플리카 샤드가 생성
- 장애 시 레플리카 샤드를 이용해 샤드를 복구
- 프라이머리 샤드의 복제본이 존재하기 때문에 물리적인 노드 하나가 죽더라도 나머지 노드 2개가 전체 데이터를 복구할 수 있다.
- 장애가 발생하면 마스터 노드는 데이터를 재분배하거나 레플리카 샤드를 프라이머리 샤드로 승격시켜 서비스 중단 없는 복구가 가능
- 장애극복 상황을 염두에 두고 노드와 샤드의 수를 적절히 구성해야 한다.
2.2 엘라스틱서치에서 제공하는 주요 API
API의 종류
- 인덱스 관리 API: 인덱스 관리
- 문서 관리 API: 문서의 추가/수정/삭제
- 검색 API: 문서 조회
- 집계 API: 문서 통계
스키마리스 기능은 가급적이면 사용하지 말자
- 스키마리스라는 강력한 기능을 제공
- 다양한 형태의 비정형 데이터를 하나의 인덱스로 구성 가능
- 성능과 밀접한 관계가 있으므로 특수한 상황에서만 사용
- 스키마리스를 사용해야 한다면 데이터 구조 및 검색 방식을 확실히 이해해야 한다.
- 테스트를 위해 인덱스를 생성하지 않고 데이터를 색인해보자
- 인덱스 매핑 정보가 정의 되지 않기 때문에 JSON 형식의 키-값을 분석해서 필드명과 각종 속성 정보를 자동으로 생성
PUT /movie/_doc/1 HTTP/1.1 { "movieCd": 1, "movieNm": "살아남은 아이", "movieNmEn": "Last Child", "prdtYear": "2017", "openDt": "", "typeNm": "장편", "prdtStartNm": "기타", "nationAlt": "한국", "genrAlt": "드라마,가족", "repNationNm": "한국", "repGenerNm": "드라마" } # 응답 { "_index": "movie", "_type": "_doc", "_id": "1", "_version": 1, "result": "created", "_shards": { "total": 2, "successful": 1, "failed": 0 }, "_seq_no": 0, "_primary_term": 1 }
- movie 인덱스가 생성, _id가 1인 문서가 추가
- 인덱스가 자동으로 생성되어 세부적인 필드 정보가 매핑되지 않음
- 특정 단어를 검색할 때 검색 결과에서 누락되는 문제가 발생할 가능성이 높음
- 인덱스의 구성 정보 조회
- 기본적으로 모든 필드가 text 타입과 keyword 타입을 동시에 제공하는 멀티필드 기능으로 구성
- 특정 필드는 text 타입만 필요할 수 있고 keyword 타입만 필요할 수 있다
- 이런 경우 데이터 공간 낭비
GET /movie HTTP/1.1 # 응답 { "movie": { "aliases": {}, "mappings": { "_doc": { "properties": { "genrAlt": { "type": "text", "fields": { "keyword": { "type": "keyword", "ignore_above": 256 } } }, "movieCd": { "type": "long" }, "movieNm": { "type": "text", "fields": { "keyword": { "type": "keyword", "ignore_above": 256 } } }, "movieNmEn": { "type": "text", "fields": { "keyword": { "type": "keyword", "ignore_above": 256 } } }, "nationAlt": { "type": "text", "fields": { "keyword": { "type": "keyword", "ignore_above": 256 } } }, "openDt": { "type": "text", "fields": { "keyword": { "type": "keyword", "ignore_above": 256 } } }, "prdtStartNm": { "type": "text", "fields": { "keyword": { "type": "keyword", "ignore_above": 256 } } }, "prdtYear": { "type": "text", "fields": { "keyword": { "type": "keyword", "ignore_above": 256 } } }, "repGenerNm": { "type": "text", "fields": { "keyword": { "type": "keyword", "ignore_above": 256 } } }, "repNationNm": { "type": "text", "fields": { "keyword": { "type": "keyword", "ignore_above": 256 } } }, "typeNm": { "type": "text", "fields": { "keyword": { "type": "keyword", "ignore_above": 256 } } } } } }, "settings": { "index": { "creation_date": "1695202798159", "number_of_shards": "5", "number_of_replicas": "1", "uuid": "r5Fu62lSTHWbY9cUQx5R6Q", "version": { "created": "6040399" }, "provided_name": "movie" } } } }
- 스키마리스는 편리하지만 특수한 상황에 제한적으로 사용
- 실무에서는 대부분 사용하지 않음
- 데이터가 대부분 복잡한 구조를 갖기 때문에 검색 품질이 떨어지거나 성능상 문제 발생 할 수 있음
- 스키마리스를 이용해서 색인한다면 기본적으로 text 타입의 Standard Analyzer를 사용하는 데이터 타입으로 정의될 것이다.
- 검색을 위해서는 Standard Analyzer가 분리한 토큰 그대로 입력해야함
- 원하는 결과를 얻기 위해서는 한글 형태소를 분석하는 분석기를 사용하도록 데이터 타입을 직접 정의
<aside> 👉 스키마리스 기능을 사용하고 싶지 않더라도 인덱스를 설정하지 않고 실수로 데이터를 색인하면 엘라스틱서치는 인덱스를 자동 생성한다. 실수를 방지하기 위해 스키마리스 기능을 명시적으로 사용하지 않도록 설정 하는 것이 가능 → action.auto_create_index를 false로 설정
</aside>
2.2.1 인덱스 관리 API
인덱스 생성
- 인덱스를 생성할 때는 매핑이라는 세부 설정을 이용
- 매핑은 문서와 문서에 포함된 필드, 필드 타입 등을 세세하게 지정하는 것이 가능한 설정 방식
- 인덱스 생성시 매핑정보를 추가 가능
- 한번 생성된 매핑 정보는 변경할 수 없다
- 잘못 생성했거나 변경해야 하는 경우 데이터를 삭제하고 다시 색인 해야함
- 단순한 문자열로 저장하고 싶을 경우 keyword 타입을 사용
- 형태소 분석을 원할 경우 text 타입 사용
PUT /movie HTTP/1.1
{
"settings": {
"number_of_shards": 3,
"number_of_replicas": 2
},
"mappings": {
"_doc": {
"properties": {
"movieCd": { "type": "integer" },
"movieNm": { "type": "text" },
"movieNmEn": { "type": "text" },
"prdtYear": { "type": "integer" },
"openDt": { "type": "date" },
"typeNm": { "type": "keyword" },
"prdtStartNm": { "type": "keyword" },
"nationAlt": { "type": "keyword" },
"genrAlt": { "type": "keyword" },
"repNationNm": { "type": "keyword" },
"repGenerNm": { "type": "keyword" }
}
}
}
}
# 응답
{
"acknowledged": true,
"shards_acknowledged": true,
"index": "movie"
}
인덱스 삭제
- 인덱스를 한번 삭제하면 다시는 복구할 수 없음
- 삭제는 신중하게
DELETE /movie HTTP/1.1
# 응답
{
"acknowledged": true
}
2.2.2 문서 관리 API
- 문서를 색인하고 조회, 수정, 삭제를 지원하는 API
- 검색을 위한 다양한 검색 패턴을 지원하는 Search API를 별도로 제공
- 색인된 문서의 ID를 기준으로 한건의 문서를 다뤄야 하는 경우 문서 관리 API를 사용
- Index API: 한 건의 문서를 색인
- Get API: 한 건의 문서를 조회
- Delete API: 한 건의 문서를 삭제
- Update API: 한 건의 문서를 업데이트
- 클러스터를 운영하다 보면 다수의 문서를 처리해야 하는 경우도 종종 발생 한다.
- Multi Get API: 다수의 문서를 조회
- Bulk API: 대량의 문서를 색인
- Delete By Query API: 다수의 문서를 삭제
- Update By Query API: 다수의 문서를 업데이트
- Reindex API: 인덱스의 문서를 다시 색인
문서 생성
- 문서의 id를 1을 지정
PUT /movie/_doc/1 HTTP/1.1
{
"movieCd": 1,
"movieNm": "살아남은 아이",
"movieNmEn": "Last Child",
"prdtYear": "2017",
"openDt": "2017-10-29",
"typeNm": "장편",
"prdtStartNm": "기타",
"nationAlt": "한국",
"genrAlt": "드라마,가족",
"repNationNm": "한국",
"repGenerNm": "드라마"
}
# 응답
{
"_index": "movie",
"_type": "_doc",
"_id": "1",
"_version": 1,
"result": "created",
"_shards": {
"total": 3,
"successful": 1,
"failed": 0
},
"_seq_no": 0,
"_primary_term": 1
}
문서 조회
- 문서의 Id를 1로 지정했기 때문에 1을 지정해서 조회
GET /movie/_doc/1 HTTP/1.1
# 응답
{
"_index": "movie",
"_type": "_doc",
"_id": "1",
"_version": 1,
"found": true,
"_source": {
"movieCd": 1,
"movieNm": "살아남은 아이",
"movieNmEn": "Last Child",
"prdtYear": "2017",
"openDt": "2017-10-29",
"typeNm": "장편",
"prdtStartNm": "기타",
"nationAlt": "한국",
"genrAlt": "드라마,가족",
"repNationNm": "한국",
"repGenerNm": "드라마"
}
}
문서 삭제
- 문서의 ID를 지정하고 DELETE 메서드를 이용해 삭제
DELETE /movie/_doc/1 HTTP/1.1
# 응답
{
"_index": "movie",
"_type": "_doc",
"_id": "1",
"_version": 2,
"result": "deleted",
"_shards": {
"total": 3,
"successful": 1,
"failed": 0
},
"_seq_no": 1,
"_primary_term": 1
}
문서의 ID를 지정하지 않고 문서를 생성
- ID 값을 직접 지정하지 않으면 자동으로 _id값이 생성이 된다.
- UUID를 통해 무작위로 생성
- 엘라스틱서치와 데이터베이스의 데이터를 동기화하기 어려움
- ID값은 데이터베이스 테이블의 식별 값과 맞추는 것이 중요
POST /movie/_doc HTTP/1.1
{
"movieCd": 1,
"movieNm": "살아남은 아이",
"movieNmEn": "Last Child",
"prdtYear": "2017",
"openDt": "2017-10-29",
"typeNm": "장편",
"prdtStartNm": "기타",
"nationAlt": "한국",
"genrAlt": "드라마,가족",
"repNationNm": "한국",
"repGenerNm": "드라마"
}
# 응답
{
"_index": "movie",
"_type": "_doc",
"_id": "OjNUtYoBW_DFCGttz4n2",
"_version": 1,
"result": "created",
"_shards": {
"total": 3,
"successful": 1,
"failed": 0
},
"_seq_no": 0,
"_primary_term": 1
}
2.2.3 검색 API
- 엘라스틱서치 검색 API의 사용 방식은 다음과 같이 크게 두 가지로 나뉜다
- HTTP URI 형태의 파라미터를 URI에 추가해 검색하는 방법
- RESTful API 방식인 QueryDSL을 사용해 요청 본문에 질의 내용을 추가해 검색하는 방법
- RESTful 방식이 URI 방식보다 제약사항이 적기 때문에 현업에서는 RESTful 방식을 선호
- 간단한 표현식이라면 두 가지 형식을 섞어서 사용하는 것도 가능
- 2017년에 개봉된 영화를 제목 기준으로 정렬
GET /movie/_doc/_search?q=prdtYear:2017&pretty=true HTTP/1.1 { "sort": { "movieCd": { "order": "asc" } } } # 응답 { "took": 30, "timed_out": false, "_shards": { "total": 3, "successful": 3, "skipped": 0, "failed": 0 }, "hits": { "total": 1, "max_score": null, "hits": [ { "_index": "movie", "_type": "_doc", "_id": "1", "_score": null, "_source": { "movieCd": 1, "movieNm": "살아남은 아이", "movieNmEn": "Last Child", "prdtYear": "2017", "openDt": "2017-10-29", "typeNm": "장편", "prdtStartNm": "기타", "nationAlt": "한국", "genrAlt": "드라마,가족", "repNationNm": "한국", "repGenerNm": "드라마" }, "sort": [ 1 ] } ] } }
- Query DSL을 사용하면 가독성이 높고, JSON 형식으로 다양한 표현이 가능
- 통계를 위한 집계 쿼리 등 복잡한 쿼리를 작성하려면 Query DSL을 사용하는 것이 좋다
URI 방식의 검색 질의
- q 파라미터를 사용해 해당 용어와 일치하는 문서만 조회
- POST /movie/_search?q=장편 HTTP/1.1 # 응답 { "took": 3, "timed_out": false, "_shards": { "total": 3, "successful": 3, "skipped": 0, "failed": 0 }, "hits": { "total": 1, "max_score": 0.2876821, "hits": [ { "_index": "movie", "_type": "_doc", "_id": "1", "_score": 0.2876821, "_source": { "movieCd": 1, "movieNm": "살아남은 아이", "movieNmEn": "Last Child", "prdtYear": "2017", "openDt": "2017-10-29", "typeNm": "장편", "prdtStartNm": "기타", "nationAlt": "한국", "genrAlt": "드라마,가족", "repNationNm": "한국", "repGenerNm": "드라마" } } ] } }
- JSON 포멧 해더에는 쿼리가 실행된 총 시간 결과를 보여준다.
- _shards에서 성공적으로 반환한 샤드의 수와 실패한 샤드의 수를 알수 있다.
- hits에서 일치하는 문서의 수와 함께 점수가 가장 높은 상위 10개의 문서를 보여준다.
- time_out 시간이 초과된다면 검색된 내용까지만 결과로 반환
- 실패한 샤드의 수가 지나치게 많다면 time_out 시간을 적절히 조정해야 한다
- 특정 필드만 조회하고 싶다면 필드명을 포함해서 요청하면 된다.
- POST /movie/_search?q=typeNm:장편 HTTP/1.1 { "took": 8, "timed_out": false, "_shards": { "total": 3, "successful": 3, "skipped": 0, "failed": 0 }, "hits": { "total": 1, "max_score": 0.2876821, "hits": [ { "_index": "movie", "_type": "_doc", "_id": "1", "_score": 0.2876821, "_source": { "movieCd": 1, "movieNm": "살아남은 아이", "movieNmEn": "Last Child", "prdtYear": "2017", "openDt": "2017-10-29", "typeNm": "장편", "prdtStartNm": "기타", "nationAlt": "한국", "genrAlt": "드라마,가족", "repNationNm": "한국", "repGenerNm": "드라마" } } ] } }
Query DSL 방식의 검색 질의
- URI 방식은 여러 필드를 각기 다른 검색어로 질의하는 것이 어렵다
- JSON으로 질의하는 것이 좋음
- JSON 포멧을 이용해 RESTful 방식으로 질의하면 매우복잡한 쿼리도 쉽게 표현할 수 있다
- POST /movie/_search HTTP/1.1 { "query": { "term": { "typeNm": "장편" } } }
- 쿼리 구문은 여러 개의 키를 조합해 객체의 키 값으로 사용
- { "size": 몇 개의 결과를 반환할 지 결정(기본은 10개) "from": 어느 위치부터 반환할지 결정 0부터 시작하면 상위 0~10건의 데이터를 반환(기본은 0) "_source": 특정 필드를 기준으로 정렬 asc, desc로 오름차순, 내림차순 정렬을 지정 가능 "query": { 검색될 조건을 정의 } "filter": { 검색 결과 중 특정한 값을 다시 보여준다. 결과 내에서 재검색할 때 사용하는 기능 중 하나 필터를 사용하게 되면 자동으로 score 값이 정렬되지 않는다. } }
2.2.4 집계 API
- 엘라스틱서치는 집계 API를 통해 기존의 패싯 API로 하기 어려운 작업을 처리하는 것이 가능
- 쿼리에 사용되는 집계에 따라 수치를 계산하고 동적으로 카운팅하거나 히스토그램 같은 작업 등도 할 수 있게 바뀜
- 엘라스틱서치의 집계 API는 각종 통계 데이터를 실시간으로 제공할 수 있는 강력한 기능
데이터 집계
- _serch API를 사용해 집계 쿼리를 만들고 terms 키워드를 이용해 genreAlt라는 필드의 데이터를 그룹화
- 버킷(Buckets) 구조 안에 그룹화된 데이터가 포함돼 있다.
- 엘라스틱서치의 집계가 강력한 이유 중 하나는 버킷 안에 버킷의 결과를 추가할 수 있다
- 다양한 집계 유형을 결합하거나 중첩, 조합하는 것이 가능
POST /movie/_search?size=0 HTTP/1.1 { "aggs": { "genre": { "terms": { "field": "genreAlt" } } } } # 응답 { "took": 23, "timed_out": false, "_shards": { "total": 3, "successful": 3, "skipped": 0, "failed": 0 }, "hits": { "total": 1, "max_score": 0.0, "hits": [] }, "aggregations": { "genre": { "doc_count_error_upper_bound": 0, "sum_other_doc_count": 0, "buckets": [] } } }
- 장르별 국가 형태를 중첩해서 보여주는 집계의 예
- POST /movie/_search?size=0 HTTP/1.1 { "aggs": { "genre": { "terms": { "field": "genreAlt" } }, "aggs": { "nation": { "terms": { "field": "nationAlt" } } } } } # 응답 { "error": { "root_cause": [ { "type": "named_object_not_found_exception", "reason": "[9:23] unable to parse BaseAggregationBuilder with name [nation]: parser not found" } ], "type": "named_object_not_found_exception", "reason": "[9:23] unable to parse BaseAggregationBuilder with name [nation]: parser not found" }, "status": 400 }
데이터 집계 타입
- 집계 기능은 현재 4가지 API로 제공
- 집계 기능은 서로 조합해 사용할 수 있으며 조합해서 매우 강력한 기능을 제공할 수 있다
- 버킷 집계
- 집계 중 가장 많이 사용
- 문서의 필드를 기준으로 버킷을 집계
- 메트릭 집계
- 문서에 추출된 값을 가지고 Sum, Max, Min, Avg를 계산
- 매트릭스 집계
- 행렬의 값을 합하거나 곱한다
- 파이프라인 집계
- 버킷에서 도출된 결과 문서를 다른 필드 값으로 재분류
- 다른 집계에 의해 생성된 출력 결과를 다시 한번 집계
- 집계가 패싯보다 강력한 이유
- 버킷 집계
728x90
'검색엔진 스터디' 카테고리의 다른 글
07장 한글 검색 확장 기능 (1) | 2023.11.08 |
---|---|
05장 데이터 집계 (3) | 2023.11.08 |
04장 데이터 검색 (1) | 2023.11.08 |
03장 데이터 모델링 (1) | 2023.11.08 |
01장 검색 시스템 이해하기 (0) | 2023.11.08 |