Harbor

Tổng hợp tin tức

Trung bìnhThu thập RSSGộp tin trùngXếp hạng cá nhânCache/CDNGiống Google News, Báo Mới

Hiểu đề

Tổng hợp tin tức là gì? Ứng dụng gom bài từ hàng nghìn trang báo qua RSS, gộp các bài viết về cùng một sự kiện thành một cụm tin và hiển thị theo chủ đề hoặc theo sở thích. Người đọc bấm vào tin thì được chuyển sang bài gốc trên trang báo.

Yêu cầu chức năng

  1. Hệ thống thu thập bài mới từ hàng nghìn nguồn (RSS, sitemap tin tức) vài phút sau khi nguồn đăng
  2. Các bài cùng một sự kiện từ nhiều báo được gộp thành một cụm tin; bản đăng lại y hệt bị loại
  3. Người dùng xem dòng tin chính và theo chủ đề (Thời sự, Thể thao, Kinh doanh...)
  4. Người dùng đã đăng nhập có feed "Dành cho bạn" xếp theo chủ đề/nguồn họ theo dõi và lịch sử đọc
  5. Bấm vào tin thì chuyển sang bài gốc, hệ thống ghi nhận lượt click

Yêu cầu phi chức năng

  • Bài mới xuất hiện trên app p95 ≤ 5 phút sau khi nguồn đăng (nguồn lớn ≤ 2 phút)
  • Tải feed p95 < 200 ms, độ sẵn sàng 99,9%
  • Bài trùng lọt ra feed thành hai tin riêng < 1% số cụm
  • Chịu đỉnh ×5 khi có tin nóng (bão lớn, chung kết bóng đá)
  • Lưu tiêu đề, tóm tắt, link của bài trong 1 năm

Ngoài phạm vi: Lưu và hiển thị toàn văn bài báo (chỉ giữ tiêu đề, sapo, ảnh đại diện, link); Bình luận, chia sẻ, mạng xã hội; Quảng cáo và đo lường quảng cáo; Tóm tắt tin bằng AI, kiểm chứng thông tin.

Thực thể và API

Source
Nguồn tin: domain, feed URL, chu kỳ lấy hiện tại, ETag/Last-Modified lần trước, điểm tin cậy.
Article
Một bài: canonical URL, url_hash, tiêu đề, sapo, ảnh, published_at, source_id, chữ ký MinHash, cluster_id, chủ đề.
Cluster (Story)
Một sự kiện: bài đại diện, số bài, số nguồn, first_seen, last_updated, chủ đề, điểm hot.
UserProfile
Chủ đề và nguồn đang theo dõi, vector sở thích theo chủ đề tính từ lịch sử đọc.
UserEvent
Sự kiện xem/click/thời gian đọc, dùng để tính điểm hot và hồ sơ sở thích.
MethodEndpointGhi chú
GET/v1/feed?topic=thoi-su&cursor=Feed chung theo chủ đề, giống nhau cho mọi người nên cache ở CDN 30 s.
GET/v1/feed/for-you?cursor=Feed cá nhân, cần đăng nhập, chỉ trả danh sách cluster_id + dữ liệu cụm đã cache.
GET/v1/stories/{clusterId}Chi tiết một cụm: các bài từ nhiều báo, sắp theo thời gian và nguồn.
PUT/v1/me/followsTheo dõi / bỏ theo dõi chủ đề hoặc nguồn.
POST/v1/eventsClient gửi lô sự kiện xem/click, trả 202 ngay.
GET/r/{articleId}Ghi click rồi trả 302 sang bài gốc.

Thiết kế tổng thể

Đi lần lượt từng yêu cầu chức năng. Mục tiêu ở đây là chạy đúng, việc tối ưu để dành cho phần đào sâu.

1) Hệ thống thu thập bài mới từ hàng nghìn nguồn

Mỗi feed có lịch lấy riêng. Crawler gửi request có điều kiện nên phần lớn lần lấy chỉ nhận 304, rồi đẩy bài mới vào queue để khâu xử lý chạy độc lập.

  1. 1Scheduler chọn các Source có next_fetch_at <= now()
  2. 2Crawler GET feed kèm If-None-Match / If-Modified-Since; 304 thì chỉ dời lịch
  3. 3Parse RSS, chuẩn hóa URL (bỏ utm_*, chuyển về canonical), tính url_hash
  4. 4url_hash đã có thì bỏ qua; bài mới thì đẩy vào queue
  5. 5Cập nhật next_fetch_at theo tần suất đăng của nguồn

