Elasticsearch优势
横向可扩展性:只需要增加台服务器,做一点儿配置,启动一下Elasticsearch就可以并入集群。
分片机制提供更好的分布性:同一个索引分成多个分片(sharding), 这点类似于HDFS的块机制;分而治之的方式可提升处理效率。
高可用:提供复制( replica) 机制,一个分片可以设置多个复制,使得某台服务器在宕机的情况下,集群仍旧可以照常运行,并会把服务器宕机丢失的数据信息复制恢复到其他可用节点上。
口使用简单:共需一条命令就可以下载文件,然后很快就能搭建一一个站内搜索引擎。
Elasticsearch应用场景
大型分布式日志分析系统ELK elasticsearch(存储日志)+logstash(收集日志)+kibana(展示数据)
大型电商商品搜索系统、网盘搜索引擎等。
Elasticsearch存储结构
Elasticsearch是文件存储,Elasticsearch是面向文档型数据库,一条数据在这里就是一个文档,用JSON作为文档序列化的格式,比如下面这条用户数据:
{
"name" : "AncientJazz",
"age" : 18
}
关系数据库 ⇒ 数据库 ⇒ 表 ⇒ 行 ⇒ 列(Columns)
Elasticsearch ⇒ 索引(Index) ⇒ 类型(type) ⇒ 文档(Docments) ⇒ 字段(Fields)
一.Java环境变量
- 卸载Centos7自带的OpenJDK,安装JDK8
1.卸载OpenJDK的命令
rpm -e --nodeps java-1.8.0-openjdk-1.8.0.102-4.b14.el7.x86_64
rpm -e --nodeps java-1.8.0-openjdk-headless-1.8.0.102-4.b14.el7.x86_64
rpm -e --nodeps java-1.7.0-openjdk-1.7.0.111-2.6.7.8.el7.x86_64
rpm -e --nodeps java-1.7.0-openjdk-headless-1.7.0.111-2.6.7.8.el7.x86_64
2.在/usr/local/src路径下创Java文件夹
cd /usr/local/src
mkdir Java
3.进入/usr/local/src/Java上传jdk-8u51-linux-x64.tar.gz并解压
cd /usr/local/src/Java
tar -zxvf jdk-8u51-linux-x64.tar.gz
4.配置环境变量
进入profile文件编辑模式
vim /etc/profile
在profile最后添加如下内容
JAVA_HOME=/usr/local/src/Java/jdk1.8.0_51
JAVA_BIN=/usr/local/src/Java/jdk1.8.0_51/bin
PATH=$JAVA_HOME/bin:$PATH
CLASSPATH=.:$JAVA_HOME/lib/dt.jar:$JAVA_HOME/lib/tools.jar
export JAVA_HOME JAVA_BIN PATH CLASSPATH
运行此命令让环境变量生效
source /etc/profile
再次查询jdk版本是否生效
二.Elasticsearch安装
1.执行下载命令
wget https://artifacts.elastic.co/downloads/elasticsearch/elasticsearch-6.4.3.tar.gz
2.解压Elasticsearch
tar -zxf elasticsearch-6.4.3.tar.gz -C /usr/local/src/
3.进入Elasticsearch修改配置文件
cd /usr/local/src/elasticsearch-6.4.3/config
我们可以看到目录下jvm.options文件,那么他是做什么用的?
执行文本输出命令,查看内容
cat jvm.options
里面有配置jvm内存的设置,可以看到默认初始内存和启动内存是1g,比较占内存,我们可以改成512m(暂未测试)
修改elasticsearch配置文件
vim elasticsearch.yml
修改为虚拟机的实际ip地址
修改端口号:Elasticsearch有两个端口号是 9200和9300,那他们有什么区别?
9200:ES节点和外部通信的端口号‘
9300是TCP协议端口号,ES集群之间通讯端口号
9200是http协议端口号,暴露ES RESTful接口端口号
后续查看集群健康度:添加这行是因为elasticsearch服务与elasticsearch-head之间可能存在跨越,修改elasticsearch配置即可,在elastichsearch.yml中添加如下命名即可:
#allow origin
http.cors.enabled: true
http.cors.allow-origin: "*"
4.启动Elasticsearch
cd /usr/local/src/elasticsearch-6.4.3/bin
./elasticsearch
启动报错:root用户无法启动
- 解决方案:
因为安全问题Elasticsearch
不让用root用户直接运行,所以要创建新用户
第一步:liunx创建新用户 adduser XXX 然后给创建的用户加密码 passwd XXX 输入两次密码。
第二步:切换刚才创建的用户 su XXX 然后执行elasticsearch 会显示Permission denied 权限不足。
第三步:给新建的XXX赋权限,chmod 777 * 这个不行,因为这个用户本身就没有权限,肯定自己不能给自己付权限。所以要用root用户登录付权限。
第四步:root给XXX赋权限,chown -R XXX /你的elasticsearch安装目录。
然后执行成功。创建一个分组
groupadd esroot
useradd esadmin -g esroot -p root
chown -R esadmin:esroot /usr/local/src/elasticsearch-6.4.3/
su esadmin
5.再次启动Elasticsearch
./elasticsearch
启动报错:直接停止运行报错
- 解决方案:
切回root用户
su root
vi /etc/sysctl.conf
vm.max_map_count=655360
cd /etc
sysctl -p
6.修改完成后再次启动Elasticsearch
再切到esadmin用户,再次启动
su esadmin
./elasticsearch
启动报错:依旧启动直接停止报错
- 解决方案:记得用root用户
vi /etc/security/limits.conf
在最后添加如下内容
* soft nofile 65536
* hard nofile 131072
* soft nproc 2048
* hard nproc 4096
再次启动的话也会报错
7.需要重启虚拟机使用esadmin启动即可运行成功
cd /usr/local/src/elasticsearch-6.4.3/bin
su esadmin
./elasticsearch
启动成功,不再报错
8.外部访问Elasticsearch
记得使用root用户关闭
临时关闭防火墙
systemctl stop firewalld
永久防火墙开机自启动
systemctl disable firewalld
查看防火墙状态
systemctl status firewalld
http://虚拟机ip地址:9200
Kibana可视化界面
1.执行下载命令
wget https://artifacts.elastic.co/downloads/kibana/kibana-6.4.3-linux-x86_64.tar.gz
2.解压Kibana
tar -zxvf kibana-6.4.3-linux-x86_64.tar.gz -C /usr/local/src/
3.进入Kibana修改配置文件
cd /usr/local/src/kibana-6.4.3-linux-x86_64/config
1.开放端口号
2.设置虚拟机地址
3.设置Elasticsearch的协议、ip地址、端口号,如果是集群就写集群的地址
4.启动Kibana
进入bin目录
cd /usr/local/src/kibana-6.4.3-linux-x86_64/bin
执行启动命令
./kibana
在windows上访问: http://虚拟机ip地址:5601
5.Kibana实现增删改查
点击Dev Tools
- 创建索引:主要不能用大写否则会报错
PUT /ancientjazz
点击运行,右侧提示创建成功
- 查询索引
GET /ancientjazz
可以用查询到刚刚创建的索引
- 添加文档 /索引名称/类型/id
PUT /ancientjazz/user/1
{
"name":"gujinhang",
"sex":0,
"age":21
}
添加成功后可以看到有索引、类型、id、以及版本号,比如多次执行或者把name属性修改了再次执行可以发现他的版本号是会自增长
- 查询文档
GET /ancientjazz/user/1
查询到刚刚添加的数据是一致的
- 删除索引
DELETE /ancientjazz
删除成功
再次查询是会报错的
Elasticsearch版本控制
为什么要进行版本控制
为了保证数据再多线程操作下的准确性悲观锁和乐观锁
悲观锁:假设会发生并发冲突,屏蔽一切可能违反数据准确性的操作
乐观锁:假设不会发生并发冲突,只在提交操作是检查是否违反数据完整性。内部版本控制和外部版本控制
内部版本控制:_version自增长,修改数据后,_version会自动的加1
外部版本控制:为了保持_version与外部版本控制的数值一致
使用version_type=external检查数据当前的version值是否小于请求中的version值
PUT /ancientjazz/user/1?version=2
{
"name":"gujinhang",
"sex":0,
"age":21
}
ik分词器安装
因为Elasticsearch中默认的标准分词器分词器对中文分词不是很友好,会将中文词语拆分成一个一个中文的汉子。因此引入中文分词器-es-ik插件
下载地址: https://github.com/medcl/elasticsearch-analysis-ik/releases
注意: es-ik分词插件版本一定要和es安装的版本对应
先停掉Elasticsearch
将解压好的文件通过FileZilla上传到此路径 /usr/local/src/elasticsearch-6.4.3/plugins
上传成功,重启Elasticsearch即可
cd /usr/local/src/elasticsearch-6.4.3/bin
./elasticsearch
自定义扩展字典
先停掉Elasticsearch
/usr/local/src/elasticsearch-6.4.3/plugins/ik/config
mkdir AncientJazz
vi AncientJazz/word.dic
修改配置文件指定词典
vi IKAnalyzer.cfg.xml
启动Elasticsearch即可
测试配置结果
这个默认分词器不太友好,把每个中文字都分开了
下面这个是配置过后的效果
集群搭建
克隆三台之前安装好Elasticsearch的虚拟机:我自己用的Hyper-v,VM的自己操作克隆
1.首先将原来的名字改掉
2.导出虚拟机
3.选择导出位置
4.导出成功后进行导入操作
5.选中导出的文件目录——下一步
6.直接下一步
7.选中导入类型——复制虚拟机——下一步
8.设置虚拟机文件存放位置——下一步
9.设置虚拟机硬盘的存放位置——下一步
10.点击完成
11.克隆成功:将其改名为Elasticsearch-1
其他两台重复操作即可,把三台都开起来设置ip地址
虚拟机名称 | IP地址 | 服务器名称 |
---|---|---|
Elasticsearch-1 | 192.168.137.151 | node-1 |
Elasticsearch-2 | 192.168.137.152 | node-2 |
Elasticsearch-3 | 192.168.137.153 | node-3 |
集群的准备工作已经做完
服务集群配置
修改配置文件
cd /usr/local/src/elasticsearch-6.4.3/config/
vi elasticsearch.yml
cluster.name: es #保证三台服务器节点集群名称相同
node.name: node-1 #每个节点名称不一样 其他两台为node-2 ,node-3
network.host: XXX.XXX.XXX.XXX #实际虚拟机服务器ip地址
discovery.zen.ping.unicast.hosts: ["192.168.137.151", "192.168.137.152","192.168.137.153"] #多个服务集群ip
discovery.zen.minimum_master_nodes: 1
修改完成后启动三台虚拟机的服务
验证集群: http://虚拟机ip地址:9200/_cat/nodes?pretty
注意克隆data文件会导致数据不同步
报该错误解决办法
failed to send join request to master
因为克隆导致data文件也克隆呢,直接清除每台服务器data文件
cd /usr/local/src/elasticsearch-6.4.3/data
rm -rf nodes/
解决完以上错误再次启动
启动可能会比较慢,最好成功了再起下一台,不然可能出现脑裂现象
cd /usr/local/src/elasticsearch-6.4.3/bin
./elasticsearch -d #后台守护线程运行
启动成功
*号是主
Elasticsearch-head插件——监控集群健康度(自行参考网上教学安装)生病写不动了~~~~
检索-SpringBoot整合Jest操作ES
- 创建项目
- 选择模块
- pom.xml文件分析
SpringBoot默认支持两种技术来和ES交互;
1.Jest(默认不生效)
需要导入jest的工具包(io.searchbox.client.JestClient)
2.SpringData ElasticSearch(版本不适配会超时连接)
1)、Client节点信息clusterNodes;clusterName
2)、ElasticSearchTemplate操作es
3)、编写一个ElasticSearchRepository的子接口来操作ES
4.配置
集群以及连接超时时间的设置
版本适配说明:https://github.com/spring-projects/spring-data-elasticsearch
如果版本不适配:
1)、升级SpringBoot版本
2)、安装对应版本的ES
5.Jest测试
package com.ancientjazz.springbootes;
import com.ancientjazz.springbootes.bean.student;
import io.searchbox.client.JestClient;
import io.searchbox.core.Index;
import io.searchbox.core.Search;
import io.searchbox.core.SearchResult;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;
import java.io.IOException;
@RunWith(SpringRunner.class)
@SpringBootTest
public class SpringbootEsApplicationTests {
@Autowired
private JestClient jestClient;
//存储
@Test
public void contextLoads() {
student student = new student();
student.setId(2);
student.setName("张三");
student.setAge(18);
student.setSex("男");
Index index = new Index.Builder(student).index("shoocl").type("student").build();
try {
jestClient.execute(index);
} catch (IOException e) {
e.printStackTrace();
}
}
//获取
@Test
public void contextLcads1(){
String json="{\n" +
" \"query\": {\n" +
" \t\"match\":{\n" +
" \t\t\"name\":\"张三\"\n" +
" \t}\n" +
" }\n" +
"}";
Search build = new Search.Builder(json).addIndex("shoocl").addType("student").build();
try {
SearchResult execute = jestClient.execute(build);
System.out.println(execute.getJsonString());
} catch (IOException e) {
e.printStackTrace();
}
}
}
测试实体类
package com.ancientjazz.springbootes.bean;
import lombok.Data;
/**
* @author Ancient Jazz
* @create 2019-02-16 14:03
**/
@Data
public class student {
private Integer id;
private String name;
private Integer age;
private String sex;
}
5.1.SpringData ElasticSearch测试
测试类
package com.ancientjazz.springbootes;
import com.ancientjazz.springbootes.bean.Book;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;
@RunWith(SpringRunner.class)
@SpringBootTest
public class SpringbootEsApplicationTests {
@Autowired
private BookMapper bookMapper;
@Test
public void contextLcads2(){
Book book = new Book();
book.setId(1);
book.setName("西游记");
bookMapper.index(book);
}
@Test
public void contextLcads3(){
for (Book book : bookMapper.findByNameLike("西")) {
System.out.println(book);
}
}
}
接口继承ElasticsearchRepository
package com.ancientjazz.springbootes;
import com.ancientjazz.springbootes.bean.Book;
import org.springframework.data.elasticsearch.repository.ElasticsearchRepository;
import java.util.List;
/**
* @author Ancient Jazz
* @create 2019-02-16 15:31
**/
public interface BookMapper extends ElasticsearchRepository<Book,Integer>{
//自定义查询条件
List<Book> findByNameLike(String bookName);
}
测试实体类
package com.ancientjazz.springbootes.bean;
import lombok.Data;
import org.springframework.data.elasticsearch.annotations.Document;
/**
* @author Ancient Jazz
* @create 2019-02-16 15:28
**/
@Document(indexName = "ancientjazz",type = "book")
@Data
public class Book {
private Integer id;
private String name;
}
项目运用
搜索框from表单提交
<div class th:fragment="html" xmlns:th="http://www.w3.org/1999/xhtml">
<a th:href="${application.contextPath}">
<img id="logo" src="img/site/logo.gif" class="logo">
</a>
<form action="search" method="get" >
<div class="searchDiv">
<input name="keyword" type="text" placeholder="时尚男鞋 太阳镜 ">
<button type="submit" class="searchButton">搜索</button>
<div class="searchBelow">
<span th:each="c,status: ${application.categories_below_search}" th:if="${status.index>=5 && status.index<=8}">
<a th:href="@{'category?cid='+${c.id}}" th:text="${c.name}" ></a>
<span th:if="${status.index!=8}">|</span>
</span>
</div>
</div>
</form>
</div>
1. 通过getUrlParms 拿到keyword参数
2. 然后访问通过 axios.js 访问
3. 获取返回之后显示出来
<div class th:fragment="html" xmlns:th="http://www.w3.org/1999/xhtml">
<script>
$(function(){
var keyword = getUrlParms("keyword");
console.log(keyword)
var data4Vue = {
uri:'foresearch',
products:[]
};
//ViewModel
var vue = new Vue({
el: '#workingArea',
data: data4Vue,
mounted:function(){ //mounted 表示这个 Vue 对象加载成功了
this.load();
},
methods: {
load:function(){
var url = this.uri+"?keyword="+keyword;
axios.post(url).then(function(response) {
vue.products = response.data;
vue.$nextTick(function(){
linkDefaultActions();
})
});
}
}
});
})
</script>
<div id="searchResult">
<div class="searchResultDiv">
<div th:replace="include/fore/productsBySearch::html" ></div>
</div>
</div>
</div>
- 获取参数keyword
- 根据keyword进行模糊查询,获取满足条件的前20个产品
- 为这些产品设置销量和评价数量
- 返回这个产品集合
@PostMapping("foresearch")
public Object search( String keyword){
if(null==keyword) {
keyword = "";
}
List<Product> ps= productService.search(keyword,0,20);
productImageService.setFirstProdutImages(ps);
productService.setSaleAndReviewNumber(ps);
return ps;
}
最初是以模糊查询完成的商品搜索功能,ProductService 做了如下改动
package com.tmall.service;
import com.tmall.dao.ProductDAO;
import com.tmall.es.ProductESDAO;
import com.tmall.pojo.Category;
import com.tmall.pojo.Product;
import com.tmall.util.Page4Navigator;
import org.elasticsearch.index.query.QueryBuilders;
import org.elasticsearch.index.query.functionscore.FunctionScoreQueryBuilder;
import org.elasticsearch.index.query.functionscore.ScoreFunctionBuilders;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.data.elasticsearch.core.query.NativeSearchQueryBuilder;
import org.springframework.data.elasticsearch.core.query.SearchQuery;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.List;
@Service
public class ProductService {
@Autowired ProductDAO productDAO;
@Autowired CategoryService categoryService;
@Autowired ProductImageService productImageService;
@Autowired OrderItemService orderItemService;
@Autowired ReviewService reviewService;
@Autowired ProductESDAO productESDAO;
//1. 是增加,删除,修改的时候,除了通过 ProductDAO 对数据库产生影响之外,还要通过 ProductESDAO 同步到 es.
public void add(Product bean) {
productDAO.save(bean);
productESDAO.save(bean);
}
public void delete(int id) {
productDAO.delete(id);
productESDAO.delete(id);
}
public Product get(int id) {
return productDAO.findOne(id);
}
public void update(Product bean) {
productDAO.save(bean);
productESDAO.save(bean);
}
public Page4Navigator<Product> list(int cid, int start, int size,int navigatePages) {
Category category = categoryService.get(cid);
Sort sort = new Sort(Sort.Direction.DESC, "id");
Pageable pageable = new PageRequest(start, size, sort);
Page<Product> pageFromJPA =productDAO.findByCategory(category,pageable);
return new Page4Navigator<>(pageFromJPA,navigatePages);
}
public void fill(List<Category> categorys) {
for (Category category : categorys) {
fill(category);
}
}
public void fill(Category category) {
List<Product> products = listByCategory(category);
productImageService.setFirstProdutImages(products);
category.setProducts(products);
}
public void fillByRow(List<Category> categorys) {
int productNumberEachRow = 8;
for (Category category : categorys) {
List<Product> products = category.getProducts();
List<List<Product>> productsByRow = new ArrayList<>();
for (int i = 0; i < products.size(); i+=productNumberEachRow) {
int size = i+productNumberEachRow;
size= size>products.size()?products.size():size;
List<Product> productsOfEachRow =products.subList(i, size);
productsByRow.add(productsOfEachRow);
}
category.setProductsByRow(productsByRow);
}
}
public List<Product> listByCategory(Category category){
return productDAO.findByCategoryOrderById(category);
}
public void setSaleAndReviewNumber(Product product) {
int saleCount = orderItemService.getSaleCount(product);
product.setSaleCount(saleCount);
int reviewCount = reviewService.getCount(product);
product.setReviewCount(reviewCount);
}
public void setSaleAndReviewNumber(List<Product> products) {
for (Product product : products)
setSaleAndReviewNumber(product);
}
//3. 查询的修改。 以前查询是模糊查询,现在通过 ProductESDAO 到 elasticsearch 中进行查询了。
public List<Product> search(String keyword, int start, int size) {
initDatabase2ES();
FunctionScoreQueryBuilder functionScoreQueryBuilder = QueryBuilders.functionScoreQuery()
//查询条件,但是并未使用,放在这里,为的是将来使用,方便参考,知道如何用
.add(QueryBuilders.matchPhraseQuery("name", keyword),
ScoreFunctionBuilders.weightFactorFunction(100))
//设置权重分求和模式
.scoreMode("sum")
//设置权重分最低分
.setMinScore(10);
//设置分页
Sort sort = new Sort(Sort.Direction.DESC,"id");
Pageable pageable = new PageRequest(start, size,sort);
SearchQuery searchQuery = new NativeSearchQueryBuilder()
.withPageable(pageable)
.withQuery(functionScoreQueryBuilder).build();
Page<Product> page = productESDAO.search(searchQuery);
return page.getContent();
}
//2. 初始化数据到es. 因为数据刚开始都在数据库中,不在es中,所以刚开始查询,先看看es有没有数据,如果没有,就把数据从数据库同步到es中。
public void initDatabase2ES(){
PageRequest pageable = new PageRequest(0, 5);
Page<Product> page = productESDAO.findAll(pageable);
if(page.getContent().isEmpty()){
Iterable<Product> products = productESDAO.findAll();
for (Product product : products) {
productESDAO.save(product);
}
}
}
}
继承ElasticsearchRepository接口
package com.tmall.es;
import com.tmall.pojo.Product;
import org.springframework.data.elasticsearch.repository.ElasticsearchRepository;
/**
* @author Ancient Jazz
* @create 2019-02-17 15:40
**/
public interface ProductESDAO extends ElasticsearchRepository<Product,Integer>{
}
在 Application 中,为es和jpa分别指定不同的包名,否则会出错
package com.tmall;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.data.elasticsearch.repository.config.EnableElasticsearchRepositories;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
@SpringBootApplication
@EnableCaching
@EnableElasticsearchRepositories(basePackages = "com.tmall.es")
@EnableJpaRepositories(basePackages = {"com.tmall.dao","com.tmall.pojo"})
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
application.properties配置
spring.data.elasticsearch.cluster-nodes=127.0.0.1:9300