Advanced Filters - Build optimizations
This commit is contained in:
@@ -90,12 +90,33 @@ public class StoryController {
|
||||
public ResponseEntity<StorySummaryDto> getRandomStory(
|
||||
@RequestParam(required = false) String searchQuery,
|
||||
@RequestParam(required = false) List<String> tags,
|
||||
@RequestParam(required = false) Long seed) {
|
||||
@RequestParam(required = false) Long seed,
|
||||
// Advanced filters
|
||||
@RequestParam(required = false) Integer minWordCount,
|
||||
@RequestParam(required = false) Integer maxWordCount,
|
||||
@RequestParam(required = false) String createdAfter,
|
||||
@RequestParam(required = false) String createdBefore,
|
||||
@RequestParam(required = false) String lastReadAfter,
|
||||
@RequestParam(required = false) String lastReadBefore,
|
||||
@RequestParam(required = false) Integer minRating,
|
||||
@RequestParam(required = false) Integer maxRating,
|
||||
@RequestParam(required = false) Boolean unratedOnly,
|
||||
@RequestParam(required = false) String readingStatus,
|
||||
@RequestParam(required = false) Boolean hasReadingProgress,
|
||||
@RequestParam(required = false) Boolean hasCoverImage,
|
||||
@RequestParam(required = false) String sourceDomain,
|
||||
@RequestParam(required = false) String seriesFilter,
|
||||
@RequestParam(required = false) Integer minTagCount,
|
||||
@RequestParam(required = false) Boolean popularOnly,
|
||||
@RequestParam(required = false) Boolean hiddenGemsOnly) {
|
||||
|
||||
logger.info("Getting random story with filters - searchQuery: {}, tags: {}, seed: {}",
|
||||
searchQuery, tags, seed);
|
||||
|
||||
Optional<Story> randomStory = storyService.findRandomStory(searchQuery, tags, seed);
|
||||
Optional<Story> randomStory = storyService.findRandomStory(searchQuery, tags, seed,
|
||||
minWordCount, maxWordCount, createdAfter, createdBefore, lastReadAfter, lastReadBefore,
|
||||
minRating, maxRating, unratedOnly, readingStatus, hasReadingProgress, hasCoverImage,
|
||||
sourceDomain, seriesFilter, minTagCount, popularOnly, hiddenGemsOnly);
|
||||
|
||||
if (randomStory.isPresent()) {
|
||||
StorySummaryDto storyDto = convertToSummaryDto(randomStory.get());
|
||||
@@ -273,12 +294,31 @@ public class StoryController {
|
||||
@RequestParam(required = false) Integer maxRating,
|
||||
@RequestParam(required = false) String sortBy,
|
||||
@RequestParam(required = false) String sortDir,
|
||||
@RequestParam(required = false) String facetBy) {
|
||||
@RequestParam(required = false) String facetBy,
|
||||
// Advanced filters
|
||||
@RequestParam(required = false) Integer minWordCount,
|
||||
@RequestParam(required = false) Integer maxWordCount,
|
||||
@RequestParam(required = false) String createdAfter,
|
||||
@RequestParam(required = false) String createdBefore,
|
||||
@RequestParam(required = false) String lastReadAfter,
|
||||
@RequestParam(required = false) String lastReadBefore,
|
||||
@RequestParam(required = false) Boolean unratedOnly,
|
||||
@RequestParam(required = false) String readingStatus,
|
||||
@RequestParam(required = false) Boolean hasReadingProgress,
|
||||
@RequestParam(required = false) Boolean hasCoverImage,
|
||||
@RequestParam(required = false) String sourceDomain,
|
||||
@RequestParam(required = false) String seriesFilter,
|
||||
@RequestParam(required = false) Integer minTagCount,
|
||||
@RequestParam(required = false) Boolean popularOnly,
|
||||
@RequestParam(required = false) Boolean hiddenGemsOnly) {
|
||||
|
||||
|
||||
if (typesenseService != null) {
|
||||
SearchResultDto<StorySearchDto> results = typesenseService.searchStories(
|
||||
query, page, size, authors, tags, minRating, maxRating, sortBy, sortDir, facetBy);
|
||||
query, page, size, authors, tags, minRating, maxRating, sortBy, sortDir, facetBy,
|
||||
minWordCount, maxWordCount, createdAfter, createdBefore, lastReadAfter, lastReadBefore,
|
||||
unratedOnly, readingStatus, hasReadingProgress, hasCoverImage, sourceDomain, seriesFilter,
|
||||
minTagCount, popularOnly, hiddenGemsOnly);
|
||||
return ResponseEntity.ok(results);
|
||||
} else {
|
||||
// Fallback to basic search if Typesense is not available
|
||||
|
||||
@@ -682,7 +682,13 @@ public class StoryService {
|
||||
*/
|
||||
@Transactional(readOnly = true)
|
||||
public Optional<Story> findRandomStory(String searchQuery, List<String> tags) {
|
||||
return findRandomStory(searchQuery, tags, null);
|
||||
return findRandomStory(searchQuery, tags, null, null, null, null, null, null, null,
|
||||
null, null, null, null, null, null, null, null, null, null, null);
|
||||
}
|
||||
|
||||
public Optional<Story> findRandomStory(String searchQuery, List<String> tags, Long seed) {
|
||||
return findRandomStory(searchQuery, tags, seed, null, null, null, null, null, null,
|
||||
null, null, null, null, null, null, null, null, null, null, null);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -695,12 +701,23 @@ public class StoryService {
|
||||
* @return Optional containing the random story if found
|
||||
*/
|
||||
@Transactional(readOnly = true)
|
||||
public Optional<Story> findRandomStory(String searchQuery, List<String> tags, Long seed) {
|
||||
public Optional<Story> findRandomStory(String searchQuery, List<String> tags, Long seed,
|
||||
Integer minWordCount, Integer maxWordCount,
|
||||
String createdAfter, String createdBefore,
|
||||
String lastReadAfter, String lastReadBefore,
|
||||
Integer minRating, Integer maxRating, Boolean unratedOnly,
|
||||
String readingStatus, Boolean hasReadingProgress,
|
||||
Boolean hasCoverImage, String sourceDomain,
|
||||
String seriesFilter, Integer minTagCount,
|
||||
Boolean popularOnly, Boolean hiddenGemsOnly) {
|
||||
|
||||
// Use Typesense if available for consistency with Library search
|
||||
if (typesenseService != null) {
|
||||
try {
|
||||
Optional<UUID> randomStoryId = typesenseService.getRandomStoryId(searchQuery, tags, seed);
|
||||
Optional<UUID> randomStoryId = typesenseService.getRandomStoryId(searchQuery, tags, seed,
|
||||
minWordCount, maxWordCount, createdAfter, createdBefore, lastReadAfter, lastReadBefore,
|
||||
minRating, maxRating, unratedOnly, readingStatus, hasReadingProgress, hasCoverImage,
|
||||
sourceDomain, seriesFilter, minTagCount, popularOnly, hiddenGemsOnly);
|
||||
if (randomStoryId.isPresent()) {
|
||||
return storyRepository.findById(randomStoryId.get());
|
||||
}
|
||||
|
||||
@@ -124,9 +124,16 @@ public class TypesenseService {
|
||||
new Field().name("wordCount").type("int32").facet(true).sort(true).optional(true),
|
||||
new Field().name("volume").type("int32").facet(true).sort(true).optional(true),
|
||||
new Field().name("createdAt").type("int64").facet(false).sort(true),
|
||||
new Field().name("lastReadAt").type("int64").facet(false).sort(true),
|
||||
new Field().name("lastReadAt").type("int64").facet(false).sort(true).optional(true),
|
||||
new Field().name("sourceUrl").type("string").facet(false).optional(true),
|
||||
new Field().name("coverPath").type("string").facet(false).optional(true)
|
||||
new Field().name("coverPath").type("string").facet(false).optional(true),
|
||||
// New advanced filter fields
|
||||
new Field().name("isRead").type("bool").facet(true).optional(true),
|
||||
new Field().name("readingPosition").type("int32").facet(true).sort(true).optional(true),
|
||||
new Field().name("hasCover").type("bool").facet(true).optional(true),
|
||||
new Field().name("sourceDomain").type("string").facet(true).optional(true),
|
||||
new Field().name("isPartOfSeries").type("bool").facet(true).optional(true),
|
||||
new Field().name("tagCount").type("int32").facet(true).sort(true).optional(true)
|
||||
);
|
||||
|
||||
CollectionSchema collectionSchema = new CollectionSchema()
|
||||
@@ -249,7 +256,23 @@ public class TypesenseService {
|
||||
Integer maxRating,
|
||||
String sortBy,
|
||||
String sortDir,
|
||||
String facetBy) {
|
||||
String facetBy,
|
||||
// Advanced filters
|
||||
Integer minWordCount,
|
||||
Integer maxWordCount,
|
||||
String createdAfter,
|
||||
String createdBefore,
|
||||
String lastReadAfter,
|
||||
String lastReadBefore,
|
||||
Boolean unratedOnly,
|
||||
String readingStatus,
|
||||
Boolean hasReadingProgress,
|
||||
Boolean hasCoverImage,
|
||||
String sourceDomain,
|
||||
String seriesFilter,
|
||||
Integer minTagCount,
|
||||
Boolean popularOnly,
|
||||
Boolean hiddenGemsOnly) {
|
||||
|
||||
try {
|
||||
long startTime = System.currentTimeMillis();
|
||||
@@ -309,6 +332,99 @@ public class TypesenseService {
|
||||
filterConditions.add("rating:<=" + maxRating);
|
||||
}
|
||||
|
||||
// Advanced filters
|
||||
if (minWordCount != null) {
|
||||
filterConditions.add("wordCount:>=" + minWordCount);
|
||||
}
|
||||
|
||||
if (maxWordCount != null) {
|
||||
filterConditions.add("wordCount:<=" + maxWordCount);
|
||||
}
|
||||
|
||||
if (createdAfter != null) {
|
||||
long timestamp = convertDateStringToTimestamp(createdAfter + "T00:00:00Z");
|
||||
filterConditions.add("createdAt:>" + timestamp);
|
||||
}
|
||||
|
||||
if (createdBefore != null) {
|
||||
long timestamp = convertDateStringToTimestamp(createdBefore + "T23:59:59Z");
|
||||
filterConditions.add("createdAt:<" + timestamp);
|
||||
}
|
||||
|
||||
if (lastReadAfter != null) {
|
||||
long timestamp = convertDateStringToTimestamp(lastReadAfter + "T00:00:00Z");
|
||||
filterConditions.add("lastReadAt:>" + timestamp);
|
||||
}
|
||||
|
||||
if (lastReadBefore != null) {
|
||||
long timestamp = convertDateStringToTimestamp(lastReadBefore + "T23:59:59Z");
|
||||
filterConditions.add("lastReadAt:<" + timestamp);
|
||||
}
|
||||
|
||||
if (unratedOnly != null && unratedOnly) {
|
||||
filterConditions.add("rating:=0");
|
||||
}
|
||||
|
||||
if (readingStatus != null) {
|
||||
switch (readingStatus.toLowerCase()) {
|
||||
case "unread":
|
||||
filterConditions.add("isRead:=false && readingPosition:=0");
|
||||
break;
|
||||
case "started":
|
||||
filterConditions.add("isRead:=false && readingPosition:>0");
|
||||
break;
|
||||
case "completed":
|
||||
filterConditions.add("isRead:=true");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (hasReadingProgress != null) {
|
||||
if (hasReadingProgress) {
|
||||
filterConditions.add("readingPosition:>0");
|
||||
} else {
|
||||
filterConditions.add("readingPosition:=0");
|
||||
}
|
||||
}
|
||||
|
||||
if (hasCoverImage != null) {
|
||||
if (hasCoverImage) {
|
||||
filterConditions.add("hasCover:=true");
|
||||
} else {
|
||||
filterConditions.add("hasCover:=false");
|
||||
}
|
||||
}
|
||||
|
||||
if (sourceDomain != null && !sourceDomain.trim().isEmpty()) {
|
||||
filterConditions.add("sourceDomain:=" + escapeTypesenseValue(sourceDomain.trim()));
|
||||
}
|
||||
|
||||
if (seriesFilter != null) {
|
||||
switch (seriesFilter.toLowerCase()) {
|
||||
case "standalone":
|
||||
filterConditions.add("isPartOfSeries:=false");
|
||||
break;
|
||||
case "series":
|
||||
filterConditions.add("isPartOfSeries:=true");
|
||||
break;
|
||||
case "firstinseries":
|
||||
filterConditions.add("isPartOfSeries:=true && volume:=1");
|
||||
break;
|
||||
case "lastinseries":
|
||||
// This is complex to implement without knowing series lengths
|
||||
// For now, we'll skip this specific filter
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (minTagCount != null) {
|
||||
filterConditions.add("tagCount:>=" + minTagCount);
|
||||
}
|
||||
|
||||
// Note: popularOnly and hiddenGemsOnly would require calculating average ratings
|
||||
// which is complex for Typesense - these could be implemented as post-processing
|
||||
// or by maintaining aggregate statistics in the index
|
||||
|
||||
if (!filterConditions.isEmpty()) {
|
||||
String finalFilter = String.join(" && ", filterConditions);
|
||||
searchParameters.filterBy(finalFilter);
|
||||
@@ -399,12 +515,26 @@ public class TypesenseService {
|
||||
}
|
||||
}
|
||||
|
||||
// Backward compatibility overloaded methods
|
||||
public Optional<UUID> getRandomStoryId(String searchQuery, List<String> tags, Long seed) {
|
||||
return getRandomStoryId(searchQuery, tags, seed, null, null, null, null, null, null,
|
||||
null, null, null, null, null, null, null, null, null, null, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 Typesense's native _rand() function for efficient randomization with optional seed support.
|
||||
*/
|
||||
public Optional<UUID> getRandomStoryId(String searchQuery, List<String> tags, Long seed) {
|
||||
public Optional<UUID> getRandomStoryId(String searchQuery, List<String> tags, Long seed,
|
||||
Integer minWordCount, Integer maxWordCount,
|
||||
String createdAfter, String createdBefore,
|
||||
String lastReadAfter, String lastReadBefore,
|
||||
Integer minRating, Integer maxRating, Boolean unratedOnly,
|
||||
String readingStatus, Boolean hasReadingProgress,
|
||||
Boolean hasCoverImage, String sourceDomain,
|
||||
String seriesFilter, Integer minTagCount,
|
||||
Boolean popularOnly, Boolean hiddenGemsOnly) {
|
||||
try {
|
||||
String normalizedQuery = (searchQuery == null || searchQuery.trim().isEmpty()) ? "*" : searchQuery.trim();
|
||||
|
||||
@@ -422,12 +552,111 @@ public class TypesenseService {
|
||||
.sortBy(sortBy)
|
||||
.perPage(1); // Only need one random result
|
||||
|
||||
// Add tag filters if provided
|
||||
// Build all filter conditions (reuse logic from searchStories)
|
||||
List<String> filterConditions = new ArrayList<>();
|
||||
|
||||
if (tags != null && !tags.isEmpty()) {
|
||||
String tagFilter = tags.stream()
|
||||
.map(tag -> "tagNames:=" + escapeTypesenseValue(tag))
|
||||
.collect(Collectors.joining(" && "));
|
||||
searchParameters.filterBy(tagFilter);
|
||||
for (String tag : tags) {
|
||||
String escaped = escapeTypesenseValue(tag);
|
||||
filterConditions.add("tagNames:=" + escaped);
|
||||
}
|
||||
}
|
||||
|
||||
// Add advanced filters (same logic as searchStories method)
|
||||
if (minWordCount != null) {
|
||||
filterConditions.add("wordCount:>=" + minWordCount);
|
||||
}
|
||||
|
||||
if (maxWordCount != null) {
|
||||
filterConditions.add("wordCount:<=" + maxWordCount);
|
||||
}
|
||||
|
||||
if (createdAfter != null) {
|
||||
filterConditions.add("createdAt:>" + createdAfter + "T00:00:00Z");
|
||||
}
|
||||
|
||||
if (createdBefore != null) {
|
||||
filterConditions.add("createdAt:<" + createdBefore + "T23:59:59Z");
|
||||
}
|
||||
|
||||
if (lastReadAfter != null) {
|
||||
filterConditions.add("lastReadAt:>" + lastReadAfter + "T00:00:00Z");
|
||||
}
|
||||
|
||||
if (lastReadBefore != null) {
|
||||
filterConditions.add("lastReadAt:<" + lastReadBefore + "T23:59:59Z");
|
||||
}
|
||||
|
||||
if (minRating != null) {
|
||||
filterConditions.add("rating:>=" + minRating);
|
||||
}
|
||||
|
||||
if (maxRating != null) {
|
||||
filterConditions.add("rating:<=" + maxRating);
|
||||
}
|
||||
|
||||
if (unratedOnly != null && unratedOnly) {
|
||||
filterConditions.add("rating:=0");
|
||||
}
|
||||
|
||||
if (readingStatus != null) {
|
||||
switch (readingStatus.toLowerCase()) {
|
||||
case "unread":
|
||||
filterConditions.add("isRead:=false && readingPosition:=0");
|
||||
break;
|
||||
case "started":
|
||||
filterConditions.add("isRead:=false && readingPosition:>0");
|
||||
break;
|
||||
case "completed":
|
||||
filterConditions.add("isRead:=true");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (hasReadingProgress != null) {
|
||||
if (hasReadingProgress) {
|
||||
filterConditions.add("readingPosition:>0");
|
||||
} else {
|
||||
filterConditions.add("readingPosition:=0");
|
||||
}
|
||||
}
|
||||
|
||||
if (hasCoverImage != null) {
|
||||
if (hasCoverImage) {
|
||||
filterConditions.add("hasCover:=true");
|
||||
} else {
|
||||
filterConditions.add("hasCover:=false");
|
||||
}
|
||||
}
|
||||
|
||||
if (sourceDomain != null && !sourceDomain.trim().isEmpty()) {
|
||||
filterConditions.add("sourceDomain:=" + escapeTypesenseValue(sourceDomain.trim()));
|
||||
}
|
||||
|
||||
if (seriesFilter != null) {
|
||||
switch (seriesFilter.toLowerCase()) {
|
||||
case "standalone":
|
||||
filterConditions.add("isPartOfSeries:=false");
|
||||
break;
|
||||
case "series":
|
||||
filterConditions.add("isPartOfSeries:=true");
|
||||
break;
|
||||
case "firstinseries":
|
||||
filterConditions.add("isPartOfSeries:=true && volume:=1");
|
||||
break;
|
||||
case "lastinseries":
|
||||
// Skip for now - complex to implement
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (minTagCount != null) {
|
||||
filterConditions.add("tagCount:>=" + minTagCount);
|
||||
}
|
||||
|
||||
if (!filterConditions.isEmpty()) {
|
||||
String finalFilter = String.join(" && ", filterConditions);
|
||||
searchParameters.filterBy(finalFilter);
|
||||
}
|
||||
|
||||
SearchResult searchResult = libraryService.getCurrentTypesenseClient().collections(getStoriesCollection())
|
||||
@@ -449,7 +678,10 @@ public class TypesenseService {
|
||||
logger.warn("Failed to use _rand() function, falling back to offset-based randomization: {}", randException.getMessage());
|
||||
|
||||
// Fallback to offset-based randomization if _rand() is not supported
|
||||
return getRandomStoryIdFallback(normalizedQuery, tags, seed);
|
||||
return getRandomStoryIdFallback(normalizedQuery, tags, seed, minWordCount, maxWordCount,
|
||||
createdAfter, createdBefore, lastReadAfter, lastReadBefore, minRating, maxRating,
|
||||
unratedOnly, readingStatus, hasReadingProgress, hasCoverImage, sourceDomain,
|
||||
seriesFilter, minTagCount, popularOnly, hiddenGemsOnly);
|
||||
}
|
||||
|
||||
} catch (Exception e) {
|
||||
@@ -462,7 +694,15 @@ public class TypesenseService {
|
||||
* Fallback method for random story selection using offset-based randomization with seed support.
|
||||
* Used when Typesense's _rand() function is not available or supported.
|
||||
*/
|
||||
private Optional<UUID> getRandomStoryIdFallback(String normalizedQuery, List<String> tags, Long seed) {
|
||||
private Optional<UUID> getRandomStoryIdFallback(String normalizedQuery, List<String> tags, Long seed,
|
||||
Integer minWordCount, Integer maxWordCount,
|
||||
String createdAfter, String createdBefore,
|
||||
String lastReadAfter, String lastReadBefore,
|
||||
Integer minRating, Integer maxRating, Boolean unratedOnly,
|
||||
String readingStatus, Boolean hasReadingProgress,
|
||||
Boolean hasCoverImage, String sourceDomain,
|
||||
String seriesFilter, Integer minTagCount,
|
||||
Boolean popularOnly, Boolean hiddenGemsOnly) {
|
||||
try {
|
||||
// First, get the total count of matching stories
|
||||
SearchParameters countParameters = new SearchParameters()
|
||||
@@ -470,12 +710,115 @@ public class TypesenseService {
|
||||
.queryBy("title,description,authorName,seriesName,tagNames")
|
||||
.perPage(0); // No results, just count
|
||||
|
||||
// Add tag filters if provided
|
||||
// Build all filter conditions (same as main method)
|
||||
List<String> filterConditions = new ArrayList<>();
|
||||
|
||||
if (tags != null && !tags.isEmpty()) {
|
||||
String tagFilter = tags.stream()
|
||||
.map(tag -> "tagNames:=" + escapeTypesenseValue(tag))
|
||||
.collect(Collectors.joining(" && "));
|
||||
countParameters.filterBy(tagFilter);
|
||||
for (String tag : tags) {
|
||||
String escaped = escapeTypesenseValue(tag);
|
||||
filterConditions.add("tagNames:=" + escaped);
|
||||
}
|
||||
}
|
||||
|
||||
// Add advanced filters
|
||||
if (minWordCount != null) {
|
||||
filterConditions.add("wordCount:>=" + minWordCount);
|
||||
}
|
||||
|
||||
if (maxWordCount != null) {
|
||||
filterConditions.add("wordCount:<=" + maxWordCount);
|
||||
}
|
||||
|
||||
if (createdAfter != null) {
|
||||
long timestamp = convertDateStringToTimestamp(createdAfter + "T00:00:00Z");
|
||||
filterConditions.add("createdAt:>" + timestamp);
|
||||
}
|
||||
|
||||
if (createdBefore != null) {
|
||||
long timestamp = convertDateStringToTimestamp(createdBefore + "T23:59:59Z");
|
||||
filterConditions.add("createdAt:<" + timestamp);
|
||||
}
|
||||
|
||||
if (lastReadAfter != null) {
|
||||
long timestamp = convertDateStringToTimestamp(lastReadAfter + "T00:00:00Z");
|
||||
filterConditions.add("lastReadAt:>" + timestamp);
|
||||
}
|
||||
|
||||
if (lastReadBefore != null) {
|
||||
long timestamp = convertDateStringToTimestamp(lastReadBefore + "T23:59:59Z");
|
||||
filterConditions.add("lastReadAt:<" + timestamp);
|
||||
}
|
||||
|
||||
if (minRating != null) {
|
||||
filterConditions.add("rating:>=" + minRating);
|
||||
}
|
||||
|
||||
if (maxRating != null) {
|
||||
filterConditions.add("rating:<=" + maxRating);
|
||||
}
|
||||
|
||||
if (unratedOnly != null && unratedOnly) {
|
||||
filterConditions.add("rating:=0");
|
||||
}
|
||||
|
||||
if (readingStatus != null) {
|
||||
switch (readingStatus.toLowerCase()) {
|
||||
case "unread":
|
||||
filterConditions.add("isRead:=false && readingPosition:=0");
|
||||
break;
|
||||
case "started":
|
||||
filterConditions.add("isRead:=false && readingPosition:>0");
|
||||
break;
|
||||
case "completed":
|
||||
filterConditions.add("isRead:=true");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (hasReadingProgress != null) {
|
||||
if (hasReadingProgress) {
|
||||
filterConditions.add("readingPosition:>0");
|
||||
} else {
|
||||
filterConditions.add("readingPosition:=0");
|
||||
}
|
||||
}
|
||||
|
||||
if (hasCoverImage != null) {
|
||||
if (hasCoverImage) {
|
||||
filterConditions.add("hasCover:=true");
|
||||
} else {
|
||||
filterConditions.add("hasCover:=false");
|
||||
}
|
||||
}
|
||||
|
||||
if (sourceDomain != null && !sourceDomain.trim().isEmpty()) {
|
||||
filterConditions.add("sourceDomain:=" + escapeTypesenseValue(sourceDomain.trim()));
|
||||
}
|
||||
|
||||
if (seriesFilter != null) {
|
||||
switch (seriesFilter.toLowerCase()) {
|
||||
case "standalone":
|
||||
filterConditions.add("isPartOfSeries:=false");
|
||||
break;
|
||||
case "series":
|
||||
filterConditions.add("isPartOfSeries:=true");
|
||||
break;
|
||||
case "firstinseries":
|
||||
filterConditions.add("isPartOfSeries:=true && volume:=1");
|
||||
break;
|
||||
case "lastinseries":
|
||||
// Skip for now - complex to implement
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (minTagCount != null) {
|
||||
filterConditions.add("tagCount:>=" + minTagCount);
|
||||
}
|
||||
|
||||
if (!filterConditions.isEmpty()) {
|
||||
String finalFilter = String.join(" && ", filterConditions);
|
||||
countParameters.filterBy(finalFilter);
|
||||
}
|
||||
|
||||
SearchResult countResult = libraryService.getCurrentTypesenseClient().collections(getStoriesCollection())
|
||||
@@ -595,6 +938,27 @@ public class TypesenseService {
|
||||
document.put("coverPath", story.getCoverPath());
|
||||
}
|
||||
|
||||
// New advanced filter fields
|
||||
document.put("isRead", story.getIsRead() != null ? story.getIsRead() : false);
|
||||
document.put("readingPosition", story.getReadingPosition() != null ? story.getReadingPosition() : 0);
|
||||
document.put("hasCover", story.getCoverPath() != null && !story.getCoverPath().trim().isEmpty());
|
||||
|
||||
// Extract domain from source URL for filtering
|
||||
if (story.getSourceUrl() != null && !story.getSourceUrl().trim().isEmpty()) {
|
||||
try {
|
||||
java.net.URI uri = java.net.URI.create(story.getSourceUrl());
|
||||
String host = uri.getHost();
|
||||
document.put("sourceDomain", host != null ? host.toLowerCase() : "");
|
||||
} catch (Exception e) {
|
||||
document.put("sourceDomain", "");
|
||||
}
|
||||
} else {
|
||||
document.put("sourceDomain", "");
|
||||
}
|
||||
|
||||
document.put("isPartOfSeries", story.getSeries() != null);
|
||||
document.put("tagCount", story.getTags() != null ? story.getTags().size() : 0);
|
||||
|
||||
return document;
|
||||
}
|
||||
|
||||
@@ -1226,6 +1590,19 @@ public class TypesenseService {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert date string in ISO format to Unix timestamp for Typesense filtering
|
||||
*/
|
||||
private long convertDateStringToTimestamp(String dateString) {
|
||||
try {
|
||||
java.time.ZonedDateTime zdt = java.time.ZonedDateTime.parse(dateString);
|
||||
return zdt.toEpochSecond();
|
||||
} catch (Exception e) {
|
||||
logger.warn("Failed to parse date string: {}", dateString, e);
|
||||
return 0L;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Escape special characters in Typesense filter values.
|
||||
* Typesense requires certain characters to be escaped or quoted.
|
||||
|
||||
Reference in New Issue
Block a user