12/08/2018, 14:19

Full Text Search với Hibernate và SpringMVC Phần 1: Hello Hibernate Search

Về khái niệm Full text search (FTS) các bạn có thể xem tại bài viết này của chị Huyền Châm, mình thấy khá đầy đủ và dễ hiểu. Tại bài viết này mình sẽ chia sẻ cách để thực hiện FTS với Hibernate trong SpringMVC. Tại sao lại với Hibernate mà không phải với MySQL hay Postgresql? Vì khi setup FTS ở ...

Về khái niệm Full text search (FTS) các bạn có thể xem tại bài viết này của chị Huyền Châm, mình thấy khá đầy đủ và dễ hiểu.

Tại bài viết này mình sẽ chia sẻ cách để thực hiện FTS với Hibernate trong SpringMVC.

Tại sao lại với Hibernate mà không phải với MySQL hay Postgresql? Vì khi setup FTS ở tầng dưới (database) như vậy, khi dự án thay đổi database engine, sang SQL server chẳng hạn, thì ta lại phải tìm hiểu và làm FTS từ đầu. Còn khi làm FTS tầng trên (Hibernate) thì ta không cần quan tâm đến database là loại gì.

Project demo có ở cuối bài viết.

Tạo trang tìm kiếm sách, nhập keyword, kết quả trả về các cuốn sách có liên quan theo tiêu đề, mô tả, tác giả.

  • Java
  • Spring MVC framework
  • Hibernate
  • PostgreSQL (các loại db khác cũng tương tự thôi)

Tạo bảng

CREATE TABLE book
(
  book_id serial NOT NULL,
  title text,
  description text,
  author text,
  CONSTRAINT book_pkey PRIMARY KEY (book_id)
)

Tạo và configure project

pom.xml Bạn cần thêm 1 số thư viện như sau:

 
        <dependency>
            <groupId>org.hibernate</groupId>
            <artifactId>hibernate-core</artifactId>
            <version>4.2.15.Final</version>
        </dependency>
        <dependency>
            <groupId>org.hibernate</groupId>
            <artifactId>hibernate-search-engine</artifactId>
            <version>4.3.0.Final</version>
        </dependency>
        <dependency>
            <groupId>org.hibernate</groupId>
            <artifactId>hibernate-search-orm</artifactId>
            <version>4.3.0.Final</version>
        </dependency>
        <dependency>
            <groupId>org.hibernate</groupId>
            <artifactId>hibernate-search-orm</artifactId>
            <version>4.3.0.Final</version>
        </dependency>
        <dependency>
            <groupId>org.hibernate.javax.persistence</groupId>
            <artifactId>hibernate-jpa-2.0-api</artifactId>
            <version>1.0.1.Final</version>
        </dependency>
 
        <dependency>
            <groupId>postgresql</groupId>
            <artifactId>postgresql</artifactId>
            <version>9.1-901.jdbc4</version>
        </dependency>

web.xml

<web-app id="WebApp_ID" version="2.4"
         xmlns="http://java.sun.com/xml/ns/j2ee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee
   http://java.sun.com/xml/ns/j2ee/web-app_2_4.xsd">

    <display-name>Spring Web MVC Application</display-name>
    <servlet>
        <servlet-name>dispatcher</servlet-name>
        <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
        <load-on-startup>1</load-on-startup>
    </servlet>

    <servlet-mapping>
        <servlet-name>dispatcher</servlet-name>
        <url-pattern>/</url-pattern>
    </servlet-mapping>

    <context-param>
        <param-name>contextConfigLocation</param-name>
        <param-value>/WEB-INF/dispatcher-servlet.xml</param-value>
    </context-param>

    <listener>
        <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
    </listener>
</web-app>

dispatcher-servlet.xml Bạn cần thay giá trị của /home/framgia/Documents/My Data/DemoFTS/indexes thành đường dẫn đến folder nào bạn muốn chứa index của ứng dụng.

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:mvc="http://www.springframework.org/schema/mvc"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:tx="http://www.springframework.org/schema/tx"
       xmlns:context="http://www.springframework.org/schema/context"
       xsi:schemaLocation="
        http://www.springframework.org/schema/beans
        http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
        http://www.springframework.org/schema/mvc
        http://www.springframework.org/schema/mvc/spring-mvc-3.0.xsd
        http://www.springframework.org/schema/tx
        http://www.springframework.org/schema/tx/spring-tx-3.0.xsd
        http://www.springframework.org/schema/context
        http://www.springframework.org/schema/context/spring-context-3.0.xsd">

    <context:component-scan base-package="vn.va"/>

    <mvc:annotation-driven />

    <bean id="myDataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close">
        <property name="driverClassName" value="org.postgresql.Driver"/>
        <property name="url"
                  value="jdbc:postgresql://localhost:5432/demofts?currentSchema=demofts?useUnicode=true&characterEncoding=UTF-8&characterSetResults=UTF-8"/>
        <property name="username" value="postgres"/>
        <property name="password" value="postgres"/>
        <property name="maxActive" value="8"/>
        <property name="maxIdle" value="4"/>
        <property name="maxWait" value="900000"/>
        <property name="validationQuery" value="SELECT 1"/>
        <property name="testOnBorrow" value="true"/>
    </bean>
    <bean id="mySessionFactory" class="org.springframework.orm.hibernate4.LocalSessionFactoryBean">
        <property name="dataSource" ref="myDataSource"/>
        <property name="packagesToScan">
            <array>
                <value>vn.va.entities</value>
            </array>
        </property>
        <property name="hibernateProperties">
            <props>
                <prop key="hibernate.dialect">org.hibernate.dialect.PostgreSQL82Dialect</prop>
                <prop key="hibernate.cache.provider_class">org.hibernate.cache.internal.NoCachingRegionFactory</prop>
                <prop key="hibernate.search.default.directory_provider">
                    org.hibernate.search.store.impl.FSDirectoryProvider
                </prop>
                <prop key="hibernate.search.default.indexBase">
                 
                    /home/framgia/Documents/My Data/DemoFTS/indexes
                </prop>
            </props>
        </property>
    </bean>

    <bean id="transactionManager"
          class="org.springframework.orm.hibernate4.HibernateTransactionManager">
        <property name="sessionFactory" ref="mySessionFactory"/>
    </bean>

    <bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
        <property name="prefix">
            <value>/</value>
        </property>
        <property name="suffix">
            <value>.jsp</value>
        </property>
    </bean>

    <tx:annotation-driven transaction-manager="transactionManager"/>
