mirror of
https://github.com/MarginaliaSearch/MarginaliaSearch.git
synced 2025-02-23 21:18:58 +00:00
Merge pull request #119 from MarginaliaSearch/result-pagination
Add pagination support for the search results
This commit is contained in:
commit
9224176202
@ -99,7 +99,8 @@ public class QueryProtobufCodec {
|
|||||||
IndexProtobufCodec.convertQueryLimits(request.getQueryLimits()),
|
IndexProtobufCodec.convertQueryLimits(request.getQueryLimits()),
|
||||||
request.getSearchSetIdentifier(),
|
request.getSearchSetIdentifier(),
|
||||||
QueryStrategy.valueOf(request.getQueryStrategy()),
|
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) {
|
public static QueryResponse convertQueryResponse(RpcQsResponse query) {
|
||||||
var results = new ArrayList<DecoratedSearchResultItem>(query.getResultsCount());
|
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)));
|
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(
|
return new QueryResponse(
|
||||||
convertSearchSpecification(query.getSpecs()),
|
convertSearchSpecification(query.getSpecs()),
|
||||||
results,
|
results,
|
||||||
query.getSearchTermsHumanList(),
|
query.getSearchTermsHumanList(),
|
||||||
query.getProblemsList(),
|
query.getProblemsList(),
|
||||||
|
query.getPagination().getPage(),
|
||||||
|
totalPages,
|
||||||
query.getDomain()
|
query.getDomain()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -304,6 +313,10 @@ public class QueryProtobufCodec {
|
|||||||
.setQueryStrategy(params.queryStrategy().name())
|
.setQueryStrategy(params.queryStrategy().name())
|
||||||
.setTemporalBias(RpcTemporalBias.newBuilder()
|
.setTemporalBias(RpcTemporalBias.newBuilder()
|
||||||
.setBias(RpcTemporalBias.Bias.valueOf(params.temporalBias().name()))
|
.setBias(RpcTemporalBias.Bias.valueOf(params.temporalBias().name()))
|
||||||
|
.build())
|
||||||
|
.setPagination(RpcQsQueryPagination.newBuilder()
|
||||||
|
.setPage(params.page())
|
||||||
|
.setPageSize(Math.min(100, params.limits().resultsTotal()))
|
||||||
.build());
|
.build());
|
||||||
|
|
||||||
if (params.nearDomain() != null)
|
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.QueryLimits;
|
||||||
import nu.marginalia.index.query.limit.QueryStrategy;
|
import nu.marginalia.index.query.limit.QueryStrategy;
|
||||||
import nu.marginalia.index.query.limit.SpecificationLimit;
|
import nu.marginalia.index.query.limit.SpecificationLimit;
|
||||||
|
|
||||||
import javax.annotation.Nullable;
|
import javax.annotation.Nullable;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
@ -23,7 +24,8 @@ public record QueryParams(
|
|||||||
QueryLimits limits,
|
QueryLimits limits,
|
||||||
String identifier,
|
String identifier,
|
||||||
QueryStrategy queryStrategy,
|
QueryStrategy queryStrategy,
|
||||||
ResultRankingParameters.TemporalBias temporalBias
|
ResultRankingParameters.TemporalBias temporalBias,
|
||||||
|
int page
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
public QueryParams(String query, QueryLimits limits, String identifier) {
|
public QueryParams(String query, QueryLimits limits, String identifier) {
|
||||||
@ -40,7 +42,8 @@ public record QueryParams(
|
|||||||
limits,
|
limits,
|
||||||
identifier,
|
identifier,
|
||||||
QueryStrategy.AUTO,
|
QueryStrategy.AUTO,
|
||||||
ResultRankingParameters.TemporalBias.NONE
|
ResultRankingParameters.TemporalBias.NONE,
|
||||||
|
1 // page
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -11,6 +11,8 @@ public record QueryResponse(SearchSpecification specs,
|
|||||||
List<DecoratedSearchResultItem> results,
|
List<DecoratedSearchResultItem> results,
|
||||||
List<String> searchTermsHuman,
|
List<String> searchTermsHuman,
|
||||||
List<String> problems,
|
List<String> problems,
|
||||||
|
int currentPage,
|
||||||
|
int totalPages,
|
||||||
@Nullable String domain)
|
@Nullable String domain)
|
||||||
{
|
{
|
||||||
public Set<String> getAllKeywords() {
|
public Set<String> getAllKeywords() {
|
||||||
|
@ -30,6 +30,8 @@ message RpcQsQuery {
|
|||||||
string searchSetIdentifier = 14;
|
string searchSetIdentifier = 14;
|
||||||
string queryStrategy = 15; // Named query configuration
|
string queryStrategy = 15; // Named query configuration
|
||||||
RpcTemporalBias temporalBias = 16;
|
RpcTemporalBias temporalBias = 16;
|
||||||
|
|
||||||
|
RpcQsQueryPagination pagination = 17;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Query service query response */
|
/* Query service query response */
|
||||||
@ -39,6 +41,19 @@ message RpcQsResponse {
|
|||||||
repeated string searchTermsHuman = 3;
|
repeated string searchTermsHuman = 3;
|
||||||
repeated string problems = 4;
|
repeated string problems = 4;
|
||||||
string domain = 5;
|
string domain = 5;
|
||||||
|
|
||||||
|
RpcQsResultPagination pagination = 6;
|
||||||
|
}
|
||||||
|
|
||||||
|
message RpcQsQueryPagination {
|
||||||
|
int32 page = 1;
|
||||||
|
int32 pageSize = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
message RpcQsResultPagination {
|
||||||
|
int32 page = 1;
|
||||||
|
int32 pageSize = 2;
|
||||||
|
int32 totalResults = 3;
|
||||||
}
|
}
|
||||||
|
|
||||||
message RpcTemporalBias {
|
message RpcTemporalBias {
|
||||||
|
@ -8,13 +8,13 @@ import io.prometheus.client.Histogram;
|
|||||||
import nu.marginalia.api.searchquery.*;
|
import nu.marginalia.api.searchquery.*;
|
||||||
import nu.marginalia.api.searchquery.model.query.ProcessedQuery;
|
import nu.marginalia.api.searchquery.model.query.ProcessedQuery;
|
||||||
import nu.marginalia.api.searchquery.model.query.QueryParams;
|
import nu.marginalia.api.searchquery.model.query.QueryParams;
|
||||||
|
import nu.marginalia.api.searchquery.model.results.DecoratedSearchResultItem;
|
||||||
import nu.marginalia.api.searchquery.model.results.ResultRankingParameters;
|
import nu.marginalia.api.searchquery.model.results.ResultRankingParameters;
|
||||||
import nu.marginalia.index.api.IndexClient;
|
import nu.marginalia.index.api.IndexClient;
|
||||||
import nu.marginalia.api.searchquery.model.results.DecoratedSearchResultItem;
|
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
import java.util.*;
|
import java.util.List;
|
||||||
|
|
||||||
@Singleton
|
@Singleton
|
||||||
public class QueryGRPCService extends QueryApiGrpc.QueryApiImplBase {
|
public class QueryGRPCService extends QueryApiGrpc.QueryApiImplBase {
|
||||||
@ -49,17 +49,30 @@ public class QueryGRPCService extends QueryApiGrpc.QueryApiImplBase {
|
|||||||
.labels(Integer.toString(request.getQueryLimits().getTimeoutMs()),
|
.labels(Integer.toString(request.getQueryLimits().getTimeoutMs()),
|
||||||
Integer.toString(request.getQueryLimits().getResultsTotal()))
|
Integer.toString(request.getQueryLimits().getResultsTotal()))
|
||||||
.time(() -> {
|
.time(() -> {
|
||||||
|
|
||||||
var params = QueryProtobufCodec.convertRequest(request);
|
var params = QueryProtobufCodec.convertRequest(request);
|
||||||
var query = queryFactory.createQuery(params, ResultRankingParameters.sensibleDefaults());
|
var query = queryFactory.createQuery(params, ResultRankingParameters.sensibleDefaults());
|
||||||
|
|
||||||
var indexRequest = QueryProtobufCodec.convertQuery(request, query);
|
var indexRequest = QueryProtobufCodec.convertQuery(request, query);
|
||||||
|
|
||||||
|
var requestPagination = request.getPagination();
|
||||||
|
|
||||||
|
IndexClient.Pagination pagination = new IndexClient.Pagination(
|
||||||
|
requestPagination.getPage(),
|
||||||
|
requestPagination.getPageSize());
|
||||||
|
|
||||||
// Execute the query on the index partitions
|
// Execute the query on the index partitions
|
||||||
List<RpcDecoratedResultItem> bestItems = indexClient.executeQueries(indexRequest);
|
IndexClient.AggregateQueryResponse response = indexClient.executeQueries(indexRequest, pagination);
|
||||||
|
|
||||||
// Convert results to response and send it back
|
// Convert results to response and send it back
|
||||||
var responseBuilder = RpcQsResponse.newBuilder()
|
var responseBuilder = RpcQsResponse.newBuilder()
|
||||||
.addAllResults(bestItems)
|
.addAllResults(response.results())
|
||||||
|
.setPagination(
|
||||||
|
RpcQsResultPagination.newBuilder()
|
||||||
|
.setPage(requestPagination.getPage())
|
||||||
|
.setPageSize(requestPagination.getPageSize())
|
||||||
|
.setTotalResults(response.totalResults())
|
||||||
|
)
|
||||||
.setSpecs(indexRequest)
|
.setSpecs(indexRequest)
|
||||||
.addAllSearchTermsHuman(query.searchTermsHuman);
|
.addAllSearchTermsHuman(query.searchTermsHuman);
|
||||||
|
|
||||||
@ -77,18 +90,22 @@ public class QueryGRPCService extends QueryApiGrpc.QueryApiImplBase {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public record DetailedDirectResult(ProcessedQuery processedQuery,
|
public record DetailedDirectResult(ProcessedQuery processedQuery,
|
||||||
List<DecoratedSearchResultItem> result) {}
|
List<DecoratedSearchResultItem> result,
|
||||||
|
int totalResults) {}
|
||||||
|
|
||||||
/** Local query execution, without GRPC. */
|
/** Local query execution, without GRPC. */
|
||||||
public DetailedDirectResult executeDirect(
|
public DetailedDirectResult executeDirect(
|
||||||
String originalQuery,
|
String originalQuery,
|
||||||
QueryParams params,
|
QueryParams params,
|
||||||
|
IndexClient.Pagination pagination,
|
||||||
ResultRankingParameters rankingParameters) {
|
ResultRankingParameters rankingParameters) {
|
||||||
|
|
||||||
var query = queryFactory.createQuery(params, rankingParameters);
|
var query = queryFactory.createQuery(params, rankingParameters);
|
||||||
var items = indexClient.executeQueries(QueryProtobufCodec.convertQuery(originalQuery, query));
|
IndexClient.AggregateQueryResponse response = indexClient.executeQueries(QueryProtobufCodec.convertQuery(originalQuery, query), pagination);
|
||||||
|
|
||||||
return new DetailedDirectResult(query, Lists.transform(items, QueryProtobufCodec::convertQueryResult));
|
return new DetailedDirectResult(query,
|
||||||
|
Lists.transform(response.results(), QueryProtobufCodec::convertQueryResult),
|
||||||
|
response.totalResults());
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -17,10 +17,14 @@ import org.slf4j.LoggerFactory;
|
|||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Comparator;
|
import java.util.Comparator;
|
||||||
|
import java.util.Iterator;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.concurrent.CompletableFuture;
|
||||||
import java.util.concurrent.ExecutorService;
|
import java.util.concurrent.ExecutorService;
|
||||||
import java.util.concurrent.Executors;
|
import java.util.concurrent.Executors;
|
||||||
|
|
||||||
|
import static java.lang.Math.clamp;
|
||||||
|
|
||||||
@Singleton
|
@Singleton
|
||||||
public class IndexClient {
|
public class IndexClient {
|
||||||
private static final Logger logger = LoggerFactory.getLogger(IndexClient.class);
|
private static final Logger logger = LoggerFactory.getLogger(IndexClient.class);
|
||||||
@ -39,17 +43,23 @@ public class IndexClient {
|
|||||||
private static final Comparator<RpcDecoratedResultItem> comparator =
|
private static final Comparator<RpcDecoratedResultItem> comparator =
|
||||||
Comparator.comparing(RpcDecoratedResultItem::getRankingScore);
|
Comparator.comparing(RpcDecoratedResultItem::getRankingScore);
|
||||||
|
|
||||||
|
public record Pagination(int page, int pageSize) {}
|
||||||
|
|
||||||
|
public record AggregateQueryResponse(List<RpcDecoratedResultItem> results,
|
||||||
|
int page,
|
||||||
|
int totalResults
|
||||||
|
) {}
|
||||||
|
|
||||||
/** Execute a query on the index partitions and return the combined results. */
|
/** Execute a query on the index partitions and return the combined results. */
|
||||||
@SneakyThrows
|
@SneakyThrows
|
||||||
public List<RpcDecoratedResultItem> executeQueries(RpcIndexQuery indexRequest) {
|
public AggregateQueryResponse executeQueries(RpcIndexQuery indexRequest, Pagination pagination) {
|
||||||
var futures =
|
List<CompletableFuture<Iterator<RpcDecoratedResultItem>>> futures =
|
||||||
channelPool.call(IndexApiGrpc.IndexApiBlockingStub::query)
|
channelPool.call(IndexApiGrpc.IndexApiBlockingStub::query)
|
||||||
.async(executor)
|
.async(executor)
|
||||||
.runEach(indexRequest);
|
.runEach(indexRequest);
|
||||||
|
|
||||||
final int resultsTotal = indexRequest.getQueryLimits().getResultsTotal();
|
final int requestedMaxResults = indexRequest.getQueryLimits().getResultsTotal();
|
||||||
final int resultsUpperBound = resultsTotal * channelPool.getNumNodes();
|
final int resultsUpperBound = requestedMaxResults * channelPool.getNumNodes();
|
||||||
|
|
||||||
List<RpcDecoratedResultItem> results = new ArrayList<>(resultsUpperBound);
|
List<RpcDecoratedResultItem> results = new ArrayList<>(resultsUpperBound);
|
||||||
|
|
||||||
@ -66,12 +76,17 @@ public class IndexClient {
|
|||||||
results.sort(comparator);
|
results.sort(comparator);
|
||||||
results.removeIf(this::isBlacklisted);
|
results.removeIf(this::isBlacklisted);
|
||||||
|
|
||||||
// Keep only as many results as were requested
|
int numReceivedResults = results.size();
|
||||||
if (results.size() > resultsTotal) {
|
|
||||||
results = results.subList(0, resultsTotal);
|
|
||||||
}
|
|
||||||
|
|
||||||
return results;
|
// pagination is typically 1-indexed, so we need to adjust the start and end indices
|
||||||
|
int indexStart = (pagination.page - 1) * pagination.pageSize;
|
||||||
|
int indexEnd = (pagination.page) * pagination.pageSize;
|
||||||
|
|
||||||
|
results = results.subList(
|
||||||
|
clamp(indexStart, 0, results.size() - 1), // from is inclusive, so subtract 1 from size()
|
||||||
|
clamp(indexEnd, 0, results.size()));
|
||||||
|
|
||||||
|
return new AggregateQueryResponse(results, pagination.page(), numReceivedResults);
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean isBlacklisted(RpcDecoratedResultItem item) {
|
private boolean isBlacklisted(RpcDecoratedResultItem item) {
|
||||||
|
@ -37,6 +37,7 @@ import java.util.Set;
|
|||||||
import java.util.concurrent.Future;
|
import java.util.concurrent.Future;
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
import java.util.stream.IntStream;
|
||||||
|
|
||||||
@Singleton
|
@Singleton
|
||||||
public class SearchOperator {
|
public class SearchOperator {
|
||||||
@ -132,6 +133,14 @@ public class SearchOperator {
|
|||||||
|
|
||||||
List<String> problems = getProblems(evalResult, queryResults, queryResponse);
|
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 the results to the user
|
||||||
return DecoratedSearchResults.builder()
|
return DecoratedSearchResults.builder()
|
||||||
.params(userParams)
|
.params(userParams)
|
||||||
@ -141,6 +150,7 @@ public class SearchOperator {
|
|||||||
.filters(new SearchFilters(websiteUrl, userParams))
|
.filters(new SearchFilters(websiteUrl, userParams))
|
||||||
.focusDomain(focusDomain)
|
.focusDomain(focusDomain)
|
||||||
.focusDomainId(focusDomainId)
|
.focusDomainId(focusDomainId)
|
||||||
|
.resultPages(resultPages)
|
||||||
.build();
|
.build();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -36,7 +36,8 @@ public class SearchQueryParamFactory {
|
|||||||
new QueryLimits(5, 100, 200, 8192),
|
new QueryLimits(5, 100, 200, 8192),
|
||||||
profile.searchSetIdentifier.name(),
|
profile.searchSetIdentifier.name(),
|
||||||
userParams.strategy(),
|
userParams.strategy(),
|
||||||
userParams.temporalBias()
|
userParams.temporalBias(),
|
||||||
|
userParams.page()
|
||||||
);
|
);
|
||||||
|
|
||||||
}
|
}
|
||||||
@ -56,7 +57,8 @@ public class SearchQueryParamFactory {
|
|||||||
new QueryLimits(count, count, 100, 512),
|
new QueryLimits(count, count, 100, 512),
|
||||||
SearchSetIdentifier.NONE.name(),
|
SearchSetIdentifier.NONE.name(),
|
||||||
QueryStrategy.AUTO,
|
QueryStrategy.AUTO,
|
||||||
ResultRankingParameters.TemporalBias.NONE
|
ResultRankingParameters.TemporalBias.NONE,
|
||||||
|
1
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -75,7 +77,8 @@ public class SearchQueryParamFactory {
|
|||||||
new QueryLimits(100, 100, 100, 512),
|
new QueryLimits(100, 100, 100, 512),
|
||||||
SearchSetIdentifier.NONE.name(),
|
SearchSetIdentifier.NONE.name(),
|
||||||
QueryStrategy.AUTO,
|
QueryStrategy.AUTO,
|
||||||
ResultRankingParameters.TemporalBias.NONE
|
ResultRankingParameters.TemporalBias.NONE,
|
||||||
|
1
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -94,7 +97,8 @@ public class SearchQueryParamFactory {
|
|||||||
new QueryLimits(100, 100, 100, 512),
|
new QueryLimits(100, 100, 100, 512),
|
||||||
SearchSetIdentifier.NONE.name(),
|
SearchSetIdentifier.NONE.name(),
|
||||||
QueryStrategy.AUTO,
|
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.QueryStrategy;
|
||||||
import nu.marginalia.index.query.limit.SpecificationLimit;
|
import nu.marginalia.index.query.limit.SpecificationLimit;
|
||||||
import nu.marginalia.search.model.SearchProfile;
|
import nu.marginalia.search.model.SearchProfile;
|
||||||
|
import spark.Request;
|
||||||
|
|
||||||
import java.net.URLEncoder;
|
import java.net.URLEncoder;
|
||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
import static nu.marginalia.search.command.SearchRecentParameter.RECENT;
|
import static nu.marginalia.search.command.SearchRecentParameter.RECENT;
|
||||||
|
|
||||||
@ -17,40 +19,60 @@ public record SearchParameters(String query,
|
|||||||
SearchRecentParameter recent,
|
SearchRecentParameter recent,
|
||||||
SearchTitleParameter searchTitle,
|
SearchTitleParameter searchTitle,
|
||||||
SearchAdtechParameter adtech,
|
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() {
|
public String profileStr() {
|
||||||
return profile.filterId;
|
return profile.filterId;
|
||||||
}
|
}
|
||||||
|
|
||||||
public SearchParameters withProfile(SearchProfile profile) {
|
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) {
|
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) {
|
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) {
|
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) {
|
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) {
|
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(query, StandardCharsets.UTF_8),
|
||||||
URLEncoder.encode(profile.filterId, StandardCharsets.UTF_8),
|
URLEncoder.encode(profile.filterId, StandardCharsets.UTF_8),
|
||||||
URLEncoder.encode(js.value, StandardCharsets.UTF_8),
|
URLEncoder.encode(js.value, StandardCharsets.UTF_8),
|
||||||
URLEncoder.encode(adtech.value, StandardCharsets.UTF_8),
|
URLEncoder.encode(adtech.value, StandardCharsets.UTF_8),
|
||||||
URLEncoder.encode(recent.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);
|
return baseUrl.withPath(path);
|
||||||
|
@ -25,6 +25,10 @@ public class DecoratedSearchResults {
|
|||||||
private final int focusDomainId;
|
private final int focusDomainId;
|
||||||
private final SearchFilters filters;
|
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,
|
// These are used by the search form, they look unused in the IDE but are used by the mustache template,
|
||||||
// DO NOT REMOVE THEM
|
// DO NOT REMOVE THEM
|
||||||
public int getResultCount() { return results.size(); }
|
public int getResultCount() { return results.size(); }
|
||||||
@ -34,5 +38,7 @@ public class DecoratedSearchResults {
|
|||||||
public String getAdtech() { return params.adtech().value; }
|
public String getAdtech() { return params.adtech().value; }
|
||||||
public String getRecent() { return params.recent().value; }
|
public String getRecent() { return params.recent().value; }
|
||||||
public String getSearchTitle() { return params.searchTitle().value; }
|
public String getSearchTitle() { return params.searchTitle().value; }
|
||||||
|
public int page() { return params.page(); }
|
||||||
public Boolean isNewFilter() { return params.newFilter(); }
|
public Boolean isNewFilter() { return params.newFilter(); }
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -3,8 +3,8 @@ package nu.marginalia.search.svc;
|
|||||||
import com.google.inject.Inject;
|
import com.google.inject.Inject;
|
||||||
import lombok.SneakyThrows;
|
import lombok.SneakyThrows;
|
||||||
import nu.marginalia.WebsiteUrl;
|
import nu.marginalia.WebsiteUrl;
|
||||||
import nu.marginalia.search.command.*;
|
import nu.marginalia.search.command.CommandEvaluator;
|
||||||
import nu.marginalia.search.model.SearchProfile;
|
import nu.marginalia.search.command.SearchParameters;
|
||||||
import nu.marginalia.search.exceptions.RedirectException;
|
import nu.marginalia.search.exceptions.RedirectException;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
@ -52,15 +52,7 @@ public class SearchQueryService {
|
|||||||
throw new RedirectException(websiteUrl.url());
|
throw new RedirectException(websiteUrl.url());
|
||||||
}
|
}
|
||||||
|
|
||||||
return new SearchParameters(queryParam.trim(),
|
return new SearchParameters(queryParam.trim(), request);
|
||||||
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"))
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
catch (Exception ex) {
|
catch (Exception ex) {
|
||||||
// Bots keep sending bad requests, suppress the error otherwise it will
|
// 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
|
// 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.
|
// hidden on modern browsers via CSS.
|
||||||
|
|
||||||
|
@ -4,7 +4,7 @@
|
|||||||
Search The Internet
|
Search The Internet
|
||||||
</h1>
|
</h1>
|
||||||
<div id="suggestions-anchor"></div>
|
<div id="suggestions-anchor"></div>
|
||||||
<input autofocus type="text" id="query" name="query" placeholder="Search..." value="{{query}}">
|
<input {{#unless query}}autofocus{{/unless}} type="text" id="query" name="query" placeholder="Search..." value="{{query}}">
|
||||||
<input type="hidden" name="js" value="{{js}}">
|
<input type="hidden" name="js" value="{{js}}">
|
||||||
<input type="hidden" name="adtech" value="{{adtech}}">
|
<input type="hidden" name="adtech" value="{{adtech}}">
|
||||||
<input type="hidden" name="searchTitle" value="{{searchTitle}}">
|
<input type="hidden" name="searchTitle" value="{{searchTitle}}">
|
||||||
|
@ -53,6 +53,12 @@
|
|||||||
{{/with}}
|
{{/with}}
|
||||||
{{/if}}
|
{{/if}}
|
||||||
{{/each}}
|
{{/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>
|
</section>
|
||||||
|
|
||||||
{{#with filters}}
|
{{#with filters}}
|
||||||
|
@ -7,6 +7,7 @@ import nu.marginalia.api.searchquery.model.query.QueryParams;
|
|||||||
import nu.marginalia.api.searchquery.model.results.Bm25Parameters;
|
import nu.marginalia.api.searchquery.model.results.Bm25Parameters;
|
||||||
import nu.marginalia.api.searchquery.model.results.ResultRankingParameters;
|
import nu.marginalia.api.searchquery.model.results.ResultRankingParameters;
|
||||||
import nu.marginalia.functions.searchquery.QueryGRPCService;
|
import nu.marginalia.functions.searchquery.QueryGRPCService;
|
||||||
|
import nu.marginalia.index.api.IndexClient;
|
||||||
import nu.marginalia.index.query.limit.QueryLimits;
|
import nu.marginalia.index.query.limit.QueryLimits;
|
||||||
import nu.marginalia.model.gson.GsonFactory;
|
import nu.marginalia.model.gson.GsonFactory;
|
||||||
import nu.marginalia.renderer.MustacheRenderer;
|
import nu.marginalia.renderer.MustacheRenderer;
|
||||||
@ -15,8 +16,14 @@ import spark.Request;
|
|||||||
import spark.Response;
|
import spark.Response;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
|
import static java.lang.Integer.min;
|
||||||
|
import static java.lang.Integer.parseInt;
|
||||||
|
import static java.util.Objects.requireNonNullElse;
|
||||||
|
|
||||||
public class QueryBasicInterface {
|
public class QueryBasicInterface {
|
||||||
private final MustacheRenderer<Object> basicRenderer;
|
private final MustacheRenderer<Object> basicRenderer;
|
||||||
private final MustacheRenderer<Object> qdebugRenderer;
|
private final MustacheRenderer<Object> qdebugRenderer;
|
||||||
@ -34,38 +41,53 @@ public class QueryBasicInterface {
|
|||||||
this.queryGRPCService = queryGRPCService;
|
this.queryGRPCService = queryGRPCService;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Handle the basic search endpoint exposed in the bare-bones search interface. */
|
||||||
public Object handleBasic(Request request, Response response) {
|
public Object handleBasic(Request request, Response response) {
|
||||||
String queryParams = request.queryParams("q");
|
String queryString = request.queryParams("q");
|
||||||
if (queryParams == null) {
|
if (queryString == null) {
|
||||||
return basicRenderer.render(new Object());
|
return basicRenderer.render(new Object());
|
||||||
}
|
}
|
||||||
|
|
||||||
int count = request.queryParams("count") == null ? 10 : Integer.parseInt(request.queryParams("count"));
|
int count = parseInt(requireNonNullElse(request.queryParams("count"), "10"));
|
||||||
int domainCount = request.queryParams("domainCount") == null ? 5 : Integer.parseInt(request.queryParams("domainCount"));
|
int page = parseInt(requireNonNullElse(request.queryParams("page"), "1"));
|
||||||
String set = request.queryParams("set") == null ? "" : request.queryParams("set");
|
int domainCount = parseInt(requireNonNullElse(request.queryParams("domainCount"), "5"));
|
||||||
|
String set = requireNonNullElse(request.queryParams("set"), "");
|
||||||
|
|
||||||
var params = new QueryParams(queryParams, new QueryLimits(
|
var params = new QueryParams(queryString, new QueryLimits(
|
||||||
domainCount, count, 250, 8192
|
domainCount, min(100, count * 10), 250, 8192
|
||||||
), set);
|
), set);
|
||||||
|
|
||||||
|
var pagination = new IndexClient.Pagination(page, count);
|
||||||
|
|
||||||
var detailedDirectResult = queryGRPCService.executeDirect(
|
var detailedDirectResult = queryGRPCService.executeDirect(
|
||||||
queryParams, params, ResultRankingParameters.sensibleDefaults()
|
queryString,
|
||||||
|
params,
|
||||||
|
pagination,
|
||||||
|
ResultRankingParameters.sensibleDefaults()
|
||||||
);
|
);
|
||||||
|
|
||||||
var results = detailedDirectResult.result();
|
var results = detailedDirectResult.result();
|
||||||
|
|
||||||
|
List<PaginationInfoPage> paginationInfo = new ArrayList<>();
|
||||||
|
|
||||||
|
for (int i = 1; i <= detailedDirectResult.totalResults() / pagination.pageSize(); i++) {
|
||||||
|
paginationInfo.add(new PaginationInfoPage(i, i == pagination.page()));
|
||||||
|
}
|
||||||
|
|
||||||
if (request.headers("Accept").contains("application/json")) {
|
if (request.headers("Accept").contains("application/json")) {
|
||||||
response.type("application/json");
|
response.type("application/json");
|
||||||
return gson.toJson(results);
|
return gson.toJson(results);
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
return basicRenderer.render(
|
return basicRenderer.render(
|
||||||
Map.of("query", queryParams,
|
Map.of("query", queryString,
|
||||||
|
"pages", paginationInfo,
|
||||||
"results", results)
|
"results", results)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Handle the qdebug endpoint, which allows for query debugging and ranking parameter tuning. */
|
||||||
public Object handleAdvanced(Request request, Response response) {
|
public Object handleAdvanced(Request request, Response response) {
|
||||||
String queryString = request.queryParams("q");
|
String queryString = request.queryParams("q");
|
||||||
if (queryString == null) {
|
if (queryString == null) {
|
||||||
@ -74,18 +96,24 @@ public class QueryBasicInterface {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
int count = request.queryParams("count") == null ? 10 : Integer.parseInt(request.queryParams("count"));
|
int count = parseInt(requireNonNullElse(request.queryParams("count"), "10"));
|
||||||
int domainCount = request.queryParams("domainCount") == null ? 5 : Integer.parseInt(request.queryParams("domainCount"));
|
int page = parseInt(requireNonNullElse(request.queryParams("page"), "1"));
|
||||||
String set = request.queryParams("set") == null ? "" : request.queryParams("set");
|
int domainCount = parseInt(requireNonNullElse(request.queryParams("domainCount"), "5"));
|
||||||
|
String set = requireNonNullElse(request.queryParams("set"), "");
|
||||||
|
|
||||||
var queryParams = new QueryParams(queryString, new QueryLimits(
|
var queryParams = new QueryParams(queryString, new QueryLimits(
|
||||||
domainCount, count, 250, 8192
|
domainCount, min(100, count * 10), 250, 8192
|
||||||
), set);
|
), set);
|
||||||
|
|
||||||
|
var pagination = new IndexClient.Pagination(page, count);
|
||||||
|
|
||||||
var rankingParams = debugRankingParamsFromRequest(request);
|
var rankingParams = debugRankingParamsFromRequest(request);
|
||||||
|
|
||||||
var detailedDirectResult = queryGRPCService.executeDirect(
|
var detailedDirectResult = queryGRPCService.executeDirect(
|
||||||
queryString, queryParams, rankingParams
|
queryString,
|
||||||
|
queryParams,
|
||||||
|
pagination,
|
||||||
|
rankingParams
|
||||||
);
|
);
|
||||||
|
|
||||||
var results = detailedDirectResult.result();
|
var results = detailedDirectResult.result();
|
||||||
@ -127,10 +155,12 @@ public class QueryBasicInterface {
|
|||||||
}
|
}
|
||||||
|
|
||||||
int intFromRequest(Request request, String param, int defaultValue) {
|
int intFromRequest(Request request, String param, int defaultValue) {
|
||||||
return Strings.isNullOrEmpty(request.queryParams(param)) ? defaultValue : Integer.parseInt(request.queryParams(param));
|
return Strings.isNullOrEmpty(request.queryParams(param)) ? defaultValue : parseInt(request.queryParams(param));
|
||||||
}
|
}
|
||||||
|
|
||||||
String stringFromRequest(Request request, String param, String defaultValue) {
|
String stringFromRequest(Request request, String param, String defaultValue) {
|
||||||
return Strings.isNullOrEmpty(request.queryParams(param)) ? defaultValue : request.queryParams(param);
|
return Strings.isNullOrEmpty(request.queryParams(param)) ? defaultValue : request.queryParams(param);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
record PaginationInfoPage(int number, boolean current) {}
|
||||||
}
|
}
|
||||||
|
@ -24,6 +24,20 @@
|
|||||||
<div><small class="text-muted">{{url}}</small></div>
|
<div><small class="text-muted">{{url}}</small></div>
|
||||||
<p>{{description}}</p>
|
<p>{{description}}</p>
|
||||||
</div>
|
</div>
|
||||||
|
{{/each}}
|
||||||
|
<nav aria-label="pagination">
|
||||||
|
<ul class="pagination">
|
||||||
|
{{#each pages}}
|
||||||
|
<form action="/search">
|
||||||
|
<input type="hidden" name="q" value="{{query}}">
|
||||||
|
<input type="hidden" name="page" value="{{number}}">
|
||||||
|
<li class="page-item {{#if current}}active{{/if}}"><input type="submit" class="page-link" value="{{number}}"></li>
|
||||||
|
</form>
|
||||||
|
{{/each}}
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
|
{{#each pages}}
|
||||||
|
|
||||||
{{/each}}
|
{{/each}}
|
||||||
{{/if}}
|
{{/if}}
|
||||||
</div>
|
</div>
|
||||||
|
Loading…
Reference in New Issue
Block a user