Elasticsearch에서 reindex를 이용해서 매핑정보 변경하기

Elasticsearch에서 index를 구성하다보면 매핑정보를 추가하거나 수정하고 싶을때가 있다.  내가 아는 내에서는 한번 생성된 index의 매핑정보를 변경하는건 어렵다. 그래서 reindex를 통해 index의 매핑정보를 변경해줘야한다.

우선 wedul_mapping이라는 인덱스가 있다고 해보자.
매핑 정보는 다음과 같다.

PUT wedul_mapping
{
  "mappings": {
    "_doc": {
      "dynamic": "false",
      "properties": {
        "id": {
          "type": "integer"
        },
        "name": {
          "type": "text",
          "fields": {
            "keyword": {
              "type": "keyword"
            }
          }
        }
      }
    }
  }
}

이때 name에서 keyword필드를 제거하고 age라는 새로운 Integer타입의 필드를 매핑하고 싶은 경우에 wedul_mapping_dump라는 새로운 임시 인덱스를 생성한다.

PUT wedul_mapping_dump
{
  "mappings": {
    "_doc": {
      "dynamic": "false",
      "properties": {
        "id": {
          "type": "integer"
        },
        "name": {
          "type": "text"
        },
        "age": {
          "type": "integer"
        }
      }
    }
  }
}

그리고 기존 wedul_mapping인데스에서 wedul_mapping_dump 인덱스로 reindex를 실행한다.

POST _reindex
{
  "source": {
    "index": "wedul_mapping"
  },
  "dest": {
    "index": "wedul_mapping_dump"
  }
}

 

그럼 데이터가 모두 변경된 인덱스 wedul_mapping_dump로 복사되면서 매핑정보가 변경된것을 알 수 있다. 그리고 이름이 같은 wedul_mapping인덱스에 다시 옮기려면 wedul_mapping인덱스를 제거하고 변경된 매핑정보로 새로 생성한뒤 다시한번 reindex를 해주면된다. 데이터가 많은 실 환경에서는 reindex 작업의 비용이 크기 때문에 한번 매핑정보를 설정할 때 잘해주는것이 좋을 것 같다.

댓글()

스프링 부트에서 사용하는 JPA 기능 정리

web/JPA|2018. 11. 4. 23:53

스프링 프레임워크에서 제공하는 JPA는 별도의 구현 클래스 없이 인터페이스만을 사용할 수 있도록 제공한다. 제공되는 인터페이스 JpaRepository는 실행시점에 자동으로 인터페이스 내용을 연결하는 엔티티에 맞게 자동으로 구현해준다. 만약 스프링 JPA 인터페이스에서 제공하지 않는 기능을 사용하고 싶을 때는 메서드명을 특정한 규칙대로 만들어서 사용하면 인터페이스가 알아서 그 이름에 맞는 JPQL을 만들어서 실행해준다.


스프링 JPA 인터페이스는 Mysql같은 RDBMS 뿐만 아니라 Mongodb, Redis와 같은 NoSQL에도 동일한 인터페이스를 사용해서 기능을 사용할 수 있도록 제공해준다. 공통으로 사용할 수 있기에 아주 편리하다.


우선 스프링 부트에 JPA를 사용하기 위해서 Gradle에 라이브러리를 넣자.

1
compile ('org.springframework.boot:spring-boot-starter-data-jpa')
cs

 

기본적인 구조

JpaRepository를 상속받아 구현하고자 하는 인터페이스를 만들고 제네릭에 구현하려는 엔티티와 엔티티의 식별자 타입을 지정하여 인터페이스를 선언한다.

1
2
3
public interface StudentRepository extends JpaRepository<Student, Long> {
}
 
cs


주요 메서드 몇개만 정리해보자.

save() : 저장하거나 업데이트한다.

delete(entity) : em.remove() 호출하여 엔티티를 제거한다.

findOne(ID) : em.find() 호출하여 엔티티를 찾는다.