Nguồn: Grokking Modern System Design · ch.35 Design a Web Crawler ↗ · mục "Design" và "Scheduling" ; Grokking Modern System Design · ch.19 Rate Limiter ↗ · mục "Token bucket algorithm"

2) Bài cùng sự kiện được gộp thành cụm

Processor so bài mới với các bài trong 48 giờ gần nhất. Dùng MinHash + LSH để chỉ so với vài ứng viên giống nhau, rồi xác nhận bằng độ tương đồng chi tiết hơn.

  1. 1Bỏ dấu, tách từ tiếng Việt cho tiêu đề + sapo, tạo shingle 3 từ
  2. 2Tính chữ ký MinHash 128 giá trị, tra bảng LSH để lấy các bài ứng viên
  3. 3Tính Jaccard / cosine với ứng viên; vượt ngưỡng thì gán cluster_id của ứng viên
  4. 4Không có ứng viên đủ giống thì tạo cụm mới
  5. 5Cập nhật số bài, số nguồn, bài đại diện và điểm hot của cụm

⚠ Chưa có nguồn trong thư viện: MinHash + LSH không có trong sách; Grokking ch.26 YouTube chỉ nhắc locality-sensitive hashing để loại trùng

3) Người dùng xem dòng tin chính và theo chủ đề

Feed chung giống nhau với mọi người nên được tính sẵn và phục vụ từ CDN. Origin chỉ dựng lại danh sách mỗi 30 giây.

  1. 1Job mỗi 30 s lấy top cụm theo điểm hot cho từng chủ đề, ghi vào Redis ZSET
  2. 2Client gọi GET /v1/feed?topic=... qua CDN (s-maxage=30, stale-while-revalidate)
  3. 3CDN miss thì API đọc ZSET + dữ liệu cụm từ Redis, trả JSON

Nguồn: Grokking Modern System Design · ch.11 Content Delivery Network (CDN) ↗ · mục "Content caching strategies in CDN" ; Grokking Modern System Design · ch.32 Design Newsfeed System ↗ · mục "The newsfeed ranking service"

4) Người dùng có feed cá nhân hóa

Xếp hạng hai bước: lấy vài trăm ứng viên từ các danh sách đã tính sẵn, rồi chấm điểm theo hồ sơ của người dùng ngay trong request.

  1. 1Lấy ứng viên: top cụm của các chủ đề người dùng theo dõi + tin nóng chung + tin địa phương
  2. 2Loại cụm đã đọc (Bloom filter theo user trong Redis)
  3. 3Điểm = mức quan tâm chủ đề × độ mới (giảm theo thời gian) × độ lớn cụm, thêm luật đa dạng nguồn
  4. 4Cache kết quả theo user 5 phút; client gửi sự kiện đọc về /v1/events để cập nhật hồ sơ

Nguồn: Grokking Modern System Design · ch.32 Design Newsfeed System ↗ · mục "The newsfeed ranking service" và "Posts ranking and newsfeed construction"

Hạ tầng theo quy mô

Cùng một sản phẩm, ba mức người dùng. Chọn từng mức để xem sơ đồ, phép tính và cấu hình server thay đổi thế nào.

Người dùng
10.000 DAU, 200 nguồn
Tải đỉnh
≈ 17 req/s đọc
Chi phí ước lượng
≈ 40–120 USD/tháng (ước lượng)

Trang tổng hợp tin nhỏ. Hai instance Node sau CDN, một Postgres, crawler chạy theo lịch trong một worker; gộp cụm bằng TF-IDF trong bộ nhớ là đủ.

Người dùngNgười đọcCDNCDNDịch vụAPI Node.jsDatabasePostgreSQLWorkerCrawler + gộpBên ngoàiBáo (RSS)HTTPScache missfeed, cụm tinGET RSSbài + cụm

Ước lượng

  • Crawl: 200 feed × 1 lần / 300 s ≈ 0,67 request/s
  • Bài mới: 200 nguồn × 50 bài/ngày = 10.000 bài/ngày / 86.400 ≈ 0,12 bài/s
  • Đọc: 10.000 DAU × 30 req / 86.400 ≈ 3,5 req/s, đỉnh ×5 ≈ 17 req/s
  • API: ceil(17 / 600) = 1 → chạy 2 để dự phòng
  • Lưu trữ: 10.000 bài × 2 KB × 365 ≈ 7,3 GB/năm
  • Gộp cụm: cửa sổ 48 h = 20.000 bài, so TF-IDF trong bộ nhớ vẫn dưới 1 s mỗi bài

