分布式搜索引擎-2

1.spring data elasticsearch

1.1 简介

Spring Data Elasticsearch 基于 spring data API 简化 Elasticsearch操作,将原始操作Elasticsearch的客户端API 进行封装 。

1.2 框架集成

1.2.1 依赖

创建项目 elasticsearch_springdata_es

编写pom.xml

<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.3.6.RELEASE</version>
    <relativePath/>
</parent>

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <optional>true</optional>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <scope>test</scope>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-elasticsearch</artifactId>
    </dependency>
</dependencies>

<build>
    <plugins>
        <!-- java编译插件 -->
        <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-compiler-plugin</artifactId>
            <version>3.2</version>
            <configuration>
               <source>1.8</source>
               <target>1.8</target>
               <encoding>UTF-8</encoding>
            </configuration>
        </plugin>
    </plugins>
</build>

1.2.2 yml配置

application.properties

# es服务地址
elasticsearch.host=127.0.0.1
# es服务端口
elasticsearch.port=9200

1.2.3 document映射

@Data
@Document(indexName = "product",shards = 1, replicas = 1)
public class Product implements Serializable {
    @Id
    private Long id;
    
    @Field(type = FieldType.Text, analyzer = "ik_max_word")
    private String productName;
    
    @Field(type = FieldType.Integer)
    private Integer store;
    
    @Field(type = FieldType.Double, index = true, store = false)
    private double price;
}

1.2.4 配置类

@Data
@ConfigurationProperties(prefix = "elasticsearch")
@Configuration
public class ElasticsearchConfig extends AbstractElasticsearchConfiguration {
    private String host ;
    private Integer port ;
 
    //重写父类方法
    @Override
    public RestHighLevelClient elasticsearchClient() {
        RestClientBuilder builder = RestClient.builder(new HttpHost(host, port));
        RestHighLevelClient restHighLevelClient = new RestHighLevelClient(builder);
        return restHighLevelClient; 
    }
}

1.2.5 dao数据访问

@Repository
public interface ProductDao extends ElasticsearchRepository<Product,Long> {
}

1.2.6 springboot启动类

@SpringBootApplication
public class Application {
    public static void main(String[] args) {
        SpringApplication.run(Application.class,args);
    }
}

2. 常用api

package com.es;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.data.domain.Sort;
import org.springframework.data.elasticsearch.core.ElasticsearchRestTemplate;
import org.springframework.test.context.junit4.SpringRunner;

@RunWith(SpringRunner.class)
@SpringBootTest
public class ElasticSearchTest {
	
	@Autowired
	private ElasticsearchRestTemplate elasticsearchTemplate;

	@Autowired
	private ProductDao productDao;

	/**
	 * 添加文档
	 * */
	@Test
	public void saveTest(){
		Product product = new Product();
		product.setId(1L);
		product.setProductName("华为手机");
		product.setStore(100);
		product.setPrice(5000.00);

		productDao.save(product);
	}

	/**
	 * 根据ID查询文档
	 * */
	@Test
	public void findById(){
		Product product = productDao.findById(1L).get();
		System.out.println(product);
	}

	@Test
	public void testFind(){
		// 查询全部,并按照价格降序排序
		Iterable<Product> products = this.productDao.findAll(Sort.by(Sort.Direction.DESC, "price"));
		products.forEach(product-> System.out.println(product));
	}
}

@Test
public void test1(){
    //Query query = new NativeSearchQuery(QueryBuilders.matchQuery("productName","华为手机"));
    //Query query = new NativeSearchQuery(QueryBuilders.termQuery("productName","智能手机"));
    //Query query = new NativeSearchQuery(QueryBuilders.prefixQuery("productName","小米"));
    //Query query = new NativeSearchQuery(QueryBuilders.rangeQuery("price").gt(10).lt(100));
    
    //BoolQueryBuilder boolQueryBuilder = QueryBuilders.boolQuery();
    //boolQueryBuilder.must(QueryBuilders.matchQuery("productName","华为"));
    //boolQueryBuilder.must(QueryBuilders.rangeQuery("price").gt(1).lt(10000));
    //Query query = new NativeSearchQuery(boolQueryBuilder);
    
    //FuzzyQueryBuilder queryBuilder = QueryBuilders.fuzzyQuery("productName", "aoliao");
	//Query query = new NativeSearchQuery(queryBuilder);
    
    //MatchAllQueryBuilder queryBuilder = QueryBuilders.matchAllQuery();
	//Query query = new NativeSearchQuery(queryBuilder).setPageable(PageRequest.of(0,2));
    
    Query query = new NativeSearchQuery(QueryBuilders.matchAllQuery());
    
    SearchHits<Product> hits = elasticsearchTemplate.search(query, Product.class);
    
    for (SearchHit<Product> searchHit : hits.getSearchHits()) {
        Product content = searchHit.getContent();
        System.out.println(content);
    }
}

