Site icon Tạp chí Đáng Nhớ

Thuật toán Random newsfeed

Các thuật toán sắp xếp newsfeed

  • Một trong những tính năng thường gặp của những trang tin tức/mạng xã hội là newsfeed.
  • Thông thường các trang này được thiết kế để hiển thị tin tức mới nhất (sort theo timestamp) hoặc những tin được ưa chuộng, đánh giá cao (sort theo hệ thống ranking được đánh giá dựa vào score/view/createdDate như hackernews, reddit) hoặc đơn giản là theo ý thích của admin, mình thích thì mình đưa lên top thôi (kipalog).
  • Tuy nhiên, hầu hết các dạng này đều phải sort dựa trên một giá trị nhất định và quan trọng nhất cần có một lượng bài viết liên tục đủ để tránh làm cho newsfeed không bị nhàm chán, nhìn chỉ có vài bài đăng đi đăng lại.

  • Ngoài ra, không như các trang tin tức hằng ngày, chỉ cần qua một đêm là đã thành tin outdated, một số trang web chia sẽ tin tức, kiến thức lại không như vậy (ví dụ chia sẽ kiến thức thiên văn, văn học, lịch sử, kỹ năng,… Hoặc chia sẽ các địa điểm, thông tin du lịch, mẹo vặt,…), đồng thời lượng bài viết cũng không nhiều bằng. Và các thuật toán newsfeed ở bên trên chỉ có thể giúp người dùng tiếp cận được một lượng thông tin rất nhỏ.
  • Và đôi khi do lượng bài cập nhật không liên tục (vd khoảng 1-2 ngày mới có 1 bài), khi user vào cứ thấy có vài bài vừa được đăng gần đây và kết luận site cùi quá, chả có gì hay, mặc dù còn một lượng bài viết rất lớn đã được đăng trước nhưng user không được tiếp cận.
  • Để giải quyết tình trạng trên, bài viết dưới sẽ mô tả về việc random dữ liệu, đồng thời thiết kế newsfeed có vẻ có hơi hướm newsfeed base user (nội dung xây dựng dựa trên hành vi người dùng).
  • Việc random có thể sẽ mang lại ưu và nhược điểm như sau:
    • Ưu: tạo cảm giác dữ liệu phong phú, site active liên tục, mỗi lần user refresh/truy cập sẽ có được thông tin khác nhau, và mỗi lần load đều là trải nghiệm dữ liệu mới.
    • Nhược: đôi khi user thấy gì đó hay ho, quay lại xem thì lại không tìm thấy cái cũ, random mà. Nhưng đôi khi đây cũng có thể coi là feature tùy theo business của product.

Random newsfeed

Yêu cầu

Để dễ hình dung mình sẽ thiết kế một API newsfeeds trả về danh sách địa điểm ăn uống một cách ngẫu nhiên. Giả sử mình đang có trong tay tập dữ liệu sample của 80000 địa điểm với nội dung khác nhau và đang nằm trong DB SQL (Mysql/Postgresql). Newsfeed sẽ giống như timeline của Facebook, tức là cứ scroll xuống thì client sẽ request lên để lấy nội dung mới về. Và cần phải đảm bảo:

  • Mỗi lần reload newsfeed phải có nội dung random khác nhau giữa các lần reload.
  • Dữ liệu trả về ở mỗi lần load more (scroll xuống) phải đảm bảo không trùng với dữ liệu đã trả về trước đó.
  • Giả sử dữ liệu của mình có dạng đơn giản như sau:
eateries
+-----+------+---------+------------+
| id  | name | address | created_at |
+-----+------+---------+------------+
| int | text | text    | timestamp  |
+-----+------+---------+------------+
  • Và format API trả về sẽ có dạng như thế này:
{
    "data": [
        {<eatery_1>},
        {<eatery_2>},
        {<eatery_3>},
        ...
    ],
    "pagination": {
        "nextUrl":"/newsfeeds?param1=x&param2=y&param3=z"
    }
}

Với format trên, client mỗi lần cần lấy thêm dữ liệu (load more) chỉ cần lấy nextUrl ra và request thẳng lên server mà không cần thêm thao tác gì khác, mọi thứ đều được quyết định ở server.

Xây dựng

