一、Meilisearch与Easy Search点击进入官网了解,本文主要从小微型公司业务出发,选择meilisearch来作为项目的全文搜索引擎,还可以当成来mongodb来使用。
二、starter封装
1、项目结构展示
2、引入依赖包(我是有包统一管理的fastjson用的1.2.83,gson用的2.8.6)
<dependencies>
<dependency>
<groupId>cn.iocoder.boot</groupId>
<artifactId>yudao-common</artifactId>
</dependency>
<!-- meilisearch 轻量级搜索 -->
<!-- https://mvnrepository.com/artifact/com.meilisearch.sdk/meilisearch-java -->
<dependency>
<groupId>com.meilisearch.sdk</groupId>
<artifactId>meilisearch-java</artifactId>
<version>0.11.2</version>
</dependency>
<!-- meilisearch 有bug:查询时不能用gosn把string转换成LocalDateTime,只有一处使用,其他的全用gson,尝试全用fastjson 各章报错 -->
<!-- https://mvnrepository.com/artifact/com.alibaba.fastjson2/fastjson2 -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
</dependency>
<!-- meilisearch 内部json的转换依赖,我们用fastjson会报各种报错 -->
<!-- https://mvnrepository.com/artifact/com.google.code.gson/gson -->
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
</dependency>
<!-- Web 相关 -->
<dependency>
<groupId>cn.iocoder.boot</groupId>
<artifactId>yudao-spring-boot-starter-web</artifactId>
<scope>provided</scope> <!-- 设置为 provided,只有 OncePerRequestFilter 使用到 -->
</dependency>
</dependencies>
3、yml参数读取代码参考
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.validation.annotation.Validated;
/**
* MeiliSearch 自动装配参数类
* 2023年9月21日
*/
@ConfigurationProperties("yudao.meilisearch")
@Data
@Validated
public class MeiliSearchProperties {
/**
* 主机地址
*/
private String hostUrl = "";
/**
* 接口访问标识
*/
private String apiKey = "123456";
}
4、自动配置类代码参考
import com.meilisearch.sdk.Client;
import com.meilisearch.sdk.Config;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Bean;
import javax.annotation.Resource;
/**
* MeiliSearch 自动装配类
* 2023年9月21日
*/
@AutoConfiguration
@EnableConfigurationProperties({MeiliSearchProperties.class})
@EnableCaching
public class MeiliSearchAutoConfiguration {
@Resource
MeiliSearchProperties properties;
@Bean
@ConditionalOnMissingBean(Client.class)
Client client() {
return new Client(config());
}
@Bean
@ConditionalOnMissingBean(Config.class)
Config config() {
return new Config(properties.getHostUrl(), properties.getApiKey());
}
}
5、数据处理类参考
import java.util.List;
/**
* MeiliSearch json解析类
* 2023年9月21日
*/
public class JsonHandler {
private com.meilisearch.sdk.json.JsonHandler jsonHandler = new MyGsonJsonHandler();
public <T> SearchResult<T> resultDecode(String o, Class<T> clazz) {
Object result = null;
try {
result = jsonHandler.decode(o, SearchResult.class, clazz);
} catch (Exception e) {
e.printStackTrace();
}
return result == null ? null : (SearchResult<T>) result;
}
public <T> List<T> listDecode(Object o, Class<T> clazz) {
Object list = null;
try {
list = jsonHandler.decode(o, List.class, clazz);
} catch (Exception e) {
e.printStackTrace();
}
return list == null ? null : (List<T>) list;
}
public String encode(Object o) {
try {
return jsonHandler.encode(o);
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
public <T> T decode(Object o, Class<T> clazz) {
T t = null;
try {
t = jsonHandler.decode(o, clazz);
} catch (Exception e) {
e.printStackTrace();
}
return t;
}
}
6、MyGsonJsonHandler类改写参考(yyyy-MM-dd'T'HH:mm:ss'Z' 时间格式可以自行修改)
import com.alibaba.fastjson.JSON;
import com.google.gson.*;
import com.google.gson.reflect.TypeToken;
import com.meilisearch.sdk.exceptions.JsonDecodingException;
import com.meilisearch.sdk.exceptions.JsonEncodingException;
import com.meilisearch.sdk.exceptions.MeilisearchException;
import com.meilisearch.sdk.model.Key;
import java.lang.reflect.Type;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
public class MyGsonJsonHandler implements com.meilisearch.sdk.json.JsonHandler {
private Gson gson;
public MyGsonJsonHandler() {
this.gson = new Gson();
}
public MyGsonJsonHandler(Gson gson) {
this.gson = gson;
}
public String encode(Object o) throws MeilisearchException {
if (o != null && o.getClass() == String.class) {
return (String) o;
} else {
if (o != null && o.getClass() == Key.class) {
GsonBuilder builder = new GsonBuilder();
this.gson = builder.setDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'").create();
Key key = (Key) o;
if (key.getExpiresAt() == null) {
JsonElement jsonElement = this.gson.toJsonTree(o);
JsonObject jsonObject = jsonElement.getAsJsonObject();
jsonObject.add("expiresAt", JsonNull.INSTANCE);
o = jsonObject;
this.gson = builder.serializeNulls().setDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'").create();
}
}
try {
return this.gson.toJson(o);
} catch (Exception var6) {
throw new JsonEncodingException(var6);
}
}
}
public <T> T decode(Object o, Class<?> targetClass, Class<?>... parameters) throws MeilisearchException {
if (o == null) {
throw new JsonDecodingException("Response to deserialize is null");
} else if (targetClass == String.class) {
return (T) o;
} else {
try {
if (parameters != null && parameters.length != 0) {
TypeToken<?> parameterized = TypeToken.getParameterized(targetClass, parameters);
Type type = parameterized.getType();
String string = o.toString().replace("\\", "");
// 创建一个GsonBuilder,用于处理LocalDateTime格式
GsonBuilder gsonBuilder = new GsonBuilder();
gsonBuilder.registerTypeAdapter(LocalDateTime.class, (JsonDeserializer<LocalDateTime>) (json, typeOfT, context) ->
LocalDateTime.parse(json.getAsJsonPrimitive().getAsString(),
DateTimeFormatter.ISO_DATE_TIME));
Gson mgson = gsonBuilder.create();
return (T) mgson.fromJson(string, type);
// return (T) JSON.parseObject(string, type);
} else {
return (T) JSON.parseObject((String) o, targetClass);
}
} catch (JsonSyntaxException var5) {
throw new JsonDecodingException(var5);
}
}
}
}
7、自定义注解代码参考
import java.lang.annotation.*;
/**
* MeiliSearch
* 2023年9月21日
*/
@Documented
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface MSFiled {
/**
* 是否开启过滤
*/
boolean openFilter() default false;
/**
* 是否不展示
*/
boolean noDisplayed() default false;
/**
* 是否开启排序
*/
boolean openSort() default false;
/**
* 处理的字段名
*/
String key() ;
}
import java.lang.annotation.*;
/**
* MeiliSearch
* 2023年9月21日
*/
@Documented
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface MSIndex {
/**
* 索引
*/
String uid() default "";
/**
* 主键
*/
String primaryKey() default "";
/**
* 分类最大数量
*/
int maxValuesPerFacet() default 100;
/**
* 单次查询最大数量
*/
int maxTotalHits() default 1000;
}
8、返回结果解析类参考
import java.util.List;
/**
* MeiliSearch
* 2023年9月21日
*/
public class SearchResult<T> {
private String query;
private long offset;
private long limit;
private long processingTimeMs;
private long nbHits;
private long hitsPerPage;
private long page;
private long totalPages;
private long totalHits;
private boolean exhaustiveNbHits;
private List<T> hits;
public String getQuery() {
return query;
}
public void setQuery(String query) {
this.query = query;
}
public long getOffset() {
return offset;
}
public void setOffset(long offset) {
this.offset = offset;
}
public long getLimit() {
return limit;
}
public void setLimit(long limit) {
this.limit = limit;
}
public long getProcessingTimeMs() {
return processingTimeMs;
}
public void setProcessingTimeMs(long processingTimeMs) {
this.processingTimeMs = processingTimeMs;
}
public long getNbHits() {
return nbHits;
}
public void setNbHits(long nbHits) {
this.nbHits = nbHits;
}
public boolean isExhaustiveNbHits() {
return exhaustiveNbHits;
}
public void setExhaustiveNbHits(boolean exhaustiveNbHits) {
this.exhaustiveNbHits = exhaustiveNbHits;
}
public List<T> getHits() {
return hits;
}
public void setHits(List<T> hits) {
this.hits = hits;
}
@Override
public String toString() {
return "SearchResult{" +
"query='" + query + '\'' +
", offset=" + offset +
", limit=" + limit +
", processingTimeMs=" + processingTimeMs +
", nbHits=" + nbHits +
", exhaustiveNbHits=" + exhaustiveNbHits +
", hits=" + hits +
'}';
}
public long getHitsPerPage() {
return hitsPerPage;
}
public void setHitsPerPage(long hitsPerPage) {
this.hitsPerPage = hitsPerPage;
}
public long getPage() {
return page;
}
public void setPage(long page) {
this.page = page;
}
public long getTotalPages() {
return totalPages;
}
public void setTotalPages(long totalPages) {
this.totalPages = totalPages;
}
public long getTotalHits() {
return totalHits;
}
public void setTotalHits(long totalHits) {
this.totalHits = totalHits;
}
}
9、基础操作接口封装
import cn.iocoder.yudao.framework.meilisearch.json.SearchResult;
import com.meilisearch.sdk.SearchRequest;
import com.meilisearch.sdk.model.Settings;
import com.meilisearch.sdk.model.Task;
import com.meilisearch.sdk.model.TaskInfo;
import java.util.List;
/**
* MeiliSearch 基础接口
* 2023年9月21日
*/
interface DocumentOperations<T> {
T get(String identifier);
List<T> list();
List<T> list(int limit);
List<T> list(int offset, int limit);
long add(T document);
long update(T document);
long add(List<T> documents);
long update(List<T> documents);
long delete(String identifier);
long deleteBatch(String... documentsIdentifiers);
long deleteAll();
SearchResult<T> search(String q);
SearchResult<T> search(String q, int offset, int limit);
SearchResult<T> search(SearchRequest sr);
Settings getSettings();
TaskInfo updateSettings(Settings settings);
TaskInfo resetSettings();
Task getUpdate(int updateId);
}
10、基本操作实现
import cn.hutool.core.util.ObjectUtil;
import cn.iocoder.yudao.framework.meilisearch.json.JsonHandler;
import cn.iocoder.yudao.framework.meilisearch.json.MSFiled;
import cn.iocoder.yudao.framework.meilisearch.json.MSIndex;
import cn.iocoder.yudao.framework.meilisearch.json.SearchResult;
import com.alibaba.fastjson.JSON;
import com.google.gson.Gson;
import com.meilisearch.sdk.Client;
import com.meilisearch.sdk.Index;
import com.meilisearch.sdk.SearchRequest;
import com.meilisearch.sdk.model.*;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.util.StringUtils;
import javax.annotation.Resource;
import java.lang.reflect.Field;
import java.lang.reflect.ParameterizedType;
import java.util.*;
/**
* MeiliSearch 基本操作实现
* 2023年9月21日
*/
public class MeilisearchRepository<T> implements InitializingBean, DocumentOperations<T> {
private Index index;
private Class<T> tClass;
private JsonHandler jsonHandler = new JsonHandler();
@Resource
private Client client;
@Override
public T get(String identifier) {
T document;
try {
document = getIndex().getDocument(identifier, tClass);
} catch (Exception e) {
throw new RuntimeException(e);
}
return document;
}
@Override
public List<T> list() {
List<T> documents;
try {
documents = Optional.ofNullable(getIndex().getDocuments(tClass))
.map(indexDocument -> indexDocument.getResults())
.map(result -> Arrays.asList(result))
.orElse(new ArrayList<>());
} catch (Exception e) {
throw new RuntimeException(e);
}
return documents;
}
@Override
public List<T> list(int limit) {
List<T> documents;
try {
DocumentsQuery query = new DocumentsQuery();
query.setLimit(limit);
documents = Optional.ofNullable(index.getDocuments(query, tClass))
.map(indexDocument -> indexDocument.getResults())
.map(result -> Arrays.asList(result))
.orElse(new ArrayList<>());
} catch (Exception e) {
throw new RuntimeException(e);
}
return documents;
}
@Override
public List<T> list(int offset, int limit) {
List<T> documents;
try {
DocumentsQuery query = new DocumentsQuery();
query.setLimit(limit);
query.setOffset(offset);
documents = Optional.ofNullable(getIndex().getDocuments(query, tClass))
.map(indexDocument -> indexDocument.getResults())
.map(result -> Arrays.asList(result))
.orElse(new ArrayList<>());
} catch (Exception e) {
throw new RuntimeException(e);
}
return documents;
}
@Override
public long add(T document) {
List<T> list = Collections.singletonList(document);
return add(list);
}
@Override
public long update(T document) {
List<T> list = Collections.singletonList(document);
return update(list);
}
@Override
public long add(List documents) {
try {
if (ObjectUtil.isNotNull(documents)) {
String jsonString = JSON.toJSONString(documents);
if (ObjectUtil.isNotNull(jsonString)) {
TaskInfo taskInfo = getIndex().addDocuments(jsonString);
if (ObjectUtil.isNotNull(taskInfo)) {
return taskInfo.getTaskUid();
}
}
}
} catch (Exception e) {
throw new RuntimeException(documents.toString(), e);
}
return 0;
}
@Override
public long update(List documents) {
int updates;
try {
updates = getIndex().updateDocuments(new Gson().toJson(documents)).getTaskUid();
} catch (Exception e) {
throw new RuntimeException(e);
}
return updates;
}
@Override
public long delete(String identifier) {
int taskId;
try {
taskId = getIndex().deleteDocument(identifier).getTaskUid();
} catch (Exception e) {
throw new RuntimeException(e);
}
return taskId;
}
@Override
public long deleteBatch(String... documentsIdentifiers) {
int taskId;
try {
taskId = getIndex().deleteDocuments(Arrays.asList(documentsIdentifiers)).getTaskUid();
} catch (Exception e) {
throw new RuntimeException(e);
}
return taskId;
}
@Override
public long deleteAll() {
int taskId;
try {
taskId = getIndex().deleteAllDocuments().getTaskUid();
} catch (Exception e) {
throw new RuntimeException(e);
}
return taskId;
}
@Override
public cn.iocoder.yudao.framework.meilisearch.json.SearchResult<T> search(String q) {
String result;
try {
result = JSON.toJSONString(getIndex().search(q));
} catch (Exception e) {
throw new RuntimeException(e);
}
return jsonHandler.resultDecode(result, tClass);
}
@Override
public cn.iocoder.yudao.framework.meilisearch.json.SearchResult<T> search(String q, int offset, int limit) {
SearchRequest searchRequest = SearchRequest.builder()
.q(q)
.offset(offset)
.limit(limit)
.build();
return search(searchRequest);
}
// @Override
public cn.iocoder.yudao.framework.meilisearch.json.SearchResult<T> searchPage(String q) {
SearchRequest searchRequest = SearchRequest.builder()
.q(q)
.build();
return search(searchRequest);
}
@Override
public SearchResult<T> search(SearchRequest sr) {
String result;
try {
result = "";
if (ObjectUtil.isNotNull(sr)) {
if (ObjectUtil.isNull(getIndex())) {
initIndex();
}
Searchable search = getIndex().search(sr);
String jsonString = JSON.toJSONString(search);
result = JSON.toJSONString(jsonString);
}
} catch (Exception e) {
throw new RuntimeException(e);
}
// 特殊处理下json 不然不好解析,不太标准
if (ObjectUtil.isNotEmpty(result)&&result.length()>0) {
result = result.replace("\\", "");
result = result.substring(1, result.length() - 1);
}
return jsonHandler.resultDecode(result, tClass);
}
@Override
public Settings getSettings() {
try {
return getIndex().getSettings();
} catch (Exception e) {
throw new RuntimeException(e);
}
}
@Override
public TaskInfo updateSettings(Settings settings) {
try {
return getIndex().updateSettings(settings);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
@Override
public TaskInfo resetSettings() {
try {
return getIndex().resetSettings();
} catch (Exception e) {
throw new RuntimeException(e);
}
}
@Override
public Task getUpdate(int updateId) {
try {
return getIndex().getTask(updateId);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
@Override
public void afterPropertiesSet() throws Exception {
initIndex();
}
public Index getIndex() {
if (ObjectUtil.isNull(index)) {
try {
initIndex();
} catch (Exception e) {
throw new RuntimeException(e);
}
}
return index;
}
/**
* 初始化索引信息
*
* @throws Exception
*/
private void initIndex() throws Exception {
Class<? extends MeilisearchRepository> clazz = getClass();
tClass = (Class<T>) ((ParameterizedType) clazz.getGenericSuperclass()).getActualTypeArguments()[0];
MSIndex annoIndex = tClass.getAnnotation(MSIndex.class);
String uid = annoIndex.uid();
String primaryKey = annoIndex.primaryKey();
if (StringUtils.isEmpty(uid)) {
uid = tClass.getSimpleName().toLowerCase();
}
if (StringUtils.isEmpty(primaryKey)) {
primaryKey = "id";
}
int maxTotalHit = 1000;
int maxValuesPerFacet = 100;
if (Objects.nonNull(annoIndex.maxTotalHits())) {
maxTotalHit = annoIndex.maxTotalHits();
}
if (Objects.nonNull(annoIndex.maxValuesPerFacet())) {
maxValuesPerFacet = 100;
}
List<String> filterKey = new ArrayList<>();
List<String> sortKey = new ArrayList<>();
List<String> noDisPlay = new ArrayList<>();
//获取类所有属性
for (Field field : tClass.getDeclaredFields()) {
//判断是否存在这个注解
if (field.isAnnotationPresent(MSFiled.class)) {
MSFiled annotation = field.getAnnotation(MSFiled.class);
if (annotation.openFilter()) {
filterKey.add(annotation.key());
}
if (annotation.openSort()) {
sortKey.add(annotation.key());
}
if (annotation.noDisplayed()) {
noDisPlay.add(annotation.key());
}
}
}
Results<Index> indexes = client.getIndexes();
Index[] results = indexes.getResults();
Boolean isHaveIndex = false;
for (Index result : results) {
if (uid.equals(result.getUid())) {
isHaveIndex = true;
break;
}
}
if (isHaveIndex) {
client.updateIndex(uid, primaryKey);
this.index = client.getIndex(uid);
Settings settings = new Settings();
settings.setDisplayedAttributes(noDisPlay.size() > 0 ? noDisPlay.toArray(new String[noDisPlay.size()]) : new String[]{"*"});
settings.setFilterableAttributes(filterKey.toArray(new String[filterKey.size()]));
settings.setSortableAttributes(sortKey.toArray(new String[sortKey.size()]));
index.updateSettings(settings);
} else {
client.createIndex(uid, primaryKey);
}
}
}
11、指定自动配置类所在
12、项目有统一版本管理的设置下版本管理
二、项目引用
1、引入starter依赖(没有版本统一管理的要把version加上)
2、基本使用
2.1、建立索引(宽表)
import cn.iocoder.yudao.framework.meilisearch.json.MSFiled;
import cn.iocoder.yudao.framework.meilisearch.json.MSIndex;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
@MSIndex(uid = "com_baidu_main", primaryKey = "id")
public class MainDO {
private Long id;
private String seedsName;
@MSFiled(openFilter = true, key = "isDelete")
private Integer isDelete;
@MSFiled(openFilter = true, key = "status")
private Integer status;
@MSFiled(openFilter = true, key = "classFiledId")
private Integer classFiledId;
private String classFiledName;
@MSFiled(openFilter = true, key = "tags")
private List<TageInfo> tags;
@MSFiled(openFilter = true,key = "createTime",openSort = true)
@JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss'Z'")
private LocalDateTime createTime;
}
2.2、集成starter里边的mapper对milisearch进行基本操作
import cn.iocoder.yudao.framework.meilisearch.core.MeilisearchRepository;
import org.springframework.stereotype.Repository;
@Repository
public class MeiliSearchMapper extends MeilisearchRepository<MainDO> {
}
2.3、当mongodb实现精准分页查询
// 条件组装,实体类根据业务需要提前加好业务字段索引注解@MSFiled,显示隐藏 索引等
StringBuffer sb = new StringBuffer();
if (ObjectUtil.isNotEmpty(pageParam.getStartTime())) {
sb.append("createTime>=").append(pageParam.getStartTime()).append(" AND ");
}
if (ObjectUtil.isNotEmpty(pageParam.getEndTime())) {
sb.append("createTime<=").append(pageParam.getEndTime()).append(" AND ");
}
sb.append("userId=" + SecurityFrameworkUtils.getLoginUserId());
// 分页查询及排序
SearchRequest searchRequest4 = SearchRequest.builder()
.sort(new String[]{"createTime:desc"})
.page(pageParam.getPageNo())
.hitsPerPage(pageParam.getPageSize())
.filter(new String[]{sb.toString()}).build();
SearchResult<SeedsDO> search = meiliSearchMapper.search(searchRequest4);
return SeedCultivateConvert.INSTANCE.convert(search);
// SeedCultivateConvert.INSTANCE.convert是个类转化器可手动转换成分页的统一数据格式
pageResult.setList(search.getHits());
pageResult.setTotal(search.getTotalPages());
.......
2.4、其他基本使用文章来源:https://uudwc.com/A/Ev0d1
@Resource
private MeiliSearchMapper meiliSearchMapper;
//根据标签分页查询
SearchRequest searchRequest4 = SearchRequest.builder()
.limit(pageParam.getPageSize().intValue())
.sort(new String[]{"createTime:desc"})
.offset(pageParam.getPageNo().intValue() == 0 ? pageParam.getPageNo().intValue() : (pageParam.getPageNo().intValue() - 1) * pageParam.getPageSize().intValue())
.filter(new String[]{"tags.id=" + "10010" + " AND status=1 AND isDelete=0"}).build();
SearchResult<MainDO> search4 = meiliSearchMapper.search(searchRequest4);
//保存Or编辑
List<SeedsDO> articleCardDTOS = new ArrayList<>();
Boolean aBoolean = meiliSearchMapper.add(articleCardDTOS) > 0 ? Boolean.TRUE : Boolean.FALSE;
//按id删除
meiliSearchMapper.delete(String.valueOf(10085));
//根据类目分页查询
SearchRequest searchRequest3 = SearchRequest.builder()
.limit(pageParam.getPageSize().intValue())
.offset(pageParam.getPageNo().intValue() == 0 ? pageParam.getPageNo().intValue() : (pageParam.getPageNo().intValue() - 1) * pageParam.getPageSize().intValue())
.build();
StringBuffer sb1 = new StringBuffer();
sb.append("status =1 AND isDelete=0").append(" AND ").append("categoryId =").append(10086L);
searchRequest.setFilter(new String[]{sb.toString()});
searchRequest.setSort(new String[]{"createTime:desc"});
SearchResult<SeedsDO> search3 = meiliSearchMapper.search(searchRequest3);
文章来源地址https://uudwc.com/A/Ev0d1