Full text search là một chức năng khá phổ biến đối với các Hệ thống thông tin. Full text search cung cấp nhiều chọn lựa hơn cho ngưòi dùng hơn các quét search like đơn thuần.

1. Giới thiệu

Full Text Search giúp cho việc tìm kiếm trở nên có nhiều sự lựa chọn hơn cho người dùng hơn, tìm kiếm được nhiều thông tin hơn, bên cạnh đó còn cải thiện được tốc độ tìm kiếm. Thông thường để làm được chức năng Full Text Search lập trình viên có 1 trong 2 lựa chọn:

  • Sử dụng cơ chế Full Text Search Native của Database: nghĩa là chúng ta sẽ phải viết câu Native query khác nhau cho từng loại Database khác nhau. Bên cạnh đó một số loại Database còn phải config ít nhiều để có thể enable Full Text Search. Lập trình viên thường phải can thiệp ít nhiều xuống tầng Database, bởi mỗi Database khác nhau có cơ chế thực hiện và cú pháp sử dụng khác nhau.
  • Sử dụng các Query Database như: Elastic Search, Apache lucene, Solr, ... để index các dữ liệu cần tìm kiếm. Các Database này sẽ dựa vào các index để so sánh với dữ liệu yêu cầu đầu vào, điều này sẽ giúp cho kết quả tìm kiếm chính xác hơn với thời gian nhanh hơn. Tuy nhiên chúng ta sẽ phải setup riêng những Database và những config đi kèm theo nó.

Hibernate search cung cấp một cơ chế hầu như one for all bằng cách tích hợp sẵn một Database tìm kiếm Apache Lucence bên trong. Về bản chất Hibernate Search chính là phương pháp số 2 nhưng với giải pháp đi kèm là giúp cho việc setup trở nên đơn giản hơn với 1 dòng dependency:

<dependency>
    <groupId>org.hibernate</groupId>
    <artifactId>hibernate-search</artifactId>
    <version>5.11.1.Final</version>
    <type>pom</type>
</dependency>

 

2. Hướng dẫn code

Thư viện Maven:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="https://maven.apache.org/POM/4.0.0"
         xmlns:xsi="https://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="https://maven.apache.org/POM/4.0.0
	https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <artifactId>spring-boot-hibernate-search</artifactId>
    <packaging>jar</packaging>
    <version>1.0</version>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.1.5.RELEASE</version>
    </parent>

    <properties>
        <java.version>1.8</java.version>
    </properties>

    <dependencies>
        <!-- Spring Boot -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
            <scope>runtime</scope>
        </dependency>

        <dependency>
            <groupId>com.h2database</groupId>
            <artifactId>h2</artifactId>
            <version>1.4.196</version>
        </dependency>

        <dependency>
            <groupId>org.hibernate</groupId>
            <artifactId>hibernate-search-orm</artifactId>
            <version>5.8.2.Final</version>
        </dependency>

    </dependencies>
    <build>
        <plugins>
            <!-- Package as an executable jar/war -->
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
</project>

 

Cú pháp sử dụng Full Text Search trong Hibernate Search như sau:

org.apache.lucene.search.Query query = queryBuilder
  .keyword()
  .onField("title")
  .matching("xxx")
  .createQuery();

org.hibernate.search.jpa.FullTextQuery jpaQuery
  = fullTextEntityManager.createFullTextQuery(query, Post.class);

List<Post> results = jpaQuery.getResultList();

Có thể thấy rằng cú pháp sử dụng khá giống với Hibernate đơn thuần.

Một điểm mà người viết ưa thích ở Hibernate Search chính là khả năng tương thích với Spring Data. Vì bản chất Spring Data chính là Hibernate nên việc chúng có khả năng hỗ trợ lẫn nhau cũng không quá khó hiểu, điều nay mang lại rất nhiều sự tiện lợi cho lập trình viên khi cấu hình Hibernate kết nối với Database và Hibernate Search dùng để tìm kiếm. Trước đây với Spring MVC và Hibernate chúng ta có thể phải config toàn bộ hoặc một phần bằng file XML, điều mà hiện nay đã đươc Spring Boot giản đơn đi.

 

Model Post

import org.hibernate.annotations.Type;
import org.hibernate.search.annotations.Field;
import org.hibernate.search.annotations.Indexed;
import org.hibernate.search.annotations.TermVector;

import javax.persistence.*;

@Entity
@Table(name = "test_post")
@Indexed
public class Post {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Field(termVector = TermVector.YES, analyze=Analyze.YES, store=Store.NO)
    private String title;

