검색엔진 스터디

03장 데이터 모델링

막이86 2023. 11. 8. 17:25
728x90

엘라스틱서치 실무 가이드 3장을 요약한 내용입니다.

  • 매핑은 색인될 문서의 데이터 모델링이라고 할 수 있다
  • 필드 데이터 타입이 자동으로 지정될 경우 실제 운영환경에서 예기치 않은 문제가 발생할 수 있다.
  • 매핑 과정은 매우 중요한 과정

3.1 매핑 API 이해하기

  • 매핑은 색인 시 데이터가 어디에 어떻게 저장될지 결정하는 설정
  • 인덱스에 추가되는 각 데이터 타입을 구체적으로 정의하는 일
  • 데이터베이스에서 테이블의 칼럼 정보를 정의하는 것이 중요하듯 엘라스틱서치에서도 데이터의 타입을 정의하는 것은 매우 중요
  • 엘라스틱서치는 스키마리스이기 때문에 명시적으로 필드를 정의하지 않아도 데이터 유형에 따라 필드 데이터 타입에 대한 매핑 정보가 자동으로 생성
  • 실수로 잘못된 타입이 지정될 경우 수정할 방법이 없음
    • 첫 번째 문서를 매핑 설정 없이 색인하면 movieCd는 숫자 타입으로 매핑
    • 두 번째 문서를 색인하면 movieCd가 문자열이라 색인이 불가능
{
	"movieCd": "20173732",
	"movieNm": "캡틴 아메리카"
}

{
	"movieCd": "XT001",
	"movieNm": "아이언맨"
}
  • 매핑 정보를 설정할 때 고려사항
    • 문자열을 분석할 것인가?
    • _source에 어떤 필드를 정의할 것인가?
    • 날짜 필드를 가지는 필드는 무엇인가?
    • 매핑에 정의되지 않고 유입되는 필드는 어떻게 처리할 것인가?

3.1.1 매핑 인덱스 만들기

  • movie_search 인덱스를 생성매핑명 필드명 필드 타입
    인덱스 키 movieCd keyword
    영화제목_국문 movieNm text
    영화제목_영문 movieNmEn text
    제작연도 prdtYear integer
    개봉연도 openDt integer
    영화유형 typeNm keyword
    제작상태 prdtStatNm keyword
    제작국가(전체) nationAlt keyword
    장르(전체) genreAlt keyword
    대표 제작국가 repNationNm keyword
    대표 장르 repGenreNm keyword
    영화감독명 directors.peopleNm object → keyword
    제작사코드 companies.companyCd object → keyword
    제작사명 companies.companyNm object → keyword
{
	"movieCd": "20173732",
	"movieNm": "살아남은 아이",
	"movieNmEn": "Last Child",
	"prdtYear": "2017",
	"openDt": "",
	"typeNm": "장편",
	"prdtStatNm": "기타",
	"nationAlt": "한국",
	"genreAlt": "드라마,가족",
	"repNationNm": "한국",
	"repGenreNm": "드라마",
	"directors": [{
		"peopleNm": "신동석"
	}],
	"companies": {
		"companyCd": "",
		"companyNm": ""
	}
}
  • 검색 대상이 되는 필드는 “영화제목” 필드이므로 분석 가능하도록 text 타입으로 정의
  • 나머지 필드는 해당 정보를 보여주만 할 것이기 때문에 특성에 따라 타입을 설정
  • directors, companies 필드는 내부적으로 또 다른 문서 구조를 가지게 되므로 계증 구조로 설정
  • 인덱스 생성
  • PUT /movie_search { "setting": { "number_of_shards": 5, "number_of_replicas": 1 }, "mappings": { "_doc": { "properties": { "movieCd": { "type": "keyword" }, "movieNm": { "type": "text", "analyzer": "standard" }, "movieNmEn": { "type": "text", "analyzer": "standard" }, "prdtYear": { "type": "integer" }, "openDt": { "type": "integer" }, "typeNm": { "type": "keyword" }, "prdtStatNm": { "type": "keyword" }, "nationAlt": { "type": "keyword" }, "genreAlt": { "type": "keyword" }, "repNationNm": { "type": "keyword" }, "repGenreNm": { "type": "keyword" }, "companies": { "properties": { "companyCd": { "type": "keyword" }, "companyNm": { "type": "keyword" } } }, "directors": { "properties": { "peopleNm": { "type": "keyword" } } } } } } }

3.1.2 매핑 확인

  • 이미 만들어진 매핑을 확인하려면 _mapping API를 사용할 수 있다.
  • GET /movie_search/_mapping

3.1.3 매핑 파라미터

  • 매핑 파라미터는 색인할 필드의 데이터를 어떻게 저장할지에 대한 다양한 옵션을 제공

analyzer

  • 해당 필드의 데이터를 형태소 분석하겠다는 의미
  • text 타입의 필드는 analyzer 매핑 파라미터를 기본적으로 사용해야 한다.
    • 기본 값은 Standard Analyzer

