[Bugfix] 修复 CurseForge 搜索部分内容正确结果很靠下的问题 & 移除没有实际意义的搜索排序方式 (#6619)

This commit is contained in:
ToobLac
2026-09-09 20:06:12 +08:00
committed by GitHub
parent 7331f619cf
commit 8767cc0e98
16 changed files with 80 additions and 172 deletions
@@ -39,8 +39,6 @@ public abstract class LocalizedRemoteAddonRepository implements RemoteAddonRepos
protected abstract RemoteAddonRepository getBackedRemoteModRepository();
protected abstract SortType getBackedRemoteModRepositorySortOrder();
@Override
public String getApiBaseUrl() {
return getBackedRemoteModRepository().getApiBaseUrl();
@@ -80,8 +78,8 @@ public abstract class LocalizedRemoteAddonRepository implements RemoteAddonRepos
SearchResult searchResult = null;
List<RemoteAddon> remoteAddons = List.of();
for (String englishSearchFilter : englishSearchFiltersSet) {
searchResult = getBackedRemoteModRepository().search(downloadProvider, gameVersion, category, pageOffset, pageSize, englishSearchFilter, getBackedRemoteModRepositorySortOrder(), sortOrder);
remoteAddons = searchResult.getUnsortedResults().toList();
searchResult = getBackedRemoteModRepository().search(downloadProvider, gameVersion, category, pageOffset, pageSize, englishSearchFilter, SortType.RELEVANCY, sortOrder);
remoteAddons = searchResult.results().toList();
if (!remoteAddons.isEmpty()) {
break;
}
@@ -100,7 +98,7 @@ public abstract class LocalizedRemoteAddonRepository implements RemoteAddonRepos
searchResultArray[englishIndex--] = remoteAddon;
}
}
totalPages = searchResult.getTotalPages();
totalPages = searchResult.totalPages();
}
StringUtils.LevCalculator levCalculator = new StringUtils.LevCalculator();
@@ -117,7 +117,7 @@ public class DownloadListPage extends Control implements DecoratorPage {
if (!searchInitialized) {
searchInitialized = true;
search("", null, 0, "", RemoteAddonRepository.SortType.POPULARITY);
search("", null, 0, "", RemoteAddonRepository.SortType.RELEVANCY);
}
if (instanceSelection) {
@@ -182,8 +182,8 @@ public class DownloadListPage extends Control implements DecoratorPage {
setLoading(false);
if (exception == null) {
items.setAll(result.getResults().collect(Collectors.toList()));
pageCount.set(result.getTotalPages());
items.setAll(result.results().collect(Collectors.toList()));
pageCount.set(result.totalPages());
failed.set(false);
} else {
failed.set(true);
@@ -371,7 +371,7 @@ public class DownloadListPage extends Control implements DecoratorPage {
sortStackPane.getChildren().setAll(sortComboBox);
sortComboBox.prefWidthProperty().bind(sortStackPane.widthProperty());
sortComboBox.getStyleClass().add("fit-width");
sortComboBox.setConverter(stringConverter(sortType -> i18n("curse.sort." + sortType.name().toLowerCase(Locale.ROOT))));
sortComboBox.setConverter(stringConverter(sortType -> i18n("addon.sort." + sortType.name().toLowerCase(Locale.ROOT))));
sortComboBox.getItems().setAll(RemoteAddonRepository.SortType.values());
sortComboBox.getSelectionModel().select(0);
searchPane.addRow(rowIndex++, new Label(i18n("addon.category")), categoryStackPane, new Label(i18n("search.sort")), sortStackPane);
@@ -102,15 +102,6 @@ public final class HMCLLocalizedDownloadListPage extends DownloadListPage {
}
}
@Override
protected SortType getBackedRemoteModRepositorySortOrder() {
if ("addon.modrinth".equals(downloadSource.get())) {
return SortType.NAME;
} else {
return SortType.POPULARITY;
}
}
@Override
public RemoteAddon.Type getType() {
return type;
@@ -190,6 +190,11 @@ addon.download.recommend=Recommended Version for Minecraft %1s
addon.download.title.release=Minecraft %s
addon.download.title.snapshot=Minecraft %s (Snapshots)
addon.modrinth=Modrinth
addon.sort.date_created=Date Created
addon.sort.last_updated=Last Updated
addon.sort.popularity=Popularity
addon.sort.relevancy=Relevancy
addon.sort.total_downloads=Total Downloads
archive.author=Author(s)
archive.date=Publish Date
@@ -371,13 +376,6 @@ curse.category.6553=Realistic
curse.category.6554=Fantasy
curse.category.6555=Vanilla
curse.sort.author=Author
curse.sort.date_created=Date Created
curse.sort.last_updated=Last Updated
curse.sort.name=Name
curse.sort.popularity=Popularity
curse.sort.total_downloads=Total Downloads
datetime.format=MMM d, yyyy, h\:mm\:ss a
download=Download
@@ -171,6 +171,10 @@ addon.check_update.source=المصدر
addon.check_update.target_version=الإصدار المستهدف
addon.curseforge=CurseForge
addon.modrinth=Modrinth
addon.sort.date_created=تاريخ الإنشاء
addon.sort.last_updated=آخر تحديث
addon.sort.popularity=الشعبية
addon.sort.total_downloads=إجمالي التنزيلات
archive.author=المؤلف/المؤلفون
archive.date=تاريخ النشر
@@ -340,13 +344,6 @@ curse.category.4550=مهام
curse.category.4555=توليد العالم
curse.category.4552=سكريبتات
curse.sort.author=المؤلف
curse.sort.date_created=تاريخ الإنشاء
curse.sort.last_updated=آخر تحديث
curse.sort.name=الاسم
curse.sort.popularity=الشعبية
curse.sort.total_downloads=إجمالي التنزيلات
datetime.format=MMM d, yyyy, h\:mm\:ss a
download=تنزيل
@@ -190,6 +190,10 @@ addon.download.recommend=Empfohlene Version für Minecraft %1s
addon.download.title.release=Minecraft %s
addon.download.title.snapshot=Minecraft %s (Snapshots)
addon.modrinth=Modrinth
addon.sort.date_created=Erstellungsdatum
addon.sort.last_updated=Zuletzt aktualisiert
addon.sort.popularity=Beliebtheit
addon.sort.total_downloads=Downloads insgesamt
archive.author=Autor(en)
archive.date=Veröffentlichungsdatum
@@ -371,13 +375,6 @@ curse.category.6553=Realistisch
curse.category.6554=Fantasie
curse.category.6555=Vanilla
curse.sort.author=Autor
curse.sort.date_created=Erstellungsdatum
curse.sort.last_updated=Zuletzt aktualisiert
curse.sort.name=Name
curse.sort.popularity=Beliebtheit
curse.sort.total_downloads=Downloads insgesamt
datetime.format=dd.MM.yyyy, HH\:mm\:ss
download=Herunterladen
@@ -152,6 +152,10 @@ addon.check_update.target_version=Versión de destino
addon.curseforge=CurseForge
addon.download.recommend=Versión recomendada - Minecraft %1s
addon.modrinth=Modrinth
addon.sort.date_created=Fecha de creación
addon.sort.last_updated=Última actualización
addon.sort.popularity=Popularidad
addon.sort.total_downloads=Descargas totales
archive.author=Autor(es)
archive.date=Fecha de publicación
@@ -320,13 +324,6 @@ curse.category.4550=Quests
curse.category.4555=Generación del mundo
curse.category.4552=Scripts
curse.sort.author=Autor
curse.sort.date_created=Fecha de creación
curse.sort.last_updated=Última actualización
curse.sort.name=Nombre
curse.sort.popularity=Popularidad
curse.sort.total_downloads=Descargas totales
datetime.format=d MMM yyyy, H\:mm\:ss
download=Descargar
@@ -119,6 +119,10 @@ addon.check_update.source=Source
addon.check_update.target_version=Target
addon.curseforge=CurseForge
addon.modrinth=Modrinth
addon.sort.date_created=作成日
addon.sort.last_updated=最終更新
addon.sort.popularity=人気
addon.sort.total_downloads=合計ダウンロード数
archive.author=作成者
archive.date=公開日
@@ -277,13 +281,6 @@ curse.category.4550=クエスト
curse.category.4555=World Gen
curse.category.4552=スクリプト
curse.sort.author=Author
curse.sort.date_created=作成日
curse.sort.last_updated=最終更新
curse.sort.name=名前
curse.sort.popularity=人気
curse.sort.total_downloads=合計ダウンロード数
datetime.format=yyyy/MM/dd H:mm:ss
download=ダウンロード
@@ -155,6 +155,10 @@ addon.check_update.target_version=將至之版
addon.curseforge=CurseForge
addon.download.recommend=宜版 - 礦藝 %1s
addon.modrinth=Modrinth
addon.sort.date_created=創立之期
addon.sort.last_updated=近日迭更
addon.sort.popularity=幾熱
addon.sort.total_downloads=引數
archive.author=作者
archive.date=廣佈之期
@@ -319,13 +323,6 @@ curse.category.4550=任務
curse.category.4555=生界新創
curse.category.4552=角本
curse.sort.author=作者
curse.sort.date_created=創立之期
curse.sort.last_updated=近日迭更
curse.sort.name=名
curse.sort.popularity=幾熱
curse.sort.total_downloads=引數
datetime.format=yyyy 年 MM 月 dd 日 HH:mm:ss
download=引
@@ -150,6 +150,10 @@ addon.check_update.source=Источник
addon.check_update.target_version=Целевая версия
addon.curseforge=CurseForge
addon.modrinth=Modrinth
addon.sort.date_created=Создан
addon.sort.last_updated=Обновлён
addon.sort.popularity=Популярность
addon.sort.total_downloads=Скачиваний
archive.author=Автор(ы)
archive.date=Дата публикации
@@ -316,13 +320,6 @@ curse.category.4550=Квесты
curse.category.4555=Генерация карты
curse.category.4552=Скрипты
curse.sort.author=Автор
curse.sort.date_created=Создан
curse.sort.last_updated=Обновлён
curse.sort.name=Название
curse.sort.popularity=Популярность
curse.sort.total_downloads=Скачиваний
datetime.format=d MMM yyyy г., HH\:mm\:ss
download=Скачать
@@ -195,6 +195,10 @@ addon.download.recommend=Рекомендована версія для Minecraf
addon.download.title.release=Minecraft %s
addon.download.title.snapshot=Minecraft %s (Снапшоти)
addon.modrinth=Modrinth
addon.sort.date_created=Дата створення
addon.sort.last_updated=Останнє оновлення
addon.sort.popularity=Популярність
addon.sort.total_downloads=Всього завантажень
archive.author=Автор(и)
archive.date=Дата публікації
@@ -371,13 +375,6 @@ curse.category.6553=Реалістично
curse.category.6554=Фентезі
curse.category.6555=Ваніла
curse.sort.author=Автор
curse.sort.date_created=Дата створення
curse.sort.last_updated=Останнє оновлення
curse.sort.name=Назва
curse.sort.popularity=Популярність
curse.sort.total_downloads=Всього завантажень
datetime.format=dd.MM.yyyy, HH:mm:ss
download=Завантажити
@@ -188,6 +188,11 @@ addon.download.recommend=推薦版本 - Minecraft %1s
addon.download.title.release=Minecraft %s
addon.download.title.snapshot=Minecraft %s (快照)
addon.modrinth=Modrinth
addon.sort.date_created=建立日期
addon.sort.last_updated=最近更新
addon.sort.popularity=熱度
addon.sort.relevancy=相關度
addon.sort.total_downloads=下載量
archive.author=作者
archive.date=發布日期
@@ -369,13 +374,6 @@ curse.category.6553=寫實
curse.category.6554=幻想
curse.category.6555=原生
curse.sort.author=作者
curse.sort.date_created=建立日期
curse.sort.last_updated=最近更新
curse.sort.name=名稱
curse.sort.popularity=熱度
curse.sort.total_downloads=下載量
datetime.format=yyyy 年 MM 月 dd 日 HH:mm:ss
download=下載
@@ -190,6 +190,11 @@ addon.download.recommend=推荐版本 - Minecraft %1s
addon.download.title.release=Minecraft %s
addon.download.title.snapshot=Minecraft %s (快照)
addon.modrinth=Modrinth
addon.sort.date_created=创建日期
addon.sort.last_updated=最近更新
addon.sort.popularity=热度
addon.sort.relevancy=相关度
addon.sort.total_downloads=下载量
archive.author=作者
archive.date=发布日期
@@ -371,13 +376,6 @@ curse.category.6553=写实
curse.category.6554=幻想
curse.category.6555=原生
curse.sort.author=作者
curse.sort.date_created=创建日期
curse.sort.last_updated=最近更新
curse.sort.name=名称
curse.sort.popularity=热度
curse.sort.total_downloads=下载量
datetime.format=yyyy 年 MM 月 dd 日 HH:mm:ss
download=下载
@@ -35,52 +35,6 @@ public interface RemoteAddonRepository {
String getBaseUrl();
enum SortType {
POPULARITY,
NAME,
DATE_CREATED,
LAST_UPDATED,
AUTHOR,
TOTAL_DOWNLOADS
}
enum SortOrder {
ASC,
DESC
}
class SearchResult {
private final Stream<RemoteAddon> sortedResults;
private final Stream<RemoteAddon> unsortedResults;
private final int totalPages;
public SearchResult(Stream<RemoteAddon> sortedResults, Stream<RemoteAddon> unsortedResults, int totalPages) {
this.sortedResults = sortedResults;
this.unsortedResults = unsortedResults;
this.totalPages = totalPages;
}
public SearchResult(Stream<RemoteAddon> sortedResults, int pages) {
this.sortedResults = sortedResults;
this.unsortedResults = sortedResults;
this.totalPages = pages;
}
public Stream<RemoteAddon> getResults() {
return this.sortedResults;
}
public Stream<RemoteAddon> getUnsortedResults() {
return this.unsortedResults;
}
public int getTotalPages() {
return this.totalPages;
}
}
SearchResult search(DownloadProvider downloadProvider, String gameVersion, @Nullable Category category, int pageOffset, int pageSize, String searchFilter, SortType sortType, SortOrder sortOrder)
throws IOException;
@@ -106,4 +60,20 @@ public interface RemoteAddonRepository {
record Category(Object self, String id, List<Category> subcategories) {
}
enum SortType {
RELEVANCY,
POPULARITY,
DATE_CREATED,
LAST_UPDATED,
TOTAL_DOWNLOADS
}
enum SortOrder {
ASC,
DESC
}
record SearchResult(Stream<RemoteAddon> results, int totalPages) {
}
}
@@ -24,7 +24,6 @@ import org.jackhuang.hmcl.addon.RemoteAddonRepository;
import org.jackhuang.hmcl.download.DownloadProvider;
import org.jackhuang.hmcl.util.Immutable;
import org.jackhuang.hmcl.util.MurmurHash2;
import org.jackhuang.hmcl.util.Pair;
import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.io.*;
import org.jackhuang.hmcl.util.versioning.GameVersionNumber;
@@ -60,8 +59,6 @@ public final class CurseForgeRemoteAddonRepository implements RemoteAddonReposit
public static final String API_KEY = System.getProperty("hmcl.curseforge.apikey", JarUtils.getAttribute("hmcl.curseforge.apikey", ""));
private static final int WORD_PERFECT_MATCH_WEIGHT = 5;
private static <R extends HttpRequest> R withApiKey(R request) {
if (request.getUrl().startsWith(PREFIX) && !API_KEY.isEmpty()) {
request.header("X-API-KEY", API_KEY);
@@ -106,13 +103,11 @@ public final class CurseForgeRemoteAddonRepository implements RemoteAddonReposit
private static int toModsSearchSortField(SortType sort) {
// https://docs.curseforge.com/rest-api/#tocS_ModsSearchSortField
return switch (sort) {
case DATE_CREATED -> 1;
case RELEVANCY -> 1; // This represents Featured, however test shows that they are quite similar
case POPULARITY -> 2;
case DATE_CREATED -> 11;
case LAST_UPDATED -> 3;
case NAME -> 4;
case AUTHOR -> 5;
case TOTAL_DOWNLOADS -> 6;
default -> 8;
};
}
@@ -128,6 +123,11 @@ public final class CurseForgeRemoteAddonRepository implements RemoteAddonReposit
return (int) Math.ceil((double) Math.min(response.pagination.totalCount, 10000) / pageSize);
}
/// Searches the configured section, preserving the order returned by the server.
///
/// @throws UnsupportedOperationException if this repository has no configured section
/// @throws NoCandidatesException if no response is obtained and no I/O failure was recorded
/// @throws IOException if all candidate requests fail with I/O errors
@Override
public SearchResult search(DownloadProvider downloadProvider, String gameVersion, @Nullable RemoteAddonRepository.Category category, int pageOffset, int pageSize, String searchFilter, SortType sortType, SortOrder sortOrder) throws IOException {
if (type == null) throw new UnsupportedOperationException();
@@ -150,18 +150,15 @@ public final class CurseForgeRemoteAddonRepository implements RemoteAddonReposit
query.put("index", Integer.toString(pageOffset * pageSize));
query.put("pageSize", Integer.toString(pageSize));
Response<List<CurseAddon>> response = null;
@Nullable Response<List<CurseAddon>> response = null;
IOException exception = null;
@Nullable IOException exception = null;
List<URI> candidates = downloadProvider.injectURLWithCandidates(NetworkUtils.withQuery(PREFIX + "/v1/mods/search", query));
for (URI candidate : candidates) {
LOG.info("Fetching " + candidate);
try {
response = withApiKey(HttpRequest.GET(candidate.toString()))
.getJson(Response.typeOf(listTypeOf(CurseAddon.class)));
if (searchFilter.isEmpty()) {
return new SearchResult(response.data().stream().map(CurseAddon::toAddon), calculateTotalPages(response, pageSize));
}
break;
} catch (IOException e) {
LOG.warning("Failed to search addons: " + candidate, e);
@@ -176,31 +173,10 @@ public final class CurseForgeRemoteAddonRepository implements RemoteAddonReposit
}
}
if (response == null) {
throw exception != null ? exception : new NoCandidatesException();
}
if (response != null)
return new SearchResult(response.data().stream().map(CurseAddon::toAddon), calculateTotalPages(response, pageSize));
// https://github.com/HMCL-dev/HMCL/issues/1549
String lowerCaseSearchFilter = searchFilter.toLowerCase(Locale.ROOT);
Map<String, Integer> searchFilterWords = new HashMap<>();
for (String s : StringUtils.tokenize(lowerCaseSearchFilter)) {
searchFilterWords.put(s, searchFilterWords.getOrDefault(s, 0) + 1);
}
StringUtils.LevCalculator levCalculator = new StringUtils.LevCalculator();
return new SearchResult(response.data().stream().map(CurseAddon::toAddon).map(remoteAddon -> {
String lowerCaseResult = remoteAddon.title().toLowerCase(Locale.ROOT);
int diff = levCalculator.calc(lowerCaseSearchFilter, lowerCaseResult);
for (String s : StringUtils.tokenize(lowerCaseResult)) {
if (searchFilterWords.containsKey(s)) {
diff -= WORD_PERFECT_MATCH_WEIGHT * searchFilterWords.get(s) * s.length();
}
}
return pair(remoteAddon, diff);
}).sorted(Comparator.comparingInt(Pair::getValue)).map(Pair::getKey), response.data().stream().map(CurseAddon::toAddon), calculateTotalPages(response, pageSize));
throw exception != null ? exception : new NoCandidatesException();
} finally {
SEMAPHORE.release();
}
@@ -146,11 +146,11 @@ public final class ModrinthRemoteAddonRepository implements RemoteAddonRepositor
private static String convertSortType(SortType sortType) {
return switch (sortType) {
case RELEVANCY -> "relevance";
case POPULARITY -> "follows";
case DATE_CREATED -> "newest";
case POPULARITY, NAME, AUTHOR -> "relevance";
case LAST_UPDATED -> "updated";
case TOTAL_DOWNLOADS -> "downloads";
default -> throw new IllegalArgumentException("Unsupported sort type " + sortType);
};
}