findAll() : 엔티티 모두를 조회한다. 


JpaRepository 인터페이스로 부족한 기능을 구현한 PagingAndSortingRepository CrudRepository 사용해서 보완할 있다.


JPA에서 메서드 사용하는 방법

JPA에서 엔티티에 맞는 메서드를 사용하는 방법은 크게 3가지이다.

- 메서드 이름으로 쿼리 생성

- 메서드 이름으로 JPA NamedQuery 호출

- @Query 어노테이션을 사용해서 레포지토리 인터페이스에 쿼리 직접 정의


1. 메서드 이름으로 쿼리 생성

메서드 이름으로 쿼리를 생성할 있는데 정식 Document 이용하면 자세히 나와있다

https://docs.spring.io/spring-data/jpa/docs/2.1.2.RELEASE/reference/html/#jpa.query-methods


2. 메서드 이름으로 JPA NamedQuery 호출

JPA NamedQuery 쿼리에 이름을 부여해서 사용하는 방법은 다음과 같이 엔티티에 선언해주면 된다.

1
2
3
4
5
@NamedQuery(
  name="student.findByName",
  query="select s from student s where s.name = :name")
public class Student {

}
cs


위와 같이 선언하고 실제 사용할 때는 entityManager에 아래와 같이 createQuery를 사용해서 쿼리를 호출하면 된다.

1
2
3
4
5
6
7
@PersistenceContext
private EntityManager entityManager;
 
public List<Student> findByUser(String name) {
  List<Student> students = entityManager.createQuery("student.findByName", Student.class).setParameter("name""wedul").getResultList();
}
 
cs


위와 같이 EntityManager를 사용할 수 있지만 스프링 JPA를 사용하여 간단하게 메소드 이름만으로 호출이 가능하다. 이렇게 호출하면 레포지토리에서 Student.쿼리메소드 형태로 쿼리를 찾는다. 만약 실행할 쿼리가 없으면 메서드 이름으로 쿼리를 자동으로 바꿔 동작한다.

1
2
3
4
5
public interface StudentRepository extends JpaRepository<Student, Long> {
  
  List<Student> findByUserName(@Param("name") String name);
  
}
cs


3. @Query 어노테이션을 사용해서 레포지토리 인터페이스에 쿼리 직접 정의

2번에서는 @Entity 클래스에서 정의한 쿼리를 레포지토리에서 메소드 형태로 접근하여 사용하였다.  이번에는 레포지토리에서 직접적으로 쿼리를 만들어서 조회하는 방식을 확인해보자.


@Query("select s from Student s where s.name = ?1")

Student findByName(String name);


인터페이스에 정의하는 메소드에 @Query 어노테이션을 붙혀서 정의하고 사용하면 된다. 바인딩 값은 1부터 시작한다. 스프링 데이터 JPA에서는 ?1 ?2 같은 위치기반 파라미터와 :name 같은 이름 기반 방식을 모두 사용가능하다.


페이징과 정렬

스프링 데이터 JPA에서 쿼리 메서드에 페이징과 정렬 기능을 사용할 있다. 파라미터로 Pageable 인터페이스를 사용할 경우에는 Page 또는 List 반환 받을 있다.

1
2
3
4
public interface StudentRepository extends JpaRepository<Student, Long> {
 
  Page<Student> findByNameStartingWith(String name, Pageable pageable);
}
cs


실제 사용할 때는 Pageable 인터페이스이기 때문에 구현체인 PageRequest 객체를 사용해서 사용한다.

// 파라미터 순서대로 페이지 번호, 사이즈, 정렬 기준 등으로 사용한다.

1
PageRequest pageRequest = new PageRequest(0, 10, new Sort(Sort.Direction.DESC, "name"));
cs


반환되는 값인 Page에서 제공하는 다양한 메소드를 사용해서 편하게 페이징과 소트 기능을 사용할 있다. 

