[Elasticsearch] Phân tích và tìm kiếm dữ liệu tiếng Việt

Giới thiệu

  • Các bài viết trước của mình có liên quan Elasticsearch: integrate with laravel dockerdocker compose
  • Ở bài viết trước mình có giới thiệu qua về Elasticsearch việc tìm kiếm cơ bản với Elasticsearch tuy nhiên dữ liệu mới chỉ dừng lại ở tiếng Anh vậy còn tiếng Việt thì sao? Mình đã thử google tìm kiếm để xem các bài hướng dẫn hay tìm hiểu của người đi trước về việc tìm kiếm trên dữ liệu tiếng Việt nhưng gần như không có bài hướng dẫn hay demo cụ thể. Nếu áp dụng những xử lý với tiếng Việt giống như với tiếng Anh mình đã thử và kết quả tìm kiếm mà mình nhận về không được tốt Bài viết hôm nay mình xin giới thiệu về phần xử tiền xử lý với dữ liệu tiếng Việt và sẽ demo ứng dụng hoàn chỉnh với tìm kiếm ở bài sau

Một số khái niệm cần biết

Analysis và Analyzers

Analysis hiểu là một process thực hiện các công việc như sau

  • Đầu tiên sẽ xử lý tách từ từ một đoạn text đầu vào thành các terms (từ hoặc cụm từ) phù hợp để sử dụng trong việc đánh chỉ mục ngược
  • Sau đó thì phân tích, chuẩn hóa các terms này để thu được data mong muốn phục vụ việc tìm kiếm. Nó giống như việc mình muốn ăn bưởi ngoài loại bỏ vỏ, tách từng múi sau khi đã có từng múi rồi thì mình lại phải bóc từng múi và bỏ hạt vậy.

Những công việc này do các thành phần máy phân tích (analyzers) đảm nhiệm thực hiện. Mỗi một analyzer là sự kết hợp của 3 functions sau:

  • Character filters: Thanh niên này có nhiệm vụ xử lý chuỗi đầu vào trước khi được tách từ, có thể coi như làm "sạch" chuỗi ví dụ như việc loại bỏ các thẻ html_tag hay chuyền ký hiệu & thành thành chữ "and". Công việc nhẹ nhàng đơn giản như việc rửa sạch rồi bóc vỏ bưởi vậy
  • Tokenizer Chuỗi sau khi được làm "sạch" bởi Chracter filters thì sẽ được tách từ bởi một bộ tách từ tokenizer do mình lựa chọn hoặc định nghĩa, đơn giản nhất là tách từ theo khoảng trắng hay dấu chấm câu, các từ được tách ra này gọi là term, hay nói vui như vừa nãy sẽ là các múi bưởi
  • Token filters Cuối cùng, mỗi term được qua Token filters (bộ lọc thẻ) để "làm mượt" thêm, ví dụ như việc chuyển các ký tự hoa về ký tự thường (lowercase) hay loại bỏ các từ dừng (từ xuất hiện nhiều nhưng gần như không ảnh hưởng tới kết quả tìm kiếm). Đây chính là khâu bóc vỏ bỏ hạt trên từng múi =))

Giới thiệu về elasticsearch-analysis-vietnamese plugin

Một open source plugin được publish bởi bởi lập trình viên Việt Nam - tác giả Duy Do trên github. Mục đích nhằm tích hợp việc xử lý ngôn ngữ tiếng Việt vào Elasticsearch.

Được viết bằng ngôn ngữ java và sử dụng thư viện tách từ tiếng Việt của thầy Lê Hồng Phương đó là vn_tokenizer với độ chính xác lên đến trên 95%

Cung cấp một analyzer gồm vi_analyzervi_tokenizer. Trong đó thì vi_analyzer đã bao gồm cả vi_tokenizer, token filters như lowercase và stop word

Cài đặt

Chuẩn bị

So với phần cài đặt chỉ gồm service elasticsearch ở bài trước lần này mình có tích hợp thêm hai plugin nữa:

  • Một là elasticsearch-analysis-vietnamese của anh Duy Đỗ với mục đích sử dụng cho việc tách từ tiếng Việt, nghĩa là sẽ tách theo ngữ nghĩa chứ không chỉ dừng lại ở tách từ đơn.
  • Thứ hai đó là analysis-icu plugin cho việc thực hiện token filter, mục đích chính của mình là loại bỏ đi dấu của từ ví dụ Bách khoa thành "Bach khoa"

Mình vẫn sử dụng docker cho việc cài đặt ELasticsearch (Bài viết trước của mình về dockerdocker compose)

  • Để có thể cài đặt được plugin analysis cần download bản release phù hợp với phiên bản elasticsearch mà mình sử dụng hoặc có thể làm theo hướng dẫn để có thể build một bản tùy theo custom của mình. Ví dụ: elasticsearch-analysis-vietnamese-5.3.1.zip

    Link releaseHướng dẫn tự build

