背景
本来在正好需要搜索服务的时候看到了瞬间暴起的 MeiliSearch 正好,自己的项目不需要ES那种重量级的搜索引擎,只是需要简单的一个搜索引擎,最好是轻量级的,单机都能跑的,这时候刚好看到火爆的 MeiliSearch 感觉很心动,开始用起来,结果显示很骨感。
错误
Index 错误
本想的是给每个 Document 类型设置一个Index 索引实体用于后续的增删改查,于是我在初始化的时候进行查询设置或者创建 Index,结果很不幸,序列化 + 模块化问题上来就行一拳又一圈:
try {
//如果不指定索引, 默认就使用表名称
index = client.getIndex(idx);
}catch (Exception e) {
log.info("client get index error:",e);
index = null;
}
if (Objects.isNull(index)) {
TaskInfo taskInfo = client.createIndex(idx);
index= client.getIndex(idx);
}
所以只能放弃这个功能想法,难受。
反射错误
首先是遇到的反射错误,在使用 MeiliSearch-SDK-Java 官方的 Java SDK 包来进行测试添加 Document ,结果在添加后,序列化响应信息就无法通过 JSON 序列化了,报错:属性字段是不可访问的,也就是 访问权限是私有的。
这个问题的原因是因为 Java 使用的 JDK 版本较为新,从 Java 9 开始出台了模块化系统,造成了各种模块化问题,由于我使用的是 Java 17 所以遇到五六个 模块化问题,只能在 Java 启动命令中加入参数:
--add-opens
java.base/java.lang=ALL-UNNAMED
--add-opens
java.base/java.lang.ref=ALL-UNNAMED
--add-opens
java.base/java.net=ALL-UNNAMED
--add-opens
java.base/sun.net.util=ALL-UNNAMED
--add-opens
java.base/java.io=ALL-UNNAMED
--add-opens
java.base/jdk.internal.ref=ALL-UNNAMED
非常的难受。
序列化错误
同样还是想查询Document 通过 官方SDK 里面的 SearchRequest 实体来承载查询实体,因为已经没法使用 SDK了,所以我只能采用自己发送 HTTP 请求来实现文档添加,查询,添加没有问题,但是在查询的时候:
@Override
public List<T> query(String query) {
SearchResult<T> tSearchResult = new SearchResult<>();
try{
// 构建url:
String url = this.config.getHostUrl() + "/indexes/" + this.indexName + "/search";
SearchRequest sr = SearchRequest.builder().q(query).build();
ResponseEntity<SearchResult> response = this.restTemplate.postForEntity(url, sr, SearchResult.class);
tSearchResult = response.getBody();
}catch(Exception e){
e.printStackTrace();
}
return tSearchResult.getHits();
}
如果你像这样写,即使你传递了官方示例里面的q 参数,看似其他的查询参数都有默认值,但是我要告述诉你这样将会给你报错:400 Bad Request, 说你的其他参数比如 offset, page, limit, mathing 等需要一个默认值,但是为空,无奈,去翻看源码,发现官方SDK 里面还真没给默认值,关键是 Meili Search 官方文档教程里面说这些是有默认值的,并且通过 URL 链接是可以查询的,无奈,我只能将全部的属性字段设置上,但是恶心的还在后面。
枚举序列化错误
先来看 SearchRequest 的属性字段
public class SearchRequest {
private String q;
private Integer offset;
private Integer limit;
private String[] attributesToRetrieve;
private String[] attributesToCrop;
private Integer cropLength;
private String cropMarker;
private String highlightPreTag;
private String highlightPostTag;
private MatchingStrategy matchingStrategy;
private String[] attributesToHighlight;
private String[] filter;
private String[][] filterArray;
private Boolean showMatchesPosition;
private String[] facets;
private String[] sort;
protected Integer page;
protected Integer hitsPerPage;
}
请仔细看MatchingStrategy matchingStrategy; 这个属性,这里是用枚举作为属性字段的,而官方示例使用的字符串:可选值有:last, all, 而且文档也说这里有默认值:last ,但是其实SDK里面压根就没有设置,当我自己手动设置上以后非常抱歉, 序列化错误:matchingStrategy 当前值为:LAST,但是需要的值是last 和 all 其中一个,快要崩溃了,这里就连MeiliSearch 官方SDK使用的 Gson 序列化库都将枚举序列化错了,把 NAME 字段当作 Value 来序列化了。
public enum MatchingStrategy {
ALL("all"),
LAST("last");
public final String matchingStrategy;
private MatchingStrategy(String matchingStrategy) {
this.matchingStrategy = matchingStrategy;
}
}
使用完全的自定义
没办法,遇到了整个 官方SDK 都没法使用的情况,只能寄希望于使用全盘的 HTTP 请求方式,纯手动进行构造请求,封装响应,解析响应,于是我的查询代码就成了一堆 HTTP 请求处理:
@Override
public List<T> query(SearchDocumentRequest searchRequest) {
List<T> list = new ArrayList<>();
try {
// 构建url:
String url = this.config.getHostUrl() + "/indexes/" + this.indexName + "/search";
// 创建Content-Type头为JSON
MediaType mediaType = MediaType.parse("application/json; charset=utf-8");
OkHttpClient client = new OkHttpClient();
// 根据ContentType构建请求体
RequestBody body = RequestBody.create(mediaType, JSON.toJSONString(searchRequest));
Request request = new Request.Builder()
.url(url)
.post(body) // 设置请求体
.build();
Response response = client.newCall(request).execute();
String string = response.body().string();
JSONObject jsonObject = JSON.parseObject(string);
JSONArray hitsArray = jsonObject.getJSONArray("hits");
List<Object> objects = hitsArray.stream().map(map -> {
Object o = new Object();
try {
BeanUtils.populate(o, (Map<String, ? extends Object>) map);
} catch (Exception e) {
e.printStackTrace();
}
return o;
}).toList();
list = objects.stream().map(o -> (T) o).toList();
}catch (Exception e) {
log.info("search the document:{}, by request failed, parameters={}, cause=", clazz.getName(), searchRequest, e);
}
return list;
}
这里我使用的是 OKHttp 因为 MeiliSearch 的底层也是使用的 OkHttp 为了统一,减少不必要的依赖,我还是继续使用这个。但是你可以看看代码:
@Override
public List<T> query(SearchDocumentRequest searchRequest) {
List<T> list = new ArrayList<>();
try {
// 构建url:
String url = this.config.getHostUrl() + "/indexes/" + this.indexName + "/search";
// 创建Content-Type头为JSON
MediaType mediaType = MediaType.parse("application/json; charset=utf-8");
OkHttpClient client = new OkHttpClient();
// 根据ContentType构建请求体
RequestBody body = RequestBody.create(mediaType, JSON.toJSONString(searchRequest));
Request request = new Request.Builder()
.url(url)
.post(body) // 设置请求体
.build();
Response response = client.newCall(request).execute();
String string = response.body().string();
JSONObject jsonObject = JSON.parseObject(string);
JSONArray hitsArray = jsonObject.getJSONArray("hits");
List<Object> objects = hitsArray.stream().map(map -> {
Object o = new Object();
try {
BeanUtils.populate(o, (Map<String, ? extends Object>) map);
} catch (Exception e) {
e.printStackTrace();
}
return o;
}).toList();
for (Object object : objects) {
list.add((T) object);
}
}catch (Exception e) {
log.info("search the document:{}, by request failed, parameters={}, cause=", clazz.getName(), searchRequest, e);
}
return list;
}
因为响应的数据结构复杂,不得不搞怎么复杂,其实也有简单的通过 gosn.decode() 方式直接转换,但是很遗憾我这里要报错。
最后
看到这庞大的复杂的调试,我最后决定放弃了,改为采用 RedisSearch 做全文搜索了。
MeiliSearch 作为一个国外开源的技术,为什么没有遵守国外对于高版本Java的一贯良好的支持性呢?