聚合查询,求最大值

@Test
public void test1(){

    MaxAggregationBuilder aggregationBuilder = AggregationBuilders.max("price_max").field("price");

    NativeSearchQuery searchQuery = new NativeSearchQueryBuilder()
        .withQuery(QueryBuilders.matchQuery("productName", "华为"))
        .withPageable(PageRequest.of(0, 3))
        .addAggregation(aggregationBuilder).build();

    SearchHits<Product> hits = elasticsearchTemplate.search(searchQuery, Product.class);
    for (SearchHit<Product> searchHit : hits.getSearchHits()) {
        Product content = searchHit.getContent();
        System.out.println(content);
    }

    Aggregations aggregations = hits.getAggregations();
    ParsedMax agg = (ParsedMax)aggregations.get("price_max");
    double value = agg.getValue();
    System.out.println(value);
}

高亮

@Test
public void test1(){

    HighlightBuilder highlightBuilder = new HighlightBuilder();
    highlightBuilder.field("productName").preTags("<b color='red'>").postTags("</b>");


    NativeSearchQuery searchQuery = new NativeSearchQueryBuilder()
        .withQuery(QueryBuilders.matchQuery("productName", "华为"))
        .withPageable(PageRequest.of(0, 3))
        .withHighlightBuilder(highlightBuilder).build();


    SearchHits<Product> hits = elasticsearchTemplate.search(searchQuery, Product.class);
    for (SearchHit<Product> searchHit : hits.getSearchHits()) {
        Product content = searchHit.getContent();
        content.setProductName(searchHit.getHighlightField("productName").get(0));
        System.out.println(content);
    }
}

3. elasticsearch集群

单台Elasticsearch服务器提供服务,往往都有最大的负载能力,超过这个阈值,服务器性能就会大大降低甚至不可用,所以生产环境中,一般都是运行在指定服务器集群中。

除了负载能力,单点服务器也存在其他问题:

  • 单台机器存储容量有限
  • 单服务器容易出现单点故障,无法实现高可用
  • 单服务的并发处理能力有限

配置服务器集群时,集群中节点数量没有限制,大于等于2个节点就可以看做是集群了。一般出于高性能及高可用方面来考虑集群中节点数量都是3个以上。

3.1 集群安装

3.1.1 环境搭建

宿主机执行命令:

vi /etc/sysctl.conf
vm.max_map_count=655360
sysctl -p

宿主机创建

/es-node1/elasticsearch.yml

cluster.name: my-application
cluster.routing.allocation.disk.threshold_enabled: false
node.name: node-1
network.host: 0.0.0.0
http.port: 9201
transport.tcp.port: 9301
node.master: true
node.data: true
cluster.initial_master_nodes: ["node-1"]
discovery.seed_hosts: ["192.168.188.128:9301","192.168.188.128:9302","192.168.188.128:9303"]
http.cors.allow-origin: "*"
http.cors.enabled: true

/es-node2/elasticsearch.yml

cluster.name: my-application
cluster.routing.allocation.disk.threshold_enabled: false
node.name: node-2
network.host: 0.0.0.0
http.port: 9202
transport.tcp.port: 9302
node.master: true
node.data: true
cluster.initial_master_nodes: ["node-1"]
discovery.seed_hosts: ["192.168.188.128:9301","192.168.188.128:9302","192.168.188.128:9303"]
http.cors.allow-origin: "*"
http.cors.enabled: true

/es-node3/elasticsearch.yml

cluster.name: my-application
cluster.routing.allocation.disk.threshold_enabled: false
node.name: node-3
network.host: 0.0.0.0
http.port: 9203
transport.tcp.port: 9303
node.master: true
node.data: true
cluster.initial_master_nodes: ["node-1"]
discovery.seed_hosts: ["192.168.188.128:9301","192.168.188.128:9302","192.168.188.128:9303"]
http.cors.allow-origin: "*"
http.cors.enabled: true

创建3个容器

