Create an index, and choose what each field does
An index is a table in your database, and its mapping decides what each field can do: full-text searchable, exact for grouping and sorting, or both. Declare a column text to search inside it, keyword to group and sort on it, and a multi-field when you need both.
The short answer
To make one column full-text searchable, declare it text when you create the index:
await es.indices.create({
index: 'shop.products',
mappings: { properties: {
description: { type: 'text' } // full-text searchable
} }
})That field now has a full-text index behind it: analyzed into terms, ranked by BM25, and reachable with match, match_phrase and fuzzy search. Everything below is how to choose when the answer is not that simple.
What each type gives you
| Type | Full-text search | Group / sort / aggregate | Use it for |
|---|---|---|---|
| text | yes, analyzed | no | prose: descriptions, messages, notes, titles you search inside |
| keyword | exact value only | yes | identifiers, tags, statuses, brands, e-mail addresses |
| long / double | no | yes | prices, counts, latencies, scores |
| date | no | yes | timestamps, in ISO-8601 or epoch milliseconds |
| boolean | no | yes | flags |
A text field cannot be grouped or sorted, and a keyword field cannot be searched inside. That is Elasticsearch's rule, not ours, and it is why the next section exists.
When you need both on one column
A product name you want to search inside and group by exactly needs two views of the same value. Declare a multi-field: the base is analyzed text, the sub-field is the exact keyword.
mappings: { properties: {
name: {
type: 'text', // match 'carbon marathon'
fields: { keyword: { type: 'keyword' } } // group by the exact name
}
} }Then search the base and aggregate the sub-field:
query: { match: { name: 'marathon' } } // searches the text
aggs: { by_name: { terms: { field: 'name.keyword' } } } // groups the exact valueIf you skip the mapping entirely
You do not have to create the index first. Write a document to a name that does not exist and the index is created for you, with dynamic mapping: every string gets both a full-text index and a .keyword twin, numbers become numeric, and anything that parses as a date becomes a date.
await es.index({
index: 'shop.reviews', // does not exist yet - created by this write
id: 'r-1',
document: { body: 'fits well, runs small', stars: 4 }
})That is the fastest way to start and the right default for logs and events. Declare a mapping when you want to be deliberate: to pick an analyzer, to keep a field out of the index, or to stop a string you never search from carrying a full-text index it does not need.
Adding a field to an index that already exists
Same call Elasticsearch uses. New fields are added; existing ones keep their type.
await es.indices.putMapping({
index: 'shop.products',
properties: { supplier: { type: 'keyword' } }
})Changing text to keyword on a live field would make every document already written disagree with the mapping. Create a new index with the mapping you want and _reindex into it — the same move you would make on a real cluster.
Checking what a field actually is
_field_caps reads the stored mapping and tells you, per field, what it is and whether it can be searched or aggregated. This is the call dashboards make on connect, and the fastest way to find out why an aggregation came back empty.
await es.fieldCaps({ index: 'shop.products', fields: '*' })
// name -> text searchable: true, aggregatable: false
// brand -> keyword searchable: true, aggregatable: true
// price -> long searchable: true, aggregatable: trueAnalyzers, when the language matters
A text field can declare an analyzer so that searching for running finds run. Set it per field in the mapping; the same analyzer is used when the document is indexed and when the query is parsed, so the two always agree.
mappings: { properties: {
description: { type: 'text', analyzer: 'english' }
} }An analyzer we cannot reproduce faithfully is refused when you create the index, rather than accepted and quietly ignored.
Index names fold
Index names are matched with case and separators folded, so Shop-Products, shop_products and shop.products are one index, not three. If a name would land on an index another name already owns, the create is refused with the owner named, rather than quietly writing into it.
// shop.products exists
await es.indices.create({ index: 'Shop-Products' })
// -> 400 already exists: [Shop-Products] differs from it only in
// case or separators and would alias its dataPick one spelling and keep it. The refusal exists so a typo cannot silently merge two datasets.
Deleting an index
await es.indices.delete({ index: 'shop.reviews' })This removes the index and its documents. A delete of a name that folds onto an index another name owns is refused, for the same reason a create is.
Elasticsearch is a trademark of Elasticsearch B.V., registered in the U.S. and in other countries. OriginChainDB is not affiliated with, endorsed by, or sponsored by Elasticsearch B.V. OriginChainDB implements a compatible HTTP API so that existing Elasticsearch clients can talk to it; it does not distribute Elasticsearch software.