Dashboard bar chart và pie chart rất hợp để xem “có bao nhiêu”, nhưng khi câu hỏi đổi sang “ở đâu” thì biểu đồ phẳng vô dụng. Một fleet xe tải, một mạng cảm biến nhiệt độ trải dài 5 tỉnh, một CDN edge log có thông tin client IP. Tất cả đều cần một bản đồ. Trong Kibana, công cụ đó là Maps, và nó là một trong những plugin ít được dùng đúng cách nhất.

Bài này gom kinh nghiệm setup Maps cho hai use case chính: IoT telemetry (sensor data) và request log có geo-IP. Sau khi đọc xong bạn sẽ biết:

  • Cách map field thành geo_point để Kibana hiểu được toạ độ
  • Các loại layer (documents, heatmap, cluster, choropleth) và khi nào dùng cái nào
  • Pitfall kinh điển: lat/lon ngược, timestamp UTC vs local, sampling khi data quá dày
  • Cách kết hợp Maps với time slider để xem chuyển động theo thời gian
  • Pattern export geojson để dùng lại trong dashboard khác

Phần 1: Mental model về geo data trong Elasticsearch

Trước khi vẽ map, phải hiểu Elasticsearch nhìn toạ độ ra sao. Đây là chỗ nhiều dev backend sa lầy vì gửi {lat, lon} lên ES nhưng Maps không nhận.

Elasticsearch có ba kiểu field cho geo:

Field typeDùng choGhi chú
geo_pointĐiểm đơn (sensor, vehicle, IP)Phổ biến nhất. Bắt buộc cho Maps point layer.
geo_shapePolygon, line, multi-shapeDùng cho ranh giới hành chính, route. Nặng hơn.
pointCartesian (không phải geo)Hệ toạ độ phẳng. Không phải lat/lon thực tế.

Vấn đề: nếu bạn index { "location": { "lat": 10.7, "lon": 106.7 } } mà chưa khai báo mapping thì Elasticsearch dynamic-map nó thành object với hai float con, không phải geo_point. Maps mở ra sẽ không thấy field nào geo-compatible.

Cách fix: tạo index template trước khi data flow vào.

PUT _index_template/iot-telemetry
{
  "index_patterns": ["iot-telemetry-*"],
  "template": {
    "mappings": {
      "properties": {
        "@timestamp": { "type": "date" },
        "device_id": { "type": "keyword" },
        "location": { "type": "geo_point" },
        "temperature": { "type": "float" },
        "battery": { "type": "byte" }
      }
    }
  }
}

Sau khi template apply, bất kỳ index nào tên iot-telemetry-* mới tạo sẽ tự dùng mapping này. Index cũ phải reindex hoặc rollover, không thay mapping in-place được.

Phần 2: Format chấp nhận được cho geo_point

Elasticsearch nhận geo_point ở nhiều format. Tốt nhất chuẩn hoá một kiểu duy nhất để tránh nhầm.

// Object form (rõ ràng, ít sai)
{ "location": { "lat": 10.762, "lon": 106.682 } }

// String form (lat,lon)
{ "location": "10.762,106.682" }

// Array form (lon,lat). Lưu ý: ngược với GeoJSON spec
{ "location": [106.682, 10.762] }

// GeoJSON form
{ "location": { "type": "Point", "coordinates": [106.682, 10.762] } }

Lưu ý sống còn: array form và GeoJSON dùng thứ tự [lon, lat], trong khi object và string dùng lat trước lon. Một lần mình debug 4 tiếng vì sensor firmware gửi array [lat, lon] nhưng ES coi là [lon, lat]. Toàn bộ device “ở Châu Phi” thay vì Đông Nam Á. Fix bằng cách bắt firmware đổi sang object form và quy ước team chỉ dùng một format duy nhất.

Phần 3: Layer types và khi nào dùng

Maps cho phép stack nhiều layer chồng lên nhau. Mỗi layer có một mục đích riêng:

LayerUse caseData scale
DocumentsMỗi document = một markerDưới 10k điểm
HeatmapMật độ nóng/lạnh10k tới triệu điểm
Cluster (Grid aggregation)Gom điểm theo grid cellTriệu trở lên
ChoroplethTô màu theo vùng hành chínhCần shape boundary
TracksNối điểm thành đường (vehicle path)Cần device_id + timestamp

Cấu hình từ Analytics → Maps → Create map → Add layer.

Documents layer cho fleet nhỏ

