+6

Phần 5: ElasticSearch: Modeling data and handling Relationships

Trong tự nhiên hầu hết các entity đều có ràng buộc và mối quan hệ với các entity khác: Blog post sẽ có các comments, tài khoản ngân hàng sẽ có các transactions... Các hệ quản trị cơ sở dữ liệu dựa trên mô hình quan hệ như: mysql, sqlserver hay oracle được thiết kế để giải quyết vấn đề ràng buộc mỗi quan hệ giữa các entity này do đó việc biểu diễn các relationship là khá đơn giản. Tuy nhiên nhược điểm của những DB này là việc hỗ trợ nghèo nàn cho tính năng full-text search, toán tử join tốn nhiều chi phí khi dữ liệu trong DB và độ phức tạp của các relationship tăng lên, hoặc khi phải join giữa các table nằm trên các server vật lý khác nhau.

Elasticsearch giống như hầu hết các NoSQL database, đều coi thế giới là "flat". Thường mỗi document là "flat", độc lập, bản thân document chứa tất cả các thông tin mà nó cần tham chiếu đến. Ví dụ một chiếc car sẽ có thông tin của hãng xe, khung xe, bánh xe:

car: {
   wheels:{}
   branch: {}
...
}

Việc thiết kế các flat document có vài ưu điểm nổi bật: • Indexing nhanh và lock-free. • Searching nhanh và lock-free. • Data có thể được lưu trữ trên các nodes khác nhau bởi chúng độc lập

Có một vài kỹ thuật để biểu diễn các relationship cho các Flat Document trong ElasticSearch: • Application-side joins • Data denormalization • Nested objects • Parent/child relationships

Thông thường để có một thiết kế hợp lý vừa đảm bảo hiệu năng search và biểu diễn được các mối quan hệ thì chúng ta phải kết hợp các kỹ thuật trên.

Application-side joins

Bản chất kỹ thuật này là thiết kế các document như thiết kế các table trong các relational database. Ví dụ: biểu diễn mối quan hệ giữa blogpost và user:

User:

PUT /my_index/user/1
{
"name": "John Smith",
"email": "john@smith.com",
"dob": "1970/10/24"
}

BlogPost:

PUT /my_index/blogpost/2
{
"title": "Relationships",
"body": "It's complicated...",
"user": 1
}
  • Tìm blog post với điều kiện user_id = 1:
GET /my_index/blogpost/_search
{
    "query": {
        "filtered": {
            "filter": {
                "term": { "user": 1 }
            }
        }
    }
}
  • Tìm blog post có tên là John, phải chạy 2 câu query, câu đầu tiên lấy user_id và câu thứ 2 tìm blog_post dựa vào user_id tìm được từ câu 1
GET /my_index/user/_search
{
    "query": {
        "match": {
            "name": "John"
        }
    }
}

GET /my_index/blogpost/_search
{
    "query": {
        "filtered": {
            "filter": {
            "terms": { "user": [1] }
            }
        }
    }
}
  • Ưu điểm: data được chuẩn hóa, không bị dư thừa.
  • Nhược điểm: phải chạy các extra queries để lấy dữ liệu cần thiết

Data denormalization

Để tăng hiệu năng tìm kiếm, các document sẽ được denomalize, do đó một số data sẽ bi duplicate tuy nhiên việc tìm kiếm sẽ rất hiệu quả:

PUT /my_index/user/1
{
"name": "John Smith",
"email":"john@smith.com",
"dob": "1970/10/24"
}

PUT /my_index/blogpost/2
{
"title": "Relationships",
"body": "It's complicated...",
    "user":
        { "id":1,
        "name":"John Smith"
        }
}

Bây giờ để tìm blog post có tên là John, chúng ta chỉ cần 1 câu query duy nhất:

GET /my_index/blogpost/_search
{
    "query": {
        "bool": {
            "must": [
                { 
                    "match": { "title": "relationships" }
                },
                { "match": { "user.name": "John"}
                }
            ]
        }
    }
}
  • Ưu điểm: Tốc độ vì không cần join với các document khác
  • Nhược điểm: duplicated data, tuy nhiên điều này dễ được xử lý bởi chi phí phần cứng ngày càng rẻ

Nested objects

Nest toàn bộ object con vào object cha.

  • Tạo nested object mapping
PUT /my_index
{
    "mappings": {
        "blogpost": {
            "properties": {
                "comments": {
                    "type": "nested",
                    "properties": {
                    "name": { "type":"string"},
                    "comment": { "type": "string"},
                    "age": { "type":"short"},
                    "stars":{ "type":"short"},
                    "date":{ "type": "date"}
                }
            }
        }
    }
}
}
  • Đánh index
PUT /my_index/blogpost/1
{
    "title": "Nest eggs",
    "body": "Making your money work...",
    "tags": [ "cash", "shares" ],
    "comments": [
        {
        "name": "John Smith",
        "comment": "Great article",
        "age":28,
        "stars":4,
        "date":"2014-09-01"
        },
        {
        "name":"Alice White",
        "comment": "More like this please",
        "age": 31,
        "stars": 5,
        "date": "2014-10-22"
        }
    ]
}
  • Giờ chúng ta có thể search:
