【运维】Elsatic Search学习笔记

基本使用

  1. Elasticsearch(简称ES): 是一个开源的高扩展的分布式全文搜索引擎

Docker安装Elasticsearch[1](#Docker安装Elasticsearch1)

yml 复制代码
version: "3.1"
services:
  elasticsearch:
    image: elasticsearch:7.13.3
    container_name: elasticsearch
    privileged: true
    environment:
      - "cluster.name=elasticsearch" #设置集群名称为elasticsearch
      - "discovery.type=single-node" #以单一节点模式启动
      - "ES_JAVA_OPTS=-Xms512m -Xmx1096m" #设置使用jvm内存大小
      - bootstrap.memory_lock=true
    volumes:
      - ./es/plugins:/opt/docker/elasticsearch/plugins #插件文件挂载
      - ./es/data:/opt/docker/elasticsearch/data:rw #数据文件挂载
      - ./es/logs:/opt/docker/elasticsearch/logs:rw
    ports:
      - 9200:9200
      - 9300:9300
    deploy:
      resources:
        limits:
          cpus: "2"
          memory: 1000M
        reservations:
          memory: 200M
  kibana:
    image: kibana:7.13.3
    container_name: kibana
    depends_on:
      - elasticsearch #kibana在elasticsearch启动之后再启动
    environment:
      ELASTICSEARCH_HOSTS: http://elasticsearch:9200 #设置访问elasticsearch的地址
      I18N_LOCALE: zh-CN
    ports:
      - 5601:5601

然后启动

bash 复制代码
docker-compose up -d

系统架构

  1. 数据格式: Elasticsearch是面向文档型的数据库, 一条数据就是一个文档, Elasticsearch与MySQL的对应如下图
    1. 因为Elasticsearch更强调全文索引, 因此在新版本中, Type的概念已经被移除

基本使用

环境准备

python 复制代码
url = "http://server.passnight.local:9200"
import requests

索引操作

创建索引
python 复制代码
In [5]: requests.put(f"{url}/shopping").text
Out[5]: '{"acknowledged":true,"shards_acknowledged":true,"index":"shopping"}'

# put请求有幂等性, 不可以重复请求
In [8]: requests.put(f"{url}/shopping").json()
Out[8]:
{'error': {'root_cause': [{'type': 'resource_already_exists_exception',
    'reason': 'index [shopping/Pxr9PA93ThaGmVHeYsbcOQ] already exists',
    'index_uuid': 'Pxr9PA93ThaGmVHeYsbcOQ',
    'index': 'shopping'}],
  'type': 'resource_already_exists_exception',
  'reason': 'index [shopping/Pxr9PA93ThaGmVHeYsbcOQ] already exists',
  'index_uuid': 'Pxr9PA93ThaGmVHeYsbcOQ',
  'index': 'shopping'},
 'status': 400}
python 复制代码
In [3]: requests.delete(f"{url}/shopping").json()
Out[3]: {'acknowledged': True}

文档操作

创建文档
python 复制代码
In [15]: requests.post(f"{url}/shopping/_doc", json={"title":"华为手机", "category":"华为","price":8848}).json()
Out[15]:
{'_index': 'shopping',
 '_type': '_doc',
 '_id': 'R6kgxIwBJszNCAI73RvT',
 '_version': 1,
 'result': 'created',
 '_shards': {'total': 2, 'successful': 1, 'failed': 0},
 '_seq_no': 0,
 '_primary_term': 1}
# 创建文档, 并自定义id
In [16]: requests.post(f"{url}/shopping/_doc/1", json={"title":"华为手机", "category":"华为","price":8848}).json()
Out[16]:
{'_index': 'shopping',
 '_type': '_doc',
 '_id': '1',
 '_version': 1,
 'result': 'created',
 '_shards': {'total': 2, 'successful': 1, 'failed': 0},
 '_seq_no': 1,
 '_primary_term': 1}
查询文档
python 复制代码
In [20]: requests.get(f"{url}/shopping/_doc/1").json()
Out[20]:
{'_index': 'shopping',
 '_type': '_doc',
 '_id': '1',
 '_version': 1,
 '_seq_no': 1,
 '_primary_term': 1,
 'found': True,
 '_source': {'title': '华为手机', 'category': '华为', 'price': 8848}}

# 对于不存在的数据, 会返回{"found": false}
In [21]: requests.get(f"{url}/shopping/_doc/no-exists").json()
Out[21]: {'_index': 'shopping', '_type': '_doc', '_id': 'no-exists', 'found': False}

#可以通过`_search`来查询所有数据
In [22]: requests.get(f"{url}/shopping/_doc/_search").json()
Out[22]:
{'took': 530,
 'timed_out': False,
 '_shards': {'total': 1, 'successful': 1, 'skipped': 0, 'failed': 0},
 'hits': {'total': {'value': 2, 'relation': 'eq'},
  'max_score': 1.0,
  'hits': [{'_index': 'shopping',
    '_type': '_doc',
    '_id': 'R6kgxIwBJszNCAI73RvT',
    '_score': 1.0,
    '_source': {'title': '华为手机', 'category': '华为', 'price': 8848}},
   {'_index': 'shopping',
    '_type': '_doc',
    '_id': '1',
    '_score': 1.0,
    '_source': {'title': '华为手机', 'category': '华为', 'price': 8848}}]}}
修改文档

修改文档分为完全修改和部分修改

python 复制代码
# 全量修改
In [23]: requests.put(f"{url}/shopping/_doc/1", json={"title":"华为手机", "category":"华为","price":18848}).json()
Out[23]:
{'_index': 'shopping',
 '_type': '_doc',
 '_id': '1',
 '_version': 2,
 'result': 'updated',
 '_shards': {'total': 2, 'successful': 1, 'failed': 0},
 '_seq_no': 2,
 '_primary_term': 1}

# 查询可以发现数据已经发生改变了
In [24]: requests.get(f"{url}/shopping/_doc/1").json()
Out[24]:
{'_index': 'shopping',
 '_type': '_doc',
 '_id': '1',
 '_version': 2,
 '_seq_no': 2,
 '_primary_term': 1,
 'found': True,
 '_source': {'title': '华为手机', 'category': '华为', 'price': 18848}}

# 通过`post`请求`_update`路径可以实现局部修改
In [25]: requests.post(f"{url}/shopping/_update/1", json={"doc":{"price":8848}}).json()
Out[25]:
{'_index': 'shopping',
 '_type': '_doc',
 '_id': '1',
 '_version': 3,
 'result': 'updated',
 '_shards': {'total': 2, 'successful': 1, 'failed': 0},
 '_seq_no': 3,
 '_primary_term': 1}

In [26]: requests.get(f"{url}/shopping/_doc/1").json()
Out[26]:
{'_index': 'shopping',
 '_type': '_doc',
 '_id': '1',
 '_version': 3,
 '_seq_no': 3,
 '_primary_term': 1,
 'found': True,
 '_source': {'title': '华为手机', 'category': '华为', 'price': 8848}}
删除文档
bash 复制代码
In [27]: requests.delete(f"{url}/shopping/_doc/1").json()
Out[27]:
{'_index': 'shopping',
 '_type': '_doc',
 '_id': '1',
 '_version': 4,
 'result': 'deleted',
 '_shards': {'total': 2, 'successful': 1, 'failed': 0},
 '_seq_no': 4,
 '_primary_term': 1}
 
# 第二次删除显示`not_found`
In [28]: requests.delete(f"{url}/shopping/_doc/1").json()
Out[28]:
{'_index': 'shopping',
 '_type': '_doc',
 '_id': '1',
 '_version': 5,
 'result': 'not_found',
 '_shards': {'total': 2, 'successful': 1, 'failed': 0},
 '_seq_no': 5,
 '_primary_term': 1}

查询

基本查询

REST风格可以通过get查询

python 复制代码
In [9]: requests.get(f"{url}/shopping").json()
Out[9]:
{'shopping': {'aliases': {},
  'mappings': {},
  'settings': {'index': {'routing': {'allocation': {'include': {'_tier_preference': 'data_content'}}},
    'number_of_shards': '1',
    'provided_name': 'shopping',
    'creation_date': '1704096812988',
    'number_of_replicas': '1',
    'uuid': 'Pxr9PA93ThaGmVHeYsbcOQ',
    'version': {'created': '7130399'}}}}}

# 如果要查询所有索引, 可以通过以下地址
In [10]: requests.get(f"{url}/_cat/indices?v").text
Out[10]: 'health status index                           uuid                   pri rep docs.count docs.deleted store.size pri.store.size\ngreen  open   .kibana_7.13.3_001              zyqQO8vfQHuf5sRKb5lhOw   1   0         14           15      2.1mb          2.1mb\ngreen  open   .kibana-event-log-7.13.3-000001 ZSaKH8K4Tf2-sREY-W2pAQ   1   0          2            0       11kb           11kb\ngreen  open   .apm-custom-link                DcnQnCxfQievZaqAS5pOFQ   1   0          0            0       208b           208b\ngreen  open   .apm-agent-configuration        T8gVN9U7RgSUEru5b3CnoA   1   0          0            0       208b           208b\nyellow open   shopping                        Pxr9PA93ThaGmVHeYsbcOQ   1   1          0            0       208b           208b\ngreen  open   .tasks                          ZMXu_QfQTymuCAkC0YVzoA   1   0          2            0      7.8kb          7.8kb\ngreen  open   .kibana_task_manager_7.13.3_001 r3uPanyMR6eW-eVePJpeWA   1   0         10          395    169.9kb        169.9kb\n'

# 查询`category=华为`的数据
In [48]: requests.get(f"{url}/shopping/_search", params={"q":"category:华为"}).json()
Out[48]:
{'took': 2,
 'timed_out': False,
 '_shards': {'total': 1, 'successful': 1, 'skipped': 0, 'failed': 0},
 'hits': {'total': {'value': 2, 'relation': 'eq'},
  'max_score': 0.36464313,
  'hits': [{'_index': 'shopping',
    '_type': '_doc',
    '_id': 'R6kgxIwBJszNCAI73RvT',
    '_score': 0.36464313,
    '_source': {'title': '华为手机', 'category': '华为', 'price': 8848}},
   {'_index': 'shopping',
    '_type': '_doc',
    '_id': '1',
    '_score': 0.36464313,
    '_source': {'title': '华为手机', 'category': '华为', 'price': 8848}}]}}

# 查询不仅可以放在query parameter中, 还可以放在body中
In [53]: requests.get(f"{url}/shopping/_search", json={"query":{"match": {"category": "华为"}}}).json()
Out[53]:
{'took': 0,
 'timed_out': False,
 '_shards': {'total': 1, 'successful': 1, 'skipped': 0, 'failed': 0},
 'hits': {'total': {'value': 2, 'relation': 'eq'},
  'max_score': 0.36464313,
  'hits': [{'_index': 'shopping',
    '_type': '_doc',
    '_id': 'R6kgxIwBJszNCAI73RvT',
    '_score': 0.36464313,
    '_source': {'title': '华为手机', 'category': '华为', 'price': 8848}},
   {'_index': 'shopping',
    '_type': '_doc',
    '_id': '1',
    '_score': 0.36464313,
    '_source': {'title': '华为手机', 'category': '华为', 'price': 8848}}]}}
分页查询
python 复制代码
In [59]: requests.get(f"{url}/shopping/_search", json={"query":{"match_all": {}}, "from": 0, "size": 1}).json()
Out[59]:
{'took': 0,
 'timed_out': False,
 '_shards': {'total': 1, 'successful': 1, 'skipped': 0, 'failed': 0},
 'hits': {'total': {'value': 2, 'relation': 'eq'},
  'max_score': 1.0,
  'hits': [{'_index': 'shopping',
    '_type': '_doc',
    '_id': 'R6kgxIwBJszNCAI73RvT',
    '_score': 1.0,
    '_source': {'title': '华为手机', 'category': '华为', 'price': 8848}}]}}
列投影
python 复制代码
In [61]: requests.get(f"{url}/shopping/_search", json={"query":{"match_all": {}}, "_source": ["title"]}).json()
Out[61]:
{'took': 0,
 'timed_out': False,
 '_shards': {'total': 1, 'successful': 1, 'skipped': 0, 'failed': 0},
 'hits': {'total': {'value': 2, 'relation': 'eq'},
  'max_score': 1.0,
  'hits': [{'_index': 'shopping',
    '_type': '_doc',
    '_id': 'R6kgxIwBJszNCAI73RvT',
    '_score': 1.0,
    '_source': {'title': '华为手机'}},
   {'_index': 'shopping',
    '_type': '_doc',
    '_id': '1',
    '_score': 1.0,
    '_source': {'title': '华为手机'}}]}}
排序
python 复制代码
In [62]: requests.get(f"{url}/shopping/_search", json={"query":{"match_all": {}}, "_source": ["title"], "sort": {"price"
    ...: : {"order": "desc"}}}).json()
Out[62]:
{'took': 0,
 'timed_out': False,
 '_shards': {'total': 1, 'successful': 1, 'skipped': 0, 'failed': 0},
 'hits': {'total': {'value': 2, 'relation': 'eq'},
  'max_score': None,
  'hits': [{'_index': 'shopping',
    '_type': '_doc',
    '_id': 'R6kgxIwBJszNCAI73RvT',
    '_score': None,
    '_source': {'title': '华为手机'},
    'sort': [8848]},
   {'_index': 'shopping',
    '_type': '_doc',
    '_id': '1',
    '_score': None,
    '_source': {'title': '华为手机'},
    'sort': [8848]}]}}
复杂条件查询
python 复制代码
In [80]: queryParams = {
    ...:   "query": {
    ...:     "bool": {
    ...:       "must": [
    ...:         {
    ...:           "match": {
    ...:             "category": "华为"
    ...:           }
    ...:         }
    ...:       ]
    ...:     }
    ...:   }
    ...: }
    ...:

In [81]: requests.get(f"{url}/shopping/_search", json=queryParams).json()
Out[81]:
{'took': 0,
 'timed_out': False,
 '_shards': {'total': 1, 'successful': 1, 'skipped': 0, 'failed': 0},
 'hits': {'total': {'value': 2, 'relation': 'eq'},
  'max_score': 0.36464313,
  'hits': [{'_index': 'shopping',
    '_type': '_doc',
    '_id': 'R6kgxIwBJszNCAI73RvT',
    '_score': 0.36464313,
    '_source': {'title': '华为手机', 'category': '华为', 'price': 8848}},
   {'_index': 'shopping',
    '_type': '_doc',
    '_id': '1',
    '_score': 0.36464313,
    '_source': {'title': '华为手机', 'category': '华为', 'price': 8848}}]}}
分组查询
python 复制代码
In [73]: requests.get(f"{url}/shopping/_search", json=queryParams).json()
Out[73]:
{'took': 1,
 'timed_out': False,
 '_shards': {'total': 1, 'successful': 1, 'skipped': 0, 'failed': 0},
 'hits': {'total': {'value': 2, 'relation': 'eq'},
  'max_score': 1.0,
  'hits': [{'_index': 'shopping',
    '_type': '_doc',
    '_id': 'R6kgxIwBJszNCAI73RvT',
    '_score': 1.0,
    '_source': {'title': '华为手机', 'category': '华为', 'price': 8848}},
   {'_index': 'shopping',
    '_type': '_doc',
    '_id': '1',
    '_score': 1.0,
    '_source': {'title': '华为手机', 'category': '华为', 'price': 8848}}]},
 'aggregations': {'price_group': {'doc_count_error_upper_bound': 0,
   'sum_other_doc_count': 0,
   'buckets': [{'key': 8848, 'doc_count': 2}]}}}

映射

创建people索引

python 复制代码
In [106]: requests.put(f"{url}/people").json()
Out[106]: {'acknowledged': True, 'shards_acknowledged': True, 'index': 'people'}

创建mapping

python 复制代码
In [98]: body = {
    ...:   "properties": {
    ...:     "name": {
    ...:       "type": "text",
    ...:       "index": True
    ...:     },
    ...:     "sex": {
    ...:       "type": "keyword", # 不能分词, 需要完整匹配
    ...:       "index": True
    ...:     },
    ...:     "tel": {
    ...:       "type": "keyword",
    ...:       "index": False # 不创建索引, 因此不能查询
    ...:     }
    ...:   }
    ...: }
In [86]: requests.put(f"{url}/people/_mapping", json=body).text
Out[86]: '{"acknowledged":true}'
# 修改完之后就能查到了
In [109]: requests.get(f"{url}/people/_mapping").json()
Out[109]:
{'people': {'mappings': {'properties': {'name': {'type': 'text'},
    'sex': {'type': 'keyword'},
    'tel': {'type': 'keyword', 'index': False}}}}}

之后添加文档

python 复制代码
In [110]: requests.post(f"{url}/people/_create/1", json={"name":"小米", "sex": "男", "tel":12345678}).json()
Out[110]:
{'_index': 'people',
 '_type': '_doc',
 '_id': '1',
 '_version': 1,
 'result': 'created',
 '_shards': {'total': 2, 'successful': 1, 'failed': 0},
 '_seq_no': 0,
 '_primary_term': 1}

查询刚刚添加的数据

python 复制代码
# 查询`小`这一个字可以查到; 说明`小米`被分词了
In [111]: requests.get(f"{url}/people/_search", json={"query":{"match":{"name":"小华为"}}}).json()
Out[111]:
{'took': 257,
 'timed_out': False,
 '_shards': {'total': 1, 'successful': 1, 'skipped': 0, 'failed': 0},
 'hits': {'total': {'value': 1, 'relation': 'eq'},
  'max_score': 0.2876821,
  'hits': [{'_index': 'people',
    '_type': '_doc',
    '_id': '1',
    '_score': 0.2876821,
    '_source': {'name': '小米', 'sex': '男', 'tel': 12345678}}]}}

# keyword标注的没有分词效果, 因此`男性`不能匹配`男`
In [113]: requests.get(f"{url}/people/_search", json={"query":{"match":{"sex":"男性"}}}).json()
Out[113]:
{'took': 0,
 'timed_out': False,
 '_shards': {'total': 1, 'successful': 1, 'skipped': 0, 'failed': 0},
 'hits': {'total': {'value': 0, 'relation': 'eq'},
  'max_score': None,
  'hits': []}}

In [114]: requests.get(f"{url}/people/_search", json={"query":{"match":{"sex":"男"}}}).json()
Out[114]:
{'took': 0,
 'timed_out': False,
 '_shards': {'total': 1, 'successful': 1, 'skipped': 0, 'failed': 0},
 'hits': {'total': {'value': 1, 'relation': 'eq'},
  'max_score': 0.2876821,
  'hits': [{'_index': 'people',
    '_type': '_doc',
    '_id': '1',
    '_score': 0.2876821,
    '_source': {'name': '小米', 'sex': '男', 'tel': 12345678}}]}}

#

而无索引的数据不能查询

python 复制代码
In [115]: requests.get(f"{url}/people/_search", json={"query":{"match":{"tel":12345678}}}).json()
Out[115]:
{'error': {'root_cause': [{'type': 'query_shard_exception',
    'reason': 'failed to create query: Cannot search on field [tel] since it is not indexed.',
    'index_uuid': 'qy9xGKlDTkWf7WBYhwoxaA',
    'index': 'people'}],
  'type': 'search_phase_execution_exception',
  'reason': 'all shards failed',
  'phase': 'query',
  'grouped': True,
  'failed_shards': [{'shard': 0,
    'index': 'people',
    'node': '06gxMBq9QO6_Q5qg0ip-KA',
    'reason': {'type': 'query_shard_exception',
     'reason': 'failed to create query: Cannot search on field [tel] since it is not indexed.',
     'index_uuid': 'qy9xGKlDTkWf7WBYhwoxaA',
     'index': 'people',
     'caused_by': {'type': 'illegal_argument_exception',
      'reason': 'Cannot search on field [tel] since it is not indexed.'}}}]},
 'status': 400}

java API

索引操作

环境准备

java 复制代码
    private final static RestHighLevelClient client = new RestHighLevelClient(RestClient.builder(
            new HttpHost("server.passnight.local", 9200, "http")
    ));

    @AfterClass
    public static void tearDownClass() throws IOException {
        client.close();
    }

增删查索引

java 复制代码
/**
     * 增加索引
     */
    @Test
    public void createIndex() throws IOException {
        CreateIndexResponse response = client.indices()
                .create(new CreateIndexRequest("user"), RequestOptions.DEFAULT);
        // 索引操作成功
        Assert.assertTrue(response.isAcknowledged());
    }

    /**
     * 查询索引
     */
    @Test
    public void queryIndex() throws IOException {
        GetIndexResponse response = client.indices()
                .get(new GetIndexRequest("user"), RequestOptions.DEFAULT);

        System.out.println(response.getAliases());
        System.out.println(response.getMappings());
        System.out.println(response.getSettings());
    }

    /**
     * 删除索引
     */
    @Test
    public void deleteIndex() throws IOException {
        AcknowledgedResponse response = client.indices()
                .delete(new DeleteIndexRequest("user"), RequestOptions.DEFAULT);

        // 索引删除成功
        Assert.assertTrue(response.isAcknowledged());
    }

文档操作

环境准备

准备pojo

java 复制代码
@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class User {
    private String name;
    private String sex;
    private Integer age;
}

准备session和工具

java 复制代码
    private final static RestHighLevelClient client = new RestHighLevelClient(RestClient.builder(
            new HttpHost("server.passnight.local", 9200, "http")
    ));

    private final static ObjectMapper objectMapper = new ObjectMapper();

    @AfterClass
    public static void tearDownClass() throws IOException {
        client.close();
    }
文档增删查改
java 复制代码
    @Test
    public void createIndex() throws IOException {
        User user = User.builder()
                .name("张三")
                .age(30)
                .sex("男")
                .build();

        // 注意, es插入数据必须转成json格式
        IndexResponse response = client.index(new IndexRequest()
                        .index("user")
                        .id("1")
                        .source(objectMapper.writeValueAsString(user), XContentType.JSON),
                RequestOptions.DEFAULT);
        System.out.println(response.getResult());
    }

    @Test
    public void updateIndex() throws IOException {
        UpdateResponse response = client.update(new UpdateRequest()
                        .index("user")
                        .id("1")
                        .doc(Map.of("sex", "男"), XContentType.JSON),
                RequestOptions.DEFAULT);
        System.out.println(response.getResult());
    }

    @Test
    public void getIndex() throws IOException {
        GetResponse response = client.get(new GetRequest()
                        .index("user")
                        .id("1"),
                RequestOptions.DEFAULT);
        System.out.println(response.getSourceAsString());
    }

    @Test
    public void deleteIndex() throws IOException {
        DeleteResponse response = client.delete(new DeleteRequest()
                        .index("user")
                        .id("1"),
                RequestOptions.DEFAULT);
        System.out.println(response);
    }
批量增删
java 复制代码
    /**
     * 批量增加
     */
    @Test
    public void batchInsert() throws IOException {
        User user1 = User.builder()
                .name("张三")
                .age(30)
                .sex("男")
                .build();

        User user2 = User.builder()
                .name("李四")
                .age(30)
                .sex("男")
                .build();

        User user3 = User.builder()
                .name("王五")
                .age(30)
                .sex("男")
                .build();

        // 注意, es插入数据必须转成json格式
        BulkResponse response = client.bulk(new BulkRequest()
                        .add(new IndexRequest().index("user")
                                .id("1")
                                .source(objectMapper.writeValueAsString(user1), XContentType.JSON))
                        .add(new IndexRequest().index("user")
                                .id("2")
                                .source(objectMapper.writeValueAsString(user2), XContentType.JSON))
                        .add(new IndexRequest().index("user")
                                .id("3")
                                .source(objectMapper.writeValueAsString(user3), XContentType.JSON)),
                RequestOptions.DEFAULT);
        System.out.println(response.getTook());
        System.out.println(Arrays.toString(response.getItems()));
    }

    /**
     * 批量删除
     */
    @Test
    public void batchDelete() throws IOException {
        BulkResponse response = client.bulk(new BulkRequest()
                        .add(new DeleteRequest().index("user")
                                .id("1"))
                        .add(new DeleteRequest().index("user")
                                .id("2"))
                        .add(new DeleteRequest().index("user")
                                .id("3")),
                RequestOptions.DEFAULT);
        System.out.println(response.getTook());
        System.out.println(Arrays.toString(response.getItems()));
    }

复杂查询

环境准备
java 复制代码
    private final static RestHighLevelClient client = new RestHighLevelClient(RestClient.builder(
            new HttpHost("server.passnight.local", 9200, "http")
    ));

    private final static ObjectMapper objectMapper = new ObjectMapper();

    @AfterClass
    public static void tearDownClass() throws IOException {
        client.bulk(new BulkRequest()
                        .add(new DeleteRequest().index("user")
                                .id("1"))
                        .add(new DeleteRequest().index("user")
                                .id("2"))
                        .add(new DeleteRequest().index("user")
                                .id("3"))
                        .add(new DeleteRequest().index("user")
                                .id("4"))
                        .add(new DeleteRequest().index("user")
                                .id("5")),
                RequestOptions.DEFAULT);
        client.close();
    }


    /**
     * 增加一些数据, 用于后续高级操作的测试
     */
    @BeforeClass
    public static void setUpClass() throws IOException {
        User user1 = User.builder()
                .name("张三")
                .age(30)
                .sex("男")
                .build();

        User user2 = User.builder()
                .name("李四")
                .age(40)
                .sex("女")
                .build();

        User user3 = User.builder()
                .name("王五")
                .age(50)
                .sex("男")
                .build();

        User user4 = User.builder()
                .name("赵六")
                .age(60)
                .sex("女")
                .build();

        User user5 = User.builder()
                .name("钱七")
                .age(20)
                .sex("男")
                .build();

        // 注意, es插入数据必须转成json格式
        client.bulk(new BulkRequest()
                        .add(new IndexRequest().index("user")
                                .id("1")
                                .source(objectMapper.writeValueAsString(user1), XContentType.JSON))
                        .add(new IndexRequest().index("user")
                                .id("2")
                                .source(objectMapper.writeValueAsString(user2), XContentType.JSON))
                        .add(new IndexRequest().index("user")
                                .id("3")
                                .source(objectMapper.writeValueAsString(user3), XContentType.JSON))
                        .add(new IndexRequest().index("user")
                                .id("4")
                                .source(objectMapper.writeValueAsString(user4), XContentType.JSON))
                        .add(new IndexRequest().index("user")
                                .id("5")
                                .source(objectMapper.writeValueAsString(user5), XContentType.JSON))
                        // es不会立即写入数据, 因此需要设置刷入策略
                        // 见: https://www.elastic.co/guide/en/elasticsearch/reference/current/docs-refresh.html
                        .setRefreshPolicy(WriteRequest.RefreshPolicy.IMMEDIATE),
                RequestOptions.DEFAULT);
    }
全量查询
java 复制代码
    /**
     * 查询全量数据
     */
    @Test
    public void simpleQuery() throws IOException {
        SearchResponse response = client.search(new SearchRequest()
                        .indices("user")
                        .source(new SearchSourceBuilder()
                                .query(QueryBuilders.matchAllQuery())),
                RequestOptions.DEFAULT);
        response.getHits().forEach(hit -> System.out.println(hit.getSourceAsString()));
    }

该查询输出了全量数据

json 复制代码
{"name":"张三","sex":"男","age":30}
{"name":"李四","sex":"女","age":40}
{"name":"王五","sex":"男","age":50}
{"name":"赵六","sex":"女","age":60}
{"name":"钱七","sex":"男","age":20}
条件查询
java 复制代码
/**
 * 查询{@code age=30}的数据
 */
    public void conditionQuery() throws IOException {
        SearchResponse response = client.search(new SearchRequest()
                        .indices("user")
                        .source(new SearchSourceBuilder()
                                .query(QueryBuilders.termQuery("age", 30))),
                RequestOptions.DEFAULT);
        response.getHits().forEach(hit -> System.out.println(hit.getSourceAsString()));
    }

只输出了年龄为30的数据

json 复制代码
{"name":"张三","sex":"男","age":30}
分页查询
java 复制代码
/**
 * 分页查询
 */
@Test
    public void pageQuery() throws IOException {
        SearchResponse response = client.search(new SearchRequest()
                        .indices("user")
                        .source(new SearchSourceBuilder()
                                .query(QueryBuilders.matchAllQuery())
                                .from(2)
                                .size(2)),
                RequestOptions.DEFAULT);
        response.getHits().forEach(hit -> System.out.println(hit.getSourceAsString()));
    }

只输出了区间 [ 2 × ( 2 − 1 ) , 4 ] [2\times(2-1),4] [2×(2−1),4]的数据

java 复制代码
{"name":"王五","sex":"男","age":50}
{"name":"赵六","sex":"女","age":60}
结果排序
java 复制代码
    /**
     * 对查询结果根据年龄降序排序
     */
    @Test
    @Test
    public void sortedQuery() throws IOException {
        SearchResponse response = client.search(new SearchRequest()
                        .indices("user")
                        .source(new SearchSourceBuilder()
                                .query(QueryBuilders.matchAllQuery())
                                .sort("age", SortOrder.DESC)),
                RequestOptions.DEFAULT);
        response.getHits().forEach(hit -> System.out.println(hit.getSourceAsString()));
    }

输出结果根据年龄降序排序

json 复制代码
{"name":"赵六","sex":"女","age":60}
{"name":"王五","sex":"男","age":50}
{"name":"李四","sex":"女","age":40}
{"name":"张三","sex":"男","age":30}
{"name":"钱七","sex":"男","age":20}
字段投影
java 复制代码
    /**
     * 过滤部分字段
     */
    @Test
    public void projectionQuery() throws IOException {
        SearchResponse response = client.search(new SearchRequest()
                        .indices("user")
                        .source(new SearchSourceBuilder()
                                .query(QueryBuilders.matchAllQuery())
                                .fetchSource(new String[]{"name"}, new String[]{})),
                RequestOptions.DEFAULT);
        response.getHits().forEach(hit -> System.out.println(hit.getSourceAsString()));
    }

只输出了字段name

json 复制代码
{"name":"张三"}
{"name":"李四"}
{"name":"王五"}
{"name":"赵六"}
{"name":"钱七"}
组合条件查询
java 复制代码
    /**
     * 组合条件查询
     */
    @Test
    public void boolQuery() throws IOException {
        SearchResponse response = client.search(new SearchRequest()
                        .indices("user")
                        .source(new SearchSourceBuilder()
                                .query(QueryBuilders.boolQuery()
                                        .must(QueryBuilders.matchQuery("age", 30))
                                        .must(QueryBuilders.matchQuery("sex", "男")))),
                RequestOptions.DEFAULT);
        response.getHits().forEach(hit -> System.out.println(hit.getSourceAsString()));
    }

只输出了age=30 and sex='男'的数据

json 复制代码
{"name":"张三","sex":"男","age":30}
范围查询
java 复制代码
    /**
     * 范围查询
     */
    @Test
    public void rangeQuery() throws IOException {
        SearchResponse response = client.search(new SearchRequest()
                        .indices("user")
                        .source(new SearchSourceBuilder()
                                .query(QueryBuilders.rangeQuery("age")
                                        .gte(30)
                                        .lt(50))),
                RequestOptions.DEFAULT);
        response.getHits().forEach(hit -> System.out.println(hit.getSourceAsString()));
    }

这样只输出 a g e ∈ [ 30 , 50 ) age \in [30,50) age∈[30,50)的数据

json 复制代码
{"name":"张三","sex":"男","age":30}
{"name":"李四","sex":"女","age":40}
模糊查询
java 复制代码
    /**
     * 模糊查询
     */
    @Test
    public void fuzzyQuery() throws IOException {
        SearchResponse response = client.search(new SearchRequest()
                        .indices("user")
                        .source(new SearchSourceBuilder()
                                .query(QueryBuilders.fuzzyQuery("name", "王")
                                        .fuzziness(Fuzziness.ONE))), // 只能差一个字
                RequestOptions.DEFAULT);
        response.getHits().forEach(hit -> System.out.println(hit.getSourceAsString()));
    }

输出为

json 复制代码
{"name":"王五","sex":"男","age":50}
{"name":"张三","sex":"男","age":30}
{"name":"李四","sex":"女","age":40}
{"name":"赵六","sex":"女","age":60}
{"name":"钱七","sex":"男","age":20}
高亮查询
java 复制代码
/**
 * 高亮查询
 */
@Test
public void highlightQuery() throws IOException {
    SearchResponse response = client.search(new SearchRequest()
                    .indices("user")
                    .source(new SearchSourceBuilder()
                            .query(QueryBuilders.termQuery("name", "王"))
                            .highlighter(new HighlightBuilder()
                                    // 添加html标签高亮(只在浏览器中生效)
                                    .preTags("<font color='red'>")
                                    .postTags("</font>")
                                    .field("name"))),
            RequestOptions.DEFAULT);
    response.getHits().forEach(hit -> System.out.println(hit.getHighlightFields()));
}

匹配部分被打上了html标签, 以高亮显示

json 复制代码
{name=[name], fragments[[<font color='red'>王</font>五]]}
聚合查询

求最大年龄

java 复制代码
    /**
     * 聚合查询; 求最大年龄
     */
    @Test
    public void maxAggQuery() throws IOException {
        SearchResponse response = client.search(new SearchRequest()
                        .indices("user")
                        .source(new SearchSourceBuilder()
                                .aggregation(AggregationBuilders.max("ageAge")
                                        .field("age"))),
                RequestOptions.DEFAULT);
        response.getAggregations()
                .asMap()
                .forEach((key, value) -> System.out.printf("%s:%s", key, ((NumericMetricsAggregation.SingleValue) value).getValueAsString()));
    }

输出为最大年龄

bash 复制代码
ageAge:60.0

求各个分组的数量

java 复制代码
    /**
     * 聚合查询: 求各个年龄的数量
     */
    @Test
    public void groupQuery() throws IOException {
        SearchResponse response = client.search(new SearchRequest()
                        .indices("user")
                        .source(new SearchSourceBuilder()
                                .aggregation(AggregationBuilders.terms("ageGroup")
                                        .field("age"))),
                RequestOptions.DEFAULT);
        System.out.println(response);
    }

聚合查询结果局部的输出为

json 复制代码
"aggregations": {
    "lterms#ageGroup": {
      "doc_count_error_upper_bound": 0,
      "sum_other_doc_count": 0,
      "buckets": [
        { "key": 20, "doc_count": 1 },
        { "key": 30, "doc_count": 1 },
        { "key": 40, "doc_count": 1 },
        { "key": 50, "doc_count": 1 },
        { "key": 60, "doc_count": 1 }
      ]
    }
  }

系统架构

核心概念

  1. 索引: 索引是一个拥有共性的文档的集合 例如有一个订单数据的索引, 存储了订单数据
  2. 类型: 在一个索引当中, 可以定义一种或多种类型 5.x版本之前一个索引支持多个类型, 6.x只有一种type, 而7.x默认不再支持自定义索引类型, 因为es和关系型数据库还是有本质上的区别
  3. 文档: 文档是一个可以被索引基础信息单元 , 也就是一条数据 在一个index/type里面, 可以存储任意多的文档
  4. 字段(Field): 相当于数据表的字段, 对文档根据不同属性进行分类和标识
  5. 映射(Mapping): 映射是处理数据的方式 , 用于对数据做一些限制 如数据的类型/是否要添加索引
  6. 分片(Shards): 一个索引可以存储超出单个节点硬件限制的大量数据, 为了解决这个问题, ES提供了将索引划分为多分的能力, 每一份就是一份分片
  7. 副本(replicas): ES允许对分片创建一份或多份拷贝 , 这些拷贝就是副本 副本即提高了系统的可用性 , 也提高了系统的吞吐
  8. 分配(Allocation): 数据有主分片和副本之分, 而该管理是由master节点管理的 例如住分片和副本不能在同一个节点上, 否则不能发挥副本的优势

集群模式架构

集群模式下会有一下特性:

故障转移

故障转移: 当集群中有多个节点 他们的cluster.name一致, 则可以为数据创建副本; 当某个节点宕机后, 数据并不会丢失

  1. 对于2.4中的node-1001宕机后, 主分片和主节点会发生转移

  2. node-1001重新加入集群后, node-1001依旧可以提供服务:

水平扩容

水平扩容: 对于正在增长中的数据按需扩容 , 当新的节点加入时, 数据会为了分散负载而对分片重新分配

  1. 如添加从两个节点扩容到三个节点, 数分布的变化(假设一个索引有两个三个分片, 且有一个副本):

  2. 然后再讲副本数+1:

分片路由

分片路由: 当有数据写入时, 数据会根据一下路由规则选择写入位置

  1. 数据会优先写入主分片, 然后才会写副本; 主分片的位置 = h a s h ( i d ) % n u m 主分片 =hash(id) \% num_{主分片} =hash(id)%num主分片
  2. 数据查询不一定要查询主分片, 因此可以随便选择需要访问的分片

集群模式下的读写流程

写流程

ES写入时一致性可以通过consistency配置, 取值有

  1. one: 表示主分片写完后响应
  2. all: 所有分片写完后完成响应
  3. quorum: 大于一半的分片写完后响应 这个是默认值

下图中, 使用红色代表主节点, 绿色代表主分片

  1. 客户端请求任意一个集群节点 该节点被称为协调节点
  2. 协调节点会将请求转发到指定节点
  3. 主分片保存数据, 并将数据发送给副本
  4. 副本保存后反馈主分片
  5. 主分片保存后反馈协调节点
  6. 协调节点反馈用户

读流程

ES可以从主分片和任意副本读取数据, 其流程为:

  1. 客户端请求任意一个集群节点
  2. 协调节点以轮询策略选择真正执行查询操作的分片
  3. 协调节点会将请求转发给执行节点, 执行节点查询结果并将结果返回给客户端

更新流程

  1. 客户端请求任意一个节点
  2. 协调节点会将请求转发到主分片所在的节点, 并写入数据
  3. 之后再执行副本同步的流程

多文档操作流程

ES提供bulkmgetapi提供多文档操作, 批量处理与单节点操作一样, 只是ES会自动对批量操作进行分配, 然后执行

分片原理

分片事ES最小的工作单元, 多个分片组成一个索引; 而分片索引的数据结构是倒排索引

倒排索引

  1. 词条: 索引中最小的存储单元查询单元 英文中一般是一个单词, 而中文中一般是一个词
  2. 词典: 是词条的集合 一般用B+Tree或HashTable实现
  3. 倒排表: 每个词条对应的文档id 查询会先根据词典判断是否在倒排表当中, 以判断是否要进行后续查询

文档搜索

  1. 倒排索引被写入磁盘之后是不可改变的, 因此有以下优势
    1. 不需要锁: 不能修改的东西无需担心多线程更新的问题
    2. 缓存容易: 因为索引不变, 所以缓存一直有效; 且基于该数据建立的更高层次的缓存也一直有效 如filter缓存
    3. 可压缩: 单个大倒排索引因为不变的缘故所以可以被压搜, 进而减少磁盘IO和内存使用量
  2. 因此也有以下劣势:
    1. 不变值难以修改, 修改成本高
  3. 为了解决上述问题, ES引入了动态更新索引: 用更新索引来反应变化, 空闲时用动态更新索引来更新倒排索引
  4. ES基于Lucene, 因此具有的概念, 每个段是一个倒排索引, 而查询时基于段的查询

近实时搜索

  1. 因为ES是根据段进行搜索的, ES的段落盘之后 才能完成搜索; 因此写入性能高 但会导致查询延迟 这里的查询延迟指延迟一段时间后才能获取最新的数据
  2. 延时产生的原因是: 主分片写入的演示+并行写入副本分片的延时
  3. 为了防止断点导致数据写入不完整, 类似于数据库的redo log; ES 引入了Translog 保存写入的片段 和数据库不同的是, 数据库是先写log再落盘; 而es因为写内存具有大量逻辑很容易失败, 所以是先写内存再写日志
  4. 为了提高实时性, 当数据写入到操作系统文件缓存 之后就可以提供服务, 这个过程称为refresh , 之后再flush 到磁盘中 默认情况下, fresh时间为1s, 而flush时间间隔为30min

文档分析

分析包括以下过程:

  1. 将文本分成适合于倒排索引的独立词条
  2. 将词条转化为统一格式以提高他们的可搜索性
  3. 分析器 包含以下三个组件
    1. 字符过滤器: 在分词前整理字符串 如将&转化为and; 或过滤掉html的标签等
    2. 分词器: 将字符串分为多个词条 最简单的情况下, 就是通过空白分隔字符
    3. token过滤器: 将词条转为统一格式 *如小写化所有单词, 删除and, a, the等无用的单词, 或增加词条, 类似jump, leap这样的同义词

内置分析器

ES软件内置了一些分析器

  1. 标准分析器: 删除绝大部分的标点, 并将词条小写化
  2. 简单分析器: 在任何不是字母的地方分隔文本
  3. 空格分析器: 在空格地方分隔文本
  4. 语言分析器: 根据特定的语言进行分词 如英语语言分析器就会删掉无意义的the

分析器使用

例如以下是标准分析器, 它保留了token及其类型/位置值等基本信息

python 复制代码
In [9]: requests.get(f"{url}/_analyze", json={"analyzer":"standard","text":"Text to analyze"}).json()
Out[9]:
{'tokens': [{'token': 'text',
   'start_offset': 0,
   'end_offset': 4,
   'type': '<ALPHANUM>',
   'position': 0},
  {'token': 'to',
   'start_offset': 5,
   'end_offset': 7,
   'type': '<ALPHANUM>',
   'position': 1},
  {'token': 'analyze',
   'start_offset': 8,
   'end_offset': 15,
   'type': '<ALPHANUM>',
   'position': 2}]}

中文分词

默认的分词器会将所有中文当做象形文字分词, 即将每个字都分开

python 复制代码
In [11]: requests.get(f"{url}/_analyze", json={"text":"测试单词"}).json()
Out[11]:
{'tokens': [{'token': '测',
   'start_offset': 0,
   'end_offset': 1,
   'type': '<IDEOGRAPHIC>',
   'position': 0},
  {'token': '试',
   'start_offset': 1,
   'end_offset': 2,
   'type': '<IDEOGRAPHIC>',
   'position': 1},
  {'token': '单',
   'start_offset': 2,
   'end_offset': 3,
   'type': '<IDEOGRAPHIC>',
   'position': 2},
  {'token': '词',
   'start_offset': 3,
   'end_offset': 4,
   'type': '<IDEOGRAPHIC>',
   'position': 3}]}

IK分词器是一个中文分词器, 可用于分词中文文本; IK 分词器需要单独安装

bash 复制代码
passnight@passnight-s600:~$ docker exec -it elasticsearch bash
[root@f10b92eeb7c9 elasticsearch]# bin/elasticsearch-plugin install https://github.com/medcl/elasticsearch-analysis-ik/releases/download/v7.13.3/elasticsearch-analysis-ik-7.13.3.zip
# 之后再宿主机上重启容器
passnight@passnight-s600:~/tmp$ docker restart elasticsearch

安装完成之后就可以使用中文分词器分词, 可以看到符合中文语义

其中, ik分词器有两个模式:

  1. ik_max_word: 按照文本最细粒度拆分
  2. ik_smart: 按照文本最粗粒度拆分
python 复制代码
In [17]: requests.get(f"{url}/_analyze", json={"analyzer":"ik_max_word","text":"中国人"}).json()
Out[17]:
{'tokens': [{'token': '中国人',
   'start_offset': 0,
   'end_offset': 3,
   'type': 'CN_WORD',
   'position': 0},
  {'token': '中国',
   'start_offset': 0,
   'end_offset': 2,
   'type': 'CN_WORD',
   'position': 1},
  {'token': '国人',
   'start_offset': 1,
   'end_offset': 3,
   'type': 'CN_WORD',
   'position': 2}]}

In [18]: requests.get(f"{url}/_analyze", json={"analyzer":"ik_smart","text":"中国人"}).json()
Out[18]:
{'tokens': [{'token': '中国人',
   'start_offset': 0,
   'end_offset': 3,
   'type': 'CN_WORD',
   'position': 0}]}

中文分词器也允许自己扩展词汇; 如下面的弗雷尔卓德表示一个词

python 复制代码
In [19]: requests.get(f"{url}/_analyze", json={"analyzer":"ik_max_word","text":"弗雷尔卓德"}).json()
Out[19]:
{'tokens': [{'token': '弗',
   'start_offset': 0,
   'end_offset': 1,
   'type': 'CN_CHAR',
   'position': 0},
  {'token': '雷',
   'start_offset': 1,
   'end_offset': 2,
   'type': 'CN_CHAR',
   'position': 1},
  {'token': '尔',
   'start_offset': 2,
   'end_offset': 3,
   'type': 'CN_CHAR',
   'position': 2},
  {'token': '卓',
   'start_offset': 3,
   'end_offset': 4,
   'type': 'CN_CHAR',
   'position': 3},
  {'token': '德',
   'start_offset': 4,
   'end_offset': 5,
   'type': 'CN_CHAR',
   'position': 4}]}

完成这个操作需要修改分词器的配置文件, 添加自定义分词[2](#2)

bash 复制代码
passnight@passnight-s600:~$ docker exec -it elasticsearch bash
[root@f10b92eeb7c9 elasticsearch]# cd config/analysis-ik
# 配置自定义词典
[root@f10b92eeb7c9 analysis-ik]# vi IKAnalyzer.cfg.xml
xml 复制代码
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE properties SYSTEM "http://java.sun.com/dtd/properties.dtd">
<properties>
    <comment>IK Analyzer 扩展配置</comment>
    <!--用户可以在这里配置自己的扩展字典 -->
    <entry key="ext_dict">custom.dic</entry>
    <!--用户可以在这里配置自己的扩展停止词字典-->
    <!-- <entry key="ext_stopwords">custom/ext_stopword.dic</entry> -->
    <!--用户可以在这里配置远程扩展字典 -->
    <!-- <entry key="remote_ext_dict">location</entry> -->
    <!--用户可以在这里配置远程扩展停止词字典-->
    <!-- <entry key="remote_ext_stopwords">http://xxx.com/xxx.dic</entry> -->
</properties>
bash 复制代码
# 将自定义分词添加到自定义词典中
[root@f10b92eeb7c9 analysis-ik]# vi custom.dic
txt 复制代码
弗雷尔卓德
bash 复制代码
# 重启es, 使配置生效
passnight@passnight-s600:~/tmp$ docker restart elasticsearch

之后再请求, 可以看到自定义分词已经生效了

python 复制代码
In [24]: requests.get(f"{url}/_analyze", json={"analyzer":"ik_max_word","text":"弗雷尔卓德"}).json()
Out[24]:
{'tokens': [{'token': '弗雷尔卓德',
   'start_offset': 0,
   'end_offset': 5,
   'type': 'CN_WORD',
   'position': 0}]}

自定义分词器

如上文所说, 自定义分词器需要定义: 字符过滤器/分词器和token过滤器三个模块

文档处理

文档冲突

两个用户同时读取原始文档, 并更新, 此时es会重新索引整个文档 , 这样只有后被索引的文档才会被保存在es中; 而前面一个更新操作会被丢失 类似与SQL事务的丢失更新, 上图两个用户同时请求-1

乐观并发控制

  1. 使用版本号来判断更新是否能够提交, 若不能提交的话, 会返回操作失败

下面华为手机被更新了一次 而请求附带参数version=0, 可以看到因为乐观并发控制, 无法成功修改

python 复制代码
In [7]: In [3]: requests.get(f"{url}/shopping/_doc/1").json()
Out[7]:
{'_index': 'shopping',
 '_type': '_doc',
 '_id': '1',
 '_version': 2,
 '_seq_no': 1,
 '_primary_term': 1,
 'found': True,
 '_source': {'title': '华为手机', 'category': '华为', 'price': 18848}}

# 因乐观锁无法更新
In [8]: requests.post(f"{url}/shopping/_update/1", json={"doc":{"price":8848}}, params={"if_seq_no":0,"if_primary_term":
   ...: 0}).json()
Out[8]:
{'error': {'root_cause': [{'type': 'action_request_validation_exception',
    'reason': 'Validation Failed: 1: ifSeqNo is set, but primary term is [0];'}],
  'type': 'action_request_validation_exception',
  'reason': 'Validation Failed: 1: ifSeqNo is set, but primary term is [0];'},
 'status': 400}

# 序号为1, 说明拿到的时最新的版本, 可以正常更新
In [9]: requests.post(f"{url}/shopping/_update/1", json={"doc":{"price":8848}}, params={"if_seq_no":1,"if_primary_term":
   ...: 1}).json()
Out[9]:
{'_index': 'shopping',
 '_type': '_doc',
 '_id': '1',
 '_version': 3,
 'result': 'updated',
 '_shards': {'total': 2, 'successful': 1, 'failed': 0},
 '_seq_no': 2,
 '_primary_term': 1}

es还支持外部版本控制 , 可以自定义版本号;

python 复制代码
In [11]: requests.post(f"{url}/shopping/_doc/1", json={"price":8848}, params={"version":4,"version_type":"external"}).js
    ...: on()
Out[11]:
{'_index': 'shopping',
 '_type': '_doc',
 '_id': '1',
 '_version': 4,
 'result': 'updated',
 '_shards': {'total': 2, 'successful': 1, 'failed': 0},
 '_seq_no': 3,
 '_primary_term': 1}

# 不指定自定义版本号的话会报错
In [12]: requests.post(f"{url}/shopping/_doc/1", json={"price":8848}, params={"version":4}).json()
Out[12]:
{'error': {'root_cause': [{'type': 'action_request_validation_exception',
    'reason': 'Validation Failed: 1: internal versioning can not be used for optimistic concurrency control. Please use `if_seq_no` and `if_primary_term` instead;'}],
  'type': 'action_request_validation_exception',
  'reason': 'Validation Failed: 1: internal versioning can not be used for optimistic concurrency control. Please use `if_seq_no` and `if_primary_term` instead;'},
 'status': 400}

悲观并发控制

  1. 操作数据时将该数据锁住, 禁止其他请求操作该数据索引都是存储在磁盘当中

优化

硬件优化

  1. es是基于Lucene开发的, 因此磁盘 在现代服务器上通常都是瓶颈, 为了优化es磁盘的性能可以采用以下方案
    1. 使用SSD: SSD的性能要远远由于hhd
    2. 使用raid: raid尤其是raid0可以显著提高磁盘性能
    3. 使用多块硬盘: ES设置多个path.data目录就可以将数据条带化分配在他们上面
    4. 尽量少使用远程挂载存储: 远程存储的性能要低于本地存储

分片策略

  1. 设置合理的分片数: 分片不是越大越好的, 每个分片都是有代价的
    1. 一个分片底层是一个Lucene索引, 会消耗一定的文件句柄/内存/CPU
    2. 每个搜索请求都要命中索引中的每一个分片, 若分片处于同一节点, 可能会导致单节点的资源竞争
    3. 计算相关度的词频统计信息是基于分片的, 若有过多的分片, 每个分片的都只有很少的数据, 进而会导致数据都只有很低的相关度
  2. 一般有以下经验:
    1. 每个分片占用的硬盘容量不超过JVM堆大小一般不超过32G, 这样若索引总量在500G左右, 则分片大小在16个左右
    2. 分片数一般不超过节点数的三倍: 若分片数大大多于节点数, 则无法享受到多节点带来的高可用及高性能优势
    3. 节点数 ≤ 主分片数 × ( 副本数 + 1 ) 节点数\le主分片数\times(副本数+1) 节点数≤主分片数×(副本数+1)
  3. 推迟分片分配: 若某个节点宕机, 会发生分片重分配的问题
    1. 当某个节点宕机后, 集群会等待一段时间来判断节点是否重新加入, 若重分配等待时间太短, 节点可能只是暂时失去响应就导致了重分配进而影响性能

数据路由

  1. 查询文档时, es是通过以下公式计算分片 s h a r d = h a s h ( r o u t i n g ) m o d    n u m b e r _ o f _ p r i m a r y _ s h a r d s shard=hash(routing)\mod number\_of\_primary\_shards shard=hash(routing)modnumber_of_primary_shards, 这里routing的默认值是文档id , 但也可以使用自定义值 如用户id, 这样在某些场景下可以提升性能
  2. 实际情况下有以下两种查询方式:
    1. 不带路由的查询: 查询不需要知道数据在那个分片上, 因此只有以下两个步骤
      1. 分发: 请求到达协调节点后, 协调节点将查询分发到每个分片上
      2. 聚合:协调节点收集到每个分片上的查询结果, 并对查询结果排序, 返回用户
    2. 带路由的查询:
      1. 可以直接根据routing信息定位到某个分片, 而不需要查询所有的节点
      2. 查询结果依旧是经过协调节点聚合后返回给用户 这样不需要将请求广播而是有明确目标的多播

写入速度优化

  1. es的默认配置考虑了数据的可靠性 /写入速度 /搜索实时性 等因素, 因此在实际使用中, 需要根据特定的场景进行有偏向性的优化, 对于搜索要求不高写入要求高的场景, 可以进行以下优化
    1. 加大Translog Flush: 降低Iops, Writeblock
    2. 增大Index Refresh : 减少segment merge次数
    3. 调整Bulk线程池和队列: 提高bulk操作的并行度
    4. 优化节点间的任务分布
    5. 优化Lucene层的索引建立, 降低CPU和IO负载
  2. 批量数据提交: es提供了Bulk api支持批量操作 注意一般单批次的数据量不应超过100M, 且批次大小的增大有边界效应, 不应无限制扩大
  3. 优化存储设备
  4. 合理使用合并: Lucene使用段合并, 当有新的索引写入时, Lucene就会创建新的段, 因此es采取默认保守的策略, 后台定期合并
  5. 减少Refresh 次数: Lucene会将数据先写入到内存中, 没refresh_interval周期会刷新一次, 若对实时性要求不高可以增大这个参数
  6. 加大flush设置: 一般当Translog达到index.translog.flush_threshold_size=521MB会触发flush, 若增大flush配置, 可以减少flush的次数
  7. 减少副本的数量: es集群写入过程要保证副本也成功写入, 若副本数量少, 这个速度的期望值也会加快

内存设置

  1. es的默认内存设置是1GB, 其主要内存分配主要有以下两个原则:
    1. 不要超过物理内存的 50 % 50\% 50%, 太大操作系统的磁盘缓存就不够用了, 这样会增大落盘次数
    2. 堆内存大小最好不要大于32G java使用内存压缩的技术节省内存, 这样就只需要使用32位指针; 若堆内存过大, 该技术就会失效, 这样指针大小就会变为64位, 极大地影响性能; 其中多出来的内存可以分配给lucence [3](#3)

重要配置

参数名 参数值 说明
cluster.name elasticsearch 配置es的集群名称, 同一网段下集群名称相同的节点会组成集群
node.name node-1 集群中的节点名, 同一集群中不能重复, 一旦设置就不能修改
node.master true 该节点是否有资格被选举为master 不代表该节点就是master
node.data true 该节点是否存储数据 数据的增删查改都是在数据节点上完成的
index.number_of_shards 1 分片数量, 影响性能, 原因见前面
index.number_of_replicas 1 副本数量, 越高可用性 越高, 但写入性能越差
transport.tcp.comporess true 窜出数据时是否压缩
discovery.zen.minimun_master_nodes 1 选举master需要有的候选人的最少数量, 需要大于半数节点参与, 若按照默认配置可能会导致脑裂
discovery.zen.ping.timeout 3s 集群中发现其他节点ping的超时时间, 网络较差时需要设置得大一点, 以防误判节点下线而导致分片转移

文档评分机制

  1. 查询的数据会评分, 查询结果会根据评分排序, 评分是查询结果中的_score字段
  2. 其中计算公式为: _ s c o r e = b o o s t × i d f × t f \_score=boost\times idf \times tf _score=boost×idf×tf 其中boost是权重

文档得分

es采用的是tf-idf公式评分

  1. TF(Term Frequency, 词频): 文本中词条出现的次数, 词条出现次数越多, 该评分越高
  2. IDF(Inverse Document Frequency, 逆文档频率):文本中各个词条在整个索引的所有文档中 出现了多少次, 出现次数越多说明越不重要, 因此该项得分越低 如the在所有文档都频繁出现, 因此其IDF低且不重要

在查询中, 可以通过添加explain=true来查看评分计算结果

python 复制代码
In [16]: requests.get(f"{url}/shopping/_search", params={"explain": "true"}, json={"query":{"match": {"title": "华为"}}}
    ...: ).json()
Out[16]:
{'took': 1,
 'timed_out': False,
 '_shards': {'total': 1, 'successful': 1, 'skipped': 0, 'failed': 0},
 'hits': {'total': {'value': 2, 'relation': 'eq'},
  'max_score': 0.26706278,
  'hits': [{'_shard': '[shopping][0]',
    '_node': 'TflZy_0kSd2KsD11vajpsg',
    '_index': 'shopping',
    '_type': '_doc',
    '_id': 'AHdB44wBL8fZAkJalBsr',
    '_score': 0.26706278,
    '_source': {'title': '华为手机', 'category': '华为', 'price': 8848},
    '_explanation': {'value': 0.26706278,
     'description': 'sum of:',
     'details': [{'value': 0.13353139,
       'description': 'weight(title:华 in 0) [PerFieldSimilarity], result of:',
       'details': [{'value': 0.13353139,
         'description': 'score(freq=1.0), computed as boost * idf * tf from:',
         'details': [{'value': 2.2, 'description': 'boost', 'details': []},
          {'value': 0.13353139,
           'description': 'idf, computed as log(1 + (N - n + 0.5) / (n + 0.5)) from:',
           'details': [{'value': 3,
             'description': 'n, number of documents containing term',
             'details': []},
            {'value': 3,
             'description': 'N, total number of documents with field',
             'details': []}]},
          {'value': 0.45454544,
           'description': 'tf, computed as freq / (freq + k1 * (1 - b + b * dl / avgdl)) from:',
           'details': [{'value': 1.0,
             'description': 'freq, occurrences of term within document',
             'details': []},
            {'value': 1.2,
             'description': 'k1, term saturation parameter',
             'details': []},
            {'value': 0.75,
             'description': 'b, length normalization parameter',
             'details': []},
            {'value': 4.0,
             'description': 'dl, length of field',
             'details': []},
            {'value': 4.0,
             'description': 'avgdl, average length of field',
             'details': []}]}]}]},
      {'value': 0.13353139,
       'description': 'weight(title:为 in 0) [PerFieldSimilarity], result of:',
       'details': [{'value': 0.13353139,
         'description': 'score(freq=1.0), computed as boost * idf * tf from:',
         'details': [{'value': 2.2, 'description': 'boost', 'details': []},
          {'value': 0.13353139,
           'description': 'idf, computed as log(1 + (N - n + 0.5) / (n + 0.5)) from:',
           'details': [{'value': 3,
             'description': 'n, number of documents containing term',
             'details': []},
            {'value': 3,
             'description': 'N, total number of documents with field',
             'details': []}]},
          {'value': 0.45454544,
           'description': 'tf, computed as freq / (freq + k1 * (1 - b + b * dl / avgdl)) from:',
           'details': [{'value': 1.0,
             'description': 'freq, occurrences of term within document',
             'details': []},
            {'value': 1.2,
             'description': 'k1, term saturation parameter',
             'details': []},
            {'value': 0.75,
             'description': 'b, length normalization parameter',
             'details': []},
            {'value': 4.0,
             'description': 'dl, length of field',
             'details': []},
            {'value': 4.0,
             'description': 'avgdl, average length of field',
             'details': []}]}]}]}]}},
   {'_shard': '[shopping][0]',
    '_node': 'TflZy_0kSd2KsD11vajpsg',
    '_index': 'shopping',
    '_type': '_doc',
    '_id': 'AXdB44wBL8fZAkJauxsr',
    '_score': 0.26706278,
    '_source': {'title': '华为公司', 'category': '华为'},
    '_explanation': {'value': 0.26706278,
     'description': 'sum of:',
     'details': [{'value': 0.13353139,
       'description': 'weight(title:华 in 1) [PerFieldSimilarity], result of:',
       'details': [{'value': 0.13353139,
         'description': 'score(freq=1.0), computed as boost * idf * tf from:',
         'details': [{'value': 2.2, 'description': 'boost', 'details': []},
          {'value': 0.13353139,
           'description': 'idf, computed as log(1 + (N - n + 0.5) / (n + 0.5)) from:',
           'details': [{'value': 3,
             'description': 'n, number of documents containing term',
             'details': []},
            {'value': 3,
             'description': 'N, total number of documents with field',
             'details': []}]},
          {'value': 0.45454544,
           'description': 'tf, computed as freq / (freq + k1 * (1 - b + b * dl / avgdl)) from:',
           'details': [{'value': 1.0,
             'description': 'freq, occurrences of term within document',
             'details': []},
            {'value': 1.2,
             'description': 'k1, term saturation parameter',
             'details': []},
            {'value': 0.75,
             'description': 'b, length normalization parameter',
             'details': []},
            {'value': 4.0,
             'description': 'dl, length of field',
             'details': []},
            {'value': 4.0,
             'description': 'avgdl, average length of field',
             'details': []}]}]}]},
      {'value': 0.13353139,
       'description': 'weight(title:为 in 1) [PerFieldSimilarity], result of:',
       'details': [{'value': 0.13353139,
         'description': 'score(freq=1.0), computed as boost * idf * tf from:',
         'details': [{'value': 2.2, 'description': 'boost', 'details': []},
          {'value': 0.13353139,
           'description': 'idf, computed as log(1 + (N - n + 0.5) / (n + 0.5)) from:',
           'details': [{'value': 3,
             'description': 'n, number of documents containing term',
             'details': []},
            {'value': 3,
             'description': 'N, total number of documents with field',
             'details': []}]},
          {'value': 0.45454544,
           'description': 'tf, computed as freq / (freq + k1 * (1 - b + b * dl / avgdl)) from:',
           'details': [{'value': 1.0,
             'description': 'freq, occurrences of term within document',
             'details': []},
            {'value': 1.2,
             'description': 'k1, term saturation parameter',
             'details': []},
            {'value': 0.75,
             'description': 'b, length normalization parameter',
             'details': []},
            {'value': 4.0,
             'description': 'dl, length of field',
             'details': []},
            {'value': 4.0,
             'description': 'avgdl, average length of field',
             'details': []}]}]}]}]}}]}}

tf计算公式

t f = f r e q f r e q + k 1 ( 1 − b + b ⋅ d l a v g d l ) tf=\frac{freq}{freq+k1(1-b+\frac{b\cdot dl}{avgdl})} tf=freq+k1(1−b+avgdlb⋅dl)freq

其中:

  1. freq为关键词在当前文档中出现的次数
  2. k1: 关键词的饱和参数 默认为1.2
  3. b: 长度系数参数 文档中可能越长的单词越重要
  4. dl: 分词后分词的个数
  5. avgdl: 所有文档中, 分词的平均个数

idf计算公式

i d f = ln ⁡ ( 1 + N − n + 0.5 n + 0.5 ) idf=\ln(1+\frac{N-n+0.5}{n+0.5}) idf=ln(1+n+0.5N−n+0.5)

  1. N: 文档中字段的总数量 这里的字段是上面的title而不是分词
  2. n: 文档中包含的关键词的数量

得分计算公式

s c o r e = b o o s t ⋅ t f ⋅ i d f score=boost \cdot tf \cdot idf score=boost⋅tf⋅idf

  1. boost是权重, 默认值为2.2

引用


  1. docker-compose安装elasticsearch及kibana - 陈远波 - 博客园 (cnblogs.com) ↩︎

  2. medcl/elasticsearch-analysis-ik: The IK Analysis plugin integrates Lucene IK analyzer into elasticsearch, support customized dictionary. (github.com) ↩︎

  3. JVM内存不要超过32G - CharyGao - 博客园 (cnblogs.com) ↩︎

相关推荐
lwf0061643 分钟前
导数学习日记
学习·算法·机器学习
ch3nyuyu15 分钟前
Ubuntu(乌班图)基础指令
linux·运维·网络
yaoxin52112325 分钟前
390. Java IO API - WatchDir 示例
java·前端·python
qeen8731 分钟前
【编程日记】现阶段总结
学习
挽安学长44 分钟前
保姆级教程,通过GACCode使用Claude Code Desktop!
运维·服务器
武帝为此1 小时前
【数据清洗缺失值处理】
python·算法·数学建模
daanpdf1 小时前
新视野大学英语视听说教程2第四版听力音频原文及答案
笔记
firstacui2 小时前
MGRE实验
运维·服务器·网络
zhangchaoxies2 小时前
如何在 Go 中安全复制接口指针所指向的值
jvm·数据库·python
曲幽2 小时前
FastAPI + Pydantic 模型终极实战手册:从能跑就行到固若金汤,这些技巧你一定用得上
python·fastapi·web·model·field·pydantic·validator·basemodel