ElasticSearch

端口 9300 组件间通信,9200 http界面端口,5601 kibana界面

基本操作

es不支持修改索引信息

### 创建索引shopping,es中的索引类似于关系数据库中的库
curl -X PUT http://localhost:9200/shopping

### 查看索引
curl -X GET http://localhost:9200/shopping

### 查看所有索引
curl -X GET http://localhost:9200/_cat/indices?v=true

### 向索引添加文档,es中文档即为具体的数据,指定id为1001,不指定id则由es自动生成
curl -X POST -H "Content-Type: application/json" \    # 因为操作是幂等性的,也可以使用 PUT
    -d '{"name":"张三","age":18}' \
    http://localhost:9200/shopping/_create/1001       # 此处的 _create 可以换成 _doc,但是使用 _create 时语义更加明确

### 查看文档数据
curl -X GET http://localhost:9200/shopping/_doc/1001?pretty=true

### 返回所有文档数据
curl -X GET http://localhost:9200/shopping/_search?pretty=true

### 完全覆盖更新,具有幂等性,可使用PUT
curl -X PUT -H "Content-Type: application/json" \
    -d '{"name":"xxx", "age":33}' \
    http://localhost:9200/shopping/_doc/1001  

### 局部更新,不具有幂等性
curl -X POST -H "Content-Type: application/json" \
    -d '{"doc":{"name":"李四"}}' \
    http://localhost:9200/shopping/_update/1001  

### 删除文档
curl -X DELETE http://localhost:9200/shopping/_doc/1001

更新文档

# 2 种更新方法
### 1. 发送部分文档,多人同时修改可能覆盖,可通过版本号进行控制
###    使用 upsert 可以在文档不存在时创建文档
curl -XPOST 'http://localhost:9200/get-together/_update/xxx'
-H 'Content-Type: application/json'
-d '{
    "doc": {
        "age": 20
    },
    "upsert": {
        "name": "John Doe",
        "age": 18
    }
}'

### 2. 使用Painless脚本更新
curl -XPOST 'http://localhost:9200/get-together/_update/xxx'
-H 'Content-Type: application/json'
-d '{
    "script": {
        "source": "ctx._source.age += params.age_gap",
        "params": {
            "age_gap": 5
        }
    }
}'

打开关闭文档

### 关闭索引,索引保存在磁盘上,关闭索引可以减少内存占用
curl -XPOST 'http://localhost:9200/get-together/_close'
### 打开索引
curl -XPOST 'http://localhost:9200/get-together/_open'

使用 match 进行匹配时默认使用分词匹配倒排索引,只要部分字词匹配目标字段即匹配成功。如:使用“小华”进行匹配时即可匹配“小米”又可以匹配“华为”。

### 创建索引
curl -XPUT 'http://localhost:9200/accounts'

### 导入 json 数据
curl -XPOST 'http://localhost:9200/accounts/_bulk' \
    -H 'Content-Type: application/json' \
    --data-binary @D:\\data\\es_accounts.json

###
curl -XGET 'http://localhost:9200/accounts' 

###
curl -XGET 'http://localhost:9200/_cat/indices?v'

###
curl -XGET 'http://localhost:9200/accounts/_search?q=gender:M'

### 匹配查询,分页查询,指定返回字段,排序
curl -XGET -H "Content-Type: application/json" 'http://localhost:9200/accounts/_search' \
-d '{
    "query": {
        "match": {
            "gender": "M"
        }
    },
    "from": 0,
    "size": 5,
    "_source": ["age", "firstname"],
    "sort": {
        "age": {
            "order": "desc"
        }
    }
}'

### 组合查询
# must表示与,should表示或
curl -XGET -H "Content-Type: application/json" "http://localhost:9200/accounts/_search" \
-d '{
    "query": {
        "bool": {
            "must": [ 
                {
                    "match_phrase": {
                        "gender": "F"
                    }
                }
            ],
            "filter": {
                "range": {
                    "age": {
                        "gte": 20,
                        "lte": 40
                    }
                }
            }
        }
    },
    "highlight": {
        "fields": {
            "firstname": {}
        }
    }
}'

### 聚合操作
# 通过 age 字段进行聚合;指定size为0,表示不返回文档,只返回聚合结果
curl -XGET -H "Content-Type: application/json" "http://localhost:9200/accounts/_search" \
-d '{
    "aggs": {
        "age_group": {
            "terms": {
                "field": "age"
            }
        }
    },
    "size": 0
}'

