Sunday, June 19, 2022

 

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

No comments:

Post a Comment

So sánh các GitFlow model và áp dụng với CICD

https://medium.com/oho-software/so-s%C3%A1nh-c%C3%A1c-gitflow-model-v%C3%A0-%C3%A1p-d%E1%BB%A5ng-v%E1%BB%9Bi-cicd-b6581cfc893a