package com.openisle.service; 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.repository.CategoryRepository; import com.openisle.repository.CommentRepository; import com.openisle.repository.PostRepository; import com.openisle.repository.TagRepository; import com.openisle.repository.UserRepository; import java.util.LinkedHashMap; import java.util.List; import java.util.stream.Collectors; import java.util.stream.Stream; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; @Service @RequiredArgsConstructor public class SearchService { private final UserRepository userRepository; private final PostRepository postRepository; private final CommentRepository commentRepository; private final CategoryRepository categoryRepository; private final TagRepository tagRepository; @org.springframework.beans.factory.annotation.Value("${app.snippet-length}") private int snippetLength; public List searchUsers(String keyword) { return userRepository.findByUsernameContainingIgnoreCase(keyword); } public List searchPosts(String keyword) { return postRepository.findByTitleContainingIgnoreCaseOrContentContainingIgnoreCaseAndStatus( keyword, keyword, PostStatus.PUBLISHED ); } public List searchPostsByContent(String keyword) { return postRepository.findByContentContainingIgnoreCaseAndStatus(keyword, PostStatus.PUBLISHED); } public List searchPostsByTitle(String keyword) { return postRepository.findByTitleContainingIgnoreCaseAndStatus(keyword, PostStatus.PUBLISHED); } public List searchComments(String keyword) { return commentRepository.findByContentContainingIgnoreCase(keyword); } public List searchCategories(String keyword) { return categoryRepository.findByNameContainingIgnoreCase(keyword); } public List searchTags(String keyword) { return tagRepository.findByNameContainingIgnoreCaseAndApprovedTrue(keyword); } public List globalSearch(String keyword) { Stream users = searchUsers(keyword) .stream() .map(u -> new SearchResult("user", u.getId(), u.getUsername(), u.getIntroduction(), null, null) ); Stream categories = searchCategories(keyword) .stream() .map(c -> new SearchResult("category", c.getId(), c.getName(), null, c.getDescription(), null) ); Stream tags = searchTags(keyword) .stream() .map(t -> new SearchResult("tag", t.getId(), t.getName(), null, t.getDescription(), null)); // Merge post results while removing duplicates between search by content // and search by title List mergedPosts = Stream.concat( searchPosts(keyword) .stream() .map(p -> new SearchResult( "post", p.getId(), p.getTitle(), p.getCategory() != null ? p.getCategory().getName() : null, extractSnippet(p.getContent(), keyword, false), null ) ), searchPostsByTitle(keyword) .stream() .map(p -> new SearchResult( "post_title", p.getId(), p.getTitle(), p.getCategory() != null ? p.getCategory().getName() : null, extractSnippet(p.getContent(), keyword, true), null ) ) ) .collect( java.util.stream.Collectors.toMap( SearchResult::id, sr -> sr, (a, b) -> a, java.util.LinkedHashMap::new ) ) .values() .stream() .toList(); Stream comments = searchComments(keyword) .stream() .map(c -> new SearchResult( "comment", c.getId(), c.getPost().getTitle(), c.getAuthor().getUsername(), extractSnippet(c.getContent(), keyword, false), c.getPost().getId() ) ); return Stream.of(users, categories, tags, mergedPosts.stream(), comments) .flatMap(s -> s) .toList(); } private String extractSnippet(String content, String keyword, boolean fromStart) { if (content == null) return ""; int limit = snippetLength; if (fromStart) { if (limit < 0) { return content; } return content.length() > limit ? content.substring(0, limit) : content; } String lower = content.toLowerCase(); String kw = keyword.toLowerCase(); int idx = lower.indexOf(kw); if (idx == -1) { if (limit < 0) { return content; } return content.length() > limit ? content.substring(0, limit) : content; } int start = Math.max(0, idx - 20); int end = Math.min(content.length(), idx + kw.length() + 20); String snippet = content.substring(start, end); if (limit >= 0 && snippet.length() > limit) { snippet = snippet.substring(0, limit); } return snippet; } public record SearchResult( String type, Long id, String text, String subText, String extra, Long postId ) {} }