Package Exports
- elasticlink
Readme
elasticlink
Beta: elasticlink is in active beta. APIs may change before the first stable release.
Type-safe, fluent API for Elasticsearch queries and index management
elasticlink simplifies building Elasticsearch queries and index management in TypeScript. Write type-checked queries that compile to valid Elasticsearch DSL with zero runtime overhead.
Features
- Mapping-Aware Type Safety: Define ES field types once via
mappings(), get compile-time constraints —match()only accepts text fields,term()only keyword fields, etc. - Fluent API: Chainable query builder with intuitive method names
- Zero Runtime Overhead: Compiles directly to Elasticsearch DSL objects
- Well-Tested: Comprehensive unit and integration test suite against live Elasticsearch
- Great DX: Excellent IntelliSense and error messages
- Types from the source: Types derived directly from
@elastic/elasticsearchfor accuracy and automatic alignment with Elasticsearch updates
Compatibility
| elasticlink | Node.js | Elasticsearch |
|---|---|---|
| 0.3.0-beta | 20, 22, 24 | 9.x (≥9.0.0) |
| 0.2.0-beta | 20, 22 | 9.x (≥9.0.0) |
| 0.1.0-beta | 20, 22 | 9.x (≥9.0.0) |
Tested against the versions listed. Peer dependency is @elastic/elasticsearch >=9.0.0.
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 { query, mappings, text, keyword, float, type Infer } from 'elasticlink';
// Define field types once — this is the single source of truth
const productMappings = mappings({
name: text(),
price: float(),
category: keyword(),
});
// Derive a TS type from mappings (optional, for use elsewhere)
type Product = Infer<typeof productMappings>;
// Build a type-safe query — field constraints are enforced at compile time
const q = query(productMappings)
.match('name', 'laptop') // ✅ 'name' is a text field
.range('price', { gte: 500, lte: 2000 })
.from(0)
.size(20)
.build();
// Send to Elasticsearch
const response = await client.search({ index: 'products', ...q });API Overview
Core Query Methods
Basic Queries
match(field, value, options?)- Full-text searchmultiMatch(fields, query, options?)- Search multiple fieldsmatchPhrase(field, query)- Exact phrase matchingmatchPhrasePrefix(field, query, options?)- Prefix phrase matching (search-as-you-type)term(field, value)- Exact term matchingterms(field, values)- Multiple exact valuesrange(field, conditions)- Range queries (gte, lte, gt, lt)exists(field)- Field existence checkwildcard(field, pattern)- Wildcard pattern matchingprefix(field, value)- Prefix matchingfuzzy(field, value, options?)- Typo toleranceids(values)- Match by document IDsmatchAll()- Match all documents
Geo Queries
geoDistance(field, center, options)- Distance-based searchgeoBoundingBox(field, options)- Bounding box searchgeoPolygon(field, options)- Polygon search
Vector Search (KNN)
knn(field, queryVector, options)- K-nearest neighbors semantic search
Advanced Queries
nested(path, fn, options?)- Nested object queriesregexp(field, pattern, options?)- Regular expression matchingconstantScore(fn, options?)- Constant scoring for filtersscript(options)- Script-based filteringscriptScore(query, script, options?)- Custom scoring with scriptspercolate(options)- Match documents against stored queries
Suggestions & Autocomplete
suggest(fn)- Add query suggestions (term, phrase, completion)term(name, text, options)- Term-level spell checkingphrase(name, text, options)- Phrase-level correctionscompletion(name, prefix, options)- Fast autocomplete
Boolean Logic
query(productMappings)
.bool()
.must(q => q.match('name', 'laptop')) // AND
.filter(q => q.range('price', { gte: 500 }))
.should(q => q.term('category', 'featured')) // OR
.mustNot(q => q.term('category', 'discontinued')) // NOT
.minimumShouldMatch(1)
.build();Conditional Building
Build queries dynamically based on runtime values:
const searchTerm = getUserInput();
const minPrice = getMinPrice();
query(productMappings)
.bool()
.must(q =>
q.when(searchTerm, q => q.match('name', searchTerm)) || q.matchAll()
)
.filter(q =>
q.when(minPrice, q => q.range('price', { gte: minPrice })) || q.matchAll()
)
.build();Query Parameters
query(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, // Per-field options (fragment_size, number_of_fragments, etc.)
pre_tags: ['<mark>'], // Top-level highlight options
post_tags: ['</mark>']
})
.build();Aggregations
Aggregations can be combined with queries or used standalone:
- Bucket:
terms(),dateHistogram(),histogram(),range() - Metric:
avg(),sum(),min(),max(),cardinality(),percentiles(),stats(),valueCount() - Composition:
subAgg()for nested aggregations
import { query, aggregations } from 'elasticlink';
// Combined query + aggregations (inline aggs auto-thread mappings)
const result = query(productMappings)
.term('category', 'electronics')
.aggs(agg =>
agg
.terms('by_category', 'category', { size: 10 })
.avg('avg_price', 'price')
)
.size(20)
.build();
// Standalone aggregations (no query) — use query(mappings, false)
const aggsOnly = query(productMappings, false)
.aggs(agg =>
agg
.terms('by_category', 'category')
.subAgg(sub =>
sub.avg('avg_price', 'price').max('max_price', 'price')
)
)
.size(0) // Common pattern: size=0 when only wanting agg results
.build();
// Standalone aggregation builder (for manual composition)
const standaloneAgg = aggregations(productMappings)
.avg('avg_price', 'price')
.terms('by_category', 'category', { size: 10 })
.build();Vector Search & Semantic Search
Requires Elasticsearch 8.0+
KNN (k-nearest neighbors) queries enable semantic search using vector embeddings from machine learning models.
import { query, mappings, text, keyword, float, denseVector } from 'elasticlink';
const productWithEmbeddingMappings = mappings({
name: text(),
description: text(),
price: float(),
category: keyword(),
embedding: denseVector({ dims: 384 }),
});
// Basic semantic search
const searchEmbedding = [0.23, 0.45, 0.67, 0.12, 0.89]; // From your ML model
const result = query(productWithEmbeddingMappings)
.knn('embedding', searchEmbedding, {
k: 10, // Return top 10 nearest neighbors
num_candidates: 100 // Consider 100 candidates per shard
})
.size(10)
.build();
// Semantic search with filters
const filtered = query(productWithEmbeddingMappings)
.knn('embedding', searchEmbedding, {
k: 20,
num_candidates: 200,
filter: {
bool: {
must: [{ term: { category: 'electronics' } }],
filter: [{ range: { price: { gte: 100, lte: 1000 } } }]
}
},
boost: 1.2, // Boost relevance scores
similarity: 0.7 // Minimum similarity threshold
})
.size(20)
.build();
// Hybrid search with aggregations
const hybridSearch = query(productWithEmbeddingMappings)
.knn('embedding', searchEmbedding, {
k: 100,
num_candidates: 1000,
filter: { term: { category: 'electronics' } }
})
.aggs(agg =>
agg
.terms('categories', 'category', { size: 10 })
.range('price_ranges', 'price', {
ranges: [
{ to: 100 },
{ from: 100, to: 500 },
{ from: 500 }
]
})
)
.size(20)
.build();Common Vector Dimensions:
- 384-768: Sentence transformers (all-MiniLM, all-mpnet)
- 512: Image embeddings (ResNet, ViT)
- 1536: OpenAI text-embedding-ada-002
- 3072: OpenAI text-embedding-3-large
Dense Vector Field Mapping Example:
import type { DenseVectorOptions } from 'elasticlink';
const mapping: DenseVectorOptions = {
dims: 384,
index: true,
similarity: 'cosine', // 'l2_norm', 'dot_product', or 'cosine'
index_options: {
type: 'hnsw',
m: 16,
ef_construction: 100
}
};Script Queries & Custom Scoring
Note: Scripts must be enabled in Elasticsearch configuration. Use with caution as they can impact performance.
import { query, mappings, text, keyword, float, long } from 'elasticlink';
const scoredProductMappings = mappings({
name: text(),
price: float(),
popularity: long(),
quality_score: float(),
});
// Script-based filtering
const filtered = query(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 = query(scoredProductMappings)
.scriptScore(
(q) => q.match('name', 'smartphone'),
{
source: "_score * Math.log(2 + doc['popularity'].value)",
lang: 'painless'
}
)
.size(20)
.build();Script Languages:
- painless (default): Elasticsearch's primary scripting language
- expression: Fast, limited expression language
- mustache: Template-based scripting
Percolate Queries
Percolate queries enable reverse search - match documents against stored queries. Perfect for alerting, content classification, and saved searches.
const alertRuleMappings = mappings({
query: percolator(),
name: keyword(),
severity: keyword(),
});
// Match document against stored queries
const alerts = query(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 alert rules
- Content Classification: Categorize documents in real-time
- Saved Searches: Notify users when new content matches their searches
- Monitoring: Trigger actions based on metric thresholds
Query Suggestions & Autocomplete
Elasticsearch Suggesters provide spell-checking, phrase correction, and autocomplete functionality. Perfect for search-as-you-type experiences and fixing user typos.
import { query, suggest, mappings, text, keyword, completion } from 'elasticlink';
const searchableMappings = mappings({
name: text(),
description: text(),
suggest_field: completion(), // Must be type: completion
});
// Term suggester - Fix typos in individual terms
const termSuggestions = suggest(searchableMappings)
.term('name-suggestions', 'laptpo', {
field: 'name',
size: 5,
suggest_mode: 'popular', // 'missing' | 'popular' | 'always'
string_distance: 'levenshtein',
max_edits: 2
})
.build();
// Completion suggester - Fast autocomplete
const autocomplete = suggest(searchableMappings)
.completion('autocomplete', 'lap', {
field: 'suggest_field',
size: 10,
skip_duplicates: true,
fuzzy: {
fuzziness: 'AUTO',
transpositions: true,
min_length: 3,
prefix_length: 1
}
})
.build();
// Combine with query - Search with autocomplete
const searchWithSuggestions = query(searchableMappings)
.match('name', 'laptpo')
.suggest((s) =>
s.term('spelling-correction', 'laptpo', {
field: 'name',
size: 3,
suggest_mode: 'popular'
})
)
.size(20)
.build();Suggester Types:
- Term: Suggests corrections for individual terms based on edit distance
- Phrase: Suggests corrections for entire phrases using n-gram language models
- Completion: Fast prefix-based autocomplete (requires
completionfield type)
Multi-Search API
Batch multiple search requests in a single API call using the NDJSON format.
import { query, msearch } from 'elasticlink';
const laptopQuery = query(productMappings)
.match('name', 'laptop')
.range('price', { gte: 500, lte: 2000 })
.build();
const phoneQuery = query(productMappings)
.match('name', 'smartphone')
.range('price', { gte: 300, lte: 1000 })
.build();
// Build as NDJSON string for Elasticsearch API
const ndjson = msearch(productMappings)
.addQuery(laptopQuery, { index: 'products', preference: '_local' })
.addQuery(phoneQuery, { index: 'products', preference: '_local' })
.build();
// Or build as array of objects
const array = msearch(productMappings)
.addQuery(laptopQuery, { index: 'products' })
.addQuery(phoneQuery, { index: 'products' })
.buildArray();NDJSON Format (for Elasticsearch _msearch endpoint):
{"index":"products","preference":"_local"}
{"query":{"bool":{"must":[{"match":{"name":"laptop"}},{"range":{"price":{"gte":500,"lte":2000}}}]}}}
{"index":"products","preference":"_local"}
{"query":{"bool":{"must":[{"match":{"name":"smartphone"}},{"range":{"price":{"gte":300,"lte":1000}}}]}}}
Header Options:
index: Target index/indices (string or array)routing: Routing value for shardingpreference: Node preference (_local, _primary, etc.)search_type: Search type (dfs_query_then_fetch, etc.)
Bulk Operations
Batch 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 (create or replace)
.index(
{ id: '1', name: 'Laptop Pro', price: 1299, category: 'electronics' },
{ _index: 'products', _id: '1' }
)
// Create (fail if exists)
.create(
{ id: '2', name: 'Wireless Mouse', price: 29, category: 'accessories' },
{ _index: 'products', _id: '2' }
)
// Update (partial document)
.update({
_index: 'products',
_id: '3',
doc: { price: 999 }
})
// Delete
.delete({ _index: 'products', _id: '4' })
.build();
// POST /_bulk with Content-Type: application/x-ndjsonNDJSON Format:
{"index":{"_index":"products","_id":"1"}}
{"id":"1","name":"Laptop Pro","price":1299,"category":"electronics"}
{"create":{"_index":"products","_id":"2"}}
{"id":"2","name":"Wireless Mouse","price":29,"category":"accessories"}
{"update":{"_index":"products","_id":"3"}}
{"doc":{"price":999}}
{"delete":{"_index":"products","_id":"4"}}
Update Options:
doc: Partial document mergescript: Script-based update (Painless)upsert: Document to insert if not existsdoc_as_upsert: Use doc as upsert documentretry_on_conflict: Retry count for version conflicts
Index Management
Configure index mappings, settings, and aliases declaratively.
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
// Content-Type: application/json
// Body: JSON.stringify(indexConfig)Produces:
{
"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": {}
}
}Field Helpers (shorthand for common field types):
import { text, keyword, integer, float, double, date, boolean, denseVector, scaledFloat, halfFloat } from 'elasticlink';
// Shorthand — pass options or use defaults
keyword() // { type: 'keyword' }
integer() // { type: 'integer' }
float() // { type: 'float' }
date() // { type: 'date' }
text({ analyzer: 'english' }) // { type: 'text', analyzer: 'english' }
denseVector({ dims: 384, index: true, similarity: 'cosine' })Field Types (25+ supported)
| Category | Helpers |
|---|---|
| Text | text, keyword, constantKeyword |
| Numeric | long, integer, short, byte, double, float, halfFloat, scaledFloat |
| Date | date, dateNanos |
| Boolean | boolean |
| Range | integerRange, floatRange, longRange, doubleRange, dateRange |
| Objects | object, nested, flattened |
| Spatial | geoPoint, geoShape |
| Specialized | ip, completion, tokenCount, denseVector, rankFeature, rankFeatures, binary, percolator |
| Alias | alias |
Mapping Properties
All field helpers accept an optional options object. Common options across types:
| 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 | Multi-fields for indexing the same value in multiple ways |
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
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
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 |
Examples
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 = query(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();Produces:
{
"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" }]
}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 = query(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();Produces:
{
"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
}Multi-Search — Parallel Queries
Batch multiple independent searches into a single HTTP request.
const listingMappings = mappings({
address: text(),
property_class: keyword(),
list_price: long(),
});
const condoSearch = query(listingMappings)
.bool()
.filter(q => q.term('property_class', 'condo'))
.filter(q => q.range('list_price', { lte: 2_000_000 }))
.aggs(agg => agg.avg('avg_price', 'list_price'))
.size(0)
.build();
const townhouseSearch = query(listingMappings)
.bool()
.filter(q => q.term('property_class', 'townhouse'))
.aggs(agg => agg.avg('avg_price', 'list_price').min('min_price', 'list_price'))
.size(0)
.build();
const ndjson = msearch(listingMappings)
.addQuery(condoSearch, { index: 'listings' })
.addQuery(townhouseSearch, { index: 'listings' })
.build();Produces:
{"index":"listings"}
{"query":{"bool":{"filter":[{"term":{"property_class":"condo"}},{"range":{"list_price":{"lte":2000000}}}]}},"aggs":{"avg_price":{"avg":{"field":"list_price"}}},"size":0}
{"index":"listings"}
{"query":{"bool":{"filter":[{"term":{"property_class":"townhouse"}}]}},"aggs":{"avg_price":{"avg":{"field":"list_price"}},"min_price":{"min":{"field":"list_price"}}},"size":0}
Suggesters — Autocomplete & Spell Check
const attorneyMappings = mappings({
name: text(),
practice_area: keyword(),
name_suggest: completion(),
});
// Standalone suggest request
const suggestions = query(attorneyMappings)
.suggest(s =>
s
.completion('autocomplete', 'kap', { field: 'name_suggest', size: 5 })
.term('spelling', 'wiliams', { field: 'name', size: 3 })
)
.size(0)
.build();Produces:
{
"suggest": {
"autocomplete": {
"prefix": "kap",
"completion": { "field": "name_suggest", "size": 5 }
},
"spelling": {
"text": "wiliams",
"term": { "field": "name", "size": 3 }
}
},
"size": 0
}Dynamic Search with Conditional Filters
const buildDynamicQuery = (filters: SearchFilters) => {
return query(productMappings)
.bool()
.must(q =>
q.when(filters.searchTerm,
q => q.match('name', filters.searchTerm, { boost: 2 })
) || q.matchAll()
)
.filter(q =>
q.when(filters.minPrice && filters.maxPrice,
q => q.range('price', { gte: filters.minPrice, lte: filters.maxPrice })
) || q.matchAll()
)
.filter(q =>
q.when(filters.category,
q => q.term('category', filters.category)
) || q.matchAll()
)
.from(filters.offset || 0)
.size(filters.limit || 20)
.build();
};Geospatial Search
const restaurantMappings = mappings({
name: text(),
cuisine: keyword(),
location: geoPoint(),
rating: float(),
});
const result = query(restaurantMappings)
.term('cuisine', 'italian')
.geoDistance(
'location',
{ lat: 40.7128, lon: -74.006 },
{ distance: '5km' }
)
.from(0)
.size(20)
.build();TypeScript Support
elasticlink provides mapping-aware TypeScript safety:
- Field-Type Constraints:
match()only accepts text fields,term()only keyword fields — enforced at compile time - Field Autocomplete: IntelliSense knows your field names and their types
Infer<S>: Derive TS document types from your mappings schema
import { query, 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 = query(userMappings).match('name', 'John').build();
// ❌ TypeScript error: 'email' is keyword, not text — use term() instead
const q2 = query(userMappings).match('email', 'john@example.com').build();
// ✅ Correct: use term() for keyword fields
const q3 = query(userMappings).term('email', 'john@example.com').build();Testing
# Run tests
npm test
# Watch mode
npm test:watch
# Coverage report
npm test:coverage
# Type check
npm run type-checkRoadmap
Current Release ✅
- Core query types (match, term, range, bool, etc.)
- Fuzzy queries for typo tolerance
- Query parameters (from, size, sort, timeout, etc.)
- Conditional query building
- Highlight support
- Aggregations (bucket and metric)
- Geo queries (distance, bounding box, polygon)
- Advanced patterns (regexp, constant_score)
- Sub-aggregation support
- Query + aggregations integration
- KNN (k-nearest neighbors) queries for vector search
- Semantic search with vector embeddings
- Dense vector field support
- Script queries and custom scoring
- Percolate queries for reverse search
- Multi-search API (NDJSON batched queries)
- Bulk operations (create, index, update, delete)
- Index management (mappings, settings, aliases)
- Query suggestions/completions (term, phrase, completion)
- Integration test suite against live Elasticsearch 9.x
- Types derived from official
@elastic/elasticsearchfor accuracy and completeness
Development
See CONTRIBUTING.md for development setup and code style.
License
MIT © 2026 misterrodger