From a0f28a7f9b5aaa85a38e9c46f6c30c929e6ed940 Mon Sep 17 00:00:00 2001 From: Viktor Lofgren Date: Wed, 10 Jan 2024 20:23:51 +0100 Subject: [PATCH] (*) 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. --- code/services-core/query-service/build.gradle | 1 + .../marginalia/query/QueryBasicInterface.java | 68 ++++++++ .../java/nu/marginalia/query/QueryModule.java | 3 + .../nu/marginalia/query/QueryService.java | 20 ++- .../main/resources/static/public/index.html | 50 ++++++ .../src/main/resources/templates/search.hdb | 31 ++++ docker-compose-barebones.yml | 163 ++++++++++++++++++ 7 files changed, 334 insertions(+), 2 deletions(-) create mode 100644 code/services-core/query-service/src/main/java/nu/marginalia/query/QueryBasicInterface.java create mode 100644 code/services-core/query-service/src/main/resources/static/public/index.html create mode 100644 code/services-core/query-service/src/main/resources/templates/search.hdb create mode 100644 docker-compose-barebones.yml diff --git a/code/services-core/query-service/build.gradle b/code/services-core/query-service/build.gradle index 4907a86e..8f7cea9a 100644 --- a/code/services-core/query-service/build.gradle +++ b/code/services-core/query-service/build.gradle @@ -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') diff --git a/code/services-core/query-service/src/main/java/nu/marginalia/query/QueryBasicInterface.java b/code/services-core/query-service/src/main/java/nu/marginalia/query/QueryBasicInterface.java new file mode 100644 index 00000000..ddef20d6 --- /dev/null +++ b/code/services-core/query-service/src/main/java/nu/marginalia/query/QueryBasicInterface.java @@ -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 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) + ); + } + } +} diff --git a/code/services-core/query-service/src/main/java/nu/marginalia/query/QueryModule.java b/code/services-core/query-service/src/main/java/nu/marginalia/query/QueryModule.java index 5a3bde1a..0cb3926f 100644 --- a/code/services-core/query-service/src/main/java/nu/marginalia/query/QueryModule.java +++ b/code/services-core/query-service/src/main/java/nu/marginalia/query/QueryModule.java @@ -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); } } diff --git a/code/services-core/query-service/src/main/java/nu/marginalia/query/QueryService.java b/code/services-core/query-service/src/main/java/nu/marginalia/query/QueryService.java index fc8bc8fb..7e864635 100644 --- a/code/services-core/query-service/src/main/java/nu/marginalia/query/QueryService.java +++ b/code/services-core/query-service/src/main/java/nu/marginalia/query/QueryService.java @@ -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) { diff --git a/code/services-core/query-service/src/main/resources/static/public/index.html b/code/services-core/query-service/src/main/resources/static/public/index.html new file mode 100644 index 00000000..7fb19c9d --- /dev/null +++ b/code/services-core/query-service/src/main/resources/static/public/index.html @@ -0,0 +1,50 @@ + + + + + + +Query Service + + +
+

Marginalia Query Service

+

This is the barebones search interface for the Marginalia Search Engine software.

+

If you are seeing this somewhere other than your own machine, someone has likely misconfigured something.

+ +

Control GUI

+

+ If this is running on your own machine, you can also use the control interface, which is by default available + on http://localhost:8081/. +

+ +

API

+ +

There is not much here, except the /search endpoint offers a very web interface for + performing queries. The same endpoint also responds with JSON if the Accept-header is provided.

+ + 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.

+ + +

Minimal integration with the API:

+
+
Curl
+
+ $ curl -H 'Accept: application/json' 'http://localhost:8080/search?q=test' +
+
+ Python 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)) +
+

Remember to URLencode the query!

+ + diff --git a/code/services-core/query-service/src/main/resources/templates/search.hdb b/code/services-core/query-service/src/main/resources/templates/search.hdb new file mode 100644 index 00000000..14bbf2b5 --- /dev/null +++ b/code/services-core/query-service/src/main/resources/templates/search.hdb @@ -0,0 +1,31 @@ + + + + + + + Query Service + + +
+

Query Service

+
+
+
+
+
+
+
+{{#if results}} +

Results

+{{#each results}} +
+ {{title}} +
{{url}}
+

{{description}}

+
+{{/each}} +{{/if}} +
+ + \ No newline at end of file diff --git a/docker-compose-barebones.yml b/docker-compose-barebones.yml new file mode 100644 index 00000000..71a4bf77 --- /dev/null +++ b/docker-compose-barebones.yml @@ -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 \ No newline at end of file