elasticsearch是一款非常强大的开源搜索引擎,具备非常多强大功能,可以帮助我们从海量数据中快速找到需要的内容
elasticsearch结合kibana、Logstash、Beats,也就是elastic stack(ELK)。被广泛应用在日志数据分析、实时监控等领域:
什么是elasticsearch?
什么是elastic stack(ELK)?
什么是Lucene?
倒排索引中有两个非常重要的概念:
Document
):用来搜索的数据,其中的每一条数据就是一个文档。例如一个网页、一个商品信息Term
):对文档数据或用户搜索数据,利用某种算法分词,得到的具备含义的词语就是词条。例如:我是中国人,就可以分为:我、是、中国人、中国、国人这样的几个词条那么为什么一个叫做正向索引,一个叫做倒排索引呢?
正向索引是最传统的,根据id索引的方式。但根据词条查询时,必须先逐条获取每个文档,然后判断文档中是否包含所需要的词条,是根据文档找词条的过程。
而倒排索引则相反,是先找到用户要搜索的词条,根据词条得到保护词条的文档的id,然后根据id获取文档。是根据词条找文档的过程。
elasticsearch是面向文档(Document)存储的,可以是数据库中的一条商品数据,一个订单信息。文档数据会被序列化为json格式后存储在elasticsearch中.
而Json文档中往往包含很多的字段(Field),类似于数据库中的列。
索引(Index),就是相同类型的文档的集合。
例如:
数据库的表会有约束信息,用来定义表的结构、字段的名称、类型等信息。因此,索引库中就有映射(mapping),是索引中文档的字段约束信息,类似表的结构约束。
MySQL | Elasticsearch | 说明 |
---|---|---|
Table | Index | 索引(index),就是文档的集合,类似数据库的表(table) |
Row | Document | 文档(Document),就是一条条的数据,类似数据库中的行(Row),文档都是JSON格式 |
Column | Field | 字段(Field),就是JSON文档中的字段,类似数据库中的列(Column) |
Schema | Mapping | Mapping(映射)是索引中文档的约束,例如字段类型约束。类似数据库的表结构(Schema) |
SQL | DSL | DSL是elasticsearch提供的JSON风格的请求语句,用来操作elasticsearch,实现CRUD |
Mysql:擅长事务类型操作,可以确保数据的安全和一致性
Elasticsearch:擅长海量数据的搜索、分析、计算
对安全性要求较高的写操作,使用mysql实现
对查询性能要求较高的搜索需求,使用elasticsearch实现
两者再基于某种方式,实现数据的同步,保证一致性
put /创建的索引库名称
{
"settings": {
"属性名": "属性值"
}
}
请求方式:GET
请求路径:/索引库名
请求参数:无
GET /索引库名
请求方式:DELETE
请求路径:/索引库名
请求参数:无
DELETE /索引库名
mapping是对索引库中文档的约束,常见的mapping属性包括:
PUT /索引库名称
{
"mappings": {
"properties": {
"字段名":{
"type": "text",
"analyzer": "ik_smart"
},
"字段名2":{
"type": "keyword",
"index": "false"
},
"字段名3":{
"properties": {
"子字段": {
"type": "keyword"
}
}
},
// ...略
}
}
}
PUT /索引库名/_mapping
{
"properties": {
"字段名1": {
"type": "类型",
"index": true,
"analyzer": "分词器"
},
"字段名2": {
"type": "类型",
"index": true,
"analyzer": "分词器"
},
...
}
}
GET /索引库名/_mapping
修改:
1.无法修改映射中已有字段进行修改
2.可以向已有的映射中添加映射
#修改原有属性price类型改为text
PUT /索引库名称/_mapping
{
"properties":{
"字段名": {
"type": "text"
}
}
}
#向已有的索引库映射添加属性
PUT /索引库名称/_mapping
{
"properties":{
"from": {
"type": "text"
}
}
}
POST /{索引库名}/_doc
{
"key":"value"
}
POST /{索引库名}/_doc/{id}
{
"key":"value"
}
GET /{索引库名称}/_doc/{id}
DELETE /{索引库名}/_doc/id值
全量修改
PUT /{索引库名}/_doc/文档id
{
"字段1": "值1",
"字段2": "值2",
// ... 略
}
增量修改
增量修改是只修改指定id匹配的文档中的部分字段。
POST /{索引库名}/_update/文档id
{
"doc": {
"字段名": "新的值",
}
}
在elasticsearch提供的API中,与elasticsearch一切交互都封装在一个名为RestHighLevelClient的类中,必须先完成这个对象的初始化,建立与elasticsearch的连接。
<dependency>
<groupId>org.elasticsearch.clientgroupId>
<artifactId>elasticsearch-rest-high-level-clientartifactId>
dependency>
RestHighLevelClient client = new RestHighLevelClient(RestClient.builder(
HttpHost.create("http://IP地址:9200")
));
创建索引库
@Test
void createHotelIndex() throws IOException {
// 1.创建Request对象
CreateIndexRequest request = new CreateIndexRequest("hotel");
// 2.准备请求的参数:DSL语句
request.source(MAPPING_TEMPLATE, XContentType.JSON); // MAPPING_TEMPLATE这个为准备好的JSON字符串
// 3.发送请求
client.indices().create(request, RequestOptions.DEFAULT);
}
JavaRestClient操作elasticsearch的流程基本类似。核心是client.indices()方法来获取索引库的操作对象。
索引库操作的基本步骤:
DSL语句
POST /{索引库名}/_doc/1
{
"name": "Jack",
"age": 21
}
Java代码
// 1.从数据库查询数据
Hotel hotel = hotelService.getById(61083L);
// 2.转换为文档类型
HotelDoc hotelDoc = new HotelDoc(hotel);
// 3.将HotelDoc转json
String json = JSON.toJSONString(hotelDoc);
// 1.准备Request对象
IndexRequest request = new IndexRequest("hotel").id(hotelDoc.getId().toString());
// 2.准备Json文档
request.source(json, XContentType.JSON);
// 3.发送请求
client.index(request, RequestOptions.DEFAULT);
DSL语句
GET /hotel/_doc/{id}
Java代码
@Test
void testGetDocumentById() throws IOException {
// 1.准备Request
GetRequest request = new GetRequest("hotel", "61082");
// 2.发送请求,得到响应
GetResponse response = client.get(request, RequestOptions.DEFAULT);
// 3.解析响应结果
String json = response.getSourceAsString();
HotelDoc hotelDoc = JSON.parseObject(json, HotelDoc.class);
System.out.println(hotelDoc);
}
文档操作的基本步骤:
查询所有:查询出所有数据,一般测试用。例如:match_all
全文检索(full text)查询:利用分词器对用户输入内容分词,然后去倒排索引库中匹配。例如:
精确查询:根据精确词条值查找数据,一般是查找keyword、数值、日期、boolean等类型字段。例如:
地理(geo)查询:根据经纬度查询。例如:
复合(compound)查询:复合查询可以将上述各种查询条件组合起来,合并查询条件。例如:
GET /indexName/_search
{
"query": {
"查询类型": {
"查询条件": "条件值"
}
}
}
// 查询所有
GET /indexName/_search
{
"query": {
"match_all": {
}
}
}
match和multi_match的区别是什么?
精确查询一般是查找keyword、数值、日期、boolean等类型字段。所以不会对搜索条件分词。常见的有:
// term查询
GET /indexName/_search
{
"query": {
"term": {
"FIELD": {
"value": "VALUE"
}
}
}
}
// range查询
GET /indexName/_search
{
"query": {
"range": {
"FIELD": {
"gte": 10, // 这里的gte代表大于等于,gt则代表大于
"lte": 20 // lte代表小于等于,lt则代表小于
}
}
}
}
矩形范围查询,也就是geo_bounding_box查询,查询坐标落在某个矩形范围的所有文档:
// geo_bounding_box查询
GET /indexName/_search
{
"query": {
"geo_bounding_box": {
"FIELD": {
"top_left": { // 左上点
"lat": 31.1,
"lon": 121.5
},
"bottom_right": { // 右下点
"lat": 30.9,
"lon": 121.7
}
}
}
}
}
附近查询,也叫做距离查询(geo_distance):查询到指定中心点小于某个距离值的所有文档。
代码示例
// geo_distance 查询
GET /indexName/_search
{
"query": {
"geo_distance": {
"distance": "15km", // 半径
"FIELD": "31.21,121.5" // 圆心
}
}
}
复合(compound)查询:复合查询可以将其它简单查询组合起来,实现更复杂的搜索逻辑。常见的有两种:
function score 查询中包含四部分内容:
function score的运行流程如下:
GET /hotel/_search
{
"query": {
"function_score": {
"query": { .... }, // 原始查询,可以是任意条件
"functions": [ // 算分函数
{
"filter": { // 满足的条件,品牌必须是如家
"term": {
"brand": "如家"
}
},
"weight": 2 // 算分权重为2
}
],
"boost_mode": "sum" // 加权模式,求和
}
}
}
function score query定义的三要素是什么?
布尔查询是一个或多个查询子句的组合,每一个子句就是一个子查询。子查询的组合方式有:
GET /hotel/_search
{
"query": {
"bool": {
"must": [
{"term": {"city": "上海" }}
],
"should": [
{"term": {"brand": "皇冠假日" }},
{"term": {"brand": "华美达" }}
],
"must_not": [
{ "range": { "price": { "lte": 500 } }}
],
"filter": [
{ "range": {"score": { "gte": 45 } }}
]
}
}
}
bool查询有几种逻辑关系?
普通字段语法示例
GET /indexName/_search
{
"query": {
"match_all": {}
},
"sort": [
{
"FIELD": "desc" // 排序字段、排序方式ASC、DESC
}
]
}
地理坐标排序示例
GET /indexName/_search
{
"query": {
"match_all": {}
},
"sort": [
{
"_geo_distance" : {
"FIELD" : "纬度,经度", // 文档中geo_point类型的字段名、目标坐标点
"order" : "asc", // 排序方式
"unit" : "km" // 排序的距离单位
}
}
]
}
这个查询的含义是:
基本分页代码示例
GET /hotel/_search
{
"query": {
"match_all": {}
},
"from": 0, // 分页开始的位置,默认为0
"size": 10, // 期望获取的文档总数
"sort": [
{"price": "asc"}
]
}
深度分页代码示例及问题
GET /hotel/_search
{
"query": {
"match_all": {}
},
"size": 200,
"sort": [
{
"id": {
"order": "desc"
}
}
],
"search_after": [
"657192"
]
}
当查询分页深度较大时,汇总数据过多,对内存和CPU会产生非常大的压力,因此elasticsearch会禁止from+ size 超过10000的请求。
上面的请求会为每一个文档返回一个包含sort排序值的数组。这些sort排序值可以被用于 search_after 参数里以便抓取下一页的数据。
from + size
:
search after
:
GET /hotel/_search
{
"query": {
"match": {
"FIELD": "TEXT" // 查询条件,高亮一定要使用全文检索查询
}
},
"highlight": {
"fields": { // 指定要高亮的字段
"FIELD": {
"pre_tags": "", // 用来标记高亮字段的前置标签
"post_tags": "" // 用来标记高亮字段的后置标签
}
}
}
}
查询的DSL是一个大的JSON对象,包含下列属性:
查询的基本步骤是:
创建SearchRequest对象
准备Request.source(),也就是DSL。
① QueryBuilders来构建查询条件
② 传入Request.source() 的 query() 方法
发送请求,得到结果
解析结果(参考JSON结果,从外到内,逐层解析)
@Test
void testMatch() throws IOException {
// 1.准备Request
SearchRequest request = new SearchRequest("索引库");
// 2.准备DSL
request.source()
.query(QueryBuilders.matchQuery("字段名", "值"));
// 3.发送请求
SearchResponse response = client.search(request, RequestOptions.DEFAULT);
// 4.解析响应
handleResponse(response);
}
**桶(Bucket)**聚合:用来对文档做分组
**度量(Metric)**聚合:用以计算一些值,比如:最大值、最小值、平均值等
**管道(pipeline)**聚合:其它聚合的结果为基础做聚合
GET /hotel/_search
{
"size": 0, // 设置size为0,结果中不包含文档,只包含聚合结果
"aggs": { // 定义聚合
"brandAgg": { //给聚合起个名字
"terms": { // 聚合的类型,按照品牌值聚合,所以选择term
"field": "brand", // 参与聚合的字段
"size": 20 // 希望获取的聚合结果数量
}
}
}
}
GET /hotel/_search
{
"size": 0,
"aggs": {
"brandAgg": {
"terms": {
"field": "brand",
"order": {
"_count": "asc" // 按照_count升序排列
},
"size": 20
}
}
}
}
GET /hotel/_search
{
"query": {
"range": {
"price": {
"lte": 200 // 只对200元以下的文档聚合
}
}
},
"size": 0,
"aggs": {
"brandAgg": {
"terms": {
"field": "brand",
"size": 20
}
}
}
}
GET /hotel/_search
{
"size": 0,
"aggs": {
"brandAgg": {
"terms": {
"field": "brand",
"size": 20
},
"aggs": { // 是brands聚合的子聚合,也就是分组后对每组分别计算
"score_stats": { // 聚合名称
"stats": { // 聚合类型,这里stats可以计算min、max、avg等
"field": "score" // 聚合字段,这里是score
}
}
}
}
}
}
aggs代表聚合,与query同级,此时query的作用是?
聚合必须的三要素:
聚合可配置属性有:
要实现根据字母做补全,就必须对文档按照拼音分词。在GitHub上恰好有elasticsearch的拼音分词插件。地址:https://github.com/medcl/elasticsearch-analysis-pinyin
POST /_analyze
{
"text": "关键字",
"analyzer": "pinyin"
}
# 创建索引库
PUT test
{
"mappings": {
"properties": {
"title":{
"type": "completion"
}
}
}
}
# 查询DSL
GET /test/_search
{
"suggest": {
"title_suggest": {
"text": "n", // 关键字
"completion": {
"field": "title", // 补全查询的字段
"skip_duplicates": true, // 跳过重复的
"size": 10 // 获取前10条结果
}
}
}
}
@Test
public void test01() throws IOException {
//1.创建请求语义对象
SearchRequest request = new SearchRequest("test");
//2.自动补全
request.source().suggest(
new SuggestBuilder().addSuggestion(
"title_suggest",
SuggestBuilders.completionSuggestion("suggestion")
.prefix("s").skipDuplicates(true).size(10))
);
//3.发送请求
SearchResponse response = client.search(request, RequestOptions.DEFAULT);
//4.解析结果
Suggest suggest = response.getSuggest();
CompletionSuggestion suggestion = suggest.getSuggestion("title_suggest");
// 遍历自动补全结果
for (CompletionSuggestion.Entry.Option option : suggestion.getOptions()) {
System.out.println(option.getText().string());
}
}