Automatically add output types to your Elasticsearch queries.
Tested with Elasticsearch ^8 and Elasticsearch ^9
Supported Aggregations
| Aggregation | Status | Documentation |
|---|---|---|
| Adjacency Matrix | âś… | docs |
| Auto Date Histogram | âś… | docs |
| Categorize Text | âś… | docs |
| Children | âś… | docs |
| Composite | âś… | docs |
| Date Histogram | âś… | docs |
| Date Range | âś… | docs |
| Diversified Sampler | ❌ | docs |
| Filter | âś… | docs |
| Filters | âś… | docs |
| Frequent Item Sets | ❌ | docs |
| Geohash Grid | âś… | docs |
| Geohex Grid | âś… | docs |
| Geotile Grid | âś… | docs |
| Global | ❌ | docs |
| Histogram | âś… | docs |
| IP Prefix | âś… | docs |
| IP Range | âś… | docs |
| Missing | âś… | docs |
| Multi Terms | ❌ | docs |
| Parent | âś… | docs |
| Nested | âś… | docs |
| Random Sampler | âś… | docs |
| Range | âś… | docs |
| Rare Terms | ❌ | docs |
| Reverse Nested | âś… | docs |
| Sampler | âś… | docs |
| Significant Terms | ❌ | docs |
| Significant Text | âś… | docs |
| Terms | âś… | docs |
| Time Series | ❌ | docs |
| Variable Width Histogram | âś… | docs |
| Aggregation | Status | Documentation |
|---|---|---|
| Avg | âś… | docs |
| Boxplot | âś… | docs |
| Cardinality | âś… | docs |
| Cartesian Bounds | âś… | docs |
| Cartesian Centroid | âś… | docs |
| Extended Stats | âś… | docs |
| Geo Bounds | âś… | docs |
| Geo Centroid | âś… | docs |
| Geo Line | âś… | docs |
| Matrix Stats | âś… | docs |
| Max | âś… | docs |
| Median Absolute Deviation | âś… | docs |
| Min | âś… | docs |
| Percentile Ranks | âś… | docs |
| Percentiles | âś… | docs |
| Rate | âś… | docs |
| Scripted Metric | âś… | docs |
| Stats | âś… | docs |
| String Stats | âś… | docs |
| Sum | âś… | docs |
| T-Test | âś… | docs |
| Top Hits | âś… | docs |
| Top Metrics | âś… | docs |
| Value Count | âś… | docs |
| Weighted Avg | âś… | docs |
| Aggregation | Status | Documentation |
|---|---|---|
| Average Bucket | ❌ | docs |
| Bucket Script | ❌ | docs |
| Bucket Count K-S Test | ❌ | docs |
| Bucket Correlation | ❌ | docs |
| Bucket Selector | ❌ | docs |
| Bucket Sort | âś… | docs |
| Change Point | ❌ | docs |
| Cumulative Cardinality | ❌ | docs |
| Cumulative Sum | ❌ | docs |
| Derivative | ❌ | docs |
| Extended Stats Bucket | ❌ | docs |
| Inference | ❌ | docs |
| Max Bucket | ❌ | docs |
| Min Bucket | ❌ | docs |
| Moving Average | ❌ | docs |
| Moving Function | ❌ | docs |
| Moving Percentiles | ❌ | docs |
| Normalize | ❌ | docs |
| Percentiles Bucket | ❌ | docs |
| Serial Differencing | ❌ | docs |
| Stats Bucket | ❌ | docs |
| Sum Bucket | ❌ | docs |
- Automatic type based on options: Automatically infers output types from query options (e.g., returning
totalcount). - Automatic output type based on requested fields and aggregations: Derives precise types from specified
_source,fields,docvalue_fieldsandaggregationsconfigurations. - Understand wildcards: The library correctly detects and infers output types even when using wildcards in
_source.
For example, given an index with fields{ created_at: string; title: string },
specifying_source: ["*_at"]will correctly return{ created_at: string }in the output type. - Supports
searchandasyncSearch: You can still use the native types if something goes wrong (see What if the library is missing a feature that you need?).
type MyIndex = {
"my-index": {
id: number;
name: string;
created_at: string;
}
};
// Having to use `as unknown` is less than ideal, but as we're overriding types, typescript isn't very happy
const client = new Client({/* config */}) as unknown as TypedClient<Indexes>;
// Query with _source (wildcard), fields, aggregation, and options
const query = typedEs(client, {
index: "my-index",
_source: ["id", "na*"],
fields: [
{
field: "created_at",
format: "yyyy-MM-dd",
},
],
track_total_hits: true,
rest_total_hits_as_int: true, // Ensures total value is returned as a number
aggs: {
name_counts: { terms: { field: "name" } },
},
});
const result = await client.search(query);
const total = result.hits.total; // number
const firstHit = result.hits.hits[0]; // { _source: { id: number; name: string}, fields: { created_at: string[] } }
const aggregationBuckets = result.aggregations.name_counts.buckets; // Array<{ key: string | number; doc_count: number; }>To highlight the benefits, here's a comparison with/without the library:
Same Example Without This Library
const result = await client.search(query);
const total = result.hits.total; // number | estypes.SearchTotalHits | undefined
const firstHit = result.hits.hits[0]._source; // unknown
const aggregationBuckets = result.aggregations!.name_counts.buckets; // any, ts error: Object is possibly 'undefined'.const result = await client.search<
{ id: number; created_at: string; },
{
name_counts: {
buckets: Array<{ key: string; doc_count: number }>;
};
}
>(query);
const total = result.hits.total; // number | estypes.SearchTotalHits | undefined
const firstHit = result.hits.hits[0]; // { _source: { id: number; created_at: string; } | undefined, fields: Record<string, unknown> }
const aggregationBuckets = result.aggregations!.name_counts.buckets; // Array<{ key: string; doc_count: number; }>// Automatic type inference - no manual definitions needed
const result = await client.search(query);
const total = result.hits.total; // number
const firstHit = result.hits.hits[0]._source; // { id: number; created_at: string }
const aggregationBuckets = result.aggregations.name_counts.buckets; // Array<{ key: string | number; doc_count: number }> bun add @vahor/typed-esNote: you can install it in dev-dependencies if you don't plan to use the typedEs function.
type CustomIndexes = {
"first-index": {
score: number;
entity_id: string;
date: string;
},
"second-index": {
"some-field": string;
}
}For complex types like "point", "shape" even "date" we currently assume that the type is string.
ex:
{
"mappings": {
"properties": {
"location": {
"type": "point"
},
"date": {
"type": "date"
}
}
}
}would give:
type CustomIndexes = {
"first-index": {
location: string;
date: string;
};
};import { Client } from "@elastic/elasticsearch";
import { TypedClient } from "@vahor/typed-es";
const client = new Client({
... // elasticsearch client config
}) as unknown as TypedClient<CustomIndexes>;import { typedEs } from "@vahor/typed-es";
const query = typedEs(client, {
index: "first-index",
_source: ["score", "entity_id", "*ate"],
});
const queryWithAggs = typedEs(client, {
index: "first-index",
_source: ["score", "entity_id", "*ate"],
aggs: {
some_agg: {
terms: {
field: "entity_id",
},
},
},
});typedEs is a simple wrapper that adds type safety to index, autocompletes on _source.
Check its definition in typed-es.ts, you can reuse the same definition to add default values to your queries.
Note: when _source is missing, the output will contain every fields.
// Use the elasticsearch client as usual
const output = await client.search(query);
// And without having to add .search<Sources, Aggs>(query) everywhere, you now have access to the correct types
const hits = output.hits.hits;
for (const hit of hits) {
// Here hit is typed as { _source: { score: number; entity_id: string, date: string } }
const score = hit._source.score; // typed as number
const entity_id = hit._source.entity_id; // typed as string
const invalid = hit._source.invalid; // error: Property 'invalid' does not exist on type '{ score: number; entity_id: string; }'
}
const outputWithAggs = await client.search(queryWithAggs);
const aggs = outputWithAggs.aggregations;
const someAgg = aggs.some_agg;
const someAggTerms = someAgg.buckets;
for (const bucket of someAggTerms) {
// Here bucket is typed as { key: string | number; doc_count: number }
const key = bucket.key; // typed as string | number
const doc_count = bucket.doc_count; // typed as number
}With this you also get type-errors when you try to access a field that doesn't exist in the index. Or an invalid index. And with that, also autocompletion for these fields.
const invalidIndex = typedEs(client, {
index: "invalid-index", // Here we get a: Type '"invalid-index"' is not assignable to type '"first-index" | "second-index"'.
_source: ["score", "entity_id"],
});See more examples in the test files.
The asyncSearch API has some complexity for us. The get method does not include the original query type information by default.
To work around that we've added a new type definition.
const query = typedEs(...);
const result = await client.asyncSearch.get<typeof query>({ id: "abc" });
const data = result.response; // Same type as if you used client.search(query);
// If you don't have a query variable, you can pass the query type explicitly.
const result = await client.asyncSearch.get<{ query: ...}>({ id: "abc" });Please open an issue or a PR.
If it's a type error and is urgent, you can add the types manually as you'd do without the library.
const myBrokenQuery = typedEs(client, {
index: "my-index",
_source: ["score", "entity_id", "*ate"],
});
const result = await (client as unknown as Client).search<TDocument, TAggregations>(myBrokenQuery); // With the `as Client` cast you are now using the native types- query fields and aggs fields are not typed.
- Some agg functions might be missing.
- _source fields allow any string as you can use wildcards. On the other hand, wildcards will result in the correct type in the output.
- has to use
as unknown as TypedClient<Indexes>which I don't like.
PRs are welcome to fix these limitations.
MIT