### 平均值
curl -XGET -H "Content-Type: application/json" "http://localhost:9200/accounts/_search" \
-d '{
    "aggs": {
        "age_avg": {
            "avg": {
                "field": "age"
            }
        }
    },
    "size": 0
}'

别名

### 查看所有别名
curl -XGET 'http://localhost:9200/_aliases'

### 查看别名
curl -XGET 'http://localhost:9200/users/_alias/'

### 创建别名
curl -XPOST 'http://localhost:9200/_aliases'
-H 'Content-Type: application/json'
-d '{
    "actions": [
        {
            "add": {
                "index": "users",
                "alias": "all_users"
            }
        }
    ]
}'

### 删除别名
curl -XPOST 'http://localhost:9200/_aliases'
-H 'Content-Type: application/json'
-d '{
    "actions": [
        {
            "remove": {
                "index": "users",
                "alias": "all_users"
            }
        }
    ]
}'

并发修改:多个请求同时修改数据,一般使用加锁解决问题。es常用乐观锁

###
curl -XPUT 'http://localhost:9200/users'

###
curl -H 'Content-Type: application/json' -XPUT 'http://localhost:9200/users/_create/1' -d '{
    "name": "zhangsan",
    "age": 18
}'
# 创建请求的返回如下,当数据被修改后version 和 seq_no 会改变
{
  "_index": "users",
  "_id": "1",
  "_version": 1,        # 早期使用,从1开始,每次修改时增加
  "result": "created",
  "_shards": {
  },
  "_seq_no": 0,         # 推荐,从0开始,每次修改时增加
  "_primary_term": 1    # 表示文档所在主分片的编号
}

### 查询数据
curl -H 'Content-Type: application/json' -XGET 'http://localhost:9200/users/_doc/1'
# 响应
{
  "_index": "users",
  "_id": "1",
  "_version": 1,
  "_seq_no": 0,
  "_primary_term": 1,
  "found": true,
  "_source": {
    "name": "zhangsan",
    "age": 18
  }
}

### 通过请求参数 version 设置version,当不符合时修改失败,version是早期使用的参数
curl -H 'Content-Type: application/json' -XPOST 'http://localhost:9200/users/_update/1?version=1' -d '{
    "age": 19
}'

### 现在推荐使用 if_seq_no if_primary_term 指定版本
curl -H 'Content-Type: application/json' -XPOST 'http://localhost:9200/users/_update/1?if_seq_no=0&if_primary_term=1' -d '{
    "age": 19
}'

还可以使用外部字段进行版本控制

指定操作索引的范围

curl -XGET 'http://localhost:9200/_search' -d ''
curl -XGET 'http://localhost:9200/_all/_search' -d ''
curl -XGET 'http://localhost:9200/*/_search' -d ''
curl -XGET 'http://localhost:9200/get-together/_search' -d ''
curl -XGET 'http://localhost:9200/get-together, other/_search' -d ''
curl -XGET 'http://localhost:9200/+get-to*, -get-together/_search' -d ''
JAVA 操作 ES
 package org.example.dir01;

import co.elastic.clients.elasticsearch.ElasticsearchAsyncClient;
import co.elastic.clients.elasticsearch.ElasticsearchClient;
import co.elastic.clients.elasticsearch._types.Result;
import co.elastic.clients.elasticsearch._types.query_dsl.MatchQuery;
import co.elastic.clients.elasticsearch._types.query_dsl.Query;
import co.elastic.clients.elasticsearch.core.*;
import co.elastic.clients.elasticsearch.core.bulk.BulkOperation;
import co.elastic.clients.elasticsearch.core.bulk.CreateOperation;
import co.elastic.clients.elasticsearch.indices.*;
import co.elastic.clients.elasticsearch.indices.ExistsRequest;
import co.elastic.clients.json.jackson.JacksonJsonpMapper;
import co.elastic.clients.transport.rest_client.RestClientTransport;
import org.apache.http.HttpHost;
import org.elasticsearch.client.RestClient;

import java.io.IOException;
import java.util.ArrayList;
import java.util.List;

public class Main {
    private static RestClientTransport transport = null;
    private static ElasticsearchClient client = null;
    private static ElasticsearchAsyncClient asyncClient = null;
    private static String INDEX_NAME = "index_test_1";

