关系型数据库搜索出现的问题
- 性能瓶颈:当数据量越来越大的时候,数据库搜索的性能会有明显的下降。虽然分库分表可以来解决存储的问题,但是性能问题还是不能彻底解决,而且系统复杂度会提高、可用性下降
- 复杂业务:有时候会有拼音、模糊等比较复杂的搜索方式
- 并发能力:数据库是磁盘存储,虽然有缓存方案,但是并不适用,因此数据库的读写并发能力较差,难以应对高并发场景
倒排索引
全文检索:全部都检索,可以按照用户定义的查询规则任意查询,得到目标数据。
内部实现的原理就是倒排索引。
概念:
- 文档(document):用来检索的海量数据,其中每一条数据就是每一个文档。
- 词条(term):对于文档或者搜索出来的数据,通过相对应的算法分词,得到的词语就是词条。
原理:
- 数据按照规则处理完成后储存到索引库(这时候会将每一个文档进行分词,一样的词条汇合成文档集合)
- 用户输入关键词按照规则处理完成后在索引库中检索(将用户输入的关键词分词去找几个分词的文档集合交集就可以得到最终都含有分词的文档)
数据是如何分布:分片机制
数据是如何交互:平行节点 内部交互 节点对等的分布式架构
数据备份:副本机制
常见的数据类型:
- String类型
- 分为text,可分词
- keyword不可分词)
- numerical类型,数值类型,分两类
- 基本数据类型:long,interger,short,byte,double,float,half_float
- 浮点型 scaled_float
- Date 日期类型
- object类型,但是es会将对象数据扁平化处理在存储
各种Request
连接客户端
//建立连接
RestHighLevelClient client = new RestHighLevelClient(
RestClient.builder(
new HttpHost("localhost", 9200, "http"),
new HttpHost("localhost", 9201, "http")));
//关闭客户端
client.close();
创建索引库-CreateIndexRequest
- 创建Request对象,并制定索引库名称
- 指定setting配置
- 指定mapping配置
- 发送请求,得到相应
CreateIndexRequest request = new CreateIndexRequest("user");
request.settings(Settings.builder()
.put("index.number_of_shards", 3)
.put("index.number_of_replicas", 1)
);
request.mapping(
"{\n" +
" \"properties\": {\n" +
" \"id\": {\n" +
" \"type\": \"long\"\n" +
" },\n" +
" \"name\":{\n" +
" \"type\": \"keyword\"\n" +
" },\n" +
" \"age\":{\n" +
" \"type\": \"integer\"\n" +
" },\n" +
" \"gender\":{\n" +
" \"type\": \"keyword\"\n" +
" },\n" +
" \"note\":{\n" +
" \"type\": \"text\",\n" +
" \"analyzer\": \"ik_max_word\"\n" +
" }\n" +
" }\n" +
" }",
//指定映射的内容的类型为json
XContentType.JSON);
CreateIndexResponse response = client.indices()
.create(request, RequestOptions.DEFAULT);
IndexRequest-文档操作
- 准备文档数据
- 创建IndexRequest对象,并指定索引库名称
- 指定新增的数据id
- 将新增的文档数据转化为json格式,发给IndexRequest
User user = new User(110L, "张三", 22, "0", "翻斗小院");
IndexRequest indexRequest = new IndexRequest("user");
indexRequest.id(user.getId().toString());
String userJson = JSON.toJSONString(user);
indexRequest.source(userJson, XContentType.JSON);
IndexResponse response = client.index(indexRequest, RequestOptions.DEFAULT);
注意:ES要是遇到id一致的话,实质是先删除后添加
GetRequest-查询文档
- 新建GetRequest对象,并指定索引库名称,文档id
- 发送请求,得到结果
- 从结果中得到json字符串的source
- 将json反序列化为对象
GetRequest getRequest = new GetRequest("user", "110");
GetResponse response = client.get(getRequest, RequestOptions.DEFAULT);
String sourceAsString = response.getSourceAsString();
User user = JSON.parseObject(sourceAsString, User.class);
System.out.println(user);
DeleteRequest-删除文档
- 创建DeleteRequest对象,指定索引库名称,文档ID
- 发送请求
DeleteRequest request = new DeleteRequest("user","110");
DeleteResponse deleteResponse = client.delete(request,RequestOptions.DEFAULT);
BulkRequest-批量处理
- 从数据库查询文档数据
- 创建BulkRequest对象
- 创建多个IndexRequest对象,组织文档数据,并添加到BulkRequest中
- 发送请求
BulkRequest bulkRequest = new BulkRequest();
for (User user : userList) {
bulkRequest.add(new IndexRequest("user")
.id(user.getId().toString())
.source(JSON.toJSONString(user), XContentType.JSON)
);
}
BulkResponse bulkResponse = client.bulk(bulkRequest, RequestOptions.DEFAULT);
各种查询
- 基本查询
- source筛选
- 分词查询
- 词条查询
- 范围查询
- 布尔查询(Filter查询)
- 排序
- 分页
- 高亮
- 聚合
大致流程
- 创建SearchSourceBuilder对象(添加查询条件QueryBuilders或者添加排序、分页插件)
- 创建SearchRequest对象,并指定索引库名称
- 添加SearchSourceBuilder对象到SearchRequest对象source中
- 发送请求,得到结果
- 解析SearchResponse(获取总条数,获取SearchHits数组并遍历
sourceBuilder.query(
QueryBuilders.matchAllQuery()
);
SearchRequest request = new SearchRequest("user");
request.source(sourceBuilder);
SearchResponse response = client.search(request, RequestOptions.DEFAULT);
SearchHits searchHits = response.getHits();
// 5.1.获取总条数
long total = searchHits.getTotalHits().value;
System.out.println("total = " + total);
// 5.2.获取SearchHit数组,并遍历
SearchHit[] hits = searchHits.getHits();
for (SearchHit hit : hits) {
//获取分数
System.out.println("文档得分:"+hit.getScore());
// - 获取其中的`_source`,是JSON数据
String json = hit.getSourceAsString();
// - 把`_source`反序列化为User对象
User user = JSON.parseObject(json, User.class);
System.out.println("user = " + user);
}
查询所有-matchALL
sourceBuilder.query(QueryBuilders.matchAllQuery());
词条查询-termQuery
ElasticSearch两个数据类型
- text:会分词,不支持聚合
- keyword:不会分词,将全部内容作为一个词条,支持聚合
term查询:不会对查询条件进行分词。
sourceBuilder.query(QueryBuilders.termQuery("name", "小红"));
分词匹配查询-matchQuery
- term query会去倒排索引中寻找确切的term,它并不知道分词器的存在。这种查询适合keyword 、numeric、date
- match query知道分词器的存在。并且理解是如何被分词的
MatchQueryBuilder queryBuilder = QueryBuilders.matchQuery("name", "小红");
模糊查询-fuzzy
设置修正的次数,最大为2
FuzzyQueryBuilder queryBuilder = QueryBuilders.fuzzyQuery("note", "模糊查询");
queryBuilder.fuzziness(Fuzziness.TWO);
范围&排序查询-range&sort
RangeQueryBuilder queryBuilder = QueryBuilders.rangeQuery("age");
// 22 <= age < 27
queryBuilder.gte(22);
queryBuilder.lt(27);
多条件查询-queryString
QueryStringQueryBuilder queryBuilder =
QueryBuilders.queryStringQuery("同学")
.field("name")
.field("note")
// .defaultField("note") // 默认搜索域
.defaultOperator(Operator.AND);
printResultByQuery(queryBuilder);
bool查询&结果过滤-boolQuery
连接方式:
- must(and):条件必须成立
- must_not(not):条件必须不成立
- should(or):条件可以成立
- filter:条件必须成立,性能比must高。不会计算得分
// 1.构建bool条件对象
BoolQueryBuilder queryBuilder = QueryBuilders.boolQuery();
// 2.构建matchQuery对象,查询相信信息`note`为: 同学
MatchQueryBuilder matchQueryBuilder = QueryBuilders.matchQuery("note", "同学");
queryBuilder.must(matchQueryBuilder);
// 3.过滤姓名`name`包含:小武
TermQueryBuilder termQueryBuilder = QueryBuilders.termQuery("name", "小武");
queryBuilder.filter(termQueryBuilder);
// 4.过滤年龄`age`在:23-27
RangeQueryBuilder rangeQueryBuilder = QueryBuilders.rangeQuery("age").gte(23).lte(27);
queryBuilder.filter(rangeQueryBuilder);
分页查询-from
int page = 2; // 当前页
int size = 2; // 一页显示条数
int from = (page - 1) * size; // 每一页起始条数
sourceBuilder.from(from);
sourceBuilder.size(size);
高亮查询-highlight
高亮三要素:
- pre_tags:前置标签,可以省略,默认是em
- post_tags:后置标签,可以省略,默认是em
- fields:需要高亮的字段
- title:这里声明title字段需要高亮,后面可以为这个字段设置特有配置,也可以空
HighlightBuilder highlight = SearchSourceBuilder.highlight();
highlight.field("note"); // 高亮显示域
highlight.preTags("<font color='red'>"); // 高亮显示前缀
highlight.postTags("</font>"); // 高亮显示后缀
sourceBuilder.highlighter(highlight);
//要把结果解析
HighlightField highlightField = searchHit.getHighlightFields().get("note"); // get("高亮显示域名称")
Text[] fragments = highlightField.getFragments();
String note = StringUtils.join(fragments);
// 判断如果是可以获取到数据则更新到用户对象中
if (StringUtils.isNotBlank(note)) {
user.setNote(note);
}
聚合查询-aggregation
参与聚合的字段,必须是keyword类型。
桶-数据分组
度量-聚合运算
- Avg Aggregation:求平均值
- Max Aggregation:求最大值
- Min Aggregation:求最小值
- Percentiles Aggregation:求百分比
- Stats Aggregation:同时返回avg、max、min、sum、count等
- Sum Aggregation:求和
- Top hits Aggregation:求前几
- Value Count Aggregation:求总数
SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();
sourceBuilder.aggregation(
AggregationBuilders
.terms("popular_color")
.field("color.keyword"));
TermsAggregationBuilder popularColorAggs = AggregationBuilders.terms("popular_color").field("color.keyword");
// ***分组后平均价格计算
AvgAggregationBuilder priceAggs = AggregationBuilders.avg("avg_price").field("price");
// ***按颜色分组后添加到子聚合计算
popularColorAggs.subAggregation(priceAggs);
sourceBuilder.aggregation(popularColorAggs)
特殊类型
大多数情况下,数据都是简单的JSON对象,但是如果我们存入的ES数据比较复杂,包含对象。Lucene是不支持对象数据的,因此ES会将数据扁平化处理。
这时候需要引入一个特殊的对象,nested类型,它允许包含一组属性类似object数组的每个对象,可以被独立的搜索,互不影响。
PUT my_index
{
"mappings": {
"properties": {
"user": {
"type": "nested",
"properties": {
"first":{"type":"keyword"},
"last":{"type":"keyword"}
}
}
}
}
}
二级标题自动补全和提示Suggester
Suggester包含三种不同方式,用的Completion模式,实现自动补全和基于上下文的提示功能
1.创建一个articles的索引库,含有suggestion的字段,类型为completion
PUT articles
{
"mappings": {
"properties": {
"suggestion":{
"type": "completion"
}
}
}
}
组词分词器analyzer
PUT /goods
{
"settings": {
"analysis": {
"analyzer": {
"my_pinyin": {
"tokenizer": "ik_smart",
"filter": [
"py"
]
}
},
"filter": {
"py": {
"type": "pinyin",
"keep_full_pinyin": false,
"keep_joined_full_pinyin": true,
"keep_original": true,
"limit_first_letter_length": 16,
"remove_duplicated_term": true
}
}
}
},
"mappings": {
"properties": {
"id": {
"type": "keyword"
},
"name": {
"type": "completion",
"analyzer": "my_pinyin",
"search_analyzer": "ik_smart"
},
"title":{
"type": "text",
"analyzer": "my_pinyin",
"search_analyzer": "ik_smart"
},
"price":{
"type": "long"
}
}
}
}
实战
<!--elastic客户端-->
<dependency>
<groupId>org.elasticsearch.client</groupId>
<artifactId>elasticsearch-rest-high-level-client</artifactId>
<version>7.4.2</version>
</dependency>
<!--日志-->
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-core</artifactId>
<version>2.11.2</version>
</dependency>
<?xml version="1.0" encoding="UTF-8"?>
<Configuration status="WARN">
<Appenders>
<Console name="Console" target="SYSTEM_OUT">
<PatternLayout pattern="%d{HH:mm:ss.SSS} [%t] %-5level %logger{36} - %msg%n"/>
</Console>
</Appenders>
<Loggers>
<Root level="error">
<AppenderRef ref="Console"/>
</Root>
</Loggers>
</Configuration>
/**
* 演示自动补全查询
*/
@Test
public void testSuggest() throws IOException {
// 1.创建 查询条件工厂(封装查询条件) 的对象
SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();
// 1.1.准备Suggest,需要指定四个内容:
// 1)自动补全的名称:name_suggest
// 2)自动补全的类型:SuggestBuilders.completionSuggestion
// 3)自动补全的字段:completionSuggestion("name")
// 4)自动补全的前缀:.prefix("s")
SuggestBuilder suggestBuilder = new SuggestBuilder();
suggestBuilder.addSuggestion("name_suggest",
SuggestBuilders.completionSuggestion("name").prefix("s").size(30));
// 1.2.添加suggest条件
searchSourceBuilder.suggest(suggestBuilder);
// 2.构建 搜索的请求 对象,把sourceBuilder放进去
SearchRequest request = new SearchRequest("goods");
request.source(searchSourceBuilder);
// 3.发请求
SearchResponse response = client.search(request, RequestOptions.DEFAULT);
// 4.解析结果
Suggest suggest = response.getSuggest();
// 4.1.根据名称获取suggest结果
Suggest.Suggestion<? extends Suggest.Suggestion.Entry<? extends Suggest.Suggestion.Entry.Option>> nameSuggest =
suggest.getSuggestion("name_suggest");
// 4.2.遍历结果
nameSuggest.forEach(suggestion -> {
// 获取其中的options
List<? extends Suggest.Suggestion.Entry.Option> options = suggestion.getOptions();
System.out.println("补全的结果如下: ");
// 遍历options
for (Suggest.Suggestion.Entry.Option option : options) {
Text text = option.getText();
System.out.println("\t" + text);
}
});
}