컨트롤러에서 사용자 요청에게 전달되는 페이징 정보를 받기 위해서는 다음과 같이 Pageable 인터페이스를 받으면 되고 받을 속성값은 page, size, sort 사용해서 받는다. (/student?page=0&size=20&sort=name,desc&sort=address.city)


1
2
3
4
5
6
7
@GetMapping("/student")
public String list(Pageable pageable, Model model) {
  Page<Student> page = studentRepository.findByNameStartingWith("dbsafer", pageable);
 
  return "dfdfdf";
}
 
cs


참고 싸이트

https://docs.spring.io/spring-data/commons/docs/current/api/org/springframework/data/domain/Page.html


출처. : 자바 ORM 표준 JPA 프로그래밍


댓글()

elasticsearch percolating 쿼리

엘라스틱 서치에서 일반적인 검색 기능은 특정 인덱스에 문서를 저장하고, 쿼리에 매칭되는 문서를 불러오는 방식으로 수행된다.

하지만 percolating 쿼리 방식은 그 반대로 동작한다. 쿼리를 사전에 저장하고, 새로 유입된 문서가 매칭되는 쿼리가 있는지 확인해 매칭되는 쿼리를 반환한다.

업무적으로 필요한 기능이어서 알아보던 중 알게되어서 정리해본다.

https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-percolate-query.html



인덱스 생성

아래 인덱스생성에 보면 두 가지 필드를 볼 수있다. 먼저 message 필드는 percolator에서 정의된 문서를 임시 인덱스로 인덱싱하기 전에 사전 처리하는 데 사용되는 필드이다. query 필드는 쿼리 문서를 인덱싱하는 데 사용된다. 실제 Elasticsearch 쿼리를 나타내는 json 객체를 보유한다. query 필드는 쿼리 dsl을 이해하고 이후에 percolate 쿼리에 정의 된 문서와 일치시키기 위해 쿼리를 저장한다.

이해하기 어려운데, 자세한 내용은 사용방법을 더 보면 알 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
PUT wedul_product
{
  "mappings": {
    "seat": {
      "properties": {
        "message": {
          "type": "text"
        },
        "query": {
          "type": "percolator"
        }
      }
    }
  }
}
 
cs


쿼리 삽입
상품명을 가지고 있는 문서에서 특정 지역들에 대한 정보가 들어있는지 확인하기 위해서 지역정보 쿼리를 미리 넣어둔다. (춘천, 서울, 등등...)

1
2
3
4
5
6
7
8
POST /wedul_product/seat/?refresh
{
    "query" : {
        "match" : {
            "message" : "춘천"
        }
    }
}
cs



문서 매칭되는 쿼리 찾아보기
'위들아이패드 서울 지점'과 '맥북 부산시 AS 지점' 두 개를 검색해보고 매칭 결과를 확인해보자. 쿼리는 score를 고려하는 것과 percolate를 사용해보자.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
#쿼리 (score 없는 filter 사용)
GET /wedul_product/_search
{
  "query": {
    "constant_score": {
      "filter": {
        "percolate": {
          "field": "query",
          "document": {
            "message": "위들아이패드 서울 지점"
          }
        }
      }
    }
  }
}
 
#결과
{
  "took": 55,
  "timed_out": false,
  "_shards": {
    "total": 5,
    "successful": 5,
    "skipped": 0,
    "failed": 0
  },
  "hits": {
    "total": 1,
    "max_score": 1,
    "hits": [
      {
        "_index": "wedul_product",
        "_type": "seat",
        "_id": "7dAz8mUBIvIDO7uZfj1s",
        "_score": 1,
        "_source": {
          "query": {
            "match": {
              "message": "서울"
            }
          }
        },
        "fields": {
          "_percolator_document_slot": [
            0
          ]
        }
      }
    ]
  }
}
 
# score가 고려된 percolate사용
GET /wedul_product/_search
{
  
  "query" : {
        "percolate" : {
            "field": "query",
            "document" : {
                "message" : "맥북 부산 시 AS 지점"
            }
        }
    }
}
 
