package jnpf.dmsService.search.meili;
|
|
import com.fasterxml.jackson.databind.JsonNode;
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
import com.fasterxml.jackson.databind.node.ObjectNode;
|
import jnpf.dmsService.search.DmsSearchProperties;
|
import org.springframework.stereotype.Service;
|
import org.springframework.util.StringUtils;
|
|
import java.io.IOException;
|
import java.net.URI;
|
import java.net.http.HttpClient;
|
import java.net.http.HttpRequest;
|
import java.net.http.HttpResponse;
|
import java.nio.charset.StandardCharsets;
|
import java.time.Duration;
|
import java.util.ArrayList;
|
import java.util.Arrays;
|
import java.util.Collections;
|
import java.util.LinkedHashMap;
|
import java.util.List;
|
import java.util.Map;
|
import java.util.UUID;
|
|
@Service
|
public class DmsMeilisearchClient {
|
public static final String HIGHLIGHT_START = "[[[DMS_HIGHLIGHT_START]]]";
|
public static final String HIGHLIGHT_END = "[[[DMS_HIGHLIGHT_END]]]";
|
|
private final DmsSearchProperties properties;
|
private final ObjectMapper objectMapper;
|
private final HttpClient httpClient;
|
|
public DmsMeilisearchClient(DmsSearchProperties properties, ObjectMapper objectMapper) {
|
this.properties = properties;
|
this.objectMapper = objectMapper;
|
this.httpClient = HttpClient.newBuilder()
|
.connectTimeout(Duration.ofMillis(
|
Math.max(1, properties.getMeilisearch().getConnectTimeoutMs())))
|
.build();
|
}
|
|
public void upsert(DmsSearchIndexDocument document) throws IOException {
|
JsonNode response = send("POST", "/indexes/" + indexName(document.getTenantId())
|
+ "/documents?primaryKey=id", Collections.singletonList(document), false);
|
if (response == null) {
|
throw new IOException("Meilisearch 写入没有返回任务信息");
|
}
|
}
|
|
/** Null means an absent index; a returned task ID must be polled before acknowledging cleanup. */
|
public Long deleteDocument(String tenantId, UUID fileVersionId) throws IOException {
|
JsonNode response = send("DELETE", "/indexes/" + indexName(tenantId)
|
+ "/documents/" + fileVersionId, null, true);
|
if (response == null) return null;
|
if (!response.path("taskUid").canConvertToLong()) throw new IOException("Meilisearch 删除没有返回任务编号");
|
return response.path("taskUid").longValue();
|
}
|
|
public String taskStatus(long taskId) throws IOException {
|
JsonNode response = send("GET", "/tasks/" + taskId, null, true);
|
return response == null ? "missing" : response.path("status").asText();
|
}
|
|
public DmsMeilisearchSearchPage search(String tenantId, String keyword,
|
int offset, int limit) throws IOException {
|
Map<String, Object> body = new LinkedHashMap<>();
|
body.put("q", keyword);
|
body.put("offset", offset);
|
body.put("limit", limit);
|
body.put("attributesToRetrieve", Arrays.asList(
|
"id", "tenant_id", "document_id", "file_version_id", "folder_id",
|
"folder_path", "document_no", "title", "document_type",
|
"responsible_dept_id", "version_label", "file_name", "mime_type",
|
"lifecycle_status", "created_at", "content_checksum"));
|
body.put("attributesToHighlight", Arrays.asList(
|
"document_no", "title", "file_name", "version_label", "content"));
|
body.put("attributesToCrop", Collections.singletonList("content:45"));
|
body.put("cropMarker", "...");
|
body.put("highlightPreTag", HIGHLIGHT_START);
|
body.put("highlightPostTag", HIGHLIGHT_END);
|
|
JsonNode response = send("POST", "/indexes/" + indexName(tenantId) + "/search",
|
body, true);
|
if (response == null) {
|
return new DmsMeilisearchSearchPage(Collections.emptyList(), 0);
|
}
|
List<DmsMeilisearchSearchHit> hits = new ArrayList<>();
|
for (JsonNode rawHit : response.path("hits")) {
|
ObjectNode documentNode = rawHit.deepCopy();
|
JsonNode formatted = documentNode.remove("_formatted");
|
documentNode.remove("_matchesPosition");
|
DmsSearchIndexDocument document = objectMapper.treeToValue(
|
documentNode, DmsSearchIndexDocument.class);
|
String title = formatted == null
|
? document.getTitle() : formatted.path("title").asText(document.getTitle());
|
String content = formatted == null ? "" : formatted.path("content").asText("");
|
hits.add(new DmsMeilisearchSearchHit(document, title, content));
|
}
|
int total = response.path("estimatedTotalHits").asInt(offset + hits.size());
|
return new DmsMeilisearchSearchPage(hits, total);
|
}
|
|
private JsonNode send(String method, String path, Object body,
|
boolean missingIndexAsEmpty) throws IOException {
|
HttpRequest.Builder request = HttpRequest.newBuilder(endpoint(path))
|
.timeout(Duration.ofMillis(
|
Math.max(1, properties.getMeilisearch().getReadTimeoutMs())))
|
.header("Accept", "application/json");
|
if (StringUtils.hasText(properties.getMeilisearch().getApiKey())) {
|
request.header("Authorization", "Bearer "
|
+ properties.getMeilisearch().getApiKey().trim());
|
}
|
if (body == null) {
|
request.method(method, HttpRequest.BodyPublishers.noBody());
|
} else {
|
request.header("Content-Type", "application/json");
|
request.method(method, HttpRequest.BodyPublishers.ofString(
|
objectMapper.writeValueAsString(body)));
|
}
|
HttpResponse<String> response;
|
try {
|
response = httpClient.send(request.build(), HttpResponse.BodyHandlers.ofString());
|
} catch (InterruptedException exception) {
|
Thread.currentThread().interrupt();
|
throw new IOException("Meilisearch 请求被中断", exception);
|
}
|
if (missingIndexAsEmpty && response.statusCode() == 404) {
|
return null;
|
}
|
if (response.statusCode() < 200 || response.statusCode() >= 300) {
|
throw new IOException("Meilisearch 请求失败,HTTP " + response.statusCode());
|
}
|
return StringUtils.hasText(response.body()) ? objectMapper.readTree(response.body()) : null;
|
}
|
|
private URI endpoint(String path) throws IOException {
|
String baseUrl = properties.getMeilisearch().getBaseUrl();
|
if (!StringUtils.hasText(baseUrl)) {
|
throw new IOException("Meilisearch 地址未配置");
|
}
|
return URI.create(baseUrl.trim().replaceAll("/+$", "") + path);
|
}
|
|
private String indexName(String tenantId) throws IOException {
|
String prefix = properties.getMeilisearch().getIndexName();
|
if (!StringUtils.hasText(prefix) || !prefix.matches("[A-Za-z0-9_-]+")) {
|
throw new IOException("Meilisearch 索引名称无效");
|
}
|
if (!StringUtils.hasText(tenantId)) {
|
throw new IOException("全文检索任务缺少租户");
|
}
|
String tenantKey = UUID.nameUUIDFromBytes(
|
tenantId.trim().getBytes(StandardCharsets.UTF_8)).toString().replace("-", "");
|
return prefix + "_" + tenantKey;
|
}
|
}
|