1.数据结构
名称 | 描述 | 与关系型数据库对应 |
Index(索引) | 就有类似特性的文档集合,是一个存储方案,必须全小写 | 数据库 |
Type(l类型) | 是索引的内部逻辑分区,对应的是不同业务的存储逻辑 | 表 |
Document(文档) | 是Lucene索引和搜索的原子单位,基于json表示 | 一条记录 |
filed(域) | 一个文档中有多个域,域可以有多个值,域代表了相同的数据类型 | 列 |
mapping(映射) | 域与数据类型的映射关系 |
|
2.服务架构
名称 | 描述 |
cluster(集群) | 不同的实例组成的集群,根据统一的cluster.name进行连接认证 |
node(节点) | 一个服务实例 |
shard(数据分片) | 一个index存在多个分片,副本永远不会分配到相同的主节点上 |
Replica(数据分片副本) | 一个主分片默认配置一个副本(可设置多个),解决了高可用和分担读压力 |
3.单节点内部架构
节点之间通过Transport模块互相通信,Discovery模块互相发现
GateWay模块持久化存储
RestfulApi层,脚本编译执行层、索引模块、查询模块
4.应用场景
全文搜索检索引擎(Stack OverFlow,GitHub)
实时日志检索系统(ElasticSearch+LogStash,Kibana)
实时在线分析系统(OlAP)
实时推荐系统
5.倒排索引
通过单词导向文档id(建立映射关系),通过增加一个索引(例如B树),索引到指定列上,指向文档的id
单词词典
记录所有稳当的单词,记录单词与倒排列表的关系,通过B+树或者哈希拉链法实现
倒排列表
记录了单词对应的文档详情,主要有一下几项:
词频TF:该单词在文档出现的次数,用于评分
位置(Position):记录单词在文档中的分词位置,用于语句搜索
偏移(Offset):记录单词的开始与结束位置,用于实现高亮显示
6.索引文档过程
根据文档id确定shard编号,转发请求到shard所在的节点(all,one,quorun模式)
在节点上,先将文档内容写入内存,同时记录translog日志,当内存满了再写入文件中
检索时,先查主分片和分片副本,查不到再查translog
7.优缺点
优点:
P2P对等架构,发现故障时自动分配其他节点工作,系统易管理,易伸缩
先写文件系统缓存,再定期刷写到磁盘,写入性能高
字段缓存,过滤器缓存,查询性能高
内存熔断机制保证系统稳定
系统不依赖其他服务,自带分布式协调器,稳定性高
插件式框架,易于开发者自定义插件,可扩展性高
基于translog recovery和NodeFaultDetection,容错性高
缺点:
权限认证、报警系统收费,未开源
没有实现事物,不适合需要事物支持的场景
集群庞大后,异常恢复时间久,网络请求风暴
对于多维度统计计算支持较差
8.学习方向(转载)
(1)偏算法方向:搜索算法/研发工程师。工作内容往往是在"使用"和"二次开发"两方面跟ES打交道,需要知道ES一些机制、DSL写法、索引构建和某些外围插件的开发和使用。如果是偏算法,那么会对分词、查询改写、召回和精排等模块进行优化。其中需要java开发能力,比如写个服务接口,提供自己实现的某算法能力。如果是偏研发,那么可能涉及ES二次开发,需要做一些跟倒排索引构建、跳表查找加速、索引压缩等相关的底层开发,需要对搜索引擎的研发有一定的了解,ES以及底层的lucene都是java开发,自然而然也是用Java。
(2)偏ops方向:ES系统/运维工程师。往往是java后台工程师会往这个方向靠,ES集群的管理、维护和升级,以及最重要的集群性能优化,都是上面搜索算法/研发工程师不具备的能力。虽然ES开箱即用,但离用得好还差个ES系统/运维工程师,而这也是很多公司缺的,因为这几年有意识往这个方向靠的java工程师不多,往往是被动得维护ES才变成了ES OPS专家。所以ES线下沙龙涉及系统优化,大家都是以经验分享,没有形成体系。
(3)偏数据流方向:日志系统研发工程师。10个ES技术分享,假设有2个分享搜索、3个分享ops,其它都是分享ELK系统搭建。虽然有搜索业务的公司很多,但有日志管理需求的公司更多,其中ELK又那么好用,所以能不能用java开发ELK系统也是强需求,能不能从0到1搭一套日志管理系统,拿出手分量也够。
9.分词Analyzer
分词器是将全文本转化为一系列单词(term/token)的过程,可以使用ES内置分词器或者第三方的分词器,除了在数据写入时转换词条,匹配查询语句时也会对查询语句进行分词分析
由三部分组成:
Character Filter:原始文本处理,去掉特殊符号与HTML格式
Tokenizer:按照规则切分单词
TokenFilters:对切分单词加工、小写、删除停顿词,增加同义词
10.API
查询语句 | 释义 |
/_search | 集群上所有索引 |
/index1/_search | index1 |
/index1,index2/_search | index1和index2 |
/index*/_search | index开头的所有索引 |
使用URI查询
get查询,查询叫cessi的姓名索引
http://ip:port/kibana/_search?q=first_name:cessi
post查询,body格式为
{
"query":{
"query_string"{
"default_field":"first_name",
"query": "cessi AND haha"
}
}
}
10.实践(转载)
pom文件配置
<dependency>
<groupId>org.elasticsearch.client</groupId>
<artifactId>elasticsearch-rest-high-level-client</artifactId>
<version>7.4.0</version>
<exclusions>
<exclusion>
<groupId>org.elasticsearch</groupId>
<artifactId>elasticsearch</artifactId>
</exclusion>
<exclusion>
<groupId>org.elasticsearch.client</groupId>
<artifactId>elasticsearch-rest-client</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.elasticsearch.client</groupId>
<artifactId>elasticsearch-rest-client</artifactId>
<version>7.4.0</version>
</dependency>
<dependency>
<groupId>org.elasticsearch</groupId>
<artifactId>elasticsearch</artifactId>
<version>7.4.0</version>
</dependency>
yml配置
es:
host: xx.xx.xx.xx
port: 9200
scheme: http
工具类
package xyz.wongs.weathertop.base.service;
import com.alibaba.fastjson.JSON;
import lombok.extern.slf4j.Slf4j;
import org.elasticsearch.action.admin.indices.delete.DeleteIndexRequest;
import org.elasticsearch.action.admin.indices.exists.indices.IndicesExistsRequest;
import org.elasticsearch.action.admin.indices.exists.indices.IndicesExistsResponse;
import org.elasticsearch.action.bulk.BulkRequest;
import org.elasticsearch.action.delete.DeleteRequest;
import org.elasticsearch.action.index.IndexRequest;
import org.elasticsearch.action.search.SearchRequest;
import org.elasticsearch.action.search.SearchResponse;
import org.elasticsearch.action.support.IndicesOptions;
import org.elasticsearch.client.RequestOptions;
import org.elasticsearch.client.RestHighLevelClient;
import org.elasticsearch.client.indices.CreateIndexRequest;
import org.elasticsearch.client.indices.CreateIndexResponse;
import org.elasticsearch.client.indices.GetIndexRequest;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.common.xcontent.XContentType;
import org.elasticsearch.index.query.QueryBuilder;
import org.elasticsearch.index.reindex.DeleteByQueryRequest;
import org.elasticsearch.search.SearchHit;
import org.elasticsearch.search.builder.SearchSourceBuilder;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import sun.rmi.runtime.Log;
import xyz.wongs.weathertop.base.entiy.ElasticEntity;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
@Slf4j
@Component
public class BaseElasticService {
@Autowired
RestHighLevelClient restHighLevelClient;
/**
* @date 2019/10/17 17:30
* @param idxName 索引名称
* @param idxSQL 索引描述
* @return void
* @throws
* @since
*/
public void createIndex(String idxName,String idxSQL){
try {
if (!this.indexExist(idxName)) {
log.error(" idxName={} 已经存在,idxSql={}",idxName,idxSQL);
return;
}
CreateIndexRequest request = new CreateIndexRequest(idxName);
buildSetting(request);
request.mapping(idxSQL, XContentType.JSON);
// request.settings() 手工指定Setting
CreateIndexResponse res = restHighLevelClient.indices().create(request, RequestOptions.DEFAULT);
if (!res.isAcknowledged()) {
throw new RuntimeException("初始化失败");
}
} catch (Exception e) {
e.printStackTrace();
System.exit(0);
}
}
/** 断某个index是否存在
* @author WCNGS@QQ.COM
* @See
* @date 2019/10/17 17:27
* @param idxName index名
* @return boolean
* @throws
* @since
*/
public boolean indexExist(String idxName) throws Exception {
GetIndexRequest request = new GetIndexRequest(idxName);
request.local(false);
request.humanReadable(true);
request.includeDefaults(false);
request.indicesOptions(IndicesOptions.lenientExpandOpen());
return restHighLevelClient.indices().exists(request, RequestOptions.DEFAULT);
}
/** 断某个index是否存在
* @date 2019/10/17 17:27
* @param idxName index名
* @return boolean
* @throws
* @since
*/
public boolean isExistsIndex(String idxName) throws Exception {
return restHighLevelClient.indices().exists(new GetIndexRequest(idxName),RequestOptions.DEFAULT);
}
/** 设置分片
* @date 2019/10/17 19:27
* @param request
* @return void
* @throws
* @since
*/
public void buildSetting(CreateIndexRequest request){
request.settings(Settings.builder().put("index.number_of_shards",3)
.put("index.number_of_replicas",2));
}
/**
* @author WCNGS@QQ.COM
* @See
* @date 2019/10/17 17:27
* @param idxName index
* @param entity 对象
* @return void
* @throws
* @since
*/
public void insertOrUpdateOne(String idxName, ElasticEntity entity) {
IndexRequest request = new IndexRequest(idxName);
log.error("Data : id={},entity={}",entity.getId(),JSON.toJSONString(entity.getData()));
request.id(entity.getId());
request.source(entity.getData(), XContentType.JSON);
// request.source(JSON.toJSONString(entity.getData()), XContentType.JSON);
try {
restHighLevelClient.index(request, RequestOptions.DEFAULT);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
/** 批量插入数据
* @date 2019/10/17 17:26
* @param idxName index
* @param list 带插入列表
* @return void
* @throws
* @since
*/
public void insertBatch(String idxName, List<ElasticEntity> list) {
BulkRequest request = new BulkRequest();
list.forEach(item -> request.add(new IndexRequest(idxName).id(item.getId())
.source(item.getData(), XContentType.JSON)));
try {
restHighLevelClient.bulk(request, RequestOptions.DEFAULT);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
/** 批量删除
* @date 2019/10/17 17:14
* @param idxName index
* @param idList 待删除列表
* @return void
* @throws
* @since
*/
public <T> void deleteBatch(String idxName, Collection<T> idList) {
BulkRequest request = new BulkRequest();
idList.forEach(item -> request.add(new DeleteRequest(idxName, item.toString())));
try {
restHighLevelClient.bulk(request, RequestOptions.DEFAULT);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
/**
* @date 2019/10/17 17:14
* @param idxName index
* @param builder 查询参数
* @param c 结果类对象
* @return java.util.List<T>
* @throws
* @since
*/
public <T> List<T> search(String idxName, SearchSourceBuilder builder, Class<T> c) {
SearchRequest request = new SearchRequest(idxName);
request.source(builder);
try {
SearchResponse response = restHighLevelClient.search(request, RequestOptions.DEFAULT);
SearchHit[] hits = response.getHits().getHits();
List<T> res = new ArrayList<>(hits.length);
for (SearchHit hit : hits) {
res.add(JSON.parseObject(hit.getSourceAsString(), c));
}
return res;
} catch (Exception e) {
throw new RuntimeException(e);
}
}
/** 删除index
* @date 2019/10/17 17:13
* @param idxName
* @return void
* @throws
* @since
*/
public void deleteIndex(String idxName) {
try {
if (!this.indexExist(idxName)) {
log.error(" idxName={} 已经存在",idxName);
return;
}
restHighLevelClient.indices().delete(new DeleteIndexRequest(idxName), RequestOptions.DEFAULT);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
/**
* @date 2019/10/17 17:13
* @param idxName
* @param builder
* @return void
* @throws
* @since
*/
public void deleteByQuery(String idxName, QueryBuilder builder) {
DeleteByQueryRequest request = new DeleteByQueryRequest(idxName);
request.setQuery(builder);
//设置批量操作数量,最大为10000
request.setBatchSize(10000);
request.setConflicts("proceed");
try {
restHighLevelClient.deleteByQuery(request, RequestOptions.DEFAULT);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}