    public static void main(String[] args) throws Exception {
        init();
//        operateIndex();
//        operateIndexLambda();
//        operateDocument();

        transport.close();
    }
    private static void init() throws IOException {
        RestClient restClient = RestClient.builder(new HttpHost("localhost", 9200, "http")).build();
        transport = new RestClientTransport(restClient, new JacksonJsonpMapper());
        client = new ElasticsearchClient(transport); // 进行同步操作
        asyncClient = new ElasticsearchAsyncClient(transport); // 进行异步操作
    }

    private static void operateIndex() throws Exception {
        // 拿到索引客户端对象
        final ElasticsearchIndicesClient indices = client.indices();
        // 判断索引是否存在
        ExistsRequest existsRequest = new ExistsRequest.Builder().index(INDEX_NAME).build();
        boolean isExists = indices.exists(existsRequest).value();
        if (isExists) {
            System.out.println("索引已存在:" + INDEX_NAME);
            DeleteIndexRequest deleteIndexRequest = new DeleteIndexRequest.Builder().index(INDEX_NAME).build();
            boolean deleted = indices.delete(deleteIndexRequest).acknowledged();
            if (!deleted) {
                System.out.println("删除已存在索引失败:" + INDEX_NAME);
                throw new RuntimeException("删除已存在索引失败:" + INDEX_NAME);
            }
            System.out.println("删除已存在索引成功:" + INDEX_NAME);
        }
        // 创建索引
        CreateIndexRequest createIndexRequest = new CreateIndexRequest.Builder().index(INDEX_NAME).build();
        CreateIndexResponse createIndexResponse = indices.create(createIndexRequest);
        System.out.println("创建索引的响应:createIndexResponse = " + createIndexResponse);
//        String index = createIndexResponse.index();
//        System.out.println("创建索引成功:" + index);

        GetIndexRequest getIndexRequest = new GetIndexRequest.Builder().index(INDEX_NAME).build();

        GetIndexResponse getIndexResponse = indices.get(getIndexRequest);
        System.out.println("查询的响应结果:getIndexResponse = " + getIndexResponse);

        IndexState indexState = getIndexResponse.get(INDEX_NAME);
        System.out.println("创建索引的结果:indexState = " + indexState);
    }

    private static void operateIndexLambda() throws Exception {
        // 拿到索引客户端对象
        final ElasticsearchIndicesClient indices = client.indices();
        // 判断索引是否存在
        boolean isExists = indices.exists(req -> req.index(INDEX_NAME)).value();
        if (isExists) {
            System.out.println("索引已存在:" + INDEX_NAME);
            boolean deleted = indices.delete(req -> req.index(INDEX_NAME)).acknowledged();
            if (!deleted) {
                System.out.println("删除已存在索引失败:" + INDEX_NAME);
                throw new RuntimeException("删除已存在索引失败:" + INDEX_NAME);
            }
            System.out.println("删除已存在索引成功:" + INDEX_NAME);
        }
        // 创建索引
        CreateIndexResponse createIndexResponse = indices.create(req -> req.index(INDEX_NAME));
        System.out.println("创建索引的响应:createIndexResponse = " + createIndexResponse);

        GetIndexResponse getIndexResponse = indices.get(req -> req.index(INDEX_NAME));
        System.out.println("查询的响应结果:getIndexResponse = " + getIndexResponse);
        IndexState indexState = getIndexResponse.get(INDEX_NAME);
        System.out.println("创建索引的结果:indexState = " + indexState);
    }

    private static void operateDocument() throws Exception {
        // 增加文档
        User user = new User("1001", "张三", 18);
        CreateRequest<User> createRequest = new CreateRequest.Builder<User>()
                .index(INDEX_NAME)
                .id("1001")
                .document(user)
                .build();
        CreateResponse createResponse = client.create(createRequest);

        // 批量添加
        List<BulkOperation> bulkOperationList = new ArrayList<>();
        for (int i = 0; i < 5; i++) {

            CreateOperation<User> createOperation = new CreateOperation.Builder<User>()
                    .index(INDEX_NAME)
                    .id("200" + i)
                    .document(new User("100" + i, "张三" + i, 18 + i))
                    .build();

            BulkOperation bulkOperation = new BulkOperation.Builder()
                    .create(createOperation)
                    .build();

            bulkOperationList.add(bulkOperation);
        }

        BulkRequest bulkRequest = new BulkRequest.Builder()
                .operations(bulkOperationList)
                .build();
        BulkResponse bulkResponse = client.bulk(bulkRequest);
        System.out.println("批量创建响应:bulkResponse = " + bulkResponse);

        // 文档删除
        DeleteRequest deleteRequest = new DeleteRequest.Builder()
                .index(INDEX_NAME)
                .id("1001")
                .build();
        DeleteResponse deleteResponse = client.delete(deleteRequest);
        System.out.println("删除文档的响应:deleteResponse = " + deleteResponse);
    }

