【运维】Elsatic Search学习笔记

news2024/11/29 3:00:52

基本使用

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

Docker安装Elasticsearch1

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

然后启动

docker-compose up -d

系统架构

  1. 数据格式: Elasticsearch是面向文档型的数据库, 一条数据就是一个文档, Elasticsearch与MySQL的对应如下图
    1. 在这里插入图片描述

    2. 因为Elasticsearch更强调全文索引, 因此在新版本中, Type的概念已经被移除

基本使用

环境准备

url = "http://server.passnight.local:9200"
import requests

索引操作

创建索引
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}
In [3]: requests.delete(f"{url}/shopping").json()
Out[3]: {'acknowledged': True}

文档操作

创建文档
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}
查询文档
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}}]}}
修改文档

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

# 全量修改
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}}
删除文档
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查询

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}}]}}
分页查询
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}}]}}
列投影
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': '华为手机'}}]}}
排序
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]}]}}
复杂条件查询
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}}]}}
分组查询
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索引

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

创建mapping

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}}}}}

之后添加文档

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}

查询刚刚添加的数据

# 查询`小`这一个字可以查到; 说明`小米`被分词了
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}}]}}

#

而无索引的数据不能查询

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

索引操作

环境准备

    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();
    }

增删查索引

/**
     * 增加索引
     */
    @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

@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class User {
    private String name;
    private String sex;
    private Integer age;
}

准备session和工具

    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();
    }
文档增删查改
    @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);
    }
批量增删
    /**
     * 批量增加
     */
    @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()));
    }

复杂查询

环境准备
    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);
    }
全量查询
    /**
     * 查询全量数据
     */
    @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()));
    }

该查询输出了全量数据

{"name":"张三","sex":"男","age":30}
{"name":"李四","sex":"女","age":40}
{"name":"王五","sex":"男","age":50}
{"name":"赵六","sex":"女","age":60}
{"name":"钱七","sex":"男","age":20}
条件查询
/**
 * 查询{@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的数据

{"name":"张三","sex":"男","age":30}
分页查询
/**
 * 分页查询
 */
@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×(21),4]的数据

{"name":"王五","sex":"男","age":50}
{"name":"赵六","sex":"女","age":60}
结果排序
    /**
     * 对查询结果根据年龄降序排序
     */
    @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()));
    }

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

{"name":"赵六","sex":"女","age":60}
{"name":"王五","sex":"男","age":50}
{"name":"李四","sex":"女","age":40}
{"name":"张三","sex":"男","age":30}
{"name":"钱七","sex":"男","age":20}
字段投影
    /**
     * 过滤部分字段
     */
    @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

{"name":"张三"}
{"name":"李四"}
{"name":"王五"}
{"name":"赵六"}
{"name":"钱七"}
组合条件查询
    /**
     * 组合条件查询
     */
    @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='男'的数据

{"name":"张三","sex":"男","age":30}
范围查询
    /**
     * 范围查询
     */
    @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)的数据

{"name":"张三","sex":"男","age":30}
{"name":"李四","sex":"女","age":40}
模糊查询
    /**
     * 模糊查询
     */
    @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()));
    }

输出为

{"name":"王五","sex":"男","age":50}
{"name":"张三","sex":"男","age":30}
{"name":"李四","sex":"女","age":40}
{"name":"赵六","sex":"女","age":60}
{"name":"钱七","sex":"男","age":20}
高亮查询
/**
 * 高亮查询
 */
@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标签, 以高亮显示

{name=[name], fragments[[<font color='red'></font>]]}
聚合查询

求最大年龄

    /**
     * 聚合查询; 求最大年龄
     */
    @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()));
    }

输出为最大年龄

ageAge:60.0

求各个分组的数量

    /**
     * 聚合查询: 求各个年龄的数量
     */
    @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);
    }

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

"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. 在这里插入图片描述

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

水平扩容

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

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

  2. 在这里插入图片描述 在这里插入图片描述

  3. 然后再讲副本数+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及其类型/位置值等基本信息

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}]}

中文分词

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

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 分词器需要单独安装

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: 按照文本最粗粒度拆分
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}]}

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

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

passnight@passnight-s600:~$ docker exec -it elasticsearch bash
[root@f10b92eeb7c9 elasticsearch]# cd config/analysis-ik
# 配置自定义词典
[root@f10b92eeb7c9 analysis-ik]# vi IKAnalyzer.cfg.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>
# 将自定义分词添加到自定义词典中
[root@f10b92eeb7c9 analysis-ik]# vi custom.dic
弗雷尔卓德
# 重启es, 使配置生效
passnight@passnight-s600:~/tmp$ docker restart elasticsearch

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

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, 可以看到因为乐观并发控制, 无法成功修改

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还支持外部版本控制, 可以自定义版本号;

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位, 极大地影响性能; 其中多出来的内存可以分配给lucence3