#결과
{
  "took": 3,
  "timed_out": false,
  "_shards": {
    "total": 5,
    "successful": 5,
    "skipped": 0,
    "failed": 0
  },
  "hits": {
    "total": 1,
    "max_score": 0.2876821,
    "hits": [
      {
        "_index": "wedul_product",
        "_type": "seat",
        "_id": "BtAz8mUBIvIDO7uZkz5Q",
        "_score": 0.2876821,
        "_source": {
          "query": {
            "match": {
              "message": "부산"
            }
          }
        },
        "fields": {
          "_percolator_document_slot": [
            0
          ]
        }
      }
    ]
  }
}
cs


이를 이용해서 다양한 것을 할 수 있을 것 같다.

네이버에서도 이 기능을 이용해서 로그 알림 기능을 만들었다. 참고하면 좋을 것 같다.
https://d2.naver.com/helloworld/1044388

댓글()

Elasticsearch 질의 DSL 정리

엘라스틱 서치를 공부하면서 봤던 DSL 쿼리를 정리해보자.


Query와 Filter의 차이

Query 일반적으로 Full Text Search(전문검색) 사용되고 필터는 YES/NO 조건의 바이너리 구분에 주로 사용된다.
쿼리는 scoring 계산되나 필터는 계산되지 않는다.
쿼리 결과는 캐싱되지 않고 필터 결과는 캐싱된다.
상대적으로 쿼리는 응답속도가 느리고 필터는 응답속도가 빠르다.


term
- term 색인이 나눠지면서 형태소로 나누어지는 저장되는 토큰등을 term이라고 한다term 쿼리는 주어진 질의문과 저장된 텀과 정확히 일치하는 문장을 찾는다.
- term으로 "name" : "cjung gglee" 라고 입력하게 되는경우에는 "cjung gglee"라는 하나의 term 찾기 때문에 결과가 나오지 않는다만약 2 이상의 term 같이 검색하고 싶을 때는 terms 쿼리를 이용해야 한다.

1
2
3
4
5
6
7
8
9
10
# terms의 사용으로 두가지 term으로 사용할 수 있다.
GET /bank/_search
{
  "query": {
    "terms": {
      "age": [3020]
    }    
  }
}
 
cs


match

match 쿼리도 term 쿼리와 마찬가지로 주어진 질의문을 색인된 term 비교해서 일치하는 도큐먼트를검색하는 질의다만 term 쿼리와 다르게 match 쿼리에서는 주어진 질의문 또한 형태소 분석을 거친 분석된 질의문으로 검색을 수행한다예를 들면 The And 검색하면 매치 쿼리는  질의문을 형태소 분석을 거쳐서 the and 질의문을 바꾸고  값을 term 비교해서 검색한다.

그리고 기본적으로 match 들어가는 데이터들은 or 검색으로 진행된다다시말하면 아래의 예에서는Diamond 또는 Street 또는 Bartlett 라는 term으로 검색한다이것을 and 바꾸고 싶은 경우에는 "operator" : "and" 옵션을 넣어주어야 한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
GET /bank/_search
{
  "query": {
    "match": {
      "address": "Diamond Street Bartlett"
    }    
  }
}
 
 
operator 적용
GET /bank/_search
{
  "query": {
    "match": {
      "address": {
        "query" : "Diamond Street Bartlett",
        "operator" : "and"
      }
    }    
  }
}
 
cs


이렇게 색인을 나누고 term을 구분할 때 사용하는 형태소 분석기를 설정하기 위해서  analyzer 선택할  있다. (굳이 지금은 볼필요가 없을 것 같아 생략.)


multi_match 
여러 필드에 대한 조건을 검색할 
blance age 필드에서 값을 조회한다
두개세개 등등의 필드에서 검색한다.

