package jnpf.dmsService.search;
|
|
import jnpf.dmsEntity.permission.enums.DmsPermissionAction;
|
import jnpf.dmsEntity.permission.enums.DmsResourceType;
|
import jnpf.dmsEntity.permission.form.PermissionDecisionForm;
|
import jnpf.dmsEntity.permission.vo.DmsPermissionDecisionVO;
|
import jnpf.dmsEntity.search.form.DmsFullTextSearchForm;
|
import jnpf.dmsEntity.search.vo.DmsFullTextSearchItemVO;
|
import jnpf.dmsEntity.search.vo.DmsFullTextSearchPageVO;
|
import jnpf.dmsPermission.DmsCurrentIdentity;
|
import jnpf.dmsMapper.DmsSearchIndexSourceMapper;
|
import jnpf.dmsPermission.DmsPermissionError;
|
import jnpf.dmsPermission.DmsPermissionException;
|
import jnpf.dmsService.DmsPermissionDecisionService;
|
import jnpf.dmsService.search.meili.DmsMeilisearchClient;
|
import jnpf.dmsService.search.meili.DmsMeilisearchSearchHit;
|
import jnpf.dmsService.search.meili.DmsMeilisearchSearchPage;
|
import jnpf.dmsService.search.meili.DmsSearchIndexDocument;
|
import lombok.RequiredArgsConstructor;
|
import org.springframework.stereotype.Service;
|
|
import java.io.IOException;
|
import java.util.ArrayList;
|
import java.util.List;
|
import java.util.UUID;
|
|
@Service
|
@RequiredArgsConstructor
|
public class DmsFullTextSearchService {
|
private static final int DEFAULT_PAGE_SIZE = 20;
|
private static final int MAX_PAGE_SIZE = 100;
|
private static final int CANDIDATE_BATCH_SIZE = 100;
|
private static final int MAX_CANDIDATES_PER_REQUEST = 1_000;
|
|
private final DmsCurrentIdentity currentIdentity;
|
private final DmsPermissionDecisionService permissionDecisionService;
|
private final DmsMeilisearchClient meilisearchClient;
|
private final DmsSearchIndexSourceMapper sourceMapper;
|
|
public DmsFullTextSearchPageVO search(DmsFullTextSearchForm form) throws IOException {
|
String keyword = form.getKeyword().trim();
|
int pageSize = pageSize(form.getPageSize());
|
int candidateOffset = cursor(form.getCursor());
|
int scanned = 0;
|
String tenantId = currentIdentity.tenantId();
|
List<DmsFullTextSearchItemVO> results = new ArrayList<>();
|
|
while (scanned < MAX_CANDIDATES_PER_REQUEST) {
|
int batchSize = Math.min(CANDIDATE_BATCH_SIZE,
|
MAX_CANDIDATES_PER_REQUEST - scanned);
|
DmsMeilisearchSearchPage candidates = meilisearchClient.search(
|
tenantId, keyword, candidateOffset, batchSize);
|
if (candidates.getHits().isEmpty()) {
|
break;
|
}
|
List<Boolean> allowed = allowed(candidates.getHits());
|
java.util.Set<String> current = new java.util.HashSet<>(sourceMapper.currentVersionIds(tenantId,
|
candidates.getHits().stream().map(hit -> hit.getDocument().getFileVersionId()).toList()));
|
for (int index = 0; index < candidates.getHits().size(); index++) {
|
int currentOffset = candidateOffset + index;
|
if (!allowed.get(index) || !current.contains(candidates.getHits().get(index).getDocument().getFileVersionId())) {
|
continue;
|
}
|
if (results.size() == pageSize) {
|
return page(results, Integer.toString(currentOffset), true);
|
}
|
results.add(toItem(candidates.getHits().get(index)));
|
}
|
int consumed = candidates.getHits().size();
|
candidateOffset += consumed;
|
scanned += consumed;
|
if (candidateOffset >= candidates.getEstimatedTotalHits()
|
|| consumed < batchSize) {
|
break;
|
}
|
}
|
if (scanned >= MAX_CANDIDATES_PER_REQUEST) {
|
return page(results, Integer.toString(candidateOffset), true);
|
}
|
return page(results, null, false);
|
}
|
|
private List<Boolean> allowed(List<DmsMeilisearchSearchHit> hits) {
|
List<PermissionDecisionForm> requests = new ArrayList<>();
|
for (DmsMeilisearchSearchHit hit : hits) {
|
PermissionDecisionForm request = new PermissionDecisionForm();
|
request.setResourceType(DmsResourceType.FILE_VERSION);
|
request.setResourceId(UUID.fromString(hit.getDocument().getFileVersionId()));
|
request.setActionCode(DmsPermissionAction.READ);
|
requests.add(request);
|
}
|
try {
|
List<DmsPermissionDecisionVO> decisions = permissionDecisionService.evaluate(requests);
|
List<Boolean> result = new ArrayList<>();
|
for (DmsPermissionDecisionVO decision : decisions) {
|
result.add(decision.isAllowed());
|
}
|
return result;
|
} catch (RuntimeException batchFailure) {
|
List<Boolean> result = new ArrayList<>();
|
for (PermissionDecisionForm request : requests) {
|
try {
|
result.add(permissionDecisionService.evaluate(request).isAllowed());
|
} catch (RuntimeException ignored) {
|
result.add(false);
|
}
|
}
|
return result;
|
}
|
}
|
|
private DmsFullTextSearchItemVO toItem(DmsMeilisearchSearchHit hit) {
|
DmsSearchIndexDocument source = hit.getDocument();
|
DmsFullTextSearchItemVO item = new DmsFullTextSearchItemVO();
|
item.setId(UUID.fromString(source.getFileVersionId()));
|
item.setDocumentId(UUID.fromString(source.getDocumentId()));
|
item.setFolderId(uuid(source.getFolderId()));
|
item.setFolderPath(source.getFolderPath());
|
item.setDocumentNo(source.getDocumentNo());
|
item.setTitle(source.getTitle());
|
item.setHighlightedTitle(highlight(hit.getFormattedTitle()));
|
item.setVersionLabel(source.getVersionLabel());
|
item.setFileName(source.getFileName());
|
item.setMimeType(source.getMimeType());
|
item.setLifecycleStatus(source.getLifecycleStatus());
|
item.setCreatedAt(source.getCreatedAt());
|
item.setHighlightedSnippet(highlight(hit.getFormattedContent()));
|
return item;
|
}
|
|
private DmsFullTextSearchPageVO page(List<DmsFullTextSearchItemVO> list,
|
String nextCursor, boolean hasMore) {
|
DmsFullTextSearchPageVO page = new DmsFullTextSearchPageVO();
|
page.setList(list);
|
page.setNextCursor(nextCursor);
|
page.setHasMore(hasMore);
|
return page;
|
}
|
|
private int pageSize(Integer requested) {
|
int value = requested == null ? DEFAULT_PAGE_SIZE : requested;
|
if (value < 1 || value > MAX_PAGE_SIZE) {
|
throw invalid("pageSize 必须在 1 到 " + MAX_PAGE_SIZE + " 之间");
|
}
|
return value;
|
}
|
|
private int cursor(String value) {
|
if (value == null || value.trim().isEmpty()) {
|
return 0;
|
}
|
try {
|
int offset = Integer.parseInt(value.trim());
|
if (offset < 0) {
|
throw invalid("cursor 无效");
|
}
|
return offset;
|
} catch (NumberFormatException exception) {
|
throw invalid("cursor 无效");
|
}
|
}
|
|
private UUID uuid(String value) {
|
return value == null || value.trim().isEmpty() ? null : UUID.fromString(value);
|
}
|
|
private String highlight(String value) {
|
String escaped = value == null ? "" : value
|
.replace("&", "&")
|
.replace("<", "<")
|
.replace(">", ">")
|
.replace("\"", """)
|
.replace("'", "'");
|
return escaped
|
.replace(DmsMeilisearchClient.HIGHLIGHT_START, "<mark>")
|
.replace(DmsMeilisearchClient.HIGHLIGHT_END, "</mark>");
|
}
|
|
private DmsPermissionException invalid(String message) {
|
return DmsPermissionException.of(DmsPermissionError.INVALID_REQUEST, message);
|
}
|
}
|