(*) Add a barebones configuration

This adds a docker-compose file 'docker-compose-barebones.yml' which will only start the minimal number of services needed to run a whitelabel Marginalia Search-style search engine, with none of the surrounding frills.

The change also adds a minimal search GUI to the query service, which is also available with JSON results if the appropriate Accept header is provided.
This commit is contained in:
Viktor Lofgren 2024-01-10 20:23:51 +01:00
parent 14b7680328
commit a0f28a7f9b
7 changed files with 334 additions and 2 deletions

View File

@ -26,6 +26,7 @@ dependencies {
implementation project(':code:common:model')
implementation project(':code:common:db')
implementation project(':code:common:service')
implementation project(':code:common:renderer')
implementation project(':code:common:service-client')
implementation project(':code:api:index-api')
implementation project(':code:api:query-api')

View File

@ -0,0 +1,68 @@
package nu.marginalia.query;
import com.google.gson.Gson;
import com.google.inject.Inject;
import nu.marginalia.client.Context;
import nu.marginalia.index.client.IndexClient;
import nu.marginalia.index.client.model.query.SearchSetIdentifier;
import nu.marginalia.index.query.limit.QueryLimits;
import nu.marginalia.model.gson.GsonFactory;
import nu.marginalia.query.model.QueryParams;
import nu.marginalia.query.svc.NodeConfigurationWatcher;
import nu.marginalia.renderer.MustacheRenderer;
import nu.marginalia.renderer.RendererFactory;
import nu.marginalia.query.svc.QueryFactory;
import spark.Request;
import spark.Response;
import java.io.IOException;
import java.util.Map;
public class QueryBasicInterface {
private final MustacheRenderer<Object> renderer;
private final NodeConfigurationWatcher nodeConfigurationWatcher;
private final IndexClient indexClient;
private final QueryFactory queryFactory;
private final Gson gson = GsonFactory.get();
@Inject
public QueryBasicInterface(RendererFactory rendererFactory,
NodeConfigurationWatcher nodeConfigurationWatcher,
IndexClient indexClient,
QueryFactory queryFactory
) throws IOException
{
this.renderer = rendererFactory.renderer("search");
this.nodeConfigurationWatcher = nodeConfigurationWatcher;
this.indexClient = indexClient;
this.queryFactory = queryFactory;
}
public Object handle(Request request, Response response) {
String queryParam = request.queryParams("q");
if (queryParam == null) {
return renderer.render(new Object());
}
var query = queryFactory.createQuery(new QueryParams(queryParam, new QueryLimits(
1, 10, 250, 8192
), SearchSetIdentifier.NONE));
var rsp = indexClient.query(
Context.fromRequest(request),
nodeConfigurationWatcher.getQueryNodes(),
query.specs
);
if (request.headers("Accept").contains("application/json")) {
response.type("application/json");
return gson.toJson(rsp);
}
else {
return renderer.render(
Map.of("query", queryParam,
"results", rsp.results)
);
}
}
}

View File

@ -5,10 +5,13 @@ import com.google.inject.AbstractModule;
import nu.marginalia.LanguageModels;
import nu.marginalia.WmsaHome;
import nu.marginalia.model.gson.GsonFactory;
import nu.marginalia.renderer.config.DefaultHandlebarsConfigurator;
import nu.marginalia.renderer.config.HandlebarsConfigurator;
public class QueryModule extends AbstractModule {
public void configure() {
bind(LanguageModels.class).toInstance(WmsaHome.getLanguageModels());
bind(Gson.class).toProvider(GsonFactory::get);
bind(HandlebarsConfigurator.class).to(DefaultHandlebarsConfigurator.class);
}
}

View File

@ -46,8 +46,13 @@ public class QueryService extends Service {
QueryGRPCService queryGRPCService,
Gson gson,
DomainBlacklist blacklist,
QueryFactory queryFactory) throws IOException {
super(params);
QueryBasicInterface queryBasicInterface,
QueryFactory queryFactory) throws IOException
{
super(params, () -> {
Spark.staticFileLocation("/static/");
});
this.indexClient = indexClient;
this.nodeWatcher = nodeWatcher;
this.gson = gson;
@ -62,6 +67,17 @@ public class QueryService extends Service {
Spark.post("/delegate/", this::delegateToIndex, gson::toJson);
Spark.post("/search/", this::search, gson::toJson);
Spark.get("/public/search", queryBasicInterface::handle);
Spark.exception(Exception.class, (e, request, response) -> {
response.status(500);
try {
e.printStackTrace(response.raw().getWriter());
} catch (IOException ex) {
throw new RuntimeException(ex);
}
});
}
private Object search(Request request, Response response) {

View File

@ -0,0 +1,50 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-T3c6CoIi6uLrA9TneNEoa7RxnatzjcDSCmG1MXxSR1GAsXEV/Dwwykc2MPK8M2HN" crossorigin="anonymous">
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Query Service</title>
</head>
<body>
<div class="container">
<h1 class="my-3">Marginalia Query Service</h1>
<p>This is the barebones search interface for the <a href="https://git.marginalia.nu/">Marginalia Search Engine software</a>. </p>
<p>If you are seeing this somewhere other than your own machine, someone has likely misconfigured something.</p>
<h2>Control GUI</h2>
<p>
If this is running on your own machine, you can also use the control interface, which is by default available
on <a href="http://localhost:8081/">http://localhost:8081/</a>.
</p>
<h2>API</h2>
<p>There is not much here, except the <a href="/search">/search</a> endpoint offers a very web interface for
performing queries. The same endpoint also responds with JSON if the <tt>Accept</tt>-header is provided.</p>
The HTML version of this interface exists for demonstration purposes only.
The same endpoint is available as a web API with JSON-results when the
'Accept: application/json'-header is sent by the client.</p>
<h3>Minimal integration with the API:</h3>
<dl>
<dt>Curl</dt>
<dd>
<code>$ curl -H 'Accept: application/json' 'http://localhost:8080/search?q=test'</code>
</dd>
<dt>
Python 3
</dt>
<dd>
<code style="white-space: pre" class="mb-3">#!/bin/env python3
import requests
import json
r = requests.get('http://localhost:8080/search?q=test',
headers={'Accept': 'application/json'})
print(json.dumps(r.json(), indent=4))</code>
</dd>
<p>Remember to URLencode the query!</p>
</body>
</html>

View File

@ -0,0 +1,31 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-T3c6CoIi6uLrA9TneNEoa7RxnatzjcDSCmG1MXxSR1GAsXEV/Dwwykc2MPK8M2HN" crossorigin="anonymous">
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Query Service</title>
</head>
<body>
<div class="container">
<h1 class="my-3">Query Service</h1>
<form action="/search" method="get">
<div class="form-group"><label for="q">Search Query</label></div>
<div class="row my-2">
<div class="col-sm-8"><input type="text" class="form-control" id="q" name="q" value="{{query}}"></div>
<div class="col-sm-2"><button type="submit" class="btn btn-primary">Submit</button></div>
</div>
</form>
{{#if results}}
<h2 class="my-3">Results</h2>
{{#each results}}
<div class="mb-3">
<a href="{{url}}">{{title}}</a>
<div><small class="text-muted">{{url}}</small></div>
<p>{{description}}</p>
</div>
{{/each}}
{{/if}}
</div>
</body>
</html>

View File

@ -0,0 +1,163 @@
x-svc: &service
env_file:
- "run/env/service.env"
volumes:
- conf:/wmsa/conf:ro
- model:/wmsa/model
- data:/wmsa/data
- logs:/var/log/wmsa
networks:
- wmsa
depends_on:
- mariadb
labels:
- "__meta_docker_port_private=7000"
x-p1: &partition-1
env_file:
- "run/env/service.env"
volumes:
- conf:/wmsa/conf:ro
- model:/wmsa/model
- data:/wmsa/data
- logs:/var/log/wmsa
- index-1:/idx
- work-1:/work
- backup-1:/backup
- samples-1:/storage
networks:
- wmsa
depends_on:
- mariadb
environment:
- "WMSA_SERVICE_NODE=1"
services:
index-service-1:
<<: *partition-1
image: "marginalia/index-service"
container_name: "index-service-1"
executor-service-1:
<<: *partition-1
image: "marginalia/executor-service"
container_name: "executor-service-1"
query-service:
<<: *service
image: "marginalia/query-service"
container_name: "query-service"
expose:
- 80
labels:
- "traefik.enable=true"
- "traefik.http.routers.search-service.rule=PathPrefix(`/`)"
- "traefik.http.routers.search-service.entrypoints=search"
- "traefik.http.routers.search-service.middlewares=add-xpublic"
- "traefik.http.routers.search-service.middlewares=add-public"
- "traefik.http.middlewares.add-xpublic.headers.customrequestheaders.X-Public=1"
- "traefik.http.middlewares.add-public.addprefix.prefix=/public"
control-service:
<<: *service
image: "marginalia/control-service"
container_name: "control-service"
expose:
- 80
labels:
- "traefik.enable=true"
- "traefik.http.routers.control-service.rule=PathPrefix(`/`)"
- "traefik.http.routers.control-service.entrypoints=control"
- "traefik.http.routers.control-service.middlewares=add-xpublic"
- "traefik.http.routers.control-service.middlewares=add-public"
- "traefik.http.middlewares.add-xpublic.headers.customrequestheaders.X-Public=1"
- "traefik.http.middlewares.add-public.addprefix.prefix=/public"
mariadb:
image: "mariadb:lts"
container_name: "mariadb"
env_file: "run/env/mariadb.env"
command: ['mysqld', '--character-set-server=utf8mb4', '--collation-server=utf8mb4_unicode_ci']
ports:
- "127.0.0.1:3306:3306/tcp"
healthcheck:
test: mysqladmin ping -h 127.0.0.1 -u $$MARIADB_USER --password=$$MARIADB_PASSWORD
start_period: 5s
interval: 5s
timeout: 5s
retries: 60
volumes:
- db:/var/lib/mysql
- "./code/common/db/src/main/resources/sql/current/:/docker-entrypoint-initdb.d/"
networks:
- wmsa
traefik:
image: "traefik:v2.10"
container_name: "traefik"
command:
#- "--log.level=DEBUG"
- "--api.insecure=true"
- "--providers.docker=true"
- "--providers.docker.exposedbydefault=false"
- "--entrypoints.search.address=:80"
- "--entrypoints.control.address=:81"
ports:
- "127.0.0.1:8080:80"
- "127.0.0.1:8081:81"
- "127.0.0.1:8090:8080"
volumes:
- "/var/run/docker.sock:/var/run/docker.sock:ro"
networks:
- wmsa
networks:
wmsa:
volumes:
db:
driver: local
driver_opts:
type: none
o: bind
device: run/db
logs:
driver: local
driver_opts:
type: none
o: bind
device: run/logs
model:
driver: local
driver_opts:
type: none
o: bind
device: run/model
conf:
driver: local
driver_opts:
type: none
o: bind
device: run/conf
data:
driver: local
driver_opts:
type: none
o: bind
device: run/data
samples-1:
driver: local
driver_opts:
type: none
o: bind
device: run/node-1/samples
index-1:
driver: local
driver_opts:
type: none
o: bind
device: run/node-1/index
work-1:
driver: local
driver_opts:
type: none
o: bind
device: run/node-1/work
backup-1:
driver: local
driver_opts:
type: none
o: bind
device: run/node-1/backup