    private static void operateDocumentLambda() throws Exception {
        // 增加文档
        Result result = client.create(
                req -> req.index(INDEX_NAME)
                        .id("1001")
                        .document(new User("1001", "张三", 18))
        ).result();
        System.out.println("创建结果:result = " + result);

        // 批量添加
        List<User> userList = new ArrayList<>();
        for (int i = 0; i < 5; i++) {
            userList.add(new User("100" + i, "张三" + i, 18 + i));
        }
        BulkResponse bulkResponse = client.bulk(
                req -> {
                    userList.forEach(user -> {
                        req.operations(
                                builder -> builder.create(
                                        d -> d.index(INDEX_NAME)
                                                .id(user.getId())
                                                .document(user)
                                )
                        );
                    });
                    return req;
                }
        );
        System.out.println("批量创建响应:bulkResponse = " + bulkResponse);

        // 文档删除
        DeleteResponse deleteResponse = client.delete(
                req -> req.index(INDEX_NAME).id("1001")
        );
        System.out.println("删除文档的响应:deleteResponse = " + deleteResponse);
    }

    private static void queryDocument() throws Exception {
        MatchQuery matchQuery = new MatchQuery.Builder()
                .field("age").query(19)
                .build();
        Query query = new Query.Builder()
                .match(matchQuery)
                .build();
        SearchRequest searchRequest = new SearchRequest.Builder()
                .query(query)
                .build();
        SearchResponse<User> userSearchResponse = client.search(searchRequest, User.class);
        System.out.println("userSearchResponse = " + userSearchResponse);
    }

    private static void queryDocumentLambda() throws Exception {
        SearchResponse<User> userSearchResponse = client.search(req -> {
            req.query(
                    q -> q.match(
                            m -> m.field("age").query(19)
                    )
            );
            return req;
        }, User.class);
        System.out.println("userSearchResponse = " + userSearchResponse);
    }

    // 异步与同步最大的区别就是多线程
    private static void asyncOpt() {
        asyncClient.indices().create(req -> req.index("newindex")).whenComplete((resp, err) -> {
            System.out.println("回调方法,在另外一个线程中执行");
            if (err != null) {
                System.out.println("创建索引失败:" + err.getMessage());
                return;
            }
            System.out.println("创建索引成功:" + resp.index());
        });
        System.out.println("主线程执行");
    }
}

映射

类似关系数据库中的表结构,

### mapping 创建映射,可在通过GET查看
curl -XPUT -H "Content-Type: application/json" "http://localhost:9200/users/_mapping"
-d '{
    "properties": {
        "name": {
            "type": "text",      # text类型会被分词,可以通过部分关键字搜索到
            "analyzer": "ik_max_word",
            "search_analyzer": "ik_max_word"
        },
        "gender": {
            "type": "keyword",   # keyword类型不会被分词
            "index": true        # 建立索引
        },
        "tel": {
            "type": "keyword",
            "index": false       # 不能够被搜索
        },
        "address": {
            "type": "text",
            "analyzer": "ik_max_word",
            "search_analyzer": "ik_max_word"
        }
    }
}'

集群

集群名称默认 elasticsearch,通过指定名字确认、加入集群。节点的名称在启动时随机生成,可通过配置文件指定。通过配置elasticsearch.yml 文件配置集群。

cluster.name: my-application   # 所有节点必须相同
node.name: node-1              # 每个节点必须唯一
node.master: true
node.data: true

network.host: 192.168.0.1
http.port: 9200
transport.tcp.port: 9301       # 集群间通信端口

# 配置集群
discovery.seed_hosts: ["localhost:9301","localhost:9302","localhost:9303"]
cluster.initial_master_nodes: ["node-1", "node-2", "node-3"]

