(search) Add view for showing mutual links between two websites

This commit is contained in:
Viktor Lofgren 2023-12-17 17:50:44 +01:00
parent 33312ab09e
commit a742503508
8 changed files with 158 additions and 3 deletions

View File

@ -36,6 +36,10 @@ public record SimilarDomain(EdgeUrl url,
BIDIRECTIONAL, BIDIRECTIONAL,
NONE; NONE;
public boolean isLinked() {
return this != NONE;
}
public static LinkType find(boolean linkStod, public static LinkType find(boolean linkStod,
boolean linkDtos) { boolean linkDtos) {
if (linkDtos && linkStod) if (linkDtos && linkStod)

View File

@ -85,6 +85,14 @@ public class SearchOperator {
return searchQueryService.getResultsFromQuery(queryResponse); return searchQueryService.getResultsFromQuery(queryResponse);
} }
public List<UrlDetails> doLinkSearch(Context context, String source, String dest) {
var queryParams = paramFactory.forLinkSearch(source, dest);
var queryResponse = queryClient.search(context, queryParams);
return searchQueryService.getResultsFromQuery(queryResponse);
}
public DecoratedSearchResults doSearch(Context ctx, SearchParameters userParams) { public DecoratedSearchResults doSearch(Context ctx, SearchParameters userParams) {
Future<String> eval = searchUnitConversionService.tryEval(ctx, userParams.query()); Future<String> eval = searchUnitConversionService.tryEval(ctx, userParams.query());
@ -181,4 +189,5 @@ public class SearchOperator {
} }
} }

View File

@ -69,4 +69,21 @@ public class SearchQueryParamFactory {
SearchSetIdentifier.NONE SearchSetIdentifier.NONE
); );
} }
public QueryParams forLinkSearch(String sourceDomain, String destDomain) {
return new QueryParams(STR."site:\{sourceDomain} links:\{destDomain}",
null,
List.of(),
List.of(),
List.of(),
List.of(),
SpecificationLimit.none(),
SpecificationLimit.none(),
SpecificationLimit.none(),
SpecificationLimit.none(),
List.of(),
new QueryLimits(100, 100, 100, 512),
SearchSetIdentifier.NONE
);
}
} }

View File

