Spring Data Elasticsearch 시작
45717 단어 elasticsearchdockerjava
같은 필드를 검색하고 정렬할 수 있도록 색인 필드를 만드는 것과 같은 일반적인 작업도 설명합니다.여러 필드에서 검색하고 필터링하기 위해 조회를 만드는 방법
새 Springboot 프로젝트 만들기
새로운 Springboot 프로젝트가 필요할 때마다 start.spring.io 생성합니다.이것은 내 설정입니다.
필요한 항목 이름과 의존 항목을 선택한 후 생성을 누르고 다운로드한 zip 파일을 추출합니다.이 예에서 프로젝트 디렉토리는 spring-data-elasticsearch-example
Elasticsearch가 시작되고 실행되었는지 확인합니다.
우선, 응용 프로그램이 연결될 수 있도록 Elasticsearch를 실행해야 합니다.
간단한 선택은 docker compose를 사용하여 하나의 Elasticsearch 노드를 시작하는 것이다
왜냐하면 나는 이미 나의 맥에 docker와dockercomposeDocker Desktop를 설치했기 때문이다.elasticsearch.yml
파일을 만들고 docker compose로 Elasticsearch 용기를 시작합니다.elasticsearch.yml
에서 생성src/main/docker
cd spring-data-elasticsearch-example
mkdir -p src/main/docker && touch src/main/docker/elasticsearch.yml
다음이 포함됩니다.
version: '2'
services:
my-elasticsearch:
image: docker.elastic.co/elasticsearch/elasticsearch:7.9.2
container_name: my-elasticsearch
# volumes:
# - ~/data/my-elasticsearch/:/usr/share/elasticsearch/data/
ports:
- 9200:9200
environment:
- 'ES_JAVA_OPTS=-Xms1024m -Xmx1024m'
- 'discovery.type=single-node'
그런 다음 Elasticsearch 컨테이너를 시작합니다.
docker-compose -f src/main/docker/elasticsearch.yml up -d
현재 Elasticsearch가 시작되었으며http://localhost:9200/
자세한 내용은 Elasticsearchdocumentation에서 확인할 수 있습니다.
ElasticSearch의 인덱스 데이터
예를 들어 Book과 Author의 예가 있습니다.나는 모든 책과 작가를 책이라는 색인에 편입시키고 싶다.
엔티티 작성 및 색인 방식 정의
@Getter
@Setter
@Accessors(chain = true)
@EqualsAndHashCode
@ToString
@Document(indexName="books")
public class Book {
@Id
private String id;
@MultiField(
mainField = @Field(type = FieldType.Text, fielddata = true),
otherFields = {
@InnerField(suffix = "raw", type = FieldType.Keyword)
}
)
private String name;
@Field(type = FieldType.Text)
private String summary;
@Field(type = FieldType.Double)
private Double price;
@Field(type = FieldType.Object)
private List<Author> authors;
}
Book entity에서 나는 @Document, @Field, @MultiField 등 다른 주석을 사용하여 이 실체와 속성을 색인하는 방법을 표시합니다.
우선, 응용 프로그램이 연결될 수 있도록 Elasticsearch를 실행해야 합니다.
간단한 선택은 docker compose를 사용하여 하나의 Elasticsearch 노드를 시작하는 것이다
왜냐하면 나는 이미 나의 맥에 docker와dockercomposeDocker Desktop를 설치했기 때문이다.
elasticsearch.yml
파일을 만들고 docker compose로 Elasticsearch 용기를 시작합니다.elasticsearch.yml
에서 생성src/main/docker
cd spring-data-elasticsearch-example
mkdir -p src/main/docker && touch src/main/docker/elasticsearch.yml
다음이 포함됩니다.version: '2'
services:
my-elasticsearch:
image: docker.elastic.co/elasticsearch/elasticsearch:7.9.2
container_name: my-elasticsearch
# volumes:
# - ~/data/my-elasticsearch/:/usr/share/elasticsearch/data/
ports:
- 9200:9200
environment:
- 'ES_JAVA_OPTS=-Xms1024m -Xmx1024m'
- 'discovery.type=single-node'
그런 다음 Elasticsearch 컨테이너를 시작합니다.docker-compose -f src/main/docker/elasticsearch.yml up -d
현재 Elasticsearch가 시작되었으며http://localhost:9200/자세한 내용은 Elasticsearchdocumentation에서 확인할 수 있습니다.
ElasticSearch의 인덱스 데이터
예를 들어 Book과 Author의 예가 있습니다.나는 모든 책과 작가를 책이라는 색인에 편입시키고 싶다.
엔티티 작성 및 색인 방식 정의
@Getter
@Setter
@Accessors(chain = true)
@EqualsAndHashCode
@ToString
@Document(indexName="books")
public class Book {
@Id
private String id;
@MultiField(
mainField = @Field(type = FieldType.Text, fielddata = true),
otherFields = {
@InnerField(suffix = "raw", type = FieldType.Keyword)
}
)
private String name;
@Field(type = FieldType.Text)
private String summary;
@Field(type = FieldType.Double)
private Double price;
@Field(type = FieldType.Object)
private List<Author> authors;
}
Book entity에서 나는 @Document, @Field, @MultiField 등 다른 주석을 사용하여 이 실체와 속성을 색인하는 방법을 표시합니다.
@Getter
@Setter
@Accessors(chain = true)
@EqualsAndHashCode
@ToString
@Document(indexName="books")
public class Book {
@Id
private String id;
@MultiField(
mainField = @Field(type = FieldType.Text, fielddata = true),
otherFields = {
@InnerField(suffix = "raw", type = FieldType.Keyword)
}
)
private String name;
@Field(type = FieldType.Text)
private String summary;
@Field(type = FieldType.Double)
private Double price;
@Field(type = FieldType.Object)
private List<Author> authors;
}
@Document(indexName="books")
는 책을 books
라는 색인에 저장하고 싶다고 밝혔다.기본적으로 색인 매핑은 Java 응용 프로그램을 시작할 때 생성됩니다.@Document 메모에는 여러 속성이 있습니다.자세한 내용은 공식 웹 사이트 documentation 를 참조하십시오.@Id
: 표지 목적에 사용되며 id에 따라 서적을 조회하거나 Elasticsearch의 기존 서적을 업데이트하는 데 도움이 됩니다.@Field
: 문자열이나 부울 값 및 예상 용도와 같은 필드에 포함된 데이터 유형을 지정합니다.데이터 유형 목록은 mapping-types 문서에서 찾을 수 있습니다.이 밖에도 analyzer, searchAnalyzer 및 normalizer 사용자 정의가 가능합니다.Elasticsearch에서 standard analyzer 은 기본 분석기입니다.
summary
에는 텍스트 유형이, price
에는 이중 유형이 있습니다.name
를 사용하여 텍스트 및 키워드 필드에 주석 인덱스를 사용합니다.마스터@MultiField
필드를 분석하여 전체 텍스트 검색을 수행하고 Text
@InnerField
은raw
으로 Elasticsearch에서 변경되지 않으며 정렬(또는 아래 저자 이름의 경우 필터링)에 사용할 수 있습니다.Keyword
는 내부 필드이므로 raw
로 액세스할 수 있습니다.name.raw
는 중첩된 JSON 객체로 색인됩니다.@Getter
@Setter
@Accessors(chain = true)
@EqualsAndHashCode
@ToString
public class Author {
@Id
private String id;
@MultiField(
mainField = @Field(type = FieldType.Text, fielddata = true),
otherFields = {
@InnerField(suffix = "raw", type = FieldType.Keyword)
}
)
private String name;
}
저자의 실체는 연락처와 같은 다른 끼워 넣는 대상을 포함할 수 있지만, 이 블로그 글에 대해 나는 원래의 모양을 유지하고 간단하다.책 제목과 유사하게
authors
또한 Author's name
(mainField) 와 키워드 (innerField Text
로 인덱스되어 raw
필드를 검색하고 author.name
필드를 필터링하고 정렬할 수 있습니다.고급 REST 클라이언트 구성
다음 단계는 제가 실행하는 Elasticserach와의 연결을 설정하고 저장소를 만들어서 책을 색인하고 검색할 수 있도록 합니다.
@Configuration
@EnableElasticsearchRepositories(
basePackages = "dev.vuongdang.springdataelasticsearchexample.repository"
)
public class ElasticSearchConfig extends AbstractElasticsearchConfiguration {
@Override
@Bean
public RestHighLevelClient elasticsearchClient() {
final ClientConfiguration clientConfiguration = ClientConfiguration.builder()
.connectedTo("localhost:9200")
.build();
return RestClients.create(clientConfiguration).rest();
}
}
ClientConfiguration을 구성해야 하지만 응용 프로그램에서는 일반적으로 더 높은 수준의 추상author.name.raw
을 사용합니다.Elasticsearch Repositories
패키지에 정의된 모든 저장소 인터페이스에 대한 저장소 지원을 활성화합니다.Spring Data Elasticsearch를 사용하여 인터페이스를 정의하면 자동으로 처리됩니다.위의 ClienConfiguration은 SSL,connect,socket 시간 초과, 헤더 및 기타 매개 변수에 대한 옵션을 설정할 수 있습니다.예:
ClientConfiguration clientConfiguration = ClientConfiguration.builder()
.connectedTo("localhost:9200")
.useSsl()
.withConnectTimeout(Duration.ofSeconds(5))
.withSocketTimeout(Duration.ofSeconds(3))
.withBasicAuth(username, password);
위의 클라이언트 구성은 에 연결됩니다@EnableElasticsearchRepositories
.dev.vuongdang.springdataelasticsearchexample.repository
를 사용하는 경우 다음과 같이 구성할 수 있습니다.final ClientConfiguration clientConfiguration = ClientConfiguration.builder()
.connectedTo("sass-testing-1537538524.eu-central-1.bonsaisearch.net:443")
.usingSsl()
.withBasicAuth("<username>", "<password>")
.build();
저장소 만들기
/**
* Define the repository interface. The implementation is done by Spring Data Elasticsearch
*/
public interface BookSearchRepository extends ElasticsearchRepository<Book, String> {
List<Book> findByAuthorsNameContaining(String name);
}
저장소를 사용하는 방법을 설명하는 테스트를 만듭니다.나는 세 권의 책을 만들고 이 동작들이 정상적으로 작동하는지 확인한다. 저장, Id 찾기, 저자 이름 찾기.@SpringBootTest
class BookServiceTest {
@Autowired
private BookService bookService;
@Autowired
private BookSearchRepository bookSearchRepository;
@Autowired
private ElasticsearchOperations elasticsearchOperations;
public static final String BOOK_ID_1 = "1";
public static final String BOOK_ID_2 = "2";
public static final String BOOK_ID_3 = "3";
private Book book1;
private Book book2;
private Book book3;
@BeforeEach
public void beforeEach() {
// Delete and recreate index
IndexOperations indexOperations = elasticsearchOperations.indexOps(Book.class);
indexOperations.delete();
indexOperations.create();
indexOperations.putMapping(indexOperations.createMapping());
// add 2 books to elasticsearch
Author markTwain = new Author().setId("1").setName("Mark Twain");
book1 = bookSearchRepository
.save(new Book().setId(BOOK_ID_1).setName("The Mysterious Stranger")
.setAuthors(singletonList(markTwain))
.setSummary("This is a fiction book"));
book2 = bookSearchRepository
.save(new Book().setId(BOOK_ID_2).setName("The Innocents Abroad")
.setAuthors(singletonList(markTwain))
.setSummary("This is a special book")
);
book3 = bookSearchRepository
.save(new Book().setId(BOOK_ID_3).setName("The Other Side of the Sky").setAuthors(
Arrays.asList(new Author().setId("2").setName("Amie Kaufman"),
new Author().setId("3").setName("Meagan Spooner"))));
}
/**
* Read books by id and ensure data are saved properly
*/
@Test
void findById() {
assertEquals(book1, bookSearchRepository.findById(BOOK_ID_1).orElse(null));
assertEquals(book2, bookSearchRepository.findById(BOOK_ID_2).orElse(null));
assertEquals(book3, bookSearchRepository.findById(BOOK_ID_3).orElse(null));
}
@Test
public void query() {
List<Book> books = bookSearchRepository.findByAuthorsNameContaining("Mark");
assertEquals(2, books.size());
assertEquals(book1, books.get(0));
assertEquals(book2, books.get(1));
}
}
localhost:9200
방법에서, 나는 매번 테스트에 새로운 데이터가 있는지 확인하기 위해 색인을 다시 만들고 책 세 권을 삽입했다.localhost:9200/books/_search로 이동하여 모든 색인 서적을 볼 수 있습니다.
또는 localhost:9200/books/_mapping로 이동하여 각 필드의 세부 매핑을 확인합니다.
검색 및 필터링 app.bonsai.io
에서 Elasticsearch JSON 조회로 자동으로 해석되는 방법을 명명할 수 있습니다.다른 방법은 주석을 사용하여 JSON 질의를 정의하는 것입니다.예:
@Query("{\"match\": {\"name\": {\"query\": \"?0\"}}}")
Page<Book> findByName(String name, Pageable pageable);
이러한 것들은 간단한 검색에 있어서는 매우 좋지만, 실천에서, 우리는 보통 최종 사용자에게 검색 필드, 필터, 정렬을 제공한다.이 점을 실현하기 위해서, 나는 내장된 검색 생성기를 사용하는 것을 더욱 좋아한다. 왜냐하면 그것은 유연성을 가지고 있기 때문이다.
여러 필드에서 검색, 필터링, 정렬을 위해 검색 생성기를 사용하여 복잡한 검색을 만드는 방법을 설명하는 BookService를 만듭니다.
@Service
public class BookService {
@Getter
@Setter
@Accessors(chain = true)
@ToString
public static class BookSearchInput {
private String searchText;
private BookFilter filter;
}
@Getter
@Setter
@Accessors(chain = true)
@ToString
public static class BookFilter {
private String authorName;
}
@Autowired
private ElasticsearchOperations operations;
public SearchPage<Book> searchBooks(BookSearchInput searchInput, Pageable pageable) {
// query
QueryBuilder queryBuilder;
if(searchInput == null || isEmpty(searchInput.getSearchText())) {
// search text is empty, match all results
queryBuilder = QueryBuilders.matchAllQuery();
} else {
// search text is available, match the search text in name, summary, and authors.name
queryBuilder = QueryBuilders.multiMatchQuery(searchInput.getSearchText())
.field("name", 3)
.field("summary")
.field("authors.name")
.fuzziness(Fuzziness.ONE) //fuzziness means the edit distance: the number of one-character changes that need to be made to one string to make it the same as another string
.prefixLength(2);//The prefix_length parameter is used to improve performance. In this case, we require that the first three characters should match exactly, which reduces the number of possible combinations.;
}
// filter by author name
BoolQueryBuilder filterBuilder = boolQuery();
if(searchInput.getFilter() != null && isNotEmpty(searchInput.getFilter().getAuthorName())){
filterBuilder.must(termQuery("authors.name.raw", searchInput.getFilter().getAuthorName()));
}
NativeSearchQuery query = new NativeSearchQueryBuilder().withQuery(queryBuilder)
.withFilter(filterBuilder)
.withPageable(pageable)
.build();
SearchHits<Book> hits = operations.search(query, Book.class);
return SearchHitSupport.searchPageFor(hits, query.getPageable());
}
}
이 Book Service #searchBook을 테스트하기 위해 위의 Book Service Test에 테스트 방법을 추가했습니다.
@Test
void searchBook() {
// Define page request: return the first 10 results. Sort by book's name ASC
Pageable pageable = PageRequest.of(0, 10, Direction.ASC, "name.raw");
// Case 1: search all books: should return 3 books
assertEquals(3, bookService.searchBooks(new BookSearchInput(), pageable)
.getTotalElements());
// Case 2: filter books by author Mark Twain: Should return [book2, book1]
SearchPage<Book> booksByAuthor = bookService.searchBooks(
new BookSearchInput().setFilter(new BookFilter().setAuthorName("Mark Twain")),
pageable); // sort by book name asc
assertEquals(2, booksByAuthor.getTotalElements());
Iterator<SearchHit<Book>> iterator = booksByAuthor.iterator();
assertEquals(book2, iterator.next().getContent()); // The Innocents Abroad
assertEquals(book1, iterator.next().getContent()); // The Mysterious Stranger
// Case 3: search by text 'special': Should return book 2 because it has summary containing 'special'
// one typo in the search text: (specila) is accepted thanks to `fuziness`
SearchPage<Book> specialBook = bookService
.searchBooks(new BookSearchInput().setSearchText("specila"), pageable);// book 2
assertEquals(1, specialBook.getTotalElements());
assertEquals(book2, specialBook.getContent().iterator().next().getContent()); // The Innocents Abroad
}
위의 beforeEach
에서 검색 텍스트에 맞춤법 오류가 있습니다. 이것은 BookSearchRepository
이지 특수한 것이 아닙니다.조회 생성기에 설치되어 있기 때문에 정상적으로 작동합니다. @Query
.
로그인 중
나는 정확한 조회를 만들기 위해 개발 환경에서 JSON 조회를 기록하는 것이 매우 유용하다는 것을 발견했다.이 로그는 Case 3
에서 사용할 수 있습니다.
logging.level.org.springframework.data.elasticsearch.client.WIRE=trace
현재, 내가 specila
테스트 방법을 실행할 때, 로그 파일에서 Elasticsearch 조회를 볼 수 있다. 아래와 같다.
{
"from": 0,
"size": 10,
"query": {
"multi_match": {
"query": "special",
"fields": [
"authors.name^1.0",
"name^3.0",
"summary^1.0"
],
"type": "best_fields",
"operator": "OR",
"slop": 0,
"fuzziness": "1",
"prefix_length": 2,
"max_expansions": 50,
"zero_terms_query": "NONE",
"auto_generate_synonyms_phrase_query": true,
"fuzzy_transpositions": true,
"boost": 1.0
}
},
"post_filter": {
"bool": {
"adjust_pure_negative": true,
"boost": 1.0
}
},
"version": true,
"sort": [
{
"name.raw": {
"order": "asc"
}
}
]
}
결론
이 박문에서 나는 다음과 같은 주제를 소개했다.
@Query("{\"match\": {\"name\": {\"query\": \"?0\"}}}")
Page<Book> findByName(String name, Pageable pageable);
@Service
public class BookService {
@Getter
@Setter
@Accessors(chain = true)
@ToString
public static class BookSearchInput {
private String searchText;
private BookFilter filter;
}
@Getter
@Setter
@Accessors(chain = true)
@ToString
public static class BookFilter {
private String authorName;
}
@Autowired
private ElasticsearchOperations operations;
public SearchPage<Book> searchBooks(BookSearchInput searchInput, Pageable pageable) {
// query
QueryBuilder queryBuilder;
if(searchInput == null || isEmpty(searchInput.getSearchText())) {
// search text is empty, match all results
queryBuilder = QueryBuilders.matchAllQuery();
} else {
// search text is available, match the search text in name, summary, and authors.name
queryBuilder = QueryBuilders.multiMatchQuery(searchInput.getSearchText())
.field("name", 3)
.field("summary")
.field("authors.name")
.fuzziness(Fuzziness.ONE) //fuzziness means the edit distance: the number of one-character changes that need to be made to one string to make it the same as another string
.prefixLength(2);//The prefix_length parameter is used to improve performance. In this case, we require that the first three characters should match exactly, which reduces the number of possible combinations.;
}
// filter by author name
BoolQueryBuilder filterBuilder = boolQuery();
if(searchInput.getFilter() != null && isNotEmpty(searchInput.getFilter().getAuthorName())){
filterBuilder.must(termQuery("authors.name.raw", searchInput.getFilter().getAuthorName()));
}
NativeSearchQuery query = new NativeSearchQueryBuilder().withQuery(queryBuilder)
.withFilter(filterBuilder)
.withPageable(pageable)
.build();
SearchHits<Book> hits = operations.search(query, Book.class);
return SearchHitSupport.searchPageFor(hits, query.getPageable());
}
}
@Test
void searchBook() {
// Define page request: return the first 10 results. Sort by book's name ASC
Pageable pageable = PageRequest.of(0, 10, Direction.ASC, "name.raw");
// Case 1: search all books: should return 3 books
assertEquals(3, bookService.searchBooks(new BookSearchInput(), pageable)
.getTotalElements());
// Case 2: filter books by author Mark Twain: Should return [book2, book1]
SearchPage<Book> booksByAuthor = bookService.searchBooks(
new BookSearchInput().setFilter(new BookFilter().setAuthorName("Mark Twain")),
pageable); // sort by book name asc
assertEquals(2, booksByAuthor.getTotalElements());
Iterator<SearchHit<Book>> iterator = booksByAuthor.iterator();
assertEquals(book2, iterator.next().getContent()); // The Innocents Abroad
assertEquals(book1, iterator.next().getContent()); // The Mysterious Stranger
// Case 3: search by text 'special': Should return book 2 because it has summary containing 'special'
// one typo in the search text: (specila) is accepted thanks to `fuziness`
SearchPage<Book> specialBook = bookService
.searchBooks(new BookSearchInput().setSearchText("specila"), pageable);// book 2
assertEquals(1, specialBook.getTotalElements());
assertEquals(book2, specialBook.getContent().iterator().next().getContent()); // The Innocents Abroad
}
나는 정확한 조회를 만들기 위해 개발 환경에서 JSON 조회를 기록하는 것이 매우 유용하다는 것을 발견했다.이 로그는
Case 3
에서 사용할 수 있습니다.logging.level.org.springframework.data.elasticsearch.client.WIRE=trace
현재, 내가 specila
테스트 방법을 실행할 때, 로그 파일에서 Elasticsearch 조회를 볼 수 있다. 아래와 같다.{
"from": 0,
"size": 10,
"query": {
"multi_match": {
"query": "special",
"fields": [
"authors.name^1.0",
"name^3.0",
"summary^1.0"
],
"type": "best_fields",
"operator": "OR",
"slop": 0,
"fuzziness": "1",
"prefix_length": 2,
"max_expansions": 50,
"zero_terms_query": "NONE",
"auto_generate_synonyms_phrase_query": true,
"fuzzy_transpositions": true,
"boost": 1.0
}
},
"post_filter": {
"bool": {
"adjust_pure_negative": true,
"boost": 1.0
}
},
"version": true,
"sort": [
{
"name.raw": {
"order": "asc"
}
}
]
}
결론
이 박문에서 나는 다음과 같은 주제를 소개했다.
샘플 소스 코드는 Github에서 찾을 수 있습니다here.
Reference
이 문제에 관하여(Spring Data Elasticsearch 시작), 우리는 이곳에서 더 많은 자료를 발견하고 링크를 클릭하여 보았다 https://dev.to/vuongddang/getting-started-with-spring-data-elasticsearch-198h텍스트를 자유롭게 공유하거나 복사할 수 있습니다.하지만 이 문서의 URL은 참조 URL로 남겨 두십시오.
우수한 개발자 콘텐츠 발견에 전념 (Collection and Share based on the CC Protocol.)