JunSearch开发笔记
前端源码地址:https://github.com/z-h-u-a-i/junso-frontend
后端源码地址:https://github.com/z-h-u-a-i/junso-backend
前端开发
-
前端实现页面防抖、用url记录页面搜索状态,当用户刷新页面时,能够从url还原之前的搜索状态
-
核心技巧:用url来控制页面状态,当用户每点击一个选项或者搜索框中输入内容就把他体现在url上,然后再通过url的参数来控制页面状态
-
步骤:
-
1、配置路由
-
导包:npm i vue-router
-
创建router文件夹并编写index.js
import { createRouter, createWebHashHistory} from "vue-router"; import IndexPage from "../components/IndexPage.vue"; import LoginPage from "../components/LoginPage" const routes = [ { path: "/", component: IndexPage, }, { path: "/login", component: LoginPage, }, ]; const router = createRouter({ history: createWebHashHistory(), routes, }); export default router;
-
在main.js中应用这个router(会自动找index.js)
import router from "@/router"; createApp(App).use(Antd).use(router).mount("#app");
-
-
2、设为动态路径
{ path: "/:category", component: IndexPage, },
-
3、当搜索时在url的query参数上加上搜索条件
const onSearch = () => { router.push({ query: searchQuery.value, }); };
-
4、当url中的text发生变化后(即用户点击搜索时)回写到输入框中,这样当用户刷新的时候,这个值就不会丢失
watchEffect(() => { searchQuery.value = { ...initSearchQuery, text: route.query.text as string, }; });
-
5、当切换选项时修改url的路径为当前选项的key值
const onTabChange = (key: string) => { router.push({ path: `/${key}`, query: searchQuery.value, }); };
-
6、每次获取activeKey都从url路径中获取
let activeKey = computed(() => { return route.params.category; });
-
-
-
前端整合Axios
-
参考文档:https://www.axios-http.cn/
import axios from "axios"; const instance = axios.create({ //访问前端项目的端口号,会自动代理到8102 baseURL: "http://localhost:8080/api", timeout: 10000, }); // 添加响应拦截器 instance.interceptors.response.use( function (response) { // 2xx 范围内的状态码都会触发该函数。 const data = response.data; if (data.code === 0) { return data.data; } console.log("request error"); return data; }, function (error) { // 超出 2xx 范围的状态码都会触发该函数。 return error.message; } ); export default instance;
-
小坑:如果Controller方法中不是从body中获取一个封装所有需要属性的请求对象,请求头要设置成application/x-www-form-urlencoded后端才拿得到参数
const instance = axios.create({ baseURL: 'http://localhost:8080/api/', timeout: 10000, headers: {'Content-Type':'application/x-www-form-urlencoded'} });
-
后端开发
-
pojo文件夹结构再做更新
-
entity
就是数据库表对应的实体类 -
req
就是请求参数的封装类 -
vo
就是把
entity
中
userId
啥的转为
UserVo
,再带上一些需要展示上页面的数据,
vo
类中要有两个静态方法:
vo
转
obj
,
obj
转
vo
-
-
查询接口小技巧:拼接
QueryWapper
@Override public QueryWrapper<Post> getQueryWrapper(PostQueryRequest postQueryRequest) { QueryWrapper<Post> queryWrapper = new QueryWrapper<>(); if (postQueryRequest == null) { return queryWrapper; } String searchText = postQueryRequest.getSearchText(); String sortField = postQueryRequest.getSortField(); String sortOrder = postQueryRequest.getSortOrder(); Long id = postQueryRequest.getId(); String title = postQueryRequest.getTitle(); String content = postQueryRequest.getContent(); List<String> tagList = postQueryRequest.getTags(); Long userId = postQueryRequest.getUserId(); Long notId = postQueryRequest.getNotId(); // 拼接查询条件 if (StringUtils.isNotBlank(searchText)) { queryWrapper.like("title", searchText).or().like("content", searchText); } queryWrapper.like(StringUtils.isNotBlank(title), "title", title); queryWrapper.like(StringUtils.isNotBlank(content), "content", content); if (CollectionUtils.isNotEmpty(tagList)) { for (String tag : tagList) { queryWrapper.like("tags", "\"" + tag + "\""); } } queryWrapper.ne(ObjectUtils.isNotEmpty(notId), "id", notId); queryWrapper.eq(ObjectUtils.isNotEmpty(id), "id", id); queryWrapper.eq(ObjectUtils.isNotEmpty(userId), "userId", userId); queryWrapper.eq("isDelete", false); queryWrapper.orderBy(SqlUtils.validSortField(sortField), sortOrder.equals(CommonConstant.SORT_ORDER_ASC), sortField); return queryWrapper; }
-
拼接
QueryWrapper
后的Controller@GetMapping("/list/page/vo") public BaseResponse<Page<PostVo>> listPostVoByPage (PostQueryRequest postQueryRequest){ int current = postQueryRequest.getCurrent(); long pageSize = postQueryRequest.getPageSize(); Page<Post> page = postService.page(new Page<>(current, pageSize), postService.getQueryWrapper(postQueryRequest)); Page<PostVo> postVoPage = new Page<>(current, pageSize, page.getTotal()); postVoPage.setRecords(postService.getPostVO(page.getRecords())); return ResultUtils.success(postVoPage); }
数据抓取
从请求地址里获取
/**
* 初始化帖子列表
*
* 实现了CommandLineRunner接口并注入到容器中后每次启动程序都会执行一次run方法
*/
//@Component
@Slf4j
public class FetchInitPostList implements CommandLineRunner {
@Autowired
PostService postService;
@Override
public void run(String... args) {
String json = "{\"current\":1,\"pageSize\":8,\"sortField\":\"_score\",\"sortOrder\":\"descend\",\"searchText\":\"\",\"category\":\"文章\",\"reviewStatus\":1}";
String url = "https://www.code-nav.cn/api/post/search/page/vo";
String result = HttpRequest
.post(url)
.body(json) //body中传入json数据
.execute()
.body();//获取返回值中body里的内容
Map<String, Object> map = JSONUtil.toBean(result, Map.class);
JSONObject data = (JSONObject)map.get("data");
JSONArray records = (JSONArray)data.get("records");
List<Post> postList = new ArrayList<>();
for (Object record : records) {
JSONObject tempRecord = (JSONObject) record;
Post post = new Post();
post.setTitle(tempRecord.getStr("title"));
post.setContent(tempRecord.getStr("content"));
post.setTags(tempRecord.getStr("tags"));
post.setUserid(1L);
postList.add(post);
}
postService.saveBatch(postList);
}
}
从页面结构中获取
-
首先打开页面的检查模式定位到想要的数据,然后找到它的css选择器,根据css选择器拿到这个标签元素,然后再逐层获取到需要的数据
-
-
int current = 1; String url = "https://cn.bing.com/images/search?q=" + "小黑子" + "&first=" + current; //获取url的整个页面 Document doc = Jsoup.connect(url).get(); //通过css选择器定位获取使用了这个选择器的所有元素 Elements elements = doc.select(".iuscp.isv"); for (Element element : elements) { //也是通过css选择器定位获取元素.get(0)获取第一个元素,.attr("")获取属性名对应的值 String attr = element.select(".inflnk").get(0).attr("aria-label"); System.out.println(attr); String m = element.select(".iusc").get(0).attr("m"); Map<String, Object> map = JSONUtil.toBean(m, Map.class); System.out.println(map.get("murl")); }
用到的设计模式
1.门面模式
- 对某个功能只提供一个接口,通过传入的某个数据进行细分需要进行什么操作
2.适配器模式
- 当调用一个API接口发现需要的跟我们自己提供的数据不一样时,就需要一个适配器来将我们的数据转为该API接口需要的数据
3.注册器模式(本质也是单例)
- 通过一个map对象或其他类型存储好后面需要用的对象,如果需要添加就往map里面注册
Elastic Stack(一套技术栈)
官网:https://www.elastic.co/cn/
包含了数据的整合 => 提取 => 存储 => 使用,一整套!
- beats:从各种不同类型的文件 / 应用来 采集数据 a,b,c,d,e,aa,bb,cc
- Logstash:从多个采集器或数据源来抽取 / 转换数据,向 es 输送 aa,bb,cc
- elasticsearch:存储、查询数据
- kibana:可视化 es 的数据
下载 ES安装包
elasticsearch:https://www.elastic.co/guide/en/elasticsearch/reference/7.17/setup.html
kibana:https://www.elastic.co/guide/en/kibana/7.17/introduction.html
安装并启动的文档:
https://www.elastic.co/guide/en/elasticsearch/reference/7.17/zip-windows.html
-
测试是否启动成功:
- 访问:localhost:9200,看看是否返回es的相关信息
https://www.elastic.co/guide/en/kibana/7.17/windows.html
-
测试是否启动成功:
- 访问:localhost:5601
只要是一套技术,所有版本必须一致!!!此处用 7.17
ES的几种调用方式
1) restful api调用(http请求)
Get请求:http://localhost:9200
curl可以发送模拟请求:curl -X GET “localhost:9200/?pretty”
Es的启动端口
- 9200:给外部用户(给客户端调用)的端口
- 9300:给ES集群内部通信的(外部调用不了的)
2) kibana界面中的devtools
3) 客户端调用
go语言、java、python
ES 语法
1) DSL
-
json格式,和http请求兼容性最好(一般用这个)
-
mapping可以理解为mysql中的表结构,可以显式创建mapping,在插入数据的时候该mapping缺少某个字段也会动态改变,而不是报错
//获取某个文档的mapping GET user/_mapping //显式设置mapping PUT user { "mappings": { "properties": { "age": { "type": "integer" }, "email": { "type": "keyword" }, "name": { "type": "text" } } } }
插入数据(如果不存在这个表就建表,字段根据传入的数据制定):es里的文档就是mysql中的表
POST 文档名(表名)/_doc
{
"title": "dog",
"desc": "dog的描述"
}
查询数据
-
查询某个文档的所有数据
GET 文档名/_search { "query": { "match_all": { } }, "sort": [ { "@timestamp": "desc" } ] }
-
根据文档中的某条数据的id查询数据
GET 文档名/_doc/某条数据id
修改数据
-
POST 文档名/_doc/某条数据id { "title": "dog2", "desc": "dog的描述2" }
删除
-
DELETE 文档名
2) EQL
专门查询ECS文档(标准指标文档)的数据语法,更加规范,但只适用于特殊场景
-
https://www.elastic.co/guide/en/elasticsearch/reference/7.17/eql.html
-
POST my_event/_doc { "title": "鱼333333皮", "@timestamp": "2099-05-06T16:21:15.000Z", "event": { "original": "192.0.2.42 - - [06/May/2099:16:21:15 +0000] \"GET /images/bg.jpg HTTP/1.0\" 200 24736" } } GET my_event/_eql/search { "query": """ any where 1 == 1 """ }
3) SQL
学习成本低,但是可能需要插件支持、性能较差
-
https://www.elastic.co/guide/en/elasticsearch/reference/7.17/sql-getting-started.html
-
POST /_sql?format=txt { "query": "SELECT * FROM post where title like '%鱼皮%'" }
分词器
内置分词器
:https://www.elastic.co/guide/en/elasticsearch/reference/7.17/analysis-analyzers.html
- 空格分词器:whitespace,结果 The、quick、brown、fox.
POST _analyze
{
"analyzer": "keyword",
"text": "The quick brown fox."
}
- 标准分词规则,结果:is、this、deja、vu
POST _analyze
{
"tokenizer": "standard",
"filter": [ "lowercase", "asciifolding" ],
"text": "Is this déja vu?",
}
- 关键词分词器:就是不分词,整句话当作专业术语
POST _analyze
{
"analyzer": "keyword",
"text": "The quick brown fox."
}
-
IK 分词器(ES 插件)
中文友好:https://github.com/medcl/elasticsearch-analysis-ik
下载地址:https://github.com/medcl/elasticsearch-analysis-ik/releases/tag/v7.17.7(注意版本一致)
思考:怎么样让 ik 按自己的想法分词?
回答:自定义词典
ik_smart 和 ik_max_word 的区别?举例:“小黑子”
ik_smart 是智能分词,尽量选择最像一个词的拆分方式,比如“小”、“黑子”
ik_max_word 尽可能地分词,可以包括组合词,比如 “小黑”、“黑子”
打分机制
有 3 条内容:
1、张三是狗
2、张三是小黑子
3、我是小黑子
用户搜索:
张三,第一条分数最高,因为第一条匹配了关键词,而且更短(匹配比例更大)
张三小黑子 => 张三、小、黑子 => 2 > 3 > 1
参考文章:https://liyupi.blog.csdn.net/article/details/119176943
官方参考文章:https://www.elastic.co/guide/en/elasticsearch/guide/master/controlling-relevance.html
应用到项目中
1、在Elastic Search中创建一个文档来对应数据库中的表
ES Mapping:
id(可以不放到字段设置里)
ES 中,尽量存放需要用户筛选(搜索)的数据
aliases:别名(为了后续方便数据迁移)
字段类型是 text,这个字段是可被分词的、可模糊查询的;而如果是 keyword,只能完全匹配、精确查询。
analyzer(存储时生效的分词器):用 ik_max_word,拆的更碎、索引更多,更有可能被搜出来
search_analyzer(查询时生效的分词器):用 ik_smart,更偏向于用户想搜的分词
如果想要让 text 类型的分词字段也支持精确查询,可以创建 keyword 类型的子字段
PUT post_v1
{
"aliases": {
"post": {}
},
"mappings": {
"properties": {
"title": {
"type": "text",
"analyzer": "ik_max_word",
"search_analyzer": "ik_smart",
"fields": {
"keyword": {
"type": "keyword",
"ignore_above": 256
}
}
},
"content": {
"type": "text",
"analyzer": "ik_max_word",
"search_analyzer": "ik_smart",
"fields": {
"keyword": {
"type": "keyword",
"ignore_above": 256
}
}
},
"tags": {
"type": "keyword"
},
"userId": {
"type": "long"
},
"createTime": {
"type": "date"
},
"updateTime": {
"type": "date"
},
"isDelete": {
"type": "integer"
}
}
}
}
2、引入依赖(注意依赖版本是否与装的elastic search相匹配)
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-elasticsearch</artifactId>
</dependency>
3、创建elastic search中文档对应的pojo类
- @id:指定该属性为数据的id,es会将这个id的值同步到es的_id中(es中,_ 开头的字段标识系统默认属性
@Document(indexName = "post")
@Data
public class PostEsDTO implements Serializable {
private static final String DATE_TIME_PATTERN = "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'";
/**
* id
*/
@Id
private Long id;
/**
* 标题
*/
private String title;
/**
* 内容
*/
private String content;
/**
* 标签列表
*/
private List<String> tags;
/**
* 创建用户 id
*/
private Long userId;
/**
* 创建时间
*/
@Field(index = false, store = true, type = FieldType.Date, format = {}, pattern = DATE_TIME_PATTERN)
private Date createTime;
/**
* 更新时间
*/
@Field(index = false, store = true, type = FieldType.Date, format = {}, pattern = DATE_TIME_PATTERN)
private Date updateTime;
/**
* 是否删除
*/
private Integer isDelete;
private static final long serialVersionUID = 1L;
/**
* 对象转包装类
*
* @param post
* @return
*/
public static PostEsDTO objToDto(Post post) {
if (post == null) {
return null;
}
PostEsDTO postEsDTO = new PostEsDTO();
BeanUtils.copyProperties(post, postEsDTO);
String tagsStr = post.getTags();
if (StringUtils.isNotBlank(tagsStr)) {
postEsDTO.setTags(JSONUtil.toBean(tagsStr, List.class));
}
return postEsDTO;
}
/**
* 包装类转对象
*
* @param postEsDTO
* @return
*/
public static Post dtoToObj(PostEsDTO postEsDTO) {
if (postEsDTO == null) {
return null;
}
Post post = new Post();
BeanUtils.copyProperties(postEsDTO, post);
List<String> tagList = postEsDTO.getTags();
if (CollectionUtils.isNotEmpty(tagList)) {
post.setTags(JSONUtil.toJsonStr(tagList));
}
return post;
}
}
-
创建操作es文档的类
-
继承
ElasticsearchRepository<pojo, ID>
,其中就会提供一些操作es的方法,也可以根据方法名自动生成操作数据的方法,继承了这个类以后这个类的对象会自动注入到
IOC
容器,不需要注解啥的,这个对象更适合做一些简单增删改查操作和定制化操作,返回结果精简public interface PostEsDao extends ElasticsearchRepository<PostEsDTO, Long> { //只需要根据它的规则写方法名即可 List<PostEsDTO> findByUserId(Long userId); }
-
通过直接引入
ElasticsearchRestTemplate
对象,这个对象里有很多复杂的操作,返回结果多、复杂@Resource private ElasticsearchRestTemplate elasticsearchRestTemplate;
-
4、实现mysql与es数据同步
需要实现两种同步:
全量同步
(首次启动时)、
增量同步
(有新数据时)
实现方式:
-
定时任务,比如1分钟1次,找到MySQL中过去几分钟内(至少是定时周期的2倍)发生改变的数据,然后更新到ES。
优点:简单易懂、占用资源少、不用引入第三方中间件
缺点:有时间差
应用场景:数据短时间内不同步影响不大、或者数据几乎不发生修改
-
双写:写数据的时候,必须也去写ES;更新删除数据库同理。(事务:建议先保证MySQL写成功,如果ES写失败了,可以通过定时任务+日志+告警进行检测和修复(补偿))
-
用Logstash数据同步管道(一般要配合kafka消息队列+beats采集器):
-
订阅数据库流水的方式canal
1、定时任务实现
全量同步:
/**
* 全量同步的定时方法,只执行一次
*
* 实现了CommandLineRunner接口并注入到容器中后每次启动程序都会执行一次run方法
*/
@Component
@Slf4j
public class FullSyncPostToEs implements CommandLineRunner {
@Resource
private PostEsDao postEsDao;
@Resource
private PostService postService;
@Override
public void run(String... args) throws Exception {
List<Post> postList = postService.list();
if (CollectionUtil.isEmpty(postList)) {
return;
}
if (CollectionUtil.isNotEmpty(postList)) {
List<PostEsDTO> postEsDTOList = postList.stream().map(PostEsDTO::objToDto).collect(Collectors.toList());
int total = postEsDTOList.size();
log.info("FullSyncPostToEs start, total {}", total);
postEsDao.saveAll(postEsDTOList);
log.info("FullSyncPostToEs start, end {}", total);
}
}
}
增量同步:
/**
* 增量同步的定时任务
*
* 先将类标注@EnableScheduling,然后在循环任务的方法上用 @Scheduled(fixedRate = 60 * 1000)控制它1分组执行一次
*/
@Component
@Slf4j
@EnableScheduling
public class IncSyncPostToEs {
@Resource
private PostEsDao postEsDao;
@Resource
private PostMapper postMapper;
/**
* 每分钟执行一次
*/
@Scheduled(fixedRate = 60 * 1000)
public void run() {
//获取前五分钟更新的数据,将这个时间设置为执行周期的3-5倍,避免前面几次更新失败,造成数据丢失
//es中如果数据插入时id一样会覆盖,所以不用担心冲突问题
Date fiveMinutesAgoDate = new Date(new Date().getTime() - 5 * 60 * 1000L);
List<Post> postList = postMapper.listPostByUpdateTime(fiveMinutesAgoDate);
List<PostEsDTO> postEsDTOList = postList.stream().map(PostEsDTO::objToDto).collect(Collectors.toList());
int total = postEsDTOList.size();
log.info("IncSyncPostToEs start, total {}", total);
postEsDao.saveAll(postEsDTOList);
log.info("IncSyncPostToEs start, end {}", total);
}
}
5、编写查询该es文档的DSL
https://www.elastic.co/guide/en/elasticsearch/reference/7.17/query-filter-context.html
https://www.elastic.co/guide/en/elasticsearch/reference/7.17/query-dsl-bool-query.html
示例:
GET post/_search
{
"query": {
"bool": { // 组合条件
"must": [ // 必须都满足
{ "match": { "title": "鱼皮" }}, // match 模糊查询
{ "match": { "content": "知识星球" }}
],
"filter": [ //会筛选(跟must一样,必须都满足),但不参与最终评分
{ "term": { "status": "published" }}, // term 精确查询
{ "range": { "publish_date": { "gte": "2015-01-01" }}} // range 范围查询
]
}
}
}
当前es文档的查询dsl:
GET post/_search
{
"query": {
"bool": {
"should": [ //其中的几个符合即可,至于到底几个要根据下面的minimum_should_match设置的值来确定
{
"match": {
"title": "张三哈哈哈哈"
}
},
{
"match": {
"content": "傻逼"
}
}
],
"filter": [
{
"term": {
"isDelete": 0
}
},
{
"term": {
"tags": "文章"
}
},
{
"term": {
"tags": "Java"
}
}
],
"minimum_should_match": 1 //should中至少有几个符合匹配条件
}
},
"from": 0,
"size": 5,
"sort": [
{
"_score": {
"order": "desc"
}
},
{
"updateTime": {
"order": "desc"
}
}
]
}
6、根据查询DSL编写Java的查询es语句
public Page<Post> getFromEs(PostQueryRequest postQueryRequest) {
if (postQueryRequest == null) {
return null;
}
String searchText = postQueryRequest.getSearchText();
String title = postQueryRequest.getTitle();
String content = postQueryRequest.getContent();
List<String> tags = postQueryRequest.getTags(); //必须全部满足的标签
List<String> orTags = postQueryRequest.getOrTags(); //只满足其中一个就行的标签
String sortField = postQueryRequest.getSortField();
String sortOrder = postQueryRequest.getSortOrder();
int current = postQueryRequest.getCurrent();
long pageSize = postQueryRequest.getPageSize();
BoolQueryBuilder queryBuilder = QueryBuilders.boolQuery();
//设置should
if (StringUtils.isNotBlank(title)) {
queryBuilder.should(QueryBuilders.matchQuery("title", title));
}
if (StringUtils.isNotBlank(content)) {
queryBuilder.should(QueryBuilders.matchQuery("content", content));
}
if (StringUtils.isNotBlank(searchText)) {
queryBuilder.should(QueryBuilders.matchQuery("title", searchText));
queryBuilder.should(QueryBuilders.matchQuery("content", searchText));
}
if (StringUtils.isAllBlank(title, content, searchText)) {
//如果上面几个查询条件都为空就查询出全部数据
queryBuilder.should(QueryBuilders.matchAllQuery());
}
queryBuilder.minimumShouldMatch(1);//设置should中最少有一个匹配条件
//设置filter
queryBuilder.filter(QueryBuilders.termQuery("isDelete", 0));
if (CollectionUtil.isNotEmpty(tags)) {
//每一个值设置一个term,就必需列表里所有的标签数据中都存在才能把这条数据查出来
for (String tag : tags) {
queryBuilder.filter(QueryBuilders.termQuery("tags", tag));
}
}
if (CollectionUtil.isNotEmpty(orTags)) {
//terms:只要数据中的tags有一个值存在与查询条件中的tags里,就会被查出来
queryBuilder.filter(QueryBuilders.termsQuery("tags", orTags));
}
//设置分页(es起始页为0!!!!!跟mysql不一样)
PageRequest pageRequest = PageRequest.of(current - 1, (int) pageSize);
//设置排序
//先根据es生成的分数排序
SortBuilder<?> sortBuilderForScore = SortBuilders.fieldSort("_score");
sortBuilderForScore.order(SortOrder.DESC);
//再根据传入的字段进行自定义排序
SortBuilder<?> sortBuilderForSortField = SortBuilders.scoreSort();
if (StringUtils.isNotBlank(sortField)) {
sortBuilderForSortField = SortBuilders.fieldSort(sortField);
sortBuilderForSortField.order(CommonConstant.SORT_ORDER_ASC.equals(sortOrder) ? SortOrder.ASC : SortOrder.DESC);
}
// 构造查询
NativeSearchQuery searchQuery = new NativeSearchQueryBuilder().withQuery(queryBuilder)
.withPageable(pageRequest).withSorts(sortBuilderForScore, sortBuilderForSortField).build();
//执行查询,查出来的数据就存在这
SearchHits<PostEsDTO> searchHits = elasticsearchRestTemplate.search(searchQuery, PostEsDTO.class);
List<Post> resultList = new ArrayList<>();
if (searchHits.hasSearchHits()) {
//查询数据库
List<SearchHit<PostEsDTO>> searchHitList = searchHits.getSearchHits();
List<Long> idList = searchHitList.stream().map(searchHit -> searchHit.getContent().getId()).collect(Collectors.toList());
//继承的ServiceImpl<PostMapper, Post>中有baseMapper这个成员变量
List<Post> postList = baseMapper.selectBatchIds(idList);
Map<Long, Post> postMap = postList.stream().collect(Collectors.toMap(Post::getId, post -> post));
for (Long id : idList) {
if (postMap.containsKey(id)) {
//数据库中存在该数据,加入结果集
resultList.add(postMap.get(id));
} else {
//数据库中不存在该数据,从es中删除
String delete = elasticsearchRestTemplate.delete(id.toString(), PostEsDTO.class);
log.info("delete post {}", delete);
}
}
}
Page<Post> page = new Page<>(current, pageSize, searchHits.getTotalHits());
page.setRecords(resultList);
return page;
}
}
压力测试
官方文档:https://jmeter.apache.org/
找到jar包:apache-jmeter–5.5 apache-jmeter-5.51 bin\ ApacheJMeter.jar 启动
-
配置线程组
-
配置通用请求头
-
配置默认请求信息
-
设置单个请求(选择HTTP请求)
-
设置响应断言
-
设置结果树
-
设置聚合报告
- 90%分位:90%的用户发送请求到服务器做出响应花的时间都在这个时间内
- 99%分位:99%的用户都在这个响应时间内
- 99%分位:99%的用户都在这个响应时间内
- 吞吐量:每秒处理的请求数
部署
安装mysql,开启3306端口
安装es
-
下载并解压es
wget https://artifacts.elastic.co/downloads/elasticsearch/elasticsearch-7.17.9-linux-x86_64.tar.gz tar -xzf elasticsearch-7.17.9-linux-x86_64.tar.gz
-
需要重新创建一个用户来执行
./elasticsearch &
加个&是后台执行useradd es passwd es
-
安装ik分词器
wget https://github.com/medcl/elasticsearch-analysis-ik/releases/download/v7.17.6/elasticsearch-analysis-ik-7.17.6.zip unzip elasticsearch-analysis-ik-7.17.6.zip #如果ik与es的小版本对不上 vim plugin-descriptor.properties 把 version 改为 你的版本 把 elasticsearch.version 改为 你的版本
-
使用http的方式向es中创建mapping
curl -XPUT "http://localhost:9200/post_v1" -H 'Content-Type: application/json' -d' { "aliases": { "post": {} }, "mappings": { "properties": { "title": { "type": "text", "analyzer": "ik_max_word", "search_analyzer": "ik_smart", "fields": { "keyword": { "type": "keyword", "ignore_above": 256 } } }, "content": { "type": "text", "analyzer": "ik_max_word", "search_analyzer": "ik_smart", "fields": { "keyword": { "type": "keyword", "ignore_above": 256 } } }, "tags": { "type": "keyword" }, "userId": { "type": "long" }, "createTime": { "type": "date" }, "updateTime": { "type": "date" }, "isDelete": { "type": "integer" } } } }'
打包Java后端为jar并允许
先把代码上传到代码仓库github
然后拉下来,在文件目录内运行这些语句,也可以在本地打包好再把jar包传过去,但是更新没这么方便
下载Java8
yum install -y java-1.8.0-openjdk*
下载maven
curl -o apache-maven-3.8.5-bin.tar.gz https://dlcdn.apache.org/maven/maven-3/3.8.5/binaries/apache-maven-3.8.5-bin.tar.gz
打包构建,跳过测试
git clone xxx 下载代码
打包构建,跳过测试
mvn package -DskipTests
加个 nohup &后 台运行
nohup java -jar ./junso-backend-0.0.1-SNAPSHOT.jar --spring.profiles.active=prod &
运行vue前端
配置nginx
vim /etc/profile
export PATH=$PATH:/www/server/nginx/sbin
先把代码上传到代码仓库github
然后拉下来,在文件目录内运行这些语句
npm install
npm i --save ant-design-vue@next
npm install axios
npm run build
nohup npm run serve & exit
问题:nohup npm run serve & 运行前端后,关闭当前窗口shell连接后前端进程就退出了
需要配置nohup的环境变量
查询nohup位置:复制这个位置
which nohup
修改环境变量
vim .bash_profile
在export之前加上 PATH=$PATH:$HOME/bin:/复制的地址
问题:es后台启动后自己kill了
修改jvm.options配置内存参数(jvm.options在es安装目录的config下)
这个就是直接修改JVM分配给elasticSearch的内存
最好将这两项参数修改成下面的参数设置:
修改之前
##-Xms4g
##-Xmx4g
修改之后
-Xms256m
-Xmx256m