Nguồn con số: Grokking Modern System Design · ch.5 Back-of-the-envelope Calculations ↗ · mục "Standard numbers to remember" và "Requests estimation" ; Quy tắc ước lượng của khoá học · /hoc/chon-server

Bảng hạ tầng

Thành phầnChọnSố lượngCấu hìnhVì sao
CDNCloudflare (gói free/pro)1Cache feed chủ đề 30 s, ảnh, JS/CSSPhần lớn lượt đọc không chạm origin.
APINode.js (NestJS) trên Cloud Run22 vCPU · 4 GB RAM17 req/s lúc đỉnh, 2 instance để không chết khi 1 cái lỗi.
Crawler + xử lýNode.js worker, rss-parser11 vCPU · 2 GB RAM0,67 request/s và 0,12 bài/s; chết vài phút chỉ làm tin trễ, không mất dữ liệu.
DatabasePostgreSQL managed12 vCPU · 8 GB · 50 GB SSD7,3 GB/năm; lưu bài, cụm, nguồn, người dùng trong một chỗ.
Thay đổi so với mức trước
  • Điểm xuất phát: một API, một worker crawl, một Postgres, CDN phía trước
  • Mỗi feed lấy 5 phút một lần với ETag
  • Gộp cụm bằng TF-IDF trong bộ nhớ, xếp hạng theo điểm hot giảm dần theo thời gian
Nút thắt tiếp theo

Khi lên hàng nghìn nguồn, so từng bài với toàn bộ cửa sổ 48 giờ trở nên chậm, và feed cá nhân query thẳng Postgres sẽ quá tải lúc tin nóng.

Cấu hình và chi phí là ước lượng để học và trình bày trong phỏng vấn. Xem cách tính ở bài Chọn server.

So sánh nhanh ba mức

Thành phầnMức 1Mức 2Mức 3
Người dùng10.000 DAU, 200 nguồn1 triệu DAU, 2.000 nguồn50 triệu DAU, 20.000 nguồn trong và ngoài nước
Chi phí≈ 40–120 USD/tháng (ước lượng)≈ 1.500–4.000 USD/tháng (ước lượng)≈ 30.000–80.000 USD/tháng (ước lượng)
CDN
Cloudflare (gói free/pro)
1 × Cache feed chủ đề 30 s, ảnh, JS/CSS
Cloudflare / CloudFront
1 × Feed chủ đề s-maxage 30 s + stale-while-revalidate, ảnh resize
CloudFront / Cloudflare, nhiều PoP
1 × Feed chủ đề, trang cụm tin, ảnh resize
API
Node.js (NestJS) trên Cloud Run
2 × 2 vCPU · 4 GB RAM
Node.js (NestJS), container autoscale
2–4 (autoscale) × 2 vCPU · 4 GB RAM
Chưa cần
Crawler + xử lý
Node.js worker, rss-parser
1 × 1 vCPU · 2 GB RAM
Chưa cầnChưa cần
Database
PostgreSQL managed
1 × 2 vCPU · 8 GB · 50 GB SSD
PostgreSQL managed (HA)
1 primary + 1 read replica × 4 vCPU · 16 GB · 200 GB SSD
PostgreSQL HA (Aurora), partition theo tháng
1 primary + 2 replica × 8 vCPU · 32 GB · 1 TB
CrawlerChưa cần
Node.js worker pool
2 × 1 vCPU · 2 GB RAM
Node.js trên Kubernetes
6 pod × 1 vCPU · 2 GB mỗi pod
QueueChưa cần
Amazon SQS / Google Pub/Sub
2 (bài mới, sự kiện) × Managed, có dead-letter queue
Chưa cần
Gộp cụmChưa cần
Node.js / Python worker, MinHash + LSH
2 × 2 vCPU · 4 GB RAM
Worker MinHash + LSH (tùy chọn embedding)
3 pod × 4 vCPU · 8 GB mỗi pod
RedisChưa cần
ElastiCache / Memorystore
1 primary + 1 replica × 2 vCPU · 8 GB
Chưa cần
Kho sự kiệnChưa cần
ClickHouse / BigQuery
1 × Managed
Chưa cần
Feed APIChưa cầnChưa cần
Node.js trên Kubernetes, 3 AZ
63 pod (HPA) × 2 vCPU · 4 GB mỗi pod
KafkaChưa cầnChưa cần
MSK / Confluent
6 broker × Topic articles 12 partition, events 24 partition, RF 3
Stream processingChưa cầnChưa cần
Apache Flink
6 TaskManager × 4 vCPU · 16 GB mỗi node
Redis ClusterChưa cầnChưa cần
ElastiCache Redis Cluster
6 shard × (1 primary + 1 replica) × 2 vCPU · 13 GB mỗi node

