Elasticsearch入门

65 阅读8分钟

Elasticsearch

介绍

认识和Docker启动

Lucene是一个Java语言的搜索引擎类库,是Apache公司的顶级项目,由DougCutting于1999年研发

Lucene的优势:

  • 易扩展
  • 高性能(基于倒排索引)

2004年Shay Banon基于Lucene开发了Compass

2010年Shay Banon重写了Compass取名为Elasticsearch

官网地址:www.elastic.co/cn/目前最新版本是8…

elasticsearch具备下列优势:

  • 支持分布式,可水平扩展
  • 提供Restful接口,可被任何语言使用

elasticsearch结合kibana、Logstash、Beats,是一整套技术栈,被叫做ELK。被广泛应用在日志数据分析,实时监控等领域

docker 命令如下:

docker run -d \
--name es \
-e "ES_JAVA_OPTS=-Xms1024m -Xmx1024m" \
-e "discovery.type=single-node" \
-v es-data:/usr/share/elasticsearch/data \
-v es-plugins:/usr/share/elasticsearch/plugins \
--privileged \
--network hmall \
-p 9200:9200 \
-p 9300:9300 \
elasticsearch:7.12.1

docker安装kibana

docker run -d \
--name kibana \
-e ELASTICSEARCH_HOSTS=http://es:9200 \
--network=hmall \
-p 5601:5601 \
kibana:7.12.1

倒排索引

传统数据库(如MySQL)采用正向索引,例如给下表中的id创建索引:

idtitleprice
1小米手机3499
2华为手机4999
3华为小米充电器49
4小米手环49

在对上述表结构执行下列语句时,会逐行搜索,然后查看是否匹配,匹配存入结果集,不匹配直接丢弃,需要将整表都搜索完

select * from table1 where title like '%手机%'

elasticsearch采用倒排索引:

  • 文档(document):每条数据就是一个文档
  • 词条(term):文档按照语义分成的词语
词条(term)文档id
小米1,3,4
手机1,2
华为2,3
充电器3
手环4

搜索“华为手机”-进行分词-得到“华为”、“手机”两个词条,去词条列表中查询文档id,得到每个词条所在文档id

IK分词器

中文分词往往需要根据语义分析,比较复杂,这就需要用到中文分词器,例如IK分词器。IK分词器是林良益在2006年开源发布的,其采用的正向迭代最细粒度切分算法一直沿用至今。

其安装的方式也比较简单,只需要将分词器插件放入elasticsearch的插件目录即可

在Kibana的DevTools中可以使用下面的语法来测试IK分词器:

POST /_analyze
{
  "analyzer": "ik_smart",
  "text": "你好我叫Bbober"
}
{
  "tokens" : [
    {
      "token" : "你好",
      "start_offset" : 0,
      "end_offset" : 2,
      "type" : "CN_WORD",
      "position" : 0
    },
    {
      "token" : "我",
      "start_offset" : 2,
      "end_offset" : 3,
      "type" : "CN_CHAR",
      "position" : 1
    },
    {
      "token" : "叫",
      "start_offset" : 3,
      "end_offset" : 4,
      "type" : "CN_CHAR",
      "position" : 2
    },
    {
      "token" : "bbober",
      "start_offset" : 4,
      "end_offset" : 10,
      "type" : "ENGLISH",
      "position" : 3
    },
    {
      "token" : "见",
      "start_offset" : 11,
      "end_offset" : 12,
      "type" : "CN_CHAR",
      "position" : 4
    },
    {
      "token" : "到你",
      "start_offset" : 12,
      "end_offset" : 14,
      "type" : "CN_WORD",
      "position" : 5
    },
    {
      "token" : "真的",
      "start_offset" : 14,
      "end_offset" : 16,
      "type" : "CN_WORD",
      "position" : 6
    },
    {
      "token" : "太棒了",
      "start_offset" : 16,
      "end_offset" : 19,
      "type" : "CN_WORD",
      "position" : 7
    }
  ]
}
POST /_analyze
{
  "analyzer": "ik_max_word",
  "text": "你好我叫Bbober,见到你真的太棒了"
}
{
  "tokens" : [
    {
      "token" : "你好",
      "start_offset" : 0,
      "end_offset" : 2,
      "type" : "CN_WORD",
      "position" : 0
    },
    {
      "token" : "我",
      "start_offset" : 2,
      "end_offset" : 3,
      "type" : "CN_CHAR",
      "position" : 1
    },
    {
      "token" : "叫",
      "start_offset" : 3,
      "end_offset" : 4,
      "type" : "CN_CHAR",
      "position" : 2
    },
    {
      "token" : "bbober",
      "start_offset" : 4,
      "end_offset" : 10,
      "type" : "ENGLISH",
      "position" : 3
    },
    {
      "token" : "见到",
      "start_offset" : 11,
      "end_offset" : 13,
      "type" : "CN_WORD",
      "position" : 4
    },
    {
      "token" : "到你",
      "start_offset" : 12,
      "end_offset" : 14,
      "type" : "CN_WORD",
      "position" : 5
    },
    {
      "token" : "真的",
      "start_offset" : 14,
      "end_offset" : 16,
      "type" : "CN_WORD",
      "position" : 6
    },
    {
      "token" : "太棒了",
      "start_offset" : 16,
      "end_offset" : 19,
      "type" : "CN_WORD",
      "position" : 7
    },
    {
      "token" : "太棒",
      "start_offset" : 16,
      "end_offset" : 18,
      "type" : "CN_WORD",
      "position" : 8
    },
    {
      "token" : "了",
      "start_offset" : 18,
      "end_offset" : 19,
      "type" : "CN_CHAR",
      "position" : 9
    }
  ]
}

