文章目录
前提
开源的搜索引擎,从海量数据中快速找到需要的内容。(分词检索,类似百度查询、博客文章关键词搜索)
elasticsearch结合 Kibana、Logstas、Beats,也就是 elastic stack(简称ELK),广泛应用于日志分析、实时监控。
JDK兼容性:https://www.elastic.co/cn/support/matrix#matrix_jvm
操作系统兼容性:https://www.elastic.co/cn/support/matrix
自身兼容性:https://www.elastic.co/cn/support/matrix#matrix_compatibility
对于ES 8.1 及以上版本而言,支持 JDK 17、JDK 18
倒排索引
MySQL、ES的区别和关联
mysql擅长事务操作(ACID),确保数据安全和一致性
ES擅长海量数据搜索、分析、计算
文档
elasticsearch是面向文档存储的(JSON),每一条数据就是一个文档
索引
es中的索引是指相同类型的文档集合
映射:索引中文档字段的约束,比如名称、类型
IK分词器
作用:
- 在创建倒排索引时对文档进行分词
- 用户搜索时,对内容进行分词
ik分词器的两种模式
POST /_analyze
{
"text":"这是程序员的一次测试,包含English",
"analyzer":"ik_max_word"
}
ik_smart:最小切分(粗粒度),分出来的词不会再细分(程序员)
ik_max_word:最细切分(细粒度),分出来的词更多更细(程序员、程序)
拓展词条、停用词条
进入docker 创建的容器的插件目录,找到Ik分词器下的 IKAnalyzer.cfg.xml 文件,扩展词典在 中添加文件名称(例如ext.dic),停用词典在 中添加,(例如stopword.dic)。当然之后需要你手段创建词典文件,内容格式为一词一行。
索引库
mapping属性
mapping映射是对索引库中文档的约束。类似mysql对表单字段的约束
json
{
"id":[1, 2, 3, 4, 5],
"name":{
"firstname":"明",
"lastname":"李"
}
}
- type:字段数据类型,常见的类型有:
- 字符串:text(可分词的文本)、keyword(不可分词的文本,例如国家、品牌、IP地址)
- 布尔:boolean
- 日期:date
- 数值:long、short、byte、integer、double、float
- Object:对象
es里面没有数组类型,json格式key、value,允许value有多个值。上图id字段
- index:是否创建索引,默认为true。就是是否创建倒排索引,不创建之后就不能通过它搜索。
- analyzer:使用那种分词器
- properties:该字段的子字段,上面name
索引库的crud
json
# 建立索引库
PUT /linsy
{
"mappings": {
"properties": {
"info": {
"type": "text",
"analyzer": "ik_smart"
},
"email": {
"type": "keyword",
"index": false
},
"name": {
"type": "object",
"properties": {
"firstname": {
"type": "keyword"
},
"lastName": {
"type": "keyword"
}
}
}
}
}
}
查询索引库 GET /索引库名 GET /linsy
删除索引库 DELETE /索引库名
ES 禁止修改索引库字段类型及属性,会影响整个索引的结构,但是允许在原有索引库中新增字段。
注意不能新增已有字段
json
PUT /索引库名/_mapping
{
"properties": {
"新字段名": {
"type": "integer"
}
}
}
文档的crud
新增操作
POST /索引库名/_doc/文档id
{
"字段1": "值1",
"字段2": "值2",
"字段3": "值3",
}
查询 GET /索引库名/_doc/文档id
删除 DELETE /索引库名/_doc/文档id
json
# 文档操作
# 插入
POST /linsy/_doc/1
{
"age": "11",
"email": "linsy@linsy.work",
"info": "this is a first test 文档",
"name": {
"firstname": "明",
"lastName": "李"
}
}
GET /linsy/_doc/1
DELETE /linsy/_doc/1
POST /linsy/_update/1
{
"doc":{
"age":1111
}
}
修改文档:
- 全量修改:删除旧文档,添加新文档。就是将上面新增的 DSL 改为 PUT
json
PUT /索引库名/_doc/文档id
{
"字段1": "值1",
"字段2": "值2",
"字段3": "值3",
}
- 增量修改,修改指定字段
json
POST /索引库名/_update/文档id
{
"doc":{
"字段名":"新的值"
}
}
RestClient
springboot 导入elasticsearch依赖需注意,它默认使用的版本和springboot的版本一致,你需要对应到安装在服务器上的版本。
xml
<dependency>
<groupId>org.elasticsearch.client</groupId>
<artifactId>elasticsearch-rest-high-level-client</artifactId>
</dependency>
xml
<properties>
<java.version>8</java.version>
<elasticsearch.version>7.17.11</elasticsearch.version>
</properties>
创建索引库的mapping映射
json
PUT /hotel
{
"mappings": {
"properties": {
"id":{
"type": "keyword"
},
"name":{
"type": "text",
"analyzer": "ik_max_word",
"copy_to": "all"
},
"address":{
"type": "keyword",
"index": false
},
"price":{
"type": "integer"
},
"score":{
"type": "integer"
},
"brand":{
"type": "keyword",
"copy_to": "all"
},
"city":{
"type": "keyword"
},
"starName":{
"type": "keyword"
},
"business":{
"type": "keyword",
"copy_to": "all"
},
"location":{
"type": "geo_point"
},
"pic":{
"type": "keyword",
"index": false
},
"all":{
"type": "text",
"analyzer": "ik_max_word"
}
}
}
}
RestHighLevelClient 的使用
java
RestHighLevelClient restHighLevelClient = new RestHighLevelClient(RestClient.builder(
HttpHost.create("http://http://192.168.52.150:9200")
));
// index的增删查
CreateIndexRequest createIndexRequest = new CreateIndexRequest("linsy");
createIndexRequest.source("建立索引库语句(put)", XContentType.JSON);
restHighLevelClient.indices().create(createIndexRequest, RequestOptions.DEFAULT);
restHighLevelClient.indices().delete(new DeleteIndexRequest("要删除的索引库名"), RequestOptions.DEFAULT);
// 判断是否存在
boolean b = restHighLevelClient.indices().exists(new GetIndexRequest("索引库名"), RequestOptions.DEFAULT);
es8.x已经弃用了RestHighLevelClient
官方创建RestClient文档
文档的crud
查询文档
java
@Autowired
private IHotelService iHotelService;
@BeforeEach
public void before() {
restHighLevelClient = new RestHighLevelClient(RestClient.builder(
HttpHost.create("http://192.168.52.150:9200")
));
}
@AfterEach
public void after() throws IOException {
restHighLevelClient.close();
}
@Test
public void addDocumentTest() throws IOException {
Hotel hotel = iHotelService.getById(61075);
HotelDoc hotelDoc = new HotelDoc(hotel);
IndexRequest indexRequest = new IndexRequest("hotel").id(hotel.getId().toString());
indexRequest.source(JSON.toJSONString(hotelDoc), XContentType.JSON);
restHighLevelClient.index(indexRequest, RequestOptions.DEFAULT);
}
@Test
public void queryDocumentTest() throws IOException {
GetResponse getResponse = restHighLevelClient.get(new GetRequest("hotel", "61075"), RequestOptions.DEFAULT);
String json = getResponse.getSourceAsString();
System.out.println(json);
}
@Test
public void updateDocumentTest() throws IOException {
UpdateRequest updateRequest = new UpdateRequest("hotel", "61075");
updateRequest.doc(
"city", "北京",
"score", "90"
);
restHighLevelClient.update(updateRequest, RequestOptions.DEFAULT);
}
@Test
public void deleteDocumentTest() throws IOException {
restHighLevelClient.delete(new DeleteRequest("hotel", "61075"), RequestOptions.DEFAULT);
}
@Test
public void batchAdd() throws IOException {
BulkRequest bulkRequest = new BulkRequest();
List<Hotel> list = iHotelService.list();
for (Hotel hotel : list) {
HotelDoc hotelDoc = new HotelDoc(hotel);
bulkRequest.add(new IndexRequest("hotel")
.id(hotel.getId().toString())
.source(JSON.toJSONString(hotelDoc), XContentType.JSON));
}
restHighLevelClient.bulk(bulkRequest, RequestOptions.DEFAULT);
}
DSL
查询
DSL 查询种类
- 查询所有:查询所有数据,一般在测试时使用。march_all,但是一般显示全部,有一个分页的功能
- 全文检索(full text)查询:利用分词器对用户的输入内容进行分词,然后去倒排索引库匹配。例如:
- match_query
- mutil_match_query
- 精确查询:根据精确词条值查询数据,一般查找的时keyword、数值、日期、boolean等字段。例如:
- ids
- term
- range
- 地理查询(geo):根据经纬度查询,例如:
- geo_distance
- geo_bounding_box
- 复合(compound)查询:复合查询时将上面各种查询条件组合在一起,合并查询条件。例如:
- bool
- funcation_score
DSL query 基本语法
json
# DSL查询
GET /indexName/_search
{
"query":{
"查询类型":{
"查询条件":"条件值"
}
}
}
match 与 multi_match 的与别是前者根据单字段查,后者根据多字段查。
参与搜索的字段越多,查询效率越低,建议利用copy_to将多个检索字段放在一起,然后使用match---all字段查。
json
GET /hotel/_search
{
"query": {
"match": {
"city": "上海"
}
}
}
GET /hotel/_search
{
"query": {
"match": {
"all": "如家"
}
}
}
GET /hotel/_search
{
"query": {
"multi_match": {
"query": "如家",
"fields": ["name","brand","business"]
}
}
}
精确查询: term字段全值匹配,range字段范围匹配。
精确查询一般查找keyword、数值、boolean等不可分词的字段
json
# term
GET /hotel/_search
{
"query": {
"term": {
"city": {
"value": "北京"
}
}
}
}
# range
GET /hotel/_search
{
"query": {
"range": {
"price": {
"gt": 1000,
"lt": 2000
}
}
}
}
地理查询:
json
GET /hotel/_search
{
"query": {
"geo_bounding_box": {
"location": {
"top_left": {
"lat": 31.1,
"lon": 121.5
},
"bottom_right": {
"lat": 30.9,
"lon": 121.7
}
}
}
}
}
GET /hotel/_search
{
"query": {
"geo_distance": {
"distance": "20km",
"location": {
"lat": 31.13,
"lon": 121.8
}
}
}
}
复合查询(compound ):将简单查询条件组合在一起,实现复杂搜索逻辑。
function score:算分函数查询
,可以控制文档的相关性算分,控制排名。例如百度竞价
es在5.1及之后就弃用了 TF-IDF 算法,开始采用 BM25算法。BM25算法不会因为词的出现频率变大而导致算分无限增大,会逐渐趋近一个值
function score query :可以修改文档相关性算分,得到新的算分。
三要素
- 过滤条件:决定哪些条件要加分
- 算分函数:如何计算function score
- 加权方式:function score 与 query score如何运算
json
GET /hotel/_search
{
"query": {
"function_score": {
"query": {
"match": {
"all": "如家酒店"
}
},
"functions": [
{
"filter": {
"term": {
"city": "上海"
}
},
"weight": 10
}
],
"boost_mode": "sum"
}
}
}
boolean query:布尔查询是一个或多个子查询的组合。
- must:必须匹配每个子查询,类似"and"
- should:选择性匹配子查询,类似"or"
- must_not:必须不匹配,不参与算分,类似"非"
- filter:必须匹配,不参与算分
json
GET /hotel/_search
{
"query": {
"bool": {
"must": [
{
"match": {
"all": "上海"
}
}
],
"must_not": [
{
"range": {
"price": {
"gt": 500
}
}
}
],
"filter": [
{
"geo_distance": {
"distance": "10km",
"location": {
"lat": 31.21,
"lon": 121.5
}
}
}
]
}
}
}
搜索结构处理
排序
es支持对搜索结构进行排序,默认是根据相关度算分(_score)进行排序。可以排序的字段有keyword,数值、地理坐标、日期类型等。
json
GET /hotel/_search
{
"query": {
"match_all": {}
},"sort": [
{
"id": {
"order": "desc"
}
}
]
}
GET /hotel/_search
{
"query": {
"match_all": {}
},"sort": [
{
"_geo_distance": {
"location": {
"lat": 31.2,
"lon": 121.5
},
"order": "asc",
"unit": "km"
}
}
]
}
这个排序的结果就是相聚的公里数。
分页
针对深度分页;ES给出了两种方案
- search after:分页时需要排序,原理是从上次的排序值开始(末尾值),查询下一页的数据。官方推荐使用,不会太占内存。手机向下反动滚页。
- scroll:原理是将排序数据形成快照,保存在内存。不推荐
高亮
ES默认搜索字段和高亮字段必须一致,否则不会高亮。或者使用 "require_field_match": "false"
也能高亮。
最后将查询结果中 highlight 与 指定高亮的字段进行替换返回给前端就行。
RestClient
普通查询
java
@Test
public void testMatchAll() throws IOException {
SearchRequest searchRequest = new SearchRequest("hotel");
searchRequest.source().query(
QueryBuilders.matchAllQuery()
);
SearchResponse searchResponse = restHighLevelClient.search(searchRequest, RequestOptions.DEFAULT);
SearchHits searchHits = searchResponse.getHits();
long value = searchHits.getTotalHits().value;
System.out.println(value);
SearchHit[] hits = searchHits.getHits();
System.out.println(hits[0]);
HotelDoc hotelDoc = JSON.parseObject(hits[0].getSourceAsString(), HotelDoc.class);
System.out.println(hotelDoc);
}
QueryBuilders.matchAllQuery()
QueryBuilders.matchQuery("all","如家")
QueryBuilders.multiMatchQuery("如家","name","brand","business")
QueryBuilders.termQuery("city","上海")
QueryBuilders.rangeQuery("price").gt(100).lt(400)
BoolQueryBuilder boolQueryBuilder = QueryBuilders.boolQuery();
boolQueryBuilder.must(QueryBuilders.termQuery("city","北京"));
boolQueryBuilder.filter(QueryBuilders.rangeQuery("price").gt(100).lt(400));
分页和排序
java
public void testPageAndSort() throws IOException {
int pageNum = 2, pageSize = 10;
SearchRequest searchRequest = new SearchRequest("hotel");
BoolQueryBuilder boolQueryBuilder = QueryBuilders.boolQuery();
TermQueryBuilder termQueryBuilder = QueryBuilders.termQuery("brand", "如家");
MatchQueryBuilder matchQueryBuilder = QueryBuilders.matchQuery("all", "北京");
boolQueryBuilder.must(termQueryBuilder);
boolQueryBuilder.must(matchQueryBuilder);
searchRequest.source().query(boolQueryBuilder);
searchRequest.source().from((pageNum - 1) * pageSize).size(pageSize);
searchRequest.source().sort("price", SortOrder.ASC);
SearchResponse searchResponse = restHighLevelClient.search(searchRequest, RequestOptions.DEFAULT);
SearchHit[] hits = searchResponse.getHits().getHits();
for (SearchHit hit : hits) {
String source = hit.getSourceAsString();
HotelDoc hotelDoc = JSON.parseObject(source, HotelDoc.class);
System.out.println(hotelDoc);
}
}
高亮
java
public void testHighLight() throws IOException {
SearchRequest searchRequest = new SearchRequest("hotel");
searchRequest.source().query(QueryBuilders.matchQuery("all","如家"));
searchRequest.source().highlighter(new HighlightBuilder().field("name").requireFieldMatch(false));
SearchResponse searchResponse = restHighLevelClient.search(searchRequest, RequestOptions.DEFAULT);
SearchHit[] hits = searchResponse.getHits().getHits();
for (SearchHit hit : hits) {
String source = hit.getSourceAsString();
HotelDoc hotelDoc = JSON.parseObject(source, HotelDoc.class);
Map<String, HighlightField> highlightFields = hit.getHighlightFields();
if(!highlightFields.isEmpty()){
HighlightField highlightField = highlightFields.get("name");
//一般value只有一个元素,取数组第一个
String name = highlightField.getFragments()[0].string();
hotelDoc.setName(name);
}
System.out.println(hotelDoc);
}
}
让指定酒店置顶 (function_score )