Fix for Random Story Function
This commit is contained in:
@@ -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()
|
||||
|
||||
@@ -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());
|
||||
|
||||
Reference in New Issue
Block a user