聚合搜索平台
快来分享你的内容吧~
- 2025-08-01·Java后端查看全文在做聚合搜索时canal监听不了修改数据,尝试了编程导航的解决方法,比如执行 ALTER USER 'canal'@'%' IDENTIFIED WITH mysql_native_password BY 'canal_user'; ALTER USER 'canal'@'%' IDENTIFIED...编程导航小智:Canal监听MySQL数据修改问题排查与解决方案根据你提供的信息,Canal无法成功监听MySQL数据修改的问题,以下是我的详细分析和解决方案:问题分析1. "show processlist" 看不到canal用户连接,说明Canal客户端并未成功连接到MySQL服务器2. 从你提供的日志链接(https://www.codecopy.cn/post/rq0y6n)可以看出有连接问题常见原因排120分享
- 2025-03-14·网站开发虽然不影响运行,但就是比较好奇 为什么一开始加载picture页面给出的是风景图片,而不是null有关的图片(只有在点击查询 几次后才会有null有关的图片编程导航小智:这个问题的答案可能依赖于具体的实现方式和代码逻辑,以下是一种可能的解释。当你打开picture页面时,页面会首先加载一些初始内容,这些内容可能是事先定义好的默认图片或风景图片。这些默认图片或风景图片可能作为页面加载的过渡内容,以提高用户体验。在你点击查询并多次进行查询后,页面上的内容会根据查询的结果进行更新。如果查询结果为null或没有相关的图片,则可能会展示显示null相关的图片。这个逻辑可能是210分享
- 2025-03-13·网站开发
- 2025-02-06·Java后端聚合搜索平台数据同步学习笔记——记录SpringBoot整合Canal查看全文sakura:补一下遇到的问题:Error: Could not create the Java Virtual Machine.Error: Could not create the Java Virtual Machine.Error: A fatal exception has occurred. Program will exit.由于使用JDK17,虚拟机不知道PermSize这个参数解决办法:删除图1318分享
- 2024-11-13·Java后端作为java后端,没有写过vue代码。我想请问大家,聚合搜索平台项目中的前端代码大家会跟着一起写吗?对我来说还很吃力,很容易踩坑😩南山之南:vue其实挺简单的,如果你时间够并且对前端感兴趣,可以一边看参考文档,一边看项目视频敲。630分享
在做聚合搜索时canal监听不了修改数据,尝试了编程导航的解决方法,比如执行 ALTER USER 'canal'@'%' IDENTIFIED WITH mysql_native_password BY 'canal_user'; ALTER USER 'canal'@'%' IDENTIFIED BY 'canal_user' PASSWORD EXPIRE NEVER; FLUSH PRIVILEGES; 以及修改canal.instance.filter.query.dml为true都不能解决 不过执行 show processlist; 的时候没有看到canal的用户信息,不清楚为什么,因为终端我执行startup.bat时canal.log文件里显示 2025-07-31 23:57:44.459 [main] INFO com.alibaba.otter.canal.deployer.CanalController - ## start the canal server[192.168.58.1(192.168.58.1):11111] 2025-07-31 23:57:45.390 [main] INFO com.alibaba.otter.canal.deployer.CanalStarter - ## the canal server is running now ...... 报错日志连接:https://www.codecopy.cn/post/rq0y6n 求求各位佬帮帮忙,提点解决方法😭
聚合搜索平台学习打卡
学习目的:为了学习ElasticSearch这门技术以及如何运用至项目里。 今天利用Jsoup库爬了必应的图片,感觉比当时在云图库里用的时候更加理解一些。 明天开始背八股 + 继续学习项目,补药再偷懒了 学习总结: 一、数据抓取的几种方式 1. 直接请求数据接口(最方便),利用HttpClient、OKHttp、Hutool等发送请求。 2. 等待网页渲染出明文内容,从前端页面中解析出需要的内容。 二、Jsoup库 Jsoup是基于Java的**HTML解析器**,不但能直接解析某个URL地址、HTML文本内容,还能通过类似于DOM、CSS或者jQuery的方法来操作数据 相关概念 Jsoup很多概念和js类似,可参照对比理解 Document :文档对象。每份HTML页面都是一个文档对象,Document 是 jsoup 体系中最顶层的结构。 Element:元素对象。一个 Document 中可以着包含着多个 Element 对象,可以使用 Element 对象来遍历节点提取数据或者直接操作HTML。 Elements:元素对象集合,类似于List。 Node:节点对象。标签名称、属性等都是节点对象,节点对象用来存储数据。 类继承关系:Document 继承自 Element(class Document extends Element) ,Element 继承自 Node(class Element extends Node)。 一般执行流程:先获取 Document 对象,然后获取 Element 对象,最后再通过 Node 对象获取数据。 参考:https://blog.csdn.net/qq_41694906/article/details/129857848
https://github.com/infinilabs/analysis-ik 上面的链接是 聚合搜索项目中 analysis-ik 插件最新仓库地址,原视频中的仓库地址已经没有了, 最新链接也是medcl作者 在维护的 最明显的区别是 现在analysis-ik有许多与 elasticsearch 同版本的zip文件可以下载
聚合搜索前端运行&部署文档
# 环境准备 + [聚合搜索源码](https://www.codefather.cn/course/1790979621621641217/section/1790981214354059265) + node >= **18** 推荐使用 [NVM 安装](https://www.codefather.cn/post/1823595688926167041) + yarn [官方链接](https://classic.yarnpkg.com/lang/en/docs/install/#windows-stable) [百度网盘](https://pan.baidu.com/s/1TexTr-PHE5sejjq6795oVg) 密码:958c + WebStorm # 本地运行 ## 安装依赖 1、使用 WebStorm 打开(VSCode 也是可以的) 用 WebStorm 打开前端源码 <img src="https://pic.code-nav.cn/post_picture/1608460212774109186/HI0fzXLULRROZC0O.webp" alt="image-20250316091637911" width="100%" /> > 回弹出一个是否相信,选择相信即可 2、找到 `Terminal` 选项 <img src="https://pic.code-nav.cn/post_picture/1608460212774109186/lcDKAkjya0hSshgC.webp" alt="image-20250211193259995" width="100%" /> --- 如果没有看到看这里有没有 <img src="https://pic.code-nav.cn/post_picture/1608460212774109186/zkTlzIWle4FfrubH.webp" alt="image-20241114101254432" width="471px" /> 还是没有的话找到 `Settings` -> `plugins` -> `Terminal` <img src="https://pic.code-nav.cn/post_picture/1608460212774109186/BaODFXxXf6FQOXh7.webp" alt="image-20241114101344618" width="334px" /> <img src="https://pic.code-nav.cn/post_picture/1608460212774109186/AIsdamnYCUCJpC7c.webp" alt="image-20241114101433358" width="100%" /> --- 3、执行 ```shell npm install --force ``` > 启动这个前端需要安装 yarn ,安装包 [官方链接](https://classic.yarnpkg.com/lang/en/docs/install/#windows-stable) [百度网盘](https://pan.baidu.com/s/1TexTr-PHE5sejjq6795oVg) 密码:958c <img src="https://pic.code-nav.cn/post_picture/1608460212774109186/pwJVOXDfa7XSmJ55.webp" alt="image-20250316091929626" width="100%" /> 最后执行效果: <img src="https://pic.code-nav.cn/post_picture/1608460212774109186/ZCpMzsoQPMoOTgSP.webp" alt="image-20250316091945606" width="100%" /> ## 运行前端 找到根目录下的 `pakage.json` <img src="https://pic.code-nav.cn/post_picture/1608460212774109186/2peaAtZC0flmH6Q3.webp" alt="image-20250211193517515" width="100%" /> <img src="https://pic.code-nav.cn/post_picture/1608460212774109186/lCvCpwJWyqcHasCc.webp" alt="image-20250316092156237" width="100%" /> > Run dev 就是正常启动,如果是想要 debug 的话建议先 dev 启动然后找到对应的 url 使用快捷键 **Ctrl + Alt + Shift + 点击 url 地址 **即可进入 debug 模式 > > 官方文档: https://www.jetbrains.com/help/webstorm/react.html#react_debug_from_tw 启动成功 <img src="https://pic.code-nav.cn/post_picture/1608460212774109186/pib8zi234eMCSzYW.webp" alt="image-20250316092450714" width="100%" /> <img src="https://pic.code-nav.cn/post_picture/1608460212774109186/GobO2ha0iwctmOzV.webp" alt="image-20250316172951416" width="100%" /> --- pakage.json ``` "scripts": { "serve": "vue-cli-service serve", "build": "vue-cli-service build", "lint": "vue-cli-service lint" }, ``` 1. serve 开发化境启动,会**热更新**(如果有修改会立刻体现) 3. buid 构建模式,会生成 dist 文件方便前端部署 ## 修改请求路径 修改 `baseURL` 就是前端请求后端的路径 <img src="https://pic.code-nav.cn/post_picture/1608460212774109186/N6KU24tBNeDYgXUG.webp" alt="image-20250316170324541" width="100%" /> 本地就使用 localhost 部署到服务器上就修改成 IP 或者域名 + http://localhost:8101 + http://192.122.11.11:8101 + http://leikooo.com:8101 域名相当于平替 IP + http://leikooo.com 没有端口?别慌 `http` 默认请求`80` 端口只需要在 `Nginx` 进行`反向代理`即可 + https://leikooo.com 和上面的区别?这个是 `https` 安全的连接,默认端口也变成 `443` ## 打包 <img src="https://pic.code-nav.cn/post_picture/1608460212774109186/IWzwoXxopMh50k9b.webp" alt="image-20250316171230047" width="100%" /> <img src="https://pic.code-nav.cn/post_picture/1608460212774109186/9z03GPzUwladGC4z.webp" alt="image-20250316171146911" width="100%" /> build 成功 <img src="https://pic.code-nav.cn/post_picture/1608460212774109186/11YgbZAX88x4HiGT.webp" alt="image-20250316171337106" width="100%" /> 可以看到出现了 dist 目录 <img src="https://pic.code-nav.cn/post_picture/1608460212774109186/RT7Jjb7sGcmHumjk.webp" alt="Snipaste_2025-03-16_17-14-06" width="100%" /> --- 如果没有出现 dist 目录,那么就按照下面进行操作一下,就可以看到了 File -> Reload All from Disk <img src="https://pic.code-nav.cn/post_picture/1608460212774109186/iWrQgmbTMdStVkJD.webp" alt="image-20250211195133450" width="362px" /> # 部署 [『 前端部署教程(通用) 』](https://www.codefather.cn/post/1836710450264096769)
虽然不影响运行,但就是比较好奇 为什么一开始加载picture页面给出的是风景图片,而不是null有关的图片(只有在点击查询 几次后才会有null有关的图片
model层
model层是管理数据模型额核心部分,通常与数据库表直接映射 # entity包 直接映射数据库表,用于持久化数据 该包存放实体类,这些类直接映射数据库表结构,实体类包含于数据库表字段对应的属性,并通过ORM框架与数据库交互 # dto包 用于层与层之间的数据传输,不直接映射数据库表 `DTO`(Data Transfer Object)是**数据传输对象**,用于在不同层之间传递数据。与实体类不同,DTO不一定直接映射数据库表,而是根据业务需求设计,通常用于减少网络传输的数据量或简化数据传递 # vo包 封装一组数据属性,用于表示完整的值 `VO`(Value Object)是**值对象**,用于封装一组相关的数据属性,VO通常用于表示一个完整的值,例如查询结果或业务逻辑中的某个数据集合,与DTO类似,VO也不直接映射数据库表,更侧重于数据的封装、展示 # enums包 定义固定的常量值,用于表示状态或类型 该包存放枚举类, 用于定义一组固定的常量值,通常用于表示状态、类型等固定的业务逻辑值,例如订单状态
聚合搜索后端运行&部署文档
## 环境准备 + [聚合搜索平台源码](https://www.codefather.cn/course/1790979621621641217/section/1790981214354059265?type=) + IDEA [你懂的教程](https://www.codefather.cn/essay/1837021571340648449) + Redis [网盘安装包](https://pan.baidu.com/s/1-QhCv0Crg2zvwG3ODxvv-Q) 提取码: vmty + MySQL5.7 [网盘安装包](https://pan.baidu.com/s/1uIDAzBtxBXHq8YZOfhlI9w?pwd=bbr5) 提取码:bbr5 + MySQL8.0 [网盘安装包](https://pan.baidu.com/s/1O6TrRCpb66A5hdgy0EY9HA?pwd=g17s)提取码:g17s + MySQL [安装指南](https://zhuanlan.zhihu.com/p/37152572) + MySQL [官方下载链接](https://dev.mysql.com/downloads/windows/installer/5.7.html) + ES 链接: [百度网盘](https://pan.baidu.com/s/1Ifb7PUtwHs8Yqy6Q9UObQg) 提取码: f32k + Kibana [百度网盘 ](https://pan.baidu.com/s/1fv35hoGMmnHoAkHCnM81qA)提取码: i1ku ## 本地启动 > 设置 Maven 完成 https://www.codefather.cn/post/1836689783992958977 使用 IDEA 打开项目 <img src="https://pic.code-nav.cn/post_picture/1608460212774109186/JIMFIMji7t17JP5K.webp" alt="image-20250224214450432" width="100%" /> ### 初始化数据 连接本地 MySQL  <img src="https://pic.code-nav.cn/post_picture/1608460212774109186/oQjTP3b87uZLyMcg.webp" alt="image-20241030155543228" width="100%" /> > 点击 Download 下载 driver files  输入,账号密码之后点击`Test Connection` 成功之后直接点击 `OK` <img src="https://pic.code-nav.cn/post_picture/1608460212774109186/rcyhyCUbczCSoz3R.webp" alt="image-20241030155950071" width="100%" /> 找到根目录 `sql` 目录下面的 `create_table.sql` Ctrl + A 全选代码右键选择 `Execute`   ### 安装依赖 + 首先需要配置好本地 `Mavne`, 和`阿里云镜像`,要是没有配置的话可以看一下这篇[文章](https://www.codefather.cn/post/1836689783992958977) <img src="https://pic.code-nav.cn/post_picture/1608460212774109186/NLVImi5ZTo4R7Nae.webp" alt="image-20241112134330112" width="518px" /> 然后可以在 Build 看到正在下载依赖  ### 选择 Java 版本 <img src="https://pic.code-nav.cn/post_picture/1608460212774109186/umCESwXAP4cPHXXu.webp" alt="image-20241101132905013" width="398px" /> <img src="https://pic.code-nav.cn/post_picture/1608460212774109186/H8GdTAxbmvYBw3PZ.webp" alt="image-20250216185612629" width="100%" /> 没有的话可以使用下面的 `Download JDK` <img src="https://pic.code-nav.cn/post_picture/1608460212774109186/Mwq2gZ6TXL6zfRkC.webp" alt="image-20241114112524129" width="100%" /> <img src="https://pic.code-nav.cn/post_picture/1608460212774109186/qZDwEeiDz1Y7HXOB.webp" alt="image-20250216185720974" width="100%" /> + 本地没有 JDK 可以直接 `Download JDK` + 本地有 JDK 但是没识别到 `Add JDK from disk` 选着对应目录即可 + 选择 Java >= 8 建议 8 ### 安装 Elasticsearch 在 `环境准备`里面有下载链接,下载之后解压到常用软件的路径 1、进入 elasticsearch/bin 在对应的目录输入 `cmd`  2、直接输入命令就可以执行成功,但是要注意在使用的时候一定不要把这个窗口给关闭 ``` elasticsearch.bat ``` <img src="https://pic.code-nav.cn/post_picture/1608460212774109186/WVGsj7shpFdZ4z6C.webp" alt="image-20241111151545481" width="100%" /> 3、如何简化启动步骤呢? `elasticsearch` 也给了我们更简单的启动方式,在 `Windows` 的 `Services` 启动 把 `elasticsearch` 安装到 `Services` 也给了对应的 `bat` 一键执行成功 1)在根目录的 bin 文件夹输入 `cmd` <img src="https://pic.code-nav.cn/post_picture/1608460212774109186/olbLTnE5qj0o6Iki.webp" alt="image-20241111152043832" width="100%" /> ``` elasticsearch-service.bat ``` <img src="https://pic.code-nav.cn/post_picture/1608460212774109186/Nvgo41GwcUb2mnp4.png" alt="image-20241111152153194" width="100%" /> 2)找到 Windows 的 Service `Win + R` 输入 `Services.msc` 找到 `elasticsearch` 直接点击左侧的 `启动`  关闭就找到左侧的 `关闭`即可,一键操作  --- 安装 `analysis-ik` 1)在 `elasticsearch/bin` 目录下面输入 `cmd` > 这个 {version} 版本看你下载的版本 <img src="https://pic.code-nav.cn/post_picture/1608460212774109186/LarRTZ2edFbLr2eM.png" alt="image-20241111223915204" width="650px" /> ``` elasticsearch-plugin install https://get.infini.cloud/elasticsearch/analysis-ik/{version} ``` 比如: ``` elasticsearch-plugin install https://get.infini.cloud/elasticsearch/analysis-ik/7.17.9 ```  2)查看是否安装成: ``` elasticsearch-plugin list ```  3)需要重启 `elasticsearch` win + R 输入 `services.msc` 找到 `elasticsearch` 直接找到左侧的 `Restart` (重启)  4、验证是否启动成功 访问:http://localhost:9200 <img src="https://pic.code-nav.cn/post_picture/1608460212774109186/vAvp3k9R3FZFfZIP.webp" alt="image-20250224223952012" width="100%" /> ### 安装 kibana 这个是 `elasticsearch` 的可视化工具所以要确保 `elasticsearch` 是运行状态 1)进入到 `kibana-7.17.9/bin` 目录在上方地址栏输入`cmd`  2) 输入 ``` kibana.bat ```  > 可能稍微有点慢 4)启动地址 http://localhost:5601  5)dev Tools   ### 使用 ES 1、需要本地启动 `Elasticsearch` 、`Kibana` 2、初始化所需文件 `sql/post_es_mapping.json`  3、启动 `kibana` 在 http://localhost:5601 的 `DevTools` ```json 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": "keyword" }, "createTime": { "type": "date" }, "updateTime": { "type": "date" }, "isDelete": { "type": "keyword" } } } } ``` <img src="https://pic.code-nav.cn/post_picture/1608460212774109186/JoEy9G8wRa5gyR8S.webp" alt="image-20241111231707152" width="100%" /> 4、注释相关代码注释 开启同步任务,将数据库的帖子同步到 Elasticsearch 找到 `com.yupi.yuso.job.once.FullSyncPostToEs `注释掉 ```java // todo 取消注释开启任务 @Component @Slf4j public class FullSyncPostToEs implements CommandLineRunner ``` 找到 `com.yupi.yuso.job.once.FetchInitPostList` ```java //@Component @Slf4j public class FetchInitPostList implements CommandLineRunner ``` 找到 `com.yupi.yuso.job.cycle.IncSyncPostToEs` ```java // todo 取消注释开启任务 @Component @Slf4j public class IncSyncPostToEs ``` 5、修改配置 等部署到服务器上,urls 修改成 http://IP:9200 如果设置账号名和密码,就把对应的信息写到 username 和 password 即可 ```yml spring: # Elasticsearch 配置 # todo 需替换配置,然后取消注释 elasticsearch: uris: http://localhost:9200 # username: root # password: 123456 ``` --- **BUG** Failed to parse mapping [_doc]: analyzer [ik_smart] has not been configured in mappings 解决:通过安装 `analysis-ik` 即可  ### 修改配置文件 配置文件在下面项目的 `resources` 目录  + application.yml 默认是本地运行时生效的配置 + application-prod.yml 当在服务器指定 ` --spring.profiles.active=prod` 才会生效,一般写线上配置,比如:线上 MySQL、Redis ... + application-test.yml 当在服务器指定 ` --spring.profiles.active=test` 才会生效 1、MySQL ```yml spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/my_db # yuapi 是数据库的名称 如果是远程 MySQL 例如: jdbc:mysql://192.129.11.11:3306/yuapi username: root # 用户名 password: 123456 # MySQL 设置的密码 ``` 2、Redis ,如果使用 Redis 修改 `@SpringBootApplication(exclude = {RedisAutoConfiguration.class})` 把 exclude 里面的内容去掉就可以正常使用 Redis ```yml spring: redis: database: 1 host: localhost # 如果线上改成线上 IP port: 6379 # Redis 具体运行端口,默认安装 6379 timeout: 5000 # password: 123456 # 密码,默认安装没有密码,如果服务器 Redis 有密码就修改 ``` 3、COS 相关的配置 ```yml # 对象存储 cos: client: accessKey: xxx secretKey: xxx region: xxx bucket: xxx ``` > 这个 accessKey 就是 SecretId ! 官方文档:https://cloud.tencent.com/document/product/436/56390  2)APPID、SecretKey 、SecretId 都是 https://console.cloud.tencent.com/capi 获取的,注意 SecretKey **只能在创建的时候可见**,注意保存! <img src="https://pic.code-nav.cn/post_picture/1608460212774109186/68dzjtOMPHoW1WzM.webp" alt="image-20250207143248159" width="100%" /> 3)host、bucket、region 都在 COS 对象存储那里获取 首先需要创建存储桶,位置 https://console.cloud.tencent.com/cos/bucket <img src="https://pic.code-nav.cn/post_picture/1608460212774109186/W9tiNDTJQo0pb6HU.webp" alt="image-20241215221117299" width="100%" />   4)获取 host、bucket、region 信息 位置 https://console.cloud.tencent.com/cos/bucket   ### 本地运行 <img src="https://pic.code-nav.cn/post_picture/1608460212774109186/prhIIKrvdnF1QCNJ.webp" alt="image-20250224224126547" width="100%" />  ### 制作 + 上传 JAR 包  > Lifecycle -> package 双击 之后我们就可以在 `target` 目录下面找到打包好的 jar 包 --- 如果报错的话: 1. 删除 `target` 目录(IDEA 右键删除即可),重新打包尝试 2. Maven -> Lifecyle -> clean 3. 退出 IDEA 在`文件管理器`找到该项目的 `.idea` 文件右键删除 ,然后重新打开 IDEA 重新走一遍流程 <img src="https://pic.code-nav.cn/post_picture/1608460212774109186/1m8F2TxtN25821XW.webp" alt="image-20240928104016670" width="100%" /> <img src="https://pic.code-nav.cn/post_picture/1608460212774109186/raiA4OE3cJ0AvSWj.webp" alt="image-20240928104101415" width="375px" /> ## 部署 后面可以参考这篇文章 [『 后端部署(通用) 』](https://www.codefather.cn/post/1837499403858690049)
聚合搜索平台-SpringBoot整合Canal
官方文档:[https://github.com/alibaba/canal/wiki/QuickStart](https://github.com/alibaba/canal/wiki/QuickStart) # 1.准备工作 开启MySQL的Binglog功能配置 binlog-format 为 ROW 模式,my.cnf 中配置如下 ```nginx [mysqld] log-bin=mysql-bin # 开启 binlog binlog-format=ROW # 选择 ROW 模式 server_id=1 # 配置 MySQL replaction 需要定义,不要和 canal 的 slaveId 重复 ``` 创建用户 ```sql CREATE USER canal IDENTIFIED BY 'canal'; GRANT SELECT, REPLICATION SLAVE, REPLICATION CLIENT ON *.* TO 'canal'@'%'; -- GRANT ALL PRIVILEGES ON *.* TO 'canal'@'%' ; FLUSH PRIVILEGES; ``` 如果是mysql8要运行以下命令, 这是由于mysql8与以前版本的mysql密码算法不同的原因 ```sql ALTER USER 'canal'@'%' IDENTIFIED WITH mysql_native_password by 'canal'; ALTER USER 'canal'@'%' IDENTIFIED BY 'canal' PASSWORD EXPIRE NEVER; FLUSH PRIVILEGES; ``` 根据快速开始,选择下载版本,1.1.8 修改配置 conf/example/instance.properties ```properties ## mysql serverId canal.instance.mysql.slaveId = 1234 #position info,需要改成自己的数据库信息 canal.instance.master.address = 127.0.0.1:3306 canal.instance.master.journal.name = canal.instance.master.position = canal.instance.master.timestamp = #canal.instance.standby.address = #canal.instance.standby.journal.name = #canal.instance.standby.position = #canal.instance.standby.timestamp = #username/password,需要改成自己的数据库信息 canal.instance.dbUsername = canal canal.instance.dbPassword = canal canal.instance.defaultDatabaseName = canal.instance.connectionCharset = UTF-8 #table regex canal.instance.filter.regex = .\*\\\\..\* ``` **注意:只是修改配置文件中的部分配置,不需要全部覆盖,否则会导致无法连接** 依赖 选择对应版本的依赖 ```xml <dependency> <groupId>com.alibaba.otter</groupId> <artifactId>canal.client</artifactId> <version>1.1.8</version> </dependency> <dependency> <groupId>com.alibaba.otter</groupId> <artifactId>canal.protocol</artifactId> <version>1.1.8</version> </dependency> ``` ## 1.1 简单示例 [https://github.com/alibaba/canal/wiki/ClientExample](https://github.com/alibaba/canal/wiki/ClientExample) ```java import com.alibaba.otter.canal.client.CanalConnector; import com.alibaba.otter.canal.client.CanalConnectors; import com.alibaba.otter.canal.common.utils.AddressUtils; import com.alibaba.otter.canal.protocol.CanalEntry; import com.alibaba.otter.canal.protocol.Message; import java.net.InetSocketAddress; import java.util.List; public class SimpleCanalClientExample { public static void main(String args[]) { // 创建链接 CanalConnector connector = CanalConnectors.newSingleConnector(new InetSocketAddress(AddressUtils.getHostIp(), 11111), "example", "", ""); int batchSize = 1000; int emptyCount = 0; try { connector.connect(); connector.subscribe(".*\\..*"); connector.rollback(); int totalEmptyCount = 120; while (emptyCount < totalEmptyCount) { Message message = connector.getWithoutAck(batchSize);// 获取指定数量的数据 long batchId = message.getId(); int size = message.getEntries().size(); if (batchId == -1 || size == 0) { emptyCount++; System.out.println("empty count : " + emptyCount); try { Thread.sleep(1000); } catch (InterruptedException e) { } } else { emptyCount = 0; // System.out.printf("message[batchId=%s,size=%s] \n", batchId, size); List<CanalEntry.Entry> entries = message.getEntries(); printEntry(entries); } connector.ack(batchId); // 提交确认 // connector.rollback(batchId); // 处理失败, 回滚数据 } System.out.println("empty too many times, exit"); } finally { connector.disconnect(); } } private static void printEntry(List<CanalEntry.Entry> entrys) { for (CanalEntry.Entry entry : entrys) { if (entry.getEntryType() == CanalEntry.EntryType.TRANSACTIONBEGIN || entry.getEntryType() == CanalEntry.EntryType.TRANSACTIONEND) { continue; } CanalEntry.RowChange rowChage = null; try { rowChage = CanalEntry.RowChange.parseFrom(entry.getStoreValue()); } catch (Exception e) { throw new RuntimeException("ERROR ## parser of eromanga-event has an error , data:" + entry.toString(), e); } CanalEntry.EventType eventType = rowChage.getEventType(); System.out.println(String.format("================> binlog[%s:%s] , name[%s,%s] , eventType : %s", entry.getHeader().getLogfileName(), entry.getHeader().getLogfileOffset(), entry.getHeader().getSchemaName(), entry.getHeader().getTableName(), eventType)); for (CanalEntry.RowData rowData : rowChage.getRowDatasList()) { if (eventType == CanalEntry.EventType.DELETE) { printColumn(rowData.getBeforeColumnsList()); } else if (eventType == CanalEntry.EventType.INSERT) { printColumn(rowData.getAfterColumnsList()); } else { System.out.println("-------> before"); List<CanalEntry.Column> beforeColumnsList = rowData.getBeforeColumnsList(); printColumn(beforeColumnsList); System.out.println("-------> after"); printColumn(rowData.getAfterColumnsList()); } } } } private static void printColumn(List<CanalEntry.Column> columns) { for (CanalEntry.Column column : columns) { System.out.println(column.getName() + " : " + column.getValue() + " update=" + column.getUpdated()); } } } ``` # 2.与springboot集成 这里只实现了一个数据库的监控,监控所有数据库应该也时差不多,但是要注意数据库之间的实体类和Es文档映射的问题 可能只试用于 MySQL ---》Es ## 2.1. Es文档类 ```java package com.sakura.demo.datasync.modal.document; import lombok.Data; import org.springframework.data.annotation.Id; import org.springframework.data.elasticsearch.annotations.*; import java.math.BigDecimal; import java.util.Date; @Document(indexName = "product", createIndex = true) @Data public class ProductDocument { @Id private Long id; @MultiField( mainField = @Field( type = FieldType.Text, analyzer = "ik_max_word", // 索引时使用细粒度分词 searchAnalyzer = "ik_smart" // 搜索时使用智能分词 ), otherFields = { @InnerField( suffix = "keyword", type = FieldType.Keyword, ignoreAbove = 256 // 超过256字符的keyword会被忽略 ) } ) private String name; @MultiField( mainField = @Field( type = FieldType.Text, analyzer = "ik_max_word", searchAnalyzer = "ik_smart" ), otherFields = { @InnerField(suffix = "keyword", type = FieldType.Keyword, ignoreAbove = 256) } ) private String description; @Field(type = FieldType.Double) private BigDecimal price; // 忽略映射 @Field(index = false, store = true, type = FieldType.Date, format = {}, pattern = DATE_TIME_PATTERN) private Date updateTime; private static final String DATE_TIME_PATTERN = "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'"; } ``` ## 2.2. 编写配置类 秉承着 约定>配置>编码,因此我们要实现一个配置类,来配置canalClient ```java /** * canal配置 */ @Component @Data @ConfigurationProperties("canal") public class CanalProperties { /** * 端口 */ private Integer port; /** * 描述 */ private String destination; /** * 用户名 */ private String username; /** * 密码 */ private String password; /** * 批量大小 */ private Integer batchSize; /** * 过滤 */ private String filter; /** * 实体类包名前缀 */ private String basePackage; /** * 实体类后缀 */ private String clazzSuffix; } ``` Spring配置文件 ```yaml # canal 配置 canal: port: 11111 batch-size: 1000 destination: example # https://github.com/alibaba/canal/wiki/AdminGuide canal.instance.filter.regex配置 filter: "my_db\\..*" username: password: # base-package 和 clazz-suffix 要与你文档标注的类一致 # 例如你有一个 com.sakura.demo.datasync.modal.document.ProductDocument 类 # 那么 base-package:com.sakura.demo.datasync.modal.document # clazz-suffix: Document base-package: com.sakura.demo.datasync.modal.document clazz-suffix: Document ``` config类 ```java /** * Canal配置 */ @Slf4j @Configuration public class CanalConfig { @Bean(destroyMethod = "disconnect") public CanalConnector canalConnector(CanalProperties properties) { log.info("Canal配置初始化"); return CanalConnectors.newSingleConnector(new InetSocketAddress(AddressUtils.getHostIp(), properties.getPort()), properties.getDestination(), properties.getUsername(), properties.getPassword()); } } ``` **特别说明:** 1. **base-package 和 clazz-suffix 要与你文档标注的类一致。** 2. **例如你有一个 com.sakura.demo.datasync.modal.document.ProductDocument 类** 1. **那么 base-package:com.sakura.demo.datasync.modal.document** 2. **clazz-suffix: Document** ## 2.3. Canal监听数据服务 为了方便查看,差分成下面几个代码模块 ```java import com.alibaba.otter.canal.client.CanalConnector; import com.alibaba.otter.canal.protocol.CanalEntry; import com.alibaba.otter.canal.protocol.Message; import com.sakura.demo.datasync.modal.properties.CanalProperties; import com.sakura.demo.datasync.utils.StringUtils; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.DisposableBean; import org.springframework.data.elasticsearch.core.ElasticsearchRestTemplate; import org.springframework.stereotype.Component; import javax.annotation.PostConstruct; import javax.annotation.Resource; import java.lang.reflect.Field; import java.math.BigDecimal; import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.Date; import java.util.List; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; @Slf4j @Component public class CanalDataSyncService implements DisposableBean { @Resource private CanalConnector canalConnector; @Resource private CanalProperties canalProperties; @Resource private ElasticsearchRestTemplate restTemplate; private ExecutorService executorService; @PostConstruct public void init() { // 初始化线程池 executorService = Executors.newSingleThreadExecutor(); // 提交任务到线程池 executorService.submit(this::process); log.info("Canal 开始监控数据改变"); } private void process() { canalConnector.connect(); canalConnector.subscribe(canalProperties.getFilter()); while (true) { Message message = canalConnector.get(100); List<CanalEntry.Entry> entries = message.getEntries(); // 处理数据 handlerEntries(entries); } } @Override public void destroy() throws Exception { // 关闭CanalConnector连接 canalConnector.disconnect(); log.info("CanalConnector已断开连接"); // 关闭线程池 if (executorService != null) { executorService.shutdown(); try { if (!executorService.awaitTermination(60, TimeUnit.SECONDS)) { executorService.shutdownNow(); } } catch (InterruptedException e) { executorService.shutdownNow(); } log.info("线程池已关闭"); } } } ``` handlerEntries方法 ```java private void handlerEntries(List<CanalEntry.Entry> entrys) { for (CanalEntry.Entry entry : entrys) { if (entry.getEntryType() == CanalEntry.EntryType.TRANSACTIONBEGIN || entry.getEntryType() == CanalEntry.EntryType.TRANSACTIONEND) { continue; } CanalEntry.RowChange rowChage = null; try { rowChage = CanalEntry.RowChange.parseFrom(entry.getStoreValue()); } catch (Exception e) { throw new RuntimeException("ERROR ## parser of eromanga-event has an error , data:" + entry.toString(), e); } CanalEntry.EventType eventType = rowChage.getEventType(); log.info("================> binlog[{}:{}] , name[{},{}] , eventType : {}", entry.getHeader().getLogfileName(), entry.getHeader().getLogfileOffset(), entry.getHeader().getSchemaName(), entry.getHeader().getTableName(), eventType); String tableName = entry.getHeader().getTableName(); // 表面对应实体类名称 // 这里利用反射拿到对应的实体类 Class<?> clazz = null; Object syncObj = null; try { String clazzName = StringUtils.toPascalCase(tableName); clazz = Class.forName(canalProperties.getBasePackage() + "." + clazzName + canalProperties.getClazzSuffix()); log.info("获取到的实体类:{}", clazz); syncObj = clazz.getDeclaredConstructor().newInstance(); } catch (Exception e) { log.error("获取实体类失败,表名:{}", tableName, e); continue; // 跳过当前条目,继续处理下一个 } for (CanalEntry.RowData rowData : rowChage.getRowDatasList()) { handleRowData(rowData, eventType, clazz, syncObj); } } } ``` handleRowData方法 ```java /** * 处理行数据 * @param rowData 行数据 * @param eventType 事件类型 * @param clazz 反射类 * @param syncObj 同步实体对象 */ private void handleRowData(CanalEntry.RowData rowData, CanalEntry.EventType eventType, Class<?> clazz, Object syncObj) { if (eventType == CanalEntry.EventType.DELETE) { // 处理删除操作 List<CanalEntry.Column> beforeColumnsList = rowData.getBeforeColumnsList(); // 只需要拿到对应的id就好 handlerColumn(beforeColumnsList, clazz, syncObj); restTemplate.delete(syncObj); } else { // 处理插入/更新操作 List<CanalEntry.Column> afterColumnsList = rowData.getAfterColumnsList(); handlerColumn(afterColumnsList, clazz, syncObj); restTemplate.save(syncObj); } } ``` handlerColumn方法 ```java /** * 处理列数据 * @param columns 列数据 * @param clazz 反射类 * @param syncObj 同步实体对象 */ private void handlerColumn(List<CanalEntry.Column> columns, Class<?> clazz, Object syncObj) { for (CanalEntry.Column column : columns) { log.info("{} : {} update={}", column.getName(), column.getValue(), column.getUpdated()); try { Field field = clazz.getDeclaredField(StringUtils.toCamelCase(column.getName())); field.setAccessible(true); // 根据字段类型设置值 setFieldValue(field, syncObj, column.getValue()); } catch (Exception e) { log.error("设置实体类属性失败,字段名:{}", column.getName(), e); } } } /** * 处理字段映射 * @param field 字段 * @param obj 实体对象 * @param value 字段值 * @throws IllegalAccessException */ private static void setFieldValue(Field field, Object obj, String value) throws IllegalAccessException { Class<?> fieldType = field.getType(); if (fieldType == String.class) { field.set(obj, value); } else if (fieldType == int.class || fieldType == Integer.class) { field.set(obj, Integer.parseInt(value)); } else if (fieldType == long.class || fieldType == Long.class) { field.set(obj, Long.parseLong(value)); } else if (fieldType == double.class || fieldType == Double.class) { field.set(obj, Double.parseDouble(value)); } else if (fieldType == boolean.class || fieldType == Boolean.class) { field.set(obj, Boolean.parseBoolean(value)); } else if (fieldType == Date.class) { SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); try { field.set(obj, dateFormat.parse(value)); } catch (ParseException e) { log.error("无法解析日期值: {}", value, e); throw new IllegalArgumentException("无法解析日期值: " + value, e); } } else if (fieldType == BigDecimal.class) { field.set(obj, new BigDecimal(value)); } else { throw new IllegalArgumentException("Unsupported field type: " + fieldType); } } ``` [Gitee地址:https://gitee.com/kk-2049/learn-dem](https://gitee.com/kk-2049/learn-demo) [Github地址:https://github.com/modakai/learn-demo](https://github.com/modakai/learn-demo) 有些地方可能写的不太好,望见谅和指正
聚合搜索平台
<h1 id="B4eLZ">聚合搜索平台笔记</h1> 我们希望做一个通用的搜索功能,用户可以通过一个页面请求多种不同的数源然后聚合返回。如下图搜索“小黑子”可以获取相关的网页,图片,视频等等。相比于普通的搜索能提升用户搜素的效率与体验。  <h2 id="YToex">业务逻辑</h2> <h2 id="I02NS">流程图</h2>  <h2 id="w8CHh">技术栈</h2> 前端: Ant Design vue Axios Lodash 后端: SpringBoot MyBaties ElasticSerch 数据同步 <h2 id="bAMoA">项目初始化</h2> 前端: [快速上手 - Ant Design Vue (antdv.com)](https://www.antdv.com/docs/vue/getting-started-cn) 后端: 拉取项目模板springboot-init,修改配置后启动即可 <h2 id="zgWbH">2024-09-11</h2> <h3 id="X7MtA">前端开发</h3> 整合vue-router 利用组件开发搜索页面:[快速上手 - Ant Design Vue (antdv.com)](https://www.antdv.com/docs/vue/getting-started-cn) 整合Axios: [Axios中文文档 | Axios中文网 (axios-http.cn)](https://www.axios-http.cn/) 记录查询状态 目标:用url记录搜索状态当刷新后可以恢复到之前的搜索状态,类似b站搜索。 双向绑定 =》 单项绑定,靠url来改变状态,舍弃状态改变url。 <h4 id="qEU3A">todo</h4> 刷新后页面动态路由参数获取不到。 <h2 id="tVQQH">2024-09-13</h2> <h3 id="B2nDr">获取不同数据源</h3> 帖子 =》 系统内部 用户 =》 系统内部 图片 =》 系统内部没有,可以尝试去互联网获取 <h3 id="Nh5Ev">抓取数据</h3> + 通过前端调试器获取接口信息,构建参数直接调用获取数据 + 获取浏览器渲染出来的页面,根据页面来解析数据 + 有一些动态加载的数据如,需要点击某一个按钮,或输入验证码才能显示数据的,可以使用一些无头浏览器 **注:爬虫不可乱用,小心包吃包住** 图片我们系统内部没有所以需要到网络上获取。 流程图:  使用jsoup库通过解析页面来获取图片数据 [jsoup: Java HTML parser, built for HTML editing, cleaning, scraping, and XSS safety](https://jsoup.org/) <h3 id="OJXXD">前后端联调</h3> 后端聚合多个接口,通过类型来区分查询。 前端页面加载和点击搜索时查询所有页面的第一页数据,翻页时根据类型进行查询 <h3 id="XxYUJ">门面模式</h3> 之后我们的数据源可能会越来越多,前端如果还是每多一个数据源就再前端再写一个方法查询,既影响性能又不便于代码的维护。所以需要利用门面模式来解决这个问题。 门面模式就是抽象一个接收请求的模块并根据接收请求的参数进行响应的逻辑处理,使我们不需要关注具体的逻辑。例如酒店的前台,我们需要房间时不会说自己一个一个去找,而是请求酒店的前台帮我们寻找房间。 <h3 id="DMwX9">适配器模式</h3> 适配器模式的作用是通过适配调用参数并不适配的方法。例如插座,充电器,数据线的关系,插座和数据线因为不适配并不能连接,但通过充电器的适配就可以让他们连接起来。 目前搜索是通过switch根据类型来搜索不同的数据源,但聚合多个接口后会导致代码臃肿,不利于维护。所以我们可以利用适配器模式来抽象查询的代码。 我们需要先定义一套接口的规范,要接入系统的数据源需要支持搜索和分页。 <h2 id="Qb8AD">2024-09-24</h2> <h3 id="phPT6">Elastic Stack (一套技术栈)</h3> 包含数据的整合 =》 提取 =》 存储 =》 使用 + beats: 从各种不同类型的文件/应用中采取数据 + logstash: 从采集器或数据源抽取数据/转换数据输送 比如向esc输送 + elasticsearch: 存储/查询数据 + kibana: 可视化查询es数据 <h3 id="bz9kr">ElasticSearch入门</h3> <h4 id="hykZ1">概念</h4> elasticsearch是一个存储服务,特点是搜索功能十分强大。相比于Mysql可以帮我们进行分词搜索非常灵活。 可以和mysql对比着理解 | mysql | es | | --- | --- | | 表 | 索引 | | | | **倒排索引** 正向索引: 可以理解为书的目录,根据目录去找文章内容 倒排索引: 理解为根据内容去找文章。 如何去找? 文章A : 你好,世界 文章B: 你好,code 切词 你好 世界 你好 code 构建倒排索引表 | 你好 | 文章A,文章B | | --- | --- | | 世界 | 文章A | | code | 文章B | 用户搜索时先将搜索内容进行切词,再到倒排索引表里去找 <h3 id="koQrk">Es的几种调用方式</h3> <h4 id="UI56M">restful请求 (http请求)</h4> GET http://localhost:9200/ 端口占用 + 9200 开放给外部调用的接口 + 9300 集群间内部通信的接口(不对外开放) <h4 id="cm8iz">kibana devtools</h4> 可以自由操作ES,本质页数resful请求。 不建议在生产环境使用 <h4 id="vYAQM">客户端调用</h4> java客户端等,各种语言的客户端 <h3 id="XlyC4">基础用法</h3> 可以跟着文档过 [Quick start | Elasticsearch Guide [7.17] | Elastic](https://www.elastic.co/guide/en/elasticsearch/reference/7.17/getting-started.html) ```json //创建索引 PUT user/_doc { "name": "无敌", "age": 18 } //插入数据 POST user/_doc { "name": "李四", "age": 18 } //查询 GET user/_search //修改 POST user/_doc/{id} { "name": "李四", "age": 19 } //删除 DELETE user ``` <h3 id="cCMoj">Es语法</h3> **DSL** json格式,适配resful,简单易懂 **EQL** 专门查询ECS文档(标准指标文档)的数据,语法更加规范,适用于特定场景 **SQL** 学习成本低,sql需要解析效率较低 **<font style="color:rgb(33, 37, 41);">Painless scripting language</font>** <font style="color:rgb(33, 37, 41);">编程式取值,更加灵活,学习成本高</font> <font style="color:rgb(33, 37, 41);"></font> <h3 id="gwkui"><font style="color:rgb(33, 37, 41);">Mapping</font></h3> <font style="color:rgb(33, 37, 41);">类似于mysql中的表结构</font> <h3 id="jULDI">分词器</h3> Es自带的分词器不是很适配中文,但Es的分词器可以自定义并且支持插件。 下载ik分词器 [infinilabs/analysis-ik: 🚌 The IK Analysis plugin integrates Lucene IK analyzer into Elasticsearch and OpenSearch, support customized dictionary. (github.com)](https://github.com/infinilabs/analysis-ik) 尝试之后发现有时分词的效果不是我们想要的效果,可以通过配置词典来辅助分词。分词时会去看看词典里是否有这个词如果有就可以进行划分 <h3 id="z002T">打分机制</h3> Es查询出的数据的顺序是按照分值的高低来进行排序的,得分越高越靠前。 当内容与搜索词越相似分值越高。 例如: 我是一个小黑子 我是一个黑子 当我们搜索黑子时第二个的分值会比第一个的高,因为第二个更短 <h3 id="LIQgA">Java客户端</h3> + 官方提供的Java客户端API调用 配置方便,更新迭代快 + SpringBoot Data ElasticSearch调用 配置更方便,可根据方法名生成实现,也支持复杂的聚合搜索。 对比版本7.17对应SpringBoot Data ElasticSearch 4.4.X 配置yml ```json spring: elasticsearch: uris: http://localhost:9200 ``` <h4 id="NWL85">继承ElasticsearchRepository调用</h4> 提供了简单的crud方法,我们也可以按照他的方法名规范来写接口,框架会帮我们自动实现。十分便捷,适用于简单查询场景。 ```java public interface PostEsDao extends ElasticsearchRepository<PostEsDTO, Long> { /** * 根据用户名查询 * @param userId * @return */ List<PostEsDTO> findByUserId(Long userId); /** * 根据id和用户名查询 * @param id * @param userId * @return */ List<PostEsDTO> findByIdAndUserId(Long id,Long userId); } ``` <h4 id="ZrTmv">通过ElasticsearchRestTemplate调用</h4> 类似于Mybatis的queryWapper可以build各种条件进行查询,十分灵活,适用于复杂查询的场景 ```java boolQueryBuilder.filter(QueryBuilders.termQuery("isDelete", 0)); if (id != null) { boolQueryBuilder.filter(QueryBuilders.termQuery("id", id)); } if (notId != null) { boolQueryBuilder.mustNot(QueryBuilders.termQuery("id", notId)); } if (userId != null) { boolQueryBuilder.filter(QueryBuilders.termQuery("userId", userId)); } // 必须包含所有标签 if (CollectionUtils.isNotEmpty(tagList)) { for (String tag : tagList) { boolQueryBuilder.filter(QueryBuilders.termQuery("tags", tag)); } } // 包含任何一个标签即可 if (CollectionUtils.isNotEmpty(orTagList)) { BoolQueryBuilder orTagBoolQueryBuilder = QueryBuilders.boolQuery(); for (String tag : orTagList) { orTagBoolQueryBuilder.should(QueryBuilders.termQuery("tags", tag)); } orTagBoolQueryBuilder.minimumShouldMatch(1); boolQueryBuilder.filter(orTagBoolQueryBuilder); } // 按关键词检索 if (StringUtils.isNotBlank(searchText)) { boolQueryBuilder.should(QueryBuilders.matchQuery("title", searchText)); boolQueryBuilder.should(QueryBuilders.matchQuery("description", searchText)); boolQueryBuilder.should(QueryBuilders.matchQuery("content", searchText)); boolQueryBuilder.minimumShouldMatch(1); } // 按标题检索 if (StringUtils.isNotBlank(title)) { boolQueryBuilder.should(QueryBuilders.matchQuery("title", title)); boolQueryBuilder.minimumShouldMatch(1); } // 按内容检索 if (StringUtils.isNotBlank(content)) { boolQueryBuilder.should(QueryBuilders.matchQuery("content", content)); boolQueryBuilder.minimumShouldMatch(1); } // 排序 SortBuilder<?> sortBuilder = SortBuilders.scoreSort(); if (StringUtils.isNotBlank(sortField)) { sortBuilder = SortBuilders.fieldSort(sortField); sortBuilder.order(CommonConstant.SORT_ORDER_ASC.equals(sortOrder) ? SortOrder.ASC : SortOrder.DESC); } // 分页 PageRequest pageRequest = PageRequest.of((int) current, (int) pageSize); // 构造查询 NativeSearchQuery searchQuery = new NativeSearchQueryBuilder().withQuery(boolQueryBuilder) .withPageable(pageRequest).withSorts(sortBuilder).build(); SearchHits<PostEsDTO> searchHits = elasticsearchRestTemplate.search(searchQuery, PostEsDTO.class); ``` <h3 id="vI0Qg">数据同步</h3> 我们需要通过es来进行检索,肯定需要先有数据,现在我们的数据是存在数据库中的,所以要同步到es中。 数据同步时我们要选择以哪一个数据源为准,如果当数据不一致时我们要知道以哪个为准,这里我们是从mysql存到es,所以以mysql为准。 数据同步分为 **全量同步** 编写一个一次性任务将mysql数据查出,增加到es中 **增量同步** 当数据是动态的时候,比如增删改时也需要进行同步,如果将全部数据查出后再同步会很消耗性能,所以我们需要同步最近方式过改变的数据。 + 定时任务 比如每分钟同步一次,每次同步查询前五分钟修改过的数据。为了防止同步失败所以设置为5分钟,同时可以增加监控告警和补偿机制 + 数据双写 当我们增删mysql的数据时,同时向es中同步本次操作,需要用到事务 + logstash数据管道 logstash他可以从数据源或应用中获取数据处理后再输出到数据库和应用中。 [logstash文档](https://www.elastic.co/guide/en/logstash/7.17/first-event.html)  ```bash .\bin\logstash.bat -e "input { stdin { } } output { stdout {} }" ``` 可以通过配置来实现 ```plain input { jdbc { jdbc_driver_library => ""D:\apache-maven-3.8.1\mav_resp\mysql\mysql-connector-java\8.0.29\mysql-connector-java-8.0.29.jar"" jdbc_driver_class => "com.mysql.cj.jdbc.Driver" jdbc_connection_string => "jdbc:mysql://localhost:3306/common_search" jdbc_user => "root" jdbc_password => "root" parameters => { "favorite_artist" => "Beethoven" } schedule => "*/5 * * * * *" statement => "SELECT * from post where 1 = 1" } } output { stdout(codec => rubydebug) } ``` + canal订阅同步 canal是alibaba开源的一款订阅数据库binlog的组件。其原理是mysql开启binlog后操作数据库会向binlog中写入操作内容,然后我们监听这个日志文件的变化就可以拿到改变的内容。canal是伪装成了mysql服务的从节点,利用主节点同步从节点的binlog文件,解析出我们可以看懂的数据。 快速开始:[https://github.com/alibaba/canal/wiki/QuickStart](https://github.com/alibaba/canal/wiki/QuickStart)++ 优点: 实时更新,方便 缺点: 需要维护额外的组件,额外的学习成本 <h3 id="AVUDB">压力测试</h3> jmeter:[https://jmeter.apache.org/](https://jmeter.apache.org/) <h3 id="cdPbv">扩展</h3> 1. 搜索词高亮显示 <h3 id="EK1FN">项目总结</h3> MI-Search是一个通用的搜索组件,是为了优化用户体验可以让用户在一个页面查询到多种类型的数据,并且可以让开发人员不用重复开发搜索功能,提高开发效率。后端可以轻松的接入多个不同的数据源,复用搜索功能。并且通过elasticSearch实现了分词搜索,高亮显示的示例,还配置了案例将mysql帖子表数据同步到Es上进行搜索。 收获: 1. 理解门面模式,适配器模式,注册模式 2. 学习ElasticSearch概念,基本语法和SpringBoot Data ElastticSearch Java客户端,熟悉用法,有遗忘应该也可以凭借印象看文档使用。 3. 全量同步,增量同步的数据同步的四种方式 (定时,双写,logstash管道,canal订阅binlog) 4. 压力测试概念,工具 jmeter(主要就是查询多线程情况下接口的稳定,响应时长,qps)
作为java后端,没有写过vue代码。我想请问大家,聚合搜索平台项目中的前端代码大家会跟着一起写吗?对我来说还很吃力,很容易踩坑😩
