必须要吐槽的Meili Search 的问题

1,055 阅读4分钟

背景

本来在正好需要搜索服务的时候看到了瞬间暴起的 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的一贯良好的支持性呢?