mirror of
https://github.com/nagisa77/OpenIsle.git
synced 2026-02-26 08:00:48 +08:00
@@ -1,368 +0,0 @@
|
||||
package com.openisle.search;
|
||||
|
||||
import com.openisle.config.OpenSearchProperties;
|
||||
import com.openisle.service.SearchService.SearchResult;
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.opensearch.client.opensearch.OpenSearchClient;
|
||||
import org.opensearch.client.opensearch._types.SortOrder;
|
||||
import org.opensearch.client.opensearch._types.query_dsl.MultiMatchQueryType;
|
||||
import org.opensearch.client.opensearch._types.query_dsl.Query;
|
||||
import org.opensearch.client.opensearch.core.MsearchRequest;
|
||||
import org.opensearch.client.opensearch.core.MsearchResponse;
|
||||
import org.opensearch.client.opensearch.core.SearchRequest;
|
||||
import org.opensearch.client.opensearch.core.SearchResponse;
|
||||
import org.opensearch.client.opensearch.core.search.HighlightField;
|
||||
import org.opensearch.client.opensearch.core.search.Hit;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Slf4j
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
@ConditionalOnProperty(prefix = "opensearch", name = "enabled", havingValue = "true")
|
||||
public class OpenSearchGateway {
|
||||
|
||||
private final OpenSearchClient client;
|
||||
private final OpenSearchProperties properties;
|
||||
|
||||
public enum PostSearchMode {
|
||||
TITLE_AND_CONTENT,
|
||||
TITLE_ONLY,
|
||||
CONTENT_ONLY,
|
||||
}
|
||||
|
||||
public List<Long> searchUserIds(String keyword) {
|
||||
return searchForIds(
|
||||
properties.getUsersIndex(),
|
||||
keyword,
|
||||
List.of("username^2", "displayName^1.5", "introduction"),
|
||||
null
|
||||
);
|
||||
}
|
||||
|
||||
public List<Long> searchPostIds(String keyword, PostSearchMode mode) {
|
||||
List<String> fields;
|
||||
switch (mode) {
|
||||
case TITLE_ONLY:
|
||||
fields = List.of("title^2");
|
||||
break;
|
||||
case CONTENT_ONLY:
|
||||
fields = List.of("content");
|
||||
break;
|
||||
default:
|
||||
fields = List.of("title^2", "content");
|
||||
}
|
||||
return searchForIds(
|
||||
properties.getPostsIndex(),
|
||||
keyword,
|
||||
fields,
|
||||
Query.of(q -> q.match(m -> m.field("status").query("PUBLISHED")))
|
||||
);
|
||||
}
|
||||
|
||||
public List<Long> searchCommentIds(String keyword) {
|
||||
return searchForIds(
|
||||
properties.getCommentsIndex(),
|
||||
keyword,
|
||||
List.of("content", "postTitle", "author"),
|
||||
null
|
||||
);
|
||||
}
|
||||
|
||||
public List<Long> searchCategoryIds(String keyword) {
|
||||
return searchForIds(
|
||||
properties.getCategoriesIndex(),
|
||||
keyword,
|
||||
List.of("name^2", "description"),
|
||||
null
|
||||
);
|
||||
}
|
||||
|
||||
public List<Long> searchTagIds(String keyword) {
|
||||
return searchForIds(
|
||||
properties.getTagsIndex(),
|
||||
keyword,
|
||||
List.of("name^2", "description"),
|
||||
Query.of(q -> q.match(m -> m.field("approved").query(true)))
|
||||
);
|
||||
}
|
||||
|
||||
public List<SearchResult> globalSearch(String keyword, int snippetLength) {
|
||||
try {
|
||||
MsearchRequest.Builder builder = new MsearchRequest.Builder();
|
||||
|
||||
builder.searches(s ->
|
||||
s
|
||||
.header(h -> h.index(properties.getUsersIndex()))
|
||||
.body(searchBody(keyword, List.of("username^2", "displayName", "introduction"), null))
|
||||
);
|
||||
builder.searches(s ->
|
||||
s
|
||||
.header(h -> h.index(properties.getCategoriesIndex()))
|
||||
.body(searchBody(keyword, List.of("name^2", "description"), null))
|
||||
);
|
||||
builder.searches(s ->
|
||||
s
|
||||
.header(h -> h.index(properties.getTagsIndex()))
|
||||
.body(
|
||||
searchBody(
|
||||
keyword,
|
||||
List.of("name^2", "description"),
|
||||
Query.of(q -> q.match(m -> m.field("approved").query(true)))
|
||||
)
|
||||
)
|
||||
);
|
||||
builder.searches(s ->
|
||||
s
|
||||
.header(h -> h.index(properties.getPostsIndex()))
|
||||
.body(
|
||||
searchBody(
|
||||
keyword,
|
||||
List.of("title^2", "content", "category", "tags"),
|
||||
Query.of(q -> q.match(m -> m.field("status").query("PUBLISHED")))
|
||||
)
|
||||
)
|
||||
);
|
||||
builder.searches(s ->
|
||||
s
|
||||
.header(h -> h.index(properties.getCommentsIndex()))
|
||||
.body(searchBody(keyword, List.of("content", "postTitle", "author"), null))
|
||||
);
|
||||
|
||||
MsearchResponse<Map<String, Object>> response = client.msearch(builder.build(), Map.class);
|
||||
|
||||
List<SearchResult> results = new ArrayList<>();
|
||||
int snippetLimit = snippetLength >= 0
|
||||
? snippetLength
|
||||
: properties.getHighlightFallbackLength();
|
||||
|
||||
// Order corresponds to request order
|
||||
List<String> types = List.of("user", "category", "tag", "post", "comment");
|
||||
for (int i = 0; i < response.responses().size(); i++) {
|
||||
var item = response.responses().get(i);
|
||||
if (item.isFailure()) {
|
||||
log.warn("OpenSearch multi search failed for {}: {}", types.get(i), item.error());
|
||||
continue;
|
||||
}
|
||||
for (Hit<Map<String, Object>> hit : item.result().hits().hits()) {
|
||||
String type = types.get(i);
|
||||
Long id = hit.id() != null ? Long.valueOf(hit.id()) : null;
|
||||
Map<String, List<String>> highlight = hit.highlight() != null
|
||||
? hit.highlight()
|
||||
: Map.of();
|
||||
Map<String, Object> source = hit.source() != null ? hit.source() : Map.of();
|
||||
String text = firstHighlight(
|
||||
highlight,
|
||||
List.of("title", "username", "name", "postTitle")
|
||||
);
|
||||
if (text == null) {
|
||||
text = optionalString(
|
||||
source,
|
||||
switch (type) {
|
||||
case "user" -> "username";
|
||||
case "post" -> "title";
|
||||
case "comment" -> "postTitle";
|
||||
default -> "name";
|
||||
}
|
||||
);
|
||||
}
|
||||
String subText = null;
|
||||
String extra = null;
|
||||
Long postId = null;
|
||||
|
||||
if ("user".equals(type)) {
|
||||
subText = optionalString(source, "displayName");
|
||||
extra = snippetFromHighlight(
|
||||
highlight,
|
||||
List.of("introduction"),
|
||||
optionalString(source, "introduction"),
|
||||
snippetLimit
|
||||
);
|
||||
} else if ("category".equals(type) || "tag".equals(type)) {
|
||||
extra = snippetFromHighlight(
|
||||
highlight,
|
||||
List.of("description"),
|
||||
optionalString(source, "description"),
|
||||
snippetLimit
|
||||
);
|
||||
} else if ("post".equals(type)) {
|
||||
subText = optionalString(source, "category");
|
||||
extra = snippetFromHighlight(
|
||||
highlight,
|
||||
List.of("content"),
|
||||
optionalString(source, "content"),
|
||||
snippetLimit
|
||||
);
|
||||
} else if ("comment".equals(type)) {
|
||||
subText = optionalString(source, "author");
|
||||
postId = optionalLong(source, "postId");
|
||||
extra = snippetFromHighlight(
|
||||
highlight,
|
||||
List.of("content"),
|
||||
optionalString(source, "content"),
|
||||
snippetLimit
|
||||
);
|
||||
}
|
||||
|
||||
results.add(new SearchResult(type, id, text, subText, extra, postId));
|
||||
}
|
||||
}
|
||||
return results;
|
||||
} catch (IOException e) {
|
||||
throw new IllegalStateException("OpenSearch global search failed", e);
|
||||
}
|
||||
}
|
||||
|
||||
private List<Long> searchForIds(String index, String keyword, List<String> fields, Query filter) {
|
||||
try {
|
||||
SearchRequest request = SearchRequest.builder()
|
||||
.index(index)
|
||||
.size(properties.getMaxResults())
|
||||
.query(q ->
|
||||
q.bool(b -> {
|
||||
b.must(
|
||||
Query.of(m ->
|
||||
m.multiMatch(mm ->
|
||||
mm.query(keyword).fields(fields).type(MultiMatchQueryType.BestFields)
|
||||
)
|
||||
)
|
||||
);
|
||||
if (filter != null) {
|
||||
b.filter(filter);
|
||||
}
|
||||
return b;
|
||||
})
|
||||
)
|
||||
.sort(s -> s.score(o -> o.order(SortOrder.Desc)))
|
||||
.build();
|
||||
SearchResponse<Map<String, Object>> response = client.search(request, Map.class);
|
||||
return response
|
||||
.hits()
|
||||
.hits()
|
||||
.stream()
|
||||
.map(Hit::id)
|
||||
.filter(id -> id != null && !id.isBlank())
|
||||
.map(Long::valueOf)
|
||||
.collect(Collectors.toList());
|
||||
} catch (IOException e) {
|
||||
throw new IllegalStateException("OpenSearch search failed for index " + index, e);
|
||||
}
|
||||
}
|
||||
|
||||
private SearchRequest searchBody(String keyword, List<String> fields, Query filter) {
|
||||
return SearchRequest.builder()
|
||||
.size(10)
|
||||
.query(q ->
|
||||
q.bool(b -> {
|
||||
b.must(
|
||||
Query.of(m ->
|
||||
m.multiMatch(mm ->
|
||||
mm.query(keyword).fields(fields).type(MultiMatchQueryType.BestFields)
|
||||
)
|
||||
)
|
||||
);
|
||||
if (filter != null) {
|
||||
b.filter(filter);
|
||||
}
|
||||
return b;
|
||||
})
|
||||
)
|
||||
.highlight(h ->
|
||||
h
|
||||
.preTags("<em>")
|
||||
.postTags("</em>")
|
||||
.fields(
|
||||
"title",
|
||||
HighlightField.of(f -> f.fragmentSize(properties.getHighlightFallbackLength()))
|
||||
)
|
||||
.fields(
|
||||
"username",
|
||||
HighlightField.of(f -> f.fragmentSize(properties.getHighlightFallbackLength()))
|
||||
)
|
||||
.fields(
|
||||
"name",
|
||||
HighlightField.of(f -> f.fragmentSize(properties.getHighlightFallbackLength()))
|
||||
)
|
||||
.fields(
|
||||
"postTitle",
|
||||
HighlightField.of(f -> f.fragmentSize(properties.getHighlightFallbackLength()))
|
||||
)
|
||||
.fields(
|
||||
"content",
|
||||
HighlightField.of(f -> f.fragmentSize(properties.getHighlightFallbackLength()))
|
||||
)
|
||||
.fields(
|
||||
"description",
|
||||
HighlightField.of(f -> f.fragmentSize(properties.getHighlightFallbackLength()))
|
||||
)
|
||||
.fields(
|
||||
"introduction",
|
||||
HighlightField.of(f -> f.fragmentSize(properties.getHighlightFallbackLength()))
|
||||
)
|
||||
)
|
||||
.build();
|
||||
}
|
||||
|
||||
private String firstHighlight(Map<String, List<String>> highlight, List<String> keys) {
|
||||
for (String key : keys) {
|
||||
List<String> values = highlight.get(key);
|
||||
if (values != null && !values.isEmpty()) {
|
||||
return values.get(0);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private String snippetFromHighlight(
|
||||
Map<String, List<String>> highlight,
|
||||
List<String> keys,
|
||||
String fallback,
|
||||
int snippetLength
|
||||
) {
|
||||
for (String key : keys) {
|
||||
List<String> values = highlight.get(key);
|
||||
if (values != null && !values.isEmpty()) {
|
||||
return values.get(0);
|
||||
}
|
||||
}
|
||||
if (fallback == null) {
|
||||
return null;
|
||||
}
|
||||
if (snippetLength < 0) {
|
||||
return fallback;
|
||||
}
|
||||
return fallback.length() > snippetLength ? fallback.substring(0, snippetLength) : fallback;
|
||||
}
|
||||
|
||||
private String optionalString(Map<String, Object> source, String key) {
|
||||
if (source == null) {
|
||||
return null;
|
||||
}
|
||||
Object value = source.get(key);
|
||||
return value != null ? value.toString() : null;
|
||||
}
|
||||
|
||||
private Long optionalLong(Map<String, Object> source, String key) {
|
||||
if (source == null) {
|
||||
return null;
|
||||
}
|
||||
Object value = source.get(key);
|
||||
if (value instanceof Number number) {
|
||||
return number.longValue();
|
||||
}
|
||||
if (value instanceof String str && !str.isBlank()) {
|
||||
try {
|
||||
return Long.valueOf(str);
|
||||
} catch (NumberFormatException e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -1,115 +0,0 @@
|
||||
package com.openisle.search;
|
||||
|
||||
import com.openisle.config.OpenSearchProperties;
|
||||
import java.io.IOException;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.opensearch.client.opensearch.OpenSearchClient;
|
||||
import org.opensearch.client.opensearch._types.mapping.Property;
|
||||
import org.opensearch.client.opensearch._types.mapping.TypeMapping;
|
||||
import org.opensearch.client.opensearch.indices.CreateIndexRequest;
|
||||
import org.opensearch.client.opensearch.indices.ExistsRequest;
|
||||
import org.opensearch.client.opensearch.indices.IndexSettings;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
import org.springframework.context.event.ContextRefreshedEvent;
|
||||
import org.springframework.context.event.EventListener;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Slf4j
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
@ConditionalOnProperty(prefix = "opensearch", name = "enabled", havingValue = "true")
|
||||
public class OpenSearchIndexManager {
|
||||
|
||||
private final OpenSearchClient client;
|
||||
private final OpenSearchProperties properties;
|
||||
|
||||
@EventListener(ContextRefreshedEvent.class)
|
||||
public void initializeIndices() {
|
||||
ensureIndex(properties.getPostsIndex(), this::postsMapping);
|
||||
ensureIndex(properties.getCommentsIndex(), this::commentsMapping);
|
||||
ensureIndex(properties.getUsersIndex(), this::usersMapping);
|
||||
ensureIndex(properties.getCategoriesIndex(), this::categoriesMapping);
|
||||
ensureIndex(properties.getTagsIndex(), this::tagsMapping);
|
||||
}
|
||||
|
||||
private void ensureIndex(String indexName, MappingSupplier supplier) {
|
||||
try {
|
||||
boolean exists = client.indices().exists(ExistsRequest.of(e -> e.index(indexName))).value();
|
||||
if (!exists) {
|
||||
log.info("Creating OpenSearch index {}", indexName);
|
||||
CreateIndexRequest request = CreateIndexRequest.builder()
|
||||
.index(indexName)
|
||||
.mappings(supplier.mapping())
|
||||
.settings(IndexSettings.of(s -> s.numberOfReplicas("1").numberOfShards("1")))
|
||||
.build();
|
||||
client.indices().create(request);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
throw new IllegalStateException("Failed to ensure index " + indexName, e);
|
||||
}
|
||||
}
|
||||
|
||||
private TypeMapping postsMapping() {
|
||||
return TypeMapping.builder()
|
||||
.properties(
|
||||
"title",
|
||||
Property.of(p -> p.text(t -> t.analyzer("standard").fields("raw", f -> f.keyword(k -> k))))
|
||||
)
|
||||
.properties("content", Property.of(p -> p.text(t -> t.analyzer("standard"))))
|
||||
.properties("author", Property.of(p -> p.keyword(k -> k)))
|
||||
.properties("category", Property.of(p -> p.keyword(k -> k)))
|
||||
.properties("tags", Property.of(p -> p.keyword(k -> k)))
|
||||
.properties("status", Property.of(p -> p.keyword(k -> k)))
|
||||
.properties("createdAt", Property.of(p -> p.date(d -> d)))
|
||||
.build();
|
||||
}
|
||||
|
||||
private TypeMapping commentsMapping() {
|
||||
return TypeMapping.builder()
|
||||
.properties("content", Property.of(p -> p.text(t -> t.analyzer("standard"))))
|
||||
.properties("author", Property.of(p -> p.keyword(k -> k)))
|
||||
.properties("postTitle", Property.of(p -> p.text(t -> t.analyzer("standard"))))
|
||||
.properties("postId", Property.of(p -> p.long_(l -> l)))
|
||||
.properties("createdAt", Property.of(p -> p.date(d -> d)))
|
||||
.build();
|
||||
}
|
||||
|
||||
private TypeMapping usersMapping() {
|
||||
return TypeMapping.builder()
|
||||
.properties(
|
||||
"username",
|
||||
Property.of(p -> p.text(t -> t.analyzer("standard").fields("raw", f -> f.keyword(k -> k))))
|
||||
)
|
||||
.properties("introduction", Property.of(p -> p.text(t -> t.analyzer("standard"))))
|
||||
.properties("displayName", Property.of(p -> p.text(t -> t.analyzer("standard"))))
|
||||
.properties("createdAt", Property.of(p -> p.date(d -> d)))
|
||||
.build();
|
||||
}
|
||||
|
||||
private TypeMapping categoriesMapping() {
|
||||
return TypeMapping.builder()
|
||||
.properties(
|
||||
"name",
|
||||
Property.of(p -> p.text(t -> t.analyzer("standard").fields("raw", f -> f.keyword(k -> k))))
|
||||
)
|
||||
.properties("description", Property.of(p -> p.text(t -> t.analyzer("standard"))))
|
||||
.build();
|
||||
}
|
||||
|
||||
private TypeMapping tagsMapping() {
|
||||
return TypeMapping.builder()
|
||||
.properties(
|
||||
"name",
|
||||
Property.of(p -> p.text(t -> t.analyzer("standard").fields("raw", f -> f.keyword(k -> k))))
|
||||
)
|
||||
.properties("description", Property.of(p -> p.text(t -> t.analyzer("standard"))))
|
||||
.properties("approved", Property.of(p -> p.boolean_(b -> b)))
|
||||
.build();
|
||||
}
|
||||
|
||||
@FunctionalInterface
|
||||
private interface MappingSupplier {
|
||||
TypeMapping mapping();
|
||||
}
|
||||
}
|
||||
@@ -1,166 +0,0 @@
|
||||
package com.openisle.search;
|
||||
|
||||
import com.openisle.config.OpenSearchProperties;
|
||||
import com.openisle.model.Category;
|
||||
import com.openisle.model.Comment;
|
||||
import com.openisle.model.Post;
|
||||
import com.openisle.model.Tag;
|
||||
import com.openisle.model.User;
|
||||
import java.io.IOException;
|
||||
import java.time.ZoneOffset;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.opensearch.client.opensearch.OpenSearchClient;
|
||||
import org.opensearch.client.opensearch.core.DeleteRequest;
|
||||
import org.opensearch.client.opensearch.core.IndexRequest;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.transaction.support.TransactionSynchronization;
|
||||
import org.springframework.transaction.support.TransactionSynchronizationManager;
|
||||
|
||||
@Slf4j
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
@ConditionalOnProperty(prefix = "opensearch", name = "enabled", havingValue = "true")
|
||||
public class OpenSearchIndexer {
|
||||
|
||||
private final OpenSearchClient client;
|
||||
private final OpenSearchProperties properties;
|
||||
|
||||
public void indexPost(Post post) {
|
||||
runAfterCommit(() -> {
|
||||
Map<String, Object> document = new HashMap<>();
|
||||
document.put("title", post.getTitle());
|
||||
document.put("content", post.getContent());
|
||||
document.put("author", post.getAuthor() != null ? post.getAuthor().getUsername() : null);
|
||||
document.put("category", post.getCategory() != null ? post.getCategory().getName() : null);
|
||||
document.put(
|
||||
"tags",
|
||||
post.getTags() != null
|
||||
? post.getTags().stream().map(Tag::getName).toList()
|
||||
: java.util.List.of()
|
||||
);
|
||||
document.put("status", post.getStatus() != null ? post.getStatus().name() : null);
|
||||
if (post.getCreatedAt() != null) {
|
||||
document.put("createdAt", post.getCreatedAt().atOffset(ZoneOffset.UTC));
|
||||
}
|
||||
indexDocument(properties.getPostsIndex(), post.getId(), document);
|
||||
});
|
||||
}
|
||||
|
||||
public void deletePost(Long postId) {
|
||||
runAfterCommit(() -> deleteDocument(properties.getPostsIndex(), postId));
|
||||
}
|
||||
|
||||
public void indexComment(Comment comment) {
|
||||
runAfterCommit(() -> {
|
||||
Map<String, Object> document = new HashMap<>();
|
||||
document.put("content", comment.getContent());
|
||||
document.put(
|
||||
"author",
|
||||
comment.getAuthor() != null ? comment.getAuthor().getUsername() : null
|
||||
);
|
||||
if (comment.getPost() != null) {
|
||||
document.put("postId", comment.getPost().getId());
|
||||
document.put("postTitle", comment.getPost().getTitle());
|
||||
}
|
||||
if (comment.getCreatedAt() != null) {
|
||||
document.put("createdAt", comment.getCreatedAt().atOffset(ZoneOffset.UTC));
|
||||
}
|
||||
indexDocument(properties.getCommentsIndex(), comment.getId(), document);
|
||||
});
|
||||
}
|
||||
|
||||
public void deleteComment(Long commentId) {
|
||||
runAfterCommit(() -> deleteDocument(properties.getCommentsIndex(), commentId));
|
||||
}
|
||||
|
||||
public void indexUser(User user) {
|
||||
runAfterCommit(() -> {
|
||||
Map<String, Object> document = new HashMap<>();
|
||||
document.put("username", user.getUsername());
|
||||
document.put("displayName", user.getDisplayName());
|
||||
document.put("introduction", user.getIntroduction());
|
||||
if (user.getCreatedAt() != null) {
|
||||
document.put("createdAt", user.getCreatedAt().atOffset(ZoneOffset.UTC));
|
||||
}
|
||||
indexDocument(properties.getUsersIndex(), user.getId(), document);
|
||||
});
|
||||
}
|
||||
|
||||
public void deleteUser(Long userId) {
|
||||
runAfterCommit(() -> deleteDocument(properties.getUsersIndex(), userId));
|
||||
}
|
||||
|
||||
public void indexCategory(Category category) {
|
||||
runAfterCommit(() -> {
|
||||
Map<String, Object> document = new HashMap<>();
|
||||
document.put("name", category.getName());
|
||||
document.put("description", category.getDescription());
|
||||
indexDocument(properties.getCategoriesIndex(), category.getId(), document);
|
||||
});
|
||||
}
|
||||
|
||||
public void deleteCategory(Long categoryId) {
|
||||
runAfterCommit(() -> deleteDocument(properties.getCategoriesIndex(), categoryId));
|
||||
}
|
||||
|
||||
public void indexTag(Tag tag) {
|
||||
runAfterCommit(() -> {
|
||||
Map<String, Object> document = new HashMap<>();
|
||||
document.put("name", tag.getName());
|
||||
document.put("description", tag.getDescription());
|
||||
document.put("approved", Boolean.TRUE.equals(tag.getApproved()));
|
||||
indexDocument(properties.getTagsIndex(), tag.getId(), document);
|
||||
});
|
||||
}
|
||||
|
||||
public void deleteTag(Long tagId) {
|
||||
runAfterCommit(() -> deleteDocument(properties.getTagsIndex(), tagId));
|
||||
}
|
||||
|
||||
private void indexDocument(String index, Long id, Map<String, Object> document) {
|
||||
if (id == null) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
IndexRequest<Map<String, Object>> request = IndexRequest.<Map<String, Object>>builder()
|
||||
.index(index)
|
||||
.id(id.toString())
|
||||
.document(document)
|
||||
.build();
|
||||
client.index(request);
|
||||
} catch (IOException e) {
|
||||
log.error("Failed to index document {} in {}", id, index, e);
|
||||
}
|
||||
}
|
||||
|
||||
private void deleteDocument(String index, Long id) {
|
||||
if (id == null) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
DeleteRequest request = DeleteRequest.of(d -> d.index(index).id(id.toString()));
|
||||
client.delete(request);
|
||||
} catch (IOException e) {
|
||||
log.error("Failed to delete document {} in {}", id, index, e);
|
||||
}
|
||||
}
|
||||
|
||||
private void runAfterCommit(Runnable runnable) {
|
||||
if (TransactionSynchronizationManager.isSynchronizationActive()) {
|
||||
TransactionSynchronizationManager.registerSynchronization(
|
||||
new TransactionSynchronization() {
|
||||
@Override
|
||||
public void afterCommit() {
|
||||
runnable.run();
|
||||
}
|
||||
}
|
||||
);
|
||||
} else {
|
||||
runnable.run();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,84 +0,0 @@
|
||||
package com.openisle.search;
|
||||
|
||||
import com.openisle.model.Category;
|
||||
import com.openisle.model.Comment;
|
||||
import com.openisle.model.Post;
|
||||
import com.openisle.model.Tag;
|
||||
import com.openisle.model.User;
|
||||
import jakarta.persistence.PostPersist;
|
||||
import jakarta.persistence.PostRemove;
|
||||
import jakarta.persistence.PostUpdate;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Slf4j
|
||||
public class SearchEntityListener {
|
||||
|
||||
private static volatile OpenSearchIndexer indexer;
|
||||
|
||||
public static void registerIndexer(OpenSearchIndexer openSearchIndexer) {
|
||||
indexer = openSearchIndexer;
|
||||
}
|
||||
|
||||
@PostPersist
|
||||
@PostUpdate
|
||||
public void afterSave(Object entity) {
|
||||
if (indexer == null) {
|
||||
return;
|
||||
}
|
||||
if (entity instanceof Post post) {
|
||||
indexer.indexPost(post);
|
||||
} else if (entity instanceof Comment comment) {
|
||||
indexer.indexComment(comment);
|
||||
} else if (entity instanceof User user) {
|
||||
indexer.indexUser(user);
|
||||
} else if (entity instanceof Category category) {
|
||||
indexer.indexCategory(category);
|
||||
} else if (entity instanceof Tag tag) {
|
||||
indexer.indexTag(tag);
|
||||
}
|
||||
}
|
||||
|
||||
@PostRemove
|
||||
public void afterDelete(Object entity) {
|
||||
if (indexer == null) {
|
||||
return;
|
||||
}
|
||||
if (entity instanceof Post post) {
|
||||
Long id = post.getId();
|
||||
if (id != null) {
|
||||
indexer.deletePost(id);
|
||||
}
|
||||
} else if (entity instanceof Comment comment) {
|
||||
Long id = comment.getId();
|
||||
if (id != null) {
|
||||
indexer.deleteComment(id);
|
||||
}
|
||||
} else if (entity instanceof User user) {
|
||||
Long id = user.getId();
|
||||
if (id != null) {
|
||||
indexer.deleteUser(id);
|
||||
}
|
||||
} else if (entity instanceof Category category) {
|
||||
Long id = category.getId();
|
||||
if (id != null) {
|
||||
indexer.deleteCategory(id);
|
||||
}
|
||||
} else if (entity instanceof Tag tag) {
|
||||
Long id = tag.getId();
|
||||
if (id != null) {
|
||||
indexer.deleteTag(id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Component
|
||||
@ConditionalOnProperty(prefix = "opensearch", name = "enabled", havingValue = "true")
|
||||
public static class Registrar {
|
||||
|
||||
public Registrar(OpenSearchIndexer openSearchIndexer) {
|
||||
SearchEntityListener.registerIndexer(openSearchIndexer);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user