前一阵子准备为项目搭建一个简单的搜索服务, 虽然业务数据库 mongodb 提供了文本搜索的支持, 但是在大量文档需要通过关键词进行定位时, es 明显更加适合去作为一个搜索引擎 (虽然我们之前大部分使用到了 ELK 那套分析和可视化的特性).Elasticsearch 建立在 Lucene 之上并且支持极其快速的查询和丰富的查询语法, 偶尔也可以作为一个轻量级的 NoSQL. 但是对复杂查询和聚合操作的能力并不是很强.
本篇不会提及如何搭建一个简单搜索服务, 而是记录一下大约一周工作时间内遇见的几个坑..
为什么选择 elasticsearch 5.x?
新服务没有任何历史包袱, 理论上应该用最新的 6.x, 然而 spring-data-elasticsearch 只支持到的 5.x, 时间紧也无法很好直接封装一层 api, 也是因为 ELK 那套东西之前版本混乱, 无奈 es 从 2.x 直接到了 5.x. 查询一下 5.x 和 2.x 的差别, 简单说就是磁盘空间 - 50%, 索引时间 - 50%, 查询性能 + 25%.
由于 spring-data-elasticsearch 必须升级到 3.0.7, 导致 spring 必须升级到 2.x, 也直接导致了后面踩到的坑.
docker 安装 es 会默认安装 x-path plugin
虽然 spring-data 支持 es5.x, 但是功能并不非常完善, 因此如果安装了 x-path 插件, 需要引入 org.elasticsearch.client:x-pack-transport:5.5.0, 版本必须和 es 版本一致, 并且自己实现 TransportClient, 如下
- @Component
- public class ESconfig {
- @Bean
- public TransportClient transportClient() throws UnknownHostException {
- TransportClient client = new PreBuiltXPackTransportClient(Settings.builder()
- .put("cluster.name", "docker-cluster")
- .put("xpack.security.user", "elastic:changeme")
- .build())
- .addTransportAddress(new InetSocketTransportAddress(InetAddress.getByName("0.0.0.0"), 9300));
- return client;
- }
- }
这也是因为不想再到 docker 里去处理 x-path 这个插件而选择的一个比较快捷的解决方案, 没必要的情况下, 暂时也不用接触到 es 本身的一些东西.
mq 会保存 message 的 class 信息导致 deserialized 失败
一直没有提到标题中的 rabbitmq, 因为只是单纯的用它作为一个消息队列, 当数据发生变化时, 将消息 id 丢入 mq, 由 search 服务这边的 consumer 去消费.
问题就是在消息丢入 mq 时, 封装成了一个自己的 Object, 导致使用 rabbitTemplate.receiveAndConvert 时失败, 因为 message 会带着 Object 的 package 信息. 无奈之下, consumer 只能直接获取 queue 里的 message bytes, 用 ObjectMapper.readValue 的方法将 json 形式转换成一个 Object.
gradle 配置可以使用 - Dloader.main 指定启动函数
正是因为引入了 mq, 所以 search 服务需要启动一个 consumer, 用的方法是另外实现一个不启动 web 服务的 Application, 并且配置一个 SimpleMessageListenerContainer 和 MessageListenerAdapter 如下:
- @Bean
- SimpleMessageListenerContainer container(ConnectionFactory connectionFactory,
- MessageListenerAdapter listenerAdapter,
- MQconfig properties) {
- SimpleMessageListenerContainer container = new SimpleMessageListenerContainer();
- container.setConnectionFactory(connectionFactory);
- container.setQueueNames(properties.getQueueName());
- container.setMessageListener(listenerAdapter);
- return container;
- }
- @Bean
- MessageListenerAdapter listenerAdapter() {
- MessageListenerAdapter listenerAdapter = new MessageListenerAdapter(itemConsumer,
- "consume");
- return listenerAdapter;
- }
问题在于 gradle 配置的时候, 找了很久如何使得 build 出来的 jar 包可以指定 - Dloader.main 指定启动 Application, 解决方法如下:
在 xxx.gradle 文件里添加
- bootJar {
- manifest {
- attributes 'Main-Class': 'org.springframework.boot.loader.PropertiesLauncher'
- }
- }
在 springboot 1.5.9 的项目里, 需要指定启动 Application, 需要添加
- springBoot{
- layout = "ZIP"
- }
查看是否生效的办法是 build 以后 直接解压 jar 包, 在 xxx(项目名)/META-INFO/MANIFEST.MF 里查看, 如果
Main-Class: org.springframework.boot.loader.PropertiesLauncher
则正确, 如果
Main-Class: org.springframework.boot.loader.JarLauncher
则依旧会启动文件里的 Start-Class
es 无法修改 Index 的 mapping
由于只是单纯使用了 es 的文本检索功能, 导致实际应用时有许多搜索结果不尽如人意的地方, 比如搜索 "桌子", 无法搜索到 "电脑桌 / 办公桌" 等 xx 桌内容, 这样的情况还有很多. 因此加入了 synonym dictionary, 在需要分词的字段上不使用本身的 ik_smart 分词器, 这样某些字段的 mapping 需要改为
- // analyzer 是自己的分词器名字
- @Field(type = FieldType.Text, index = true, analyzer = "synonym")
- private String description;
由于 es 的 mapping 无法修改, 只能通过手动创建一个新的 mapping, 再通过 reIndex 方法去 backfill 数据 (es5.x 自带了 reIndex 的 api). 网上有通过 alias 的方法, 在某些修改场景下, 不需要重新启动 / 部署应用就可以平滑的修改 mapping, 具体可以查询了解一下.
以上差不多搭建一个搜索服务踩到的一些坑, 有几个消耗了大量时间和精力去解决, 在此列出来希望希望有借鉴意义. 之后搜索服务有优化的地方, 还会继续慢慢更新
来源: http://www.jianshu.com/p/9c60ed244878