Package Exports
- elasticlink
Readme
elasticlink
Type-safe, fluent API for Elasticsearch queries and index management
Define your Elasticsearch mappings once, then build queries with full IntelliSense and compile-time safety. match() only accepts text fields, term() only keyword fields, etc. Compiles to standard Elasticsearch DSL with no runtime overhead.
Features
- Mapping-Aware Type Safety: define your ES mappings once with
mappings(), and every query method constrains its field arguments to the correct type. - Types from the source: autocomplete for field names, query options, and aggregation methods. Structural types are hand-rolled, leaf option types are derived from
@elastic/elasticsearchtypes, so they stay current. - Fluent Chainable API: readable query building with boolean logic, conditional branches (using
.when()), aggregations, and pagination in a single chain. - No Runtime Overhead or Lock-In: builds plain Elasticsearch DSL objects. Pass them to the official
@elastic/elasticsearchclient or any HTTP client. - Production-Ready Settings Presets: helpers for production search, fast ingest, and index sort presets for common lifecycle stages, with full type support.
- 40+ Field Helpers:
text(),keyword(),denseVector(),nested(), and more with options for IDE tooltips.
Installation
npm install elasticlink @elastic/elasticsearchRequires Node.js 20+ and @elastic/elasticsearch 9.x as a peer dependency.
Note: elasticlink derives its types directly from the official
@elastic/elasticsearchpackage for accuracy, completeness, and automatic alignment with Elasticsearch updates. This is a compile-time only dependency — it adds zero runtime overhead. If you're already using the Elasticsearch client, you're all set. If not, install it as a dev dependency:npm install -D @elastic/elasticsearch.
Quick Start
import { queryBuilder, mappings, text, keyword, float, type Infer } from 'elasticlink';
// Define field types once — this is the single source of truth
const productMappings = mappings({
name: text(),
description: text(),
price: float(),
category: keyword()
});
// Derive a TS type from mappings (optional, for use elsewhere in your application)
type Product = Infer<typeof productMappings>;
// Build a type-safe query — field constraints are enforced at compile time
const elasticQuery = queryBuilder(productMappings)
.match('name', 'laptop') // ✅ 'name' is a text field
.range('price', { gte: 500, lte: 2000 })
.sort('price', 'asc')
.from(0)
.size(20)
.build();
// Send to Elasticsearch
const response = await client.search({ index: 'products', ...elasticQuery });Boolean queries with aggregations:
const facetedSearch = queryBuilder(productMappings)
.bool()
.must((q) => q.match('name', 'gaming laptop', { operator: 'and' }))
.filter((q) => q.term('category', 'electronics'))
.filter((q) => q.range('price', { gte: 800, lte: 2000 }))
.aggs((agg) => agg.terms('by_category', 'category', { size: 10 }).avg('avg_price', 'price'))
.highlight(['name', 'description'])
.size(20)
.build();Type safety in action — wrong field types are caught at compile time:
queryBuilder(productMappings).match('category', 'electronics');
// ^^^^^^^^^^
// TypeScript error: 'category' is a keyword field — use term(), not match()
queryBuilder(productMappings).term('category', 'electronics'); // ✅ CorrectExamples
More examples available in src/__tests__/examples.test.ts.
E-commerce Product Search
A complete search request: boolean query with must/filter/should, aggregations for facets and price ranges, highlights, _source filtering, and pagination.
const ecommerceMappings = mappings({
name: text(),
description: text(),
category: keyword(),
price: float(),
tags: keyword(),
in_stock: boolean()
});
const searchTerm = 'gaming laptop';
const category = 'electronics';
const minPrice = 800;
const maxPrice = 2000;
const result = queryBuilder(ecommerceMappings)
.bool()
.must((q) => q.match('name', searchTerm, { operator: 'and', boost: 2 }))
.should((q) => q.fuzzy('description', searchTerm, { fuzziness: 'AUTO' }))
.filter((q) => q.term('category', category))
.filter((q) => q.range('price', { gte: minPrice, lte: maxPrice }))
.filter((q) => q.term('in_stock', true))
.minimumShouldMatch(1)
.aggs((agg) =>
agg.terms('by_category', 'category', { size: 10 }).range('price_ranges', 'price', {
ranges: [{ to: 800 }, { from: 800, to: 1500 }, { from: 1500 }]
})
)
.highlight(['name', 'description'], {
fragment_size: 150,
pre_tags: ['<mark>'],
post_tags: ['</mark>']
})
._source(['name', 'price', 'category', 'tags'])
.timeout('5s')
.from(0)
.size(20)
.sort('_score', 'desc')
.build();Produced DSL (click to expand)
{
"query": {
"bool": {
"must": [{ "match": { "name": { "query": "gaming laptop", "operator": "and", "boost": 2 } } }],
"should": [{ "fuzzy": { "description": { "value": "gaming laptop", "fuzziness": "AUTO" } } }],
"filter": [
{ "term": { "category": "electronics" } },
{ "range": { "price": { "gte": 800, "lte": 2000 } } },
{ "term": { "in_stock": true } }
],
"minimum_should_match": 1
}
},
"aggs": {
"by_category": { "terms": { "field": "category", "size": 10 } },
"price_ranges": {
"range": {
"field": "price",
"ranges": [{ "to": 800 }, { "from": 800, "to": 1500 }, { "from": 1500 }]
}
}
},
"highlight": {
"fields": { "name": { "fragment_size": 150 }, "description": { "fragment_size": 150 } },
"pre_tags": ["<mark>"],
"post_tags": ["</mark>"]
},
"_source": ["name", "price", "category", "tags"],
"timeout": "5s",
"from": 0,
"size": 20,
"sort": [{ "_score": "desc" }]
}Dynamic Search with Conditional Filters
Build queries dynamically based on runtime values. .when(condition, fn) — when the condition is falsy, the builder is returned unchanged.
const buildDynamicQuery = (filters: SearchFilters) => {
return queryBuilder(productMappings)
.bool()
.when(filters.searchTerm, (q) => q.must((q2) => q2.match('name', filters.searchTerm!, { boost: 2 })))
.when(filters.category, (q) => q.filter((q2) => q2.term('category', filters.category!)))
.when(filters.minPrice != null && filters.maxPrice != null, (q) =>
q.filter((q2) => q2.range('price', { gte: filters.minPrice!, lte: filters.maxPrice! }))
)
.from(filters.offset || 0)
.size(filters.limit || 20)
.build();
};Aggregations — Portfolio Analytics
Terms + sub-aggregation + date histogram in one request.
const instrumentMappings = mappings({
name: text(),
asset_class: keyword(),
sector: keyword(),
price: float(),
yield_rate: float(),
listed_date: date()
});
const result = queryBuilder(instrumentMappings)
.bool()
.filter((q) => q.term('asset_class', 'fixed-income'))
.filter((q) => q.range('yield_rate', { gte: 3.0 }))
.aggs((agg) =>
agg
.terms('by_sector', 'sector', { size: 10 })
.subAgg((sub) => sub.avg('avg_yield', 'yield_rate').max('max_price', 'price'))
.dateHistogram('listings_over_time', 'listed_date', {
interval: 'quarter',
min_doc_count: 1
})
.subAgg((sub) => sub.percentiles('yield_percentiles', 'yield_rate', { percents: [25, 50, 75, 95] }))
)
.size(0)
.build();Produced DSL (click to expand)
{
"query": {
"bool": {
"filter": [{ "term": { "asset_class": "fixed-income" } }, { "range": { "yield_rate": { "gte": 3.0 } } }]
}
},
"aggs": {
"by_sector": {
"terms": { "field": "sector", "size": 10 },
"aggs": {
"avg_yield": { "avg": { "field": "yield_rate" } },
"max_price": { "max": { "field": "price" } }
}
},
"listings_over_time": {
"date_histogram": { "field": "listed_date", "interval": "quarter", "min_doc_count": 1 },
"aggs": {
"yield_percentiles": { "percentiles": { "field": "yield_rate", "percents": [25, 50, 75, 95] } }
}
}
},
"size": 0
}Geospatial Search
const restaurantMappings = mappings({
name: text(),
cuisine: keyword(),
location: geoPoint(),
rating: float()
});
const result = queryBuilder(restaurantMappings)
.bool()
.filter((q) => q.term('cuisine', 'italian'))
.filter((q) => q.geoDistance('location', { lat: 40.7128, lon: -74.006 }, { distance: '5km' }))
.sort('rating', 'desc')
.size(20)
.build();TypeScript Support
elasticlink provides mapping-aware TypeScript safety:
- Field-Type Constraints: enforced at compile time across all methods —
match()only accepts text fields,term()only keyword/numeric fields,sort()only sortable fields (keyword, numeric, date, boolean, ip),collapse()only keyword/numeric fields,highlight()only text/keyword fields - Field Autocomplete: IntelliSense knows your field names and their types
Infer<S>: Derive TS document types from your mappings schema for use elsewhere in your application- Exported Field Group Types:
TextFields<M>,KeywordFields<M>,NumericFields<M>,DateFields<M>,BooleanFields<M>,GeoPointFields<M>,GeoShapeFields<M>,VectorFields<M>,DenseVectorFields<M>,SparseVectorFields<M>,RankFeatureFields<M>,CompletionFields<M>,IpFields<M>,FieldsOfType<M, T>, and others are exported for use in your own typed utilities - Exported Builder Types:
QueryBuilder<M>,ClauseBuilder<M>,RootAggregationBuilder<M>,NestedAggregationBuilder<M>,NestedEntryBuilder<M>,MSearchBuilder<M>,BulkBuilder<T>,SuggesterBuilder<M>,IndexBuilder<M>,AnalysisConfig,IndexSortFieldSpec,MSearchHeader,MSearchRequestParams
import { queryBuilder, mappings, text, keyword, integer, type Infer } from 'elasticlink';
const userMappings = mappings({
name: text(),
email: keyword(),
age: integer()
});
type User = Infer<typeof userMappings>;
// => { name: string; email: string; age: number }
// ✅ 'name' is a text field — match() accepts it
const q1 = queryBuilder(userMappings).match('name', 'John').build();
// ❌ TypeScript error: 'email' is keyword, not text — use term() instead
const q2 = queryBuilder(userMappings).match('email', 'john@example.com').build();
// ✅ Correct: use term() for keyword fields
const q3 = queryBuilder(userMappings).term('email', 'john@example.com').build();Settings Presets
Ready-made index settings for common lifecycle stages. Use with .settings() on indexBuilder() or pass directly to the ES _settings API.
import { indexBuilder, productionSearchSettings, indexSortSettings, fastIngestSettings } from 'elasticlink';
// Create index with production settings
const indexConfig = indexBuilder().mappings(myMappings).settings(productionSearchSettings()).build();
// Index-time sort for compression and early termination
const sortedConfig = indexBuilder()
.mappings(myMappings)
.settings({
...productionSearchSettings(),
index: indexSortSettings({ timestamp: 'desc', status: 'asc' })
})
.build();
// Before bulk ingest — disables refresh, removes replicas, async translog
await client.indices.putSettings({ index: 'my-index', body: fastIngestSettings() });
// Perform bulk ingest...
// Restore production settings afterward
await client.indices.putSettings({ index: 'my-index', body: productionSearchSettings() });
await client.indices.refresh({ index: 'my-index' });| Preset | Purpose |
|---|---|
productionSearchSettings(overrides?) |
Balanced production defaults — 1 replica, 5s refresh |
indexSortSettings(fields) |
Configure index-time sort order for disk compression and early termination |
fastIngestSettings(overrides?) |
Maximum indexing throughput — async translog, no replicas, refresh disabled |
All presets accept an optional overrides argument typed as Partial<IndicesIndexSettings>. fastIngestSettings deep-merges the translog key so individual translog overrides don't clobber the other defaults.
API Overview
Query Builder
queryBuilder(schema, includeQuery?) creates a fluent, immutable query builder. Every chain method returns a new builder instance.
queryBuilder(productMappings)
.bool()
.must((q) => q.match('name', 'laptop'))
.filter((q) => q.range('price', { gte: 500 }))
.should((q) => q.term('category', 'featured'))
.mustNot((q) => q.term('category', 'discontinued'))
.minimumShouldMatch(1)
.build();Query Methods
| Category | Methods | Description |
|---|---|---|
| Full-text | match, multiMatch, matchPhrase, matchPhrasePrefix, matchBoolPrefix, combinedFields |
Analyzed text search — field must be a text field |
| Term-level | term, terms, prefix, wildcard, exists, ids, matchAll, matchNone |
Exact value matching — field must be keyword, numeric, date, boolean, or ip |
| Range | range |
Range queries (gte, lte, gt, lt) on numeric, date, keyword, or ip fields |
| Fuzzy & Pattern | fuzzy, regexp, queryString, simpleQueryString, moreLikeThis |
Typo tolerance, regex, Lucene syntax, and similarity search |
| Boolean | bool → must, mustNot, should, filter, minimumShouldMatch |
Combine clauses with AND/OR/NOT/filter logic |
| Conditional | when(condition, fn) |
Dynamic query building — skips the branch when condition is falsy |
| Geo | geoDistance, geoBoundingBox, geoPolygon, geoShape |
Spatial queries — field must be geo_point or geo_shape |
| Vector & Relevance | knn, sparseVector, rankFeature, distanceFeature |
Semantic/vector search, rank features, and decay functions |
| Nested & Structure | nested, constantScore, hasChild, hasParent, parentId |
Query nested objects, parent/child joins, or wrap in constant-score filter |
| Sorting & Pagination | from, size, sort, searchAfter, collapse |
Result ordering, pagination, and field collapsing |
| Source & Fields | _source, sourceIncludes, sourceExcludes, fields, docValueFields, storedFields |
Control which fields are returned |
| Highlighting & Scoring | highlight, rescore, indicesBoost, minScore, trackScores, explain |
Result highlighting, rescoring, and relevance tuning |
| Positional | intervals, spanTerm, spanNear, spanOr, spanNot, spanFirst, spanContaining, spanWithin, spanMultiTerm, spanFieldMasking |
Term proximity and ordering queries |
| Advanced | postFilter, scriptFields, runtimeMappings, script, scriptScore, functionScore, percolate |
Post-query filtering, computed fields, and custom scoring |
| Configuration | timeout, preference, pit, terminateAfter, version, seqNoPrimaryTerm, trackTotalHits |
Search execution options |
| Inline builders | aggs(fn), suggest(fn) |
Attach aggregations or suggesters to the query |
| Output | build() |
Returns the final Elasticsearch DSL object |
All query methods accept options from their corresponding @elastic/elasticsearch type. See query.types.ts for complete signatures.
Conditional Building
.when(condition, fn) — the chain is never broken. When the condition is falsy, the builder is returned unchanged. condition resolves as: functions are called, booleans are used as-is, and any other value uses a nullish check (!= null) — so numeric 0 and empty string '' are treated as truthy.
const searchTerm: string | undefined = param.searchTerm;
const minPrice: number | undefined = param.minPrice;
queryBuilder(productMappings)
.bool()
.when(searchTerm, (q) => q.must((q2) => q2.match('name', searchTerm!)))
.when(minPrice, (q) => q.filter((q2) => q2.range('price', { gte: minPrice! })))
.build();Note: TypeScript cannot narrow closure variables inside callbacks. Even though
.when(searchTerm, fn)guarantees the value is defined insidefn, you still need a non-null assertion (!).
Query Parameters
queryBuilder(productMappings)
.match('name', 'laptop')
.from(0) // Pagination offset
.size(20) // Results per page
.sort('price', 'asc') // Sort by field
._source(['name', 'price']) // Which fields to return
.timeout('5s') // Query timeout
.trackScores(true) // Enable scoring in filter context
.trackTotalHits(true) // Track total hit count (or pass a number threshold)
.explain(true) // Return scoring explanation
.minScore(10) // Minimum relevance score
.version(true) // Include document version in results
.seqNoPrimaryTerm(true) // Include seq_no and primary_term for optimistic concurrency
.highlight(['name', 'description'], {
fragment_size: 150,
pre_tags: ['<mark>'],
post_tags: ['</mark>']
})
.build();Aggregations
Aggregations can be combined with queries via .aggs() or used standalone with the aggregations() builder.
import { queryBuilder, aggregations } from 'elasticlink';
// Inline aggregations (most common)
const result = queryBuilder(productMappings)
.term('category', 'electronics')
.aggs((agg) =>
agg
.terms('by_category', 'category', { size: 10 })
.subAgg((sub) => sub.avg('avg_price', 'price').max('max_price', 'price'))
)
.size(20)
.build();
// Aggregations-only (no query) — use queryBuilder(mappings, false)
const aggsOnly = queryBuilder(productMappings, false)
.aggs((agg) => agg.terms('by_category', 'category'))
.size(0)
.build();
// Standalone aggregation builder (for manual composition)
const standaloneAgg = aggregations(productMappings)
.avg('avg_price', 'price')
.terms('by_category', 'category', { size: 10 })
.build();Aggregation Methods
| Category | Methods | Description |
|---|---|---|
| Bucket | terms, dateHistogram, histogram, range, dateRange, filters, significantTerms, rareTerms, multiTerms, autoDateHistogram, composite, filter, missing, geoDistance, geohashGrid, geotileGrid |
Group documents into buckets |
| Metric | avg, sum, min, max, cardinality, percentiles, stats, valueCount, extendedStats, topHits, topMetrics, weightedAvg, geoBounds, geoCentroid |
Compute metrics over documents |
| Pipeline | derivative, cumulativeSum, bucketScript, bucketSelector |
Compute over other aggregation results |
| Structure | subAgg, nested, reverseNested, global |
Nest aggregations, navigate nested fields, or escape to global scope |
subAgg() attaches sub-aggregations to the last aggregation defined before the call — chain order matters. See aggregation.types.ts for complete option types.
Index Management
Configure index mappings, settings, and aliases declaratively with indexBuilder().
import { indexBuilder, mappings, keyword, integer, float, date, text } from 'elasticlink';
const matterMappings = mappings({
title: text({ analyzer: 'english' }),
practice_area: keyword(),
billing_rate: integer(),
risk_score: float(),
opened_at: date()
});
const indexConfig = indexBuilder()
.mappings(matterMappings)
.settings({
number_of_shards: 2,
number_of_replicas: 1,
refresh_interval: '5s'
})
.alias('matters-current', { is_write_index: true })
.alias('matters-all')
.build();
// PUT /matters-v1
await client.indices.create({ index: 'matters-v1', ...indexConfig });Produced DSL (click to expand)
{
"mappings": {
"properties": {
"title": { "type": "text", "analyzer": "english" },
"practice_area": { "type": "keyword" },
"billing_rate": { "type": "integer" },
"risk_score": { "type": "float" },
"opened_at": { "type": "date" }
}
},
"settings": {
"number_of_shards": 2,
"number_of_replicas": 1,
"refresh_interval": "5s"
},
"aliases": {
"matters-current": { "is_write_index": true },
"matters-all": {}
}
}IndexBuilder Methods
| Method | Description |
|---|---|
mappings(schemaOrFields, options?) |
Set index mappings from a MappingsSchema or raw field definitions |
settings(settings) |
Set index settings (IndicesIndexSettings from @elastic/elasticsearch) |
analysis(config) |
Configure custom analyzers, tokenizers, filters, char filters, and normalizers |
alias(name, options?) |
Add an index alias with optional filter, routing, and write index settings |
build() |
Returns the final CreateIndexOptions object |
Object and Nested Fields
Use object() for structured sub-documents queried with dot-notation. Use nested() for arrays of objects where cross-field queries within the same element must be accurate.
import { mappings, text, keyword, float, integer, boolean, object, nested, type Infer } from 'elasticlink';
const productMappings = mappings({
name: text(),
in_stock: boolean(),
address: object({
// Single structured value — queried with dot-notation
street: text(),
city: keyword(),
country: keyword()
}),
variants: nested({
// Array of objects — cross-field accuracy preserved
sku: keyword(),
color: keyword(),
price: float(),
stock: integer()
})
});
type Product = Infer<typeof productMappings>;
// { name: string; in_stock: boolean;
// address: { street: string; city: string; country: string };
// variants: Array<{ sku: string; color: string; price: number; stock: number }> }
// object sub-fields — query with dot-notation directly
queryBuilder(productMappings)
.bool()
.filter((q) => q.term('address.country', 'US'))
.filter((q) => q.match('address.street', 'Main'))
.build();
// nested sub-fields — must use .nested() wrapper; field names are relative
queryBuilder(productMappings)
.nested('variants', (q) => q.term('color', 'black'))
.build();
queryBuilder(productMappings)
.nested('variants', (q) => q.range('price', { lte: 150 }), { score_mode: 'min' })
.build();Inner field names are relative. Inside a
.nested()callback, you write field names relative to the nested path (e.g.'color', not'variants.color'). The library automatically qualifies them in the generated DSL.
Field Helpers
| Category | Helpers |
|---|---|
| Text | text, keyword, constantKeyword, matchOnlyText, searchAsYouType, wildcardField |
| Numeric | long, integer, short, byte, double, float, halfFloat, scaledFloat, unsignedLong |
| Date | date, dateNanos |
| Boolean | boolean |
| Binary | binary |
| IP | ip |
| Range | integerRange, floatRange, longRange, doubleRange, dateRange, ipRange |
| Objects | object, nested, flattened |
| Spatial | geoPoint, geoShape |
| Vector | denseVector, quantizedDenseVector, sparseVector |
| Rank | rankFeature, rankFeatures |
| Semantic | semanticText |
| Completion | completion |
| Special | percolator, alias, tokenCount, murmur3Hash, join |
// Shorthand — pass options or use defaults
keyword(); // { type: 'keyword' }
integer(); // { type: 'integer' }
text({ analyzer: 'english' }); // { type: 'text', analyzer: 'english' }
denseVector({ dims: 384, index: true, similarity: 'cosine' });
// Multi-fields carry type info — dot-notation paths work in queries
const m = mappings({
name: text({ fields: { raw: keyword() } }),
price: float({ fields: { string: keyword() } }),
});
const qb = queryBuilder(m);
qb.term('name.raw', 'exact match'); // ✓ name.raw is a KeywordFields<M> path
qb.match('name', 'full text'); // ✓ name is a TextFields<M> pathSee field.types.ts for all field helper option types.
Mapping Properties Reference (click to expand)
| Option | Types | Description |
|---|---|---|
analyzer |
text |
Index-time analyzer |
search_analyzer |
text |
Query-time analyzer (overrides analyzer) |
normalizer |
keyword |
Keyword normalizer (e.g. lowercase) |
index |
most types | Whether to index the field (default: true) |
store |
most types | Store field value separately (default: false) |
doc_values |
most types | Enable doc values for sorting/aggregations |
boost |
text, keyword, numeric | Index-time boost factor |
coerce |
numeric, range | Convert strings to numbers (default: true) |
format |
date |
Date format string (e.g. "yyyy-MM-dd") |
scaling_factor |
scaledFloat |
Required multiplier for scaled floats |
dims |
denseVector |
Number of dimensions (required for KNN indexing) |
similarity |
denseVector |
Similarity function: 'cosine', 'dot_product', 'l2_norm', 'max_inner_product' |
element_type |
denseVector |
Element type: 'float' (default), 'byte', 'bit' |
fields |
text, keyword, numeric, date | Multi-fields — type-safe dot-notation paths (e.g. name.raw) in query constraints |
properties |
object, nested | Sub-field mappings |
enabled |
object |
Disable indexing of object fields |
path |
alias |
Path to the target field |
max_input_length |
completion |
Max input length for completion suggestions |
preserve_separators |
completion |
Preserve separator characters |
preserve_position_increments |
completion |
Preserve position increments |
orientation |
geoShape |
Default orientation for polygons |
Index Settings Reference (click to expand)
The .settings() method accepts the full IndicesIndexSettings type from @elastic/elasticsearch. Common options:
| Setting | Type | Description |
|---|---|---|
number_of_shards |
number |
Primary shard count (set at creation, immutable) |
number_of_replicas |
number |
Replica count (can be changed after creation) |
refresh_interval |
string |
How often to refresh ('1s', '-1s' to disable) |
max_result_window |
number |
Max from + size (default: 10000) |
analysis |
object |
Custom analyzers, tokenizers, filters |
codec |
string |
Compression codec ('best_compression') |
Alias Options Reference (click to expand)
The .alias() method accepts an optional IndicesAlias object:
| Option | Type | Description |
|---|---|---|
filter |
object |
Query filter — only matching documents visible through alias |
is_write_index |
boolean |
Designate this index as the write target for the alias |
routing |
string |
Custom routing value for alias operations |
index_routing |
string |
Routing value for index operations only |
search_routing |
string |
Routing value for search operations only |
is_hidden |
boolean |
Hide alias from wildcard expressions |
Suggesters & Autocomplete
suggest(schema) creates a standalone suggester builder. Suggesters can also be attached inline via queryBuilder().suggest(fn).
import { queryBuilder, suggest, mappings, text, keyword, completion } from 'elasticlink';
const searchableMappings = mappings({
name: text(),
description: text(),
suggest_field: completion()
});
// Standalone suggest
const autocomplete = suggest(searchableMappings)
.completion('autocomplete', 'lap', {
field: 'suggest_field',
size: 10,
skip_duplicates: true,
fuzzy: { fuzziness: 'AUTO', min_length: 3, prefix_length: 1 }
})
.build();
// Inline with query — search + spell-check in one request
const searchWithSuggestions = queryBuilder(searchableMappings)
.match('name', 'laptpo')
.suggest((s) =>
s
.completion('autocomplete', 'lap', { field: 'suggest_field', size: 5 })
.term('spelling', 'laptpo', { field: 'name', size: 3, suggest_mode: 'popular' })
)
.size(20)
.build();| Method | Description |
|---|---|
term(name, text, options) |
Suggest corrections for individual terms based on edit distance |
phrase(name, text, options) |
Suggest corrections for entire phrases using n-gram language models |
completion(name, prefix, options) |
Fast prefix-based autocomplete (requires completion field type) |
build() |
Returns { suggest: ... } object |
See suggester.types.ts for complete option types (TermSuggesterOptions, PhraseSuggesterOptions, CompletionSuggesterOptions).
Multi-Search
msearch(schema) batches multiple search requests into a single API call using NDJSON format.
import { queryBuilder, msearch } from 'elasticlink';
const laptopQuery = queryBuilder(productMappings).match('name', 'laptop').range('price', { gte: 500, lte: 2000 }).build();
const phoneQuery = queryBuilder(productMappings).match('name', 'smartphone').range('price', { gte: 300, lte: 1000 }).build();
// NDJSON string for Elasticsearch _msearch endpoint
const ndjson = msearch(productMappings)
.addQuery(laptopQuery, { index: 'products', preference: '_local' })
.addQuery(phoneQuery, { index: 'products', preference: '_local' })
.build();
// Or as an array of objects
const array = msearch(productMappings)
.addQuery(laptopQuery, { index: 'products' })
.addQuery(phoneQuery, { index: 'products' })
.buildArray();| Method | Description |
|---|---|
add(request) |
Add a raw request (header + body) |
addQuery(body, header?) |
Add a built query with optional header |
addQueryBuilder(qb, header?) |
Add a QueryBuilder instance directly |
withParams(params) |
Set shared request parameters |
build() |
Returns NDJSON string |
buildArray() |
Returns array of header/body pairs |
buildParams() |
Returns shared request parameters |
Header options: index, routing, preference, search_type. See multi-search.types.ts for full types.
Bulk Operations
bulk(schema) batches create, index, update, and delete operations efficiently.
import { bulk, mappings, keyword, text, float } from 'elasticlink';
const productMappings = mappings({
id: keyword(),
name: text(),
price: float(),
category: keyword()
});
const bulkOp = bulk(productMappings)
.index({ id: '1', name: 'Laptop Pro', price: 1299, category: 'electronics' }, { _index: 'products', _id: '1' })
.create({ id: '2', name: 'Wireless Mouse', price: 29, category: 'accessories' }, { _index: 'products', _id: '2' })
.update({ _index: 'products', _id: '3', doc: { price: 999 } })
.delete({ _index: 'products', _id: '4' })
.build();
// POST /_bulk with Content-Type: application/x-ndjson| Method | Description |
|---|---|
index(doc, meta?) |
Index a document (create or replace) |
create(doc, meta?) |
Create a document (fail if exists) |
update(meta) |
Update with doc, script, upsert, doc_as_upsert, or retry_on_conflict |
delete(meta) |
Delete a document |
build() |
Returns NDJSON string |
buildArray() |
Returns array of action/document pairs |
Vector Search & Semantic Search
KNN (k-nearest neighbors) queries enable semantic search using vector embeddings.
import { queryBuilder, mappings, text, keyword, float, denseVector } from 'elasticlink';
const productWithEmbeddingMappings = mappings({
name: text(),
description: text(),
price: float(),
category: keyword(),
embedding: denseVector({ dims: 384 })
});
const searchEmbedding = [0.23, 0.45, 0.67, 0.12, 0.89]; // From your ML model
// Basic semantic search
const result = queryBuilder(productWithEmbeddingMappings)
.knn('embedding', searchEmbedding, {
k: 10,
num_candidates: 100
})
.size(10)
.build();
// Hybrid search — KNN + filters + aggregations
const hybridSearch = queryBuilder(productWithEmbeddingMappings)
.knn('embedding', searchEmbedding, {
k: 100,
num_candidates: 1000,
filter: { term: { category: 'electronics' } },
boost: 1.2,
similarity: 0.7
})
.aggs((agg) => agg.terms('categories', 'category', { size: 10 }))
.size(20)
.build();Additional vector and relevance methods: sparseVector(field, options) for learned sparse retrieval, rankFeature(field, options?) for rank feature scoring, and distanceFeature(field, options) for recency/proximity decay functions.
Script Queries & Custom Scoring
Script-based filtering and custom scoring for advanced relevance tuning.
import { queryBuilder, mappings, text, float, long } from 'elasticlink';
const scoredProductMappings = mappings({
name: text(),
price: float(),
popularity: long()
});
// Script-based filtering
const filtered = queryBuilder(scoredProductMappings)
.bool()
.must((q) => q.match('name', 'laptop'))
.filter((q) =>
q.script({
source: "doc['price'].value > params.threshold",
params: { threshold: 500 }
})
)
.build();
// Custom scoring with script_score
const customScored = queryBuilder(scoredProductMappings)
.scriptScore((q) => q.match('name', 'smartphone'), {
source: "_score * Math.log(2 + doc['popularity'].value)",
lang: 'painless'
})
.size(20)
.build();Percolate Queries
Percolate queries enable reverse search — match documents against stored queries.
import { queryBuilder, mappings, keyword, percolator } from 'elasticlink';
const alertRuleMappings = mappings({
query: percolator(),
name: keyword(),
severity: keyword()
});
const alerts = queryBuilder(alertRuleMappings)
.percolate({
field: 'query',
document: {
level: 'ERROR',
message: 'Database connection failed',
timestamp: '2024-01-15T10:30:00Z'
}
})
.size(100)
.build();Common use cases: alerting (match events against rules), content classification, saved search notifications, and metric threshold monitoring.
Compatibility
| elasticlink | Node.js | Elasticsearch |
|---|---|---|
| 1.0.0-beta.1 | 20, 22, 24 | 9.x (≥9.0.0) |
Tested against the versions listed. Peer dependency is @elastic/elasticsearch >=9.0.0.
Development
See CONTRIBUTING.md for development setup, design principles, and code style.
License
MIT © 2026 misterrodger