Discover là cổng vào Kibana mà ai cũng biết — mở lên, gõ KQL, đọc log. Nhưng lúc gặp log schema “sai ý” (field bị encode, thiếu field cần filter), hoặc phải lọc object lồng nhau, nhiều dev nghĩ đến phương án nặng nề là reindex hoặc sửa pipeline ingestion. Thực tế Kibana cho bạn hàng loạt tính năng để xử tại chỗ, không đụng đến index — nhanh hơn và an toàn hơn nhiều.

Đây là bài thứ 3 trong series Kibana từ A đến Z. Sau bài này bạn sẽ:

  • Tạo được Runtime field bằng Painless mà không cần reindex
  • Filter nested object, mảng JSON, và dùng regex trong Discover
  • Bật highlighting để mắt tự bắt từ khoá trong log dài
  • Phân biệt Saved Query và Saved Search, chọn đúng loại để team reuse
  • Dùng Inspect request để debug KQL và tối ưu query chậm

Phần 1: Runtime field — tạo field ảo on-the-fly

Runtime field là field tính lúc query, không lưu trong index. Đây là workaround nhanh nhất khi schema thiếu field cần filter hoặc cần derive.

Use case thực tế

  • Tách hour_of_day từ @timestamp để tìm pattern traffic theo giờ.
  • Extract subdomain từ URL (https://api.example.com/...api).
  • Combine 2 field: user_id + tenant_id thành key duy nhất.
  • Mask PII: thay 4 số cuối của phone bằng ****.
  • Normalize case: lowercase(service_name) khi logger sinh ra hỗn hợp.

Tạo qua Data View Management (persistent)

  • ≡ → Management → Stack Management → Data Views → chọn data view → Add field.
  • Đặt Name: hour_of_day.
  • Type: Long.
  • Set value: bật → paste Painless script:
emit(doc['@timestamp'].value.getHour());
  • Save. Từ giờ field hour_of_day xuất hiện trong list field Discover, filter được như field thường: hour_of_day >= 9 and hour_of_day < 17.

Tạo ad-hoc (chỉ trong session hiện tại)

Discover → button + Add field ngay cạnh field list → điền tương tự. Field chỉ tồn tại trong tab đang mở, đóng là mất. Tiện để thử nghiệm.

Painless script mẫu

Extract subdomain từ URL:

if (doc['url.keyword'].size() == 0) return;
String url = doc['url.keyword'].value;
int start = url.indexOf("://") + 3;
int end = url.indexOf(".", start);
if (end > start) emit(url.substring(start, end));

Mask 4 số cuối phone:

if (doc['phone.keyword'].size() == 0) return;
String p = doc['phone.keyword'].value;
if (p.length() > 4) emit(p.substring(0, p.length() - 4) + "****");

Combine 2 field:

if (doc['tenant_id.keyword'].size() == 0 || doc['user_id.keyword'].size() == 0) return;
emit(doc['tenant_id.keyword'].value + ":" + doc['user_id.keyword'].value);

Trade-off phải biết

  • Chậm hơn field thường. Runtime field tính lại mỗi query. Trên index 100M docs, filter theo runtime field có thể chậm 5-10x so với field indexed.
  • Không dùng trong aggregation nặng. STATS trên runtime field của ES|QL chạy được nhưng không scale.
  • Script lỗi = document bị skip, không lỗi query. Debug khó — kiểm tra script bằng Dev Tools trước khi save.
  • Giải pháp dài hạn: nếu runtime field được dùng thường xuyên → backfill bằng update_by_query vào field thật, hoặc thêm vào pipeline ingestion.

Phần 2: Filter phức tạp

Nested object vs dot notation

Elasticsearch có 2 cách lưu object lồng:

  • Flatten (mặc định): user.name, user.email → index như field riêng. Dot notation trong KQL đọc thẳng: user.name : "alice".
  • Nested type: mảng object giữ quan hệ 1-1 giữa các field trong cùng phần tử. Cần toán tử nested đặc biệt.

Mapping nested:

{
  "properties": {
    "comments": {
      "type": "nested",
      "properties": { "author": {"type": "keyword"}, "score": {"type": "integer"} }
    }
  }
}

Filter comments.author == "alice" AND comments.score > 5 cùng 1 comment với flatten sẽ ra sai (match bất kỳ author alice + bất kỳ score > 5 trong cả array). Cần nested query DSL:

{
  "nested": {
    "path": "comments",
    "query": {
      "bool": {
        "must": [
          {"term": {"comments.author": "alice"}},
          {"range": {"comments.score": {"gt": 5}}}
        ]
      }
    }
  }
}

Trong KQL (8.x hỗ trợ nested):

comments: { author : "alice" and score > 5 }

Cú pháp field : { ... } giữ constraint trong cùng phần tử.

Regex filter

KQL hỗ trợ regex qua /pattern/:

Message : /timeout \d{4,}ms/

Lưu ý: regex chạy trên text field đã analyzed không phải raw. Muốn regex exact → Message.keyword:

Message.keyword : /timeout \d{4,}ms/

Regex trên index lớn rất chậm. Tránh regex cho filter thường xuyên — dùng wildcard hoặc runtime field đã normalize.

Filter theo script (Dev Tools)

Khi KQL không đủ, dùng ES Query DSL trong Dev Tools hoặc filter script trong GUI:

Discover → Add filter → Edit as Query DSL:

{
  "script": {
    "script": {
      "source": "doc['response_time_ms'].value > params.threshold",
      "params": { "threshold": 1000 }
    }
  }
}

Script filter không dùng index — cực chậm trên dữ liệu lớn. Chỉ hợp khi không thể viết bằng KQL/range query.

Phần 3: Highlighting

Mặc định Discover không highlight từ khoá search. Khi log dài vài kilobyte, tìm đoạn match bằng mắt rất mỏi.

Bật highlighting

Kibana 8.x bật mặc định trên hầu hết data view. Nếu không thấy highlight:

  • Advanced Settings → doc_table:highlight → set true.
  • Reload Discover.

Custom tag

Mặc định Kibana dùng <mark> CSS. Muốn đổi màu:

/* user CSS trong Space Settings hoặc deploy custom */
.kbnDocViewer__buttonGroup mark {
  background: #fef08a;
  color: #854d0e;
  padding: 0 2px;
  border-radius: 2px;
}

Highlight trong ES|QL

ES|QL chưa hỗ trợ highlight native (8.14). Muốn xem match trong bảng ES|QL, dùng EVAL + SUBSTRING để trích đoạn quanh match, hoặc quay về Discover cho view chi tiết.

Document context

Click 1 row trong Discover → View surrounding documents. Kibana sẽ hiện 5 doc trước + 5 doc sau theo timestamp, hữu ích để trace bối cảnh xung quanh 1 error cụ thể.

Hai khái niệm hay nhầm:

Saved QuerySaved Search
Lưu cái gìChỉ KQL + filtersKQL + filters + columns + sort + data view + time range
Phạm viDùng lại query ở nhiều Saved Search / DashboardMở Discover là thấy nguyên view
Tạo từMenu “Saved queries” trong search barButton Save ở góc phải Discover
Embed vào dashboardKHÔNG (chỉ apply filter)CÓ (Add from library)

Khi nào dùng Saved Query

Khi cùng 1 query filter được dùng ở nhiều nơi:

  • Dashboard Errors dùng Level : ("Error" or "Fatal").
  • Dashboard Tenant A dùng cùng filter trên tenant A.
  • Alert rule dùng cùng filter.

Tạo 1 Saved Query “Production Errors”, áp dụng mọi nơi → sửa 1 chỗ, các nơi update theo.

Khi team cần 1 view đầy đủ để mở nhanh: cột cụ thể, sort cụ thể, time range ngầm định. Như bài 1 đã làm với Saved Search “All Errors”.

Tạo Saved Query

  • Discover → gõ KQL → click icon disc cạnh search bar → Save current query.
  • Đặt Title, description, chọn Include filters + Include time filter nếu muốn.

Phần 5: Inspect request — debug KQL như pro

Discover có menu ẩn cho phép xem ES Query DSL Kibana thực sự gửi đi. Dùng khi:

  • KQL ra 0 kết quả mà không rõ sao.
  • Query chậm bất thường.
  • Cần copy DSL để dùng ở ES client khác (Python, Java).

Cách mở

  • Trong Discover, click nút Inspect (góc phải trên) hoặc menu ba chấm → Inspect.
  • Tab Request hiển thị DSL. Tab Response hiển thị raw ES response.
  • Tab Statisticstook (thời gian), hits.total, shard info.

Checklist khi query chậm

  1. Mở Inspect → Statistics → xem took (ms).
  2. Nếu took > 1s: copy DSL sang Dev Tools → Profile (POST /<index>/_search?_profile=true).
  3. Xem profile.shards[].searches[].query[] để biết phase nào chậm.
  4. Dấu hiệu thường gặp:
    • wildcard query trên large text field → đổi sang match hoặc thêm keyword sub-field.
    • Scripted query → thay bằng runtime field indexed.
    • Nhiều should clause → gom lại terms query.

Ví dụ: tối ưu filter chậm

KQL gốc:

Message : *timeout* and Level : "Error"

DSL sau khi inspect:

{
  "query": {
    "bool": {
      "must": [
        {"query_string": {"query": "*timeout*", "default_field": "Message"}},
        {"term": {"Level.keyword": "Error"}}
      ]
    }
  }
}

Wildcard leading *timeout* tắt toàn bộ inverted index optimization — ES phải scan từng doc. Thay bằng:

Message : "timeout" and Level : "Error"

(phrase match dùng index được) hoặc thêm field has_timeout (runtime hoặc indexed) để filter boolean.

Cheatsheet

Nhu cầuCông cụCách làm
Field không tồn tại trong indexRuntime fieldPainless emit(...), trade-off chậm hơn
Filter object lồng nhauNested KQLfield: { subfield: value }
Regex exact.keyword + /pattern/Tránh trên index lớn
Script filter phức tạpAdd filter → Edit as Query DSLRất chậm, last resort
Highlight từ khoáAdvanced Settingsdoc_table:highlight = true
Document contextRow → View surroundingTrace bối cảnh quanh error
Query reusable nhiều nơiSaved QueryDisc icon cạnh search bar
View Discover đầy đủSaved SearchButton Save góc phải
Debug KQL ra 0 kết quảInspect → RequestXem DSL thực
Query chậmInspect → Statistics → Profile APITìm wildcard, script filter

Lời kết

Khi Discover “không chịu làm theo ý”, 90% case không cần reindex — runtime field xử lý field thiếu, nested KQL xử lý array object, inspect request chỉ ra lý do query chậm. Dev backend nắm được 5 kỹ thuật này sẽ tự chủ với log schema khó, không còn phải chờ DevOps sửa pipeline ingestion cho những việc tương đối nhỏ.

Bài tiếp theo trong series Kibana từ A đến Z sẽ bước sang nhóm Visualization: dùng Lens để dựng chart từ drag-drop cho tới Formula phức tạp (error rate, SLO burn rate, time-shift so sánh tuần trước), annotation layer cho deploy marker, và reference line cho threshold. Nếu có topic Discover cụ thể bạn muốn mình viết sâu hơn — ví dụ “runtime field hay indexed field: benchmark thực tế”, “migrate từ Lucene sang KQL”, “Discover với data stream và time series” — cứ drop comment hoặc email.