mirror of
https://github.com/MarginaliaSearch/MarginaliaSearch.git
synced 2025-02-23 13:09:00 +00:00
(search-service) Add pagination support to the search GUI
This commit is contained in:
parent
73f973cc06
commit
4a0356e26f
@ -99,7 +99,8 @@ public class QueryProtobufCodec {
|
||||
IndexProtobufCodec.convertQueryLimits(request.getQueryLimits()),
|
||||
request.getSearchSetIdentifier(),
|
||||
QueryStrategy.valueOf(request.getQueryStrategy()),
|
||||
ResultRankingParameters.TemporalBias.valueOf(request.getTemporalBias().getBias().name())
|
||||
ResultRankingParameters.TemporalBias.valueOf(request.getTemporalBias().getBias().name()),
|
||||
request.getPagination().getPage()
|
||||
);
|
||||
}
|
||||
|
||||
@ -107,14 +108,22 @@ public class QueryProtobufCodec {
|
||||
public static QueryResponse convertQueryResponse(RpcQsResponse query) {
|
||||
var results = new ArrayList<DecoratedSearchResultItem>(query.getResultsCount());
|
||||
|
||||
for (int i = 0; i < query.getResultsCount(); i++)
|
||||
for (int i = 0; i < query.getResultsCount(); i++) {
|
||||
results.add(convertDecoratedResult(query.getResults(i)));
|
||||
}
|
||||
|
||||
var requestPagination = query.getPagination();
|
||||
int totalResults = requestPagination.getTotalResults();
|
||||
int pageSize = requestPagination.getPageSize();
|
||||
int totalPages = (totalResults + pageSize - 1) / pageSize;
|
||||
|
||||
return new QueryResponse(
|
||||
convertSearchSpecification(query.getSpecs()),
|
||||
results,
|
||||
query.getSearchTermsHumanList(),
|
||||
query.getProblemsList(),
|
||||
query.getPagination().getPage(),
|
||||
totalPages,
|
||||
query.getDomain()
|
||||
);
|
||||
}
|
||||
@ -304,6 +313,10 @@ public class QueryProtobufCodec {
|
||||
.setQueryStrategy(params.queryStrategy().name())
|
||||
.setTemporalBias(RpcTemporalBias.newBuilder()
|
||||
.setBias(RpcTemporalBias.Bias.valueOf(params.temporalBias().name()))
|
||||
.build())
|
||||
.setPagination(RpcQsQueryPagination.newBuilder()
|
||||
.setPage(params.page())
|
||||
.setPageSize(Math.min(100, params.limits().resultsTotal()))
|
||||
.build());
|
||||
|
||||
if (params.nearDomain() != null)
|
||||
|
@ -4,6 +4,7 @@ import nu.marginalia.api.searchquery.model.results.ResultRankingParameters;
|
||||
import nu.marginalia.index.query.limit.QueryLimits;
|
||||
import nu.marginalia.index.query.limit.QueryStrategy;
|
||||
import nu.marginalia.index.query.limit.SpecificationLimit;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
import java.util.List;
|
||||
|
||||
@ -23,7 +24,8 @@ public record QueryParams(
|
||||
QueryLimits limits,
|
||||
String identifier,
|
||||
QueryStrategy queryStrategy,
|
||||
ResultRankingParameters.TemporalBias temporalBias
|
||||
ResultRankingParameters.TemporalBias temporalBias,
|
||||
int page
|
||||
)
|
||||
{
|
||||
public QueryParams(String query, QueryLimits limits, String identifier) {
|
||||
@ -40,7 +42,8 @@ public record QueryParams(
|
||||
limits,
|
||||
identifier,
|
||||
QueryStrategy.AUTO,
|
||||
ResultRankingParameters.TemporalBias.NONE
|
||||
ResultRankingParameters.TemporalBias.NONE,
|
||||
1 // page
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -11,6 +11,8 @@ public record QueryResponse(SearchSpecification specs,
|
||||
List<DecoratedSearchResultItem> results,
|
||||
List<String> searchTermsHuman,
|
||||
List<String> problems,
|
||||
int currentPage,
|
||||
int totalPages,
|
||||
@Nullable String domain)
|
||||
{
|
||||
public Set<String> getAllKeywords() {
|
||||
|
@ -49,6 +49,7 @@ public class QueryGRPCService extends QueryApiGrpc.QueryApiImplBase {
|
||||
.labels(Integer.toString(request.getQueryLimits().getTimeoutMs()),
|
||||
Integer.toString(request.getQueryLimits().getResultsTotal()))
|
||||
.time(() -> {
|
||||
|
||||
var params = QueryProtobufCodec.convertRequest(request);
|
||||
var query = queryFactory.createQuery(params, ResultRankingParameters.sensibleDefaults());
|
||||
|
||||
@ -68,7 +69,8 @@ public class QueryGRPCService extends QueryApiGrpc.QueryApiImplBase {
|
||||
.addAllResults(response.results())
|
||||
.setPagination(
|
||||
RpcQsResultPagination.newBuilder()
|
||||
.setPage(response.page())
|
||||
.setPage(requestPagination.getPage())
|
||||
.setPageSize(requestPagination.getPageSize())
|
||||
.setTotalResults(response.totalResults())
|
||||
)
|
||||
.setSpecs(indexRequest)
|
||||
|
@ -37,6 +37,7 @@ import java.util.Set;
|
||||
import java.util.concurrent.Future;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.IntStream;
|
||||
|
||||
@Singleton
|
||||
public class SearchOperator {
|
||||
@ -132,6 +133,14 @@ public class SearchOperator {
|
||||
|
||||
List<String> problems = getProblems(evalResult, queryResults, queryResponse);
|
||||
|
||||
List<DecoratedSearchResults.Page> resultPages = IntStream.rangeClosed(1, queryResponse.totalPages())
|
||||
.mapToObj(number -> new DecoratedSearchResults.Page(
|
||||
number,
|
||||
number == userParams.page(),
|
||||
userParams.withPage(number).renderUrl(websiteUrl)
|
||||
))
|
||||
.toList();
|
||||
|
||||
// Return the results to the user
|
||||
return DecoratedSearchResults.builder()
|
||||
.params(userParams)
|
||||
@ -141,6 +150,7 @@ public class SearchOperator {
|
||||
.filters(new SearchFilters(websiteUrl, userParams))
|
||||
.focusDomain(focusDomain)
|
||||
.focusDomainId(focusDomainId)
|
||||
.resultPages(resultPages)
|
||||
.build();
|
||||
}
|
||||
|
||||
|
@ -36,7 +36,8 @@ public class SearchQueryParamFactory {
|
||||
new QueryLimits(5, 100, 200, 8192),
|
||||
profile.searchSetIdentifier.name(),
|
||||
userParams.strategy(),
|
||||
userParams.temporalBias()
|
||||
userParams.temporalBias(),
|
||||
userParams.page()
|
||||
);
|
||||
|
||||
}
|
||||
@ -56,7 +57,8 @@ public class SearchQueryParamFactory {
|
||||
new QueryLimits(count, count, 100, 512),
|
||||
SearchSetIdentifier.NONE.name(),
|
||||
QueryStrategy.AUTO,
|
||||
ResultRankingParameters.TemporalBias.NONE
|
||||
ResultRankingParameters.TemporalBias.NONE,
|
||||
1
|
||||
);
|
||||
}
|
||||
|
||||
@ -75,7 +77,8 @@ public class SearchQueryParamFactory {
|
||||
new QueryLimits(100, 100, 100, 512),
|
||||
SearchSetIdentifier.NONE.name(),
|
||||
QueryStrategy.AUTO,
|
||||
ResultRankingParameters.TemporalBias.NONE
|
||||
ResultRankingParameters.TemporalBias.NONE,
|
||||
1
|
||||
);
|
||||
}
|
||||
|
||||
@ -94,7 +97,8 @@ public class SearchQueryParamFactory {
|
||||
new QueryLimits(100, 100, 100, 512),
|
||||
SearchSetIdentifier.NONE.name(),
|
||||
QueryStrategy.AUTO,
|
||||
ResultRankingParameters.TemporalBias.NONE
|
||||
ResultRankingParameters.TemporalBias.NONE,
|
||||
1
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -5,9 +5,11 @@ import nu.marginalia.api.searchquery.model.results.ResultRankingParameters;
|
||||
import nu.marginalia.index.query.limit.QueryStrategy;
|
||||
import nu.marginalia.index.query.limit.SpecificationLimit;
|
||||
import nu.marginalia.search.model.SearchProfile;
|
||||
import spark.Request;
|
||||
|
||||
import java.net.URLEncoder;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Objects;
|
||||
|
||||
import static nu.marginalia.search.command.SearchRecentParameter.RECENT;
|
||||
|
||||
@ -17,40 +19,60 @@ public record SearchParameters(String query,
|
||||
SearchRecentParameter recent,
|
||||
SearchTitleParameter searchTitle,
|
||||
SearchAdtechParameter adtech,
|
||||
boolean poisonResults,
|
||||
boolean newFilter
|
||||
boolean newFilter,
|
||||
int page
|
||||
) {
|
||||
|
||||
public SearchParameters(String queryString, Request request) {
|
||||
this(
|
||||
queryString,
|
||||
SearchProfile.getSearchProfile(request.queryParams("profile")),
|
||||
SearchJsParameter.parse(request.queryParams("js")),
|
||||
SearchRecentParameter.parse(request.queryParams("recent")),
|
||||
SearchTitleParameter.parse(request.queryParams("searchTitle")),
|
||||
SearchAdtechParameter.parse(request.queryParams("adtech")),
|
||||
"true".equals(request.queryParams("newfilter")),
|
||||
Integer.parseInt(Objects.requireNonNullElse(request.queryParams("page"), "1"))
|
||||
);
|
||||
}
|
||||
|
||||
public String profileStr() {
|
||||
return profile.filterId;
|
||||
}
|
||||
|
||||
public SearchParameters withProfile(SearchProfile profile) {
|
||||
return new SearchParameters(query, profile, js, recent, searchTitle, adtech, poisonResults, true);
|
||||
return new SearchParameters(query, profile, js, recent, searchTitle, adtech, true, page);
|
||||
}
|
||||
|
||||
public SearchParameters withJs(SearchJsParameter js) {
|
||||
return new SearchParameters(query, profile, js, recent, searchTitle, adtech, poisonResults, true);
|
||||
return new SearchParameters(query, profile, js, recent, searchTitle, adtech, true, page);
|
||||
}
|
||||
public SearchParameters withAdtech(SearchAdtechParameter adtech) {
|
||||
return new SearchParameters(query, profile, js, recent, searchTitle, adtech, poisonResults, true);
|
||||
return new SearchParameters(query, profile, js, recent, searchTitle, adtech, true, page);
|
||||
}
|
||||
|
||||
public SearchParameters withRecent(SearchRecentParameter recent) {
|
||||
return new SearchParameters(query, profile, js, recent, searchTitle, adtech, poisonResults, true);
|
||||
return new SearchParameters(query, profile, js, recent, searchTitle, adtech, true, page);
|
||||
}
|
||||
|
||||
public SearchParameters withTitle(SearchTitleParameter title) {
|
||||
return new SearchParameters(query, profile, js, recent, title, adtech, poisonResults, true);
|
||||
return new SearchParameters(query, profile, js, recent, title, adtech, true, page);
|
||||
}
|
||||
|
||||
public SearchParameters withPage(int page) {
|
||||
return new SearchParameters(query, profile, js, recent, searchTitle, adtech, false, page);
|
||||
}
|
||||
|
||||
public String renderUrl(WebsiteUrl baseUrl) {
|
||||
String path = String.format("/search?query=%s&profile=%s&js=%s&adtech=%s&recent=%s&searchTitle=%s&newfilter=true",
|
||||
String path = String.format("/search?query=%s&profile=%s&js=%s&adtech=%s&recent=%s&searchTitle=%s&newfilter=%s&page=%d",
|
||||
URLEncoder.encode(query, StandardCharsets.UTF_8),
|
||||
URLEncoder.encode(profile.filterId, StandardCharsets.UTF_8),
|
||||
URLEncoder.encode(js.value, StandardCharsets.UTF_8),
|
||||
URLEncoder.encode(adtech.value, StandardCharsets.UTF_8),
|
||||
URLEncoder.encode(recent.value, StandardCharsets.UTF_8),
|
||||
URLEncoder.encode(searchTitle.value, StandardCharsets.UTF_8)
|
||||
URLEncoder.encode(searchTitle.value, StandardCharsets.UTF_8),
|
||||
Boolean.valueOf(newFilter).toString(),
|
||||
page
|
||||
);
|
||||
|
||||
return baseUrl.withPath(path);
|
||||
|
@ -25,6 +25,10 @@ public class DecoratedSearchResults {
|
||||
private final int focusDomainId;
|
||||
private final SearchFilters filters;
|
||||
|
||||
private final List<Page> resultPages;
|
||||
|
||||
public record Page(int number, boolean current, String href) {}
|
||||
|
||||
// These are used by the search form, they look unused in the IDE but are used by the mustache template,
|
||||
// DO NOT REMOVE THEM
|
||||
public int getResultCount() { return results.size(); }
|
||||
@ -34,5 +38,7 @@ public class DecoratedSearchResults {
|
||||
public String getAdtech() { return params.adtech().value; }
|
||||
public String getRecent() { return params.recent().value; }
|
||||
public String getSearchTitle() { return params.searchTitle().value; }
|
||||
public int page() { return params.page(); }
|
||||
public Boolean isNewFilter() { return params.newFilter(); }
|
||||
|
||||
}
|
||||
|
@ -3,8 +3,8 @@ package nu.marginalia.search.svc;
|
||||
import com.google.inject.Inject;
|
||||
import lombok.SneakyThrows;
|
||||
import nu.marginalia.WebsiteUrl;
|
||||
import nu.marginalia.search.command.*;
|
||||
import nu.marginalia.search.model.SearchProfile;
|
||||
import nu.marginalia.search.command.CommandEvaluator;
|
||||
import nu.marginalia.search.command.SearchParameters;
|
||||
import nu.marginalia.search.exceptions.RedirectException;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
@ -52,15 +52,7 @@ public class SearchQueryService {
|
||||
throw new RedirectException(websiteUrl.url());
|
||||
}
|
||||
|
||||
return new SearchParameters(queryParam.trim(),
|
||||
SearchProfile.getSearchProfile(request.queryParams("profile")),
|
||||
SearchJsParameter.parse(request.queryParams("js")),
|
||||
SearchRecentParameter.parse(request.queryParams("recent")),
|
||||
SearchTitleParameter.parse(request.queryParams("searchTitle")),
|
||||
SearchAdtechParameter.parse(request.queryParams("adtech")),
|
||||
"1".equals(request.headers("X-Poison-Results")),
|
||||
"true".equals(request.queryParams("newfilter"))
|
||||
);
|
||||
return new SearchParameters(queryParam.trim(), request);
|
||||
}
|
||||
catch (Exception ex) {
|
||||
// Bots keep sending bad requests, suppress the error otherwise it will
|
||||
|
@ -794,6 +794,25 @@ footer {
|
||||
}
|
||||
}
|
||||
|
||||
.page-link {
|
||||
padding-top: 0.25ch;
|
||||
padding-bottom: 0.25ch;
|
||||
padding-left: 0.5ch;
|
||||
padding-right: 0.5ch;
|
||||
margin-right: 0.5ch;
|
||||
|
||||
font-size: 12pt;
|
||||
border: 1px solid var(--clr-border);
|
||||
background-color: var(--clr-bg-highlight);
|
||||
color: var(--clr-text-ui) !important;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.page-link.active {
|
||||
border: 1px solid var(--clr-text-ui);
|
||||
background-color: var(--clr-bg-ui);
|
||||
}
|
||||
|
||||
// The search results page is very confusing on text-based browsers, so we add a hr to separate the search results. This is
|
||||
// hidden on modern browsers via CSS.
|
||||
|
||||
|
@ -53,6 +53,12 @@
|
||||
{{/with}}
|
||||
{{/if}}
|
||||
{{/each}}
|
||||
|
||||
<nav aria-label="pagination">
|
||||
{{#each resultPages}}
|
||||
<a {{#unless current}}href="{{{href}}}"{{/unless}} class="page-link {{#if current}}active{{/if}}">{{number}}</a>
|
||||
{{/each}}
|
||||
</nav>
|
||||
</section>
|
||||
|
||||
{{#with filters}}
|
||||
|
Loading…
Reference in New Issue
Block a user