# 设置允许跨域
http.cors.enabled: true
http.cors.allow-origin: "*"

 查看集群

curl -XGET http://localhost:1001/_cluster/health
curl -XGET http://localhost:1001/_cat/nodes

进阶

分片:一个索引存储的数据量可能超出了单个节点的限制,es提供将索引划分为多份的能力,每一份称为一个分片。分片数量可以在创建索引时指定。每个分片本身也是一个完善且独立的“索引”,其可放置在集群任何一个节点上。

  • 分片允许水平分割、拓展内容容量
  • 分片上可进行分布式的、并行的操作,进而提高吞吐量

 副本(Replicas):节点可能宕机,为此将一个分片复制多份并放在不同节点上。

  • 副本提高了可用性
  • 副本提高了搜索的并行度

 

 集群中有一个master负责不同机器间的调度,但数据的具体操作不由master决定。扩容时分片会自动在节点间重新分配。

### 创建一个索引,有3个分片,1个副本
curl -H 'Content-Type: application/json' -XPUT 'http://localhost:9200/users'
-d '{
    "settings": {
        "numbers_of_shards": 3,
        "numbers_of_replicas": 1
    }
}'

分片数量在索引创建时确定,但副本数可以动态修改

curl -H 'Content-Type: application/json' -XPUT 'http://localhost:9200/users/_settings'
-d '{
    "settings": {
        "numbers_of_replicas": 2
    }
}'

写:存储数据时通过计算数据hash并对分片取余而确定路由
读:用户可以访问任何一个有主分片或副本的节点读取数据,这个节点称为协调节点。一般采用轮询操作。

写数据

默认等待副本完成同步后再返回成功响应,可通过请求参数设置异步完成副本复制。

consistency: one(只需主分片完成) all(所有分片完成写入) quorum(默认数量完成写入)
timeout: 30s

读操作

更新文档

  1. 客户端发送请求
  2. 经过路由确定处理请求的主分片
  3. 查询数据并做更新
  4. 同步到副本

写入延迟

持久化与日志

ES选择先写入内存,再写入日志。因为es有分词等复杂操作,如果先写日志再写内存,当宕机时会有大量无效日志。
写入内存的数据无法被搜索,必须在被refresh到磁盘缓存后才能被读取。

数据以segment的格式进行存储,是不可修改的。通过追加的方式完成增删改,需要定期合并磁盘中的段。

文档分析

分析工作主要包括:将一块文本分为适合倒排索引的独立词条,将词条统一化为标准格式以提高“可搜索性”。

分析器主要有3个功能:

  1. 字符过滤器,替换HTML元素、&等特殊字符
  2. 将字符串分为单个词条
  3. Token过滤器,可能改变Token,如将token小写,去除a an 等无用token,或增加词条
### 测试分词器
curl -XGET 'http://localhost:9200/users/_analyze?pretty' -d '{
    "analyzer": "standard", # ik_max_word ik_smart
    "text": "这里输入需要进行分词测试的语句"
}'

为了支持中文需要手动下载ik分词器并放入es的plugin文件夹中,ik下的config目录中可以自定义词典文件。

ES 优化

硬件

es的索引和文档都是存储在本地磁盘上,其性能严重依赖磁盘:

  1. 使用SSD
  2. 使用 RAID0,条带化技术可加快磁盘IO,无需镜像或奇偶校验、es自带此功能
  3. 使用多块磁盘,在配置文件中配置多个 path.data 允许使用数据条带技术
  4. 不使用远程挂载存储

分片

es可通过分片和索引加速加大访问并行度,因为访问时的路由策略,索引创建后无法修改分片数。如果分片数过大也有代价:

  1. 一个分片底层就是一个Lucene索引,会消耗一定的文件句柄、内存、cpu运转
  2. 每个索引请求都要命中一个索引分片,如果多个分片在同一个节点上竞争使用相同资源就很糟糕
  3. 计算相关的的词项统计是基于分片的,如果有多个分片而每个都只有很少的数据,会导致低相关度

