Data ingestion and query

Updated at:

This topic describes how to ingest data into the PolarDB-X Search engine and how to run full-text search, vector search (KNN), hybrid queries, and aggregations.

Data ingestion

Write a single document

# Write with a specified document ID
curl -XPUT "https://<Search engine endpoint>:9200/products/_doc/1" \
  -u "<username>:<password>" -k \
  -H "Content-Type: application/json" \
  -d '{
    "name":        "Wireless Bluetooth headphones",
    "description": "Premium noise-canceling Bluetooth headphones, 30-hour battery life",
    "price":       299.0,
    "category":    "Electronics",
    "tags":        ["Bluetooth","Noise canceling","Headphones"],
    "in_stock":    true,
    "created_at":  "2025-06-01"
  }'

# Auto-generate the document ID
curl -XPOST "https://<Search engine endpoint>:9200/products/_doc" \
  -u "<username>:<password>" -k \
  -H "Content-Type: application/json" \
  -d '{ "name": "Mechanical keyboard", "price": 499.0, "category": "Electronics" }'

Batch write (_bulk API)

The _bulk API executes multiple write operations in a single request, which significantly improves write throughput:

curl -XPOST "https://<Search engine endpoint>:9200/_bulk" \
  -u "<username>:<password>" -k \
  -H "Content-Type: application/json" \
  -d '
{"index": {"_index": "products", "_id": "3"}}
{"name": "Smart watch", "price": 899.0, "category": "Wearables"}
{"index": {"_index": "products", "_id": "4"}}
{"name": "Power bank", "price": 129.0, "category": "Electronics"}
'

Batch write recommendations

  • Bulk size per request: 5-15 MB. Requests that are too large put memory pressure on the cluster.

  • Documents per request: 1,000 to 5,000, depending on the size of each document.

  • Concurrent write threads: 2 to 4, based on the number of CPU cores on each node.

  • During the write, set refresh_interval to 30s or -1, and restore it to 1s after the write completes.

Update and delete

# Partial update
curl -XPOST "https://<Search engine endpoint>:9200/products/_update/1" \
  -u "<username>:<password>" -k -H "Content-Type: application/json" \
  -d '{ "doc": { "price": 259.0, "in_stock": false } }'

# Delete a single document
curl -XDELETE "https://<Search engine endpoint>:9200/products/_doc/1" -u "<username>:<password>" -k

# Delete by query
curl -XPOST "https://<Search engine endpoint>:9200/products/_delete_by_query" \
  -u "<username>:<password>" -k -H "Content-Type: application/json" \
  -d '{ "query": { "term": { "in_stock": false } } }'

Full-text search

match query

The most common full-text search query. The query text is tokenized and then matched:

{
  "query": {
    "match": {
      "description": {
        "query": "noise canceling Bluetooth",
        "operator": "and",
        "minimum_should_match": "75%"
      }
    }
  }
}

multi_match query

{
  "query": {
    "multi_match": {
      "query": "smart watch sports",
      "fields": ["name^3", "description"],
      "type": "best_fields"
    }
  }
}
Note

name^3 means that the name field is weighted three times higher than description.

match_phrase query

Requires the tokens after tokenization to appear in order and adjacent to each other:

{ "query": { "match_phrase": { "description": "smart watch" } } }

query_string query (Lucene syntax)

{
  "query": {
    "query_string": {
      "query":         "(noise-canceling OR Bluetooth) AND headphones",
      "default_field": "description"
    }
  }
}

Highlighting

{
  "query": { "match": { "description": "noise-canceling headphones" } },
  "highlight": {
    "pre_tags":  ["<em>"],
    "post_tags": ["</em>"],
    "fields": {
      "description": {
        "fragment_size":       150,
        "number_of_fragments": 3
      }
    }
  }
}

Pagination

For shallow pagination (within the first 10,000 results), use from + size. For deep pagination, use search_after:

