分布式搜索引擎-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/,或按照下图打开“扩展程序”
将课件中【ElasticSearch-head-Chrome-0.1.5-Crx4Chrome.crx】文件拖到扩展程序页面上即可。
3.1.3 查看集群情况
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”。
这个名字是重要的,因为一个节点只能通过指定某个集群的名字,来加入这个集群。
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
}
}
★ 代表当前节点为master节点
● 表示DataNode节点
粗线框格子为主分片,细线框为副本分片,为了保证可靠性,主分片与副本分片不能同时在一台机器上。
3.4 读写流程
3.4.1 写流程
新建和删除请求都是写操作, 必须在主分片上面完成之后才能被复制到相关的副本分片
路由计算分片位置:
shard = hash(routing) % number_of_primary_shards
routing 默认值是文档的 id,也可以采用自定义值,比如用户 id。
3.4.2 读流程
4.分片原理
4.1 倒排索引
Elasticsearch 使用一种称为*倒排索引*的结构,它适用于快速的全文搜索。
- 正向索引,就是搜索引擎会将待搜索的文件都对应一个文件ID,搜索时将这个ID和搜索关键字进行对应,形成K-V对,然后对关键字进行统计计数。
但是如果海量的文档数据,这样的索引结构根本无法满足实时返回排名结果的要求。
- 倒排索引:即把文件ID对应到关键词的映射转换为关键词到文件ID的映射,每个关键词都对应着一系列的文件,这些文件中都出现这个关键词。
一个倒排索引由文档中所有不重复词的列表构成,对于其中每个词,有一个包含它的文档列表。例如,假设我们有两个文档,每个文档的 content 域包含如下内容:
-
The quick brown fox jumped over the lazy dog
-
Quick brown foxes leap over lazy dogs in summer
为了创建倒排索引,我们首先将每个文档的 content 域拆分成单独的 词(我们称它为 词条 或 tokens ),创建一个包含所有不重复词条的排序列表(倒排表),然后列出每个词条出现在哪个文档。结果如下所示:

现在,如果我们想搜索 quick brown ,我们只需要查找包含每个词条的文档:
两个文档都匹配,但是第一个文档比第二个匹配度更高。
4.2 倒排索引的不可变性(可以删除,只是逻辑删除)
es中倒排索引建立后写入磁盘,便不会发生改变。
- 好处:
- 不需要锁。由于它的不变性,就不用担心多线程并发修改导致的数据安全问题
- es有缓存,只要缓存中有足够的空间,磁盘上的数据加载到内存缓存中,大部分请求会读取缓存,而不是命中磁盘,提升了性能。
- 单个大索引允许被数据压缩,减少磁盘IO。
4.3 动态更新索引(段搜索)
如何在保留不变性的前提下实现倒排索引的更新?
答案是: 用更多的索引。
通过增加新的补充索引来反映新近的修改,而不是直接重写整个倒排索引。查询时,轮流遍历查询每一个倒排索引,再对结果进行合并。
Elasticsearch 基于 Lucene,这个java库引入了按段搜索的概念。 每一 段本身都是一个倒排索引。
还增加了提交点的概念:一个列出了所有已知段的文件。
提交点 包含一个 .del 文件,文件中会列出被删除文档的段信息。
当一个文档被 “删除” 时,它实际上只是在 .del 文件中被标记删除。一个被标记删除的文档仍然可以被查询匹配到,但它会在最终结果被返回前从结果集中移除。
当一个文档被更新时,旧版本文档被标记删除,文档的新版本被索引到一个新的段中。 可能两个版本的文档都会被一个查询匹配到,但被删除的那个旧版本文档在结果集返回前就已经被移除。
4.4 近实时搜索
新写入的数据,1s秒后即可查询到,这就是近实时搜索。
-
分段数据先写入到内存缓存中,同时文档操作也会记录translog日志
-
内存的数据对查询不可见,默认间隔1s将内存中数据写入到文件系统缓存中,这里面的数据对查询可见。
-
文件系统缓存数据间隔30分钟再将数据刷入磁盘中,形成一个段。
-
如果文件系统缓存数据在没有刷新到硬盘时宕机了,可以从translog中恢复数据到磁盘,数据恢复完成后translog数据也会清理。
4.5 段合并
由于自动刷新流程每秒会创建一个新的段 ,这样会导致短时间内的段数量暴增。而段数目太多会带来较大的麻烦。 每一个段都会消耗文件句柄、内存和cpu运行周期。更重要的是,每个搜索请求都必须轮流检查每个段;所以段越多,搜索也就越慢。
Elasticsearch通过在后台进行段合并来解决这个问题。小的段被合并到大的段,然后这些大的段再被合并到更大的段。
段合并的时候会将那些旧的已删除文档从文件系统中清除。被删除的文档(或被更新文档的旧版本)不会被拷贝到新的大段中。
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内存。
5.6 减少Flush
Flush 的主要目的是把文件缓存系统中的段持久化到硬盘;减少flush的目的:降低磁盘IO。
触发flush的时机:
- 30分钟
- Translog 达到512MB
5.7 减少副本的数量
副本越多,写数据的效率会降低,因为数据要同步到副本上。
5.8 内存设置
假设服务器有 128 GB 的内存,你可以创建两个节点,每个节点分配内存 32 GB,剩下的 64 GB 的内存给 Lucene的缓存使用。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· .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语句:使用策略模式优化代码结构