mirror of
https://gitee.com/blossom-editor/blossom.git
synced 2025-12-07 01:08:26 +08:00
(modify) 增加tags字段的检索, 修改分词器使用lucene标准分词器, 增加基于用户的索引库隔离, 调整doc构建逻辑,防止索引缺失
This commit is contained in:
parent
38b2c50147
commit
a9150eaec7
@ -69,6 +69,24 @@
|
|||||||
<artifactId>thumbnailator</artifactId>
|
<artifactId>thumbnailator</artifactId>
|
||||||
<version>${thumbnailator.version}</version>
|
<version>${thumbnailator.version}</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<!-- Lucene核心库 -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.apache.lucene</groupId>
|
||||||
|
<artifactId>lucene-core</artifactId>
|
||||||
|
<version>9.9.1</version>
|
||||||
|
</dependency>
|
||||||
|
<!-- Lucene解析库 -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.apache.lucene</groupId>
|
||||||
|
<artifactId>lucene-queryparser</artifactId>
|
||||||
|
<version>9.9.1</version>
|
||||||
|
</dependency>
|
||||||
|
<!-- Lucene结果高亮 -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.apache.lucene</groupId>
|
||||||
|
<artifactId>lucene-highlighter</artifactId>
|
||||||
|
<version>9.9.1</version>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
</dependencies>
|
</dependencies>
|
||||||
|
|
||||||
|
|||||||
@ -0,0 +1,55 @@
|
|||||||
|
package com.blossom.backend.base.search;
|
||||||
|
|
||||||
|
import com.blossom.backend.base.search.message.ArticleIndexMsg;
|
||||||
|
import com.blossom.backend.base.search.message.IndexMsg;
|
||||||
|
import com.blossom.backend.base.search.message.IndexMsgTypeEnum;
|
||||||
|
import com.blossom.backend.base.search.message.consumer.BatchIndexMsgConsumer;
|
||||||
|
import com.blossom.backend.server.article.draft.ArticleService;
|
||||||
|
import com.blossom.backend.server.article.draft.pojo.ArticleEntity;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.scheduling.annotation.Scheduled;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
import org.springframework.util.StringUtils;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 对既有索引进行监控与维护
|
||||||
|
*/
|
||||||
|
|
||||||
|
@Component
|
||||||
|
@Slf4j
|
||||||
|
public class IndexObserver {
|
||||||
|
|
||||||
|
private SearchProperties searchProperties;
|
||||||
|
private ArticleService articleService;
|
||||||
|
private BatchIndexMsgConsumer batchIndexMsgConsumer;
|
||||||
|
|
||||||
|
|
||||||
|
IndexObserver(SearchProperties searchProperties, ArticleService articleService, BatchIndexMsgConsumer batchIndexMsgConsumer){
|
||||||
|
this.searchProperties = searchProperties;
|
||||||
|
this.articleService = articleService;
|
||||||
|
this.batchIndexMsgConsumer = batchIndexMsgConsumer;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 进行索引的维护
|
||||||
|
*/
|
||||||
|
@Scheduled(cron = "0 0 04 * * ?")
|
||||||
|
public void reloadIndex() throws IOException {
|
||||||
|
if (StringUtils.hasText(searchProperties.getPath())){
|
||||||
|
List<ArticleEntity> allArticleWithContent = articleService.listAllArticleWithContent();
|
||||||
|
List<IndexMsg> batchReloadMsgs = new ArrayList<>();
|
||||||
|
allArticleWithContent.forEach(article ->{
|
||||||
|
ArticleIndexMsg articleIndexMsg = new ArticleIndexMsg(IndexMsgTypeEnum.ADD,article.getId(),article.getName(),article.getTags(),article.getMarkdown(),article.getUserId());
|
||||||
|
batchReloadMsgs.add(articleIndexMsg);
|
||||||
|
});
|
||||||
|
|
||||||
|
batchIndexMsgConsumer.batchReload(batchReloadMsgs);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
@ -0,0 +1,26 @@
|
|||||||
|
package com.blossom.backend.base.search;
|
||||||
|
|
||||||
|
import cn.hutool.core.convert.Convert;
|
||||||
|
import lombok.Data;
|
||||||
|
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.nio.file.Paths;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@Configuration
|
||||||
|
@ConfigurationProperties(prefix = "project.search")
|
||||||
|
public class SearchProperties {
|
||||||
|
|
||||||
|
private String path = "";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据用户id, 获取对应索引库path
|
||||||
|
* @param userId 用户id
|
||||||
|
* @return 索引库path
|
||||||
|
*/
|
||||||
|
public Path getUserIndexDirectory(Long userId){
|
||||||
|
return Paths.get(this.path, Convert.toStr(userId));
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,27 @@
|
|||||||
|
package com.blossom.backend.base.search;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 全文搜索返回对象
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
public class SearchResult {
|
||||||
|
/**
|
||||||
|
* 主键
|
||||||
|
*/
|
||||||
|
private Long id;
|
||||||
|
/**
|
||||||
|
* 标题
|
||||||
|
*/
|
||||||
|
private String title;
|
||||||
|
/**
|
||||||
|
* 标签
|
||||||
|
*/
|
||||||
|
private String tags;
|
||||||
|
/**
|
||||||
|
* 正文
|
||||||
|
*/
|
||||||
|
private String content;
|
||||||
|
|
||||||
|
}
|
||||||
@ -0,0 +1,136 @@
|
|||||||
|
package com.blossom.backend.base.search;
|
||||||
|
|
||||||
|
import cn.hutool.core.convert.Convert;
|
||||||
|
import cn.hutool.core.util.ArrayUtil;
|
||||||
|
import com.blossom.common.base.exception.XzException500;
|
||||||
|
import org.apache.lucene.analysis.standard.StandardAnalyzer;
|
||||||
|
import org.apache.lucene.document.Document;
|
||||||
|
import org.apache.lucene.index.DirectoryReader;
|
||||||
|
import org.apache.lucene.index.IndexReader;
|
||||||
|
import org.apache.lucene.queryparser.classic.MultiFieldQueryParser;
|
||||||
|
import org.apache.lucene.search.IndexSearcher;
|
||||||
|
import org.apache.lucene.search.Query;
|
||||||
|
import org.apache.lucene.search.ScoreDoc;
|
||||||
|
import org.apache.lucene.search.TopDocs;
|
||||||
|
import org.apache.lucene.search.highlight.Highlighter;
|
||||||
|
import org.apache.lucene.search.highlight.QueryScorer;
|
||||||
|
import org.apache.lucene.search.highlight.SimpleFragmenter;
|
||||||
|
import org.apache.lucene.search.highlight.SimpleHTMLFormatter;
|
||||||
|
import org.apache.lucene.store.Directory;
|
||||||
|
import org.apache.lucene.store.FSDirectory;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
import org.springframework.util.StringUtils;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
@Component
|
||||||
|
public class Searcher {
|
||||||
|
|
||||||
|
private SimpleHTMLFormatter simpleHTMLFormatter;
|
||||||
|
|
||||||
|
private String[] queryField;
|
||||||
|
|
||||||
|
private Map<String, Float> boostsMap;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private SearchProperties searchProperties;
|
||||||
|
|
||||||
|
|
||||||
|
Searcher() {
|
||||||
|
// 构造f高亮显示formatter
|
||||||
|
this.simpleHTMLFormatter = new SimpleHTMLFormatter("<B>", "<B>");
|
||||||
|
// 构造默认查询域
|
||||||
|
this.queryField = new String[3];
|
||||||
|
this.queryField[0] = "content";
|
||||||
|
this.queryField[1] = "title";
|
||||||
|
this.queryField[2] = "tags";
|
||||||
|
// 构造权重配置
|
||||||
|
this.boostsMap = new HashMap<>();
|
||||||
|
boostsMap.put("title", 2F);
|
||||||
|
boostsMap.put("tags", 2F);
|
||||||
|
boostsMap.put("content", 1F);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 进行索引查询, 传入关键词以及用户id
|
||||||
|
* @param keyword 关键词
|
||||||
|
* @param userId 用户id
|
||||||
|
* @return 查询结果
|
||||||
|
*/
|
||||||
|
public List<SearchResult> search(String keyword, Long userId) {
|
||||||
|
List<SearchResult> result = new ArrayList<>();
|
||||||
|
if (!StringUtils.hasText(searchProperties.getPath())) {
|
||||||
|
throw new XzException500("未配置索引库地址,无法进行全文检索");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (userId == null){
|
||||||
|
throw new IllegalArgumentException("未获取到用户信息");
|
||||||
|
}
|
||||||
|
|
||||||
|
try (Directory directory = FSDirectory.open(searchProperties.getUserIndexDirectory(userId));
|
||||||
|
IndexReader indexReader = DirectoryReader.open(directory);
|
||||||
|
) {
|
||||||
|
IndexSearcher indexSearcher = new IndexSearcher(indexReader);
|
||||||
|
MultiFieldQueryParser multiFieldQueryParser = new MultiFieldQueryParser(queryField, new StandardAnalyzer(),boostsMap);
|
||||||
|
Query query = multiFieldQueryParser.parse(keyword);
|
||||||
|
TopDocs topDocs = indexSearcher.search(query, 10);
|
||||||
|
ScoreDoc[] scoreDocs = topDocs.scoreDocs;
|
||||||
|
if (!ArrayUtil.isEmpty(scoreDocs)) {
|
||||||
|
Highlighter highlighter = new Highlighter(simpleHTMLFormatter, new QueryScorer(query));
|
||||||
|
highlighter.setTextFragmenter(new SimpleFragmenter(20));
|
||||||
|
for (ScoreDoc doc : scoreDocs) {
|
||||||
|
Document document = indexSearcher.doc(doc.doc);
|
||||||
|
String id = document.get("id");
|
||||||
|
String title = document.get("title");
|
||||||
|
String content = document.get("content");
|
||||||
|
String tags = document.get("tags");
|
||||||
|
SearchResult searchResult = new SearchResult();
|
||||||
|
searchResult.setId(Convert.toLong(id));
|
||||||
|
if (StringUtils.hasText(title)){
|
||||||
|
String matchTitle = highlighter.getBestFragment(new StandardAnalyzer(), "title", title);
|
||||||
|
if (StringUtils.hasText(matchTitle)){
|
||||||
|
searchResult.setTitle(matchTitle);
|
||||||
|
}else {
|
||||||
|
searchResult.setTitle(title);
|
||||||
|
}
|
||||||
|
}else {
|
||||||
|
searchResult.setContent(title);
|
||||||
|
}
|
||||||
|
if (StringUtils.hasText(content)){
|
||||||
|
String matchContent = highlighter.getBestFragment(new StandardAnalyzer(), "content", content);
|
||||||
|
if (StringUtils.hasText(matchContent)){
|
||||||
|
searchResult.setContent(matchContent);
|
||||||
|
}else {
|
||||||
|
searchResult.setContent(content);
|
||||||
|
}
|
||||||
|
}else {
|
||||||
|
searchResult.setContent(content);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (StringUtils.hasText(tags)){
|
||||||
|
String matchTags= highlighter.getBestFragment(new StandardAnalyzer(), "tags", tags);
|
||||||
|
if (StringUtils.hasText(matchTags)){
|
||||||
|
searchResult.setTags(matchTags);
|
||||||
|
}else {
|
||||||
|
searchResult.setTags(tags);
|
||||||
|
}
|
||||||
|
}else {
|
||||||
|
searchResult.setTags(tags);
|
||||||
|
}
|
||||||
|
result.add(searchResult);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new XzException500("索引查询异常");
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,70 @@
|
|||||||
|
package com.blossom.backend.base.search.message;
|
||||||
|
|
||||||
|
import cn.hutool.core.convert.Convert;
|
||||||
|
import org.apache.lucene.document.Document;
|
||||||
|
import org.apache.lucene.document.Field;
|
||||||
|
import org.apache.lucene.document.StringField;
|
||||||
|
import org.apache.lucene.document.TextField;
|
||||||
|
import org.springframework.util.StringUtils;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 文章索引消息的实现
|
||||||
|
*/
|
||||||
|
|
||||||
|
public class ArticleIndexMsg implements IndexMsg {
|
||||||
|
|
||||||
|
private IndexMsgTypeEnum type;
|
||||||
|
|
||||||
|
private Long data;
|
||||||
|
|
||||||
|
private Document document;
|
||||||
|
|
||||||
|
private Long userId;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
public ArticleIndexMsg(IndexMsgTypeEnum indexMsgType, Long id, Long userId) {
|
||||||
|
this.type = indexMsgType;
|
||||||
|
this.data = id;
|
||||||
|
this.userId = userId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ArticleIndexMsg(IndexMsgTypeEnum indexMsgType, Long id, String title, String tags, String content, Long userId){
|
||||||
|
this.type = indexMsgType;
|
||||||
|
this.data = id;
|
||||||
|
this.userId = userId;
|
||||||
|
Document document = new Document();
|
||||||
|
// 存储文章的id, content
|
||||||
|
document.add(new StringField("id", Convert.toStr(id), Field.Store.YES));
|
||||||
|
if (StringUtils.hasText(title)){
|
||||||
|
document.add(new TextField("title", title, Field.Store.YES));
|
||||||
|
}
|
||||||
|
if (StringUtils.hasText(content)){
|
||||||
|
document.add(new TextField("content", content, Field.Store.YES));
|
||||||
|
}
|
||||||
|
if (StringUtils.hasText(tags)){
|
||||||
|
document.add(new TextField("tags", tags, Field.Store.YES));
|
||||||
|
}
|
||||||
|
this.document = document;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public IndexMsgTypeEnum getType() {
|
||||||
|
return this.type;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Long getId() {
|
||||||
|
return this.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Document getDoc() {
|
||||||
|
return this.document;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Long getCurrentUserId() {
|
||||||
|
return this.userId;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,34 @@
|
|||||||
|
package com.blossom.backend.base.search.message;
|
||||||
|
|
||||||
|
import org.apache.lucene.document.Document;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 索引消息接口
|
||||||
|
*/
|
||||||
|
public interface IndexMsg {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 消息类型
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
IndexMsgTypeEnum getType();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 主键id
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
Long getId();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 为批量reload提供的提前构造数据的接口,避免多次查询数据库
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
Document getDoc();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 消息对应用户id
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
Long getCurrentUserId();
|
||||||
|
|
||||||
|
}
|
||||||
@ -0,0 +1,10 @@
|
|||||||
|
package com.blossom.backend.base.search.message;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 索引动作类型
|
||||||
|
*/
|
||||||
|
public enum IndexMsgTypeEnum {
|
||||||
|
|
||||||
|
ADD,DELETE
|
||||||
|
|
||||||
|
}
|
||||||
@ -0,0 +1,71 @@
|
|||||||
|
package com.blossom.backend.base.search.message.consumer;
|
||||||
|
|
||||||
|
import cn.hutool.core.convert.Convert;
|
||||||
|
import com.blossom.backend.base.search.SearchProperties;
|
||||||
|
import com.blossom.backend.base.search.message.IndexMsg;
|
||||||
|
import com.blossom.backend.base.search.message.IndexMsgTypeEnum;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.apache.lucene.analysis.standard.StandardAnalyzer;
|
||||||
|
import org.apache.lucene.document.Document;
|
||||||
|
import org.apache.lucene.index.IndexWriter;
|
||||||
|
import org.apache.lucene.index.IndexWriterConfig;
|
||||||
|
import org.apache.lucene.index.Term;
|
||||||
|
import org.apache.lucene.store.Directory;
|
||||||
|
import org.apache.lucene.store.FSDirectory;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
import org.springframework.util.StringUtils;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
@Component
|
||||||
|
@Slf4j
|
||||||
|
public class BatchIndexMsgConsumer {
|
||||||
|
|
||||||
|
private SearchProperties searchProperties;
|
||||||
|
|
||||||
|
BatchIndexMsgConsumer(SearchProperties searchProperties) {
|
||||||
|
this.searchProperties = searchProperties;
|
||||||
|
if (!StringUtils.hasText(searchProperties.getPath())) {
|
||||||
|
log.info("未配置索引库地址, 关闭全文搜索功能支持");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void batchReload(List<IndexMsg> list) throws IOException {
|
||||||
|
// 需要对所有用户的索引进行维护,减少文件打开次数, 优先进行分组
|
||||||
|
Map<Long, List<IndexMsg>> userGroupMsgMap = list.stream().collect(Collectors.groupingBy(IndexMsg::getCurrentUserId));
|
||||||
|
// 遍历map, 逐个用户进行索引重建
|
||||||
|
for (Map.Entry<Long, List<IndexMsg>> entity : userGroupMsgMap.entrySet()){
|
||||||
|
Long userId = entity.getKey();
|
||||||
|
List<IndexMsg> msgList = entity.getValue();
|
||||||
|
if (userId == null){
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
try (Directory directory = FSDirectory.open(searchProperties.getUserIndexDirectory(userId));
|
||||||
|
IndexWriter indexWriter = new IndexWriter(directory, new IndexWriterConfig(new StandardAnalyzer()));
|
||||||
|
){
|
||||||
|
for (IndexMsg indexMsg : msgList){
|
||||||
|
if (IndexMsgTypeEnum.ADD.equals(indexMsg.getType())) {
|
||||||
|
// 插入 or 更新索引
|
||||||
|
// 打开索引库 --->通过getDoc方法获取索引文档
|
||||||
|
Document document = indexMsg.getDoc();
|
||||||
|
String id = document.get("id");
|
||||||
|
indexWriter.updateDocument(new Term("id", id), document);
|
||||||
|
} else if (IndexMsgTypeEnum.DELETE.equals(indexMsg.getType())) {
|
||||||
|
// 删除索引
|
||||||
|
Long id = indexMsg.getId();
|
||||||
|
indexWriter.deleteDocuments(new Term("id", Convert.toStr(id)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 完成
|
||||||
|
indexWriter.flush();
|
||||||
|
indexWriter.commit();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@ -0,0 +1,110 @@
|
|||||||
|
package com.blossom.backend.base.search.message.consumer;
|
||||||
|
|
||||||
|
import cn.hutool.core.convert.Convert;
|
||||||
|
import com.blossom.backend.base.search.SearchProperties;
|
||||||
|
import com.blossom.backend.base.search.message.IndexMsg;
|
||||||
|
import com.blossom.backend.base.search.message.IndexMsgTypeEnum;
|
||||||
|
import com.blossom.backend.base.search.queue.IndexMsgQueue;
|
||||||
|
import com.blossom.backend.server.article.draft.ArticleService;
|
||||||
|
import com.blossom.backend.server.article.draft.pojo.ArticleEntity;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.apache.lucene.analysis.standard.StandardAnalyzer;
|
||||||
|
import org.apache.lucene.document.Document;
|
||||||
|
import org.apache.lucene.document.Field;
|
||||||
|
import org.apache.lucene.document.StringField;
|
||||||
|
import org.apache.lucene.document.TextField;
|
||||||
|
import org.apache.lucene.index.IndexWriter;
|
||||||
|
import org.apache.lucene.index.IndexWriterConfig;
|
||||||
|
import org.apache.lucene.index.Term;
|
||||||
|
import org.apache.lucene.store.Directory;
|
||||||
|
import org.apache.lucene.store.FSDirectory;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
import org.springframework.util.StringUtils;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.util.concurrent.Executors;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 索引消息的消费者
|
||||||
|
*
|
||||||
|
* @author Andecheal
|
||||||
|
*/
|
||||||
|
@Component
|
||||||
|
@Slf4j
|
||||||
|
public class IndexMsgConsumer {
|
||||||
|
|
||||||
|
private SearchProperties searchProperties;
|
||||||
|
|
||||||
|
private ArticleService articleService;
|
||||||
|
|
||||||
|
IndexMsgConsumer(SearchProperties searchProperties, ArticleService articleService) {
|
||||||
|
this.searchProperties = searchProperties;
|
||||||
|
this.articleService = articleService;
|
||||||
|
if (!StringUtils.hasText(searchProperties.getPath())) {
|
||||||
|
log.info("未配置索引库地址, 关闭全文搜索功能支持");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Executors.newSingleThreadExecutor().submit(new Runnable() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
while (true) {
|
||||||
|
try {
|
||||||
|
IndexMsg indexMsg = IndexMsgQueue.take();
|
||||||
|
// 首先获取消息中的userId, 根据userId 打开对应的索引库
|
||||||
|
Long userId = indexMsg.getCurrentUserId();
|
||||||
|
if (userId == null){
|
||||||
|
// 记录异常并继续消费
|
||||||
|
log.error("消费异常. 获取用户id为空");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (IndexMsgTypeEnum.ADD.equals(indexMsg.getType())) {
|
||||||
|
// 插入 or 更新索引
|
||||||
|
// 打开索引库
|
||||||
|
try (Directory directory = FSDirectory.open(searchProperties.getUserIndexDirectory(userId));
|
||||||
|
IndexWriter indexWriter = new IndexWriter(directory, new IndexWriterConfig(new StandardAnalyzer()));
|
||||||
|
) {
|
||||||
|
// 查询doc数据 ---> 避免service部分功能未查询全部字段数据导致的索引field丢失
|
||||||
|
Long id = indexMsg.getId();
|
||||||
|
ArticleEntity article = articleService.getById(id);
|
||||||
|
Document document = new Document();
|
||||||
|
String title = article.getName();
|
||||||
|
String markdownContent = article.getMarkdown();
|
||||||
|
String tags = article.getTags();
|
||||||
|
// 存储文章的id, content
|
||||||
|
document.add(new StringField("id", Convert.toStr(id), Field.Store.YES));
|
||||||
|
if (StringUtils.hasText(title)){
|
||||||
|
document.add(new TextField("title", article.getName(), Field.Store.YES));
|
||||||
|
}
|
||||||
|
if (StringUtils.hasText(markdownContent)){
|
||||||
|
document.add(new TextField("content", markdownContent, Field.Store.YES));
|
||||||
|
}
|
||||||
|
if (StringUtils.hasText(tags)){
|
||||||
|
document.add(new TextField("tags", tags, Field.Store.YES));
|
||||||
|
}
|
||||||
|
|
||||||
|
indexWriter.updateDocument(new Term("id", Convert.toStr(id)), document);
|
||||||
|
indexWriter.flush();
|
||||||
|
indexWriter.commit();
|
||||||
|
}
|
||||||
|
} else if (IndexMsgTypeEnum.DELETE.equals(indexMsg.getType())) {
|
||||||
|
// 删除索引
|
||||||
|
try (Directory directory = FSDirectory.open(new File(searchProperties.getPath()).toPath());
|
||||||
|
IndexWriter indexWriter = new IndexWriter(directory, new IndexWriterConfig(new StandardAnalyzer()));
|
||||||
|
|
||||||
|
) {
|
||||||
|
Long id = indexMsg.getId();
|
||||||
|
indexWriter.deleteDocuments(new Term("id", Convert.toStr(id)));
|
||||||
|
indexWriter.flush();
|
||||||
|
indexWriter.commit();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("消费失败" + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -0,0 +1,46 @@
|
|||||||
|
package com.blossom.backend.base.search.queue;
|
||||||
|
|
||||||
|
import com.blossom.backend.base.search.message.IndexMsg;
|
||||||
|
|
||||||
|
import java.util.concurrent.ArrayBlockingQueue;
|
||||||
|
import java.util.concurrent.BlockingQueue;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 消息处理使用的阻塞队列
|
||||||
|
* @author Andecheal
|
||||||
|
*/
|
||||||
|
public class IndexMsgQueue {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 阻塞队列 , 存放消息
|
||||||
|
*/
|
||||||
|
private static final BlockingQueue<IndexMsg> indexMsgQueue = new ArrayBlockingQueue<>(2048);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 应用提交消息
|
||||||
|
* @param msg
|
||||||
|
* @throws InterruptedException
|
||||||
|
*/
|
||||||
|
public static void add(IndexMsg msg) throws InterruptedException {
|
||||||
|
indexMsgQueue.add(msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 提供一个阻塞式消息入口
|
||||||
|
* @param msg
|
||||||
|
* @throws InterruptedException
|
||||||
|
*/
|
||||||
|
public static void put(IndexMsg msg) throws InterruptedException {
|
||||||
|
indexMsgQueue.put(msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取消息
|
||||||
|
* @return
|
||||||
|
* @throws InterruptedException
|
||||||
|
*/
|
||||||
|
public static IndexMsg take() throws InterruptedException {
|
||||||
|
return indexMsgQueue.take();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -28,6 +28,8 @@ public interface ArticleMapper extends BaseMapper<ArticleEntity> {
|
|||||||
*/
|
*/
|
||||||
List<ArticleEntity> listAll(ArticleEntity entity);
|
List<ArticleEntity> listAll(ArticleEntity entity);
|
||||||
|
|
||||||
|
List<ArticleEntity> listAllArticleWithContent();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 根据ID修改
|
* 根据ID修改
|
||||||
*/
|
*/
|
||||||
|
|||||||
@ -4,6 +4,10 @@ import cn.hutool.core.collection.CollUtil;
|
|||||||
import cn.hutool.core.util.ObjUtil;
|
import cn.hutool.core.util.ObjUtil;
|
||||||
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
||||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||||
|
import com.blossom.backend.base.auth.AuthContext;
|
||||||
|
import com.blossom.backend.base.search.message.ArticleIndexMsg;
|
||||||
|
import com.blossom.backend.base.search.message.IndexMsgTypeEnum;
|
||||||
|
import com.blossom.backend.base.search.queue.IndexMsgQueue;
|
||||||
import com.blossom.backend.server.article.TagEnum;
|
import com.blossom.backend.server.article.TagEnum;
|
||||||
import com.blossom.backend.server.article.draft.pojo.ArticleEntity;
|
import com.blossom.backend.server.article.draft.pojo.ArticleEntity;
|
||||||
import com.blossom.backend.server.article.draft.pojo.ArticleQueryReq;
|
import com.blossom.backend.server.article.draft.pojo.ArticleQueryReq;
|
||||||
@ -79,6 +83,18 @@ public class ArticleService extends ServiceImpl<ArticleMapper, ArticleEntity> {
|
|||||||
return articles;
|
return articles;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取所有文章,包含markdown字段,用于索引的批量维护
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
public List<ArticleEntity> listAllArticleWithContent() {
|
||||||
|
List<ArticleEntity> articles = baseMapper.listAllArticleWithContent();
|
||||||
|
if (CollUtil.isEmpty(articles)) {
|
||||||
|
return new ArrayList<>();
|
||||||
|
}
|
||||||
|
return articles;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 查询列表
|
* 查询列表
|
||||||
* <p>避免在查询主要信息时返回正文信息造成的性能影响, 该接口不返回文章正文 toc/markdown/html</p>
|
* <p>避免在查询主要信息时返回正文信息造成的性能影响, 该接口不返回文章正文 toc/markdown/html</p>
|
||||||
@ -138,6 +154,13 @@ public class ArticleService extends ServiceImpl<ArticleMapper, ArticleEntity> {
|
|||||||
@Transactional(rollbackFor = Exception.class)
|
@Transactional(rollbackFor = Exception.class)
|
||||||
public ArticleEntity insert(ArticleEntity req) {
|
public ArticleEntity insert(ArticleEntity req) {
|
||||||
baseMapper.insert(req);
|
baseMapper.insert(req);
|
||||||
|
ArticleIndexMsg articleIndexMsg = new ArticleIndexMsg(IndexMsgTypeEnum.ADD, req.getId(), AuthContext.getUserId());
|
||||||
|
try {
|
||||||
|
IndexMsgQueue.add(articleIndexMsg);
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
// 不抛出, 暂时先记录
|
||||||
|
log.error("索引更新失败" + e.getMessage());
|
||||||
|
}
|
||||||
return req;
|
return req;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -149,6 +172,13 @@ public class ArticleService extends ServiceImpl<ArticleMapper, ArticleEntity> {
|
|||||||
public Long update(ArticleEntity req) {
|
public Long update(ArticleEntity req) {
|
||||||
XzException404.throwBy(req.getId() == null, "ID不得为空");
|
XzException404.throwBy(req.getId() == null, "ID不得为空");
|
||||||
baseMapper.updById(req);
|
baseMapper.updById(req);
|
||||||
|
ArticleIndexMsg articleIndexMsg = new ArticleIndexMsg(IndexMsgTypeEnum.ADD, req.getId(),AuthContext.getUserId());
|
||||||
|
try {
|
||||||
|
IndexMsgQueue.add(articleIndexMsg);
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
// 不抛出, 暂时先记录
|
||||||
|
log.error("索引更新失败" + e.getMessage());
|
||||||
|
}
|
||||||
return req.getId();
|
return req.getId();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -169,6 +199,14 @@ public class ArticleService extends ServiceImpl<ArticleMapper, ArticleEntity> {
|
|||||||
baseMapper.updContentById(req);
|
baseMapper.updContentById(req);
|
||||||
referenceService.bind(req.getUserId(), req.getId(), req.getName(), req.getReferences());
|
referenceService.bind(req.getUserId(), req.getId(), req.getName(), req.getReferences());
|
||||||
logService.insert(req.getId(), 0, req.getMarkdown());
|
logService.insert(req.getId(), 0, req.getMarkdown());
|
||||||
|
// 更新索引
|
||||||
|
ArticleIndexMsg articleIndexMsg = new ArticleIndexMsg(IndexMsgTypeEnum.ADD, req.getId(), AuthContext.getUserId());
|
||||||
|
try {
|
||||||
|
IndexMsgQueue.add(articleIndexMsg);
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
// 不抛出, 暂时先记录
|
||||||
|
log.error("索引更新失败" + e.getMessage());
|
||||||
|
}
|
||||||
return req.getWords();
|
return req.getWords();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -197,6 +235,14 @@ public class ArticleService extends ServiceImpl<ArticleMapper, ArticleEntity> {
|
|||||||
referenceService.delete(id);
|
referenceService.delete(id);
|
||||||
// 删除访问记录
|
// 删除访问记录
|
||||||
viewService.delete(id);
|
viewService.delete(id);
|
||||||
|
// 删除索引
|
||||||
|
ArticleIndexMsg articleIndexMsg = new ArticleIndexMsg(IndexMsgTypeEnum.DELETE, id, AuthContext.getUserId());
|
||||||
|
try {
|
||||||
|
IndexMsgQueue.add(articleIndexMsg);
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
// 不抛出, 暂时先记录
|
||||||
|
log.error("索引更新失败" + e.getMessage());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -3,9 +3,13 @@ package com.blossom.backend.server.article.recycle;
|
|||||||
import cn.hutool.core.util.ObjUtil;
|
import cn.hutool.core.util.ObjUtil;
|
||||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||||
|
import com.blossom.backend.base.auth.AuthContext;
|
||||||
import com.blossom.backend.base.param.ParamEnum;
|
import com.blossom.backend.base.param.ParamEnum;
|
||||||
import com.blossom.backend.base.param.ParamService;
|
import com.blossom.backend.base.param.ParamService;
|
||||||
import com.blossom.backend.base.param.pojo.ParamEntity;
|
import com.blossom.backend.base.param.pojo.ParamEntity;
|
||||||
|
import com.blossom.backend.base.search.message.ArticleIndexMsg;
|
||||||
|
import com.blossom.backend.base.search.message.IndexMsgTypeEnum;
|
||||||
|
import com.blossom.backend.base.search.queue.IndexMsgQueue;
|
||||||
import com.blossom.backend.server.article.recycle.pojo.ArticleRecycleEntity;
|
import com.blossom.backend.server.article.recycle.pojo.ArticleRecycleEntity;
|
||||||
import com.blossom.backend.server.folder.FolderService;
|
import com.blossom.backend.server.folder.FolderService;
|
||||||
import com.blossom.backend.server.folder.pojo.FolderEntity;
|
import com.blossom.backend.server.folder.pojo.FolderEntity;
|
||||||
@ -58,6 +62,13 @@ public class ArticleRecycleService extends ServiceImpl<ArticleRecycleMapper, Art
|
|||||||
baseMapper.restore(id, folder.getId());
|
baseMapper.restore(id, folder.getId());
|
||||||
}
|
}
|
||||||
baseMapper.deleteById(id);
|
baseMapper.deleteById(id);
|
||||||
|
ArticleIndexMsg articleIndexMsg = new ArticleIndexMsg(IndexMsgTypeEnum.ADD, article.getId(), AuthContext.getUserId());
|
||||||
|
try {
|
||||||
|
IndexMsgQueue.add(articleIndexMsg);
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
// 不抛出, 暂时先记录
|
||||||
|
log.error("索引更新失败" + e.getMessage());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -77,4 +77,7 @@ project:
|
|||||||
# 请以 /pic 结尾, 如果你在 nginx 中配置有代理, 注意别忘了添加你的代理路径
|
# 请以 /pic 结尾, 如果你在 nginx 中配置有代理, 注意别忘了添加你的代理路径
|
||||||
domain: "http://localhost:9999/pic/"
|
domain: "http://localhost:9999/pic/"
|
||||||
# 请以 / 开头, / 结尾, 简短的路径在文章中有更好的显示效果, 过长一定程度会使文章内容混乱
|
# 请以 / 开头, / 结尾, 简短的路径在文章中有更好的显示效果, 过长一定程度会使文章内容混乱
|
||||||
default-path: "/home/bl/img/"
|
default-path: "/home/bl/img/"
|
||||||
|
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫ 全文搜索 ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||||
|
search:
|
||||||
|
path: "/home/index/"
|
||||||
@ -79,3 +79,6 @@ project:
|
|||||||
domain: "https://www.wangyunf.com/bl/pic/"
|
domain: "https://www.wangyunf.com/bl/pic/"
|
||||||
# 请以 / 开头, / 结尾, 简短的路径在文章中有更好的显示效果, 过长一定程度会使文章内容混乱
|
# 请以 / 开头, / 结尾, 简短的路径在文章中有更好的显示效果, 过长一定程度会使文章内容混乱
|
||||||
default-path: "/home/bl/img/"
|
default-path: "/home/bl/img/"
|
||||||
|
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫ 全文搜索 ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||||
|
search:
|
||||||
|
path: "/home/index/"
|
||||||
|
|||||||
@ -81,3 +81,6 @@ project:
|
|||||||
domain: "https://www.wangyunf.com/blall/pic/"
|
domain: "https://www.wangyunf.com/blall/pic/"
|
||||||
# 请以 / 开头, / 结尾, 简短的路径在文章中有更好的显示效果, 过长一定程度会使文章内容混乱
|
# 请以 / 开头, / 结尾, 简短的路径在文章中有更好的显示效果, 过长一定程度会使文章内容混乱
|
||||||
default-path: "/home/blall/img/"
|
default-path: "/home/blall/img/"
|
||||||
|
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫ 全文搜索 ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||||
|
search:
|
||||||
|
path: "/home/index/"
|
||||||
@ -48,6 +48,17 @@
|
|||||||
</where>
|
</where>
|
||||||
</select>
|
</select>
|
||||||
|
|
||||||
|
<!-- 查询全部文章,包含id, name , markdown, tags , userId字段,用于批量索引的建立 -->
|
||||||
|
<select id="listAllArticleWithContent" resultType="com.blossom.backend.server.article.draft.pojo.ArticleEntity">
|
||||||
|
select
|
||||||
|
id,
|
||||||
|
`name`,
|
||||||
|
markdown,
|
||||||
|
tags,
|
||||||
|
user_id
|
||||||
|
from blossom_article
|
||||||
|
</select>
|
||||||
|
|
||||||
|
|
||||||
<!-- 根据ID修改 -->
|
<!-- 根据ID修改 -->
|
||||||
<update id="updById">
|
<update id="updById">
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user