docker run -p 9201:9201 -p 9301:9301   -d --name node-1    -v  /es-node1/elasticsearch.yml:/usr/share/elasticsearch/config/elasticsearch.yml   docker.elastic.co/elasticsearch/elasticsearch:7.8.0
docker run -p 9202:9202 -p 9302:9302   -d --name node-2    -v  /es-node2/elasticsearch.yml:/usr/share/elasticsearch/config/elasticsearch.yml   docker.elastic.co/elasticsearch/elasticsearch:7.8.0
docker run -p 9203:9203 -p 9303:9303   -d --name node-3    -v  /es-node3/elasticsearch.yml:/usr/share/elasticsearch/config/elasticsearch.yml   docker.elastic.co/elasticsearch/elasticsearch:7.8.0

3.1.2 head插件

kibana中查看集群相关的信息不是那么的直观,这里介绍一款第三方浏览器插件(elasticsearch-head)来查看和管理集群。

在Chrome浏览器地址栏中输入:chrome://extensions/,或按照下图打开“扩展程序”

head_chrome

将课件中【ElasticSearch-head-Chrome-0.1.5-Crx4Chrome.crx】文件拖到扩展程序页面上即可。

head_chrome2

3.1.3 查看集群情况

head_chrome3

3.1.4 修改kibana

server.port: 5601
server.host: "0.0.0.0"
elasticsearch.hosts: ["http://192.168.188.128:9201","http://192.168.188.128:9202","http://192.168.188.128:9203"]

重启kibana

netstat -anltp|grep 5601

kill -9 进程id

3.1.5 集群测试

# 请求方法:PUT
PUT /shopping
{
  "settings": {},
  "mappings": {
      "properties": {
        "title":{
          "type": "text"
        },
        "subtitle":{
          "type": "text"
        },
        "images":{
          "type": "keyword",
          "index": false
        },
        "price":{
          "type": "float",
          "index": true
        }
      }
  }
}

# 添加文档
POST /shopping/_doc/1
{
    "title":"小米手机",
    "images":"http://www.gulixueyuan.com/xm.jpg",
    "price":3999.00
}

3.1.6 服务器运行状态

l Green

所有的主分片和副本分片都已分配。你的集群是 100% 可用的。

l yellow

所有的主分片已经分片了,但至少还有一个副本是缺失的。不会有数据丢失,所以搜索结果依然是完整的。不过,你的高可用性在某种程度上被弱化。如果 更多的 分片消失,你就会丢数据了。把 yellow 想象成一个需要及时调查的警告。

l red

至少一个主分片(以及它的全部副本)都在缺失中。这意味着你在缺少数据:搜索只能返回部分数据,而分配到这个分片上的写入请求会返回一个异常。

3.2 elasticsearch中的集群核心概念

3.2.1 集群Cluster

一个集群就是由一个或多个服务器节点组织在一起,共同持有整个数据,并一起提供索引和搜索功能。

一个Elasticsearch集群有一个唯一的名字标识,这个名字默认就是”elasticsearch”

这个名字是重要的,因为一个节点只能通过指定某个集群的名字,来加入这个集群。

image-20211007172319628

3.2.2 节点Node

集群中包含很多服务器,一个节点就是其中的一个服务器。作为集群的一部分,它存储数据,参与集群的索引和搜索功能。

无论我们将请求发送到哪个节点,它都能负责从各个包含我们所需文档的节点收集回数据,并将最终结果返回給客户端。

3.2.3 分片(Shards)

提高存储能力,提高搜索的响应效率,需要将数据分片存储。

分片很重要,主要有两方面的原因:

1)允许你水平分割 / 扩展你的内容容量。

2)允许你在分片之上进行分布式的、并行的操作,进而提高性能/吞吐量。

3.2.4 副本(Replicas)

在一个网络 / 云的环境里,失败随时都可能发生,在某个分片/节点不知怎么的就处于离线状态,或者由于任何原因消失了,这种情况下,有一个故障转移机制是非常有用并且是强烈推荐的。

为此目的,Elasticsearch允许你创建分片的一份或多份拷贝,这些拷贝叫做复制分片(副本)。

复制分片之所以重要,有两个主要原因:

  • 在分片/节点失败的情况下,提供了高可用性。因为这个原因,注意到复制分片从不与原/主要(original/primary)分片置于同一节点上是非常重要的。
  • 扩展你的搜索量/吞吐量,因为搜索可以在所有的副本上并行运行。

3.2.5 分配(Allocation)

将分片分配给某个节点的过程,包括分配主分片或者副本。

如果是副本,还包含从主分片复制数据的过程。

这个过程是由master节点完成的。 即:Elasticsearch的分片分配和均衡机制。

3.2.6 节点类型

es集群中的节点类型分为:Master、DataNode。

主节点(Master Node)