Simple approach: Lọc phần tử trùng mỗi lần load

  • Cách tiếp cận đơn giản nhất: mỗi lần load sẽ query random, đồng thời loại bỏ các địa điểm đã load trước đó, câu query sẽ có dạng như sau:
    SELECT * FROM eateries WHERE id NOT IN (?) ORDER BY RANDOM()
  • Dữ liệu trong param ? nhận vào là danh sách id của những địa điểm đã load trước đó.
  • Câu hỏi đặt ra là làm sao biết user hiện tại đã load những địa điểm nào rồi để bỏ qua? Cách đơn giản nhất là lưu thẳng danh sách địa điểm vào trong nextUrl và khi client request lên thì lọc ra. Ví dụ:
    • Lần load đầu tiên đã lấy ra 3 điểm có id là [1, 2, 3].
    • Trong response trả về cho user sẽ là
    "pagination": {
        "nextUrl":"/newsfeeds?ignoreEatery=1,2,3"
    }
  • Lần tới khi client request với nextUrl trên, ta sẽ có danh sách id cần loại ra, câu SQL của lần load kế tiếp sẽ là:
    SELECT * FROM eateries WHERE id NOT IN (1,2,3) ORDER BY RANDOM() LIMIT 10
  • Vấn đề:
    • URL có giới hạn về số lượng ký tự, đâu đó tầm 2000 characters tùy theo browsers, nên chắc chắn không thể lưu được 80000 địa điểm.
    • Giả sử bằng cách nào đó có thể truy xuất được danh sách id cần loại ra ở trên, ví dụ ta sẽ cache tạm ở đâu đó trong memcache/redis, lúc nào cần thì đọc từ cache ra, sau đó query đến SQL database, khi response data về thì bổ sung thêm id mới vào cache. Có vẻ ổn nhưng thực tế sẽ phát sinh các vấn đề như sau:
    • Server round trip: IDs phải luân chuyển từ cache -> application -> SQL Database -> Update to cache
    • Query sử dụng điều kiện NOT IN sẽ không đảm bảo database sẽ sử dụng index (ví như ở trên id là primary key = đã indexed) nhưng db vẫn sẽ sequences scan, nguyên nhân vì planners thường sẽ dựa vào khá nhiều thông tin để quyết định xem có nên dùng index không, index nào, optimize thế nào để có performance tốt nhất và có thể sẽ đưa ra quyết định không chính xác, xem thêm Indexes and NOT Equal.
    • Dưới đây là thông tin sau khi explain ra với data sample khoảng 80k records (Postgresql), thông tin chi tiết có thể xem ở đây https://explain.depesz.com/s/W7yl
    EXPLAIN analyze
    SELECT * FROM eateries WHERE id NOT IN (<List Ids here about 1000 items>)
    ORDER BY random()
    LIMIT 10
   Limit  (cost=109157.01..109157.04 rows=10 width=896) (actual time=804.610..804.614 rows=10 loops=1)
  ->  Sort  (cost=109157.01..109348.06 rows=76419 width=896) (actual time=804.608..804.611 rows=10 loops=1)
        Sort Key: (random())
        Sort Method: top-N heapsort  Memory: 29kB
        ->  Seq Scan on eateries  (cost=0.00..107505.63 rows=76419 width=896) (actual time=0.024..743.249 rows=77288 loops=1)
              Filter: (id <> ALL ('{<List ids dài quáaaaaaaaaaaaaaaaaaaaaaaa}'::integer[]))
              Rows Removed by Filter: 783
Planning time: 1.187 ms
Execution time: 804.675 ms
  • Per node type stats