GET /_search
{
    "query": {
        "bool": {
            "must": [
                { "match": { "name": "Alice" }},
                { "match": { "age": 28}}
            ]
        }
    }
}
  • Ưu điểm: Tốc độ vì không cần join với các document khác
  • Nhược điểm: duplicated data, khi nested object thay đổi thì phải đánh lại index cho parent cha

Parent/child relationships

Thiết lập mối quan hệ parent-child sử dụng thuộc tính _parent Ví dụ: biểu diễn mối quan hệ giữa company và branch

  • Tạo mapping:
PUT /company
{
    "mappings": {
        "branch": {},
        "employee": {
            "_parent": {
            "type": "branch"
            }
        }
    }
}
  • Insert buld dữ liệu:
POST /company/branch/_bulk
{ "index": { "_id": "london" }}
{ "name": "London Westminster", "city": "London", "country": "UK" }
{ "index": { "_id": "liverpool" }}
{ "name": "Liverpool Central", "city": "Liverpool", "country": "UK" }
{ "index": { "_id": "paris" }}
{ "name": "Champs Élysées", "city": "Paris", "country": "France" }

Khi index cho child documents, cần chỉ dõ ID của parent document:

PUT /company/employee/1?parent=london
{
    "name": "Alice Smith",
    "dob": "1970-10-24",
    "hobby": "hiking"
}
POST /company/employee/_bulk
{ "index": { "_id": 2, "parent": "london" }}
{ "name": "Mark Thomas", "dob": "1982-05-16", "hobby": "diving" }
{ "index": { "_id": 3, "parent": "liverpool" }}
{ "name": "Barry Smith", "dob": "1979-04-01", "hobby": "hiking" }
{ "index": { "_id": 4, "parent": "paris" }}
{ "name": "Adrien Grand", "dob": "1987-05-11", "hobby": "horses" }
  • Tìm parents bởi childrent của chúng:
GET /company/branch/_search
{
    "query": {
        "has_child": {
            "type": "employee",
            "query": {
                "range": {
                    "dob": {
                    "gte": "1980-01-01"
                    }
                }
            }
        }
    }
}
  • Tìm childrent bởi parent:
GET /company/employee/_search
{
    "query": {
        "has_parent": {
            "type": "branch",
            "query": {
                "match": {
                "country": "UK"
                }
            }
        }
    }
}
  • Ưu điểm so với nested model • The parent document được updated mà không cần đánh lại index khi document childrent thay đổi • Child documents có thể được thêm, sửa, cóa và không ảnh hưởng đến parent và các childrent khác. Điều này đặc biệt quan trọng khi số lượng document chidrent lớn và cần phải update và thay đổi thường xuyên • Child documents có thể được trả về trong kết quả của 1 câu truy vấn query

  • Nhược điểm: • Parent và tất cả các child documents phải được lưu trữ trên cùng một shard


All rights reserved

Bình luận

Đăng nhập để bình luận
Avatar

em chào a ạ. em mới có tìm hiểu về elasticsearch. hiện tại e khá quan tâm đến đến các thuật toán tính similarity để tìm score cho mỗi document. a có thể chỉ giúp em cách thay đổi hàm tính similarity trong function elasticsearch.search() được ko ạ . e muốn so sánh hiệu năng giữa các hàm tính similarity trong elasticsearch ạ. em cảm ơn ạ. a = Elasticsearch.search(index = 'megas', doc_type = 'employs', body = { "query": { "match": { "about": "i like" } } }) es_sim.png

Avatar
@vuquyen
thg 6 13, 2019 2:24 SA

Bài viết hay! Mình có vài góp ý nhỏ là nên làm rõ nhược điểm của những phương pháp này.

  1. Nhược điểm của Application-side joins thì ai cũng thấy, việc phải query 2 lần sẽ làm mất đi hiệu năng 1 tool search như Elasticsearch. (lưu ý các bạn nếu theo phương pháp này thì cần set cho field của parent_id không analyze).
  2. Nhược điểm của Data denormalization và Nested object: khi data của object thay đổi thì giá trị object kia không thay đổi theo, phải handle việc cập nhập lại (cả 2 chiều con-cha và cha-con), việc này không hợp lý về mặt logic.
  3. Nhược điểm của Parent/child relationships: Nhìn sơ qua thì mọi người sẽ thấy cách này hợp lý nhất, nhưng: Elasticsearch chia shard dựa trên mối quan hệ, nên lợi ích của việc chia shard mất, tiếp theo thì khả năng maintain khó hơn( ví dụ 2 object này có mối quan hệ 1-n, nếu thay đổi thì như thế nào), câu query khó hơn và performance lại thấp hơn vì thực ra 2 object này nằm trên 2 document riêng biệt. Tùy vào hoàn cảnh cụ thể để chọn 😁
Avatar
@unstoppable
thg 2 25, 2020 10:04 SA

mới tìm hiểu. bác có thể ra thêm phần 6 7 8 ... nữa ko? hi.

Avatar
+6
Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí