ELK Stack — Bool Queries

8 minute read

Introduction

Real-world search requirements are rarely satisfied by a single query clause. You need to combine conditions — “find error logs from the last hour that are NOT from the health-check endpoint.” The bool query is the workhorse that makes this possible.

A bool query lets you combine any number of query clauses using boolean logic, with full control over which clauses affect scoring and which act as filters.

Bool Query Structure

The bool query supports four clause types:

┌──────────────────────────────────────────────────────────┐
│                     Bool Query                           │
├──────────────┬───────────────────────────────────────────┤
│  must        │ All clauses MUST match (AND)              │
│              │ → Contributes to score                    │
├──────────────┼───────────────────────────────────────────┤
│  should      │ At least one SHOULD match (OR)            │
│              │ → Contributes to score                    │
│              │ → Optional if must/filter present         │
├──────────────┼───────────────────────────────────────────┤
│  must_not    │ Clauses MUST NOT match (NOT)              │
│              │ → Filter context (no scoring)             │
│              │ → Results are cached                      │
├──────────────┼───────────────────────────────────────────┤
│  filter      │ Clauses MUST match (AND)                  │
│              │ → Filter context (no scoring)             │
│              │ → Results are cached                      │
├──────────────┴───────────────────────────────────────────┤
│  Scoring:  must + should = final _score                  │
│            filter + must_not = no score contribution      │
└──────────────────────────────────────────────────────────┘

Basic Bool Query

Let’s find GET requests that had errors (status >= 400):

curl -s -X GET "localhost:9200/web-logs/_search?pretty" -H 'Content-Type: application/json' -d '{
  "query": {
    "bool": {
      "must": [
        { "term": { "method": "GET" } }
      ],
      "filter": [
        { "range": { "status_code": { "gte": 400 } } }
      ]
    }
  }
}'
{
  "took": 2,
  "hits": {
    "total": { "value": 1, "relation": "eq" },
    "max_score": 0.35667494,
    "hits": [
      {
        "_score": 0.35667494,
        "_source": {
          "method": "GET",
          "url": "/api/users/42",
          "status_code": 404,
          "message": "User not found error occurred"
        }
      }
    ]
  }
}

The must clause (method = GET) contributes to scoring. The filter clause (status >= 400) does not affect the score but is cached for performance.

The must Clause

All clauses inside must are required. They operate in query context (affect scoring).

curl -s -X GET "localhost:9200/web-logs/_search?pretty" -H 'Content-Type: application/json' -d '{
  "query": {
    "bool": {
      "must": [
        { "match": { "message": "user" } },
        { "range": { "response_time": { "gt": 100 } } }
      ]
    }
  }
}'
{
  "took": 3,
  "hits": {
    "total": { "value": 3, "relation": "eq" },
    "max_score": 1.6931472,
    "hits": [
      {
        "_score": 1.6931472,
        "_source": { "message": "New user created successfully", "response_time": 350.2 }
      },
      {
        "_score": 1.6931472,
        "_source": { "message": "Successfully fetched user list", "response_time": 120.5 }
      },
      {
        "_score": 1.6931472,
        "_source": { "message": "Order history fetched for user", "response_time": 210.7 }
      }
    ]
  }
}

The should Clause

should clauses are optional boosters — they increase the score of matching documents but are not required.

Important behavior:

  • If the bool query has no must or filter clauses, then at least one should clause must match.
  • If must or filter is present, should clauses are purely optional (score boosters).
curl -s -X GET "localhost:9200/web-logs/_search?pretty" -H 'Content-Type: application/json' -d '{
  "query": {
    "bool": {
      "must": [
        { "match": { "message": "user" } }
      ],
      "should": [
        { "term": { "status_code": 200 } },
        { "match": { "message": "error" } }
      ]
    }
  }
}'
{
  "took": 2,
  "hits": {
    "total": { "value": 4, "relation": "eq" },
    "max_score": 2.4849067,
    "hits": [
      {
        "_score": 2.4849067,
        "_source": { "message": "User not found error occurred", "status_code": 404 }
      },
      {
        "_score": 1.6931472,
        "_source": { "message": "Successfully fetched user list", "status_code": 200 }
      },
      {
        "_score": 0.6931471,
        "_source": { "message": "New user created successfully", "status_code": 201 }
      },
      {
        "_score": 0.5753642,
        "_source": { "message": "Order history fetched for user", "status_code": 200 }
      }
    ]
  }
}