node type count sum of times % of query
Limit 1 0.003 ms 0.0 %
Seq Scan 1 743.249 ms 92.4 %
Sort 1 61.362 ms 7.6 %
  • Nhìn vào query plan ở trên ta thấy NOT IN đã được chuyển thành id <> ALL('{}'::integer[]), và quan trọng nhất là index trên primary key id đã không được dùng mà planner đã dùng sequence scan aka full scan table, chiếm 743ms, quá chậm.
  • Ngoài ra còn một vấn đề khác là hàm RANDOM() cũng làm ảnh hưởng đến tốc độ, 61.3ms, và thực tế con số này rất vô chừng có thể tăng hoặc giảm tùy theo database stats, nhưng có thể chắc chắn sẽ chậm với lượng data lớn.
  • Vậy có cách nào khác để tăng tốc cho hàm RANDOM() không? Có thể sử dụng một vài cách khác để pick ra được random data nhưng theo đánh giá cá nhân của mình sẽ đều có trade off, tức tốc độ query sẽ tỉ lệ nghịch với mức độ random, và thật sự mà nói cũng không thể cải thiện được nhiều. Xem thêm Best way to select random rows PostgreSQL Performance of ORDER BY RANDOM to select random rows? và Quality of PostgreSQL’s random() function? . Mình sử dụng postgresql là chính nên chỉ research về postgresql, nhưng cơ bản random là vấn đề của computer science nói chung nên mình nghĩ ở database khác cũng sẽ khó có cách tiếp cận tốt hơn, xem thêm về cách Cloudflare random bằng cách dùng đèn lava LavaRand in Production: The Nitty-Gritty Technical Details

Chữ “Cà” trong tiếng Việt

Trong tiếng Việt, chữ “cà” là một trong những chữ thuộc loại đa dạng vì có nhiều tiếng đôi. Vì có một số độc giả thuộc giới trẻ lớn lên...

Cuộc sống ở nông thôn Nam Bộ một thế kỷ trước qua tranh màu của Pháp

Người nông dân chăm sóc ruộng rau, thiếu niên chăn trâu, cụ ông thư giãn trên tấm phản… là loạt tranh màu được in trong cuốn sách của Pháp: “Chuyên...

Truyền kỳ về Viên Thụ San – Vị thầy xem tướng lừng danh một thời

Xem tướng đoán mệnh là một chuyện kỳ diệu lạ lùng, thế nhưng, có người vẫn không để tâm, đặc biệt là những quân nhân trẻ tuổi tinh lực dồi...

Giấc mơ hàng hiệu Liên Xô thời bao cấp

Mỗi đợt nhận được vài cái quần bò, bán rẻ thì một chiếc cũng phải 2 chỉ vảng, có chiếc hàng độc, người ta kì kèo tôi bán với giá...

Hình chim trên trống đồng Lạc Việt

 1. Xuất xứ của chữ “Lạc” Tài liệu bằng văn bản quan trọng đầu tiên về chữ 雒 (lạc) trong danh từ 雒越 (Lạc Việt/Lo Yueh), cho đến ngày nay...

Chữ quốc ngữ bành trướng từ Nam ra Bắc

1. Chữ quốc ngữ buổi đầu ở Nam Kỳ thuộc Pháp Từ quốc ngữ dùng ở chương này dùng để chỉ tiếng Việt và chữ viết theo mẫu tự La Tinh. Tình...

Sự tích một chiếc nghiên xưa

Ở viện bảo tàng, tại đế đô (Huế) có một cái nghiên. Ông coi việc rất lấy làm vinh hạnh khi có một người lạ đến thăm viện... Ông được...

Để phiền não trở thành chuyện tích cực hơn

Nước mềm mại mới có thể chảy ra tới biển sâu, vậy nên làm người biết thay đổi tâm thái, chuyển hướng trong tâm thì chuyện xấu cũng ắt sẽ...

Bố mẹ chúng ta khổ như thế nào?

“Mẹ ơi, hai hôm nữa là đến hạn nộp học phí của con rồi ạ.” “Ừ… ừ mẹ biết rồi, đợi mẹ đi hỏi thêm vài người nữa.” Vài câu...

Lột trần Việt ngữ – Kỳ 5/25 – Tài ba của Việt ngữ

Mẹ hát con khen hay thì chẳng được ai khen theo hết. Nhưng chúng tôi khen vì MẸ CHÚNG TA tài tình thật sự. Nam Dương và Nhựt Bổn đã...

Ngự tiền chi bảo – con dấu vàng đặc biệt của Hoàng đế nhà Nguyễn

Triều Nguyễn đúc hai chiếc Kim bảo Ngự tiền chi bảo 御前之寶 bằng vàng với hình rồng: Một ấn có mặt hình bầu dục đúc thời Vua Gia Long; một...

Cái chết và câu chuyện của Ngũ tổ Thiền Tông

“Sinh, lão, bệnh, tử” là quy luật của sinh mệnh con người. So với “lão, bệnh” thì “sinh” và “tử” có lẽ vẫn là điều vô cùng thần bí mà...

Exit mobile version