</beans>

hibernate.cfg.xml Nếu bạn không dùng Postgesql thì hãy sửa file này.

<?xml version="1.0" encoding="UTF-8"?>
<hibernate-configuration>
    <session-factory>
         
        <property name="hibernate.connection.driver_class">org.postgresql.Driver</property>
        <property name="hibernate.connection.url">jdbc:postgresql://localhost:5432/demofts?currentSchema=demofts?useUnicode=true&characterEncoding=UTF-8&characterSetResults=UTF-8</property>
        <property name="hibernate.connection.username">postgres</property>
        <property name="hibernate.connection.password">postgres</property>

         
        <property name="hibernate.dialect">org.hibernate.dialect.PostgreSQL82Dialect</property>

         
        <property name="show_sql">true</property>

         
        <property name="hibernate.hbm2ddl.auto">create-drop</property>

         
        <mapping class="vn.va.entities.Book"/>

    </session-factory>
</hibernate-configuration>

Book.java

@Entity
@Indexed
@Table(name = "demofts.book")
public class Book {
	@Id
	@Column(name = "book_id")
	private Integer bookId;

	@Column(name = "title", nullable= false, length = 128)
	@Field(index= Index.YES, analyze= Analyze.YES, store= Store.NO)
	private String title;

	@Column(name = "description", nullable= false, length = 256)
	@Field(index=Index.YES, analyze=Analyze.YES, store=Store.NO)
	private String description;

	@Column(name = "author", nullable= false, length = 64)
	@Field(index=Index.YES, analyze=Analyze.YES, store=Store.NO)
	private String author;

    // Getter and Setter
    //....
}

CRUD class (BookDAOImpl.java)

Để Hibernate có thể tìm kiếm được thì trước tiên data cần phải được index. Khi bạn insert data bằng entity Book, hibernate sẽ tự động đánh index cho object đó. Vậy nếu với lượng data đã có từ trước thì sao? Bạn chỉ cần chạy hàm sau, tất cả table có Entity tương ứng với annotation @Indexed sẽ được đánh index:

public void indexBooks() throws Exception {
	try {
		Session session = sessionFactory.getCurrentSession();
		FullTextSession fullTextSession = Search.getFullTextSession(session);
		fullTextSession.createIndexer().startAndWait();
	} catch(Exception e) {
		throw e;
	}
}

Add book mới sẽ tự động được index:

public void addBook(String bookTitle, String bookDescription, String bookAuthor) {
	Book book = new Book();
	book.setAuthor(bookAuthor);
	book.setDescription(bookDescription);
	book.setTitle(bookTitle);
	sessionFactory.getCurrentSession().save(book);
}

Để tìm kiếm với input là 1 keyword, sử dụng hàm sau:

public List<Book> search(String keyword) {
	Session session = sessionFactory.getCurrentSession();

	FullTextSession fullTextSession = Search.getFullTextSession(session);

	QueryBuilder qb = fullTextSession.getSearchFactory()
				.buildQueryBuilder().forEntity(Book.class).get();
	org.apache.lucene.search.Query query = qb
				.keyword().onFields("title", "description", "author") // Chỉ định tìm theo cột nào
				.matching(keyword)
				.createQuery();

	org.hibernate.Query hibQuery =
				fullTextSession.createFullTextQuery(query, Book.class);

	List<Book> results = hibQuery.list();
	return results;
}

Kết quả trả về là tất cả các Book có title, description hoặc author liên quan đến từ khóa chúng ta nhập vào. Và thứ tự kết quả sẽ được sort theo "độ liên quan" đến từ khóa. Độ liên quan được tính bằng công thức Lucene Scoring và bạn hoàn toàn có thể custom công thức này để thay đổi thứ tự kết quả search theo ý muốn, chi tiết các bạn tham khảo tại đây.

Controller

Để tập trung vào việc tìm kiếm dữ liệu, mình viết Controller trả về json chứ không viết giao diện html.

@Controller
public class HomeController {
	@Autowired
	BookDAO bookDAO;

	@ResponseBody
	@RequestMapping(value = "/indexData", method = RequestMethod.GET)
	public String indexData() {
		try {
			bookDAO.indexBooks();
		} catch (Exception e) {
			e.printStackTrace();
		}
		return "Indexed at " + new Date().toGMTString();
	}

	@ResponseBody
	@RequestMapping(value = "/search", method = RequestMethod.GET)
	public List<Book> search(@RequestParam(value = "keyword") String keyword) {
		return bookDAO.search(keyword);
	}
}

Như các bạn thấy, có 2 controller (tương ứng 2 api). Trước khi GET search?keyword=xxx, mình sẽ GET /indexData trước. API này chỉ cần gọi 1 lần khi chạy ứng dụng để index data đã có từ trước trong bảng book. Bạn có thể tự động index bằng nhiều cách khác nhau (mình sẽ không đề cập ở đây để tránh mất tập trung vào việc search             </div>
            
            <div class=

0