diff options
| author | Elias Haugsbakk <[email protected]> | 2026-07-06 21:12:40 +0200 |
|---|---|---|
| committer | Elias Haugsbakk <[email protected]> | 2026-07-06 21:12:40 +0200 |
| commit | 463c202d80f57efb8e46bab4ee4ad7038444b25d (patch) | |
| tree | 1b1675a02f57ad720a0ffbab5e99d80e2763d8f4 /clams-server/src/main/java | |
| parent | 05f77926b147fe3a849eb1dbeeed2afcf97ad866 (diff) | |
implement post searching
Diffstat (limited to 'clams-server/src/main/java')
6 files changed, 185 insertions, 61 deletions
diff --git a/clams-server/src/main/java/no/eliashaugsbakk/clams/server/Main.java b/clams-server/src/main/java/no/eliashaugsbakk/clams/server/Main.java index e456e7b..5ba7c31 100644 --- a/clams-server/src/main/java/no/eliashaugsbakk/clams/server/Main.java +++ b/clams-server/src/main/java/no/eliashaugsbakk/clams/server/Main.java @@ -27,8 +27,8 @@ public class Main { config.routes.get("/projects", ctx -> ctx.render("templates/projects.html", Map.of("page_title", "Projects - Elias Haugsbakk", "page_css", "projects"))); + config.routes.get("/blog", blogController::handleBlogRequest); config.routes.get("/blog/{slug}", blogController::handleGetPost); - config.routes.get("/blog", blogController::handleGetOverview); config.events.serverStopped(dbManager::close); }).start(7070); diff --git a/clams-server/src/main/java/no/eliashaugsbakk/clams/server/controller/BlogController.java b/clams-server/src/main/java/no/eliashaugsbakk/clams/server/controller/BlogController.java index 9111a0a..b82c3fe 100644 --- a/clams-server/src/main/java/no/eliashaugsbakk/clams/server/controller/BlogController.java +++ b/clams-server/src/main/java/no/eliashaugsbakk/clams/server/controller/BlogController.java @@ -3,86 +3,123 @@ package no.eliashaugsbakk.clams.server.controller; import io.javalin.http.Context; import java.time.ZoneId; import java.time.format.DateTimeFormatter; +import java.util.ArrayList; +import java.util.Comparator; import java.util.List; import java.util.Map; +import java.util.TreeMap; +import java.util.stream.Collectors; +import no.eliashaugsbakk.clams.server.model.Post; +import no.eliashaugsbakk.clams.server.model.PostMetaData; import no.eliashaugsbakk.clams.server.repository.BlogPostRepo; import no.eliashaugsbakk.clams.server.repository.BlogPostRepoSqlite; import no.eliashaugsbakk.clams.server.repository.SqliteManager; +import no.eliashaugsbakk.clams.server.service.BlogSearchService; import no.eliashaugsbakk.clams.server.utils.MarkdownConverter; public class BlogController { + private static final ZoneId OSLO_ZONE = ZoneId.of("Europe/Oslo"); + private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern("MMM dd, yyyy"); + private final BlogPostRepo blogPostRepo; + private final BlogSearchService blogSearchService; + + public record SearchResultItem(String title, String slug, String summary, String formattedDate) {} public BlogController(SqliteManager sqliteManager) { this.blogPostRepo = new BlogPostRepoSqlite(sqliteManager); + this.blogSearchService = new BlogSearchService(blogPostRepo); } public void handleGetPost(Context ctx) { - blogPostRepo.getPost(ctx.pathParam("slug")).ifPresentOrElse(post -> ctx.render( - "templates/post.html", - Map.of("page_title", post.title() + " -- Elias Haugsbakk", - "page_css", "post", - "title", post.title(), - "published_date", post.timePublished().atZone(ZoneId.of("Europe/Oslo")).format( - DateTimeFormatter.ofPattern("MMM dd, yyyy")), - "author", "by Elias Haugsbakk", - "content", MarkdownConverter.convertToHtml(post.content()), - "back_to_index", "<a href=\"/blog/\">back to all posts</a>" - )), () -> ctx.status(404)); + var postOpt = blogPostRepo.getPost(ctx.pathParam("slug")); + + if (postOpt.isEmpty()) { + ctx.status(404); + return; + } + + Post post = postOpt.get(); + String formattedDate = post.timePublished().atZone(OSLO_ZONE).format(DATE_FORMATTER); + + ctx.render("templates/post.html", Map.of( + "page_title", post.title() + " -- Elias Haugsbakk", + "page_css", "post", + "title", post.title(), + "published_date", formattedDate, + "author", "by Elias Haugsbakk", + "content", MarkdownConverter.convertToHtml(post.content()), + "back_to_index", "<a href=\"/blog/\">back to all posts</a>" + )); } - public void handleGetOverview(Context ctx) { - StringBuilder allPosts = new StringBuilder(); - - int prevYear = blogPostRepo.listPostsByPublishedDesc().getFirst().published() - .atZone(ZoneId.of("Europe/Oslo")).getYear(); - - allPosts.append(String.format(""" - <details class="year-accordion" open> - <summary class="year-toggle">%d</summary> - <ul class="post-list"> - """, prevYear)); - - for (var post : blogPostRepo.listPostsByPublishedDesc()) { - int year = post.published().atZone(ZoneId.of("Europe/Oslo")).getYear(); - if (year == prevYear) { - allPosts.append(String.format(""" - <li><a href="/blog/%s">%s</a></li> - """, post.slug(), post.title())); - } else { - allPosts.append(String.format(""" - </ul> - </details> - <details class="year-accordion"> - <summary class="year-toggle">%d</summary> - <ul class="post-list"> - <li><a href="/blog/%s">%s</a></li> - """, year, post.slug(), post.title())); - prevYear = year; - } + public void handleBlogRequest(Context ctx) { + String searchTerm = ctx.queryParam("search"); + if (searchTerm != null) { + handleSearch(ctx, searchTerm); + } else { + handleBlogIndex(ctx); } - allPosts.append(""" - </ul> - </details> - """); + } + private void handleBlogIndex(Context ctx) { - var featuredSlugs = List.of("test-page", "another-one"); + List<PostMetaData> allPosts = blogPostRepo.listPostsMetaData(); + Map<Integer, List<PostMetaData>> postsByYear = groupPostsByYear(allPosts); - StringBuilder featuredPosts = new StringBuilder(); - featuredPosts.append("<ul class=\"recent-list\">"); + // Manually list featured posts + var featuredSlugs = List.of( + "" + ); + List<Post> featuredPosts = new ArrayList<>(); for (String slug : featuredSlugs) { - blogPostRepo.getPost(slug).ifPresent(post -> featuredPosts.append(String.format(""" - <li> - <a href="/blog/%s">%s</a> <span class="featured-desc">-- %s</span> - </li> - """, post.slug(), post.title(), post.summary()))); + blogPostRepo.getPost(slug).ifPresent(featuredPosts::add); } - featuredPosts.append("</ul>"); ctx.render("templates/blog.html", - Map.of("page_title", "Elias Haugsbakk's Blog", "page_css", "blog", "all_posts", - allPosts.toString(), "recent_posts", featuredPosts.toString())); + Map.of( + "page_title", "Elias Haugsbakk's Blog", + "page_css", "blog", + "posts_by_year", postsByYear, + "featured_posts", featuredPosts, + "search_value", "" + )); + } + + private void handleSearch(Context ctx, String searchTerm) { + String query = searchTerm.trim(); + List<PostMetaData> results = query.isEmpty() ? List.of() : blogSearchService.searchPosts(query); + + List<SearchResultItem> resultItems = results.stream() + .map(post -> new SearchResultItem( + post.title(), + post.slug(), + post.summary() != null ? post.summary() : "", + post.timePublished().atZone(OSLO_ZONE).format(DATE_FORMATTER) + )) + .toList(); + + ctx.render("templates/blog-search.html", + Map.of( + "page_title", "Search Results -- Elias Haugsbakk", + "page_css", "blog", + "search_term", query, + "results", resultItems, + "results_count", resultItems.size() + )); + } + + private Map<Integer, List<PostMetaData>> groupPostsByYear(List<PostMetaData> posts) { + Map<Integer, List<PostMetaData>> unsortedGroups = posts.stream() + .collect(Collectors.groupingBy( + post -> post.timePublished().atZone(OSLO_ZONE).getYear(), + Collectors.toList() + )); + + Map<Integer, List<PostMetaData>> sortedGroups = new TreeMap<>(Comparator.reverseOrder()); + sortedGroups.putAll(unsortedGroups); + + return sortedGroups; } } diff --git a/clams-server/src/main/java/no/eliashaugsbakk/clams/server/model/PostMetaData.java b/clams-server/src/main/java/no/eliashaugsbakk/clams/server/model/PostMetaData.java index 98a1c49..44d667d 100644 --- a/clams-server/src/main/java/no/eliashaugsbakk/clams/server/model/PostMetaData.java +++ b/clams-server/src/main/java/no/eliashaugsbakk/clams/server/model/PostMetaData.java @@ -2,5 +2,5 @@ package no.eliashaugsbakk.clams.server.model; import java.time.Instant; -public record PostMetaData(String title, String slug, Instant published, Instant lastEdit) { +public record PostMetaData(String title, String slug, String summary, Instant timePublished, Instant lastEdit) { } diff --git a/clams-server/src/main/java/no/eliashaugsbakk/clams/server/repository/BlogPostRepo.java b/clams-server/src/main/java/no/eliashaugsbakk/clams/server/repository/BlogPostRepo.java index d63e095..8e43e2c 100644 --- a/clams-server/src/main/java/no/eliashaugsbakk/clams/server/repository/BlogPostRepo.java +++ b/clams-server/src/main/java/no/eliashaugsbakk/clams/server/repository/BlogPostRepo.java @@ -6,6 +6,7 @@ import no.eliashaugsbakk.clams.server.model.Post; import no.eliashaugsbakk.clams.server.model.PostMetaData; public interface BlogPostRepo { - List<PostMetaData> listPostsByPublishedDesc(); + List<PostMetaData> listPostsMetaData(); Optional<Post> getPost(String slug); + List<PostMetaData> searchPostsBody(String query); } diff --git a/clams-server/src/main/java/no/eliashaugsbakk/clams/server/repository/BlogPostRepoSqlite.java b/clams-server/src/main/java/no/eliashaugsbakk/clams/server/repository/BlogPostRepoSqlite.java index 2979589..77a7407 100644 --- a/clams-server/src/main/java/no/eliashaugsbakk/clams/server/repository/BlogPostRepoSqlite.java +++ b/clams-server/src/main/java/no/eliashaugsbakk/clams/server/repository/BlogPostRepoSqlite.java @@ -19,9 +19,9 @@ public class BlogPostRepoSqlite implements BlogPostRepo { } @Override - public List<PostMetaData> listPostsByPublishedDesc() { + public List<PostMetaData> listPostsMetaData() { String sql = """ - SELECT title, slug, published, last_edited + SELECT title, slug, summary, published, last_edited FROM posts ORDER BY published DESC """; @@ -35,13 +35,14 @@ public class BlogPostRepoSqlite implements BlogPostRepo { while (resultSet.next()) { String title = resultSet.getString("title"); String slug = resultSet.getString("slug"); + String summary = resultSet.getString("summary"); Instant published = Instant.parse(resultSet.getString("published")); String lastEditRaw = resultSet.getString("last_edited"); Instant lastEdit = (lastEditRaw != null) ? Instant.parse(lastEditRaw) : null; - posts.add(new PostMetaData(title, slug, published, lastEdit)); + posts.add(new PostMetaData(title, slug, summary, published, lastEdit)); } return posts; @@ -83,4 +84,40 @@ public class BlogPostRepoSqlite implements BlogPostRepo { throw new RepoException("Error fetching full post by slug: " + slug, e); } } + + @Override + public List<PostMetaData> searchPostsBody(String query) { + String sql = """ + SELECT title, slug, summary, published, last_edited + FROM posts + WHERE content LIKE ? + ORDER BY published DESC + """; + + List<PostMetaData> posts = new ArrayList<>(); + + try (Connection conn = manager.getConnection(); + PreparedStatement stmt = conn.prepareStatement(sql)) { + + String likeParam = "%" + query + "%"; + stmt.setString(1, likeParam); + + try (ResultSet resultSet = stmt.executeQuery()) { + while (resultSet.next()) { + String title = resultSet.getString("title"); + String slug = resultSet.getString("slug"); + String summary = resultSet.getString("summary"); + Instant published = Instant.parse(resultSet.getString("published")); + Instant lastEdit = Instant.parse(resultSet.getString("last_edited")); + + posts.add(new PostMetaData(title, slug, summary, published, lastEdit)); + } + } + + return posts; + + } catch (SQLException e) { + throw new RepoException("Error searching posts for query: " + query, e); + } + } } diff --git a/clams-server/src/main/java/no/eliashaugsbakk/clams/server/service/BlogSearchService.java b/clams-server/src/main/java/no/eliashaugsbakk/clams/server/service/BlogSearchService.java new file mode 100644 index 0000000..26595f1 --- /dev/null +++ b/clams-server/src/main/java/no/eliashaugsbakk/clams/server/service/BlogSearchService.java @@ -0,0 +1,49 @@ +package no.eliashaugsbakk.clams.server.service; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import no.eliashaugsbakk.clams.server.model.PostMetaData; +import no.eliashaugsbakk.clams.server.repository.BlogPostRepo; + +public class BlogSearchService { + private final BlogPostRepo blogPostRepo; + + public BlogSearchService(BlogPostRepo blogPostRepo) { + this.blogPostRepo = blogPostRepo; + } + + public List<PostMetaData> searchPosts(String query) { + if (query == null || query.isBlank()) return List.of(); + + int titleValue = 10; + int summaryValue = 5; + int bodyValue = 1; + + Map<PostMetaData, Integer> searchRanking = new HashMap<>(); + String lowerQuery = query.toLowerCase(); + + List<PostMetaData> postsMetaData = blogPostRepo.listPostsMetaData(); + List<PostMetaData> bodyResults = blogPostRepo.searchPostsBody(query); + + postsMetaData.stream() + .filter(post -> post.title().toLowerCase().contains(lowerQuery)) + .forEach(post -> searchRanking.merge(post, titleValue, Integer::sum)); + + postsMetaData.stream() + .filter(post -> post.summary() != null && post.summary().toLowerCase().contains(lowerQuery)) + .forEach(post -> searchRanking.merge(post, summaryValue, Integer::sum)); + + bodyResults.forEach(post -> searchRanking.merge(post, bodyValue, Integer::sum)); + + for (var item : searchRanking.entrySet()) { + IO.println(item.getKey()); + IO.println(item.getValue()); + } + + return searchRanking.entrySet().stream() + .sorted((e1, e2) -> Integer.compare(e2.getValue(), e1.getValue())) + .map(Map.Entry::getKey) + .toList(); + } +} |