Thực hiện

Đối với sử dụng docker (mình demo với bản elastic 5.3.1)

  • Chuẩn bị

    $ mkdir my-elastic
    
  • Pull image Elasticsearch 5.3.1 từ dockerhub

    $ cd my-elastic
    $ docker pull elasticsearch:5.3.1
    
  • Dockerfile

    #Dockerfile
    FROM elasticsearch:5.3.1
    
    MAINTAINER [email protected]
    
    COPY elasticsearch-analysis-vietnamese-5.3.1.zip /usr/share/elasticsearch/
    
    RUN cd /usr/share/elasticsearch && \
      bin/elasticsearch-plugin install file:///usr/share/elasticsearch/elasticsearch-analysis-vietnamese-5.3.1.zip && \
      bin/elasticsearch-plugin install analysis-icu
    

    Trong lệnh RUN mình đã cài đặt 2 plugin đã được nêu ở trên phần chuẩn bị

  • Build image:

    docker build -t telosma/elasticsearch:5.3.1 .
    
  • Run image vừa được build

    $ docker run -p 9300:9200 telosma/elasticsearch:5.3.1
    

    Cùng nhìn kết quả sau khi đoạn lệnh trên được thực hiện

    Kết quả sau khi run image

    Theo ảnh kết quả trên bạn có thể thấy là 2 plugin đã được load đó là analysis-icuelasticsearch-analysis-vietnamese

Không sử dụng docker

  • Nếu bạn không sử dụng docker mình giả sử bạn đã cài đặt service elastic phiên bản 5.3.1 hay bất kỳ phiên bản nào bạn muốn

  • Tải phiên bản release của elasticsearch-analysis-vietnamese về và tiến hành cài đặt như sau

    $ bin/elasticsearch-plugin install path/to/elasticsearch-analysis-vietnamese-[version].zip
    
    Ví dụ: bin/elasticsearch-plugin install file:///usr/share/elasticsearch/elasticsearch-analysis-vietnamese-5.3.1.zip
    
    $ bin/elasticsearch-plugin install analysis-icu
    

Kết quả chạy thử

Thông thường port giao tiếp mặc định với elasticsearch là 9200, mình đã expose port để giao tiếp với elasticsearch bên trong docker là 9300 nên url của mình tương ứng là localhost:9300 Giờ mình sẽ sử dụng addon Sense trên google chrome để gửi request tới Elasticsearch