{
  "size": 10,
  "query": { "match_all": {} },
  "sort":  [ { "created_at": "desc" }, { "_id": "asc" } ],
  "search_after": ["2025-05-25", "4"]
}
Note

Avoid using from + size for deep pagination (for example, from=10000), which causes severe performance issues. Use search_after instead.

Exact queries and filters

// term
{ "query": { "term":  { "category": "Electronics" } } }

// terms
{ "query": { "terms": { "category": ["Electronics","Wearables"] } } }

// range
{ "query": { "range": { "price": { "gte": 100, "lte": 500 } } } }

// Date range
{ "query": { "range": { "created_at": { "gte": "2025-05-01", "lt": "2025-06-01" } } } }

// exists
{ "query": { "exists": { "field": "tags" } } }

Bool compound query

{
  "query": {
    "bool": {
      "must":     [ { "match": { "description": "headphones" } } ],
      "filter":   [
        { "term":  { "in_stock": true } },
        { "range": { "price": { "lte": 500 } } }
      ],
      "should":   [ { "term": { "tags":     "Noise canceling" } } ],
      "must_not": [ { "term": { "category": "Wearables" } } ]
    }
  }
}

Clause

Description

Participates in scoring

must

Must match.

Yes

filter

Must match but does not participate in scoring.

No (uses cache for better performance).

should

Matches at least one clause to add to the score.

Yes

must_not

Must not match.

No

Note

Place filter conditions that do not need scoring (such as statuses and date ranges) in filter. This uses the cache to improve performance.

Vector search (KNN)

Basic KNN query

{
  "size": 5,
  "query": {
    "knn": {
      "embedding": {
        "vector": [0.1, 0.2, 0.3, "..."],
        "k":      5
      }
    }
  }
}

Vector search with filter conditions

{
  "size": 5,
  "query": {
    "knn": {
      "embedding": {
        "vector": [0.1, 0.2, 0.3, "..."],
        "k":      5,
        "filter": {
          "bool": {
            "must": [
              { "term":  { "category":   "Technical documentation" } },
              { "range": { "created_at": { "gte": "2025-01-01" } } }
            ]
          }
        }
      }
    }
  }
}

Vector + full-text hybrid search

{
  "size": 10,
  "query": {
    "bool": {
      "should": [
        { "match": { "content":   { "query": "distributed database architecture", "boost": 0.3 } } },
        { "knn":   { "embedding": { "vector": [0.1, 0.2, 0.3, "..."], "k": 10 } } }
      ]
    }
  }
}

Aggregation analysis

// Metric aggregation
{
  "size": 0,
  "aggs": {
    "avg_price":   { "avg":   { "field": "price" } },
    "max_price":   { "max":   { "field": "price" } },
    "min_price":   { "min":   { "field": "price" } },
    "price_stats": { "stats": { "field": "price" } }
  }
}

// Terms grouping aggregation (nested sub-aggregation)
{
  "size": 0,
  "aggs": {
    "by_category": {
      "terms": { "field": "category", "size": 10, "order": { "_count": "desc" } },
      "aggs":  { "avg_price": { "avg": { "field": "price" } } }
    }
  }
}

// Date Histogram
{
  "size": 0,
  "aggs": {
    "products_over_time": {
      "date_histogram": { "field": "created_at", "calendar_interval": "month", "format": "yyyy-MM" }
    }
  }
}

Geospatial query

// geo_distance
{
  "query": {
    "geo_distance": {
      "distance": "5km",
      "location": { "lat": 31.23, "lon": 121.47 }
    }
  },
  "sort": [
    {
      "_geo_distance": {
        "location": { "lat": 31.23, "lon": 121.47 },
        "order":    "asc",
        "unit":     "km"
      }
    }
  ]
}

// geo_bounding_box
{
  "query": {
    "geo_bounding_box": {
      "location": {
        "top_left":     { "lat": 31.3, "lon": 121.4 },
        "bottom_right": { "lat": 31.1, "lon": 121.6 }
      }
    }
  }
}