Full Text Search với Hibernate và SpringMVC Phần 2: Search tiếng Nhật

Các bạn chưa xem P1 có thể xem lại tại đây.

Giới thiệu

Ở phần 2 này, mình sẽ chia sẻ về Full text search với tiếng Nhật, khá thiết thực khi làm việc trong dự án outsource Nhật. Những kiến thức mình chia sẻ dưới đây dừng lại ở mức cơ bản vì mình cũng mới tìm hiểu thôi, nhưng mình nghĩ nó cũng sẽ giúp ích bước đầu cho những bạn newbie như mình. OK, vào vấn đề chính.

Luồng chương trình

Có thể hiểu nôm na như sau:

  • Dữ liệu đầu vào cần được Lucene phân tích (analysis) và lưu vào Lucene Index. Khi đó, tồn tại song song 2 vùng dữ liệu: Database và Lucene Index
  • Khi tạo mới, update, hay delete trên Database, Lucene cần analysis lại để đảm bảo tính đồng bộ dữ liệu (cái này được Hibernate làm tự động).
  • Khi search: Lucene parse từ khóa và tìm kiếm Lucene index- > Trả về kết quả là list các id đã được sắp xếp theo thứ tự liên quan đến từ khóa -> Hibernate lấy ra những trường cần thiết theo id đã nhận được và trả về.

Vậy, để search được tiếng Nhật (hay các ngôn ngữ khác) thì chỉ cần tác động vào bước analysis data sao cho hợp lý là được.

Một số khái niệm hay gặp

Khi tìm hiểu về Lucene hay Hibernate Search các bạn sẽ bắt gặp những khái niệm sau, mình tổng hợp lại để tiện cho các bạn nghiên cứu sau này. Còn nếu bạn chỉ muốn chạy sample thì bỏ qua phần này cũng được.

  • Tokenization: Quá trình index data.
  • Tokens: Kết quả của quá trình tokenization sẽ chia nhỏ data ban đầu ra thành những thành phần nhỏ hơn, được gọi là token.
  • Analysis: Đôi khi việc tokenization khá đơn giản nên không đủ cho việc tìm kiếm, khi đó cần đến biện pháp phân tích sâu hơn -> gọi là Analysis. Có 2 cơ chế analysis: analysis trước khi tokenization và sau khi tokenization (Pre-tokenization and Post-Tokenization).
  • Pre-tokenization có thể bao gồm việc cắt bỏ HTML, biến đổi hoặc xóa bỏ text theo pattern.
  • Post-tokenization: Có nhiều loại, mỗi loại có 1 tác dụng khác nhau. Trong Lucene thường kế thừa từ lớp TokenFilter. Các loại TokenFilter tiêu biểu là: ⋅ Stemming: Thay thế từ ban đầu bởi từ gốc của nó. Ví dụ: bikes -> bike ⋅ Stop Words Filtering: Những từ như a, and, an, the được gọi là stop word. CHúng được cho là không có ý nghĩa cho việc search, vậy nên bước này xóa bỏ stop words đi để giảm index size và tăng performance tìm kiếm: Tăng tốc độ và giảm nhiễu. ⋅ Synonym Expansion: Thêm những token với từ đồng nghĩa với từ gốc để tăng hiệu quả tìm kiếm Tùy vào yêu cầu cụ thể mà chúng ta lựa chọn kiểu tokenization nào.
  • (bonus) Solr: Search server viết bằng Lucene

Coding

Như đã nói ở trên, tất cả những gì chúng ta cần làm là tìm cách để analysis tiếng Nhật. Để làm được điều đó, ta sử dụng thư viện kuromoji. Thư viện này được phát triển bởi công ty atilika Nhật Bản (repo gốc) nhưng sau đó đã được tích hợp vào Lucene từ version 3.6. File pom.xml cần thêm sẽ có dạng như này, đầy đủ có trong sample project cuối bài viết.

        <!-- Hibernate -->
        <dependency>
            <groupId>org.hibernate</groupId>
            <artifactId>hibernate-core</artifactId>
            <version>5.0.11.Final</version>
        </dependency>
        <dependency>
            <groupId>org.hibernate</groupId>
            <artifactId>hibernate-search-engine</artifactId>
            <version>5.5.5.Final</version>
        </dependency>
        <dependency>
            <groupId>org.hibernate</groupId>
            <artifactId>hibernate-search-orm</artifactId>
            <version>5.5.5.Final</version>
        </dependency>
        <!-- Apache lucene -->
        <dependency>
            <groupId>org.apache.lucene</groupId>
            <artifactId>lucene-core</artifactId>
            <version>5.3.1</version>
        </dependency>
        <dependency>
            <groupId>org.apache.lucene</groupId>
            <artifactId>lucene-analyzers-common</artifactId>
            <version>5.3.1</version>
        </dependency>
        <dependency>
            <groupId>org.apache.lucene</groupId>
            <artifactId>lucene-analyzers-kuromoji</artifactId>
            <version>5.3.1</version>
        </dependency>

