Elasticsearch實戰篇——Spring Boot整合ElasticSearch

當前Spring Boot很是流行,包括我自己,也是在用Spring Boot集成其他框架進行項目開發,所以這一節,我們一起來探討Spring Boot整合ElasticSearch的問題。

步驟如下:

第一步,通讀文檔。

第二步,用三種方式實現基本CRUD操作。

第三步,分析實戰中可能用到的查詢。

第四步,搜索專題。

還沒有學過Elasticsearch的朋友,可以先學這個系列的第一節(這個系列共三節),如果你有不明白或者不正確的地方,可以給我評論、留言或者私信。

第一步,通讀文檔

Spring Data Elasticsearch 官方文檔,這是當前最新的文檔。

關於repository

文檔一開始就介紹 CrudRepository ,比如,繼承 Repository,其他比如 JpaRepositoryMongoRepository是繼承CrudRepository。也對其中的方法做了簡單說明,我們一起來看一下:

public interface CrudRepository<T, ID extends Serializable>
  extends Repository<T, ID> {

// Saves the given entity.
  <S extends T> S save(S entity);      

// Returns the entity identified by the given ID.
  Optional<T> findById(ID primaryKey); 

// Returns all entities.
  Iterable<T> findAll();               

// Returns the number of entities.
  long count();                        

// Deletes the given entity.
  void delete(T entity);               

// Indicates whether an entity with the given ID exists.
  boolean existsById(ID primaryKey);   

  // … more functionality omitted.
}

好了,下面我們看一下今天的主角 ElasticsearchRepository 他是怎樣的吧。

這說明什麼?

  • 用法和JPA一樣;

  • 再這他除了有CRUD的基本功能之外,還有分頁和排序。

清楚了這之後,是不是應該考慮該如何使用了呢?

如何用?

沒錯,接下來,開始說如何用,也寫了很多示例代碼。相對來說,還是比較簡單,這裏就貼一下代碼就行了吧。

interface PersonRepository extends Repository<User, Long> {

  List<Person> findByEmailAddressAndLastname(EmailAddress emailAddress, String lastname);

  // Enables the distinct flag for the query
  List<Person> findDistinctPeopleByLastnameOrFirstname(String lastname, String firstname);
  List<Person> findPeopleDistinctByLastnameOrFirstname(String lastname, String firstname);

  // Enabling ignoring case for an individual property
  List<Person> findByLastnameIgnoreCase(String lastname);
  // Enabling ignoring case for all suitable properties
  List<Person> findByLastnameAndFirstnameAllIgnoreCase(String lastname, String firstname);

  // Enabling static ORDER BY for a query
  List<Person> findByLastnameOrderByFirstnameAsc(String lastname);
  List<Person> findByLastnameOrderByFirstnameDesc(String lastname);
}

是不是這樣,就可以正常使用了呢?

問題

當然可以,但是如果錯了問題怎麼辦呢,官網寫了一個常見的問題,比如包掃描問題,沒有你要的方法。

interface HumanRepository {
  void someHumanMethod(User user);
}

class HumanRepositoryImpl implements HumanRepository {

  public void someHumanMethod(User user) {
    // Your custom implementation
  }
}

interface ContactRepository {

  void someContactMethod(User user);

  User anotherContactMethod(User user);
}

class ContactRepositoryImpl implements ContactRepository {

  public void someContactMethod(User user) {
    // Your custom implementation
  }

  public User anotherContactMethod(User user) {
    // Your custom implementation
  }
}

你也可以自己寫接口,並且去實現它。

說完理論,作爲我,應該在實際的代碼中如何運用呢?

示例

官方也提供了很多示例代碼,我們一起來看看。

@Controller
class PersonController {

  @Autowired PersonRepository repository;

  @RequestMapping(value = "/persons", method = RequestMethod.GET)
  HttpEntity<PagedResources<Person>> persons(Pageable pageable,
    PagedResourcesAssembler assembler) {

    Page<Person> persons = repository.findAll(pageable);
    return new ResponseEntity<>(assembler.toResources(persons), HttpStatus.OK);
  }
}

這段代碼相對來說還是十分經典的,我相信很多人都看到別人的代碼,可能都會問,它爲什麼會這麼用呢,答案或許就在這裏吧。