1
2
3
4
5
6
7
8
9
10
GET /bank/_search
{
  "query": {
    "multi_match": {
      "fields": ["balance", "age"],
      "query": 20
    }
  }
}
 
cs


(bool) 쿼리
조건문인  조합으로 적용해서 최종 검색 결과를 찾아내자.
bool 조건에는 must, must_not, should가 존재한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
GET /bank/_search
{
  "query": {
    "bool": {
      "must": [
        {
          "term": {
            "address": {
              "value": "800"
            }
          }
        }
      ],
      "must_not": [
        {
          "term": {
            "state": {
              "value": "ID"
            }
          }
        }
      ]
    }
  }
}
 
cs


문자열 쿼리(q=)
URL 검색에서 q 매개변수에 다양한 질의문을 사용해서 검색을 수행했던 방식과 동일하게 사용할수 있는 방식이다.
여러 필드의 조건으로 검색 가능

1
2
3
4
5
6
7
8
9
10
GET /bank/_search
{
  "query": {
    "query_string": {
      "default_field": "address",
      "query": "Street 800"
    }
  }
}
 
cs


접두어 쿼리

1
2
3
4
5
6
7
8
9
10
11
GET /bank/_search
{
  "query": {
    "prefix": {
      "address": {
        "value": "800"
      }
    }
  }
}
 
cs


범위 쿼리 (날짜와 시간 데이터도 범위쿼리 사용가능)

1
2
3
4
5
6
7
8
9
10
11
12
GET /bank/_search
{
  "query": {
    "range": {
      "balance": {
        "gte": 10,
        "lte": 201212132
      }
    }
  }
}
 
cs


퍼지(fuzzy) 쿼리
주어진 질의문을 레벤슈타인 거리(Levenshtein distance) 알고리즘을 기반으로 유사한 단어의 검색을 지원
주소이름이 Noble 유사한 데이터들이 출력
특정 페이지 출력도 가능

1
2
3
4
5
6
7
8
9
GET /bank/_search
{
  "query": {
    "fuzzy": {
      "address": "Noble"
    }
  }
}
 
cs


Filter 쿼리
메모리도 캐싱되고 점수도 따지지 않기 때문에 단순 검색에서는 Filter 사용하라.
예전에는 filter 밖에 있을수 있었지만 이제는 query, bool 안에 있어야 사용가능

1
2
3
4
5
6
7
8
9
10
11
12
GET /bank/_search
{
  "query": {
    "bool": {
      "filter": {
        "terms": {
          "address": ["800", "Street"]
        }
      }
    }
  }
}
cs


댓글()

인덱스 생성 및 데이터 삽입

Elasticsearch에서 인덱스를 만들고 타입을 지정하여 데이터를 삽입하는 과정을 정리해보자. elasticsearch는 Restful API가 지원되기 때문에 BSL 쿼리를 이용하여 쉽게 데이터를 조작할 수 있다.


인덱스 생성

Methd : put
URLI : /{indexname}?pretty


생성된 인덱스 확인

Method : GET
URI : _cat/indices?v
kibana dev-tool에서 customer 인덱스가 생성된 것을 확인할 수 있다.


타입, Document 생성 및 데이터 추가

Method : PUT
URI : /{indexname}/{typename}/[documentid]?pretty
만약 documentid를 넣지 않으면 랜덤으로 만들어서 삽입된다.


입력된 데이터 확인
Method : GET
URI : /{indexname}/{typename}/{documentid}

데이터를 조회해보면 _source에 삽입한 데이터가 들어가 있는것을 확인할 수 있다


색인 삭제

Method : DELETE
URI : /{indexname}?pretty

데이터 삭제

Method : DELETE
URI : /{indexname}/{typename}/{documentid}?prettry

데이터 수정

Method : POST
URI : /{indexname}/{typename}/{documentid}/_update?pretty

변경된 내용 확인

Bulk update

Method : POST
URI : /{indexname}/{typename}/_bulk?pretty
여러가지 쿼리를 한번에 수행할 수 있다.

