faucet-source-elasticsearch
An Elasticsearch search source for the faucet-stream ecosystem. Runs a query-DSL search against an index, walks the result set page-by-page via the scroll API, emits each hit's _source as a serde_json::Value, and streams pages straight into any faucet-stream sink so memory stays bounded no matter how many documents match.
Reach for it when you need to export logs, metrics, orders, or any indexed documents out of Elasticsearch — for backup, reindexing, analytics offload, or feeding a database/warehouse/queue — with one declarative config and no glue code.
Feature highlights
- Scroll-based streaming — the source uses the scroll API so an index of any size is drained at
O(batch_size)client memory; each scroll response becomes exactly oneStreamPagewritten to the sink as it lands. - Full query-DSL support — pass any Elasticsearch query object verbatim (
bool,range,term,match, …); defaults tomatch_all. - Four auth modes — none, HTTP Basic, Bearer token, or API key. The shared
ElasticsearchAuthenum is re-exported fromfaucet-common-elasticsearchso it matches the Elasticsearch sink byte-for-byte; credentials are masked in debug output. - Shared auth providers —
auth: { ref: <name> }points at a provider in the CLI's top-levelauth:catalog, so many sources share one token with single-flight refresh. - Unconditional scroll cleanup — the open
_scroll_idis always sent toDELETE _search/scrollon every exit path (clean drain,max_pagestruncation, mid-stream error, or the consumer dropping the stream) via an RAII guard, so the cluster never leaks server-side scroll state. - Matrix-context interpolation —
${field.path}placeholders inindexandqueryresolve against the parent-record context at runtime, so one config can fan out across indices or query values. max_pagescap — bound the number of scroll responses for previews or partial exports.- No-batching sentinel —
batch_size: 0issues a single non-scroll_search?size=10000for small lookup indices or sinks that prefer one large request. - Client built once — the
reqwestclient is constructed innew()and reused for every request.
Installation
# As a library:
# Or via the umbrella crate:
# In the CLI (opt-in connector feature):
source-elasticsearch is an opt-in feature — it is not part of the CLI/umbrella default build.
Quick start
# pipeline.yaml — faucet run pipeline.yaml
version: 1
pipeline:
source:
type: elasticsearch
config:
base_url: http://localhost:9200
index: my_index
# query defaults to { match_all: {} }
sink:
type: jsonl
config:
path: ./docs.jsonl
Configuration reference
| Field | Type | Default | Description |
|---|---|---|---|
base_url |
string | — (required) | Base URL of the Elasticsearch cluster (e.g. http://localhost:9200). A trailing slash is trimmed. |
index |
string | — (required) | Index (or pattern, e.g. metrics-*) to search. Supports ${field.path} matrix-context placeholders. |
query |
object | { "match_all": {} } |
Elasticsearch query DSL, sent verbatim as the query of the search body. Supports ${field.path} placeholders resolved against the parent-record context. |
scroll_timeout |
string | "1m" |
Scroll-context keep-alive sent on the initial and every follow-up scroll request (e.g. "1m", "5m"). Must exceed the time taken to process one page downstream. |
auth |
ElasticsearchAuth |
{ type: none } |
Authentication — inline { type, config } or { ref: <name> }. See Authentication. |
max_pages |
int | (unset = no limit) | Maximum number of scroll responses to emit. The cap applies after a page is yielded. |
batch_size |
int | 1000 |
Docs per emitted StreamPage, also the scroll API size parameter. 0 = no batching: a single non-scroll _search?size=10000 is issued instead. Validated against MAX_BATCH_SIZE (1,000,000) at construction. |
Authentication
auth uses the shared ElasticsearchAuth enum (the project-wide { type, config } shape):
type |
config |
Sent as |
|---|---|---|
none |
(none) | No Authorization header. |
basic |
{ username, password } |
HTTP Basic Authorization: Basic <base64>. |
bearer |
{ token } |
Authorization: Bearer <token>. |
api_key |
{ key } |
Authorization: ApiKey <key> (the base64 id:api_key value). |
# No auth (local dev)
auth:
type: none
# HTTP Basic
auth:
type: basic
config:
username: elastic
password: ${env:ES_PASSWORD}
# Bearer token
auth:
type: bearer
config:
token: ${env:ES_BEARER_TOKEN}
# API key (recommended for Elastic Cloud)
auth:
type: api_key
config:
key: ${env:ES_API_KEY}
Shared provider via the top-level auth: catalog (single token shared across sources):
auth:
es_token:
type: static
config:
pipeline:
source:
type: elasticsearch
config:
base_url: https://es.example.com:9200
index: orders
auth:
A shared provider may only yield a bearer or basic credential — header- and token-style credentials have no Elasticsearch equivalent and surface as an auth error.
Examples
Filtered error-log export to S3
# Elasticsearch (API-key auth) → S3 with sharded parallel uploads.
version: 1
name: elasticsearch_to_s3
pipeline:
source:
type: elasticsearch
config:
base_url: https://es.example.com:9200
index: logs-2026-05
query:
match:
level: error
scroll_timeout: 2m
batch_size: 2000
auth:
type: api_key
config:
key: ${env:ES_API_KEY}
sink:
type: s3
config:
bucket: my-es-backups
prefix: logs/2026-05/
region: us-east-1
file_extension: .jsonl
max_records_per_file: 50000
concurrency: 16
Bool query with a time range and Basic auth
source:
type: elasticsearch
config:
base_url: https://elasticsearch.example.com:9200
index: application-logs-2026.03
query:
bool:
must:
- match:
- range:
"@timestamp":
scroll_timeout: 5m
batch_size: 5000
max_pages: 100
auth:
type: basic
config:
username: elastic
password: ${env:ES_PASSWORD}
Small lookup index — one request, no scroll
source:
type: elasticsearch
config:
base_url: http://localhost:9200
index: country_codes
batch_size: 0 # single _search?size=10000, no scroll context
Daily metrics window driven by a matrix context
source:
type: elasticsearch
config:
base_url: https://es.example.com:9200
index: "metrics-${region.name}"
query:
range:
"@timestamp":
auth:
Streaming & batching
The source overrides Source::stream_pages: the pipeline writes documents to the sink as each scroll page lands, so client-side memory stays O(batch_size) regardless of the index's total document count.
batch_size(default1000) is passed straight to the scroll API as thesizeparameter on the initialPOST /{index}/_search?scroll={timeout}&size={batch_size}. Each scroll response — initial and follow-up — becomes exactly oneStreamPage. The trailing empty scroll page is the end-of-scroll sentinel and is consumed but not emitted.batch_size: 0is the "no batching" sentinel: the source skips scroll entirely and issues a singlePOST /{index}/_search?size=10000, emitting one page. The10000cap mirrors Elasticsearch's defaultindex.max_result_window; indices with a larger window still receive only10000hits — switch back to scroll if you need more.batch_size: 0is also honoured on the bufferedfetch_all/fetch_with_contextpath — it maps to the samesize=10000initial search (not the literalsize=0, which would return zero hits).max_pages, when set, caps the total number of scroll responses emitted; the cap applies after the page is yielded.- The trait-level
batch_sizeargument tostream_pagesis ignored in favour of the config field, so a pipeline-supplied hint never silently overrides an explicit config value. - This source has no incremental-replication / resume mode today, so every emitted page carries
bookmark: None(see Resume & state). For incremental loads, encode the watermark in the query (e.g. arangeon@timestamp) and drive it from a matrix context or${now.*}token.
Resume & state
The Elasticsearch search source is stateless — it does not implement state_key() / apply_start_bookmark(), so each run re-executes the full query and there is no durable bookmark. Make runs incremental at the query level: filter on a monotonic field (@timestamp, a sequence id) and supply the lower bound via a ${now.*} token, a matrix-context value, or relative date math (now-1h).
Config loading & schema
Load from YAML/JSON files, environment variables, or a .env file via the helpers in faucet_core::config:
use ;
use ElasticsearchSourceConfig;
let config: ElasticsearchSourceConfig = load_json?;
let config: ElasticsearchSourceConfig = load_env_file?;
# .env
ES_SOURCE_BASE_URL=http://localhost:9200
ES_SOURCE_INDEX=my_index
ES_SOURCE_SCROLL_TIMEOUT=1m
ES_SOURCE_BATCH_SIZE=1000
ES_SOURCE_MAX_PAGES=50
Inspect the full JSON Schema with:
Library usage
use Source;
use ;
use json;
# async
For streaming into a sink, build a Pipeline (or call faucet_core::run_stream) with this source — it drives stream_pages automatically so memory stays bounded.
How it works
new()validatesbatch_sizeand builds areqwest::Clientonce (no I/O on construction).- The effective
indexandqueryare resolved against the matrix context (placeholder substitution), and auth is resolved once per run and reused across every scroll request. - An initial
POST /{index}/_search?scroll={timeout}&size={batch_size}returns the first page of hits plus a_scroll_id. - Each scroll response's
hits.hits[*]._sourceis extracted into aStreamPageand handed to the sink; the source thenPOST /_search/scrolls the_scroll_idfor the next page. - The loop stops on an empty hits array (the end-of-scroll sentinel) or when
max_pagesis reached. - An RAII
ScrollGuardowns the live_scroll_idandDELETEs the scroll context on every exit — clean drain, truncation, error, or drop — by spawning the cleanup so it survives the stream future being cancelled mid-await.
Lineage dataset URI
http://<host>:<port>/<index> with credentials stripped — e.g. http://localhost:9200/my_index.
Feature flags
This crate has no optional features of its own; enable it in the CLI/umbrella via the source-elasticsearch feature. ElasticsearchAuth is re-exported from faucet-common-elasticsearch, shared with faucet-sink-elasticsearch — faucet_source_elasticsearch::ElasticsearchAuth continues to work unchanged.
Troubleshooting / FAQ
| Symptom | Likely cause & fix |
|---|---|
401 Unauthorized / 403 |
Missing or wrong credentials. Set auth to basic / bearer / api_key with valid values; confirm the user/key has read privilege on the index. |
auth references provider '<name>' but no provider was supplied |
The config uses auth: { ref: <name> } but no matching entry exists in the top-level auth: catalog. Add the provider, or switch to inline { type, config }. |
| Auth provider error: "must yield a bearer or basic credential" | A shared provider returned a Header/Token credential, which Elasticsearch can't use. Use a provider type that yields a bearer or basic credential, or inline api_key auth. |
Empty result / 0 documents with batch_size: 0 |
The index's max_result_window is below the matched-doc count, or more than 10,000 docs match. The no-batching path caps at 10,000 hits — switch to scroll (batch_size: 1000) to drain everything. |
Search context not found / scroll expired mid-run |
scroll_timeout is shorter than the time to process one page downstream. Raise it (e.g. 5m) so the context outlives each page's sink write. |
index_not_found_exception |
The index doesn't exist (after any ${...} substitution). Verify the index/pattern name; patterns like metrics-* need allow_no_indices semantics on the cluster. |
parsing_exception / 400 on the query |
The query object isn't a valid query-DSL clause. It is sent verbatim as the body's query — wrap a single clause directly (e.g. { "match": { … } }), not inside another query: key. |
| Connection refused / TLS error | Wrong base_url scheme/port, or a self-signed cert. Use the correct https://host:9200 and ensure the cert chain is trusted by the host. |
| Resumes re-read everything | This source is stateless by design — encode an incremental watermark in the query (see Resume & state). |
See also
- Connector reference — the full capability matrix.
- Authentication cookbook — inline vs shared providers.
- Configuration grammar — the YAML/JSON config shape.
faucet-sink-elasticsearch— the matching bulk-index sink.faucet-common-elasticsearch— the sharedElasticsearchAuthenum.
License
Licensed under either of Apache License, Version 2.0 or MIT license at your option.