當然,這是以前的代碼,或許現在用不一定合適。

高級搜索

終於到高潮了!

學完我的第一節,你應該已經發現了,Elasticsearch搜索是一件十分複雜的事,爲了用好它,我們不得不學好它。一起加油。

到這裏,官方文檔我們算是過了一遍了,大致明白了,他要告訴我們什麼。其實,文檔還有很多內容,可能你遇到的問題都能在裏面找到答案。

最後,我們繼續看一下官網寫的一段處理得十分優秀的一段代碼吧:

SearchQuery searchQuery = new NativeSearchQueryBuilder()
    .withQuery(matchAllQuery())
    .withIndices(INDEX_NAME)
    .withTypes(TYPE_NAME)
    .withFields("message")
    .withPageable(PageRequest.of(0, 10))
    .build();

CloseableIterator<SampleEntity> stream = elasticsearchTemplate.stream(searchQuery, SampleEntity.class);

List<SampleEntity> sampleEntities = new ArrayList<>();
while (stream.hasNext()) {
    sampleEntities.add(stream.next());
}

Spring Boot整合ElasticSearch

添加依賴

implementation 'org.springframework.boot:spring-boot-starter-data-elasticsearch'

添加配置

spring:
  data:
    elasticsearch:
      cluster-nodes: localhost:9300
      cluster-name: es-wyf

這樣就完成了整合,接下來我們用兩種方式操作。

Model

我們先寫一個的實體類,藉助這個實體類呢來完成基礎的CRUD功能。

@Data
@Accessors(chain = true)
@Document(indexName = "blog", type = "java")
public class BlogModel implements Serializable {

    private static final long serialVersionUID = 6320548148250372657L;

    @Id
    private String id;

    private String title;

    //@Field(type = FieldType.Date, format = DateFormat.basic_date)
    @DateTimeFormat(pattern = "yyyy-MM-dd")
    @JsonFormat(pattern = "yyyy-MM-dd", timezone = "GMT+8")
    private Date time;
}

注意id字段是必須的,可以不寫註解@Id。

Repository

BlogRepository

public interface BlogRepository extends ElasticsearchRepository<BlogModel, String> {
}

基礎操作

基礎操作的代碼,都是在 BlogController 裏面寫。

@RestController
@RequestMapping("/blog")
public class BlogController {
    @Autowired
    private BlogRepository blogRepository;
}

添加

@PostMapping("/add")
public Result add(@RequestBody BlogModel blogModel) {
    blogRepository.save(blogModel);
    return Result.success();
}

我們添加一條數據,標題是:Elasticsearch實戰篇:Spring Boot整合ElasticSearch,時間是:2019-03-06。我們來測試,看一下成不成功。

POST http://localhost:8080/blog/add

{
    "title":"Elasticsearch實戰篇:Spring Boot整合ElasticSearch",
    "time":"2019-05-06"
}

得到響應:

{
    "code": 0,
    "msg": "Success"
}

嘿,成功了。那接下來,我們一下查詢方法測試一下。

查詢

  • 根據ID查詢
@GetMapping("/get/{id}")
public Result getById(@PathVariable String id) {
    if (StringUtils.isEmpty(id))
        return Result.error();
    Optional<BlogModel> blogModelOptional = blogRepository.findById(id);
    if (blogModelOptional.isPresent()) {
        BlogModel blogModel = blogModelOptional.get();
        return Result.success(blogModel);
    }
    return Result.error();
}

測試一下:

ok,沒問題。

  • 查詢所有
@GetMapping("/get")
public Result getAll() {
    Iterable<BlogModel> iterable = blogRepository.findAll();
    List<BlogModel> list = new ArrayList<>();
    iterable.forEach(list::add);
    return Result.success(list);
}

測試一下:

GET http://localhost:8080/blog/get

結果:

{
    "code": 0,
    "msg": "Success",
    "data": [
        {
            "id": "fFXTTmkBTzBv3AXCweFS",
            "title": "Elasticsearch實戰篇:Spring Boot整合ElasticSearch",
            "time": "2019-05-06"
        }
    ]
}

根據ID修改

