// Query: đơn của 1 customer trong tháng 9
await ddb.send(new QueryCommand({
TableName: 'app',
KeyConditionExpression: 'PK = :pk AND begins_with(SK, :m)',
ExpressionAttributeValues: { ':pk': 'TENANT#42#CUST#7', ':m': 'ORDER#2026-09' },
}));
NoSQL & Search (Elasticsearch)
draftedKhi nào NoSQL, DynamoDB single-table, MongoDB essentials, Elasticsearch: inverted index, analyzer, mapping, shard, reindex với alias, sync DB → ES và drift.
0/48 rated · mastery 0%
Bài học
11 bài · ~160 phút đọc. Học theo thứ tự, rồi làm câu hỏi.- 01~19 phút· 4 câuverify
Chọn NoSQL hay relational: các họ NoSQL, CAP/PACELC, polyglot persistence
NoSQL tối ưu cho access pattern chứ không 'nhanh hơn SQL': các họ key-value, document, wide-column, graph, search; tín hiệu để chọn; CAP/PACELC nói thật gì; và vì sao Elasticsearch là read model chứ không phải source of truth.
- 02~15 phút· 4 câuverify
DynamoDB: partition key, sort key, Query vs Scan
Partition key được hash để chọn partition vật lý có giới hạn throughput riêng; sort key cho range query; Query vs Scan và bẫy FilterExpression; Limit + LastEvaluatedKey; hot partition và write sharding. Có output chạy thật trên DynamoDB Local 3.3.1.
- 03~13 phút· 4 câuverify
DynamoDB: single-table design, GSI/LSI, consistency, transactions
Thiết kế từ access pattern ngược về key: item collection, GSI overloading, sparse index; GSI vs LSI; đọc eventual vs strong và nơi không có strong; optimistic locking, uniqueness và TransactWriteItems vs BatchWriteItem. Output chạy thật trên DynamoDB Local 3.3.1.
- 04~14 phút· 4 câuverify
MongoDB: embed vs reference, ESR index, write/read concern
Mô hình document: khi nào nhúng, khi nào tham chiếu, giới hạn 16 MB và mảng không giới hạn; compound index theo ESR đọc bằng explain; write concern, read concern, read preference và khi nào đáng dùng multi-document transaction. Output chạy thật trên MongoDB 8.2.1.
- 05~13 phút· 4 câuverify
Inverted index & analyzer: search tiếng Việt, autocomplete
Vì sao inverted index nhanh hơn LIKE '%x%', analyzer biến text thành term ra sao (char filter → tokenizer → token filter), index-time vs search-time analyzer, bỏ dấu tiếng Việt mà vẫn giữ độ chính xác, NFC vs NFD, và so sánh edge_ngram, search_as_you_type, completion suggester. Output chạy thật trên Elasticsearch 9.5.3.
- 06~14 phút· 6 câuverify
Mapping: text vs keyword, dynamic mapping, nested, mapping explosion
Mapping là schema của index: text vs keyword và multi-field, dynamic mapping đoán kiểu ra sao (và cắt 4.5 thành 4), dynamic strict vs false, vì sao không đổi được kiểu field, object bị flatten vs nested, flattened, và mapping explosion với giới hạn 1000 field. Output chạy thật trên Elasticsearch 9.5.3.
- 07~14 phút· 4 câuverify
Query DSL, BM25 & phân trang sâu
match vs term, bool must/filter/should/must_not và filter cache; query-then-fetch; BM25 đọc từ _explain và vì sao IDF theo shard làm lệch điểm; tuning relevance cho product search; vì sao from+size bị chặn ở 10.000 và cách dùng search_after + PIT. Output chạy thật trên Elasticsearch 9.5.3.
- 08~16 phút· 5 câuverify
Shard, routing, refresh & sức khoẻ cluster
Primary shard và replica, routing document tới shard, custom routing theo tenant; vì sao Elasticsearch là near real-time (buffer, translog, refresh, flush, search idle); oversharding và cỡ shard; cluster yellow vs red và cách điều tra bằng allocation explain. Output chạy thật trên Elasticsearch 9.5.3.
- 09~13 phút· 5 câuverify
Reindex không downtime: alias, backfill, bulk load, rollout mapping
Alias và write index, vì sao app không bao giờ dùng tên index thật; _reindex vs backfill từ DB; xử lý write trong lúc reindex (dual-write, replay Kafka) và race với backfill; tối ưu bulk load 50 triệu document; kế hoạch đổi mapping cho 500 triệu document có shadow traffic và rollback. Output chạy thật trên Elasticsearch 9.5.3.
- 10~15 phút· 5 câuverify
Đồng bộ DB → Elasticsearch: dual write, outbox, CDC và indexer đúng khi lỗi
Vì sao dual write gây drift; transactional outbox và CDC (Debezium) hoạt động ra sao; indexer idempotent: key Kafka theo entity, coi event là tín hiệu và load trạng thái mới nhất, external version chống out-of-order, delete và cửa sổ gc_deletes; enrichment chuẩn hoá dữ liệu provider. Output chạy thật trên Postgres 17 + Elasticsearch 9.5.3.
- 11~15 phút· 5 câuverify
Drift, reconciliation & thiết kế product search
Phát hiện, đo và sửa drift giữa DB và Elasticsearch (count theo tenant che lỗi bù trừ, so version theo lô id, bẫy delete không version); freshness SLO; thiết kế product search multi-tenant có facet và giá theo nhóm khách; khi nào thêm Elasticsearch là sai và Postgres FTS/pg_trgm/unaccent làm được gì. Output chạy thật trên Postgres 17 + Elasticsearch 9.5.3.
Tóm tắt & cheat sheet của track
TL;DR
- NoSQL không phải "SQL nhưng nhanh hơn": mỗi loại tối ưu cho access pattern cụ thể. DynamoDB: key-value/wide-column, thiết kế từ query ngược về schema. MongoDB: document linh hoạt, embed vs reference. Relational vẫn là mặc định cho dữ liệu quan hệ + transaction.
- Elasticsearch là search engine (Lucene) — tuyệt cho full-text, relevance, facet, aggregation; không phải source of truth: near-real-time (refresh ~1s), không transaction đa document, mapping khó đổi.
- Pattern chuẩn: DB là nguồn sự thật → sync sang ES qua event/CDC → indexer idempotent, có versioning → reconcile để phát hiện drift → reindex bằng alias không downtime.
- Senior interview hỏi: mapping explosion, text vs keyword, analyzer tiếng Việt, shard sizing, deep pagination, dual-write, ordering/idempotency của indexer, hot partition DynamoDB.
Khái niệm cốt lõi
| Khái niệm | Một câu | Ví dụ ngắn |
|---|---|---|
| Partition key (DynamoDB) | Hash quyết định partition vật lý; cần phân tán đều | PK = TENANT#42#ORDER#991 |
| Sort key | Sắp xếp item trong cùng partition; cho phép range query | SK = ITEM#0001, begins_with(SK, 'ITEM#') |
| GSI / LSI | Index phụ: GSI khác partition key (eventual), LSI cùng PK khác SK (tạo lúc tạo table) | GSI GSI1PK = CUSTOMER#7 |
| Single-table design | Nhiều entity trong một table, key overload để một Query lấy đủ dữ liệu | Order + items cùng PK |
| Embed vs reference (MongoDB) | Nhúng dữ liệu đọc cùng nhau; tham chiếu khi lớn/không giới hạn/chia sẻ | Address nhúng trong user; order tham chiếu product |
| Inverted index | term → danh sách document chứa term | "cotton" → [doc1, doc7] |
| Analyzer | char filter → tokenizer → token filter | "Áo Thun" → [ao, thun] với lowercase + asciifolding |
text vs keyword | text: analyze để full-text; keyword: nguyên giá trị cho filter/sort/agg | name text + name.keyword |
| Mapping | Schema của index; field đã map không đổi kiểu được | dynamic: strict |
| Shard / replica | Primary shard chia dữ liệu (cố định khi tạo, trừ split/shrink); replica cho HA + đọc | 3 primary × 1 replica |
| Refresh | Làm document mới searchable (mặc định 1s) → near-real-time | refresh_interval: 30s khi bulk |
| Alias | Tên ảo trỏ tới một/nhiều index → đổi index không đổi code | products → products_v7 |
| CDC | Đọc log thay đổi của DB (WAL/binlog/CDC table) để phát event | Debezium → Kafka → indexer |
Cơ chế hoạt động
Sync DB → Elasticsearch (outbox/CDC + indexer)
Reindex không downtime bằng alias
Search: query then fetch
Coordinating node gửi query tới một bản copy của mỗi shard (scatter) → mỗi shard trả top-N id + score → gộp, sort → fetch _source của top kết quả. from + size lớn = mỗi shard phải trả from + size kết quả → deep pagination đắt.
Trade-offs
| Nhu cầu | Relational (Postgres/SQL Server) | DynamoDB | MongoDB | Elasticsearch |
|---|---|---|---|---|
| Transaction đa entity | Mạnh | Có (TransactWriteItems, giới hạn số item) | Có (multi-document, tốn hơn) | Không |
| Query ad-hoc / join | Mạnh | Yếu (phải biết trước access pattern) | Khá ($lookup, aggregation) | Không join; denormalize |
| Full-text + relevance | Cơ bản (FTS, pg_trgm) | Không | Có (Atlas Search) | Mạnh nhất |
| Scale ngang | Khó hơn (shard thủ công/Citus) | Tự động, gần như vô hạn | Sharding có sẵn | Shard có sẵn |
| Schema | Chặt | Linh hoạt, thiết kế key chặt | Linh hoạt (+ validator) | Mapping — khó đổi |
| Vận hành | Trung bình | Managed hoàn toàn | Trung bình / Atlas | Nặng (heap, shard, upgrade) |
| Cách sync DB → ES | Ưu | Nhược |
|---|---|---|
| Dual-write trong request | Đơn giản | Không atomic → drift khi một bên lỗi; tăng latency |
| Outbox + relay | Atomic với DB, thứ tự theo entity | Thêm table + relay |
| CDC (Debezium) | Không đụng code app, bắt mọi thay đổi | Hạ tầng Kafka/Connect, event mức row (cần transform) |
Batch theo updated_at | Dễ | Trễ, bỏ sót delete, clock/transaction commit muộn |
Pitfalls
- Dùng ES làm source of truth hoặc đọc-sau-ghi từ ES rồi ngạc nhiên vì chưa refresh.
dynamic: truecho object có key do user/provider định nghĩa → mapping explosion (vượtindex.mapping.total_fields.limit, mặc định 1000).termtrên fieldtext; sort/aggregate trêntext.- Quá nhiều shard nhỏ (index per tenant per ngày...) → cluster state phình, heap cạn.
from/sizesâu (mặc định chặn ởindex.max_result_window = 10000) → dùngsearch_after+ PIT.- Indexer không idempotent/không versioning → event cũ đến sau ghi đè dữ liệu mới.
- DynamoDB: partition key thấp cardinality (
status) → hot partition;Scan+FilterExpressionvẫn tính capacity cho mọi item đã đọc. - MongoDB: mảng không giới hạn trong document (vượt 16MB, index multikey phình).
Cheat sheet
- ES:
textcho search,keywordcho filter/sort/agg, multi-field cho cả hai;filtercontext không tính score và cache được. - ES đổi mapping field đã có → index mới +
_reindex+ alias swap. Primary shard count: cố định (có_split/_shrinkvới điều kiện). - Bulk load:
refresh_interval: -1,number_of_replicas: 0, bulk 5–15MB/request, bật lại sau. - Indexer: key Kafka = entity id,
version_type: externalvới version từ DB, retry idempotent, DLQ. - DynamoDB: liệt kê access pattern trước; Query > Scan; GSI eventual; item ≤ 400KB; conditional write cho optimistic locking.
- MongoDB: index theo ESR (Equality → Sort → Range); embed khi "đọc cùng nhau, giới hạn kích thước".
References
- Elastic docs — Mapping ↗
- Elastic docs — Text analysis ↗
- Elastic docs — Aliases ↗
- Elastic docs — Size your shards ↗
- Elastic docs — Paginate search results (search_after, PIT) ↗
- Elastic docs — Tune for indexing speed ↗
- AWS docs — Best practices for designing and architecting with DynamoDB ↗
- AWS docs — DynamoDB service quotas ↗
- MongoDB docs — Data modeling ↗
- MongoDB docs — The ESR (Equality, Sort, Range) guideline ↗
- Debezium documentation ↗
Personal notes (Notion)
Ghi chú cá nhân — có thể sai hoặc lỗi thời, dùng để tham khảo.
Interview Questions
Showing 48 / 48
await ddb.send(new UpdateCommand({
TableName: 'app',
Key: { PK: 'TENANT#42#PRODUCT#9', SK: 'STOCK' },
UpdateExpression: 'SET qty = qty - :n, version = version + :one',
ConditionExpression: 'qty >= :n AND version = :v',
ExpressionAttributeValues: { ':n': 2, ':one': 1, ':v': 17 },
}));
POST products/_analyze
{ "field": "name", "text": "Áo Thun Cotton" }
GET products/_search
{
"query": {
"bool": {
"must": [{ "multi_match": { "query": "ao thun", "fields": ["name^3", "description"] } }],
"filter": [
{ "term": { "tenantId": "42" } },
{ "term": { "status": "active" } },
{ "range": { "price": { "lte": 200000 } } }
]
}
}
}
POST _aliases
{
"actions": [
{ "remove": { "index": "products_v7", "alias": "products" } },
{ "add": { "index": "products_v8", "alias": "products", "is_write_index": true } }
]
}
PUT products_v8
{
"settings": { "analysis": { "analyzer": {
"vi_folded": { "tokenizer": "standard", "filter": ["lowercase", "asciifolding"] }
} } },
"mappings": { "properties": {
"name": { "type": "text", "fields": { "folded": { "type": "text", "analyzer": "vi_folded" } } }
} }
}
{
"productId": "p-991",
"tenantId": "42",
"name": "Mechanical Keyboard",
"attributes": {
"switch_type": "brown",
"Kích thước": "75%",
"provider_x_custom_field_8812": "yes",
"bluetooth version": "5.1"
}
}
// mapping created by dynamic mapping: "status": { "type": "text", "fields": { "keyword": { "type": "keyword" } } }
GET products/_search
{ "query": { "bool": { "filter": [ { "term": { "status": "Active" } } ] } } }
// Table "jobs": PK = status, SK = createdAt#jobId
await ddb.send(new PutCommand({
TableName: 'jobs',
Item: { PK: 'PENDING', SK: `${new Date().toISOString()}#${id}`, tenantId, payload },
}));
// Workers: Query PK = 'PENDING' ORDER BY SK LIMIT 50
async function updateProduct(id: string, patch: Patch) {
await db.transaction(async (tx) => {
await tx.products.update(id, patch);
});
// outside the transaction, fire-and-forget
es.index({ index: 'products', id, document: await buildDoc(id) }).catch(() => {});
}
{ "productId": "p1", "variants": [ { "color": "red", "size": "S" }, { "color": "blue", "size": "M" } ] }