normalizer

  • term query 분석기를 사용
  • 분석기에 asciifolding 같은 필터를 사용 할 수 있다.
    • cafe, Cafe, Cafe`는 서로 같은 문서로 인식 하도록

boost

  • 필드에 가중치를 부여
  • 가중치에 따라 유사도 점수가 달라짐
  • 색인 시점에 boost 설정을 하게된다면 재색인하지 않는 이상 가중치 변경을 할 수 없음
  • 검색 시점에만 사용하는 것을 권장

coerce

  • 색인시 자동 변환을 허용 여부를 설정
  • “10”과 같은 숫자 형태의 문자열이 integer 타입의 필드에 들어온다면 자동으로 형변환을 수행
  • coerce 설정을 미사용한다면 색인에 실패 할 수 있음

copy_to

  • 매핑 파라미터를 추가한 필드의 값을 지정한 필드로 복사
  • 여러 개의 필드 데이터를 하나의 필드에 모아서 전체 검색 용도로 사용 하기도 함
  • copy_to 파라미터를 이용해 movieNm과 movieNmEn의 결과를 합해 “살아남은 아이 Last Child”데이터 생성 가능

fielddata

  • 힙 공간에 생성하는 메모리 캐시
  • doc_values라는 새로운 형태의 캐시를 제공
  • text 타입의 필드를 제외한 모든 필드는 기본적으로 doc_values캐시 사용

doc_values

  • 기본 캐시
  • doc_values를 사용함으로써 힙에 대한 부담을 없애고 운영체제의 파일 시스템 캐시를 통해 디스크에 있는 데이터에 바르게 접근 가능
  • 필드의 정렬, 집계할 필요가 없고 스크립트에서 필드값을 접근할 필요가 없다면 디스크 공간 절약을 위해 doc_values를 비활성화할 수 있다.

dynamic

  • 필드를 추가할 때 동적으로 생성할지 말지 결정

enabled

  • 검색 결과에는 포함되지만 색인은 하고 싶지 않은 경우도 있다.
  • 색인을 원치 않는 날짜오 사용자 ID의 매핑 파라미터 중 enabled를 false로 설정하면 _source에는 검색이 되지만 색인은 하지 않는다.

format

  • 날짜/시간을 문자열로 변경할 때 미리 구성된 포멧을 사용

ignore_above

  • 문자열이 지정한 크기를 넘어서면 빈 값으로 색인

ignore_malformed

  • 해당 필드만 무시하고 문서를 색인 가능

index

  • 필드값을 색인할지를 결정
    • 기본값은 true

fields

  • 다중 필드를 설정할 수 있는 옵션
  • 필드 안에 또 다른 필드의 정보를 추가할 수 있음
  • PUT /movie_search_mapping { "mappings": { "_doc": { "properties": { "awards": { "type": "text", "fields": { "name": { "type": "keyword" } } } } } } }

norms

  • _source 값 계산에 필요한 정규화 인수를 사용할지 여부 설정
    • 기본은 true
  • 단순 필터링 용도로 사용하는 필드는 비활성화해서 디스크 공간을 절약할 수 있다.

null_value

  • 색인 시 문서에 필드가 없거나 필드의 값이 null이면 필드를 생성하지 않음
  • null_value를 설정하면 값이 null이더라도 필드를 생성하고 값을 저장
  • PUT /moive_search_mapping/_mapping/doc { "properties": { "aduiCnt": { "type": "integer", "null_value": "0" } } }

position_increment_gap

  • 배열 형태의 데이터를 색인할 때 검색의 정확도를 높이기 위해 제공하는 옵션
  • 검색시 단어와 단어 사이의 간격을 기준으로 일치하는 문서를 찾는데 필요
    • [”John Abraham”, “Linco Smith”] 데이터에서 “Abraham John” 검색이 가능

properties

  • 오브젝트 타입이나 중첩 타입의 스키마를 정의할 때 사용

search_analyzer

  • 색인과 검색 시 같은 분석기를 사용
  • 검색 시 사용할 분석기를 별도로 지정 가능

similarity

  • 유사도 측정 알고리즘을 지정
  • 기본 알고리즘인 BM25에서 다른 알고리즘으로 변경 가능

store

  • 검색 결과에 값을 포함하기 위한 매핑 파라미터
  • 매핑 파라미터를 사용하면 해당 필드를 자체적으로 저장 할 수 있다
    • 데이터를 매핑한 상태라면 _source를 로드해서 해당 필드를 찾는 것보다 사용할 각 필드만 로드해서 사용하는 편이 효율적
  • 매핑 파라미터를 사용하면 디스크를 더 많이 사용

term_vector

  • 루씬에서 분석된 용어의 정보를 포함할지 여주를 결정
  • 설정 가능한 인자인자 설명
    no 텀백터를 저장하지 않는다.
    yes 필드와 용어만 저장
    with_positions 용어, 용어의 시작과 끝 위치를 저장
    with_offsets 용어, 문자 오프셋을 저장
    with_positions_offsets 용어, 용어의 시작과 끝 위치, 문자 오프셋을 모두 저장

3.2 메타 필드

  • 메타 필드는 엘라스틱서치에서 생성한 문서에서 제공하는 특별한 필드
  • 색인된 문서를 조회
    • 실제 문서의 정보를 담고 있는 항목은 _source
    • _index, _type, _id, _score 엘라스틱서치가 직접 생성한 메타 필드
    {
    	"_index": "movie_search",
    	"_type": "_doc",
    	"_id": "8",
    	"_score": 1,
    	"_source": {
    		"movieCd": "20178401",
    		"movieNm": "검객",
    		"movieNmEn": "",
    		"prdtYear": "2017",
    		"openDt": "",
    		"typeNm": "장편",
    		"prdtStatNm": "후반작업",
    		"nationAlt": "한국",
    		"genreAlt": "사극, 액션"
    	}
    }
    

3.2.1 _index 메타 필드

  • 해당 문서가 속한 인덱스의 이름
  • POST movie_search/_search { "size": 0, "aggs": { "indices": { "terms": { "field": "_index", "size": 10 } } } }
  • 인덱스별 카운트 정보
  • { "took": 2, "timed_out": false, "_shards": { "total": 5, "successful": 5, "skipped": 0, "failed": 0 }, "hits": { "total": 1, "max_score": 0.0, "hits": [] }, "aggregations": { "indices": { "doc_count_error_upper_bound": 0, "sum_other_doc_count": 0, "buckets": [ { "key": "movie_search", "doc_count": 1 } ] } } }

3.2.2 _type 메타 필드

  • 문서가 속한 매핑의 타입 정보
  • POST movie_search/_search { "size": 0, "aggs": { "indices": { "terms": { "field": "_type", "size": 10 } } } }
  • 타입별 카운트 정보
  • { "took": 3, "timed_out": false, "_shards": { "total": 5, "successful": 5, "skipped": 0, "failed": 0 }, "hits": { "total": 1, "max_score": 0.0, "hits": [] }, "aggregations": { "indices": { "doc_count_error_upper_bound": 0, "sum_other_doc_count": 0, "buckets": [ { "key": "_doc", "doc_count": 1 } ] } } }

3.2.3 _id 메타 필드

  • 문서를 식별하는 유일한 키 값
  • POST movie_search/_search { "size": 0, "aggs": { "indices": { "terms": { "field": "_id", "size": 10 } } } }
  • Id별 카운트 정보
  • { "took": 2, "timed_out": false, "_shards": { "total": 5, "successful": 5, "skipped": 0, "failed": 0 }, "hits": { "total": 1, "max_score": 0.0, "hits": [] }, "aggregations": { "indices": { "doc_count_error_upper_bound": 0, "sum_other_doc_count": 0, "buckets": [ { "key": "1", "doc_count": 1 } ] } } }

3.2.4 _uid 메타 필드

  • 특수한 목적의 식별키
    • 태그를 사용해 _type, _id 값을 조합해 사용

    • 내부적으로만 사용되기 때문에 검색 시 조회되는 값은 아님
    POST movie_search/_search
    {
    	"size": 0,
    	"aggs": {
    		"indices": {
    			"terms": {
    				"field": "_uid",
    				"size": 10
    			}
    		}
    	}
    }
    
  • 조회 결과
  • { "took": 28, "timed_out": false, "_shards": { "total": 5, "successful": 5, "skipped": 0, "failed": 0 }, "hits": { "total": 1, "max_score": 0.0, "hits": [] }, "aggregations": { "indices": { "doc_count_error_upper_bound": 0, "sum_other_doc_count": 0, "buckets": [ { "key": "_doc#1", "doc_count": 1 } ] } } }

3.2.5 _source 메타 필드

  • 문서의 원본 데이터를 제공
  • _reindex API나 스크립트를 사용해 해당 값을 계산할 때 활용 가능
    • movieCD값이 “20173732”인 값만 조회해서 재색인하고 prdtYear 값을 변경
    • # 재색인을 위해 reindex_movie 인덱스 생성 PUT /reindex_movie # reindex API를 이용해 재색인 # prdtYear 필드값 1 추가 POST /_reindex { "source": { "index": "movie_search", "query": { "movieCd": "20173732" } }, "dest": { "index": "reindex_movie" }, "script": { "source": "ctx._source.prdtYear++" } }

3.2.6 _all 메타 필드

  • 색인에 사용된 모든 필드의 정보를 가진 메타 필드
  • PUT movie_index_meta_fields_all/_doc/20173732 { "movieCd": "20173732", "movieNm": "살아남은 아이", "movieNmEn": "Last Child", "prdtYear": "2017" }
  • _all 필드에는 4개 필드의 값이 모두 합쳐져서 하나의 문자열(”20173732 살아남은 아이 last Child 2017”) 이 생성
  • _all 필드는 데이터 크기가 너무 많이 차지하는 문제가 있어 폐기됨

3.2.7 _routing 메타 필드

  • 특정 문서를 특정 샤드에 저장하기 위해 사용자가 지정하는 필드
  • 별도의 설정이 없으면 문서는 샤드에 골고루 분산되어 저장
  • 특정 문서를 하나의 샤드에 저장하고 싶을때 _routing 사용
  • Hash (_routing) % num_of_shards
  • _routing 값을 ko로 지정해 문서를 색인
  • PUT movie_routing/_doc/1?routing=ko { "repGenreNm": "한국어", "movieNm": "살아남은 아이" }
  • 검색할 때도 색인할 때와 마찬가지로 _routing 값을 지정
  • POST movice_routing/_doc/_search?routing=ko

3.3 필드 데이터 타입

  • 필드에 데이터 타입을 지정 가능
    • keyword, text 같은 문자열 데이터 타입
    • date, long, double, integer, boolean, ip 같은 일반적인 데이터 타입
    • 객체 또는 중첩문과 같은 JSON 계층의 특성 데이터 타입
    • geo_point, geo_shape 같은 특수한 데이터 타입

3.3.1 Keyword 데이터 타입

  • 검색 시 필터링되는 항목
  • 정렬이 필요한 항목
  • 집계 해야 하는 항목

3.3.2 Text 데이터 타입

  • 색인 시 지정된 분석기가 컬럼의 데이터를 문자열 데이터로 인식하고 분석
  • 전문 검색이 가능한 점이 가장 큰 특징
  • 검색, 정렬, 집계, 연산을 사용할때 Text, Keyword 타입을 동시에 설정

3.3.3 Array 데이터 타입

  • Array 타입에 저장되는 값은 모두 같은 타입으로만 구성 해야 한다

3.3.4 Numeric 데이터 타입

  • 데이터의 크기에 알맞는 타입을 제공함으로써 색인과 검색을 효율적으로 처리

3.3.5 Date 데이터 타입

  • 날짜는 다양하게 표현될 수 있기 때문에 날짜 문자열 형식을 명시적으로 설정 해야함
  • PUT movie_text/_mapping/_doc { "properties": { "date": { "type": "date", "format": "yyyy-MM-dd HH:mm:ss" } } }

3.3.6 Range 데이터 타입

  • 범위가 있는 데이터를 저장할 때 사용
    • 개봉일부터 종료일 까지
    PUT movie_search_datatype/_mapping/_doc
    {
    	"properties": {
    		"showRange": {
    			"type": "date_type"
    		}
    	}
    }
    
    # 시작값과 종료값 범위 지정
    PUT movie_search_datatype/_doc/2
    {
    	"showRange": {
    		"gte": "2001-01-01",
    		"lte": "2001-12-31"
    	}
    }
    

3.3.7 Boolean 데이터 타입

  • true, false 값을 가지는 데이터

3.3.8 Gep-Point 데이터 타입

  • 위도 경도 등 위치 정보를 담은 데이터 저장

3.3.9 IP 데이터 타입

  • IPv4나 IPv6 모두 지정 가능

3.3.10 Object 데이터 타입

  • JSON 포멧의 문서는 내부 객체를 계층적으로 포함 가능
  • Object 데이터 타입을 정의할 때 특정 키워드를 이용하지 않는다.
    • 구조를 입력
    PUT movice_search_datatype/_mapping/_doc
    {
    	"properties": {
    		"companies": {
    			"properties": {
    				"companyName": {
    					"type": "text"
    				}
    			}
    		}
    	}
    }
    
    PUT movice_search_datatype/_doc/5
    {
    	"title": "해리포터와 마법사의 돌",
    	"companies": {
    		"companyName": "워너브라더스"
    	}
    }
    

3.3.11 Nested 데이터 타입

  • Object 객체 배열을 독집적으로 색인하고 질의하는 데이터 타입
    • 배열 형태로 저장되면 필드 내의 검색은 기본적으로 OR 조건으로 검색
    • companyName이 “워너브라더스”이고 companyCd가 “2”인 조건 검색
      • OR연산으로 인해 의도와 다른 결과 발생
    PUT movice_search_datatype/_doc/5
    {
    	"title": "해리포터와 마법사의 돌",
    	"companies": [
    		{
    			"companyCd": "1",
    			"companyName": "워너브라더스"
    		},
    		{
    			"companyCd": "2",
    			"companyName": "Heyday Films"
    		}
    	]
    }
    
  • Nested 데이터 타입을 사용해서 해결 가능
  • POST movie_sarch_datatype/_search { "query": { "nested": { "path": "companies", "query": { "bool": { "must": [ { "match": ["companies.companyName": "워너브라더스"] }, { "match": ["companies.companyCd": "2"] } ] } } } } }

3.4 엘라스틱서치 분석기

3.4.1 텍스트 분석 개요

<aside> 👉 우리나라가 좋은나라, 대한민국 화이팅

</aside>

  • “우리나라”를 입력하면 검색이 될까?
    • “우리나라”라는 단어가 존재하지 않아 검색되지 않는다.
  • 엘라스틱서치에서 제공하는 Analyze API를 사용해 분석결과를 확인 할 수 있다.
  • POST _analyze { "analyzer": "standard", "text": "우리나라가 좋은나라, 대한민국 화이팅" } # 응답 { "tokens": [ { "token": "우리나라가", "start_offset": 0, "end_offset": 5, "type": "<HANGUL>", "position": 0 }, { "token": "좋은나라", "start_offset": 6, "end_offset": 10, "type": "<HANGUL>", "position": 1 }, { "token": "대한민국", "start_offset": 12, "end_offset": 16, "type": "<HANGUL>", "position": 2 }, { "token": "화이팅", "start_offset": 17, "end_offset": 20, "type": "<HANGUL>", "position": 3 } ] }
  • Token은 “우리나라가”, “좋은나라”, “대한민국”, “화이팅” 으로 분리

3.4.2 역색인 구조

  • 특정한 단어를 알고 있지만 해당 단어가 등장하는 페이지를 알지 못할때 책의 마지막 부분에 나열된 목록을 보게 된다.
  • 역색인 구조를 간단하게 정리
    • 모든 문서가 가지는 단어의 고유 단어 목록
    • 해당 단어가 어떤 문서에 속해있는지에 대한 정보
    • 전체 문서에 각 단어가 몇 개 들어있는지에 대한 정보
    • 하나의 문서에 단어가 몇 번씩 출현했는지에 대한 빈도
  • 토큰의 정보가 정확하게 일치하는 데이터만 출력
    • “Eleasticsearch”, “elasticsearch” 다른 단어로 인식
    • 색인시 같은 단어로 인식하게 모두 소문자로 변경 가능
    • 색인 파일에 들어갈 토큰을 변경하여 저장되고 실제 문서의 내용은 변함 없다

3.4.3 분석기 구조

  • 기본 프로세스
    • 문장을 특정한 규칙에 의해 수정
    • 수정한 문장을 개별 토큰으로 분리
    • 개별 토큰을 특정한 규칙에 의해 변경
  • CHARACTER FILTER
    • 문장을 분석하기 전에 입력 텍스트에 대해 특정 단어를 변경하거나 HTML과 같은 태그를 제거하는 역활을 하는 필터
    • 해당 내용은 텍스트를 개별 토큰화하기 전에 전처리 과정이며, ReplaceAll() 함수처럼 패턴으로 텍스트를 변경하거나 사용자가 정의한 필터를 적용할 수 있다.
  • TOKENIZER FILTER
    • Tokenizer Filter는 분석기를 구성할 때 하나만 사용할 수 있으며 텍스트를 어떻게 나눌 것인지를 정의
    • 한글을 분해할 때는 한글 형태소 분석기의 Tokenizer를 사용하고, 영문을 분석할 때는 영문 형태소 분석기의Tokenizer를 사용하는 등 상황에 맞게 적절한Tokenizer를 사용
  • TOKEN FILTER
    • 토큰화된 단어를 하나씩 필터링해서 사용자가 원하는 토큰으로 변환
    • 불필요한 단어를 제거하거나 동의어 사전을 만들어 단어를 추가하거나 영문 단어를 소문자로 변환하는 등의 작업 수행
    • Token Filter는 여러 단계가 순차적으로 이뤄지며 순서를 어떻게 지정하느냐에 따라 검색의 질이 달라질 수 있다.

<aside> 👉 <B>Elasticsearch</B> is cool

</aside>

  • 분석기를 생성
    • custom_movie_analyzer 이름으로 분석기를 설정
    • [char_filter]
      • 전체 텍스트 문장에서 HTML 태그를 제거
    • [tokenizer]
      • 특수문자 혹은 공백을 기준으로 텍스트를 분할
    • [filter]
      • 모든 토큰을 소문자로 변환
    PUT /movie_analyzer
    {
    	"settings": {
    		"index": {
    			"number_of_shards": 5,
    			"number_of_replicas": 1
    		}
    	},
    	"analysis": {
    		"analyzer": {
    			"custom_movie_analyzer": {
    				"type": "custom",
    				"char_filter": [
    					"html_strip"
    				],
    				"tokenizer": "standard",
    				"filter": [
    					"lowercase"
    				]
    			}
    		}
    	}
    }
    
  • [그림 3.2 토큰 정제 흐름] 참고

3.4.3.1 분석기 사용법

  • 분석기를 사용하기 위해 엘라스틱서치에서는 _analyze API를 제공

분석기를 이용한 분석

  • 미리 정의된 분석기의 경우 쉽게 테스트해볼 수 있다.
  • POST _analyze { "analyzer": "standard", "text": "캐리비안의 해적" }

필드를 이용한 분석

  • 인덱스를 설정할 때 분석기를 직접 설정할 수 있다.
  • custom_movie_analyzer를 title이라는 필드에 매핑했다면 필드를 지정해 _analyzer API를 사용할 수 있다.
  • POST movie_analyzer/_analyze { "field": "title", "text": "캐리비안의 해적" }

색인과 검색 시 분석기를 각각 설정

  • 색인할 때 사용하는 Index Analyzer와 검색할 때 사용되는 Search Analyzer로 구분해서 구성 가능
    • 매핑시 title 필드는 Text 타입으로 설정
    • 색인 시점과 검색 시점을 나눠 분석기를 설정
    • “analyzer” 항목을 이용해 설정
      • 색인 시점과 검색 시점에 모두 동일한 분석기를 사용하겠다는 의미
    • 시점마다 다른 분석기를 사용하려면 “search_anayzer” 항목을 사용해 정의
    PUT movie_analyzer
    {
    	"settings": {
    		"index": {
    			"number_of_shards": 5,
    			"number_of_replicas": 1
    		},
    		"analysis": {
    			"analyzer": {
    				"moive_lower_test_analyzer": {
    					"type": "custom",
    					"tokenizer": "standard",
    					"filter": [
    						"lowercase"
    					]
    				},
    				"movie_stop_test_analyzer": {
    					"type": "custom",
    					"tokenizer": "standard",
    					"filter": [
    						"lowercase",
    						"english_stop"
    					]
    				}
    			},
    			"filter": {
    				"english_stop": {
    					"type": "stop",
    					"stopwords": "_english_"
    				}
    			}
    		},
    		"mappings": {
    			"_doc": {
    				"properties": {
    					"title": {
    						"type": "text",
    						"analyzer": "movie_stop_test_analyzer",
    						"search_analyzer": "movie_lower_test_analyzer"
    					}
    				}
    			}
    		}
    	}
    }
    
  • 인덱스 생성이 완료되면 문서를 색인
    • [harry], [potter], [chamber], [secrets]
    PUT movie_analyzer/_doc/1
    {
    	"title": "Harry Potter and the Chamber of Secrets"
    }
    
  • 검색
    • [chaber], [of], [secrets]
    POST movie_analyzer/_search
    {
    	"query": {
    		"query_string": {
    			"default_operator": "AND",
    			"query": "Chaber of Secrets"
    		}
    	}
    }
    

3.4.3.2 대표적인 분석기

Standard Analyzer

  • 공백 혹은 특수 기호를 기준으로 토큰을 분리하고 모든 문자를 소문자로 변경하는 토큰 필터를 사용
  • POST move_analyzer/_analyze { "analyzer": "standard", "text": "Harry Potter and the Chamber of Secrets" }

Whitespace 분석기

  • 공백 문자열을 기준으로 토큰을 분리
  • POST move_analyzer/_analyze { "analyzer": "whitespace", "text": "Harry Potter and the Chamber of Secrets" }

Keyword 분석기

  • 전체 입력 문자열을 하나의 키워드처럼 처리
  • POST move_analyzer/_analyze { "analyzer": "keyword", "text": "Harry Potter and the Chamber of Secrets" }

3.4.4 전처리 필터

  • 분석기는 전처리 필터를 이용한 데이터 정제 후 토크나이저를 이용해 토큰 분리 작업을 수행
  • 토크나이저 내부에 일종의 전처리가 가능하기 때문에 전처리 필터는 상대적으로 활용도가 떨어짐

Html strip char 필터

  • HTML을 제거하는 전처리 필터
  • 테스트를 위한 인덱스를 생성
  • PUT movie_html_analyzer { "settings": { "analysis": { "html_strip_analyze": { "tokenizer": "keyword", "char_filter": [ "html_strip_char_filter" ] } }, "char_filter": { "html_strip_char_filter": { "type": "html_strip", "escaped_tags": [ "b" ] } } } }
  • HTML 태그가 제거되는지 확인
  • POST movie_html_analyzer/_analyze { "analyzer": "html_strip_analyzer", "text": "<span>Harry Potter</span> and th <b>Chaber</b> of Screts" }

3.4.5 토크나이저 필터

  • 토크나이저 필터는 분석기를 구성하는 가장 핵심 구성요소
  • 전처리 필터를 거쳐 토크나이저 필터로 문서가 넘어오면 해당 텍스트는 Tokenizer의 특성에 맞게 적절히 분해

Standard 토크나이저

  • 일반적으로 사용하는 토크나이저로서 대부분의 기호를 만나면 토큰으로 나눈다
  • POST movie_analyzer/_analyze { "tokenizer": "standard", "text": "Harry Potter and the Chamber of Secrets" }

WHITESPACE 토크나이저

  • 공백을 만나면 텍스트를 토큰화 한다
  • POST movie_analyzer/_analyze { "tokenizer": "whitespace", "text": "Harry Potter and the Chamber of Secrets" }

Ngram 토크나이저

  • 한 글자씩 토큰화
  • 다양한 옵션을 조합해서 자동완성을 만들 때 유용

Edge Ngram 토크나이저

  • 지정된 문자의 목록 중 하나를 만날 때마다 시작부분을 고정시켜 단어를 자르는 방식
  • 자동 완성을 구현할 때 유용

Keyword 토크나이저

  • 텍스트를 하나의 토큰으로 만든다

3.4.6 토큰 필터

  • 토큰 필터는 토크나이저에서 분리된 토큰을 변형하거나 추가, 삭제할 때 사용하는 필터
  • 토크나이저에 의해 토큰이 모두 분리되면 분리된 토큰은 배열 형태로 토큰 필터로 전달
  • 토큰이 모두 분리돼야 동작하기 때문에 독립적으로 사용할 수 없다

Ascii Folding 토큰 필터

  • 아스키 코드에 해당하는 127개의 알파벳, 숫자, 기호에 해당하지 않는 경우 문자를 ASCII 요소로 변경
  • PUT movie_af_analyzer { "settings": { "analysis": { "analyzer": { "asciifolding_analyzer": { "tokenizer": "standard", "filter": [ "standard", "asciifolding" ] } } } } }
  • 토큰 필터 사용
  • POST movie_af_analyzer/_analyze { "analyzer": "asciifolding_analyzer", "text": "hello javacafe" }

Lowercase 토큰 필터

  • 토큰을 구성하는 전체 문자열을 소문자로 변환
  • PUT movie_lower_analyzer { "settings": { "analysis": { "analyzer": { "lowercae_analyzer": { "tokenizer": "standard", "filter": [ "lowercase" ] } } } } }
  • 토큰 필터 사용
  • POST movie_lower_analyzer/_analyze { "analyzer": "lowercae_analyzer", "text": "hello javacafe" }

Uppercase 토큰 필터

  • Lowercase 토큰 필터와는 반대로 전체 문자열을 대문자로 변환
  • PUT movie_upper_analyzer { "settings": { "analysis": { "analyzer": { "uppercase_analyzer": { "tokenizer": "standard", "filter": [ "uppercase" ] } } } } }
  • 토큰 필터 사용
  • POST movie_upper_analyzer/_analyze { "analyzer": "uppercase_analyzer", "text": "hello javacafe" }

Stop 토큰 필터

  • 불용어로 등록할 사전을 구축해서 사용하는 필터를 의미
  • 인덱스로 만들고 싶지 않거나 검색되지 않게 하고 싶은 단어를 등록
  • PUT movie_stop_analyzer { "settings": { "analysis": { "analyzer": { "stop_filter_analyzer": { "tokenizer": "standard", "filter": [ "standard", "stop_filter" ] } }, "filter": { "stop_filter": { "type": "stop", "stopwords": [ "and", "is", "the" ] } } } } }
  • 토큰 필터 사용
  • POST movie_stop_analyzer/_analyze { "analyzer": "stop_filter_analyzer", "text": "Harry Potter and the Chamber of Secrets" }

Stemmer 토큰 필터

  • Stemming 알고리즘을 사용해 토큰을 변형하는 필터
  • ex) 영어 문장을 토큰으로 분리하고 분리된 토큰이 영어 단어 원형으로 변환되는지 확인
  • PUT movie_stem_analyzer { "settings": { "analysis": { "analyzer": { "stemmer_filter_analyzer": { "tokenizer": "standard", "filter": [ "standard", "lowercase", "stemmer_eng_filter" ] } }, "filter": { "stemmer_eng_filter": { "type": "stemmer", "name": "english" } } } } }
  • 토큰 필터 사용
  • POST movie_stem_analyzer/_analyze { "analyzer": "stemmer_eng_filter", "text": "Harry Potter and the Chamber of Secrets" }

Synonym 토큰 필터

  • 동의어를 처리할 수 있는 필터
  • PUT movie_syno_analyzer { "settings": { "analysis": { "analyzer": { "synonym_analyzer": { "tokenizer": "whitespace", "filter": [ "synonym_filter" ] } }, "filter": { "synonym_filter": { "type": "synonym", "synonyms": [ "Harry => 해리" ] } } } } }
  • 토큰 필터 사용
  • POST movie_syno_analyzer/_analyze { "analyzer": "synonym_analyzer", "text": "Harry Potter and the Chamber of Secrets" }

Trim 토큰 필터

  • 앞뒤 공백을 제거하는 토큰 필터
  • PUT movie_trim_analyzer { "settings": { "analysis": { "analyzer": { "trim_analyzer": { "tokenizer": "keyword", "filter": [ "lowercase", "trim" ] } } } } }
  • 토큰 필터 사용
  • POST movie_trim_analyzer/_analyze { "analyzer": "trim_analyzer", "text": "Harry Potter and the Chamber of Secrets " }

3.4.7 동의어 사전

  • 동의어는 검색 기능을 풍부하게 할 수 있게 도와주는 도구 중 하나
  • 원문에 특정 단어가 존재하지 않더라도 색인 데이터를 토큰화해서 저장할 때 동의어나 유의어에 해당하는 단어를 함께 저장해서 검색이 가능해지게 하는 기술
    • “Elasticsearch”를 “엘라스틱서치”로 검색이 가능
  • 동의어를 추가하는 방식
    • 동의어를 매핑 설정 정보에 미리 파라미터로 등록
      • 운영중에 동의어를 변경하기 어렵기 때문에 사용하기 어려움
    • 특정 파일을 별도로 생성해서 관리
  • 엘라스틱서치에서 가장 까다로운 부분 중 하나가 바로 동의어를 관리하는 것
    • 분야가 많아지면 많아질 수록 동의어의 수도 늘어남
    • 동의어 변환 규칙도 많아짐
  • 동의어를 모아둔 파일을 칭할 때 “동의어 사전”이라는 용어 사용

동의어 사전 만들기

  • 엘라스틱서치가 설지된 서버 아래의 config 디렉터리에 생성
  • <엘라스틱서치 설치 디렉터리>/config/analysis/synonym.txt

동의어 추가

  • 동의어를 추가할 때 단어를 쉼표로 분리해 등록
  • Elasticsearch, 엘라스틱서치

<aside> 💡 최신 버저의 엘라스틱서치에서는 동의어를 처리할 때 영문 대소문자가 사전에 등록된 단어와 일치하지 않더라도 자동으로 인식해서 동의어 처리를 한다

</aside>

동의어 치환

  • 특정 단어를 어떤 단어로 변경하고 싶다면 동의어 치환 기능을 이용
  • 원본 토큰이 제거되고 변경될 새로운 토큰이 추가
  • 화살표로 표시
    • Harry ⇒ 해리
Elasticsearch, 엘라스틱서치
Harry => 해리
  • 동의어 사전은 실시간으로 적용되지 않음
    • 인덱스를 Reload 해야 한다
  • 주의해야 할 부분
    • 검색 시점에는 사전의 내용이 변경되더라도 해당 내용이 반영
    • 색인 시점에 동의어 사전이 사용됐다면 사전의 내용이 변경되더라도 색인이 변경되지 않음
  • 검색 시점에만 동의어를 적용하는 방식으로 문제점을 해결하기도 함
  • 인덱스를 생성할 때 매핑 정보에 동의어 사전을 등록
  • PUT movie_analyzer { "settings": { "index": { "analysis": { "analyzer": { "tokenizer": "standard", "filter": [ "lowercase", "synonym_filter" ] } }, "filter": { "synonym_filter": { "type": "synonym", "ignore_case": true, "synonyms_path": "analysis/synonym.txt } } } } }
  • 문장 테스트
  • POST movie_analyzer/_analyze { "analyzer": "synonym_analyzer", "text": "Elasticsearch Harry Potter" } # 응답 결과
  • 동의어 사전을 추가
    • 동의어 사전이 변경될 경우 인덱스를 Reload 해야 한다.
      • POST movie_analyzer/_close: close가 되면 검색도 불가능
      • POST movie_analyzer/_open: open하면 Reload가 완료됨
    Elasticsearch, 엘라스틱서치
    Harry => 해리
    Potter => 포터
    

3.5 Document API 이해하기

엘라스틱서치에서 제공하는 대표적인 Document API

  • Index API: 문서 생성
  • Get API: 문서 조회
  • Delete API: 문서 삭제
  • Update API: 문서 수정
  • Bulk API: 대량의 문서를 처리
  • Reindex API: 문서 복사

3.5.1 문서 파라미터

문서 ID 자동 생성

  • 문서를 생성할 때는 기본적으로 ID가 반드시 필요
  • 지정하지 않으면 자동으로 ID가 생성
    • UUID 값으로

버전 관리

  • 색인된 모든 문서는 버전 값을 가지고 있다.
    • 같은 ID로 다시 색인하면 _version 값이 2로 변경
  • Update API를 이용할 경우 스냅숏을 생성해 문서를 수정하고 인덱스에 다시 재색인
  • 스냅숏이 생성된 사이에 버전 값이 달라졌다면 실패로 처리
  • PUT /movie_dynamic/_doc/1 HTTP/1.1 { "nationAlt": "한국" } # 응답 { "_index": "movie_dynamic", "_type": "_doc", "_id": "1", "_version": 1, "result": "created", "_shards": { "total": 2, "successful": 1, "failed": 0 }, "_seq_no": 0, "_primary_term": 1 } # 다시 요청 { "_index": "movie_dynamic", "_type": "_doc", "_id": "1", "_version": 2, "result": "updated", "_shards": { "total": 2, "successful": 1, "failed": 0 }, "_seq_no": 1, "_primary_term": 1 }
  • 과거 버전을 호출
  • POST /movie_dynamic/_doc/1?version=1 HTTP/1.1 { "nationAlt": "한국" } # 응답 { "error": { "root_cause": [ { "type": "version_conflict_engine_exception", "reason": "[_doc][1]: version conflict, current version [2] is different than the one provided [1]", "index_uuid": "Z0Vn5AmaRpeqRAXMA-LPtQ", "shard": "3", "index": "movie_dynamic" } ], "type": "version_conflict_engine_exception", "reason": "[_doc][1]: version conflict, current version [2] is different than the one provided [1]", "index_uuid": "Z0Vn5AmaRpeqRAXMA-LPtQ", "shard": "3", "index": "movie_dynamic" }, "status": 409 }

오퍼레이션 타입

  • ID가 이미 존재하는 경우에는 update 작업이 실행
  • ID가 없을 경우 create 작업이 실행
  • update가 실행되지 않고 실패하기를 원한다면 op_type을 사용
  • PUT /movie_dynamic/_doc/1?op_type=create HTTP/1.1 { "nationAlt": "한국" } # 응답 { "error": { "root_cause": [ { "type": "version_conflict_engine_exception", "reason": "[_doc][1]: version conflict, document already exists (current version [5])", "index_uuid": "Z0Vn5AmaRpeqRAXMA-LPtQ", "shard": "3", "index": "movie_dynamic" } ], "type": "version_conflict_engine_exception", "reason": "[_doc][1]: version conflict, document already exists (current version [5])", "index_uuid": "Z0Vn5AmaRpeqRAXMA-LPtQ", "shard": "3", "index": "movie_dynamic" }, "status": 409 }

타임아웃 설정

  • 일반적으로 색인을 요청하면 대부분 즉시 처리됨
  • 이미 색인 작업이 진행 중인 동안 추가적으로 색인 API가 호출될 경우 일정기간 대기
    • 1분이 지나면 실패 처리 됨
  • timeout 파라미터를 설정해 대기시간을 조정 가능
    • 5m: 5분간 다른 색인 API가 완료되기를 기다린다
    PUT /movie_dynamic/_doc/1?timeout=5m HTTP/1.1
    {
        "nationAlt": "한국"
    }
    

3.5.2 Delete API

  • Delete API를 이용하면 문서를 삭제할 수 있다.
    • result 항목에 “deleted”라는 값이 반환됨
    • version값이 1만큼 증가
    DELETE /movie_dynamic/_doc/1?timeout=5m HTTP/1.1
    
    # 응답
    {
        "_index": "movie_dynamic",
        "_type": "_doc",
        "_id": "1",
        "_version": 7,
        "result": "deleted",
        "_shards": {
            "total": 2,
            "successful": 1,
            "failed": 0
        },
        "_seq_no": 6,
        "_primary_term": 1
    }
    

3.5.5 Delete By Query API

  • 특정 인덱스에서 검색을 수행한 후 결과에 해당하는 문서만 삭제하고 싶은 경우 사용
    • 몇건이 조회되고 몇건이 삭제되었는지 결과에 나옴
    # 문서 생성
    PUT /movie_dynamic/_doc/1 HTTP/1.1
    {
        "movieCd": "20173732",
        "movieNm": "살아남은 아이",
        "movieEn": "Last Child"
    }
    
    # 문서 삭제 쿼리 
    POST /movie_dynamic/_delete_by_query HTTP/1.1
    {
        "query": {
            "term": {
                "movieCd": "20173732"
            }
        }
    }
    
    # 응답
    {
        "took": 31,
        "timed_out": false,
        "total": 1,
        "deleted": 1,
        "batches": 1,
        "version_conflicts": 0,
        "noops": 0,
        "retries": {
            "bulk": 0,
            "search": 0
        },
        "throttled_millis": 0,
        "requests_per_second": -1.0,
        "throttled_until_millis": 0,
        "failures": []
    }
    

3.5.6 Update API

  • 스크립트를 바탕으로 문서를 수정 가능
    • “ctx._source.필드명” 형태로 접근
  • 엘라스틱서치에서는 Update API는 Index에서 문서를 가져와 스크립트 수행후 재색인한다
  • Update API를 사용하기 위해서는 _source 필드가 활성화되어 있어야 함
  • _source 변수뿐 아니라 _index, _type, _id, _version, _routing, _now 등 추가적인 변수도 사용
  • # 문서 생성 PUT /movie_dynamic/_doc/2 HTTP/1.1 { "movieEn": "Last Child", "counter": 1000 } # 문서 업데이트 POST /movie_dynamic/_doc/2/_update HTTP/1.1 { "script": { "source": "ctx._source.counter += params.count", "lang": "painless", "params": { "count": 1 } } } # 조회 GET /movie_dynamic/_doc/2 HTTP/1.1 # 응답 { "_index": "movie_dynamic", "_type": "_doc", "_id": "2", "_version": 2, "found": true, "_source": { "movieEn": "Last Child", "counter": 1001 } }

3.5.7 Bulk API

  • Bulk API는 한 번의 API 호출로 다수의 문서를 색인하거나 삭제할 수 있다
  • 색인 작업의 경우 한번에 처리함으로써 색인 속도를 크게 향상시킬 수 있다.
  • POST /_bulk HTTP/1.1 {"index": {"_index": "movie_dynamic", "_type": "_doc", "_id": 1}} {"title": "살아남은 아이"} {"index": {"_index": "movie_dynamic", "_type": "_doc", "_id": 2}} {"title": "해리포터와 비밀의 방"} {"index": {"_index": "movie_dynamic", "_type": "_doc", "_id": 3}} {"title": "어벤저스"} # 응답 { "took": 28, "errors": false, "items": [ { "index": { "_index": "movie_dynamic", "_type": "_doc", "_id": "1", "_version": 1, "result": "created", "_shards": { "total": 2, "successful": 1, "failed": 0 }, "_seq_no": 11, "_primary_term": 1, "status": 201 } }, { "index": { "_index": "movie_dynamic", "_type": "_doc", "_id": "2", "_version": 3, "result": "updated", "_shards": { "total": 2, "successful": 1, "failed": 0 }, "_seq_no": 4, "_primary_term": 1, "status": 200 } }, { "index": { "_index": "movie_dynamic", "_type": "_doc", "_id": "3", "_version": 1, "result": "created", "_shards": { "total": 2, "successful": 1, "failed": 0 }, "_seq_no": 0, "_primary_term": 1, "status": 201 } } ] }
  • 한번에 여러 작업을 수행할 수 있다
  • POST /_bulk HTTP/1.1 {"index": {"_index": "movie_dynamic", "_type": "_doc", "_id": 1}} {"title": "살아남은 아이"} {"delete": {"_index": "movie_dynamic", "_type": "_doc", "_id": 2}} {"index": {"_index": "movie_dynamic", "_type": "_doc", "_id": 2}} {"title": "해리포터와 비밀의 방"} {"index": {"_index": "movie_dynamic", "_type": "_doc", "_id": 3}} {"title": "어벤저스"} {"update": {"_index": "movie_dynamic", "_type": "_doc", "_id": 1}} {"doc": {"movieNmEn": "Last Child"}} # 응답 { "took": 6, "errors": false, "items": [ { "index": { "_index": "movie_dynamic", "_type": "_doc", "_id": "1", "_version": 4, "result": "updated", "_shards": { "total": 2, "successful": 1, "failed": 0 }, "_seq_no": 14, "_primary_term": 1, "status": 200 } }, { "delete": { "_index": "movie_dynamic", "_type": "_doc", "_id": "2", "_version": 5, "result": "deleted", "_shards": { "total": 2, "successful": 1, "failed": 0 }, "_seq_no": 6, "_primary_term": 1, "status": 200 } }, { "index": { "_index": "movie_dynamic", "_type": "_doc", "_id": "2", "_version": 6, "result": "created", "_shards": { "total": 2, "successful": 1, "failed": 0 }, "_seq_no": 7, "_primary_term": 1, "status": 201 } }, { "index": { "_index": "movie_dynamic", "_type": "_doc", "_id": "3", "_version": 3, "result": "updated", "_shards": { "total": 2, "successful": 1, "failed": 0 }, "_seq_no": 2, "_primary_term": 1, "status": 200 } }, { "update": { "_index": "movie_dynamic", "_type": "_doc", "_id": "1", "_version": 5, "result": "updated", "_shards": { "total": 2, "successful": 1, "failed": 0 }, "_seq_no": 15, "_primary_term": 1, "status": 200 } } ] }

3.5.8 Reindex API

  • 한 인덱스에서 다른 인덱스로 문서를 복사할 때 사용
    • movie_dynamic → movie_dynamic_new 복사
    POST /_reindex HTTP/1.1
    {
        "source": {
            "index": "movie_dynamic"
        },
        "dest": {
            "index": "movie_dynamic_new"
        }
    }
    
    # 응답
    {
        "took": 143,
        "timed_out": false,
        "total": 3,
        "updated": 0,
        "created": 3,
        "deleted": 0,
        "batches": 1,
        "version_conflicts": 0,
        "noops": 0,
        "retries": {
            "bulk": 0,
            "search": 0
        },
        "throttled_millis": 0,
        "requests_per_second": -1.0,
        "throttled_until_millis": 0,
        "failures": []
    }
    
  • 특정 조회 결과와 일치하는 문서만 복사할 수 있다
  • POST /_reindex HTTP/1.1 { "source": { "index": "movie_dynamic", "type": "_doc", "query": { "term": { "title.keyword": "프렌즈: 몬스터섬의비밀" } } }, "dest": { "index": "movie_dynamic_new" } }
  • 검색시 정렬 작업은 리소스를 많이 사용하기 때문에 색인할 때 정렬된 상태로 색인할 수 있으면 좋다
    • 특정 문서를 복사해 새로운 인덱스를 만들 때 새로운 정렬 방식으로 데이터를 정렬 후 복사 가능
    • 관객 수를 기준으로 정렬해서 문서를 복사
      • 10,000건 단위로 스크롤을 수행
      POST /_reindex HTTP/1.1
      {
          "size": 10000,
          "source": {
              "index": "movie_dynamic",
              "sort": {
                  "counter": "desc"
              }
          },
          "dest": {
              "index": "movie_dynamic_new_1"
          }
      }
      
728x90

'검색엔진 스터디' 카테고리의 다른 글

07장 한글 검색 확장 기능  (1) 2023.11.08
05장 데이터 집계  (3) 2023.11.08
04장 데이터 검색  (1) 2023.11.08
02장 엘라스틱서치 살펴보기  (4) 2023.11.08
01장 검색 시스템 이해하기  (0) 2023.11.08