All four documents match the must clause (“user” in message). Documents that also match a should clause get a score boost.

The must_not Clause

Excludes documents matching any of the must_not clauses. Runs in filter context — no scoring, results are cached.

curl -s -X GET "localhost:9200/web-logs/_search?pretty" -H 'Content-Type: application/json' -d '{
  "query": {
    "bool": {
      "must": [
        { "match_all": {} }
      ],
      "must_not": [
        { "term": { "method": "GET" } },
        { "term": { "method": "POST" } }
      ]
    }
  }
}'
{
  "took": 1,
  "hits": {
    "total": { "value": 1, "relation": "eq" },
    "max_score": 1.0,
    "hits": [
      {
        "_score": 1.0,
        "_source": {
          "method": "DELETE",
          "url": "/api/users/7",
          "status_code": 500
        }
      }
    ]
  }
}

The filter Clause

Works like must (all clauses required), but runs in filter context — no scoring, results are cached. Use filter when you don’t care about relevance ranking.

curl -s -X GET "localhost:9200/web-logs/_search?pretty" -H 'Content-Type: application/json' -d '{
  "query": {
    "bool": {
      "filter": [
        { "term": { "method": "GET" } },
        { "range": { "response_time": { "lt": 200 } } },
        { "range": { "@timestamp": { "gte": "2026-04-01T08:00:00Z", "lt": "2026-04-01T09:00:00Z" } } }
      ]
    }
  }
}'
{
  "took": 1,
  "hits": {
    "total": { "value": 3, "relation": "eq" },
    "max_score": 0.0,
    "hits": [
      { "_score": 0.0, "_source": { "method": "GET", "url": "/api/users", "response_time": 120.5 } },
      { "_score": 0.0, "_source": { "method": "GET", "url": "/api/products", "response_time": 95.3 } },
      { "_score": 0.0, "_source": { "method": "GET", "url": "/api/users/42", "response_time": 15.1 } }
    ]
  }
}

All scores are 0.0 because everything runs in filter context.

minimum_should_match

Controls how many should clauses must match when there are no must or filter clauses:

curl -s -X GET "localhost:9200/web-logs/_search?pretty" -H 'Content-Type: application/json' -d '{
  "query": {
    "bool": {
      "should": [
        { "match": { "message": "error" } },
        { "match": { "message": "user" } },
        { "range": { "status_code": { "gte": 400 } } }
      ],
      "minimum_should_match": 2
    }
  }
}'
{
  "took": 2,
  "hits": {
    "total": { "value": 2, "relation": "eq" },
    "max_score": 2.9549103,
    "hits": [
      {
        "_score": 2.9549103,
        "_source": { "message": "User not found error occurred", "status_code": 404 }
      },
      {
        "_score": 2.5404451,
        "_source": { "message": "Internal server error during delete operation", "status_code": 500 }
      }
    ]
  }
}

Only documents matching at least 2 of the 3 should clauses are returned.

Nested Bool Queries

Bool queries can be nested inside other bool queries for complex logic:

Scenario: Find documents where:

  • Method is GET or POST (group 1)
  • AND status code is 200 or message contains “error” (group 2)
curl -s -X GET "localhost:9200/web-logs/_search?pretty" -H 'Content-Type: application/json' -d '{
  "query": {
    "bool": {
      "must": [
        {
          "bool": {
            "should": [
              { "term": { "method": "GET" } },
              { "term": { "method": "POST" } }
            ]
          }
        },
        {
          "bool": {
            "should": [
              { "term": { "status_code": 200 } },
              { "match": { "message": "error" } }
            ]
          }
        }
      ]
    }
  }
}'
{
  "took": 3,
  "hits": {
    "total": { "value": 4, "relation": "eq" },
    "max_score": 1.5498152,
    "hits": [
      { "_source": { "method": "GET", "status_code": 404, "message": "User not found error occurred" } },
      { "_source": { "method": "GET", "status_code": 200, "url": "/api/users" } },
      { "_source": { "method": "GET", "status_code": 200, "url": "/api/products" } },
      { "_source": { "method": "POST", "status_code": 201, "message": "New user created successfully" } }
    ]
  }
}