@PostMapping("/update")
public Result updateById(@RequestBody BlogModel blogModel) {
    String id = blogModel.getId();
    if (StringUtils.isEmpty(id))
        return Result.error();
    blogRepository.save(blogModel);
    return Result.success();
}

測試:

POST http://localhost:8080/blog/update

{
    "id":"fFXTTmkBTzBv3AXCweFS",
    "title":"Elasticsearch入門篇",
    "time":"2019-05-01"
}

響應:

{
    "code": 0,
    "msg": "Success"
}

查詢一下:

ok,成功!

刪除

  • 根據ID刪除
@DeleteMapping("/delete/{id}")
public Result deleteById(@PathVariable String id) {
    if (StringUtils.isEmpty(id))
        return Result.error();
    blogRepository.deleteById(id);
    return Result.success();
}

測試:

DELETE http://localhost:8080/blog/delete/fFXTTmkBTzBv3AXCweFS

響應:

{
    "code": 0,
    "msg": "Success"
}

我們再查一下:

  • 刪除所有數據
@DeleteMapping("/delete")
public Result deleteById() {
    blogRepository.deleteAll();
    return Result.success();
}

構造數據

爲了方便測試,我們先構造數據

Repository查詢操作

搜索標題中的關鍵字

BlogRepository

List<BlogModel> findByTitleLike(String keyword);

BlogController

@GetMapping("/rep/search/title")
public Result repSearchTitle(String keyword) {
    if (StringUtils.isEmpty(keyword))
        return Result.error();
    return Result.success(blogRepository.findByTitleLike(keyword));
}

我們來測試一下。

POST http://localhost:8080/blog/rep/search/title?keyword=java

結果:

{
    "code": 0,
    "msg": "Success",
    "data": [
        {
            "id": "f1XrTmkBTzBv3AXCeeFA",
            "title": "java實戰",
            "time": "2018-03-01"
        },
        {
            "id": "fVXrTmkBTzBv3AXCHuGH",
            "title": "java入門",
            "time": "2018-01-01"
        },
        {
            "id": "flXrTmkBTzBv3AXCUOHj",
            "title": "java基礎",
            "time": "2018-02-01"
        },
        {
            "id": "gFXrTmkBTzBv3AXCn-Eb",
            "title": "java web",
            "time": "2018-04-01"
        },
        {
            "id": "gVXrTmkBTzBv3AXCzuGh",
            "title": "java ee",
            "time": "2018-04-10"
        }
    ]
}

繼續搜索:

GET http://localhost:8080/blog/rep/search/title?keyword=入門

結果:

{
    "code": 0,
    "msg": "Success",
    "data": [
        {
            "id": "hFXsTmkBTzBv3AXCtOE6",
            "title": "Elasticsearch入門",
            "time": "2019-01-20"
        },
        {
            "id": "fVXrTmkBTzBv3AXCHuGH",
            "title": "java入門",
            "time": "2018-01-01"
        },
        {
            "id": "glXsTmkBTzBv3AXCBeH_",
            "title": "php入門",
            "time": "2018-05-10"
        }
    ]
}

爲了驗證,我們再換一個關鍵字搜索:

GET http://localhost:8080/blog/rep/search/title?keyword=java入門

{
    "code": 0,
    "msg": "Success",
    "data": [
        {
            "id": "fVXrTmkBTzBv3AXCHuGH",
            "title": "java入門",
            "time": "2018-01-01"
        },
        {
            "id": "hFXsTmkBTzBv3AXCtOE6",
            "title": "Elasticsearch入門",
            "time": "2019-01-20"
        },
        {
            "id": "glXsTmkBTzBv3AXCBeH_",
            "title": "php入門",
            "time": "2018-05-10"
        },
        {
            "id": "gFXrTmkBTzBv3AXCn-Eb",
            "title": "java web",
            "time": "2018-04-01"
        },
        {
            "id": "gVXrTmkBTzBv3AXCzuGh",
            "title": "java ee",
            "time": "2018-04-10"
        },
        {
            "id": "f1XrTmkBTzBv3AXCeeFA",
            "title": "java實戰",
            "time": "2018-03-01"
        },
        {
            "id": "flXrTmkBTzBv3AXCUOHj",
            "title": "java基礎",
            "time": "2018-02-01"
        }
    ]
}