Khi flotilla dưới 1000 xe, mỗi xe muốn nhìn rõ vị trí thì Documents layer là phù hợp. Trong cấu hình:

  • Data view: iot-telemetry-*
  • Geospatial field: location
  • Scaling: Limit results với top hits aggregation, sort @timestamp DESC, top size = 1 per device_id.

Top hits trick: thay vì hiển thị mọi document (mỗi device có vài nghìn data point trong window), chỉ lấy bản ghi mới nhất cho mỗi device. Maps có dropdown “Show top hits per entity” trong Source settings.

Heatmap cho mật độ

Hợp với customer behavior: hàng triệu request log có geo-IP, muốn biết khu vực nào click nhiều. Setup:

  • Layer type: Heat map
  • Source: geo_grid aggregation
  • Metric: count hoặc sum của một field số
  • Resolution: bắt đầu coarse, zoom in mới chuyển fine. finest rất nặng cho ES.

Tip: nếu trigger trên ES bị OOM, giảm thời gian window từ “Last 30 days” xuống “Last 24 hours” để bớt document phải scan.

Cluster cho big data

Khi document quá triệu, kể cả heatmap cũng chậm. Dùng Cluster với grid aggregation:

  • Layer type: Clusters and grids
  • Aggregation: geo_tile (chia thế giới thành ô vuông theo zoom level)
  • Mỗi ô hiển thị 1 marker, label = count

Ưu điểm: ES chỉ trả về N ô thay vì N điểm. Performance ổn định kể cả data trăm triệu dòng.

Choropleth cho báo cáo theo tỉnh

Muốn tô màu Việt Nam theo số lỗi mỗi tỉnh thì cần hai thứ:

  1. Boundary GeoJSON cho 63 tỉnh thành.
  2. Field trong document map được với tỉnh (ví dụ province_code).

Layer setup:

  • Type: Choropleth
  • Boundaries source: upload custom GeoJSON hoặc dùng Elastic Maps Service nếu có sẵn.
  • Join field: province_code ở cả hai bên (boundary và log).
  • Metric: count of records hoặc sum of errors.

Custom GeoJSON cho tỉnh Việt Nam có thể download từ các repo open data, sau đó upload qua Stack Management → Maps → Settings.

Phần 4: Time slider cho dữ liệu chuyển động

Map tĩnh chỉ cho biết hiện trạng. Map có time slider cho thấy fleet di chuyển ra sao, sensor nào fail dần, hotspot dịch chuyển thế nào.

Bật time slider:

  1. Trong Maps editor, panel bên trái, mục Layer settings, tick Apply global time to layer data.
  2. Trên thanh top, click icon đồng hồ kế bên time range → Show time slider.
  3. Drag slider, map sẽ filter document theo từng khoảng nhỏ.

Tip cho fleet tracking:

  • Dùng Tracks layer: chọn device_id làm split-by, sort theo @timestamp ASC. Maps sẽ vẽ đường đi mỗi xe.
  • Nếu đường zigzag bất thường, kiểm tra GPS noise filtering ở device. Đôi khi cần smoothing pre-processing trước khi index.

Phần 5: Pitfall storytelling

Mình gom 4 ca đã debug để cảnh báo trước:

Ca 1: Toàn bộ device “ở Châu Phi”

Sensor IoT gửi array [lat, lon] đúng theo lat-first convention nội bộ. Elasticsearch decode array là [lon, lat] theo GeoJSON. Result: tất cả device VN nhảy sang điểm (106, 10) của châu Phi. Fix: chuẩn hoá format object {lat, lon} ở edge gateway, đặt rule lint reject array form.

Ca 2: Heatmap “trống”

Dev mới mở Maps, chọn data view, add heatmap layer, không thấy gì. Debug: field location đang là object type với hai sub-field lat, lon float, không phải geo_point. Maps không list được. Fix: tạo index template, reindex hoặc tạo runtime field geo_point từ hai sub-field.

PUT iot-telemetry-2026.05/_mapping
{
  "runtime": {
    "location_geo": {
      "type": "geo_point",
      "script": {
        "source": "emit(doc['location.lat'].value, doc['location.lon'].value);"
      }
    }
  }
}

Runtime field giải quyết short-term, nhưng dài hạn vẫn phải fix mapping gốc vì runtime tính lúc query, không index, nên không aggregate được trên geo_grid lớn.

Ca 3: Timestamp lệch múi giờ