    @Field(termVector = TermVector.YES, analyze=Analyze.YES, store=Store.NO)
    @Type(type = "text")
    private String content;

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public String getTitle() {
        return title;
    }

    public void setTitle(String title) {
        this.title = title;
    }

    public String getContent() {
        return content;
    }

    public void setContent(String content) {
        this.content = content;
    }
}

Khi chúng ta muốn Hibernate Search tìm kiếm với Model nào thì chúng ta khai báo @Indexed trên tên class của Model đó, đồng thời đánh index cho các trường mà chúng ta sẽ dùng để tìm kiếm. Vậy nếu với lượng data đã có từ trước thì sao? Chúng ta sẽ chạy hàm sau để re-index lại cho toàn bộ dữ liệu trong database.

 

PostController

import org.hibernate.search.jpa.FullTextEntityManager;
import org.hibernate.search.jpa.Search;
import org.hibernate.search.query.dsl.QueryBuilder;
import org.json.JSONArray;
import org.json.JSONObject;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpMethod;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.client.RestTemplate;
import thecoderoot.springboot.hibernatesearch.model.Post;
import thecoderoot.springboot.hibernatesearch.repository.PostRepository;

import javax.persistence.EntityManager;
import java.util.List;
import java.util.Objects;

@RestController
@RequestMapping(value = "/post")
public class PostController {

    @Autowired
    private EntityManager entityManager;

    @Autowired
    private PostRepository postRepository;

    @GetMapping(value = "/search")
    public List<Post> fullTextSearch(@RequestParam(value = "searchKey") String searchKey) {
        FullTextEntityManager fullTextEntityManager = Search.getFullTextEntityManager(entityManager);

        QueryBuilder queryBuilder = fullTextEntityManager.getSearchFactory()
                .buildQueryBuilder()
                .forEntity(Post.class)
                .get();

        org.apache.lucene.search.Query query = queryBuilder
                .keyword()
                .onFields("title", "content")
                .matching(searchKey)
                .createQuery();

        org.hibernate.search.jpa.FullTextQuery jpaQuery
                = fullTextEntityManager.createFullTextQuery(query, Post.class);

        return jpaQuery.getResultList();

    }

    @GetMapping(value = "/insert")
    public ResponseEntity<String> insert(){
        RestTemplate restTemplate = new RestTemplate();
        String fakeApiUrl = "https://jsonplaceholder.typicode.com/posts";

        ResponseEntity<String> responseEntity = restTemplate.exchange(fakeApiUrl, HttpMethod.GET, null, String.class);

        JSONArray jsonArray = new JSONArray(Objects.requireNonNull(responseEntity.getBody()));
        int length = jsonArray.length();

        for (int i = 0; i < length; i++){
            JSONObject jsonObject = jsonArray.getJSONObject(i);
            Post post = new Post();
            post.setTitle(jsonObject.getString("title"));
            post.setContent(jsonObject.getString("body"));
            postRepository.save(post);
        }

        return ResponseEntity.ok("success");
    }
}

Ở controller chúng ta implement 2 method:

  • insert: để test đươc hàm search tất nhiên chúng ta phải có data thì mới search được. Trong bài hướng dẫn này người viết sử dụng mock api tại trang JSON Place Holder để lấy dữ liệu dưới dạng JSON và insert xuống DB (MySQL).
  • search: dùng chức năng Full Text search của Hibernate Search để tìm kiếm data trên các trường "title" và "content" của Model Post.

 

PostRepository

import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import thecoderoot.springboot.hibernatesearch.model.Post;

@Repository
public interface PostRepository extends JpaRepository<Post, Long> {
}

Vì chúng ta chỉ sử dụng hàm save của Repository và cả chức năng search chúng ta cũng sử dụng của Hibernate Search  nên không cần phải implement các method khác.

 

3. Testing:

Để test được project này, đầu tiên chúng ta phải setup trước 1 Database trước, và các bạn nhớ điều chỉnh thông tin kết nối Database trong file application.properties. Các bạn có thể tìm đọc lại bài Kết nối CSDL .Bước tiếp theo các bạn phải trigger url để ứng dụng lấy dữ liệu giả và insert vào Database:

localhost:8899/post/insert

Sau cùng chúng ta có thể test được chức năng bằng url sau:

localhost:8899/post/search?searchKey=<search>

Thay thế <search> bằng cụm từ mà các bạn muốn tìm kiếm.

Các bạn có thể download source code tại đây.

Chúc các bạn thành công.

AutoCode.VN

minhnhatict@gmail.com Hibernate ORM