哈哈,有沒有覺得很眼熟。

那根據上次的經驗,我們正好換一種方式解決這個問題。

@Query("{\"match_phrase\":{\"title\":\"?0\"}}")
List<BlogModel> findByTitleCustom(String keyword);

值得一提的是,官方文檔示例代碼可能是爲了好看,出現問題。

官網文檔給的錯誤示例:

官網示例代碼:

官方示例代碼

另外,?0 代指變量的意思。

@GetMapping("/rep/search/title/custom")
public Result repSearchTitleCustom(String keyword) {
    if (StringUtils.isEmpty(keyword))
        return Result.error();
    return Result.success(blogRepository.findByTitleCustom(keyword));
}

測試一下:

ok,沒有問題。

ElasticsearchTemplate

@Autowired
private ElasticsearchTemplate elasticsearchTemplate;

@GetMapping("/search/title")
public Result searchTitle(String keyword) {
    if (StringUtils.isEmpty(keyword))
        return Result.error();
    SearchQuery searchQuery = new NativeSearchQueryBuilder()
            .withQuery(queryStringQuery(keyword))
            .build();
    List<BlogModel> list = elasticsearchTemplate.queryForList(searchQuery, BlogModel.class);
    return Result.success(list);
}

測試:

POST http://localhost:8080/blog/search/title?keyword=java入門

結果:

{
    "code": 0,
    "msg": "Success",
    "data": [
        {
            "id": "fVXrTmkBTzBv3AXCHuGH",
            "title": "java入門",
            "time": "2018-01-01"
        },
        {
            "id": "hFXsTmkBTzBv3AXCtOE6",
            "title": "Elasticsearch入門",
            "time": "2019-01-20"
        },
        {
            "id": "glXsTmkBTzBv3AXCBeH_",
            "title": "php入門",
            "time": "2018-05-10"
        },
        {
            "id": "gFXrTmkBTzBv3AXCn-Eb",
            "title": "java web",
            "time": "2018-04-01"
        },
        {
            "id": "gVXrTmkBTzBv3AXCzuGh",
            "title": "java ee",
            "time": "2018-04-10"
        },
        {
            "id": "f1XrTmkBTzBv3AXCeeFA",
            "title": "java實戰",
            "time": "2018-03-01"
        },
        {
            "id": "flXrTmkBTzBv3AXCUOHj",
            "title": "java基礎",
            "time": "2018-02-01"
        }
    ]
}

OK,暫時先到這裏,關於搜索,我們後面會專門開一個專題,學習搜索。

Jest

搜索中,發現Jest也可以操縱Elasticsearch,目前官方有star數1500+。網址:https://github.com/spring-projects/spring-data-elasticsearch

另外,看到有人提供了Spring Boot整合的代碼,如下:

@Service
public class JestClientService implements Serializable {
 private static final long serialVersionUID = 1L;
JestClient client=null;
 
 @Value(“${jest.elasticsearch.host}”)
 String host;
 
 @Value(“${jest.elasticsearch.port}”)
 String port;
 
 @Value(“${jest.elasticsearch.index}”)
 String indexName;
/**
 * 
 */
 public JestClient getClient() {
 if (this.client==null){
GsonFireBuilder fireBuilder = new GsonFireBuilder();
 fireBuilder.enableExposeMethodResult();
 
 GsonBuilder builder = fireBuilder.createGsonBuilder();
 builder.excludeFieldsWithoutExposeAnnotation();
 
 final Gson gson = builder.setDateFormat(AbstractJestClient.ELASTIC_SEARCH_DATE_FORMAT).create();
System.out.println(“Establishing JEST Connection to Elasticsearch over HTTP: “+”http://”+this.host+”:”+this.port);
 JestClientFactory factory = new JestClientFactory();
 factory.setHttpClientConfig(new HttpClientConfig
 .Builder(“http://”+this.host+”:”+this.port)
 .multiThreaded(true)
 .readTimeout(20000)
 .gson(gson)
 .build());
 this.client = factory.getObject();
 
 }
 
 return this.client;
 }
}

鏈接

ElasticSearch 學習系列

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章