重要配置

参数名参数值说明
cluster.nameelasticsearch配置es的集群名称, 同一网段下集群名称相同的节点会组成集群
node.namenode-1集群中的节点名, 同一集群中不能重复, 一旦设置就不能修改
node.mastertrue该节点是否有资格被选举为master 不代表该节点就是master
node.datatrue该节点是否存储数据 数据的增删查改都是在数据节点上完成的
index.number_of_shards1分片数量, 影响性能, 原因见前面
index.number_of_replicas1副本数量, 越高可用性越高, 但写入性能越差
transport.tcp.comporesstrue窜出数据时是否压缩
discovery.zen.minimun_master_nodes1选举master需要有的候选人的最少数量, 需要大于半数节点参与, 若按照默认配置可能会导致脑裂
discovery.zen.ping.timeout3s集群中发现其他节点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来查看评分计算结果

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(1b+avgdlbdl)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.5Nn+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=boosttfidf

  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) ↩︎

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1560172.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

HTTP 常见面试题(计算机网络)

HTTP 基本概念 一、HTTP 是什么&#xff1f; HTTP(HyperText Transfer Protocol) &#xff1a;超文本传输协议。 HTTP 是一个在计算机世界里专门在「两点」之间「传输」文字、图片、音频、视频等「超文本」数据的「约定和规范」。 「HTTP 是用于从互联网服务器传输超文本到本…

【4】单链表(有虚拟头节点)

【4】单链表&#xff08;有虚拟头节点&#xff09; 1、虚拟头节点2、构造方法3、node(int index) 返回索引位置的节点4、添加5、删除6、ArrayList 复杂度分析(1) 复杂度分析(2) 数组的随机访问(3) 动态数组 add(E element) 复杂度分析(4) 动态数组的缩容(5) 复杂度震荡 7、单链…

uniapp 小程序发布体验版 http://198.18.0.1:7001 不在以下 request 合法域名列表中(踩坑记录二)

问题一&#xff1a; 小程序发布体验版时出现报错信息&#xff1a; http://198.18.0.1:7001 不在以下 request 合法域名列表中无法连接uniCloud本地调试服务&#xff0c;请检查当前客户端是否与主机在同一局域网下 解决方案&#xff1a; 请务必在HBuilderX内使用【发行】菜单打…

Vastbase编程利器:PL/pgSQL原理简介

PL/pgSQL是Vastbase提供的一种过程语言&#xff0c;在普通SQL语句的使用上增加了编程语言的特点&#xff0c;可以用于创建函数、存储过程、触发器过程以及创建匿名块等。 本文介绍Vastbase中PL/pgSQL的执行流程&#xff0c;包括PL/pgSQL的编译与运行。 1、编译 PL/pgSQL的编译…

什么是HTTP? HTTP 和 HTTPS 的区别?

文章目录 一、HTTP二、HTTPS三、区别参考文献 一、HTTP HTTP (HyperText Transfer Protocol)&#xff0c;即超文本运输协议&#xff0c;是实现网络通信的一种规范 在计算机和网络世界有&#xff0c;存在不同的协议&#xff0c;如广播协议、寻址协议、路由协议等等… 而HTTP是…

CleanMyMac X2024专业的Mac清理工,具一次激活,永久使用

CleanMyMac&#xff0c;作为一款专为Mac系统设计的垃圾清理工具&#xff0c;以其强大的清理能力、简便的操作方式以及卓越的系统兼容性&#xff0c;受到了众多Mac用户的青睐。以下是对这款软件功能的详细介绍&#xff1a; CleanMyMac X2024全新版下载如下: https://wm.makedin…

机器人---人形机器人之技术方向

1 背景介绍 在前面的文章《行业杂谈---人形机器人的未来》中&#xff0c;笔者初步介绍了人形机器人的未来发展趋势。同智能汽车一样&#xff0c;它也会是未来机器人领域的一个重要分支。目前地球上最高智慧的结晶体就是人类&#xff0c;那么人形机器人的未来会有非常大的发展空…

Qt笔记-解决Qt程序连不上数据库MySQL数据库(重编libqsqlmysql.so)

使用QSqlDatabase连接MySQL数据库时。在自己程序配置没有错误的情况下报这类错误&#xff1a; QSqlDatabase: QMYSQL driver not loaded QSqlDatabase::exec: database not open 造成这样的问题大多数是libqsqlmysql.so有问题。 Qt的QSqlDatabase使用的是libqsqlmysql.so&a…

【GitLab】开启GitLab 的邮箱通知服务