一般遵循以下配置:

  • 控制每个分片占用的硬盘容量不超过ES的最大VM的堆空间设置(一般设置不超过32G,参考下文的VM设置原则),因此,如果索引的总容量在500G左右,那分片大小在16个左右即可;当然,最好同时考虑原则2。
  • 考虑一下node数量,一般一个节点有时候就是一台物理机,如果分片数过多,大大超过了节点数,很可能会导致一个节点上存在多个分片,一旦该节点故障,即使保持了1个以上的副本,同样有可能会导致数据丢失,集群无法恢复。所以,一般都设置分片数不超过节点数的3倍。
  • 主分片,副本和节点最大数之间数量,我们分配的时候可以参考以下关系:节点数 <= 主分片数*(副本数+1)

写入速度优化

默认设置兼顾可靠性、实时性、写入速度等。实际使用时要根据要求进行偏向化设置。针对搜索性能要求不高,但写入要求较高的场景,尽可能选择写优化场景。

  • 加大 Translog Flush,用于降低 IOops、Writeblock
  • 增加 Index Refresh 间隔,用于减少 Segment Merge 次数
  • 调整 Bulk 线程池和队列
  • 优化节点间任务分布
  • 优化底层Luece索引,降低CPU和IO
  • 对于大量数据使用批量数据操作 Bulk API

es集群的选举流程

  • Elasticsearch的选主是ZenDiscovery模块负责的,主要包含Ping(节点之间通过这个RPc来发现彼此)和Unicast(单播模块包含一个主机列表以控制哪些节点需要pi血g通)这两部分
  • 对所有可以成为master的节点 (node,master:tnue) 根据nodeld字典排序,每次选举每个节点都把自己所知道节点排一次序,然后选出第一个(第0位)节点,暂且认为它是master节点。
  • 如果对某个节点的投票数达到一定的值(可以成为master节点数n/2+1)并且该节点自己也选举自己,那这个节点就是master。否则重新选举一直到满足上述条件。
  • master节点的职责主要包括集群、节点和索引的管理,不负贵文档级别的管理;data节点可以关闭hp功能。
集群“脑裂”原因
  • 网络问题:集群间的网络延迟导致一些节点访问不到master,认为master挂掉了从而选举出新的master,并对master上的分片和副本标红,分配新的主分片
  • 节点负载:主节点的角色既为master又为data,访问量较大时可能会导致ES停止响应造成大面积延迟,此时其他节点得不到主节点的响应认为主节点挂掉了,会重新选取主节点。
  • 内存回收:data节点上的ES进程占用的内存较大,引发VM的大规模内存回收,造成Es进程失去响应。

集群“脑裂”解决

  • 减少误判:discovery.zen.ping timeout节点状态的响应时间,默认为3s,可以适当调大,如果master在该响应时间的范围内没有做出响应应答,判断该节点已经挂掉了。调大参数(如6s,discovery.zen-ping_timeout:.6),可适当减少误判。
  • 选举触发:discovery.zen.minimum_master_nodes:1 该参数是用于控制选举行为发生的最小集群主节点数量。当备选主节点的个数大于等于该参数的值,且备选主节点中有该参数个节点认为主节点挂了,进行选举。官方建议为(2)+1,n为主节点个数(即有资格成为主节点的节点个数)
  • 角色份离:即master节点与data节点分离,限制角色
    • 主节点配置为:node.master:true       node.data:false
    • 从节点配置为:node.master:false      node..data:tue

es中删除和更新文档的流程

  • 删除与更新也是写操作,但是es中文档是不可变的
  • 删除操作:磁盘生每个段有一个相应的.del文件,接收删除请求后在del文件中将数据标记为删除,查询时依旧能够匹配但是在结果中会被过滤掉。当合并段时del文件中的数据不会被合并
  • 更新操作:生成新文档时为其生成一个版本号,执行更新时旧版本文档在del文件中标记为删除,新版本文档被索引到一个新的段。旧文档依旧能匹配查询,但会在结果中过滤掉

ES 文档评分机制

Lucene和ES的得分机制基于词频和逆文档词频公式,简称 TF-IDF 公式。

ES 实现

https://mp.weixin.qq.com/s/WviiVaYJpRDx8AVGxGWO8Q
https://mp.weixin.qq.com/s/wnOZl68GlEw34Ms9obtshw

  • 词条:索引中最小存储和查询单元
  • 词典:词条的集合,一般使用B+或HashMap存储
  • 倒排索引:通过词典查询数据id,再通过id查询数据

 

posted @ 2023-06-07 10:07  某某人8265  阅读(17)  评论(0编辑  收藏  举报