Skip to content
DevRecall
← Roadmap
15

NoSQL & Search (Elasticsearch)

drafted

Khi 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.
  1. 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.

  2. 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.

  3. 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.

  4. 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.

  5. 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.

  6. 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.

  7. 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.

  8. 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.

  9. 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. 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. 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ệmMột câuVí dụ ngắn
Partition key (DynamoDB)Hash quyết định partition vật lý; cần phân tán đềuPK = TENANT#42#ORDER#991
Sort keySắp xếp item trong cùng partition; cho phép range querySK = ITEM#0001, begins_with(SK, 'ITEM#')
GSI / LSIIndex 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 designNhiều entity trong một table, key overload để một Query lấy đủ dữ liệuOrder + 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 indexterm → danh sách document chứa term"cotton" → [doc1, doc7]
Analyzerchar filter → tokenizer → token filter"Áo Thun" → [ao, thun] với lowercase + asciifolding
text vs keywordtext: analyze để full-text; keyword: nguyên giá trị cho filter/sort/aggname text + name.keyword
MappingSchema của index; field đã map không đổi kiểu đượcdynamic: strict
Shard / replicaPrimary shard chia dữ liệu (cố định khi tạo, trừ split/shrink); replica cho HA + đọc3 primary × 1 replica
RefreshLàm document mới searchable (mặc định 1s) → near-real-timerefresh_interval: 30s khi bulk
AliasTên ảo trỏ tới một/nhiều index → đổi index không đổi codeproducts → products_v7
CDCĐọc log thay đổi của DB (WAL/binlog/CDC table) để phát eventDebezium → 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ầuRelational (Postgres/SQL Server)DynamoDBMongoDBElasticsearch
Transaction đa entityMạnhCó (TransactWriteItems, giới hạn số item)Có (multi-document, tốn hơn)Không
Query ad-hoc / joinMạnhYếu (phải biết trước access pattern)Khá ($lookup, aggregation)Không join; denormalize
Full-text + relevanceCơ bản (FTS, pg_trgm)KhôngCó (Atlas Search)Mạnh nhất
Scale ngangKhó hơn (shard thủ công/Citus)Tự động, gần như vô hạnSharding có sẵnShard có sẵn
SchemaChặtLinh hoạt, thiết kế key chặtLinh hoạt (+ validator)Mapping — khó đổi
Vận hànhTrung bìnhManaged hoàn toànTrung bình / AtlasNặng (heap, shard, upgrade)
Cách sync DB → ESƯuNhược
Dual-write trong requestĐơn giảnKhông atomic → drift khi một bên lỗi; tăng latency
Outbox + relayAtomic với DB, thứ tự theo entityThêm table + relay
CDC (Debezium)Không đụng code app, bắt mọi thay đổiHạ tầng Kafka/Connect, event mức row (cần transform)
Batch theo updated_atDễ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: true cho object có key do user/provider định nghĩa → mapping explosion (vượt index.mapping.total_fields.limit, mặc định 1000).
  • term trên field text; sort/aggregate trên text.
  • Quá nhiều shard nhỏ (index per tenant per ngày...) → cluster state phình, heap cạn.
  • from/size sâu (mặc định chặn ở index.max_result_window = 10000) → dùng search_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 + FilterExpression vẫ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: text cho search, keyword cho filter/sort/agg, multi-field cho cả hai; filter context 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/_shrink vớ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: external vớ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".

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

// 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' },
}));
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" } ] }