一、Meilisearch与Easy Search点击进入官网了解,本文主要从小微型公司业务出发,选择meilisearch来作为项目的全文搜索引擎,还可以当成来mongodb来使用。
二、starter封装
1、项目结构展示
2、引入依赖包
<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><!-- https://mvnrepository.com/artifact/com.alibaba.fastjson2/fastjson2 --><dependency><groupId>com.alibaba.fastjson2</groupId><artifactId>fastjson2</artifactId><version>2.0.40</version><scope>provided</scope></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 {@ResourceMeiliSearchProperties 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 com.meilisearch.sdk.json.GsonJsonHandler;import java.util.List;/*** MeiliSearch json解析类* 2023年9月21日*/
public class JsonHandler {private com.meilisearch.sdk.json.JsonHandler jsonHandler = new GsonJsonHandler();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;}
}import java.util.List;
import java.util.Map;/*** MeiliSearch* 2023年9月21日*/
public class MatchedBean<T> {private T _formatted;private Map<String, List<Matching>> _matchesInfo;public T get_formatted() {return _formatted;}public void set_formatted(T _formatted) {this._formatted = _formatted;}public Map<String, List<Matching>> get_matchesInfo() {return _matchesInfo;}public void set_matchesInfo(Map<String, List<Matching>> _matchesInfo) {this._matchesInfo = _matchesInfo;}private class Matching {long start;long length;public long getStart() {return start;}public void setStart(long start) {this.start = start;}public long getLength() {return length;}public void setLength(long length) {this.length = length;}}
}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 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;}@Overridepublic String toString() {return "SearchResult{" +"query='" + query + '\'' +", offset=" + offset +", limit=" + limit +", processingTimeMs=" + processingTimeMs +", nbHits=" + nbHits +", exhaustiveNbHits=" + exhaustiveNbHits +", hits=" + hits +'}';}
}
6、自定义注解代码参考
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;
}
7、基础操作接口封装
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);
}
8、基本操作实现
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.fastjson2.JSON;
import com.meilisearch.sdk.*;
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();@Resourceprivate Client client;@Overridepublic T get(String identifier) {T document;try {document = index.getDocument(identifier, tClass);} catch (Exception e) {throw new RuntimeException(e);}return document;}@Overridepublic List<T> list() {List<T> documents;try {documents = Optional.ofNullable(index.getDocuments(tClass)).map(indexDocument -> indexDocument.getResults()).map(result -> Arrays.asList(result)).orElse(new ArrayList<>());} catch (Exception e) {throw new RuntimeException(e);}return documents;}@Overridepublic 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;}@Overridepublic List<T> list(int offset, int limit) {List<T> documents;try {DocumentsQuery query = new DocumentsQuery();query.setLimit(limit);query.setOffset(offset);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;}@Overridepublic long add(T document) {List<T> list = Collections.singletonList(document);return add(list);}@Overridepublic long update(T document) {List<T> list = Collections.singletonList(document);return update(list);}@Overridepublic long add(List documents) {int taskId;try {taskId = index.addDocuments(JSON.toJSONString(documents)).getTaskUid();} catch (Exception e) {throw new RuntimeException(e);}return taskId;}@Overridepublic long update(List documents) {int updates;try {updates = index.updateDocuments(JSON.toJSONString(documents)).getTaskUid();} catch (Exception e) {throw new RuntimeException(e);}return updates;}@Overridepublic long delete(String identifier) {int taskId;try {taskId = index.deleteDocument(identifier).getTaskUid();} catch (Exception e) {throw new RuntimeException(e);}return taskId;}@Overridepublic long deleteBatch(String... documentsIdentifiers) {int taskId;try {taskId = index.deleteDocuments(Arrays.asList(documentsIdentifiers)).getTaskUid();} catch (Exception e) {throw new RuntimeException(e);}return taskId;}@Overridepublic long deleteAll() {int taskId;try {taskId = index.deleteAllDocuments().getTaskUid();} catch (Exception e) {throw new RuntimeException(e);}return taskId;}@Overridepublic cn.iocoder.yudao.framework.meilisearch.json.SearchResult<T> search(String q) {String result;try {result = JSON.toJSONString(index.search(q));} catch (Exception e) {throw new RuntimeException(e);}return jsonHandler.resultDecode(result, tClass);}@Overridepublic 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);}@Overridepublic SearchResult<T> search(SearchRequest sr) {String result;try {result = JSON.toJSONString(index.search(sr));} catch (Exception e) {throw new RuntimeException(e);}return jsonHandler.resultDecode(result, tClass);}@Overridepublic Settings getSettings() {try {return index.getSettings();} catch (Exception e) {throw new RuntimeException(e);}}@Overridepublic TaskInfo updateSettings(Settings settings) {try {return index.updateSettings(settings);} catch (Exception e) {throw new RuntimeException(e);}}@Overridepublic TaskInfo resetSettings() {try {return index.resetSettings();} catch (Exception e) {throw new RuntimeException(e);}}@Overridepublic Task getUpdate(int updateId) {try {return index.getTask(updateId);} catch (Exception e) {throw new RuntimeException(e);}}@Overridepublic void afterPropertiesSet() throws Exception {initIndex();}public Index getIndex() {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);}}
}
9、指定自动配置类所在
10、项目有统一版本管理的设置下版本管理
二、项目引用
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;
}
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、基本使用
@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);