Boosting

You can increase the importance of specific clauses using the boost parameter:

curl -s -X GET "localhost:9200/web-logs/_search?pretty" -H 'Content-Type: application/json' -d '{
  "query": {
    "bool": {
      "should": [
        {
          "match": {
            "message": {
              "query": "error",
              "boost": 3
            }
          }
        },
        {
          "match": {
            "message": {
              "query": "user",
              "boost": 1
            }
          }
        }
      ]
    }
  }
}'
{
  "took": 2,
  "hits": {
    "total": { "value": 5, "relation": "eq" },
    "max_score": 4.1684057,
    "hits": [
      {
        "_score": 4.1684057,
        "_source": { "message": "User not found error occurred" }
      },
      {
        "_score": 3.4893353,
        "_source": { "message": "Internal server error during delete operation" }
      },
      {
        "_score": 0.6931471,
        "_source": { "message": "Successfully fetched user list" }
      }
    ]
  }
}

Documents with “error” get a much higher score due to the boost: 3 multiplier.

function_score Basics

For more advanced scoring control, function_score wraps a query and modifies scores using functions:

curl -s -X GET "localhost:9200/web-logs/_search?pretty" -H 'Content-Type: application/json' -d '{
  "query": {
    "function_score": {
      "query": {
        "match_all": {}
      },
      "functions": [
        {
          "filter": { "range": { "status_code": { "gte": 500 } } },
          "weight": 10
        },
        {
          "filter": { "range": { "status_code": { "gte": 400, "lt": 500 } } },
          "weight": 5
        }
      ],
      "score_mode": "sum",
      "boost_mode": "replace"
    }
  }
}'
{
  "took": 3,
  "hits": {
    "total": { "value": 6, "relation": "eq" },
    "max_score": 10.0,
    "hits": [
      { "_score": 10.0, "_source": { "status_code": 500, "message": "Internal server error during delete operation" } },
      { "_score": 5.0, "_source": { "status_code": 404, "message": "User not found error occurred" } },
      { "_score": 1.0, "_source": { "status_code": 200, "url": "/api/users" } },
      { "_score": 1.0, "_source": { "status_code": 201, "url": "/api/users" } },
      { "_score": 1.0, "_source": { "status_code": 200, "url": "/api/products" } },
      { "_score": 1.0, "_source": { "status_code": 200, "url": "/api/orders" } }
    ]
  }
}

Key function_score parameters:

Parameter Description
score_mode How function scores combine: multiply, sum, avg, first, max, min
boost_mode How function score combines with query score: multiply, replace, sum, avg, max, min

Real-World Example: Log Search Dashboard

Here’s a practical bool query combining everything — the kind you’d use behind a log search dashboard:

curl -s -X GET "localhost:9200/web-logs/_search?pretty" -H 'Content-Type: application/json' -d '{
  "query": {
    "bool": {
      "must": [
        { "match": { "message": "error" } }
      ],
      "filter": [
        { "range": { "@timestamp": { "gte": "now-24h" } } },
        { "terms": { "method": ["GET", "POST", "DELETE"] } }
      ],
      "must_not": [
        { "wildcard": { "url.raw": "/api/health*" } }
      ],
      "should": [
        { "range": { "status_code": { "gte": 500, "boost": 5 } } }
      ]
    }
  },
  "sort": [
    { "@timestamp": "desc" }
  ],
  "size": 20
}'

This query:

  1. Must contain “error” in the message (scored)
  2. Filter to last 24 hours and specific methods (cached, no scoring)
  3. Exclude health-check endpoints
  4. Boost 5xx errors higher in results

Quick Reference

Clause Required? Scoring? Cached? Use For
must Yes (AND) Yes No Conditions that should affect ranking
should Depends Yes No Optional boosters or OR conditions
must_not No (NOT) No Yes Excluding documents
filter Yes (AND) No Yes Required conditions without scoring
minimum_should_match — — — Control how many should clauses must match
boost — — — Increase importance of a clause
function_score — Yes — Custom scoring functions

Next: Aggregations — Analyze data with metrics, buckets, and pipeline aggregations.