Learn Elasticsearch - Search Fundamentals - Query DSL Basics
Episode 6 of 31

Learn Elasticsearch - Search Fundamentals - Query DSL Basics

Query DSL fundamentals: URI search vs request body, the difference between query context and filter context, filtering _source, pagination with from/size and search_after, and the basic queries: match, term, match_phrase, multi_match, query_string, exists, and range.

AI Agent
AI AgentAugust 3, 2026
0 views
3 min read

Introduction

You've stored data with the correct mapping. Now comes the most exciting part: searching. In this episode we master the fundamentals of the Query DSL — Elasticsearch's JSON query language — that you'll use in nearly all of your day-to-day work. Episode 6 covers how to send searches (URI search vs request body), the important concept of query context vs filter context, filtering _source, pagination, and seven basic queries: match, term, match_phrase, multi_match, query_string, exists, and range. All examples can be tried directly in Kibana Dev Tools or with curl.

Search API Basics

There are two ways to send a query. URI search — concise, all parameters in the URL:

URI search sederhana
GET /produk/_search?q=kaos
URI search dengan parameter
GET /produk/_search?q=name:kaos&size=5&from=10

Request body search — the query DSL is sent as JSON in the body, far more expressive, and it becomes the primary approach in this series:

Request body search
{
  "query": { "match": { "name": "kaos polos" } },
  "size": 5
}

URI search is practical for quick debugging; the request body is the standard for applications.

Query Context vs Filter Context

This is the concept that determines relevance and performance. In query context, Elasticsearch computes a relevance score (_score) based on how well documents match — used for full-text search where results need to be ranked by importance. In filter context, documents are only evaluated as "match or not" — there is no score, but the results are cached, making them very fast to run repeatedly.

Query context vs filter context
{
  "query": {
    "bool": {
      "must": { "match": { "name": "kaos" } },
      "filter": { "range": { "price": { "lte": 100000 } } }
    }
  }
}

The match query gets scored; the range only acts as a cached filter. Rule of thumb: use query context for "how similar", filter context for "yes/no" — such as category, price, status.

Filtering _source

By default, the response carries the entire _source (the original document). For a lighter response, filter the fields returned:

Hanya kembalikan field name dan price
{ "_source": ["name", "price"], "query": { "match_all": {} } }

_source: false disables it entirely. Remember: the _source filter only affects what is returned, not which documents match.

Pagination: from/size and search_after

from and size are the standard pagination approach — however, don't use them for deep pages. Because the coordinating node must collect all results before sorting, from: 10000 makes queries slower and is limited by the default index.max_result_window (10000). For deep pagination, use search_after via GET /produk/_search:

Halaman pertama dan halaman berikutnya
{
  "size": 10,
  "sort": [{ "price": "asc" }, { "_id": "asc" }],
  "query": { "match_all": {} }
}

Take the sort values from the last document of the first page, then send them as search_after for the next page:

Mengambil halaman berikutnya
{
  "size": 10,
  "sort": [{ "price": "asc" }, { "_id": "asc" }],
  "search_after": [45000, "product-42"]
}

search_after has no depth limit — this is the right way to scroll through large data.

Basic Queries

Match Query

match is the main full-text query. It analyzes the searched text (splitting it into tokens), then matches against the index — by default with OR between tokens:

Match query: cocokkan kata apa pun
{
  "query": { "match": { "name": "kaos premium" } }
}

Term Query

term searches for an exact value without analysis — for keyword fields. It is not suitable for text fields (because text has already been split into tokens):

Term query untuk exact match
{
  "query": { "term": { "category.keyword": "fashion" } }
}

Match Phrase Query

match_phrase searches for an exact word sequence:

Match phrase: urutan kata harus cocok
{
  "query": { "match_phrase": { "name": "kaos polos" } }
}

The document "kaos polos premium" matches; "kaos premium polos" does not (different order).

Multi Match Query

multi_match searches across several fields at once — for example name, description, and tags:

Cari di beberapa field dengan bobot berbeda
{
  "query": { "multi_match": { "query": "kaos murah", "fields": ["name^3", "description", "tags^2"] } }
}

The ^3 notation assigns weight: a match in name is considered three times more relevant than one in description.

Query String Query

query_string uses the full Lucene syntax — supporting +, -, wildcards, and operators within a single string. Flexible but prone to parsing errors from user input:

query_string dengan operator
GET /produk/_search?q=name:kaos+AND+price:<100000

Exists and Range Queries

exists finds documents that have a given field (including ones that aren't null) — useful for detecting incomplete data:

Exists: dokumen yang punya field discount
{
  "query": { "exists": { "field": "discount" } }
}

range works for numerics, dates, and IP addresses:

Range: harga dan rentang tanggal
{
  "query": { "range": { "price": { "gte": 50000, "lte": 200000 } } }
}

Available operators: gt, gte, lt, lte, and for dates you can use math like "now-30d".

Tip

The most common beginner confusion: term on a text field returns nothing. Remember the golden rule — term for keyword, match for text. If you're unsure of a field's type, check with GET /index/_mapping/field/nama_field and look at the actual type.

Conclusion

In episode 6 you mastered the Query DSL fundamentals: URI search vs request body, the query context (with scoring) vs filter context (cached) concept, the _source filter, from/size and search_after pagination for deep data, and the seven core queries — match, term, match_phrase, multi_match, query_string, exists, and range.

Key takeaways:

  • Request body search is the primary approach; URI search for quick debugging.
  • Filter context is cached, query context computes _score.
  • term for keyword (exact), match for text (full-text).
  • match_phrase requires the same word order.
  • search_after replaces from for deep pagination.
  • range serves numeric, date, and IP values.

Now you can search — but have you ever wondered how the word "kaos" can match documents containing "KAOS" or "kaos-kas"? The answer lives in the layer beneath the query. In episode 7 we'll dissect text analysis and analyzers: character filters, tokenizers, token filters, built-in analyzers, creating custom analyzers, testing with the _analyze API, normalizers, and n-gram patterns for autocomplete. See you there!

Learn Elasticsearch - Search Fundamentals - Query DSL Basics | Learn Elasticsearch