主要负责管理集群的全局状态,包括创建或删除索引,跟踪哪些节点是集群的一部分,以及决定哪些分片分配给哪些节点。

集群中任何时候只能有一个主节点负责这些任务,但是可以有多个候选主节点以防当前主节点失败。

数据节点(Data Node)

存储数据,执行与数据相关的操作,如CRUD(创建、读取、更新、删除)、搜索和聚合。

数据节点的数量和性能直接影响Elasticsearch的数据处理能力。

3.3 elasticsearch分片

我们可以在建立索引的时候创建分片信息:

#number_of_shards:主分片数量,默认1(6.x版本默认为5)
#number_of_replicas:每个主分片对应的副本数量,默认1
PUT /users
{
  "settings": {
    "number_of_shards": 3,
    "number_of_replicas": 2
  }
}

image-20211007185928732

★ 代表当前节点为master节点

● 表示DataNode节点

粗线框格子为主分片,细线框为副本分片,为了保证可靠性,主分片与副本分片不能同时在一台机器上。

3.4 读写流程

3.4.1 写流程

新建和删除请求都是写操作, 必须在主分片上面完成之后才能被复制到相关的副本分片

image-20211007220017456

路由计算分片位置:

shard = hash(routing) % number_of_primary_shards

routing 默认值是文档的 id,也可以采用自定义值,比如用户 id

3.4.2 读流程

image-20211007222817128

4.分片原理

4.1 倒排索引

Elasticsearch 使用一种称为*倒排索引*的结构,它适用于快速的全文搜索。

  • 正向索引,就是搜索引擎会将待搜索的文件都对应一个文件ID,搜索时将这个ID和搜索关键字进行对应,形成K-V对,然后对关键字进行统计计数。

但是如果海量的文档数据,这样的索引结构根本无法满足实时返回排名结果的要求。

img

  • 倒排索引:即把文件ID对应到关键词的映射转换为关键词到文件ID的映射,每个关键词都对应着一系列的文件,这些文件中都出现这个关键词。

img

一个倒排索引由文档中所有不重复词的列表构成,对于其中每个词,有一个包含它的文档列表。例如,假设我们有两个文档,每个文档的 content 域包含如下内容:

  • The quick brown fox jumped over the lazy dog

  • Quick brown foxes leap over lazy dogs in summer

为了创建倒排索引,我们首先将每个文档的 content 域拆分成单独的 词(我们称它为 词条 或 tokens ),创建一个包含所有不重复词条的排序列表(倒排表),然后列出每个词条出现在哪个文档。结果如下所示:

img

现在,如果我们想搜索 quick brown ,我们只需要查找包含每个词条的文档:

img

两个文档都匹配,但是第一个文档比第二个匹配度更高。

4.2 倒排索引的不可变性(可以删除,只是逻辑删除)

es中倒排索引建立后写入磁盘,便不会发生改变。

  • 好处:
    • 不需要锁。由于它的不变性,就不用担心多线程并发修改导致的数据安全问题
    • es有缓存,只要缓存中有足够的空间,磁盘上的数据加载到内存缓存中,大部分请求会读取缓存,而不是命中磁盘,提升了性能。
    • 单个大索引允许被数据压缩,减少磁盘IO。

4.3 动态更新索引(段搜索)

如何在保留不变性的前提下实现倒排索引的更新?

答案是: 用更多的索引。

通过增加新的补充索引来反映新近的修改,而不是直接重写整个倒排索引。查询时,轮流遍历查询每一个倒排索引,再对结果进行合并。

Elasticsearch 基于 Lucene,这个java库引入了按段搜索的概念。 每一 段本身都是一个倒排索引。

还增加了提交点的概念:一个列出了所有已知段的文件。

img

提交点 包含一个 .del 文件,文件中会列出被删除文档的段信息。

当一个文档被 “删除” 时,它实际上只是在 .del 文件中被标记删除。一个被标记删除的文档仍然可以被查询匹配到,但它会在最终结果被返回前从结果集中移除。

当一个文档被更新时,旧版本文档被标记删除,文档的新版本被索引到一个新的段中。 可能两个版本的文档都会被一个查询匹配到,但被删除的那个旧版本文档在结果集返回前就已经被移除。

4.4 近实时搜索

新写入的数据,1s秒后即可查询到,这就是近实时搜索。

image-20211008105503240

  • 分段数据先写入到内存缓存中,同时文档操作也会记录translog日志

  • 内存的数据对查询不可见,默认间隔1s将内存中数据写入到文件系统缓存中,这里面的数据对查询可见。

  • 文件系统缓存数据间隔30分钟再将数据刷入磁盘中,形成一个段。

  • 如果文件系统缓存数据在没有刷新到硬盘时宕机了,可以从translog中恢复数据到磁盘,数据恢复完成后translog数据也会清理。