语法说明:

  • POST:请求方式
  • /_analyze:请求路径,这里省略了ip,kibana会帮助补充
  • 请求参数,json风格:
    • analyzer:分词器类型
    • text要分词的内容

ik分词器允许配置扩展词典来增加自定义的词库

ik-config-IKAnalyzer.cfg.xml

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE properties SYSTEM "http://java.sun.com/dtd/properties.dtd">
<properties>
	<comment>IK Analyzer 扩展配置</comment>
	<!--用户可以在这里配置自己的扩展字典 -->
	<entry key="ext_dict">my.dic</entry>
	 <!--用户可以在这里配置自己的扩展停止词字典-->
	<entry key="ext_stopwords">my.dic</entry>
	<!--用户可以在这里配置远程扩展字典 -->
	<!-- <entry key="remote_ext_dict">words_location</entry> -->
	<!--用户可以在这里配置远程扩展停止词字典-->
	<!-- <entry key="remote_ext_stopwords">words_location</entry> -->
</properties>

基础概念

elasticsearch中的文档数据会被序列化为json格式后存储在elasticsearch中。

传统数据库中的表会在elasticsearch抽象为索引(index)

索引:相同类型的文档的集合

映射(mapping):索引中文档的字段约束信息,类似于表的结构约束

MySQLElasticsearch说明
TableIndex索引(index),就是文档的集合,类似数据库的表(table)
RowDocument文档(Document),就是一条条的数据,类似于数据库中的行(Row),文档都是JSON格式
ColumnField字段(Field),就是JSON文档中的字段,类似数据库中的列(Column)
SchemaMappingMapping(映射)是索引中文档的约束,例如字段类型的约束。类似于数据库中的表结构(Schema)
SQLDSLDSL是elasticsearch提供的JSON风格的请求语句,用来定义搜索条件

索引库操作

Mapping映射属性

mapping是对索引库中文档的约束,常见的mapping属性包括:

  • type:字段数据类型,常见的简单类型有:
    • 字符串:text(可分词的文本)、keyword(精确值,例如:品牌、国家、ip地址)
    • 数值:long、integer、short、byte、double、float
    • 布尔:boolean
    • 日期:date
    • 对象:object
  • index:是否创建索引,默认为true
  • analyzer:使用哪种分词器
  • properties:该字段的子字段

索引库操作

Elasticsearch提供的所有API都是Restful的接口,遵循Restful的基本规范:

接口类型请求方式请求路径请求参数
查询用户GET/users/{id}路径中的id
新增用户POST/usersjson格式user对象
修改用户PUT/users/{id}路径中的id、json格式对象
删除用户DELETE/users/{id}路径中的id

创建索引库和mapping的请求语法如下:

PUT /索引库名称
{
    "mappings": {
        "properties": {
            "字段名": {
                "type":"text",
                "analyzer":"ik_smart"
            },
            "字段名2": {
                "type":"keyword",
                "index":"false"
            },
            "字段名3": {
                "properties":{
                    "子字段":{
                        "type":"keyword",
                    }
                }
            },
            //...略
        }
    }
}

查看索引库语法:

GET /索引库名

删除索引库语法:

DELETE /索引库名

索引库和mapping一旦创建无法修改,但是可以添加新的字段,语法如下:

PUT /索引库名/_mapping
{
    "properties":{
        "新字段名":{
            "type":"integer"
        }
    }
}

文档操作

文档CRUD

新增文档的请求格式如下:

POST /索引库名/_doc/文档id
{
    "字段1":"值1",
    "字段2":"值2",
    "字段3":"值3",
    "字段4":{
        "子属性1":"值4",
        "子属性2":"值5"
    }
}

查询文档

GET /索引库名/_doc/1

删除文档

DELETE /索引库名/_doc/1

修改文档

方式一:全量修改,会删除旧文档,添加新文档

PUT /索引库名/_doc/文档id
{
    "字段1":"值1",
    "字段2":"值2"
}

方式二:增量修改,修改指定的字段名

POST /索引库名/_update/文档id
{
    "doc":{
        "字段名":"新的值"
    }
}

批量处理

Elasticsearch中允许通过一次请求中携带多次文档操作,也就是批量处理,语法格式如下:

