(search) Add view for showing mutual links between two websites
This commit is contained in:
parent
33312ab09e
commit
a742503508
@ -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)
|
||||||
|
@ -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 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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);
|
||||||
|
@ -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();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
@ -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;
|
||||||
|
@ -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>
|
||||||
|
|
||||||
|
|
@ -44,7 +44,9 @@
|
|||||||
{{#if screenshot}}📷{{/if}}
|
{{#if screenshot}}📷{{/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}}📷{{/if}}
|
{{#if screenshot}}📷{{/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>
|
||||||
|
Loading…
Reference in New Issue
Block a user