为了GitLab使用更方便&#xff0c;让用户及时收到推送代码、修改密码等相关的通知&#xff0c;可以开启邮箱推送&#xff0c;下面是两种邮箱的配置方法。 使用系统Postfix 邮箱 如果要使用 Postfix 来发送电子邮件通知&#xff0c;执行以下安装命令。 sudo apt-get install …

图的存储及基本操作试题

01&#xff0e;下列关于图的存储结构的说法中&#xff0c;错误的是&#xff08;). A.使用邻接矩阵存储一个图时&#xff0c;在不考虑压缩存储的情况下&#xff0c;所占用的存储空间大小 只与图中的顶点数有关&#xff0c;与边数无关 B&#xff0e;邻接表只用于有向图的存储&…

打造高效安全的电池管理 | 基于ACM32 MCU的两轮车充电桩方案

前 言 随着城市化进程的加快、人们生活水平的提高和节能环保理念的普及&#xff0c;越来越多的人选择了电动车作为代步工具&#xff0c;而两轮电动车的出行半径较短&#xff0c;需要频繁充电&#xff0c;因此在城市中设置两轮车充电桩就非常有必要了。城市中的充电桩不仅能解决…

学习transformer模型-Dropout的简明介绍

Dropout的定义和目的&#xff1a; Dropout 是一种神经网络正则化技术&#xff0c;它在训练时以指定的概率丢弃一个单元&#xff08;以及连接&#xff09;p。 这个想法是为了防止神经网络变得过于依赖特定连接的共同适应&#xff0c;因为这可能是过度拟合的症状。直观上&#…

AcWing-乌龟棋

312. 乌龟棋 - AcWing题库 所需知识&#xff1a;动态规划 闫氏dp分析法&#xff1a; 整体思路&#xff1a;由于走的方式有四种&#xff0c;所以dp[i][j][m][n]的来源有四种&#xff0c;状态转移方程式要求不重不漏&#xff0c;所以我们可以以使用的最后一个卡片上的数值来进行…

三台电机的顺启逆停

1&#xff0c;开启按钮输入信号是 电机一开始启动&#xff0c;5秒回电机2启动 &#xff0c;在5秒电机三启动 关闭按钮输入时电机3关闭 &#xff0c;5秒后电机2关闭 最后电机一关闭 2&#xff0c;思路开启按钮按下接通电机1 并且接通定时器T0 定时器T0 到时候接通电机2 并且开…

快速创建zookeeper集群

先说明&#xff0c;zookeeper集群的3个节点都放在同一个虚拟机&#xff08;穷&#xff09;&#xff0c;所以搭建是一个伪集群&#xff0c;因为一个服务器挂机&#xff0c;所有节点都会停止。工作实际情况安装到三个服务器&#xff0c;并修改节点配置的ip地址即可&#xff08;红…

星云曲库测试报告

文章目录 一、项目介绍1.1项目背景1.2功能介绍 二、测试环境三、测试执行过程3.1功能测试3.1.1登录页面测试3.1.2歌曲列表页面测试3.1.3“我喜欢”页面测试3.1.4上传页面测试 3.2界面自动化测试3.2.1登录页面测试3.2.2歌曲列表页面测试3.2.3“我喜欢”页面测试3.2.4上传页面测试…

零失误微信支付商家转账到零钱功能开通教程

商家转账到零钱是什么&#xff1f; 使用商家转账到零钱这个功能&#xff0c;可以让商户同时向多个用户的零钱转账。商户可以使用这个功能用于费用报销、员工福利发放、合作伙伴货款或分销返佣等场景&#xff0c;提高效率。 商家转账到零钱的使用场景有哪些&#xff1f; 商家…

都江堰操作系统系统架构图

都江堰操作系统设计思想源于中国传统的“天人合一&#xff0c;道法自然”哲学思想&#xff0c;内核调度系统采用事件调度&#xff0c;全球首创&#xff0c;突破单机桎梏&#xff0c;实现异构网络调度&#xff0c;开拓新赛道&#xff0c;实现换道超车。“有事就动&#xff0c;没…

Vue.js前端开发零基础教学(四)

学习目标&#xff1a; 熟悉选项式API和组合式API&#xff0c;能够说出选项式API和组合式API的区别 掌握注册组件的方法&#xff0c;能够运用全局注册或者局部注册的方式完成组件的注册 掌握父组件向子组件传递数据的方法&#xff0c;能够使用props实现数据传递等等 前言 在学习…

Linux 学习之路 -- 进程篇 -- 背景介绍

目录 1、冯诺依曼体系架构 2.操作系统 1、冯诺依曼体系架构 再开始学习进程之前我们要先了解一下计算机的体系结构&#xff0c;这里我们以最经典的冯诺依曼体系结构为例&#xff0c;简单介绍一下一下计算机的体系结构&#xff0c;方便我们对进程的理解。 这里的中央处理器就是…