From a7599069b822269daf3f1bbee384494ff541ed5b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andreas=20Kr=C3=BChlmann?= Date: Thu, 11 Dec 2025 02:24:25 +0100 Subject: [PATCH] Optimize search --- .../app/repository/ChannelRepository.java | 3 ++ .../app/service/ChannelDirectoryService.java | 10 ++-- .../ChannelDirectoryApiIntegrationTest.java | 47 +++++++++++++++++++ .../app/ChannelDirectoryServiceTest.java | 8 ++++ 4 files changed, 61 insertions(+), 7 deletions(-) create mode 100644 src/test/java/com/imgfloat/app/ChannelDirectoryApiIntegrationTest.java diff --git a/src/main/java/com/imgfloat/app/repository/ChannelRepository.java b/src/main/java/com/imgfloat/app/repository/ChannelRepository.java index 1e245c1..6f96f35 100644 --- a/src/main/java/com/imgfloat/app/repository/ChannelRepository.java +++ b/src/main/java/com/imgfloat/app/repository/ChannelRepository.java @@ -3,5 +3,8 @@ package com.imgfloat.app.repository; import com.imgfloat.app.model.Channel; import org.springframework.data.jpa.repository.JpaRepository; +import java.util.List; + public interface ChannelRepository extends JpaRepository { + List findTop50ByBroadcasterContainingIgnoreCaseOrderByBroadcasterAsc(String broadcasterFragment); } diff --git a/src/main/java/com/imgfloat/app/service/ChannelDirectoryService.java b/src/main/java/com/imgfloat/app/service/ChannelDirectoryService.java index aabc49d..ed20038 100644 --- a/src/main/java/com/imgfloat/app/service/ChannelDirectoryService.java +++ b/src/main/java/com/imgfloat/app/service/ChannelDirectoryService.java @@ -42,7 +42,6 @@ import java.util.Collection; import java.util.Comparator; import java.util.List; import java.util.Locale; -import java.util.Objects; import java.util.Optional; import javax.imageio.ImageIO; import javax.imageio.ImageReader; @@ -94,13 +93,10 @@ public class ChannelDirectoryService { public List searchBroadcasters(String query) { String normalizedQuery = normalize(query); - return channelRepository.findAll().stream() + String searchTerm = normalizedQuery == null || normalizedQuery.isBlank() ? "" : normalizedQuery; + return channelRepository.findTop50ByBroadcasterContainingIgnoreCaseOrderByBroadcasterAsc(searchTerm) + .stream() .map(Channel::getBroadcaster) - .map(this::normalize) - .filter(Objects::nonNull) - .filter(name -> normalizedQuery == null || normalizedQuery.isBlank() || name.contains(normalizedQuery)) - .sorted() - .limit(50) .toList(); } diff --git a/src/test/java/com/imgfloat/app/ChannelDirectoryApiIntegrationTest.java b/src/test/java/com/imgfloat/app/ChannelDirectoryApiIntegrationTest.java new file mode 100644 index 0000000..e80ca8e --- /dev/null +++ b/src/test/java/com/imgfloat/app/ChannelDirectoryApiIntegrationTest.java @@ -0,0 +1,47 @@ +package com.imgfloat.app; + +import com.imgfloat.app.model.Channel; +import com.imgfloat.app.repository.ChannelRepository; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.web.servlet.MockMvc; + +import static org.hamcrest.Matchers.hasSize; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@SpringBootTest(properties = { + "spring.security.oauth2.client.registration.twitch.client-id=test-client-id", + "spring.security.oauth2.client.registration.twitch.client-secret=test-client-secret" +}) +@AutoConfigureMockMvc +class ChannelDirectoryApiIntegrationTest { + + @Autowired + private MockMvc mockMvc; + + @Autowired + private ChannelRepository channelRepository; + + @BeforeEach + void cleanChannels() { + channelRepository.deleteAll(); + } + + @Test + void searchesBroadcastersCaseInsensitiveAndSorted() throws Exception { + channelRepository.save(new Channel("Beta")); + channelRepository.save(new Channel("alpha")); + channelRepository.save(new Channel("ALPINE")); + + mockMvc.perform(get("/api/channels").param("q", "Al")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$", hasSize(2))) + .andExpect(jsonPath("$[0]").value("alpha")) + .andExpect(jsonPath("$[1]").value("alpine")); + } +} diff --git a/src/test/java/com/imgfloat/app/ChannelDirectoryServiceTest.java b/src/test/java/com/imgfloat/app/ChannelDirectoryServiceTest.java index 930835f..99dcf6f 100644 --- a/src/test/java/com/imgfloat/app/ChannelDirectoryServiceTest.java +++ b/src/test/java/com/imgfloat/app/ChannelDirectoryServiceTest.java @@ -21,6 +21,7 @@ import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; import java.util.Collection; +import java.util.Comparator; import java.util.List; import java.util.Map; import java.util.Optional; @@ -172,6 +173,13 @@ class ChannelDirectoryServiceTest { }); when(channelRepository.findAll()) .thenAnswer(invocation -> List.copyOf(channels.values())); + when(channelRepository.findTop50ByBroadcasterContainingIgnoreCaseOrderByBroadcasterAsc(anyString())) + .thenAnswer(invocation -> channels.values().stream() + .filter(channel -> Optional.ofNullable(channel.getBroadcaster()).orElse("") + .contains(Optional.ofNullable(invocation.getArgument(0, String.class)).orElse("").toLowerCase())) + .sorted(Comparator.comparing(Channel::getBroadcaster)) + .limit(50) + .toList()); when(assetRepository.save(any(Asset.class))) .thenAnswer(invocation -> {