Andrew Mercer
on this page
Tool Where Best for
KQL Kibana query bar (Discover, dashboards) Quick interactive filtering
Query DSL Dev Tools, the _search API, applications Full control: scoring, bool logic, everything
Aggregations Inside a _search body Summaries such as counts, top-N, histograms
ES|QL Discover (ES|QL mode), Dev Tools, the _query API Piped queries with transforms, stats and joins

The examples use the OPNsense data stream from the ingest section.

Kibana query bar (KQL)

observer.hostname : "fw01" and event.action : "block"
destination.port : (22 or 3389) and not source.ip : 10.0.0.0/8
process.name : filter*
message : "link state changed"

KQL only filters. Time range, sorting and aggregations come from the Kibana UI around it.

Query DSL

GET logs-opnsense-*/_search
{
  "size": 20,
  "sort": [{ "@timestamp": "desc" }],
  "_source": ["@timestamp", "source.ip", "destination.ip", "destination.port"],
  "query": {
    "bool": {
      "filter": [
        { "term":  { "event.action": "block" } },
        { "range": { "@timestamp": { "gte": "now-24h" } } }
      ],
      "must_not": [
        { "term": { "source.ip": "10.0.0.0/8" } }
      ]
    }
  }
}
  • Use filter for exact yes/no conditions: they're cached and don't affect scoring. Use must only when relevance ranking matters.
  • term is for keyword, ip, numeric and date fields. match is for analyzed text.

Aggregations

Use an aggregation when the question is about a set of documents ("which source IPs are blocked most?") rather than about individual documents. Set "size": 0 to skip returning hits.

GET logs-opnsense-*/_search
{
  "size": 0,
  "query": { "range": { "@timestamp": { "gte": "now-7d" } } },
  "aggs": {
    "top_blocked_sources": {
      "terms": { "field": "source.ip", "size": 10 }
    },
    "per_hour": {
      "date_histogram": { "field": "@timestamp", "fixed_interval": "1h" }
    }
  }
}

ES|QL

ES|QL is a piped query language: each | stage transforms the previous result.

FROM logs-opnsense-*
| WHERE @timestamp > NOW() - 24 hours AND event.action == "block"
| STATS hits = COUNT(*) BY source.ip
| SORT hits DESC
| LIMIT 10

From Dev Tools or curl:

POST _query?format=txt
{
  "query": "FROM logs-opnsense-* | STATS hits = COUNT(*) BY destination.port | SORT hits DESC | LIMIT 10"
}