POST /_bulk
{"index":{"_index":"索引库名","_id":"1"}}
{"字段1":"值1","字段2":"值2"}
{"index":{"_index":"索引库名","_id":"1"}}
{"字段1":"值1","字段2":"值2"}
{"index":{"_index":"索引库名","_id":"1"}}
{"字段1":"值1","字段2":"值2"}
{"delete":{"_index":"test","_id":"2"}}
{"update":{"_index":"索引库名","_id":"1"}}
{"doc":{"字段名":"新的值"}}

JavaRestClient

Elasticsearch目前最新版本是8.0,其Java客户端有很大变化。不过大多数还是使用8以下版本,故在此讨论早期的JavaRestClient客户端。

客户端初始化

  1. 引入es的RestHighLevelClient依赖:

    <!--es依赖-->
    <dependency>
        <groupId>org.elasticsearch.client</groupId>
        <artifactId>elasticsearch-rest-high-level-client</artifactId>
    </dependency>
    
  2. 因为SpringBoot默认的ES版本是7.17.0,需要覆盖默认的ES版本:

    <properties>
    	<elasticsearch.version>7.12.1</elasticsearch.version>
    </properties>
    
  3. 初始化RestHighLevelClient:

    void setUp(){
    	client = new RestHighLevelClient(RestClient.builder(
    		HttpHost.create("http://xxx.xxx.xxx.xxx:9200")
    	));
    }
    

数据表Mapping映射

数据表例如商品表

我们要实现商品搜素,那么索引的字段肯定要满足页面搜索的需求:

字段名是否需要是否搜索
id(主键)11
name(商品名)11
price(价格)11
category(类目名称)11
brand(品牌名称)11
image(图片)10
comment_count(评论)10
sold(销量)11
isAD(推广广告)11

索引库操作

新增:

void testCreat() throws IOException {
	//1.创建Request对象
	CreateIndexRequest request = new CreateIndexRequest("indexName");
	//2.请求参数,MAPPING_TEMPLATE是静态常量字符串,内容是JSON格式请求体
	request.source(MAPPING_TEMPLATE, XContentType.JSON);
	//3.发起请求
	client.indices().create(request, RequestOptions.DEFAULT);
}

删除:

void testDelete() throws IOException {
	//1.创建Request对象
	DeleteIndexRequest request = new DeleteIndexRequest("indexName");
	//2.发起请求
	client.indices().delete(request,RequestOptions.DEFAULT);
}

查询:

void testSelect() throws IOException {
	//1.创建Request对象
	GetIndexRequest request = new GetIndexRequest("indexName");
	//2.发起请求
	boolean is = client.indices().exists(request,RequestOptions.DEFAULT);
	System.out.println(is);
}

文档处理

新增文档的JavaAPI如下:

void testAddDocument() throws IOException {
	//1.创建request对象
	IndexRequest request = new IndexRequest("indexName").id("1");
	//2.准备JSON文档
	request.source("{\n" +
		"    \"name\":\"Jack\",\n" +
		"    \"age\":21\n" +
		"}",XContentType.JSON);
	//3.发送请求
	client.index(request,RequestOptions.DEFAULT);
}

删除文档的JavaAPI如下:

void testDeleteDocument() throws IOException {
	//1.创建request对象
	DeleteRequest request = new DeleteRequest("indexName").id("1");
	//2.发送请求
	DeleteResponse response = client.delete(request, RequestOptions.DEFAULT);
}

查询文档的JavaAPI如下:

void testSelectDocument() throws IOException {
	//1.创建request对象
	GetRequest request = new GetRequest("indexName").id("1");
	//2.发送请求得到结果
	GetResponse response = client.get(request, RequestOptions.DEFAULT);
	//3.解析结果
	String json = response.getSourceAsString();
}

修改文档数据两种方式:

方式一:全量新增。再次写入id一样的文档,就会删除旧文档,添加新文档。与新增的JavaAPI一致。

方式二:局部新增。只更新指定部分字段。

void testUpdateDocument() throws IOException {
	//1.创建request对象
	UpdateRequest request = new UpdateRequest("indexName","1");
	//2.准备参数
	request.doc("age",18,"name","Rose");
	//3.更新文档
	client.update(request,RequestOptions.DEFAULT);
}

批处理

批处理代码流程与之前类似,只不过构建请求会用到一个名为BulkRequest来封装普通的CRUD请求

void testBulkDoc() throws IOException {
   //1.创建request对象
   BulkRequest request = new BulkRequest();
   request.add(new IndexRequest("indexName").id("1").source("json",XContentType.JSON));
   request.add(new IndexRequest("indexName").id("2").source("json",XContentType.JSON));
   request.add(new IndexRequest("indexName").id("3").source("json",XContentType.JSON));
   request.add(new DeleteRequest("indexName").id("1"));
   request.add(new DeleteRequest("indexName").id("2"));
   request.add(new DeleteRequest("indexName").id("3"));
   //2.发送请求
   client.bulk(request,RequestOptions.DEFAULT);
}