From c08082c0d6544ce460e0c966a851e111b410c689 Mon Sep 17 00:00:00 2001 From: Stefan Hardegger Date: Mon, 28 Jul 2025 14:37:58 +0200 Subject: [PATCH] Correct tag facets handling --- .../java/com/storycove/dto/FacetCountDto.java | 31 +++++++++++++ .../com/storycove/dto/SearchResultDto.java | 20 ++++++++ .../storycove/service/TypesenseService.java | 46 ++++++++++++++++++- frontend/src/app/library/page.tsx | 32 +++++-------- frontend/src/types/api.ts | 6 +++ 5 files changed, 114 insertions(+), 21 deletions(-) create mode 100644 backend/src/main/java/com/storycove/dto/FacetCountDto.java diff --git a/backend/src/main/java/com/storycove/dto/FacetCountDto.java b/backend/src/main/java/com/storycove/dto/FacetCountDto.java new file mode 100644 index 0000000..294d90c --- /dev/null +++ b/backend/src/main/java/com/storycove/dto/FacetCountDto.java @@ -0,0 +1,31 @@ +package com.storycove.dto; + +public class FacetCountDto { + + private String value; + private int count; + + public FacetCountDto() {} + + public FacetCountDto(String value, int count) { + this.value = value; + this.count = count; + } + + // Getters and Setters + public String getValue() { + return value; + } + + public void setValue(String value) { + this.value = value; + } + + public int getCount() { + return count; + } + + public void setCount(int count) { + this.count = count; + } +} \ No newline at end of file diff --git a/backend/src/main/java/com/storycove/dto/SearchResultDto.java b/backend/src/main/java/com/storycove/dto/SearchResultDto.java index b55ecb6..812036d 100644 --- a/backend/src/main/java/com/storycove/dto/SearchResultDto.java +++ b/backend/src/main/java/com/storycove/dto/SearchResultDto.java @@ -1,6 +1,7 @@ package com.storycove.dto; import java.util.List; +import java.util.Map; public class SearchResultDto { @@ -10,6 +11,7 @@ public class SearchResultDto { private int perPage; private String query; private long searchTimeMs; + private Map> facets; public SearchResultDto() {} @@ -22,6 +24,16 @@ public class SearchResultDto { this.searchTimeMs = searchTimeMs; } + public SearchResultDto(List results, long totalHits, int page, int perPage, String query, long searchTimeMs, Map> facets) { + this.results = results; + this.totalHits = totalHits; + this.page = page; + this.perPage = perPage; + this.query = query; + this.searchTimeMs = searchTimeMs; + this.facets = facets; + } + // Getters and Setters public List getResults() { return results; @@ -70,4 +82,12 @@ public class SearchResultDto { public void setSearchTimeMs(long searchTimeMs) { this.searchTimeMs = searchTimeMs; } + + public Map> getFacets() { + return facets; + } + + public void setFacets(Map> facets) { + this.facets = facets; + } } \ No newline at end of file diff --git a/backend/src/main/java/com/storycove/service/TypesenseService.java b/backend/src/main/java/com/storycove/service/TypesenseService.java index 8f16f66..804ff86 100644 --- a/backend/src/main/java/com/storycove/service/TypesenseService.java +++ b/backend/src/main/java/com/storycove/service/TypesenseService.java @@ -1,6 +1,7 @@ package com.storycove.service; import com.storycove.dto.AuthorSearchDto; +import com.storycove.dto.FacetCountDto; import com.storycove.dto.SearchResultDto; import com.storycove.dto.StorySearchDto; import com.storycove.entity.Author; @@ -227,6 +228,8 @@ public class TypesenseService { .highlightFields("title,description") .highlightStartTag("") .highlightEndTag("") + .facetBy("tagNames,authorName,rating") + .maxFacetValues(100) .sortBy(buildSortParameter(normalizedQuery, sortBy, sortDir)); // Add filters @@ -268,6 +271,7 @@ public class TypesenseService { List results = convertSearchResult(searchResult); + Map> facets = processFacetCounts(searchResult); long searchTime = System.currentTimeMillis() - startTime; return new SearchResultDto<>( @@ -276,7 +280,8 @@ public class TypesenseService { page, perPage, query, - searchTime + searchTime, + facets ); } catch (Exception e) { @@ -391,6 +396,45 @@ public class TypesenseService { return document; } + @SuppressWarnings("unchecked") + private Map> processFacetCounts(SearchResult searchResult) { + Map> facetMap = new HashMap<>(); + + if (searchResult.getFacetCounts() != null) { + for (FacetCounts facetCounts : searchResult.getFacetCounts()) { + String fieldName = facetCounts.getFieldName(); + List facetValues = new ArrayList<>(); + + if (facetCounts.getCounts() != null) { + for (Object countObj : facetCounts.getCounts()) { + if (countObj instanceof Map) { + Map countMap = (Map) countObj; + String value = (String) countMap.get("value"); + Integer count = (Integer) countMap.get("count"); + + if (value != null && count != null && count > 0) { + facetValues.add(new FacetCountDto(value, count)); + } + } + } + } + + if (!facetValues.isEmpty()) { + // Sort by count descending, then by value ascending + facetValues.sort((a, b) -> { + int countCompare = Integer.compare(b.getCount(), a.getCount()); + if (countCompare != 0) return countCompare; + return a.getValue().compareToIgnoreCase(b.getValue()); + }); + + facetMap.put(fieldName, facetValues); + } + } + } + + return facetMap; + } + @SuppressWarnings("unchecked") private List convertSearchResult(SearchResult searchResult) { return searchResult.getHits().stream() diff --git a/frontend/src/app/library/page.tsx b/frontend/src/app/library/page.tsx index 6312d2b..5aa1089 100644 --- a/frontend/src/app/library/page.tsx +++ b/frontend/src/app/library/page.tsx @@ -2,7 +2,7 @@ import { useState, useEffect } from 'react'; import { searchApi } from '../../lib/api'; -import { Story, Tag } from '../../types/api'; +import { Story, Tag, FacetCount } from '../../types/api'; import AppLayout from '../../components/layout/AppLayout'; import { Input } from '../../components/ui/Input'; import Button from '../../components/ui/Button'; @@ -29,24 +29,16 @@ export default function LibraryPage() { - // Extract tags from current search results with counts - const extractTagsFromResults = (stories: Story[]): Tag[] => { - const tagCounts: { [key: string]: number } = {}; + // Convert facet counts to Tag objects for the UI + const convertFacetsToTags = (facets?: Record): Tag[] => { + if (!facets || !facets.tagNames) { + return []; + } - stories.forEach(story => { - story.tagNames?.forEach(tagName => { - if (tagCounts[tagName]) { - tagCounts[tagName]++; - } else { - tagCounts[tagName] = 1; - } - }); - }); - - return Object.entries(tagCounts).map(([tagName, count]) => ({ - id: tagName, // Use tag name as ID since we don't have actual IDs from search results - name: tagName, - storyCount: count + return facets.tagNames.map(facet => ({ + id: facet.value, // Use tag name as ID since we don't have actual IDs from search results + name: facet.value, + storyCount: facet.count })); }; @@ -72,8 +64,8 @@ export default function LibraryPage() { setTotalPages(Math.ceil((result?.totalHits || 0) / 20)); setTotalElements(result?.totalHits || 0); - // Always update tags based on current search results (including initial wildcard search) - const resultTags = extractTagsFromResults(currentStories); + // Update tags from facets - these represent all matching stories, not just current page + const resultTags = convertFacetsToTags(result?.facets); setTags(resultTags); } catch (error) { console.error('Failed to load stories:', error); diff --git a/frontend/src/types/api.ts b/frontend/src/types/api.ts index 754778c..e195609 100644 --- a/frontend/src/types/api.ts +++ b/frontend/src/types/api.ts @@ -58,6 +58,11 @@ export interface AuthResponse { expiresIn: number; } +export interface FacetCount { + value: string; + count: number; +} + export interface SearchResult { results: Story[]; totalHits: number; @@ -65,6 +70,7 @@ export interface SearchResult { perPage: number; query: string; searchTimeMs: number; + facets?: Record; } export interface PagedResult {