Bên trong kuromoji có rất nhiều các TokenFilter, ý nghĩa của mỗi loại các bạn có thể tra cứu tại đây (những class có đuôi Filter). Sử dụng filter nào thì tùy vào bạn.

Tại bài viết này mình sẽ sử dụng các loại Filter với ý nghĩa như sau:

  1. JapaneseBaseFormFilter: Thay thế các tính từ, động từ bằng từ gốc, ví dụ: từ 買います (mua) sẽ được thay thế bởi từ 買う
  2. JapanesePartOfSpeechStopFilter: Loại bỏ những stop-word bằng phương pháp gán nhãn từ loại (part-of-speech tagging). Danh sách stop-word tiếng Nhật các bạn xem tại đây.
  3. CJKWidthFilter: Chuẩn hóa ký tự half-width và full-width
  4. StopFilter: Loại bỏ stop-word
  5. JapaneseKatakanaStemFilter: Chuẩn hóa cách đọc của những từ katakana thông dụng. Trong tiếng Nhật, katakana là chữ để phiên âm những từ tiếng nước ngoài cho người Nhật dễ đọc, vì vậy 1 từ tiếng Anh có thể phiên âm nhiều cách. Ví dụ manager có thể phiên âm thành マネージャー, マネージャ hoặc マネジャー.
  6. LowerCaseFilter: Nếu trong văn bản có chứa ký tự latinh thì nó sẽ được chuẩn hóa về ký tự viết thường.
  7. (Optional) JapaneseReadingFormFilter: Nếu các bạn muốn tìm kiếm theo cách đọc của chữ kanji thì sử dụng thêm Filter này. Các chữ kanji sẽ được tokenize theo cách đọc dựa vào ngữ cảnh tương ứng.

Sử dụng các Filter này, các bạn khai báo bộ analyzer và filter (gọi là custom analyzer chain) trong Entity class bằng @AnalyzerDef. Sử dụng tại trường nào mà các bạn muốn bằng @Analyzer(definition = "tên-analyzer-chain"). Trường nào không có @Analyzer thì sẽ dùng analyzer default.

Book.java sau khi khai báo như sau:

@Entity
@Indexed
@Table(name = "demofts.book")
@AnalyzerDef(name = "customanalyzer",
		tokenizer = @TokenizerDef(factory = JapaneseTokenizerFactory.class),
		filters = {
				@TokenFilterDef(factory = JapaneseBaseFormFilterFactory.class),
				@TokenFilterDef(factory = JapanesePartOfSpeechStopFilterFactory.class),
				@TokenFilterDef(factory = CJKWidthFilterFactory.class),
				@TokenFilterDef(factory = StopFilterFactory.class),
				@TokenFilterDef(factory = JapaneseKatakanaStemFilterFactory.class),
				@TokenFilterDef(factory = JapaneseReadingFormFilterFactory.class),
				@TokenFilterDef(factory = LowerCaseFilterFactory.class)
		})
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)
	@Analyzer(definition = "customanalyzer")
	private String title;

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

	@Column(name = "author", nullable= false, length = 64)
	@Analyzer(definition = "customanalyzer")
	@Field(index=Index.YES, analyze=Analyze.YES, store=Store.NO)
	private String author;
// Getter and setter
}

Thử nghiệm

Sorry các bạn vì phần này mình đã viết rồi nhưng ko hiểu sao khi public lại bị mất. Kết quả dưới đây do mình nhớ và viết lại thôi. Tìm kiếm với từ khóa 買う

[
  {
    "title": "買います",
    "author": "lorem ipsum",
    "bookId": lorem ipsum,
    "description": "lorem ipsum"
  }
]

Tìm kiếm với từ khóa てんき

[
  {
    "title": "天気",
    "author": "lorem ipsum",
    "bookId": lorem ipsum,
    "description": "lorem ipsum"
  }
]

Download

Các bạn download source code sample tại đây: https://drive.google.com/file/d/0B9SCEcmTeQHBLTBBVUU0ejV6VE0/view?usp=sharing

Tham khảo

https://speakerdeck.com/atilika/japanese-linguistics-in-lucene-and-solr (Nên đọc) https://speakerdeck.com/atilika/language-support-and-linguistics-in-lucene-solr-and-elasticsearch-and-the-eco-system http://hibernate.org/search/documentation/getting-started/ http://www.lucenetutorial.com/index.html