[Performance] rex + dedup is ~11× slower than an equivalent request-time derived-field query
Summary
For queries of the shape source=<index> | rex field=<f> "<pattern>" | dedup <extracted_field>, the current pushdown compiles the extracted column into an OpenSearch composite aggregation whose bucket key and top-hits fields are computed by invoking a REX_EXTRACT UDF via the plugin's compounded-script pipeline. On a 100k-document, single-shard, force-merged corpus, the PPL query takes ~3.6 s end-to-end (server-side took not reported for _ppl).
An equivalent raw _search that uses a request-time (search-time) derived field — declared inline in the _search body, no mapping change — completes the same workload in ~0.31 s server-side (took) — a ~11.5× speedup. Only the field actually referenced downstream (name1) is materialized; name2 is not projected.
Environment
- Branch:
main (commit 7c430dda6, "Decouple Calcite PPL planning from ExprType")
- OpenSearch:
3.8.0-SNAPSHOT (integTest bundle via ./gradlew opensearch-sql:run)
plugins.calcite.enabled=true
- Single node, single shard, single segment (force-merged),
refresh_interval=-1
- Request/query cache cleared; solution path uses
request_cache=false
Sample data (100k docs)
Each document is alice{k}@example{i}.com where k = i mod 500, so name1 has 500 distinct values (200 dupes each) and name2 is unique per doc. name2 is not referenced by the query, so only name1 needs to be materialized.
Index mapping — plain text field, no derived fields declared:
PUT /log_bench_a
{
"settings": {"number_of_shards": 1, "number_of_replicas": 0, "refresh_interval": "-1"},
"mappings": {"properties": {"v": {"type": "text"}}}
}
Bulk generator (100k docs):
N = 100_000
K = 500
for i in range(N):
k = i % K
doc = {"v": f"alice{k}@example{i}.com"}
# bulk-index `doc` into log_bench_a
Sample rows:
| _id |
v |
| 1 |
alice0@example0.com |
| 2 |
alice1@example1.com |
| … |
… |
| 501 |
alice0@example500.com |
| 100000 |
alice499@example99999.com |
After indexing, force-merge to a single segment:
POST /log_bench_a/_forcemerge?max_num_segments=1
Both the baseline and the solution run against the same index — the solution does not require any mapping change.
Queries
Baseline — PPL rex + dedup:
POST /_plugins/_ppl
{
"query": "source=log_bench_a | rex field=v \"(?<name1>.+)@(?<name2>.+)\" | dedup name1"
}
Solution — raw _search with a request-time derived field for name1 only, composite over name1, top_hits fetching v and name1. This is the request the planner would emit if it recognized the rex pattern and materialized only the referenced group as a request-time derived field:
POST /log_bench_a/_search?request_cache=false
{
"size": 0,
"derived": {
"name1": {
"type": "keyword",
"script": {
"source": "def m = /(?<name1>.+)@(?<name2>.+)/.matcher(params._source.v); if (m.find()) emit(m.group(1));"
}
}
},
"aggs": {
"by_name1": {
"composite": {
"size": 10000,
"sources": [{"name1": {"terms": {"field": "name1"}}}]
},
"aggs": {
"top": {
"top_hits": {"size": 1, "fields": ["v", "name1"], "_source": false}
}
}
}
}
}
Both queries produce the same 500 dedup groups with a representative v per group.
Results
Three cache-cleared runs per path. Baseline took is not reported by the PPL endpoint; only end-to-end is available.
| Path |
End-to-end (ms) |
Server took (ms) |
Baseline — PPL rex + dedup |
3548 / 3626 / 3710 (avg 3628) |
n/a |
Solution — request-time derived name1 + composite + top_hits |
393 / 401 / 413 (avg 402) |
308 / 311 / 324 (avg 314) |
| Speedup |
~9.0× end-to-end |
~11.5× on took |
[Performance]
rex + dedupis ~11× slower than an equivalent request-time derived-field querySummary
For queries of the shape
source=<index> | rex field=<f> "<pattern>" | dedup <extracted_field>, the current pushdown compiles the extracted column into an OpenSearch composite aggregation whose bucket key and top-hits fields are computed by invoking aREX_EXTRACTUDF via the plugin's compounded-script pipeline. On a 100k-document, single-shard, force-merged corpus, the PPL query takes ~3.6 s end-to-end (server-sidetooknot reported for_ppl).An equivalent raw
_searchthat uses a request-time (search-time) derived field — declared inline in the_searchbody, no mapping change — completes the same workload in ~0.31 s server-side (took) — a ~11.5× speedup. Only the field actually referenced downstream (name1) is materialized;name2is not projected.Environment
main(commit7c430dda6, "Decouple Calcite PPL planning from ExprType")3.8.0-SNAPSHOT(integTest bundle via./gradlew opensearch-sql:run)plugins.calcite.enabled=truerefresh_interval=-1request_cache=falseSample data (100k docs)
Each document is
alice{k}@example{i}.comwherek = i mod 500, soname1has 500 distinct values (200 dupes each) andname2is unique per doc.name2is not referenced by the query, so onlyname1needs to be materialized.Index mapping — plain text field, no derived fields declared:
Bulk generator (100k docs):
Sample rows:
alice0@example0.comalice1@example1.comalice0@example500.comalice499@example99999.comAfter indexing, force-merge to a single segment:
Both the baseline and the solution run against the same index — the solution does not require any mapping change.
Queries
Baseline — PPL
rex + dedup:Solution — raw
_searchwith a request-time derived field forname1only, composite overname1, top_hits fetchingvandname1. This is the request the planner would emit if it recognized therexpattern and materialized only the referenced group as a request-time derived field:Both queries produce the same 500 dedup groups with a representative
vper group.Results
Three cache-cleared runs per path. Baseline
tookis not reported by the PPL endpoint; only end-to-end is available.took(ms)rex + dedupname1+ composite + top_hitstook