@ -32,6 +32,7 @@ public class SearchService extends Service {
SearchErrorPageService errorPageService, SearchErrorPageService errorPageService,
SearchAddToCrawlQueueService addToCrawlQueueService, SearchAddToCrawlQueueService addToCrawlQueueService,
SearchSiteInfoService siteInfoService, SearchSiteInfoService siteInfoService,
SearchCrosstalkService crosstalkService,
SearchQueryService searchQueryService SearchQueryService searchQueryService
) { ) {
super(params); super(params);
@ -55,6 +56,8 @@ public class SearchService extends Service {
Spark.get("/public/site/:site", siteInfoService::handle); Spark.get("/public/site/:site", siteInfoService::handle);
Spark.post("/public/site/:site", siteInfoService::handlePost); Spark.post("/public/site/:site", siteInfoService::handlePost);
Spark.get("/public/crosstalk/", crosstalkService::handle);
Spark.exception(Exception.class, (e,p,q) -> { Spark.exception(Exception.class, (e,p,q) -> {
logger.error("Error during processing", e); logger.error("Error during processing", e);
errorPageService.serveError(Context.fromRequest(p), p, q); errorPageService.serveError(Context.fromRequest(p), p, q);

View File

@ -0,0 +1,70 @@
package nu.marginalia.search.svc;
import com.google.inject.Inject;
import nu.marginalia.client.Context;
import nu.marginalia.renderer.MustacheRenderer;
import nu.marginalia.renderer.RendererFactory;
import nu.marginalia.search.SearchOperator;
import nu.marginalia.search.model.UrlDetails;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import spark.Request;
import spark.Response;
import java.io.IOException;
import java.sql.SQLException;
import java.util.List;
public class SearchCrosstalkService {
private static final Logger logger = LoggerFactory.getLogger(SearchCrosstalkService.class);
private final SearchOperator searchOperator;
private final MustacheRenderer<CrosstalkResult> renderer;
@Inject
public SearchCrosstalkService(SearchOperator searchOperator,
RendererFactory rendererFactory) throws IOException
{
this.searchOperator = searchOperator;
this.renderer = rendererFactory.renderer("search/site-info/site-crosstalk");
}
public Object handle(Request request, Response response) throws SQLException {
String domains = request.queryParams("domains");
String[] parts = StringUtils.split(domains, ',');
if (parts.length != 2) {
throw new IllegalArgumentException("Expected exactly two domains");
}
response.type("text/html");
for (int i = 0; i < parts.length; i++) {
parts[i] = parts[i].trim();
}
var resAtoB = searchOperator.doLinkSearch(Context.fromRequest(request), parts[0], parts[1]);
var resBtoA = searchOperator.doLinkSearch(Context.fromRequest(request), parts[1], parts[0]);
var model = new CrosstalkResult(parts[0], parts[1], resAtoB, resBtoA);
return renderer.render(model);
}
private record CrosstalkResult(String domainA,
String domainB,
List<UrlDetails> forward,
List<UrlDetails> backward)
{
public boolean isFocusDomain() {
return true; // Hack to get the search result templates behave well
}
public boolean hasBoth() {
return !forward.isEmpty() && !backward.isEmpty();
}
}
}

View File

@ -352,6 +352,15 @@ footer {
align-items: start; align-items: start;
} }
#crosstalk-view {
display: grid;
grid-template-columns: 1fr 1fr;
grid-template-rows: auto 1fr;
grid-gap: 1ch;
align-content: start;
justify-content: start;
align-items: start;
}
#similar-view { #similar-view {
display: grid; display: grid;
@ -405,7 +414,7 @@ footer {
} }
@media (max-device-width: 900px) { @media (max-device-width: 900px) {
#similar-view { #similar-view, #crosstalk-view {
display: block; display: block;
* { * {
margin-bottom: 1ch; margin-bottom: 1ch;

View File

@ -0,0 +1,39 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Marginalia Search - {{domainA}} and {{domainB}}</title>
<link rel="stylesheet" href="/serp.css" />
<link rel="search" type="application/opensearchdescription+xml" href="/opensearch.xml" title="Marginalia">
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="robots" content="noindex" />
</head>
<body>
{{>search/parts/search-header}}
{{>search/parts/search-form}}
<div class="infobox">
Showing results containing links between <a href="/site/{{domainA}}">{{domainA}}</a> and <a href="/site/{{domainB}}">{{domainB}}</a>.
</div>
{{#each tests}}{{.}}{{/each}}
<div {{#if hasBoth}}id="crosstalk-view"{{/if}}>
<div>
{{#each forward}}
{{>search/parts/search-result}}
{{/each}}
</div>
<div>
{{#each backward}}
{{>search/parts/search-result}}
{{/each}}
</div>
</div>
{{>search/parts/search-footer}}
</body>

View File

@ -44,7 +44,9 @@
{{#if screenshot}}&#x1f4f7;{{/if}} {{#if screenshot}}&#x1f4f7;{{/if}}
</td> </td>
<td> <td>
<span title="{{linkType.description}}">{{{linkType}}}</span> {{#if linkType.isLinked}}
<span title="{{linkType.description}}"><a href="/crosstalk/?domains={{domain}},{{url.domain}}">{{{linkType}}}</a></span>
{{/if}}
</td> </td>
<td> <td>
<span title="{{rank}}%">{{{rankSymbols}}}</span> <span title="{{rank}}%">{{{rankSymbols}}}</span>
@ -92,7 +94,9 @@
{{#if screenshot}}&#x1f4f7;{{/if}} {{#if screenshot}}&#x1f4f7;{{/if}}
</td> </td>
<td> <td>
<span title="{{linkType.description}}">{{{linkType}}}</span> {{#if linkType.isLinked}}
<span title="{{linkType.description}}"><a href="/crosstalk/?domains={{domain}},{{url.domain}}">{{{linkType}}}</a></span>
{{/if}}
</td> </td>
<td> <td>
<span title="{{rank}}%">{{{rankSymbols}}}</span> <span title="{{rank}}%">{{{rankSymbols}}}</span>