Server: localhost:9300

  • Setting index

    PUT icu_vi_sample
    {
      "settings": {
        "index": {
            "number_of_shards" : 1,
            "number_of_replicas" : 1,        
          "analysis": {
            "analyzer": {
              "my_analyzer": {
                "tokenizer": "vi_tokenizer",
                "char_filter":  [ "html_strip" ],
                "filter": [
                  "icu_folding"
                ]
              }
            }
          }
        }
      },
        "mappings": {
            "my_type": {
                "properties" : {
                    "user" : {
                        "type" : "text",
                        "analyzer": "my_analyzer"
                    },
                    "introduce" : {
                        "type" : "text",
                        "analyzer" : "my_analyzer"
                    }
                }            
            }
        }
    }
    

    Tạo index có tên là icu_vi_sample Như đã nêu ở trên về ba thành phần của một máy phân tích (analyzer) trên và mình đã áp dụng như sau:

    • char filters: html_strip loại bỏ các thẻ tag và ký tự đặc biệt
    • tokenizer: sử dụng vi_tokenizer của analysis_vietnamese_plugin vào việc tách từ tiếng Việt
    • token filter: sử dụng icu_folding sẽ giúp đưa từ về dạng không dấu

    Để có thể dễ dàng so sánh thì mình sẽ tạo thêm một index tương tự như icu_vi_sample bên trên đặt tên là standard_sample khác ở chỗ là mình sẽ chỉnh phần analyzer như sau

    "my_analyzer": {
      "type": "custom",
      "tokenizer": "standard",
      "char_filter":  [ "html_strip" ],
      "filter": [
        "icu_folding"
      ]
    }
    

    Khác biệt ở đây nằm ở chỗ mình sẽ không sử dụng tách từ tiếng Việt đối với index này mà chỉ sử dụng tách từ theo khoảng trắng.

  • Kiểm tra kết quả analyzer với hai index trên

    • standard_sample Truy vấn

      GET standard_sample/_analyze
      {
        "analyzer": "my_analyzer",
        "text": "<p>đại học Mỹ</p>"  
      }  
      

      Kết quả

          {
             "tokens": [
                {
                   "token": "dai",
                   "start_offset": 0,
                   "end_offset": 3,
                   "type": "<ALPHANUM>",
                   "position": 0
                },
                {
                   "token": "hoc",
                   "start_offset": 4,
                   "end_offset": 7,
                   "type": "<ALPHANUM>",
                   "position": 1
                },
                {
                   "token": "my",
                   "start_offset": 8,
                   "end_offset": 10,
                   "type": "<ALPHANUM>",
                   "position": 2
                }
             ]
          }
      
    • icu_vi_analyzer Truy vấn

      GET icu_vi_sample/_analyze
      {
        "analyzer": "my_analyzer",
        "text": "<p>đại học Mỹ</p>"  
      }  
      

      Kết quả

          {
             "tokens": [
                {
                   "token": "dai hoc",
                   "start_offset": 0,
                   "end_offset": 7,
                   "type": "word",
                   "position": 0
                },
                {
                   "token": "my",
                   "start_offset": 8,
                   "end_offset": 10,
                   "type": "name1",
                   "position": 1
                }
             ]
          }
      

      Rõ ràng là máy phân tích đã hoạt động tốt và nhận biết được "đại học" là từ ghép có nghĩa.

  • Thêm dữ liệu Sử dụng index API do elastic service cung cấp Data

    [
      {
        "user" : "DUYLX",
        "introduce" : "Duy là sinh viên đại học"
      },
      {
        "user" : "Tan Phan",
        "introduce" : "Tân đang học mỹ thuật"      
      },
      {
        "user" : "LuongNP",
        "introduce" : "Lương đang đi du lịch tại Mỹ Tho"      
      },
      {
        "user" : "Hoi",
        "introduce" : "Hợi đang du học ở Mỹ"      
      },
      {
        "user" : "Việt Nam",
        "introduce" : "VN đang đá bóng ở sân Mỹ Đình"
      }
    ]
    
  • Thực hiện truy vấn Truy vấn

    GET **index_name**/my_type/_search
    {
       "query": {
          "match": {
             "introduce": {
                 "query": "đại học Mỹ",
                 "analyzer": "my_analyzer"
             }
          }
       },
       "highlight": {
           "fields": {
                 "introduce" : {
                     "fragment_size" : 150,
                     "number_of_fragments" : 3
                 }
           }
       }
    }    
    

    Ở đây index_name sẽ là icu_vi_sample hoặc standard_sample để so sánh sự khác biệt. Và câu truy vấn cũng Kết quả

    • Sử dụng tách từ tiếng Việt (icu_vi_sample) icu_result
    • Sử dụng tách từ khoảng trắng (standard_sample) standard_result

    Ở đây thì "operator" để tìm kiếm từ khóa mà elasticsearch đã mặc định là "or" nghĩa là: tìm thằng có A hoặc B Hãy chú ý vào thành phần được highlight đặt trong cặp thẻ <em></em> để thấy sự khác biệt từ cùng một kết quả trả về. Có thể thấy rõ sự khác biệt ở đây là chúng ta sử dụng tách từ tiếng Việt và tìm kiếm dữ liệu tiếng Việt sẽ trả về cho mình kết quả là gần đúng nhất với mong muốn tìm kiếm nhất và tránh bị dư thừa kết quả trả về, ví dụ standard_sample thì trả về cả 5 bản ghi do nó đều từ "học" hoặc "Mỹ" mà câu "VN đang đá bóng ở sân Mỹ Đình" thì không hề liên quan tới nội dung muốn tìm kiếm.

Vấn đề gặp phải

  • Có một vấn đề đó là đầu vào tìm kiếm là tiếng Việt không dấu có thể trả về kết quả khá sai lệch so với mong muốn tìm kiếm ban đầu
  • Lý do: Chúng ta không thể tách từ đối với tiếng Việt không dấu. Ví dụ dữ liệu mẫu có từ đại học sau khi được tách từ và đánh chỉ mục sẽ thành dai_hoc tuy nhiên lúc tìm kiếm mà input vào là "dai hoc" thì hệ thống đang hiểu mình đi tìm daihoc
  • Từ đó thì tùy thuộc vào loại dữ liệu bạn đang có và mục đích tìm kiếm dữ liệu của bạn như thế nào để áp dụng phương pháp tìm kiếm hợp lý

Tổng kết

  • Qua bài viết mình cũng đã chia sẻ những kiến thức mà mình tìm hiểu được khi sử dụng Elasticsearch tuy nhiên vẫn còn nhiều thiếu xót mong được sự góp ý của mọi người ở phần comment bên dưới bài viết.
  • Trong bài sau mình sẽ trình bày về một demo cụ thể sử dụng Elasticsearch và kết hợp thêm một số tùy chọn trong truy vấn tìm kiếm với Elasticsearch để cải thiện kết quả tìm kiếm

Cảm ơn bạn đã dành thời gian đọc hết bài chia sẻ của mình!