수행 결과

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
{
  "took"16,
  "errors"false,
  "items": [
    {
      "index": {
        "_index""customer",
        "_type""wedul",
        "_id""2",
        "_version"1,
        "result""created",
        "_shards": {
          "total"2,
          "successful"1,
          "failed"0
        },
        "_seq_no"0,
        "_primary_term"1,
        "status"201
      }
    },
    {
      "index": {
        "_index""customer",
        "_type""wedul",
        "_id""3",
        "_version"1,
        "result""created",
        "_shards": {
          "total"2,
          "successful"1,
          "failed"0
        },
        "_seq_no"0,
        "_primary_term"1,
        "status"201
      }
    },
    {
      "update": {
        "_index""customer",
        "_type""wedul",
        "_id""1",
        "_version"3,
        "result""updated",
        "_shards": {
          "total"2,
          "successful"1,
          "failed"0
        },
        "_seq_no"4,
        "_primary_term"1,
        "status"200
      }
    },
    {
      "delete": {
        "_index""customer",
        "_type""wedul",
        "_id""2",
        "_version"2,
        "result""deleted",
        "_shards": {
          "total"2,
          "successful"1,
          "failed"0
        },
        "_seq_no"1,
        "_primary_term"1,
        "status"200
      }
    }
  ]
}
cs

검색 API

기본적으로 검색 실행방법은 REST 요청 URI 통해 검색 매개변수를 보내는 방법과 REST 요청 본문을 통해 보내는 방식이있다. (REST 요청 본문으로 보내는 것은 위에서 확인 하였음.) 이 두가지 방식 중에 요청 본문 방식을 이용하는것이  효율적이다. 왜냐하면 URI를 조작해서 검색을 하는 것 보다 본문에 내용을 조작해서 수행하는 것이 더 유연하게 수정할 수 있다.

주소로 요청하기 위해서는 _search 엔드포인트를 사용해서 엑세스 해야한다.

그럼 먼저 주소 요청을 진행해보자.



URI 요청을 통한 검색

Method : GET
URI : /customer/_search?q=*&sort=age:desc&pretty
q=*  : Elasticssearch에게 색인의 모든 문서를 비교하여 일치 여부를 확인 하라고 표시.
sort={fieldname}:order_type(desc | asc) : 정렬을 사용할 필드와 정렬방식을 지정


결과
- took : Elasticsearch 검색하는데 걸린시간
- timed_out : 검색 시간 초과 여부
- _shards : 검색한 샤드   검색에 성공/실패한 샤드 
- hits: 검색 결과 (Array 순서대로 검색결과가 출력)
- hits > total :  검색 결과
- hits > hits : 검색 결과의 실제 배열
- hits > sort : 결과의 정렬 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
{
  "took": 1,
  "timed_out": false,
  "_shards": {
    "total": 5,
    "successful": 5,
    "skipped": 0,
    "failed": 0
  },
  "hits": {
    "total": 2,
    "max_score": null,
    "hits": [
      {
        "_index": "customer",
        "_type": "wedul",
        "_id": "3",
        "_score": null,
        "_source": {
          "name": "kskim",
          "age": 35,
          "add": "Gwang ju"
        },
        "sort": [
          35
        ]
      },
      {
        "_index": "customer",
        "_type": "wedul",
        "_id": "1",
        "_score": null,
        "_source": {
          "name": "wedul babo",
          "age": 28,
          "addr": "seoul"
        },
        "sort": [
          28
        ]
      }
    ]
  }
}
cs


#본문 요청 검색

Method : GET
URI : /{indexname}/_search_pretty
나머지 조건은 body에 삽입

1
2
3
4
5
6
7
GET /customer/_search?pretty
{
  "query": { "match_all": {} },
  "sort": [
    { "age": "desc" }
  ]
}
cs


다음번에는 더 자세한 검색 조건을 수행해보자.

댓글()