4.5 段合并

由于自动刷新流程每秒会创建一个新的段 ,这样会导致短时间内的段数量暴增。而段数目太多会带来较大的麻烦。 每一个段都会消耗文件句柄、内存和cpu运行周期。更重要的是,每个搜索请求都必须轮流检查每个段;所以段越多,搜索也就越慢。

Elasticsearch通过在后台进行段合并来解决这个问题。小的段被合并到大的段,然后这些大的段再被合并到更大的段。

段合并的时候会将那些旧的已删除文档从文件系统中清除。被删除的文档(或被更新文档的旧版本)不会被拷贝到新的大段中。

image-20240825111142449

5.优化建议

5.1 硬件选择

Elasticsearch 重度使用磁盘,优化磁盘 I/O 的技巧:

  • 使用 SSD 固态硬盘,比机械磁盘性能好。

  • 使用 RAID 0。也称为数据分条(Data Stripping)或条带化(Stripe),是一种磁盘阵列技术,旨在通过将数据分散存储在多个硬盘上以提高存储性能。

  • 不要使用远程挂载的存储,比如 NFS 或者 SMB/CIFS。这个引入的延迟对性能来说完全是背道而驰的。

5.2 合理设置分片数

分片和副本的设计为 ES 提供了支持分布式和故障转移的特性,但并不意味着分片和副本是可以无限分配的。

主分片,副本和节点最大数之间数量,我们分配的时候可以参考以下关系:

节点数 <= 主分片数*(副本数+1)

5.3 推迟分片分配

对于节点瞬时中断的问题,默认情况,集群会等待一分钟来查看节点是否会重新加入,如果这个节点在此期间重新加入,重新加入的节点会保持其现有的分片数据,不会触发新的分片分配。这样就可以减少 ES 在自动再平衡可用分片时所带来的极大开销。

通过修改参数 delayed_timeout ,可以延长再均衡的时间,可以全局设置也可以在索引级别进行修改:

PUT /_all/_settings 
{
 "settings": {
  "index.unassigned.node_left.delayed_timeout": "5m" 
 }
}

5.4 使用批量操作

ES 提供了 Bulk API 支持批量操作,当我们有大量的写任务时,可以使用 Bulk 来进行批量写入。

通用的策略如下:Bulk 默认设置批量提交的数据量不能超过 100M。数据条数一般是根据文档的大小和服务器性能而定的,但是单次批处理的数据大小应从 5MB~15MB 逐渐增加,当性能没有提升时,把这个数据量作为最大值。

5.5 减少Refresh的次数

Lucene 在新增数据时,采用了延迟写入的策略,默认情况下索引的 refresh_interval 为 1 秒。

Lucene 将待写入的数据先写到内存中,超过 1 秒(默认)时就会触发一次 Refresh,然后 Refresh 会把内存中的的数据刷新到操作系统的文件缓存系统中。

如果我们对搜索的实效性要求不高,可以将 Refresh 周期延长,例如 30 秒。

这样还可以有效地减少段刷新次数,但这同时意味着需要消耗更多的Heap内存。

image-20211008105503240

5.6 减少Flush

Flush 的主要目的是把文件缓存系统中的段持久化到硬盘;减少flush的目的:降低磁盘IO。

触发flush的时机:

  • 30分钟
  • Translog 达到512MB

5.7 减少副本的数量

副本越多,写数据的效率会降低,因为数据要同步到副本上。

5.8 内存设置

假设服务器有 128 GB 的内存,你可以创建两个节点,每个节点分配内存 32 GB,剩下的 64 GB 的内存给 Lucene的缓存使用。

posted @   LilyFlower  阅读(4)  评论(0编辑  收藏  举报
编辑推荐:
· .NET Core 中如何实现缓存的预热?
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 基于Microsoft.Extensions.AI核心库实现RAG应用
· Linux系列:如何用heaptrack跟踪.NET程序的非托管内存泄露
阅读排行:
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· 阿里巴巴 QwQ-32B真的超越了 DeepSeek R-1吗?
· 【译】Visual Studio 中新的强大生产力特性
· 10年+ .NET Coder 心语 ── 封装的思维:从隐藏、稳定开始理解其本质意义
· 【设计模式】告别冗长if-else语句:使用策略模式优化代码结构
点击右上角即可分享
微信分享提示