Dashboard hiển thị xe dừng từ 8h sáng tới 5h chiều, nhưng tracker bảo xe chạy cả ngày. Lý do: device gửi timestamp local (+07:00) nhưng không kèm offset, ES interpret là UTC, lệch 7 tiếng. Trong window “Last 24 hours” thì 7 tiếng đầu của ca không lọt. Fix: bắt device gửi ISO8601 với offset rõ ràng (2026-05-17T08:00:00+07:00) hoặc convert sang UTC trước khi index.

Ca 4: Cluster nhảy lung tung khi zoom

Cluster layer hiển thị 1 cụm “Hà Nội: 12000 device”, zoom in thì cụm tách thành 4 cụm nhỏ, mỗi cụm 3000 device. Tổng vẫn 12000. Nhưng marker dịch chuyển vị trí. Đây là expected behavior của geo_tile aggregation: mỗi zoom level chia grid lại, center của cụm là tâm hình học của các điểm trong ô, không phải vị trí cố định. Solution: nếu user phàn nàn về “device nhảy”, chuyển sang geohex_grid (kết quả ổn định hơn) hoặc dùng Documents layer khi zoom đủ cao.

Phần 6: Combine với dashboard và alert

Maps không sống một mình. Pattern phổ biến:

  • Embed map vào dashboard cùng với Lens chart và table.
  • Filter chéo: click một marker trên map → toàn bộ dashboard filter theo device_id.
  • Alert có geo condition: trigger khi sensor di chuyển ra khỏi geofence.

Geofence alert qua API:

curl -s -u "$KB_USER:$KB_PASS" \
  -H "kbn-xsrf: true" \
  -H "Content-Type: application/json" \
  -X POST "$KIBANA_URL/api/alerting/rule" \
  -d '{
    "name": "Vehicle exit geofence",
    "rule_type_id": ".geo-containment",
    "consumer": "alerts",
    "schedule": {"interval": "1m"},
    "params": {
      "index": "iot-telemetry-*",
      "indexId": "<DATA_VIEW_ID>",
      "geoField": "location",
      "entity": "device_id",
      "dateField": "@timestamp",
      "boundaryType": "entireIndex",
      "boundaryIndexTitle": "warehouses",
      "boundaryGeoField": "shape"
    },
    "actions": []
  }'

Rule type .geo-containment cần một index thứ hai chứa polygon ranh giới (warehouses index với field shape: geo_shape). Mỗi khi device thoát khỏi polygon hoặc bước vào, rule trigger.

Phần 7: Performance và cost

Maps là một trong những view tốn ES nhất. Vài rule of thumb:

SymptomFix
Map mất 30s mới loadGiảm time range, dùng cluster thay vì documents
ES OOM khi pan/zoomTăng heap, hoặc dùng geo_tile agg với precision thấp
Heatmap chậm khi zoom inSet request precision trong layer settings: limit max precision
Browser lag với 50k markerĐổi sang Cluster layer, hoặc top hits filter

Quy tắc cá nhân: nếu data point trên 100k trong time window thì không bao giờ dùng Documents layer raw. Luôn aggregate trước.

Cheatsheet

ViệcCách làm
Map field thành geo_pointIndex template với "type": "geo_point"
Chuẩn format toạ độObject {lat, lon}, tránh array (dễ đảo)
Hiển thị fleet < 1000 deviceDocuments layer + top hits per device_id
Mật độ user (geo-IP)Heatmap với geo_grid aggregation
Big data (triệu điểm)Cluster/grid layer
Theo tỉnh thànhChoropleth với boundary GeoJSON
Xem chuyển độngTime slider + Tracks layer
Geofence alertRule type .geo-containment
Runtime fix (không reindex)Runtime field emit geo_point

Lời kết

Maps trong Kibana không phải replacement cho Google Maps. Nó là layer geospatial cho dữ liệu đã có sẵn trong Elasticsearch. Nắm được cách map field, chọn layer phù hợp với scale, và xử lý đúng pitfall về format toạ độ là đủ để dựng dashboard fleet hoặc telemetry chuyên nghiệp trong vài giờ.

Bài tiếp theo trong series Kibana từ A đến Z sẽ đi vào những pitfall hay gặp khi visualize: aggregation đếm trùng, time bucket lệch, terms aggregation cắt mất nhóm nhỏ. Đây là phần làm dashboard “trông đúng” nhưng thật ra sai số liệu. Nếu bạn đang dựng map cho IoT hoặc telemetry mà gặp tình huống lạ, cứ comment chia sẻ; mình rất hứng thú với edge case của domain này.