Spring Boot + MeiliSearch 快速整合指南
前言
在數據檢索需求日益增長的當下,傳統數據庫的查詢方式在復雜搜索場景下逐漸顯得力不從心,而MeiliSearch作為一款輕量級、高性能的開源搜索引擎,憑借其簡單易用的API和出色的搜索性能脫穎而出。
MeiliSearch 簡介
MeiliSearch 是一個用 Rust 編寫的輕量級、實時、開源搜索引擎,具有以下特點:
- 快速部署:可通過二進制文件或Docker快速啟動。
- 實時更新:數據的添加、更新和刪除操作能即時反映在搜索結果中。
- 簡單易用:提供簡潔的HTTP API,方便與各種編程語言集成。
- 強大的搜索功能:支持關鍵詞高亮、模糊搜索、分面搜索等高級功能。
安裝
選擇自己的版本https://github.com/meilisearch/meilisearch/tags:
圖片
把下載后的文件meilisearch-linux-amd64放置在meilisearch目錄中,同時給上可執行權限。
在meilisearch目錄下,創建配置文件 config.toml,參考以下內容:
# development or production,表示開發或生產環境,線上記得使用生產環境
env = "development"
# api訪問的master_key
master_key = "yianweilai"
# http api 啟動的IP和端口
http_addr = "0.0.0.0:7700"
# 數據文件所在目錄
db_path = "./data"
dump_dir = "./dumps"
snapshot_dir = "./snapshots"
# 日志等級
log_level = "INFO"
啟動測試:./meilisearch-linux-amd64 --config-file-path=./config.toml
圖片
訪問http管理界面:
圖片
如何使用
添加 MeiliSearch 依賴
<!-- https://mvnrepository.com/artifact/com.meilisearch.sdk/meilisearch-java -->
<dependency>
<groupId>com.meilisearch.sdk</groupId>
<artifactId>meilisearch-java</artifactId>
<version>0.14.2</version>
</dependency>
配置 MeiliSearch
meilisearch:
host: http://172.18.2.73:7700
api-key: yianweilai # 如果MeiliSearch配置了API Key,在此處填寫,若無則留空
創建 MeiliSearch 配置類
@Configuration
public class MeiliSearchConfig {
@Value("${meilisearch.host:}")
private String host;
@Value("${meilisearch.api-key:}")
private String apiKey;
@Bean
public Client meiliSearchClient() {
Config config = new Config(host, apiKey);
return new Client(config);
}
}
基本操作實現
/**
* MeiliSearch 基本操作實現
*/
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 = index.getDocument(identifier, tClass);
} catch (Exception e) {
throw new RuntimeException(e);
}
return document;
}
@Override
public 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;
}
@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(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 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) {
int taskId;
try {
taskId = index.addDocuments(JSON.toJSONString(documents)).getTaskUid();
} catch (Exception e) {
throw new RuntimeException(e);
}
return taskId;
}
@Override
public long update(List documents) {
int updates;
try {
updates = index.updateDocuments(JSON.toJSONString(documents)).getTaskUid();
} catch (Exception e) {
throw new RuntimeException(e);
}
return updates;
}
@Override
public long delete(String identifier) {
int taskId;
try {
taskId = index.deleteDocument(identifier).getTaskUid();
} catch (Exception e) {
throw new RuntimeException(e);
}
return taskId;
}
@Override
public long deleteBatch(String... documentsIdentifiers) {
int taskId;
try {
taskId = index.deleteDocuments(Arrays.asList(documentsIdentifiers)).getTaskUid();
} catch (Exception e) {
throw new RuntimeException(e);
}
return taskId;
}
@Override
public long deleteAll() {
int taskId;
try {
taskId = index.deleteAllDocuments().getTaskUid();
} catch (Exception e) {
throw new RuntimeException(e);
}
return taskId;
}
@Override
public 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);
}
@Override
public SearchResult<T> search(String q, int offset, int limit) {
SearchRequest searchRequest = SearchRequest.builder().build();
searchRequest.setQ(q);
searchRequest.setOffset(offset);
searchRequest.setLimit(limit);
return search(searchRequest);
}
@Override
public 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);
}
@Override
public String select(SearchRequest sr) {
try {
if (ObjectUtil.isNotNull(sr)) {
if (ObjectUtil.isNull(getIndex())) {
initIndex();
}
Searchable search = getIndex().search(sr);
String jsonString = JSON.toJSONString(search);
return jsonString;
}
} catch (Exception e) {
throw new RuntimeException(e);
}
return null;
}
@Override
public Settings getSettings() {
try {
return index.getSettings();
} catch (Exception e) {
throw new RuntimeException(e);
}
}
@Override
public TaskInfo updateSettings(Settings settings) {
try {
return index.updateSettings(settings);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
@Override
public TaskInfo resetSettings() {
try {
return index.resetSettings();
} catch (Exception e) {
throw new RuntimeException(e);
}
}
@Override
public Task getUpdate(int updateId) {
try {
return index.getTask(updateId);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
@Override
public 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);
}else {
client.createIndex(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);
}
}
以Demo為例
@Repository
public class MeiliSearchMapper extends MeilisearchRepository<DemoDO> {
}
測試
@Slf4j
@SpringBootTest
public class TestDemo {
@Autowired
private MeiliSearchMapper meiliSearchMapper;
@Test
public void test1(){
meiliSearchMapper.add(new DemoDO(1,"yian1","beijing1","yian1@yian.com","https://images.pexels.com/photos/3866556/pexels-photo-3866556.png",
System.currentTimeMillis(),System.currentTimeMillis()));
meiliSearchMapper.add(new DemoDO(2,"yian2","beijing2","yian2@yian.com","https://images.pexels.com/photos/19184114/pexels-photo-19184114.jpeg",
System.currentTimeMillis(),System.currentTimeMillis()));
meiliSearchMapper.add(new DemoDO(3,"yian3","beijing3","yian3@yian.com","https://images.pexels.com/photos/6310172/pexels-photo-6310172.jpeg",
System.currentTimeMillis(),System.currentTimeMillis()));
}
@Test
public void test2(){
StringBuffer sb = new StringBuffer();
StringBuffer demo = sb.append("name=yian1").append(" AND ").append("addr=beijing1");
SearchRequest searchRequest = SearchRequest.builder().build()
.setFilter(new String[]{demo.toString()})
.setLimit(10);
SearchResult<DemoDO> search = meiliSearchMapper.search(searchRequest);
log.info("{}",search);
}
@Test
public void test3(){
meiliSearchMapper.deleteAll();
}
@Test
public void test4(){
meiliSearchMapper.update(new DemoDO(1,"yian1","beijing1","yian1@yian.com","https://images.pexels.com/photos/3866556/pexels-photo-3866556.png",
System.currentTimeMillis(),System.currentTimeMillis()));
}
}
圖片