Đào sâu

Mỗi câu hỏi đi từ cách đơn giản đến cách người phỏng vấn mong đợi ở cấp senior.

1) Lấy bài từ hàng nghìn nguồn thế nào để nhanh mà không làm phiền các trang báo?

Cách chưa tốt
Cron mỗi phút gọi lần lượt mọi feed

Một vòng lặp tuần tự gọi tất cả feed, mỗi lần tải lại toàn bộ file RSS.

Đánh đổi: Vài nghìn feed thì một vòng đã dài hơn một phút; báo ít đăng bị gọi thừa, báo lớn lại bị lấy chậm.

Nguồn: Grokking Modern System Design · ch.35 Design a Web Crawler ↗ · mục "Scheduling" (tần suất theo domain)

Cách tốt
Lịch riêng cho từng feed + request có điều kiện + worker song song

Mỗi Source có next_fetch_at; worker pool lấy feed đến hạn. Gửi ETag/Last-Modified để nhận 304 khi không có gì mới. Giới hạn số request đồng thời theo domain (token bucket, Grokking ch.19).

Đánh đổi: Chu kỳ cố định cho mỗi nguồn vẫn phải chỉnh tay; nguồn đăng dồn dập lúc có tin nóng vẫn bị trễ.

Nguồn: Grokking Modern System Design · ch.35 ↗ · mục "Self-throttle at a domain while crawling" ; Grokking Modern System Design · ch.19 Rate Limiter ↗ · mục "Token bucket algorithm" ; Chưa có nguồn trong thư viện: ETag/Last-Modified không có trong sách

Cách tốt nhất
Chu kỳ thích ứng theo tần suất đăng + nhận đẩy khi nguồn hỗ trợ

Ước lượng tốc độ đăng của nguồn theo khung giờ: nguồn đăng 10 bài/giờ được hỏi mỗi 1-2 phút, nguồn đăng 2 bài/ngày thì 30 phút; khi phát hiện bài mới thì rút ngắn chu kỳ tạm thời. Nguồn hỗ trợ WebSub hoặc sitemap tin tức thì nhận đẩy và dùng polling làm dự phòng. Fetch và xử lý tách bằng queue nên đợt tin nóng không làm crawler nghẽn.

Đánh đổi: Thêm logic ước lượng và theo dõi tình trạng từng nguồn; vẫn cần bộ parse HTML cho các báo không có RSS chuẩn.

Nguồn: Grokking Modern System Design · ch.35 ↗ · mục "Scheduling" ; Chưa có nguồn trong thư viện: WebSub và chu kỳ thích ứng là thiết kế của khoá học

2) Làm sao biết 20 bài của 20 báo khác nhau là cùng một sự kiện?

Cách chưa tốt
So trùng URL hoặc tiêu đề chính xác

Chỉ gộp khi URL hoặc tiêu đề giống hệt. Bắt được bài đăng lại nhưng các báo luôn đặt tiêu đề khác nhau.

Đánh đổi: Feed đầy tin trùng: cùng một trận bóng xuất hiện 15 lần.

⚠ Chưa có nguồn trong thư viện: so trùng URL là nhận định chung

Cách tốt
TF-IDF + cosine với các bài cùng chủ đề trong 48 giờ

Biểu diễn tiêu đề + sapo bằng vector TF-IDF đã bỏ dấu và tách từ, so cosine với từng bài gần đây cùng chủ đề, vượt ngưỡng thì gộp.

Đánh đổi: Mỗi bài mới so với hàng chục nghìn bài nên chi phí tăng tuyến tính; chỉ ổn ở quy mô vài trăm nguồn.

