1. 项目背景
工程文件中的价格数据需要保存归档,可在新的工程文件中参考历史价格。为了实现快速搜索,选择用elasticSearch保存和查询数据。
2. 版本1.0 httpClient封装
初次接触es,经了解es可以通过RESTful API with JSON over HTTP方式交互,项目需求相对简单,通过自行拼装api方式实现对es的操作,采用这种方式,可以快速实现需求。
该方式优点在于自由度高,学习成本较低,可以快速实现简单需求。缺点在于需要进行大量的json字符请求拼装,复杂操作封装难度较高,扩展性差,没有发挥java语言优势。
3. 版本2.0 springboot + jpa
1.0版本自行封装请求,进行了较多字符串拼接操作,实现难度较高,扩展困难,开发周期相对较长,因此考虑借用已有框架操作es。项目本身基于springboot,springboot支持对es的操作,所以2.0版本采用springboot+jpa的方式实现。
在项目中添加相应的Maven依赖,开始基本使用。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-elasticsearch</artifactId>
</dependency>
实体类
@Data
@NoArgsConstructor
public class BQItemDocument {
String code;
String description;
Double price;
String unit;
}
repository类
public interface CustomerRepository extends ElasticsearchRepository<BQItemDocument, String> {
public List<BQItemDocument> findByCode(String code);
public List<BQItemDocument> findByDescription(String description);
}
采用springboot+jpa方式实现简单,从编码层面该实现方式封装了es特性,采用jpa操作es和采用jpa操作MySQL、mongodb区别不大,对于熟悉jpa的人来说比较友好。持续使用一段时间后,发现该使用方式存在一定的问题。
- 版本问题
Spring Boot Version (x) | Spring Data Elasticsearch Version (y) | Elasticsearch Version (z) |
---|---|---|
x <= 1.3.5 | y <= 1.3.4 | z <= 1.7.2* |
x >= 1.4.x | 2.0.0 <=y < 5.0.0** | 2.0.0 <= z < 5.0.0** |
根据版本信息表,项目基于springboot 1.5.9版本,对应的es版本不能高于5,新版本es已更新到6.2+,不能使用新版的es的特性,例如deleteByQuery等,功能不全面,使用不方便
- entity设计和mapping映射问题
采用该方式需要进行实体类设计,需要针对文件提取部分重要字段形成实体记录,项目文件以json文件保存,无法枚举所有字段,提取实体类后源文件会有部分字段丢失,丢失字段当前需求不影响,但是当需求变更时,可能会存在数据缺失问题。项目对于es的定位为数据仓库,希望能保存所有数据已被不时之需。另外,采用注解方式定义字段类型时出现不生效情况,无法解决。
- 个性化查询问题
采用jpa方式进行查询时,进行简单查询相对容易,需要进行个性化的复杂查询时,实现比较复杂。
基于上述考量,2.0版本放弃。
4. 版本3.0 TransportClient
es基于java实现,使用java连接具有天生优势,java可以通过transportClient连接9300端口操作es,transportClient 提供了丰富的Java API,因此版本3.0出现。
在pom文件中引入相关依赖,依赖版本和使用的es对应即可。
<!-- elasticsearch -->
<dependency>
<groupId>org.elasticsearch</groupId>
<artifactId>elasticsearch</artifactId>
<version>${elasticsearch.version}</version>
</dependency>
<dependency>
<groupId>org.elasticsearch.client</groupId>
<artifactId>transport</artifactId>
<version>${elasticsearch.version}</version>
</dependency>
ES连接配置
- application.properties文件
# ElasticSearch配置
elasticsearch.host=127.0.0.1
elasticsearch.port=9300
elasticsearch.cluster-name=elasticsearch
- TransportClient配置类
@Configuration
public class ESConfig {
@Value("${elasticsearch.host}")
private String host;
@Value("${elasticsearch.port}")
private Integer port;
@Value("${elasticsearch.cluster-name}")
private String clusterName;
@Bean
public TransportClient transportClient() throws UnknownHostException {
Settings settings = Settings.builder().put("cluster.name", clusterName).build();
TransportClient client = new PreBuiltTransportClient(settings);
String[] hosts = Strings.splitStringByCommaToArray(host);
for (int i = 0; i < hosts.length; i++) {
TransportAddress hostNode = new TransportAddress(InetAddress.getByName(hosts[i]), port);
client.addTransportAddress(hostNode);
}
return client;
}
}
mapping配置方式
2.0版本中的mapping设置问题,在3.0版本中采用index template方式在进行定义,在创建新的index时定义好index中字段映射。
在项目resource文件夹下新建bqitem.json文件,设置关键字段映射类型,在项目启动时,检查index是否存在,不存在则依据temlate创建。
- temlate定义
{
"template":"bqitem",
"index_patterns":["bqitem_*"],
"mappings": {
"bqitem": {
"dynamic": true,
"properties": {
"code": {
"type": "keyword" },
"description": {
"type": "text" },
"descriptionOld": {
"type": "text" },
"spec": {
"type": "text" },
"ord": {
"type": "double" },
"date": {
"type": "date",
"format": "yyyy-MM-dd HH:mm:ss" }
}
}
}
}
- index初始化
@Component
public class InitESIndexCLRunner implements CommandLineRunner {
@Autowired
private TransportClient client;
@Value("${company.name}")
private String companyName;
@Override
public void run(String... args) throws Exception {
HashMap<String, String> indexConfigMap = buildIndexConfigMap();
CountDownLatch countDownLatch = new CountDownLatch(indexConfigMap.size());
for (HashMap.Entry<String, String> entry : indexConfigMap.entrySet()) {
new Thread(() -> {
checkAndCreateIndex(client.admin(), entry.getKey(), entry.getValue());
countDownLatch.countDown();
}).start();
}
try {
countDownLatch.await();
System.out.println("index mapping config inited");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
private HashMap<String, String> buildIndexConfigMap() {
HashMap<String, String> map = Maps.newHashMap();
map.put(getIndexName(ArchiveConsts.ITEM_INDEX_NAME), ArchiveConsts.ITEM_CONFIG_PATH);
map.put(getIndexName(ArchiveConsts.DB_ITEM_INDEX_NAME), ArchiveConsts.DB_ITEM_CONFIG_PATH);
return map;
}
private void checkAndCreateIndex(AdminClient adminClient, String index, String configPath) {
if (!existIndex(adminClient, index)) {
CreateIndexRequest createRequest = Requests.createIndexRequest(index);
try {
putTemplate(adminClient, index, configPath);
CreateIndexResponse createResponse = adminClient.indices().create(createRequest).get();
System.out.println(new StringBuilder().append(index).append(" mapping inited").toString());
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
}
private boolean existIndex(AdminClient adminClient, String index) {
IndicesExistsRequest existsRequest = Requests.indicesExistsRequest(index);
IndicesExistsResponse existsResponse = adminClient.indices().exists(existsRequest).actionGet();
return existsResponse.isExists();
}
private void putTemplate(AdminClient adminClient, String index, String path) throws IOException {
Resource res = new ClassPathResource(path);
if (!res.exists()) {
return;
}
InputStream inputStream = res.getInputStream();
String content = IOUtils.toString(inputStream, Charsets.UTF_8);
JSONObject jsonObject = JSONObject.parseObject(content);
PutIndexTemplateRequest putTplRequest = adminClient.indices().preparePutTemplate(index).request().source(jsonObject.toString(), XContentType.JSON);
PutIndexTemplateResponse response = adminClient.indices().putTemplate(putTplRequest).actionGet();
}
private String getIndexName(String index) {
if (!Strings.isNullOrEmpty(companyName) && !companyName.equals("company.name")) {
return index + "_" + companyName;
}
return index;
}
}
简单使用
service层方法:获取全部文档,进行业务逻辑处理
@Service
public class QueryServiceImpl {
@Autowired
private TransportClient esClient;
public String getAll(){
SearchRequestBuilder searchRequestBuilder = esClient.prepareSearch(getIndexName());
searchRequestBuilder.setScroll(new TimeValue(ESUtils.QueryScrollAliveTime)).setSize(ESUtils.QueryHitsSize);
SearchResponse response = searchRequestBuilder.get();
do {
for (SearchHit hit : response.getHits().getHits()) {
Map<String, Object> sourceMap = hit.getSourceAsMap();
//处理业务逻辑
}
response = esClient.prepareSearchScroll(response.getScrollId()).setScroll(new TimeValue(ESUtils.QueryScrollAliveTime)).execute().actionGet();
} while (response.getHits().getHits().length != 0);
return null;
}
}
至此,3.0版本完成基本的基础设置、mapping映射、demo验证,可以进行业务逻辑的实现了。
5. 总结
从确定需求到选型es后,在不断学习es文档,编码实现功能过程中,面对全新的技术,确实走过了比较多的弯路,踩不不少坑,得到一定的经验和教训。
- 快速试错,发现问题后快速验证,及时调整方向;
- 选择比努力重要,特别是基础工具的选择,对后续业务逻辑的实现具有重大影响;
- 对于新接触的技术,尽量使用新的版本,多看官方文档。
- 路漫漫其修远兮。。。