feat: include categories and tags in global search

This commit is contained in:
Tim
2025-07-31 12:52:25 +08:00
parent aa38c2e5e1
commit d4bfef36f7
4 changed files with 51 additions and 5 deletions

View File

@@ -74,7 +74,9 @@ export default {
const iconMap = { const iconMap = {
user: 'fas fa-user', user: 'fas fa-user',
post: 'fas fa-file-alt', post: 'fas fa-file-alt',
comment: 'fas fa-comment' comment: 'fas fa-comment',
category: 'fas fa-folder',
tag: 'fas fa-hashtag'
} }
watch(selected, val => { watch(selected, val => {
@@ -89,6 +91,10 @@ export default {
if (opt.postId) { if (opt.postId) {
router.push(`/posts/${opt.postId}#comment-${opt.id}`) router.push(`/posts/${opt.postId}#comment-${opt.id}`)
} }
} else if (opt.type === 'category') {
router.push({ path: '/', query: { category: opt.id } })
} else if (opt.type === 'tag') {
router.push({ path: '/', query: { tags: opt.id } })
} }
selected.value = null selected.value = null
keyword.value = '' keyword.value = ''

View File

@@ -3,5 +3,8 @@ package com.openisle.repository;
import com.openisle.model.Category; import com.openisle.model.Category;
import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.JpaRepository;
import java.util.List;
public interface CategoryRepository extends JpaRepository<Category, Long> { public interface CategoryRepository extends JpaRepository<Category, Long> {
List<Category> findByNameContainingIgnoreCase(String keyword);
} }

View File

@@ -4,9 +4,13 @@ import com.openisle.model.Post;
import com.openisle.model.PostStatus; import com.openisle.model.PostStatus;
import com.openisle.model.Comment; import com.openisle.model.Comment;
import com.openisle.model.User; import com.openisle.model.User;
import com.openisle.model.Category;
import com.openisle.model.Tag;
import com.openisle.repository.PostRepository; import com.openisle.repository.PostRepository;
import com.openisle.repository.CommentRepository; import com.openisle.repository.CommentRepository;
import com.openisle.repository.UserRepository; import com.openisle.repository.UserRepository;
import com.openisle.repository.CategoryRepository;
import com.openisle.repository.TagRepository;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
@@ -21,6 +25,8 @@ public class SearchService {
private final UserRepository userRepository; private final UserRepository userRepository;
private final PostRepository postRepository; private final PostRepository postRepository;
private final CommentRepository commentRepository; private final CommentRepository commentRepository;
private final CategoryRepository categoryRepository;
private final TagRepository tagRepository;
@org.springframework.beans.factory.annotation.Value("${app.snippet-length:50}") @org.springframework.beans.factory.annotation.Value("${app.snippet-length:50}")
private int snippetLength; private int snippetLength;
@@ -48,6 +54,14 @@ public class SearchService {
return commentRepository.findByContentContainingIgnoreCase(keyword); return commentRepository.findByContentContainingIgnoreCase(keyword);
} }
public List<Category> searchCategories(String keyword) {
return categoryRepository.findByNameContainingIgnoreCase(keyword);
}
public List<Tag> searchTags(String keyword) {
return tagRepository.findByNameContainingIgnoreCaseAndApprovedTrue(keyword);
}
public List<SearchResult> globalSearch(String keyword) { public List<SearchResult> globalSearch(String keyword) {
Stream<SearchResult> users = searchUsers(keyword).stream() Stream<SearchResult> users = searchUsers(keyword).stream()
.map(u -> new SearchResult( .map(u -> new SearchResult(
@@ -59,6 +73,26 @@ public class SearchService {
null null
)); ));
Stream<SearchResult> categories = searchCategories(keyword).stream()
.map(c -> new SearchResult(
"category",
c.getId(),
c.getName(),
null,
c.getDescription(),
null
));
Stream<SearchResult> 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 // Merge post results while removing duplicates between search by content
// and search by title // and search by title
List<SearchResult> mergedPosts = Stream.concat( List<SearchResult> mergedPosts = Stream.concat(
@@ -101,7 +135,8 @@ public class SearchService {
c.getPost().getId() c.getPost().getId()
)); ));
return Stream.concat(Stream.concat(users, mergedPosts.stream()), comments) return Stream.of(users, categories, tags, mergedPosts.stream(), comments)
.flatMap(s -> s)
.toList(); .toList();
} }

View File

@@ -61,10 +61,10 @@ class SearchIntegrationTest {
String admin = registerAndLoginAsAdmin("admin1", "a@a.com"); String admin = registerAndLoginAsAdmin("admin1", "a@a.com");
String user = registerAndLogin("bob_nice", "b@b.com"); String user = registerAndLogin("bob_nice", "b@b.com");
ResponseEntity<Map> catResp = postJson("/api/categories", Map.of("name", "misc", "description", "d", "icon", "i"), admin); ResponseEntity<Map> catResp = postJson("/api/categories", Map.of("name", "nic-cat", "description", "d", "icon", "i"), admin);
Long catId = ((Number)catResp.getBody().get("id")).longValue(); Long catId = ((Number)catResp.getBody().get("id")).longValue();
ResponseEntity<Map> tagResp = postJson("/api/tags", Map.of("name", "misc", "description", "d", "icon", "i"), admin); ResponseEntity<Map> tagResp = postJson("/api/tags", Map.of("name", "nic-tag", "description", "d", "icon", "i"), admin);
Long tagId = ((Number)tagResp.getBody().get("id")).longValue(); Long tagId = ((Number)tagResp.getBody().get("id")).longValue();
ResponseEntity<Map> postResp = postJson("/api/posts", ResponseEntity<Map> postResp = postJson("/api/posts",
@@ -76,9 +76,11 @@ class SearchIntegrationTest {
Map.of("content", "Nice article"), admin); Map.of("content", "Nice article"), admin);
List<Map<String, Object>> results = rest.getForObject("/api/search/global?keyword=nic", List.class); List<Map<String, Object>> results = rest.getForObject("/api/search/global?keyword=nic", List.class);
assertEquals(3, results.size()); assertEquals(5, results.size());
assertTrue(results.stream().anyMatch(m -> "user".equals(m.get("type")))); assertTrue(results.stream().anyMatch(m -> "user".equals(m.get("type"))));
assertTrue(results.stream().anyMatch(m -> "post".equals(m.get("type")))); assertTrue(results.stream().anyMatch(m -> "post".equals(m.get("type"))));
assertTrue(results.stream().anyMatch(m -> "comment".equals(m.get("type")))); assertTrue(results.stream().anyMatch(m -> "comment".equals(m.get("type"))));
assertTrue(results.stream().anyMatch(m -> "category".equals(m.get("type"))));
assertTrue(results.stream().anyMatch(m -> "tag".equals(m.get("type"))));
} }
} }