Nguồn: Grokking Modern System Design · ch.21 Distributed Search ↗ · mục "Inverted index" ; Chưa có nguồn trong thư viện: TF-IDF và cosine không có trong sách

Cách tốt nhất
MinHash + LSH để lấy ứng viên, xác nhận bằng độ tương đồng + thực thể, gom cụm theo cửa sổ thời gian

Chữ ký MinHash chia thành các band; bài nào trùng ít nhất một band là ứng viên, nên mỗi bài chỉ so chi tiết với vài bài. Xác nhận bằng Jaccard trên shingle cộng với trùng thực thể (tên người, địa danh, tỉ số). Cụm chỉ nhận bài trong 48 giờ để tin cũ không hút tin mới. Một job offline mỗi giờ gộp hoặc tách lại cụm bị sai. Có thể thay MinHash bằng embedding và tìm láng giềng gần khi cần hiểu nghĩa hơn.

Đánh đổi: Ngưỡng phải tinh chỉnh bằng dữ liệu gán nhãn; tách từ tiếng Việt sai làm giảm chất lượng; gộp nhầm hai sự kiện khác nhau tệ hơn để sót trùng.

⚠ Chưa có nguồn trong thư viện: MinHash + LSH không có trong sách; Grokking ch.26 chỉ nhắc locality-sensitive hashing

3) Xếp hạng feed "Dành cho bạn" thế nào cho nhanh và hợp sở thích?

Cách chưa tốt
Sắp theo thời gian đăng mới nhất

Ai cũng thấy cùng một danh sách mới nhất. Không cần mô hình gì.

Đánh đổi: Nguồn đăng nhiều chiếm hết feed, tin lớn bị đẩy xuống sau vài phút, không cá nhân hóa.

⚠ Chưa có nguồn trong thư viện: sắp theo thời gian là nhận định chung

Cách tốt
Điểm hot có giảm dần theo thời gian + lọc theo chủ đề theo dõi

Điểm cụm = thời điểm xuất hiện + hằng số × (số nguồn + lượt click), giống cách cộng 432 điểm mỗi phiếu trong Redis in Action §1.3.1, nên tin cũ tự tụt hạng. Lưu ZSET theo chủ đề, feed cá nhân là trộn các ZSET của chủ đề người dùng theo dõi.

Đánh đổi: Cá nhân hóa còn thô, chỉ ở mức chủ đề; người dùng mới chưa theo dõi gì thì feed giống feed chung.

Nguồn: Redis in Action ↗ · ch.1 · §1.3 Hello Redis, mục "Voting on articles" (tr. 15) ; Redis in Action ↗ · ch.3 · §3.5 Sorted sets

Cách tốt nhất
Hai bước: lấy ứng viên từ danh sách tính sẵn, xếp hạng theo hồ sơ người dùng khi request

Ứng viên là ~500 cụm từ ZSET các chủ đề quan tâm, tin nóng và tin địa phương. Bước xếp hạng nhẹ (dưới 50 ms) kết hợp vector sở thích tính từ click và thời gian đọc, độ mới, độ lớn cụm, loại tin đã đọc, rồi áp luật đa dạng (không quá 2 tin liền nhau cùng nguồn hoặc chủ đề). Hồ sơ cập nhật bằng stream sự kiện, giống ranking service của Grokking ch.32. Người dùng mới bắt đầu từ tin phổ biến theo vùng.

Đánh đổi: Cần pipeline sự kiện và thêm một dịch vụ phải đo A/B; dễ tạo bong bóng lọc nếu không giữ một phần tin chung.

Nguồn: Grokking Modern System Design · ch.32 Design Newsfeed System ↗ · mục "The newsfeed ranking service"

4) Lúc có tin nóng, lượng đọc tăng ×5. Phục vụ feed nhanh thế nào?

Cách chưa tốt
Mỗi request feed query Postgres join articles và clusters

Truy vấn sắp xếp theo điểm, join bảng bài và cụm cho từng request.

Đánh đổi: Hàng nghìn query nặng mỗi giây đè sập DB đúng lúc nhiều người đọc nhất.

⚠ Chưa có nguồn trong thư viện: query Postgres mỗi request là nhận định chung

Cách tốt
Cache feed theo chủ đề trong Redis, TTL 60 s

API đọc danh sách đã tính sẵn trong Redis, DB chỉ bị chạm khi dựng lại cache.

