Skip to content
/ typed-es Public

Automatically add output types to your Elasticsearch queries.

License

Vahor/typed-es

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Typed ES

Code quality npm downloads Elasticsearch

Automatically add output types to your Elasticsearch queries.

Tested with Elasticsearch ^8 and Elasticsearch ^9

Supported Aggregations

Bucket 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

Metrics Aggregations

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

Pipeline Aggregations

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

Features

  • Automatic type based on options: Automatically infers output types from query options (e.g., returning total count).
  • Automatic output type based on requested fields and aggregations: Derives precise types from specified _source, fields, docvalue_fields and aggregations configurations.
  • 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 search and asyncSearch: You can still use the native types if something goes wrong (see What if the library is missing a feature that you need?).

Example Usage

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; }>

Why This Library?

To highlight the benefits, here's a comparison with/without the library:

Same Example Without This Library

Without providing any types

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'.

With manual type definitions

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; }>

With @vahor/typed-es

// 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 }> 

Install

bun add @vahor/typed-es

Note: you can install it in dev-dependencies if you don't plan to use the typedEs function.

Usage

Step 1: Define your index types

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;
	};
};

Step 2: Create a client

import { Client } from "@elastic/elasticsearch";
import { TypedClient } from "@vahor/typed-es";

const client = new Client({
    ... // elasticsearch client config
}) as unknown as TypedClient<CustomIndexes>;

Step 3: Use the typedEs function

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.

Step 4: Enjoy an easy type-safe output

// 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.

Usage with asyncSearch

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" });

What if the library is missing a feature that you need?

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

Limitations

  • 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.

License

MIT

About

Automatically add output types to your Elasticsearch queries.

Topics

Resources

License

Contributing

Stars

Watchers

Forks

Sponsor this project