Fix for Random Story Function

This commit is contained in:
Stefan Hardegger
2025-08-14 13:14:46 +02:00
parent 4357351ec8
commit 1d14d3d7aa
5 changed files with 128 additions and 5 deletions

View File

@@ -10,6 +10,8 @@ import com.storycove.repository.TagRepository;
import com.storycove.service.exception.DuplicateResourceException;
import com.storycove.service.exception.ResourceNotFoundException;
import jakarta.validation.Valid;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.data.domain.Page;
@@ -25,11 +27,14 @@ import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.UUID;
import java.util.stream.Collectors;
@Service
@Validated
@Transactional
public class StoryService {
private static final Logger logger = LoggerFactory.getLogger(StoryService.class);
private final StoryRepository storyRepository;
private final TagRepository tagRepository;
@@ -653,12 +658,35 @@ public class StoryService {
/**
* Find a random story based on optional filters.
* Uses count + random offset approach for performance with large datasets.
* Supports text search and multiple tags.
* Uses Typesense for consistency with Library search functionality.
* Supports text search and multiple tags using the same logic as the Library view.
*/
@Transactional(readOnly = true)
public Optional<Story> findRandomStory(String searchQuery, List<String> tags) {
// Use Typesense if available for consistency with Library search
if (typesenseService != null) {
try {
Optional<UUID> randomStoryId = typesenseService.getRandomStoryId(searchQuery, tags);
if (randomStoryId.isPresent()) {
return storyRepository.findById(randomStoryId.get());
}
return Optional.empty();
} catch (Exception e) {
// Fallback to database queries if Typesense fails
logger.warn("Typesense random story lookup failed, falling back to database queries", e);
}
}
// Fallback to original database-based implementation if Typesense is not available
return findRandomStoryFallback(searchQuery, tags);
}
/**
* Fallback method for random story selection using database queries.
* Used when Typesense is not available or fails.
*/
private Optional<Story> findRandomStoryFallback(String searchQuery, List<String> tags) {
// Clean up inputs
String cleanSearchQuery = (searchQuery != null && !searchQuery.trim().isEmpty()) ? searchQuery.trim() : null;
List<String> cleanTags = (tags != null) ? tags.stream()

View File

@@ -352,6 +352,87 @@ public class TypesenseService {
}
}
/**
* Get a random story using the same search logic as the Library view.
* This ensures consistency between Library search results and Random Story functionality.
* Uses offset-based randomization since Typesense v0.25.0 doesn't support _rand() sorting.
*/
public Optional<UUID> getRandomStoryId(String searchQuery, List<String> tags) {
try {
String normalizedQuery = (searchQuery == null || searchQuery.trim().isEmpty()) ? "*" : searchQuery.trim();
// First, get the total count of matching stories
SearchParameters countParameters = new SearchParameters()
.q(normalizedQuery)
.queryBy("title,description,contentPlain,authorName,seriesName,tagNames")
.perPage(0); // No results, just count
// Add tag filters if provided
if (tags != null && !tags.isEmpty()) {
String tagFilter = tags.stream()
.map(tag -> "tagNames:=" + escapeTypesenseValue(tag))
.collect(Collectors.joining(" && "));
countParameters.filterBy(tagFilter);
}
logger.debug("Getting random story with query: '{}', tags: {}", normalizedQuery, tags);
SearchResult countResult = typesenseClient.collections(STORIES_COLLECTION)
.documents()
.search(countParameters);
long totalHits = countResult.getFound();
if (totalHits == 0) {
logger.debug("No stories found matching filters");
return Optional.empty();
}
// Generate random offset within the total hits
long randomOffset = (long) (Math.random() * totalHits);
logger.debug("Total hits: {}, using random offset: {}", totalHits, randomOffset);
// Now get the actual story at that offset
SearchParameters storyParameters = new SearchParameters()
.q(normalizedQuery)
.queryBy("title,description,contentPlain,authorName,seriesName,tagNames")
.page((int) (randomOffset / 250) + 1) // Calculate page (Typesense uses 1-based pages)
.perPage(250) // Get multiple results to account for offset within page
.sortBy("_text_match:desc,createdAt:desc"); // Use standard sorting
// Add same tag filters
if (tags != null && !tags.isEmpty()) {
String tagFilter = tags.stream()
.map(tag -> "tagNames:=" + escapeTypesenseValue(tag))
.collect(Collectors.joining(" && "));
storyParameters.filterBy(tagFilter);
}
SearchResult storyResult = typesenseClient.collections(STORIES_COLLECTION)
.documents()
.search(storyParameters);
if (storyResult.getHits().isEmpty()) {
logger.debug("No stories found in random offset query");
return Optional.empty();
}
// Calculate which hit to select from the page
int indexInPage = (int) (randomOffset % 250);
indexInPage = Math.min(indexInPage, storyResult.getHits().size() - 1);
SearchResultHit hit = storyResult.getHits().get(indexInPage);
String storyId = (String) hit.getDocument().get("id");
logger.debug("Found random story ID: {} at offset {} (page {}, index {})",
storyId, randomOffset, storyParameters.getPage(), indexInPage);
return Optional.of(UUID.fromString(storyId));
} catch (Exception e) {
logger.error("Failed to get random story with query: '{}', tags: {}", searchQuery, tags, e);
return Optional.empty();
}
}
private Map<String, Object> createStoryDocument(Story story) {
Map<String, Object> document = new HashMap<>();
document.put("id", story.getId().toString());