Đánh đổi: Mọi request vẫn đi tới origin; nhiều request cùng lúc khi cache vừa hết hạn có thể dội xuống DB (cache stampede).

Nguồn: Grokking Modern System Design · ch.16 Distributed Cache ↗ · mục "Writing policies" và "Cache invalidation" (TTL)

Cách tốt nhất
Feed chung trên CDN, feed cá nhân chỉ là danh sách ID, dữ liệu cụm cache riêng

Feed theo chủ đề giống nhau cho mọi người nên CDN (Grokking ch.11) giữ 30 s với stale-while-revalidate, origin chỉ nhận vài request mỗi chủ đề mỗi 30 s. Feed cá nhân chỉ chứa danh sách cluster_id; nội dung cụm lấy từ cache dùng chung (Redis, Grokking ch.16) nên một cụm hot chỉ nằm một chỗ. Khi dựng lại cache thì gộp các request trùng (request coalescing). Ảnh đại diện resize và cache qua CDN.

Đánh đổi: Feed chung trễ tới 30 s; tin cực nóng cần purge CDN theo tag chủ đề.

Nguồn: Grokking Modern System Design · ch.11 Content Delivery Network (CDN) ↗ · mục "Content caching strategies in CDN" ; Grokking Modern System Design · ch.16 Distributed Cache ↗ · mục "Cache client" ; Chưa có nguồn trong thư viện: stale-while-revalidate không có trong sách

Trong phỏng vấn

Nên nói
  • “Hệ thống chia làm hai đường: đường ghi (crawl, gộp cụm) ít tải nhưng cần đúng, đường đọc (feed) rất nhiều tải nên phục vụ từ cache và CDN.”
  • “Tôi lấy feed theo chu kỳ thích ứng với request có điều kiện, nên phần lớn lần lấy chỉ nhận 304.”
  • “Gộp tin dùng MinHash + LSH để tìm ứng viên trong cửa sổ 48 giờ, không so mỗi bài với toàn bộ kho.”
  • “Feed chung giống nhau cho mọi người nên nằm trên CDN; feed cá nhân chỉ là danh sách ID trỏ vào dữ liệu cụm dùng chung.”
Bẫy hay gặp
  • Gộp tin bằng so trùng tiêu đề hoặc URL.
  • Gọi lại mọi feed với cùng chu kỳ cố định, không dùng ETag/Last-Modified.
  • Lưu toàn văn bài báo khi chỉ cần tiêu đề, sapo và link.
  • Tính feed cá nhân bằng query DB cho từng request.

Đọc thêm

Caching
Đặt cache ở đâu (client, CDN, app, Redis), các chiến lược đọc ghi cache-aside, write-through, write-back, write-around, eviction LRU/LFU/TTL, invalidation, chống stampede và hot key, cách tính RAM Redis và toán hit ratio.
Scale đọc
Phục vụ lượng đọc gấp hàng trăm lần lượng ghi: index, read replica và replication lag, cache nhiều tầng, CDN, read model denormalized theo CQRS, cùng cách xử lý cache stampede và hot key.
Redis
Redis là kho key-value trong RAM với nhiều cấu trúc dữ liệu: dùng làm cache, bộ đếm, leaderboard, hàng đợi, GEO và pub/sub. Bài này đi qua mô hình dữ liệu, persistence, replication, Cluster, Lua và cách tính RAM.
Kafka
Kafka là log phân tán bền vững: topic chia partition, consumer đọc theo offset và có thể đọc lại. Bài này đi qua thứ tự theo partition, consumer group, replication với ISR và acks, giới hạn của exactly-once, cách chọn số partition và so sánh với SQS, Pub/Sub, RabbitMQ.
Apache Flink
Flink là engine xử lý stream có trạng thái, đọc event từ Kafka, gom theo cửa sổ thời gian và cho kết quả chính xác kể cả khi event đến muộn hoặc máy chết. Bài này giải thích event time, watermark, các loại window, state và checkpoint, exactly-once với Kafka, và so sánh với Kafka Streams, Spark.
Vector database
Vector database lưu embedding và tìm các vector gần nhất trong vài mili giây. Bài này đi qua độ đo tương đồng, index HNSW và IVF, pgvector so với Qdrant, Milvus, Pinecone, bài toán lọc metadata, RAG và cách tính RAM.

Sách trong thư viện của bạn. Số chương có thể lệch nhẹ so với bản PDF.