mirror of
https://github.com/MarginaliaSearch/MarginaliaSearch.git
synced 2025-02-22 20:48:59 +00:00
(*) 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:
parent
14b7680328
commit
a0f28a7f9b
@ -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')
|
||||
|
@ -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)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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) {
|
||||
|
@ -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>
|
@ -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>
|
163
docker-compose-barebones.yml
Normal file
163
docker-compose-barebones.yml
Normal 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
|
Loading…
Reference in New Issue
Block a user