mirror of
https://github.com/nagisa77/OpenIsle.git
synced 2026-07-16 09:36:08 +00:00
feat: opensearch init
This commit is contained in:
@@ -132,6 +132,19 @@
|
|||||||
<artifactId>springdoc-openapi-starter-webmvc-api</artifactId>
|
<artifactId>springdoc-openapi-starter-webmvc-api</artifactId>
|
||||||
<version>2.2.0</version>
|
<version>2.2.0</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<!-- 高阶 Java 客户端 -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.opensearch.client</groupId>
|
||||||
|
<artifactId>opensearch-java</artifactId>
|
||||||
|
<version>3.2.0</version>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<!-- 低阶 RestClient,提供 org.opensearch.client.RestClient 给你的 RestClientTransport 用 -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.opensearch.client</groupId>
|
||||||
|
<artifactId>opensearch-rest-client</artifactId>
|
||||||
|
<version>3.2.0</version>
|
||||||
|
</dependency>
|
||||||
</dependencies>
|
</dependencies>
|
||||||
|
|
||||||
<build>
|
<build>
|
||||||
|
|||||||
@@ -0,0 +1,14 @@
|
|||||||
|
package com.openisle.search;
|
||||||
|
|
||||||
|
public class NoopSearchIndexer implements SearchIndexer {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void indexDocument(String index, SearchDocument document) {
|
||||||
|
// no-op
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void deleteDocument(String index, Long id) {
|
||||||
|
// no-op
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
package com.openisle.search;
|
||||||
|
|
||||||
|
import org.apache.hc.client5.http.auth.AuthScope;
|
||||||
|
import org.apache.hc.client5.http.auth.UsernamePasswordCredentials;
|
||||||
|
import org.apache.hc.client5.http.impl.auth.BasicCredentialsProvider;
|
||||||
|
import org.apache.hc.core5.http.HttpHost;
|
||||||
|
import org.opensearch.client.RestClient;
|
||||||
|
import org.opensearch.client.RestClientBuilder;
|
||||||
|
import org.opensearch.client.json.jackson.JacksonJsonpMapper;
|
||||||
|
import org.opensearch.client.opensearch.OpenSearchClient;
|
||||||
|
import org.opensearch.client.transport.rest_client.RestClientTransport;
|
||||||
|
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
|
||||||
|
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
|
||||||
|
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||||
|
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
||||||
|
import org.springframework.context.annotation.Bean;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
import org.springframework.util.StringUtils;
|
||||||
|
|
||||||
|
@Configuration
|
||||||
|
@EnableConfigurationProperties(OpenSearchProperties.class)
|
||||||
|
public class OpenSearchConfig {
|
||||||
|
|
||||||
|
@Bean(destroyMethod = "close")
|
||||||
|
@ConditionalOnProperty(prefix = "app.search", name = "enabled", havingValue = "true")
|
||||||
|
public RestClient openSearchRestClient(OpenSearchProperties properties) {
|
||||||
|
RestClientBuilder builder = RestClient.builder(
|
||||||
|
new HttpHost(properties.getScheme(), properties.getHost(), properties.getPort())
|
||||||
|
);
|
||||||
|
if (StringUtils.hasText(properties.getUsername())) {
|
||||||
|
BasicCredentialsProvider credentialsProvider = new BasicCredentialsProvider();
|
||||||
|
credentialsProvider.setCredentials(
|
||||||
|
new AuthScope(properties.getHost(), properties.getPort()),
|
||||||
|
new UsernamePasswordCredentials(
|
||||||
|
properties.getUsername(),
|
||||||
|
properties.getPassword() != null ? properties.getPassword().toCharArray() : new char[0]
|
||||||
|
)
|
||||||
|
);
|
||||||
|
builder.setHttpClientConfigCallback(httpClientBuilder ->
|
||||||
|
httpClientBuilder.setDefaultCredentialsProvider(credentialsProvider)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return builder.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Bean(destroyMethod = "close")
|
||||||
|
@ConditionalOnBean(RestClient.class)
|
||||||
|
public RestClientTransport openSearchTransport(RestClient restClient) {
|
||||||
|
return new RestClientTransport(restClient, new JacksonJsonpMapper());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
@ConditionalOnBean(RestClientTransport.class)
|
||||||
|
public OpenSearchClient openSearchClient(RestClientTransport transport) {
|
||||||
|
return new OpenSearchClient(transport);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
@ConditionalOnBean(OpenSearchClient.class)
|
||||||
|
public SearchIndexInitializer searchIndexInitializer(
|
||||||
|
OpenSearchClient client,
|
||||||
|
OpenSearchProperties properties
|
||||||
|
) {
|
||||||
|
return new SearchIndexInitializer(client, properties);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
@ConditionalOnBean(OpenSearchClient.class)
|
||||||
|
public SearchIndexer openSearchIndexer(OpenSearchClient client, OpenSearchProperties properties) {
|
||||||
|
return new OpenSearchIndexer(client);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
@ConditionalOnMissingBean(SearchIndexer.class)
|
||||||
|
public SearchIndexer noopSearchIndexer() {
|
||||||
|
return new NoopSearchIndexer();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
package com.openisle.search;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
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.opensearch.client.opensearch.core.IndexResponse;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class OpenSearchIndexer implements SearchIndexer {
|
||||||
|
|
||||||
|
private final OpenSearchClient client;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void indexDocument(String index, SearchDocument document) {
|
||||||
|
if (document == null || document.entityId() == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
IndexRequest<SearchDocument> request = IndexRequest.of(builder ->
|
||||||
|
builder.index(index).id(document.entityId().toString()).document(document)
|
||||||
|
);
|
||||||
|
IndexResponse response = client.index(request);
|
||||||
|
if (log.isDebugEnabled()) {
|
||||||
|
log.debug(
|
||||||
|
"Indexed document {} into {} with result {}",
|
||||||
|
document.entityId(),
|
||||||
|
index,
|
||||||
|
response.result()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (IOException e) {
|
||||||
|
log.warn("Failed to index document {} into {}", document.entityId(), index, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void deleteDocument(String index, Long id) {
|
||||||
|
if (id == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
client.delete(DeleteRequest.of(builder -> builder.index(index).id(id.toString())));
|
||||||
|
} catch (IOException e) {
|
||||||
|
log.warn("Failed to delete document {} from {}", id, index, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
package com.openisle.search;
|
||||||
|
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.Setter;
|
||||||
|
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
@Setter
|
||||||
|
@ConfigurationProperties(prefix = "app.search")
|
||||||
|
public class OpenSearchProperties {
|
||||||
|
|
||||||
|
private boolean enabled = false;
|
||||||
|
private String host = "localhost";
|
||||||
|
private int port = 9200;
|
||||||
|
private String scheme = "http";
|
||||||
|
private String username;
|
||||||
|
private String password;
|
||||||
|
private String indexPrefix = "openisle";
|
||||||
|
private boolean initialize = true;
|
||||||
|
private int highlightFragmentSize = 200;
|
||||||
|
|
||||||
|
private Indices indices = new Indices();
|
||||||
|
|
||||||
|
public String postsIndex() {
|
||||||
|
return indexName(indices.posts);
|
||||||
|
}
|
||||||
|
|
||||||
|
public String commentsIndex() {
|
||||||
|
return indexName(indices.comments);
|
||||||
|
}
|
||||||
|
|
||||||
|
public String usersIndex() {
|
||||||
|
return indexName(indices.users);
|
||||||
|
}
|
||||||
|
|
||||||
|
public String categoriesIndex() {
|
||||||
|
return indexName(indices.categories);
|
||||||
|
}
|
||||||
|
|
||||||
|
public String tagsIndex() {
|
||||||
|
return indexName(indices.tags);
|
||||||
|
}
|
||||||
|
|
||||||
|
private String indexName(String suffix) {
|
||||||
|
if (indexPrefix == null || indexPrefix.isBlank()) {
|
||||||
|
return suffix;
|
||||||
|
}
|
||||||
|
return indexPrefix + "-" + suffix;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
@Setter
|
||||||
|
public static class Indices {
|
||||||
|
|
||||||
|
private String posts = "posts";
|
||||||
|
private String comments = "comments";
|
||||||
|
private String users = "users";
|
||||||
|
private String categories = "categories";
|
||||||
|
private String tags = "tags";
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
package com.openisle.search;
|
||||||
|
|
||||||
|
import java.time.Instant;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public record SearchDocument(
|
||||||
|
String type,
|
||||||
|
Long entityId,
|
||||||
|
String title,
|
||||||
|
String content,
|
||||||
|
String author,
|
||||||
|
String category,
|
||||||
|
List<String> tags,
|
||||||
|
Long postId,
|
||||||
|
Instant createdAt
|
||||||
|
) {}
|
||||||
@@ -0,0 +1,125 @@
|
|||||||
|
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 java.time.Instant;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.time.ZoneId;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
public final class SearchDocumentFactory {
|
||||||
|
|
||||||
|
private SearchDocumentFactory() {}
|
||||||
|
|
||||||
|
public static SearchDocument fromPost(Post post) {
|
||||||
|
if (post == null || post.getId() == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
List<String> tags = post.getTags() == null
|
||||||
|
? Collections.emptyList()
|
||||||
|
: post
|
||||||
|
.getTags()
|
||||||
|
.stream()
|
||||||
|
.map(Tag::getName)
|
||||||
|
.filter(Objects::nonNull)
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
return new SearchDocument(
|
||||||
|
"post",
|
||||||
|
post.getId(),
|
||||||
|
post.getTitle(),
|
||||||
|
post.getContent(),
|
||||||
|
post.getAuthor() != null ? post.getAuthor().getUsername() : null,
|
||||||
|
post.getCategory() != null ? post.getCategory().getName() : null,
|
||||||
|
tags,
|
||||||
|
post.getId(),
|
||||||
|
toInstant(post.getCreatedAt())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static SearchDocument fromComment(Comment comment) {
|
||||||
|
if (comment == null || comment.getId() == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
Post post = comment.getPost();
|
||||||
|
List<String> tags = post == null || post.getTags() == null
|
||||||
|
? Collections.emptyList()
|
||||||
|
: post
|
||||||
|
.getTags()
|
||||||
|
.stream()
|
||||||
|
.map(Tag::getName)
|
||||||
|
.filter(Objects::nonNull)
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
return new SearchDocument(
|
||||||
|
"comment",
|
||||||
|
comment.getId(),
|
||||||
|
post != null ? post.getTitle() : null,
|
||||||
|
comment.getContent(),
|
||||||
|
comment.getAuthor() != null ? comment.getAuthor().getUsername() : null,
|
||||||
|
post != null && post.getCategory() != null ? post.getCategory().getName() : null,
|
||||||
|
tags,
|
||||||
|
post != null ? post.getId() : null,
|
||||||
|
toInstant(comment.getCreatedAt())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static SearchDocument fromUser(User user) {
|
||||||
|
if (user == null || user.getId() == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return new SearchDocument(
|
||||||
|
"user",
|
||||||
|
user.getId(),
|
||||||
|
user.getUsername(),
|
||||||
|
user.getIntroduction(),
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
Collections.emptyList(),
|
||||||
|
null,
|
||||||
|
toInstant(user.getCreatedAt())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static SearchDocument fromCategory(Category category) {
|
||||||
|
if (category == null || category.getId() == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return new SearchDocument(
|
||||||
|
"category",
|
||||||
|
category.getId(),
|
||||||
|
category.getName(),
|
||||||
|
category.getDescription(),
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
Collections.emptyList(),
|
||||||
|
null,
|
||||||
|
null
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static SearchDocument fromTag(Tag tag) {
|
||||||
|
if (tag == null || tag.getId() == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return new SearchDocument(
|
||||||
|
"tag",
|
||||||
|
tag.getId(),
|
||||||
|
tag.getName(),
|
||||||
|
tag.getDescription(),
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
Collections.emptyList(),
|
||||||
|
null,
|
||||||
|
toInstant(tag.getCreatedAt())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Instant toInstant(LocalDateTime time) {
|
||||||
|
return time == null ? null : time.atZone(ZoneId.systemDefault()).toInstant();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
package com.openisle.search;
|
||||||
|
|
||||||
|
import com.openisle.search.event.DeleteDocumentEvent;
|
||||||
|
import com.openisle.search.event.IndexDocumentEvent;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
import org.springframework.transaction.event.TransactionPhase;
|
||||||
|
import org.springframework.transaction.event.TransactionalEventListener;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
|
@Component
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class SearchIndexEventListener {
|
||||||
|
|
||||||
|
private final SearchIndexer searchIndexer;
|
||||||
|
|
||||||
|
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT, fallbackExecution = true)
|
||||||
|
public void handleIndex(IndexDocumentEvent event) {
|
||||||
|
if (event == null || event.document() == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
searchIndexer.indexDocument(event.index(), event.document());
|
||||||
|
}
|
||||||
|
|
||||||
|
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT, fallbackExecution = true)
|
||||||
|
public void handleDelete(DeleteDocumentEvent event) {
|
||||||
|
if (event == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
searchIndexer.deleteDocument(event.index(), event.id());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,99 @@
|
|||||||
|
package com.openisle.search;
|
||||||
|
|
||||||
|
import com.openisle.model.Category;
|
||||||
|
import com.openisle.model.Comment;
|
||||||
|
import com.openisle.model.Post;
|
||||||
|
import com.openisle.model.PostStatus;
|
||||||
|
import com.openisle.model.Tag;
|
||||||
|
import com.openisle.model.User;
|
||||||
|
import com.openisle.search.event.DeleteDocumentEvent;
|
||||||
|
import com.openisle.search.event.IndexDocumentEvent;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.context.ApplicationEventPublisher;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
@Component
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class SearchIndexEventPublisher {
|
||||||
|
|
||||||
|
private final ApplicationEventPublisher publisher;
|
||||||
|
private final OpenSearchProperties properties;
|
||||||
|
|
||||||
|
public void publishPostSaved(Post post) {
|
||||||
|
if (!properties.isEnabled() || post == null || post.getStatus() != PostStatus.PUBLISHED) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
SearchDocument document = SearchDocumentFactory.fromPost(post);
|
||||||
|
if (document != null) {
|
||||||
|
publisher.publishEvent(new IndexDocumentEvent(properties.postsIndex(), document));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void publishPostDeleted(Long postId) {
|
||||||
|
if (!properties.isEnabled() || postId == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
publisher.publishEvent(new DeleteDocumentEvent(properties.postsIndex(), postId));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void publishCommentSaved(Comment comment) {
|
||||||
|
if (!properties.isEnabled() || comment == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
SearchDocument document = SearchDocumentFactory.fromComment(comment);
|
||||||
|
if (document != null) {
|
||||||
|
publisher.publishEvent(new IndexDocumentEvent(properties.commentsIndex(), document));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void publishCommentDeleted(Long commentId) {
|
||||||
|
if (!properties.isEnabled() || commentId == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
publisher.publishEvent(new DeleteDocumentEvent(properties.commentsIndex(), commentId));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void publishUserSaved(User user) {
|
||||||
|
if (!properties.isEnabled() || user == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
SearchDocument document = SearchDocumentFactory.fromUser(user);
|
||||||
|
if (document != null) {
|
||||||
|
publisher.publishEvent(new IndexDocumentEvent(properties.usersIndex(), document));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void publishCategorySaved(Category category) {
|
||||||
|
if (!properties.isEnabled() || category == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
SearchDocument document = SearchDocumentFactory.fromCategory(category);
|
||||||
|
if (document != null) {
|
||||||
|
publisher.publishEvent(new IndexDocumentEvent(properties.categoriesIndex(), document));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void publishCategoryDeleted(Long categoryId) {
|
||||||
|
if (!properties.isEnabled() || categoryId == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
publisher.publishEvent(new DeleteDocumentEvent(properties.categoriesIndex(), categoryId));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void publishTagSaved(Tag tag) {
|
||||||
|
if (!properties.isEnabled() || tag == null || !tag.isApproved()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
SearchDocument document = SearchDocumentFactory.fromTag(tag);
|
||||||
|
if (document != null) {
|
||||||
|
publisher.publishEvent(new IndexDocumentEvent(properties.tagsIndex(), document));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void publishTagDeleted(Long tagId) {
|
||||||
|
if (!properties.isEnabled() || tagId == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
publisher.publishEvent(new DeleteDocumentEvent(properties.tagsIndex(), tagId));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,102 @@
|
|||||||
|
package com.openisle.search;
|
||||||
|
|
||||||
|
import jakarta.annotation.PostConstruct;
|
||||||
|
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;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class SearchIndexInitializer {
|
||||||
|
|
||||||
|
private final OpenSearchClient client;
|
||||||
|
private final OpenSearchProperties properties;
|
||||||
|
|
||||||
|
@PostConstruct
|
||||||
|
public void initialize() {
|
||||||
|
if (!properties.isEnabled() || !properties.isInitialize()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
ensureIndex(properties.postsIndex(), this::postMapping);
|
||||||
|
ensureIndex(properties.commentsIndex(), this::commentMapping);
|
||||||
|
ensureIndex(properties.usersIndex(), this::userMapping);
|
||||||
|
ensureIndex(properties.categoriesIndex(), this::categoryMapping);
|
||||||
|
ensureIndex(properties.tagsIndex(), this::tagMapping);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ensureIndex(String index, java.util.function.Supplier<TypeMapping> mappingSupplier) {
|
||||||
|
try {
|
||||||
|
boolean exists = client
|
||||||
|
.indices()
|
||||||
|
.exists(builder -> builder.index(index))
|
||||||
|
.value();
|
||||||
|
if (exists) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
client.indices().create(builder -> builder.index(index).mappings(mappingSupplier.get()));
|
||||||
|
log.info("Created OpenSearch index {}", index);
|
||||||
|
} catch (IOException e) {
|
||||||
|
log.warn("Failed to initialize OpenSearch index {}", index, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private TypeMapping postMapping() {
|
||||||
|
return TypeMapping.of(builder ->
|
||||||
|
builder
|
||||||
|
.properties("type", Property.of(p -> p.keyword(k -> k)))
|
||||||
|
.properties("title", Property.of(p -> p.text(t -> t)))
|
||||||
|
.properties("content", Property.of(p -> p.text(t -> t)))
|
||||||
|
.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("postId", Property.of(p -> p.long_(l -> l)))
|
||||||
|
.properties("createdAt", Property.of(p -> p.date(d -> d)))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private TypeMapping commentMapping() {
|
||||||
|
return TypeMapping.of(builder ->
|
||||||
|
builder
|
||||||
|
.properties("type", Property.of(p -> p.keyword(k -> k)))
|
||||||
|
.properties("title", Property.of(p -> p.text(t -> t)))
|
||||||
|
.properties("content", Property.of(p -> p.text(t -> t)))
|
||||||
|
.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("postId", Property.of(p -> p.long_(l -> l)))
|
||||||
|
.properties("createdAt", Property.of(p -> p.date(d -> d)))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private TypeMapping userMapping() {
|
||||||
|
return TypeMapping.of(builder ->
|
||||||
|
builder
|
||||||
|
.properties("type", Property.of(p -> p.keyword(k -> k)))
|
||||||
|
.properties("title", Property.of(p -> p.text(t -> t)))
|
||||||
|
.properties("content", Property.of(p -> p.text(t -> t)))
|
||||||
|
.properties("createdAt", Property.of(p -> p.date(d -> d)))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private TypeMapping categoryMapping() {
|
||||||
|
return TypeMapping.of(builder ->
|
||||||
|
builder
|
||||||
|
.properties("type", Property.of(p -> p.keyword(k -> k)))
|
||||||
|
.properties("title", Property.of(p -> p.text(t -> t)))
|
||||||
|
.properties("content", Property.of(p -> p.text(t -> t)))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private TypeMapping tagMapping() {
|
||||||
|
return TypeMapping.of(builder ->
|
||||||
|
builder
|
||||||
|
.properties("type", Property.of(p -> p.keyword(k -> k)))
|
||||||
|
.properties("title", Property.of(p -> p.text(t -> t)))
|
||||||
|
.properties("content", Property.of(p -> p.text(t -> t)))
|
||||||
|
.properties("createdAt", Property.of(p -> p.date(d -> d)))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
package com.openisle.search;
|
||||||
|
|
||||||
|
public interface SearchIndexer {
|
||||||
|
void indexDocument(String index, SearchDocument document);
|
||||||
|
void deleteDocument(String index, Long id);
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
package com.openisle.search.event;
|
||||||
|
|
||||||
|
public record DeleteDocumentEvent(String index, Long id) {}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
package com.openisle.search.event;
|
||||||
|
|
||||||
|
import com.openisle.search.SearchDocument;
|
||||||
|
|
||||||
|
public record IndexDocumentEvent(String index, SearchDocument document) {}
|
||||||
@@ -3,6 +3,7 @@ package com.openisle.service;
|
|||||||
import com.openisle.config.CachingConfig;
|
import com.openisle.config.CachingConfig;
|
||||||
import com.openisle.model.Category;
|
import com.openisle.model.Category;
|
||||||
import com.openisle.repository.CategoryRepository;
|
import com.openisle.repository.CategoryRepository;
|
||||||
|
import com.openisle.search.SearchIndexEventPublisher;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import org.springframework.cache.annotation.CacheEvict;
|
import org.springframework.cache.annotation.CacheEvict;
|
||||||
@@ -14,6 +15,7 @@ import org.springframework.stereotype.Service;
|
|||||||
public class CategoryService {
|
public class CategoryService {
|
||||||
|
|
||||||
private final CategoryRepository categoryRepository;
|
private final CategoryRepository categoryRepository;
|
||||||
|
private final SearchIndexEventPublisher searchIndexEventPublisher;
|
||||||
|
|
||||||
@CacheEvict(value = CachingConfig.CATEGORY_CACHE_NAME, allEntries = true)
|
@CacheEvict(value = CachingConfig.CATEGORY_CACHE_NAME, allEntries = true)
|
||||||
public Category createCategory(String name, String description, String icon, String smallIcon) {
|
public Category createCategory(String name, String description, String icon, String smallIcon) {
|
||||||
@@ -22,7 +24,9 @@ public class CategoryService {
|
|||||||
category.setDescription(description);
|
category.setDescription(description);
|
||||||
category.setIcon(icon);
|
category.setIcon(icon);
|
||||||
category.setSmallIcon(smallIcon);
|
category.setSmallIcon(smallIcon);
|
||||||
return categoryRepository.save(category);
|
Category saved = categoryRepository.save(category);
|
||||||
|
searchIndexEventPublisher.publishCategorySaved(saved);
|
||||||
|
return saved;
|
||||||
}
|
}
|
||||||
|
|
||||||
@CacheEvict(value = CachingConfig.CATEGORY_CACHE_NAME, allEntries = true)
|
@CacheEvict(value = CachingConfig.CATEGORY_CACHE_NAME, allEntries = true)
|
||||||
@@ -48,12 +52,15 @@ public class CategoryService {
|
|||||||
if (smallIcon != null) {
|
if (smallIcon != null) {
|
||||||
category.setSmallIcon(smallIcon);
|
category.setSmallIcon(smallIcon);
|
||||||
}
|
}
|
||||||
return categoryRepository.save(category);
|
Category saved = categoryRepository.save(category);
|
||||||
|
searchIndexEventPublisher.publishCategorySaved(saved);
|
||||||
|
return saved;
|
||||||
}
|
}
|
||||||
|
|
||||||
@CacheEvict(value = CachingConfig.CATEGORY_CACHE_NAME, allEntries = true)
|
@CacheEvict(value = CachingConfig.CATEGORY_CACHE_NAME, allEntries = true)
|
||||||
public void deleteCategory(Long id) {
|
public void deleteCategory(Long id) {
|
||||||
categoryRepository.deleteById(id);
|
categoryRepository.deleteById(id);
|
||||||
|
searchIndexEventPublisher.publishCategoryDeleted(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
public Category getCategory(Long id) {
|
public Category getCategory(Long id) {
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import com.openisle.repository.PointHistoryRepository;
|
|||||||
import com.openisle.repository.PostRepository;
|
import com.openisle.repository.PostRepository;
|
||||||
import com.openisle.repository.ReactionRepository;
|
import com.openisle.repository.ReactionRepository;
|
||||||
import com.openisle.repository.UserRepository;
|
import com.openisle.repository.UserRepository;
|
||||||
|
import com.openisle.search.SearchIndexEventPublisher;
|
||||||
import com.openisle.service.NotificationService;
|
import com.openisle.service.NotificationService;
|
||||||
import com.openisle.service.PointService;
|
import com.openisle.service.PointService;
|
||||||
import com.openisle.service.SubscriptionService;
|
import com.openisle.service.SubscriptionService;
|
||||||
@@ -49,6 +50,7 @@ public class CommentService {
|
|||||||
private final PointHistoryRepository pointHistoryRepository;
|
private final PointHistoryRepository pointHistoryRepository;
|
||||||
private final PointService pointService;
|
private final PointService pointService;
|
||||||
private final ImageUploader imageUploader;
|
private final ImageUploader imageUploader;
|
||||||
|
private final SearchIndexEventPublisher searchIndexEventPublisher;
|
||||||
|
|
||||||
@CacheEvict(value = CachingConfig.POST_CACHE_NAME, allEntries = true)
|
@CacheEvict(value = CachingConfig.POST_CACHE_NAME, allEntries = true)
|
||||||
@Transactional
|
@Transactional
|
||||||
@@ -124,6 +126,7 @@ public class CommentService {
|
|||||||
}
|
}
|
||||||
notificationService.notifyMentions(content, author, post, comment);
|
notificationService.notifyMentions(content, author, post, comment);
|
||||||
log.debug("addComment finished for comment {}", comment.getId());
|
log.debug("addComment finished for comment {}", comment.getId());
|
||||||
|
searchIndexEventPublisher.publishCommentSaved(comment);
|
||||||
return comment;
|
return comment;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -221,6 +224,7 @@ public class CommentService {
|
|||||||
}
|
}
|
||||||
notificationService.notifyMentions(content, author, parent.getPost(), comment);
|
notificationService.notifyMentions(content, author, parent.getPost(), comment);
|
||||||
log.debug("addReply finished for comment {}", comment.getId());
|
log.debug("addReply finished for comment {}", comment.getId());
|
||||||
|
searchIndexEventPublisher.publishCommentSaved(comment);
|
||||||
return comment;
|
return comment;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -360,7 +364,9 @@ public class CommentService {
|
|||||||
|
|
||||||
// 逻辑删除评论
|
// 逻辑删除评论
|
||||||
Post post = comment.getPost();
|
Post post = comment.getPost();
|
||||||
|
Long commentId = comment.getId();
|
||||||
commentRepository.delete(comment);
|
commentRepository.delete(comment);
|
||||||
|
searchIndexEventPublisher.publishCommentDeleted(commentId);
|
||||||
// 删除积分历史
|
// 删除积分历史
|
||||||
pointHistoryRepository.deleteAll(pointHistories);
|
pointHistoryRepository.deleteAll(pointHistories);
|
||||||
|
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import com.openisle.repository.PostSubscriptionRepository;
|
|||||||
import com.openisle.repository.ReactionRepository;
|
import com.openisle.repository.ReactionRepository;
|
||||||
import com.openisle.repository.TagRepository;
|
import com.openisle.repository.TagRepository;
|
||||||
import com.openisle.repository.UserRepository;
|
import com.openisle.repository.UserRepository;
|
||||||
|
import com.openisle.search.SearchIndexEventPublisher;
|
||||||
import com.openisle.service.EmailSender;
|
import com.openisle.service.EmailSender;
|
||||||
import java.time.Duration;
|
import java.time.Duration;
|
||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
@@ -73,6 +74,8 @@ public class PostService {
|
|||||||
private final ConcurrentMap<Long, ScheduledFuture<?>> scheduledFinalizations =
|
private final ConcurrentMap<Long, ScheduledFuture<?>> scheduledFinalizations =
|
||||||
new ConcurrentHashMap<>();
|
new ConcurrentHashMap<>();
|
||||||
|
|
||||||
|
private final SearchIndexEventPublisher searchIndexEventPublisher;
|
||||||
|
|
||||||
@Value("${app.website-url:https://www.open-isle.com}")
|
@Value("${app.website-url:https://www.open-isle.com}")
|
||||||
private String websiteUrl;
|
private String websiteUrl;
|
||||||
|
|
||||||
@@ -103,7 +106,8 @@ public class PostService {
|
|||||||
PostChangeLogService postChangeLogService,
|
PostChangeLogService postChangeLogService,
|
||||||
PointHistoryRepository pointHistoryRepository,
|
PointHistoryRepository pointHistoryRepository,
|
||||||
@Value("${app.post.publish-mode:DIRECT}") PublishMode publishMode,
|
@Value("${app.post.publish-mode:DIRECT}") PublishMode publishMode,
|
||||||
RedisTemplate redisTemplate
|
RedisTemplate redisTemplate,
|
||||||
|
SearchIndexEventPublisher searchIndexEventPublisher
|
||||||
) {
|
) {
|
||||||
this.postRepository = postRepository;
|
this.postRepository = postRepository;
|
||||||
this.userRepository = userRepository;
|
this.userRepository = userRepository;
|
||||||
@@ -130,6 +134,7 @@ public class PostService {
|
|||||||
this.publishMode = publishMode;
|
this.publishMode = publishMode;
|
||||||
|
|
||||||
this.redisTemplate = redisTemplate;
|
this.redisTemplate = redisTemplate;
|
||||||
|
this.searchIndexEventPublisher = searchIndexEventPublisher;
|
||||||
}
|
}
|
||||||
|
|
||||||
@EventListener(ApplicationReadyEvent.class)
|
@EventListener(ApplicationReadyEvent.class)
|
||||||
@@ -346,6 +351,9 @@ public class PostService {
|
|||||||
);
|
);
|
||||||
scheduledFinalizations.put(pp.getId(), future);
|
scheduledFinalizations.put(pp.getId(), future);
|
||||||
}
|
}
|
||||||
|
if (post.getStatus() == PostStatus.PUBLISHED) {
|
||||||
|
searchIndexEventPublisher.publishPostSaved(post);
|
||||||
|
}
|
||||||
return post;
|
return post;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -868,10 +876,12 @@ public class PostService {
|
|||||||
if (!tag.isApproved()) {
|
if (!tag.isApproved()) {
|
||||||
tag.setApproved(true);
|
tag.setApproved(true);
|
||||||
tagRepository.save(tag);
|
tagRepository.save(tag);
|
||||||
|
searchIndexEventPublisher.publishTagSaved(tag);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
post.setStatus(PostStatus.PUBLISHED);
|
post.setStatus(PostStatus.PUBLISHED);
|
||||||
post = postRepository.save(post);
|
post = postRepository.save(post);
|
||||||
|
searchIndexEventPublisher.publishPostSaved(post);
|
||||||
notificationService.createNotification(
|
notificationService.createNotification(
|
||||||
post.getAuthor(),
|
post.getAuthor(),
|
||||||
NotificationType.POST_REVIEWED,
|
NotificationType.POST_REVIEWED,
|
||||||
@@ -895,13 +905,16 @@ public class PostService {
|
|||||||
if (!tag.isApproved()) {
|
if (!tag.isApproved()) {
|
||||||
long count = postRepository.countDistinctByTags_Id(tag.getId());
|
long count = postRepository.countDistinctByTags_Id(tag.getId());
|
||||||
if (count <= 1) {
|
if (count <= 1) {
|
||||||
|
Long tagId = tag.getId();
|
||||||
post.getTags().remove(tag);
|
post.getTags().remove(tag);
|
||||||
tagRepository.delete(tag);
|
tagRepository.delete(tag);
|
||||||
|
searchIndexEventPublisher.publishTagDeleted(tagId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
post.setStatus(PostStatus.REJECTED);
|
post.setStatus(PostStatus.REJECTED);
|
||||||
post = postRepository.save(post);
|
post = postRepository.save(post);
|
||||||
|
searchIndexEventPublisher.publishPostDeleted(post.getId());
|
||||||
notificationService.createNotification(
|
notificationService.createNotification(
|
||||||
post.getAuthor(),
|
post.getAuthor(),
|
||||||
NotificationType.POST_REVIEWED,
|
NotificationType.POST_REVIEWED,
|
||||||
@@ -1042,6 +1055,9 @@ public class PostService {
|
|||||||
if (!oldTags.equals(newTags)) {
|
if (!oldTags.equals(newTags)) {
|
||||||
postChangeLogService.recordTagChange(updated, user, oldTags, newTags);
|
postChangeLogService.recordTagChange(updated, user, oldTags, newTags);
|
||||||
}
|
}
|
||||||
|
if (updated.getStatus() == PostStatus.PUBLISHED) {
|
||||||
|
searchIndexEventPublisher.publishPostSaved(updated);
|
||||||
|
}
|
||||||
return updated;
|
return updated;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1094,8 +1110,10 @@ public class PostService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
String title = post.getTitle();
|
String title = post.getTitle();
|
||||||
|
Long postId = post.getId();
|
||||||
postChangeLogService.deleteLogsForPost(post);
|
postChangeLogService.deleteLogsForPost(post);
|
||||||
postRepository.delete(post);
|
postRepository.delete(post);
|
||||||
|
searchIndexEventPublisher.publishPostDeleted(postId);
|
||||||
if (adminDeleting) {
|
if (adminDeleting) {
|
||||||
notificationService.createNotification(
|
notificationService.createNotification(
|
||||||
author,
|
author,
|
||||||
|
|||||||
@@ -11,14 +11,26 @@ import com.openisle.repository.CommentRepository;
|
|||||||
import com.openisle.repository.PostRepository;
|
import com.openisle.repository.PostRepository;
|
||||||
import com.openisle.repository.TagRepository;
|
import com.openisle.repository.TagRepository;
|
||||||
import com.openisle.repository.UserRepository;
|
import com.openisle.repository.UserRepository;
|
||||||
|
import com.openisle.search.OpenSearchProperties;
|
||||||
|
import com.openisle.search.SearchDocument;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.ArrayList;
|
||||||
import java.util.LinkedHashMap;
|
import java.util.LinkedHashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Optional;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
import java.util.stream.Stream;
|
import java.util.stream.Stream;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.opensearch.client.opensearch.OpenSearchClient;
|
||||||
|
import org.opensearch.client.opensearch._types.query_dsl.TextQueryType;
|
||||||
|
import org.opensearch.client.opensearch.core.SearchResponse;
|
||||||
|
import org.opensearch.client.opensearch.core.search.Hit;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
@Service
|
@Service
|
||||||
|
@Slf4j
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
public class SearchService {
|
public class SearchService {
|
||||||
|
|
||||||
@@ -27,10 +39,14 @@ public class SearchService {
|
|||||||
private final CommentRepository commentRepository;
|
private final CommentRepository commentRepository;
|
||||||
private final CategoryRepository categoryRepository;
|
private final CategoryRepository categoryRepository;
|
||||||
private final TagRepository tagRepository;
|
private final TagRepository tagRepository;
|
||||||
|
private final Optional<OpenSearchClient> openSearchClient;
|
||||||
|
private final OpenSearchProperties openSearchProperties;
|
||||||
|
|
||||||
@org.springframework.beans.factory.annotation.Value("${app.snippet-length}")
|
@org.springframework.beans.factory.annotation.Value("${app.snippet-length}")
|
||||||
private int snippetLength;
|
private int snippetLength;
|
||||||
|
|
||||||
|
private static final int DEFAULT_OPEN_SEARCH_LIMIT = 50;
|
||||||
|
|
||||||
public List<User> searchUsers(String keyword) {
|
public List<User> searchUsers(String keyword) {
|
||||||
return userRepository.findByUsernameContainingIgnoreCase(keyword);
|
return userRepository.findByUsernameContainingIgnoreCase(keyword);
|
||||||
}
|
}
|
||||||
@@ -64,6 +80,23 @@ public class SearchService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public List<SearchResult> globalSearch(String keyword) {
|
public List<SearchResult> globalSearch(String keyword) {
|
||||||
|
if (keyword == null || keyword.isBlank()) {
|
||||||
|
return List.of();
|
||||||
|
}
|
||||||
|
if (isOpenSearchEnabled()) {
|
||||||
|
try {
|
||||||
|
List<SearchResult> results = searchWithOpenSearch(keyword);
|
||||||
|
if (!results.isEmpty()) {
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
} catch (IOException e) {
|
||||||
|
log.warn("OpenSearch global search failed, falling back to database query", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return fallbackGlobalSearch(keyword);
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<SearchResult> fallbackGlobalSearch(String keyword) {
|
||||||
Stream<SearchResult> users = searchUsers(keyword)
|
Stream<SearchResult> users = searchUsers(keyword)
|
||||||
.stream()
|
.stream()
|
||||||
.map(u ->
|
.map(u ->
|
||||||
@@ -138,6 +171,143 @@ public class SearchService {
|
|||||||
.toList();
|
.toList();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private boolean isOpenSearchEnabled() {
|
||||||
|
return openSearchProperties.isEnabled() && openSearchClient.isPresent();
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<SearchResult> searchWithOpenSearch(String keyword) throws IOException {
|
||||||
|
OpenSearchClient client = openSearchClient.orElse(null);
|
||||||
|
if (client == null) {
|
||||||
|
return List.of();
|
||||||
|
}
|
||||||
|
String trimmed = keyword.trim();
|
||||||
|
SearchResponse<SearchDocument> response = client.search(
|
||||||
|
builder ->
|
||||||
|
builder
|
||||||
|
.index(searchIndices())
|
||||||
|
.query(q ->
|
||||||
|
q.multiMatch(mm ->
|
||||||
|
mm
|
||||||
|
.query(trimmed)
|
||||||
|
.fields(List.of("title^3", "content^2", "author^2", "category", "tags"))
|
||||||
|
.type(TextQueryType.BestFields)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.highlight(h ->
|
||||||
|
h
|
||||||
|
.preTags("<mark>")
|
||||||
|
.postTags("</mark>")
|
||||||
|
.fields("content", f -> f.fragmentSize(highlightFragmentSize()).numberOfFragments(1))
|
||||||
|
.fields("title", f -> f.fragmentSize(highlightFragmentSize()).numberOfFragments(1))
|
||||||
|
)
|
||||||
|
.size(DEFAULT_OPEN_SEARCH_LIMIT),
|
||||||
|
SearchDocument.class
|
||||||
|
);
|
||||||
|
return mapHits(response.hits().hits(), trimmed);
|
||||||
|
}
|
||||||
|
|
||||||
|
private int highlightFragmentSize() {
|
||||||
|
int configured = openSearchProperties.getHighlightFragmentSize();
|
||||||
|
if (configured > 0) {
|
||||||
|
return configured;
|
||||||
|
}
|
||||||
|
if (snippetLength > 0) {
|
||||||
|
return snippetLength;
|
||||||
|
}
|
||||||
|
return 200;
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<String> searchIndices() {
|
||||||
|
return List.of(
|
||||||
|
openSearchProperties.postsIndex(),
|
||||||
|
openSearchProperties.commentsIndex(),
|
||||||
|
openSearchProperties.usersIndex(),
|
||||||
|
openSearchProperties.categoriesIndex(),
|
||||||
|
openSearchProperties.tagsIndex()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<SearchResult> mapHits(List<Hit<SearchDocument>> hits, String keyword) {
|
||||||
|
List<SearchResult> results = new ArrayList<>();
|
||||||
|
for (Hit<SearchDocument> hit : hits) {
|
||||||
|
SearchResult result = mapHit(hit, keyword);
|
||||||
|
if (result != null) {
|
||||||
|
results.add(result);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
|
private SearchResult mapHit(Hit<SearchDocument> hit, String keyword) {
|
||||||
|
SearchDocument document = hit.source();
|
||||||
|
if (document == null || document.entityId() == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
Map<String, List<String>> highlight = hit.highlight();
|
||||||
|
String highlightedContent = firstHighlight(highlight, "content");
|
||||||
|
String highlightedTitle = firstHighlight(highlight, "title");
|
||||||
|
boolean highlightTitle = highlightedTitle != null && !highlightedTitle.isBlank();
|
||||||
|
String documentType = document.type() != null ? document.type() : "";
|
||||||
|
String effectiveType = documentType;
|
||||||
|
if ("post".equals(documentType) && highlightTitle) {
|
||||||
|
effectiveType = "post_title";
|
||||||
|
}
|
||||||
|
String snippet = highlightedContent != null && !highlightedContent.isBlank()
|
||||||
|
? cleanHighlight(highlightedContent)
|
||||||
|
: null;
|
||||||
|
if (snippet == null && highlightTitle) {
|
||||||
|
snippet = cleanHighlight(highlightedTitle);
|
||||||
|
}
|
||||||
|
boolean fromStart = "post_title".equals(effectiveType);
|
||||||
|
if (snippet == null || snippet.isBlank()) {
|
||||||
|
snippet = fallbackSnippet(document.content(), keyword, fromStart);
|
||||||
|
}
|
||||||
|
if (snippet == null) {
|
||||||
|
snippet = "";
|
||||||
|
}
|
||||||
|
String subText = null;
|
||||||
|
Long postId = null;
|
||||||
|
if ("post".equals(documentType) || "post_title".equals(effectiveType)) {
|
||||||
|
subText = document.category();
|
||||||
|
} else if ("comment".equals(documentType)) {
|
||||||
|
subText = document.author();
|
||||||
|
postId = document.postId();
|
||||||
|
}
|
||||||
|
return new SearchResult(
|
||||||
|
effectiveType,
|
||||||
|
document.entityId(),
|
||||||
|
document.title(),
|
||||||
|
subText,
|
||||||
|
snippet,
|
||||||
|
postId
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private String firstHighlight(Map<String, List<String>> highlight, String field) {
|
||||||
|
if (highlight == null || field == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
List<String> values = highlight.get(field);
|
||||||
|
if (values == null || values.isEmpty()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return values.get(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
private String cleanHighlight(String value) {
|
||||||
|
if (value == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return value.replaceAll("<[^>]+>", "");
|
||||||
|
}
|
||||||
|
|
||||||
|
private String fallbackSnippet(String content, String keyword, boolean fromStart) {
|
||||||
|
if (content == null) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
return extractSnippet(content, keyword, fromStart);
|
||||||
|
}
|
||||||
|
|
||||||
private String extractSnippet(String content, String keyword, boolean fromStart) {
|
private String extractSnippet(String content, String keyword, boolean fromStart) {
|
||||||
if (content == null) return "";
|
if (content == null) return "";
|
||||||
int limit = snippetLength;
|
int limit = snippetLength;
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import com.openisle.model.Tag;
|
|||||||
import com.openisle.model.User;
|
import com.openisle.model.User;
|
||||||
import com.openisle.repository.TagRepository;
|
import com.openisle.repository.TagRepository;
|
||||||
import com.openisle.repository.UserRepository;
|
import com.openisle.repository.UserRepository;
|
||||||
|
import com.openisle.search.SearchIndexEventPublisher;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import org.springframework.cache.annotation.CacheEvict;
|
import org.springframework.cache.annotation.CacheEvict;
|
||||||
@@ -20,6 +21,7 @@ public class TagService {
|
|||||||
private final TagRepository tagRepository;
|
private final TagRepository tagRepository;
|
||||||
private final TagValidator tagValidator;
|
private final TagValidator tagValidator;
|
||||||
private final UserRepository userRepository;
|
private final UserRepository userRepository;
|
||||||
|
private final SearchIndexEventPublisher searchIndexEventPublisher;
|
||||||
|
|
||||||
@CacheEvict(value = CachingConfig.TAG_CACHE_NAME, allEntries = true)
|
@CacheEvict(value = CachingConfig.TAG_CACHE_NAME, allEntries = true)
|
||||||
public Tag createTag(
|
public Tag createTag(
|
||||||
@@ -43,7 +45,9 @@ public class TagService {
|
|||||||
.orElseThrow(() -> new com.openisle.exception.NotFoundException("User not found"));
|
.orElseThrow(() -> new com.openisle.exception.NotFoundException("User not found"));
|
||||||
tag.setCreator(creator);
|
tag.setCreator(creator);
|
||||||
}
|
}
|
||||||
return tagRepository.save(tag);
|
Tag saved = tagRepository.save(tag);
|
||||||
|
searchIndexEventPublisher.publishTagSaved(saved);
|
||||||
|
return saved;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Tag createTag(
|
public Tag createTag(
|
||||||
@@ -78,12 +82,15 @@ public class TagService {
|
|||||||
if (smallIcon != null) {
|
if (smallIcon != null) {
|
||||||
tag.setSmallIcon(smallIcon);
|
tag.setSmallIcon(smallIcon);
|
||||||
}
|
}
|
||||||
return tagRepository.save(tag);
|
Tag saved = tagRepository.save(tag);
|
||||||
|
searchIndexEventPublisher.publishTagSaved(saved);
|
||||||
|
return saved;
|
||||||
}
|
}
|
||||||
|
|
||||||
@CacheEvict(value = CachingConfig.TAG_CACHE_NAME, allEntries = true)
|
@CacheEvict(value = CachingConfig.TAG_CACHE_NAME, allEntries = true)
|
||||||
public void deleteTag(Long id) {
|
public void deleteTag(Long id) {
|
||||||
tagRepository.deleteById(id);
|
tagRepository.deleteById(id);
|
||||||
|
searchIndexEventPublisher.publishTagDeleted(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
public Tag approveTag(Long id) {
|
public Tag approveTag(Long id) {
|
||||||
@@ -91,7 +98,9 @@ public class TagService {
|
|||||||
.findById(id)
|
.findById(id)
|
||||||
.orElseThrow(() -> new IllegalArgumentException("Tag not found"));
|
.orElseThrow(() -> new IllegalArgumentException("Tag not found"));
|
||||||
tag.setApproved(true);
|
tag.setApproved(true);
|
||||||
return tagRepository.save(tag);
|
Tag saved = tagRepository.save(tag);
|
||||||
|
searchIndexEventPublisher.publishTagSaved(saved);
|
||||||
|
return saved;
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<Tag> listPendingTags() {
|
public List<Tag> listPendingTags() {
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import com.openisle.exception.FieldException;
|
|||||||
import com.openisle.model.Role;
|
import com.openisle.model.Role;
|
||||||
import com.openisle.model.User;
|
import com.openisle.model.User;
|
||||||
import com.openisle.repository.UserRepository;
|
import com.openisle.repository.UserRepository;
|
||||||
|
import com.openisle.search.SearchIndexEventPublisher;
|
||||||
import com.openisle.service.AvatarGenerator;
|
import com.openisle.service.AvatarGenerator;
|
||||||
import com.openisle.service.PasswordValidator;
|
import com.openisle.service.PasswordValidator;
|
||||||
import com.openisle.service.UsernameValidator;
|
import com.openisle.service.UsernameValidator;
|
||||||
@@ -34,6 +35,7 @@ public class UserService {
|
|||||||
private final RedisTemplate redisTemplate;
|
private final RedisTemplate redisTemplate;
|
||||||
|
|
||||||
private final EmailSender emailService;
|
private final EmailSender emailService;
|
||||||
|
private final SearchIndexEventPublisher searchIndexEventPublisher;
|
||||||
|
|
||||||
public User register(
|
public User register(
|
||||||
String username,
|
String username,
|
||||||
@@ -58,7 +60,9 @@ public class UserService {
|
|||||||
// u.setVerificationCode(genCode());
|
// u.setVerificationCode(genCode());
|
||||||
u.setRegisterReason(reason);
|
u.setRegisterReason(reason);
|
||||||
u.setApproved(mode == com.openisle.model.RegisterMode.DIRECT);
|
u.setApproved(mode == com.openisle.model.RegisterMode.DIRECT);
|
||||||
return userRepository.save(u);
|
User saved = userRepository.save(u);
|
||||||
|
searchIndexEventPublisher.publishUserSaved(saved);
|
||||||
|
return saved;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── 再按邮箱查 ───────────────────────────────────────────
|
// ── 再按邮箱查 ───────────────────────────────────────────
|
||||||
@@ -75,7 +79,9 @@ public class UserService {
|
|||||||
// u.setVerificationCode(genCode());
|
// u.setVerificationCode(genCode());
|
||||||
u.setRegisterReason(reason);
|
u.setRegisterReason(reason);
|
||||||
u.setApproved(mode == com.openisle.model.RegisterMode.DIRECT);
|
u.setApproved(mode == com.openisle.model.RegisterMode.DIRECT);
|
||||||
return userRepository.save(u);
|
User saved = userRepository.save(u);
|
||||||
|
searchIndexEventPublisher.publishUserSaved(saved);
|
||||||
|
return saved;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── 完全新用户 ───────────────────────────────────────────
|
// ── 完全新用户 ───────────────────────────────────────────
|
||||||
@@ -89,14 +95,18 @@ public class UserService {
|
|||||||
user.setAvatar(avatarGenerator.generate(username));
|
user.setAvatar(avatarGenerator.generate(username));
|
||||||
user.setRegisterReason(reason);
|
user.setRegisterReason(reason);
|
||||||
user.setApproved(mode == com.openisle.model.RegisterMode.DIRECT);
|
user.setApproved(mode == com.openisle.model.RegisterMode.DIRECT);
|
||||||
return userRepository.save(user);
|
User saved = userRepository.save(user);
|
||||||
|
searchIndexEventPublisher.publishUserSaved(saved);
|
||||||
|
return saved;
|
||||||
}
|
}
|
||||||
|
|
||||||
public User registerWithInvite(String username, String email, String password) {
|
public User registerWithInvite(String username, String email, String password) {
|
||||||
User user = register(username, email, password, "", com.openisle.model.RegisterMode.DIRECT);
|
User user = register(username, email, password, "", com.openisle.model.RegisterMode.DIRECT);
|
||||||
user.setVerified(true);
|
user.setVerified(true);
|
||||||
// user.setVerificationCode(genCode());
|
// user.setVerificationCode(genCode());
|
||||||
return userRepository.save(user);
|
User saved = userRepository.save(user);
|
||||||
|
searchIndexEventPublisher.publishUserSaved(saved);
|
||||||
|
return saved;
|
||||||
}
|
}
|
||||||
|
|
||||||
private String genCode() {
|
private String genCode() {
|
||||||
@@ -209,7 +219,9 @@ public class UserService {
|
|||||||
.findByUsername(username)
|
.findByUsername(username)
|
||||||
.orElseThrow(() -> new com.openisle.exception.NotFoundException("User not found"));
|
.orElseThrow(() -> new com.openisle.exception.NotFoundException("User not found"));
|
||||||
user.setRegisterReason(reason);
|
user.setRegisterReason(reason);
|
||||||
return userRepository.save(user);
|
User saved = userRepository.save(user);
|
||||||
|
searchIndexEventPublisher.publishUserSaved(saved);
|
||||||
|
return saved;
|
||||||
}
|
}
|
||||||
|
|
||||||
public User updateProfile(String currentUsername, String newUsername, String introduction) {
|
public User updateProfile(String currentUsername, String newUsername, String introduction) {
|
||||||
|
|||||||
@@ -45,6 +45,16 @@ app.user.replies-limit=${USER_REPLIES_LIMIT:50}
|
|||||||
# Length of extracted snippets for posts and search (-1 to disable truncation)
|
# Length of extracted snippets for posts and search (-1 to disable truncation)
|
||||||
app.snippet-length=${SNIPPET_LENGTH:200}
|
app.snippet-length=${SNIPPET_LENGTH:200}
|
||||||
|
|
||||||
|
# OpenSearch integration
|
||||||
|
app.search.enabled=${SEARCH_ENABLED:true}
|
||||||
|
app.search.host=${SEARCH_HOST:localhost}
|
||||||
|
app.search.port=${SEARCH_PORT:9200}
|
||||||
|
app.search.scheme=${SEARCH_SCHEME:http}
|
||||||
|
app.search.username=${SEARCH_USERNAME:}
|
||||||
|
app.search.password=${SEARCH_PASSWORD:}
|
||||||
|
app.search.index-prefix=${SEARCH_INDEX_PREFIX:openisle}
|
||||||
|
app.search.highlight-fragment-size=${SEARCH_HIGHLIGHT_FRAGMENT_SIZE:${SNIPPET_LENGTH:200}}
|
||||||
|
|
||||||
# Captcha configuration
|
# Captcha configuration
|
||||||
app.captcha.enabled=${CAPTCHA_ENABLED:false}
|
app.captcha.enabled=${CAPTCHA_ENABLED:false}
|
||||||
recaptcha.secret-key=${RECAPTCHA_SECRET_KEY:}
|
recaptcha.secret-key=${RECAPTCHA_SECRET_KEY:}
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import com.openisle.repository.PointHistoryRepository;
|
|||||||
import com.openisle.repository.PostRepository;
|
import com.openisle.repository.PostRepository;
|
||||||
import com.openisle.repository.ReactionRepository;
|
import com.openisle.repository.ReactionRepository;
|
||||||
import com.openisle.repository.UserRepository;
|
import com.openisle.repository.UserRepository;
|
||||||
|
import com.openisle.search.SearchIndexEventPublisher;
|
||||||
import com.openisle.service.PointService;
|
import com.openisle.service.PointService;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
@@ -29,6 +30,7 @@ class CommentServiceTest {
|
|||||||
PointHistoryRepository pointHistoryRepo = mock(PointHistoryRepository.class);
|
PointHistoryRepository pointHistoryRepo = mock(PointHistoryRepository.class);
|
||||||
PointService pointService = mock(PointService.class);
|
PointService pointService = mock(PointService.class);
|
||||||
ImageUploader imageUploader = mock(ImageUploader.class);
|
ImageUploader imageUploader = mock(ImageUploader.class);
|
||||||
|
SearchIndexEventPublisher searchIndexEventPublisher = mock(SearchIndexEventPublisher.class);
|
||||||
|
|
||||||
CommentService service = new CommentService(
|
CommentService service = new CommentService(
|
||||||
commentRepo,
|
commentRepo,
|
||||||
@@ -41,7 +43,8 @@ class CommentServiceTest {
|
|||||||
nRepo,
|
nRepo,
|
||||||
pointHistoryRepo,
|
pointHistoryRepo,
|
||||||
pointService,
|
pointService,
|
||||||
imageUploader
|
imageUploader,
|
||||||
|
searchIndexEventPublisher
|
||||||
);
|
);
|
||||||
|
|
||||||
when(commentRepo.countByAuthorAfter(eq("alice"), any())).thenReturn(3L);
|
when(commentRepo.countByAuthorAfter(eq("alice"), any())).thenReturn(3L);
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import static org.mockito.Mockito.*;
|
|||||||
import com.openisle.exception.RateLimitException;
|
import com.openisle.exception.RateLimitException;
|
||||||
import com.openisle.model.*;
|
import com.openisle.model.*;
|
||||||
import com.openisle.repository.*;
|
import com.openisle.repository.*;
|
||||||
|
import com.openisle.search.SearchIndexEventPublisher;
|
||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
@@ -42,6 +43,7 @@ class PostServiceTest {
|
|||||||
PostChangeLogService postChangeLogService = mock(PostChangeLogService.class);
|
PostChangeLogService postChangeLogService = mock(PostChangeLogService.class);
|
||||||
PointHistoryRepository pointHistoryRepository = mock(PointHistoryRepository.class);
|
PointHistoryRepository pointHistoryRepository = mock(PointHistoryRepository.class);
|
||||||
RedisTemplate redisTemplate = mock(RedisTemplate.class);
|
RedisTemplate redisTemplate = mock(RedisTemplate.class);
|
||||||
|
SearchIndexEventPublisher searchIndexEventPublisher = mock(SearchIndexEventPublisher.class);
|
||||||
|
|
||||||
PostService service = new PostService(
|
PostService service = new PostService(
|
||||||
postRepo,
|
postRepo,
|
||||||
@@ -67,7 +69,8 @@ class PostServiceTest {
|
|||||||
postChangeLogService,
|
postChangeLogService,
|
||||||
pointHistoryRepository,
|
pointHistoryRepository,
|
||||||
PublishMode.DIRECT,
|
PublishMode.DIRECT,
|
||||||
redisTemplate
|
redisTemplate,
|
||||||
|
searchIndexEventPublisher
|
||||||
);
|
);
|
||||||
when(context.getBean(PostService.class)).thenReturn(service);
|
when(context.getBean(PostService.class)).thenReturn(service);
|
||||||
|
|
||||||
@@ -118,6 +121,7 @@ class PostServiceTest {
|
|||||||
PostChangeLogService postChangeLogService = mock(PostChangeLogService.class);
|
PostChangeLogService postChangeLogService = mock(PostChangeLogService.class);
|
||||||
PointHistoryRepository pointHistoryRepository = mock(PointHistoryRepository.class);
|
PointHistoryRepository pointHistoryRepository = mock(PointHistoryRepository.class);
|
||||||
RedisTemplate redisTemplate = mock(RedisTemplate.class);
|
RedisTemplate redisTemplate = mock(RedisTemplate.class);
|
||||||
|
SearchIndexEventPublisher searchIndexEventPublisher = mock(SearchIndexEventPublisher.class);
|
||||||
|
|
||||||
PostService service = new PostService(
|
PostService service = new PostService(
|
||||||
postRepo,
|
postRepo,
|
||||||
@@ -143,7 +147,8 @@ class PostServiceTest {
|
|||||||
postChangeLogService,
|
postChangeLogService,
|
||||||
pointHistoryRepository,
|
pointHistoryRepository,
|
||||||
PublishMode.DIRECT,
|
PublishMode.DIRECT,
|
||||||
redisTemplate
|
redisTemplate,
|
||||||
|
searchIndexEventPublisher
|
||||||
);
|
);
|
||||||
when(context.getBean(PostService.class)).thenReturn(service);
|
when(context.getBean(PostService.class)).thenReturn(service);
|
||||||
|
|
||||||
@@ -207,6 +212,7 @@ class PostServiceTest {
|
|||||||
PostChangeLogService postChangeLogService = mock(PostChangeLogService.class);
|
PostChangeLogService postChangeLogService = mock(PostChangeLogService.class);
|
||||||
PointHistoryRepository pointHistoryRepository = mock(PointHistoryRepository.class);
|
PointHistoryRepository pointHistoryRepository = mock(PointHistoryRepository.class);
|
||||||
RedisTemplate redisTemplate = mock(RedisTemplate.class);
|
RedisTemplate redisTemplate = mock(RedisTemplate.class);
|
||||||
|
SearchIndexEventPublisher searchIndexEventPublisher = mock(SearchIndexEventPublisher.class);
|
||||||
|
|
||||||
PostService service = new PostService(
|
PostService service = new PostService(
|
||||||
postRepo,
|
postRepo,
|
||||||
@@ -232,7 +238,8 @@ class PostServiceTest {
|
|||||||
postChangeLogService,
|
postChangeLogService,
|
||||||
pointHistoryRepository,
|
pointHistoryRepository,
|
||||||
PublishMode.DIRECT,
|
PublishMode.DIRECT,
|
||||||
redisTemplate
|
redisTemplate,
|
||||||
|
searchIndexEventPublisher
|
||||||
);
|
);
|
||||||
when(context.getBean(PostService.class)).thenReturn(service);
|
when(context.getBean(PostService.class)).thenReturn(service);
|
||||||
|
|
||||||
@@ -283,6 +290,7 @@ class PostServiceTest {
|
|||||||
PostChangeLogService postChangeLogService = mock(PostChangeLogService.class);
|
PostChangeLogService postChangeLogService = mock(PostChangeLogService.class);
|
||||||
PointHistoryRepository pointHistoryRepository = mock(PointHistoryRepository.class);
|
PointHistoryRepository pointHistoryRepository = mock(PointHistoryRepository.class);
|
||||||
RedisTemplate redisTemplate = mock(RedisTemplate.class);
|
RedisTemplate redisTemplate = mock(RedisTemplate.class);
|
||||||
|
SearchIndexEventPublisher searchIndexEventPublisher = mock(SearchIndexEventPublisher.class);
|
||||||
|
|
||||||
PostService service = new PostService(
|
PostService service = new PostService(
|
||||||
postRepo,
|
postRepo,
|
||||||
@@ -308,7 +316,8 @@ class PostServiceTest {
|
|||||||
postChangeLogService,
|
postChangeLogService,
|
||||||
pointHistoryRepository,
|
pointHistoryRepository,
|
||||||
PublishMode.DIRECT,
|
PublishMode.DIRECT,
|
||||||
redisTemplate
|
redisTemplate,
|
||||||
|
searchIndexEventPublisher
|
||||||
);
|
);
|
||||||
when(context.getBean(PostService.class)).thenReturn(service);
|
when(context.getBean(PostService.class)).thenReturn(service);
|
||||||
|
|
||||||
@@ -375,6 +384,7 @@ class PostServiceTest {
|
|||||||
PostChangeLogService postChangeLogService = mock(PostChangeLogService.class);
|
PostChangeLogService postChangeLogService = mock(PostChangeLogService.class);
|
||||||
PointHistoryRepository pointHistoryRepository = mock(PointHistoryRepository.class);
|
PointHistoryRepository pointHistoryRepository = mock(PointHistoryRepository.class);
|
||||||
RedisTemplate redisTemplate = mock(RedisTemplate.class);
|
RedisTemplate redisTemplate = mock(RedisTemplate.class);
|
||||||
|
SearchIndexEventPublisher searchIndexEventPublisher = mock(SearchIndexEventPublisher.class);
|
||||||
|
|
||||||
PostService service = new PostService(
|
PostService service = new PostService(
|
||||||
postRepo,
|
postRepo,
|
||||||
@@ -400,7 +410,8 @@ class PostServiceTest {
|
|||||||
postChangeLogService,
|
postChangeLogService,
|
||||||
pointHistoryRepository,
|
pointHistoryRepository,
|
||||||
PublishMode.DIRECT,
|
PublishMode.DIRECT,
|
||||||
redisTemplate
|
redisTemplate,
|
||||||
|
searchIndexEventPublisher
|
||||||
);
|
);
|
||||||
when(context.getBean(PostService.class)).thenReturn(service);
|
when(context.getBean(PostService.class)).thenReturn(service);
|
||||||
|
|
||||||
|
|||||||
@@ -9,7 +9,9 @@ import com.openisle.repository.CommentRepository;
|
|||||||
import com.openisle.repository.PostRepository;
|
import com.openisle.repository.PostRepository;
|
||||||
import com.openisle.repository.TagRepository;
|
import com.openisle.repository.TagRepository;
|
||||||
import com.openisle.repository.UserRepository;
|
import com.openisle.repository.UserRepository;
|
||||||
|
import com.openisle.search.OpenSearchProperties;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
import org.mockito.Mockito;
|
import org.mockito.Mockito;
|
||||||
|
|
||||||
@@ -27,7 +29,9 @@ class SearchServiceTest {
|
|||||||
postRepo,
|
postRepo,
|
||||||
commentRepo,
|
commentRepo,
|
||||||
categoryRepo,
|
categoryRepo,
|
||||||
tagRepo
|
tagRepo,
|
||||||
|
Optional.empty(),
|
||||||
|
new OpenSearchProperties()
|
||||||
);
|
);
|
||||||
|
|
||||||
Post post1 = new Post();
|
Post post1 = new Post();
|
||||||
|
|||||||
Reference in New Issue
Block a user