1. Fundamentals
1.1 Origin: Lucene
- Lucene: Apache’s open-source Java full-text search engine library.
- Advantages: easy to extend, high performance (pure Java, embeddable).
- Drawbacks: complex to use — you must handle low-level details like index creation and query parsing, with no distributed support.
- Elasticsearch is built on Lucene, providing a distributed, easy-to-use RESTful API.
1.2 The tech stack (ELK)
- Elasticsearch: the storage, search, and analytics engine.
- Logstash: a server-side data processing pipeline that collects and transforms data before sending it to ES.
- Kibana: a visualization platform for building charts and dashboards and managing ES.
- Beats: lightweight data shippers that send to Logstash or ES.
1.3 Installing with Docker
Elasticsearch (single node):
docker run -d --name es \
-e "ES_JAVA_OPTS=-Xms512m -Xmx512m" \
-e "discovery.type=single-node" \
-v es-data:/usr/share/elasticsearch/data \
-v es-plugins:/usr/share/elasticsearch/plugins \
--privileged \
--network testNet \
-p 9200:9200 -p 9300:9300 \
elasticsearch:7.12.1
- 9200: the HTTP API port
- 9300: the internal node communication port
discovery.type=single-node: single-node mode (for testing)
Kibana:
docker run -d --name kibana \
-e ELASTICSEARCH_HOSTS=http://es:9200 \
--network=testNet \
-p 5601:5601 \
kibana:7.12.1
1.4 The Inverted Index
- Forward index: document ID → document content. Good for exact lookup by ID, but fuzzy search requires scanning all documents — inefficient.
- Inverted index:
- Document: a piece of JSON data stored in ES.
- Term: the smallest unit of a document after tokenization.
- Structure: term → list of document IDs (with position, frequency, etc.).
- Advantage: quickly locates all documents containing a given term, enabling efficient full-text search.
1.5 The Analyzer
A component that splits text into terms, made up of three parts:
- Character Filter: preprocessing (e.g. stripping HTML tags).
- Tokenizer: tokenizes by rules.
- Token Filter: further processes terms (lowercasing, removing stop words).
The IK analyzer: the most commonly used Chinese tokenization plugin.
- Install: download the matching-version zip into the
plugins/ikdirectory and restart ES. - Two modes:
ik_smart: coarsest-grained splitting.ik_max_word: finest-grained splitting.
- Custom dictionaries: edit
IKAnalyzer.cfg.xmland addext.dic(new words) andstopword.dic(stop words).
1.6 Basic concepts
- Index: a collection of documents of the same type, like a “table” in a database.
- Document: the basic storage unit in ES, serialized to JSON.
- Mapping: defines the types, analyzers, etc. of a document’s fields, like a “Schema” in a database.
Common Mapping field types:
- Strings:
text(tokenized),keyword(exact match, not tokenized). - Numbers:
long,integer,short,byte,double,float. - Date:
date. - Boolean:
boolean. - Geo:
geo_point(lat/lon),geo_shape(regions). - Object:
object(nested fields). - Array: not defined specially; ES supports it automatically.
2. Index Operations (RESTful API)
All operations send JSON-formatted data over the HTTP API.
2.1 Create an index
PUT /index-name
{
"settings": {
"number_of_shards": 3, // number of shards
"number_of_replicas": 2 // number of replicas
},
"mappings": {
"properties": {
"field-name": {
"type": "text", // field type
"analyzer": "ik_max_word" // analyzer
}
}
}
}
2.2 Read an index
GET /index-name
GET /index-name/_mapping
GET /_cat/indices?v # view all indexes
2.3 Update an index
- Full document update: a PUT request with the full JSON data, equivalent to deleting the old document and creating a new one.
- Partial document update: a POST request using the
_updateAPI.
POST /index-name/_update/document-ID
{
"doc": {
"field": "new value"
}
}
Mapping update limitation: once a field type is created, most types can’t be changed (e.g. text to keyword), but you can add new fields. To change a type, you must rebuild the index.
2.4 Delete an index
DELETE /index-name
2.5 Bulk operations
Complete multiple document inserts/deletes/updates in a single request.
POST /_bulk
{"index":{"_index":"idx","_id":"1"}}
{"title":"Doc 1"}
{"delete":{"_index":"idx","_id":"2"}}
{"update":{"_index":"idx","_id":"3"}}
{"doc":{"title":"Updated"}}
Practices for designing a Mapping from a database table:
- MySQL’s
varchar→ usetextfor full-text search,keywordfor exact query/sorting. - Numeric types map to
integer/long/float. - Use
dateuniformly for dates; a format can be specified. - Use
geo_pointfor lat/lon queries. - Set
index: falseon fields that don’t need searching to save space.
3. Java REST Client
The official recommendation is the Java High Level REST Client (7.x) and the Elasticsearch Java API Client (recommended for 8.x+). Here we use the 7.x high-level client as an example.
Add the dependency:
<dependency>
<groupId>org.elasticsearch.client</groupId>
<artifactId>elasticsearch-rest-high-level-client</artifactId>
<version>7.12.1</version>
</dependency>
Initialize the client:
RestHighLevelClient client = new RestHighLevelClient(
RestClient.builder(new HttpHost("localhost", 9200, "http"))
);
3.1 Index operations
// Create an index
CreateIndexRequest request = new CreateIndexRequest("my_index");
request.settings(Settings.builder()
.put("index.number_of_shards", 3)
.put("index.number_of_replicas", 2));
request.mapping("{\"properties\":{...}}", XContentType.JSON);
CreateIndexResponse response = client.indices().create(request, RequestOptions.DEFAULT);
// Delete an index
DeleteIndexRequest deleteRequest = new DeleteIndexRequest("my_index");
client.indices().delete(deleteRequest, RequestOptions.DEFAULT);
// Existence check
GetIndexRequest getRequest = new GetIndexRequest("my_index");
boolean exists = client.indices().exists(getRequest, RequestOptions.DEFAULT);
3.2 Document operations
// Add a document
IndexRequest indexReq = new IndexRequest("my_index").id("1");
User user = new User("John Smith", 25);
indexReq.source(JSON.toJSONString(user), XContentType.JSON);
IndexResponse indexResp = client.index(indexReq, RequestOptions.DEFAULT);
// Partial update
UpdateRequest updateReq = new UpdateRequest("my_index", "1")
.doc("age", 26);
client.update(updateReq, RequestOptions.DEFAULT);
// Bulk operations
BulkRequest bulk = new BulkRequest();
bulk.add(new IndexRequest("my_index").id("1").source(...));
bulk.add(new DeleteRequest("my_index", "2"));
client.bulk(bulk, RequestOptions.DEFAULT);
3.3 Query operations
Build the query conditions and execute the search.
SearchRequest searchRequest = new SearchRequest("my_index");
SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();
sourceBuilder.query(QueryBuilders.matchQuery("title", "elasticsearch"));
searchRequest.source(sourceBuilder);
SearchResponse response = client.search(searchRequest, RequestOptions.DEFAULT);
SearchHit[] hits = response.getHits().getHits();
for (SearchHit hit : hits) {
String json = hit.getSourceAsString();
User user = JSON.parseObject(json, User.class);
}
4. DSL Query Syntax
DSL (Domain Specific Language) builds queries based on JSON and is ES’s most powerful search method.
4.1 Query categories overview
- Leaf queries: query specific values on specific fields.
- Full Text: matches after tokenization, with relevance scoring.
- Term-Level: no tokenization, direct exact matching of terms.
- Geo: coordinate queries.
- Compound queries: combine multiple leaf or compound queries, modifying scoring, filtering, etc.
- Special queries: e.g.
script,exists.
4.2 Full-text queries
Apply to text fields; they tokenize and compute a relevance score _score.
match: fuzzy match, OR relationship after tokenization.
GET /index/_search
{
"query": {
"match": {
"content": "Elasticsearch intro"
}
}
}
match_phrase: phrase match, requiring the tokens to be in the same order and contiguous.
{ "query": { "match_phrase": { "content": "Elasticsearch intro" } } }
multi_match: multi-field match.
{ "query": { "multi_match": { "query": "intro", "fields": ["title", "content"] } } }
4.3 Exact queries
Apply to keyword, numbers, dates, booleans; they don’t compute a relevance score.
term: exact-value match.
{ "query": { "term": { "status": "active" } } }
range: range query.
{
"query": {
"range": {
"price": { "gte": 100, "lte": 500 }
}
}
}
terms: multi-value exact match.
{ "query": { "terms": { "category": ["Technology", "Education"] } } }
4.4 Geo queries
The field type must be geo_point.
// Rectangular range
{
"query": {
"geo_bounding_box": {
"location": {
"top_left": { "lat": 40, "lon": 116 },
"bottom_right": { "lat": 39, "lon": 117 }
}
}
}
}
// Distance range (center + radius)
{
"query": {
"geo_distance": {
"distance": "10km",
"location": { "lat": 39.9, "lon": 116.4 }
}
}
}
4.5 Compound Queries
bool query: combines multiple conditions, including must (AND), should (OR), must_not (NOT), and filter (filter, no scoring).
{
"query": {
"bool": {
"must": [
{ "match": { "title": "phone" } }
],
"filter": [
{ "range": { "price": { "gte": 1000, "lte": 3000 } } },
{ "term": { "brand": "Huawei" } }
],
"must_not": [
{ "term": { "status": "delisted" } }
],
"should": [
{ "term": { "feature": "5G" } },
{ "term": { "feature": "fast-charge" } }
],
"minimum_should_match": 1
}
}
}
filterdoesn’t participate in scoring and performs better; prefer it for filter conditions.
4.6 Sorting and pagination
Sorting:
{
"query": { "match_all": {} },
"sort": [
{ "price": "desc" },
{ "_score": "desc" }
]
}
Sorting on a text field requires enabling fielddata or using the keyword sub-field.
Pagination:
{
"query": { "match_all": {} },
"from": 0, // starting offset
"size": 10 // documents per page
}
The deep-pagination problem:
- As
fromgrows, ES has to fetchfrom+sizedocuments from each shard and sort them at the coordinating node; memory and latency rise sharply. - Solutions:
- search_after: uses the sort value of the previous page’s last document to query the next page in real time, with no
fromoverhead, but doesn’t support jumping to arbitrary pages. - scroll: generates a data snapshot, good for traversing all data (e.g. exports), but not real-time.
- Limit pagination depth (e.g.
max_result_window: 10000).
- search_after: uses the sort value of the previous page’s last document to query the next page in real time, with no
4.7 Highlighting
{
"query": { "match": { "content": "elasticsearch" } },
"highlight": {
"fields": {
"content": {
"pre_tags": ["<em>"],
"post_tags": ["</em>"]
}
}
}
}
The response returns an extra highlight field containing the highlighted snippets.
4.8 Aggregations
Aggregations extract statistics, groupings, and other analytical info from data.
Aggregation categories:
- Bucket aggregations: grouping, e.g.
terms,range,date_histogram. - Metric aggregations: compute statistics, e.g.
avg,sum,max,min,stats. - Pipeline aggregations: compute again on the results of other aggregations.
DSL aggregation example:
{
"size": 0, // don't return documents, only the aggregation result
"aggs": {
"brand_group": {
"terms": { "field": "brand" },
"aggs": {
"avg_price": { "avg": { "field": "price" } }
}
}
}
}
Meaning: group by brand and compute each group’s average price.
Implementing aggregations with the Java client:
SearchRequest searchRequest = new SearchRequest("items");
SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();
sourceBuilder.query(QueryBuilders.matchAllQuery());
// Build the aggregation
TermsAggregationBuilder aggregation = AggregationBuilders
.terms("brand_group")
.field("brand.keyword")
.subAggregation(AggregationBuilders.avg("avg_price").field("price"));
sourceBuilder.aggregation(aggregation);
searchRequest.source(sourceBuilder);
SearchResponse response = client.search(searchRequest, RequestOptions.DEFAULT);
Aggregations aggs = response.getAggregations();
Terms brandTerms = aggs.get("brand_group");
for (Terms.Bucket bucket : brandTerms.getBuckets()) {
String brand = bucket.getKeyAsString();
Avg avgPrice = bucket.getAggregations().get("avg_price");
System.out.println(brand + ":" + avgPrice.getValue());
}
5. Supplement on ES Core Principles
5.1 Shards and replicas
- Shard: an index is horizontally split into multiple shards, each an independent Lucene index.
- Advantages: distributed storage, parallel search, horizontal scaling of capacity and throughput.
- Replica: a copy of each primary shard, providing high availability and sharing query load.
- The shard count can’t be changed after index creation (it can be adjusted via the
_split/_shrinkAPI). - The replica count can be adjusted dynamically.
- The shard count can’t be changed after index creation (it can be adjusted via the
5.2 Write and search flows
- Write: the document is routed to a primary shard; after the primary writes it, it forwards to the replicas, and confirmation returns only when all are done.
- Search: the coordinating node forwards the request to the relevant shards (primary or replica); each shard returns results, and the coordinating node merges and sorts them.
5.3 Optimization tips
- Avoid too many fields and deep aggregations.
- Use
filterinstead ofmustto reduce scoring computation. - Design the Mapping sensibly; use
keywordinstead oftextfor exact search. - Separate hot and cold data, combined with Index Lifecycle Management (ILM).