(cat{})
+ }
+ first-phase {
+ expression: sum(query(user_profile) * attribute(category_scores))
+ }
+ }
+
+}
diff --git a/tests/testapps/production-deployment-with-tests-java/src/main/application/services.xml b/tests/testapps/production-deployment-with-tests-java/src/main/application/services.xml
new file mode 100644
index 00000000..32f7047c
--- /dev/null
+++ b/tests/testapps/production-deployment-with-tests-java/src/main/application/services.xml
@@ -0,0 +1,23 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 2
+
+
+
+
+
+
+
+
+
diff --git a/tests/testapps/production-deployment-with-tests-java/src/test/java/ai/vespa/example/album/ApplicationMain.java b/tests/testapps/production-deployment-with-tests-java/src/test/java/ai/vespa/example/album/ApplicationMain.java
new file mode 100644
index 00000000..f0985db9
--- /dev/null
+++ b/tests/testapps/production-deployment-with-tests-java/src/test/java/ai/vespa/example/album/ApplicationMain.java
@@ -0,0 +1,30 @@
+// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package ai.vespa.example.album;
+
+import com.yahoo.application.Application;
+import com.yahoo.application.Networking;
+
+import java.nio.file.FileSystems;
+import java.util.Objects;
+
+/**
+ * This uses the Application class to set up a container instance of this application
+ * in this JVM. All other aspects of the application package than a single container
+ * cluster is ignored. This is useful for e.g starting a container instance in your IDE
+ * and serving real HTTP requests for interactive debugging.
+ *
+ * After running main you can e.g open
+ * http://localhost:8080/search/?query=title:foo&tracelevel=2
+ * in a browser.
+ */
+public class ApplicationMain {
+
+ public static void main(String[] args) throws Exception {
+ try (Application app = Application.fromApplicationPackage(FileSystems.getDefault().getPath("src/main/application"),
+ Networking.enable)) {
+ Objects.requireNonNull(app);
+ Thread.sleep(Long.MAX_VALUE);
+ }
+ }
+
+}
diff --git a/tests/testapps/production-deployment-with-tests-java/src/test/java/ai/vespa/example/album/FeedAndSearchSystemTest.java b/tests/testapps/production-deployment-with-tests-java/src/test/java/ai/vespa/example/album/FeedAndSearchSystemTest.java
new file mode 100644
index 00000000..2f59a80e
--- /dev/null
+++ b/tests/testapps/production-deployment-with-tests-java/src/test/java/ai/vespa/example/album/FeedAndSearchSystemTest.java
@@ -0,0 +1,63 @@
+// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package ai.vespa.example.album;
+
+import ai.vespa.hosted.cd.Endpoint;
+import ai.vespa.hosted.cd.SystemTest;
+import ai.vespa.hosted.cd.TestRuntime;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.TestReporter;
+
+import java.io.IOException;
+import java.net.http.HttpResponse;
+import java.util.Map;
+
+import static java.net.http.HttpRequest.BodyPublishers.ofString;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+@SystemTest
+class FeedAndSearchSystemTest {
+
+ private final Endpoint endpoint = TestRuntime.get().deploymentToTest().endpoint("default");
+
+ @Test
+ void testOutput(TestReporter testReporter) {
+ testReporter.publishEntry("Hello from an empty test!");
+ assertTrue(true, "Text from assertion for comparison");
+ }
+
+ @Test
+ void feedAndSearch() throws IOException {
+ String documentPath = "/document/v1/mynamespace/music/docid/test1";
+ String deleteAll = "/document/v1/mynamespace/music/docid?selection=true&cluster=music";
+ String document = """
+ {
+ "fields": {
+ "artist": "Coldplay"
+ }
+ }""";
+ String yql = "SELECT * FROM music WHERE artist CONTAINS 'coldplay'";
+
+ HttpResponse deleteResult = endpoint.send(endpoint.request(deleteAll).DELETE());
+ assertEquals(200, deleteResult.statusCode());
+
+ // the first query needs a higher timeout than the default 500ms, to warm up the code
+ HttpResponse emptyResult = endpoint.send(endpoint.request("/search/",
+ Map.of("yql", yql,
+ "timeout", "10s")));
+ assertEquals(200, emptyResult.statusCode());
+ assertEquals(0, new ObjectMapper().readTree(emptyResult.body())
+ .get("root").get("fields").get("totalCount").asLong());
+
+ HttpResponse feedResult = endpoint.send(endpoint.request(documentPath)
+ .POST(ofString(document)));
+ assertEquals(200, feedResult.statusCode());
+
+ HttpResponse searchResult = endpoint.send(endpoint.request("/search/",
+ Map.of("yql", yql)));
+ assertEquals(200, searchResult.statusCode());
+ assertEquals(1, new ObjectMapper().readTree(searchResult.body())
+ .get("root").get("fields").get("totalCount").asLong());
+ }
+}
diff --git a/tests/testapps/production-deployment-with-tests-java/src/test/java/ai/vespa/example/album/StagingCommons.java b/tests/testapps/production-deployment-with-tests-java/src/test/java/ai/vespa/example/album/StagingCommons.java
new file mode 100644
index 00000000..bda757b8
--- /dev/null
+++ b/tests/testapps/production-deployment-with-tests-java/src/test/java/ai/vespa/example/album/StagingCommons.java
@@ -0,0 +1,83 @@
+// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package ai.vespa.example.album;
+
+import ai.vespa.hosted.cd.Endpoint;
+import ai.vespa.hosted.cd.TestRuntime;
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+
+import java.io.IOException;
+import java.io.UncheckedIOException;
+import java.net.http.HttpResponse;
+import java.util.Map;
+import java.util.stream.Stream;
+
+import static java.net.URLEncoder.encode;
+import static java.nio.charset.StandardCharsets.UTF_8;
+import static java.util.stream.Collectors.toUnmodifiableMap;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+class StagingCommons {
+
+ private static final ObjectMapper mapper = new ObjectMapper();
+
+ /** Returns the container endpoint to do requests against. */
+ static Endpoint container() {
+ return TestRuntime.get().deploymentToTest().endpoint("default");
+ }
+
+ /** Returns the document path of the document with the given name. */
+ static String documentPath(String documentName) {
+ return "/document/v1/mynamespace/music/docid/" + encode(documentName, UTF_8);
+ }
+
+ /** Reads and returns the contents of the JSON test resource with the given name. */
+ static byte[] readDocumentResource(String documentName) {
+ try {
+ return StagingSetupTest.class.getResourceAsStream("/" + documentName + ".json").readAllBytes();
+ }
+ catch (IOException e) {
+ throw new UncheckedIOException(e);
+ }
+ }
+
+ /** Returns static document ID paths and document bytes for the three static staging test documents. */
+ static Map documentsByPath() {
+ return Stream.of("A-Head-Full-of-Dreams",
+ "Hardwired...To-Self-Destruct",
+ "Love-Is-Here-To-Stay")
+ .collect(toUnmodifiableMap(StagingCommons::documentPath,
+ StagingCommons::readDocumentResource));
+ }
+
+ /** Warm-up query matching all "music" documents — high timeout as the fresh container needs to warm up. */
+ static Map warmupQueryForAllDocuments() {
+ return Map.of("yql", "SELECT * FROM music WHERE true", "timeout", "10s");
+ }
+
+ static Map queryForArtist() {
+ return Map.of("yql", "SELECT * FROM music WHERE true");
+ }
+
+ /** Verifies the static staging documents are searchable, ranked correctly, and render as expected. */
+ static void verifyDocumentsAreSearchable() throws IOException {
+ warmup();
+
+ // Verify that the cluster filters and ranks documents as expected, prior to upgrade.
+ HttpResponse queryResponse = container().send(container().request("/search/", queryForArtist()));
+ assertEquals(200, queryResponse.statusCode());
+ JsonNode root = mapper.readTree(queryResponse.body()).get("root");
+ assertEquals(3, root.get("fields").get("totalCount").asLong());
+ }
+
+ private static void warmup() throws IOException {
+ // Verify that the cluster has the fed documents, and that they are searchable.
+ for (int i = 0; i <= 5; i++) {
+ HttpResponse warmUpResponse = container().send(container().request("/search/", warmupQueryForAllDocuments()));
+ assertEquals(200, warmUpResponse.statusCode());
+ assertEquals(3, mapper.readTree(warmUpResponse.body())
+ .get("root").get("fields").get("totalCount").asLong());
+ }
+ }
+
+}
diff --git a/tests/testapps/production-deployment-with-tests-java/src/test/java/ai/vespa/example/album/StagingSetupTest.java b/tests/testapps/production-deployment-with-tests-java/src/test/java/ai/vespa/example/album/StagingSetupTest.java
new file mode 100644
index 00000000..6fee1592
--- /dev/null
+++ b/tests/testapps/production-deployment-with-tests-java/src/test/java/ai/vespa/example/album/StagingSetupTest.java
@@ -0,0 +1,38 @@
+// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package ai.vespa.example.album;
+
+import ai.vespa.hosted.cd.StagingSetup;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+
+import java.io.IOException;
+import java.net.http.HttpResponse;
+import java.util.Map;
+
+import static ai.vespa.example.album.StagingCommons.container;
+import static ai.vespa.example.album.StagingCommons.documentsByPath;
+import static ai.vespa.example.album.StagingCommons.verifyDocumentsAreSearchable;
+import static java.net.http.HttpRequest.BodyPublishers.ofByteArray;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+@StagingSetup
+class StagingSetupTest {
+
+ @Test
+ @DisplayName("Feed documents to the staging cluster, before upgrade")
+ void feedAndSearch() throws IOException {
+ // Make sure cluster is empty
+ String deleteAll = "/document/v1/mynamespace/music/docid";
+ HttpResponse deleteResult = container().send(container().request(deleteAll, Map.of("selection", "true", "cluster", "music")).DELETE());
+ assertEquals(200, deleteResult.statusCode());
+
+ // Feed the static staging test documents
+ documentsByPath().forEach((documentPath, document) -> {
+ assertEquals(200, container().send(container().request(documentPath).POST(ofByteArray(document))).statusCode());
+ });
+
+ // Verify documents are searchable and rendered as expected, prior to upgrade.
+ verifyDocumentsAreSearchable();
+ }
+
+}
diff --git a/tests/testapps/production-deployment-with-tests-java/src/test/java/ai/vespa/example/album/StagingVerificationTest.java b/tests/testapps/production-deployment-with-tests-java/src/test/java/ai/vespa/example/album/StagingVerificationTest.java
new file mode 100644
index 00000000..5ea5ef21
--- /dev/null
+++ b/tests/testapps/production-deployment-with-tests-java/src/test/java/ai/vespa/example/album/StagingVerificationTest.java
@@ -0,0 +1,47 @@
+// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package ai.vespa.example.album;
+
+import ai.vespa.hosted.cd.StagingTest;
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+
+import java.io.IOException;
+import java.io.UncheckedIOException;
+import java.net.http.HttpResponse;
+import java.util.Iterator;
+import java.util.List;
+
+import static ai.vespa.example.album.StagingCommons.container;
+import static ai.vespa.example.album.StagingCommons.documentsByPath;
+import static ai.vespa.example.album.StagingCommons.verifyDocumentsAreSearchable;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+@StagingTest
+class StagingVerificationTest {
+
+ @Test
+ @DisplayName("Verify documents can be searched after upgrade")
+ void verify() throws IOException {
+ // Verify each of the documents fed in setup are still there, with expected data.
+ ObjectMapper mapper = new ObjectMapper();
+ documentsByPath().forEach((documentPath, document) -> {
+ try {
+ HttpResponse documentResponse = container().send(container().request(documentPath).GET());
+ assertEquals(200, documentResponse.statusCode());
+ JsonNode retrieved = mapper.readTree(documentResponse.body()).get("fields");
+ JsonNode expected = mapper.readTree(document).get("fields");
+ for (String name : List.of("text"))
+ assertEquals(expected.get(name), retrieved.get(name));
+ }
+ catch (IOException e) {
+ throw new UncheckedIOException(e);
+ }
+ });
+
+ // Verify documents are searchable and rendered as expected, after the upgrade.
+ verifyDocumentsAreSearchable();
+ }
+
+}
diff --git a/tests/testapps/production-deployment-with-tests-java/src/test/resources/A-Head-Full-of-Dreams.json b/tests/testapps/production-deployment-with-tests-java/src/test/resources/A-Head-Full-of-Dreams.json
new file mode 100644
index 00000000..c67b9066
--- /dev/null
+++ b/tests/testapps/production-deployment-with-tests-java/src/test/resources/A-Head-Full-of-Dreams.json
@@ -0,0 +1,15 @@
+{
+ "put": "id:mynamespace:music::a-head-full-of-dreams",
+ "fields": {
+ "album": "A Head Full of Dreams",
+ "artist": "Coldplay",
+ "year": 2015,
+ "category_scores": {
+ "cells": [
+ { "address" : { "cat" : "pop" }, "value": 1 },
+ { "address" : { "cat" : "rock" }, "value": 0.2 },
+ { "address" : { "cat" : "jazz" }, "value": 0 }
+ ]
+ }
+ }
+}
diff --git a/tests/testapps/production-deployment-with-tests-java/src/test/resources/Hardwired...To-Self-Destruct.json b/tests/testapps/production-deployment-with-tests-java/src/test/resources/Hardwired...To-Self-Destruct.json
new file mode 100644
index 00000000..319025e7
--- /dev/null
+++ b/tests/testapps/production-deployment-with-tests-java/src/test/resources/Hardwired...To-Self-Destruct.json
@@ -0,0 +1,16 @@
+{
+ "put": "id:mynamespace:music::hardwired-to-self-destruct",
+ "fields": {
+ "album": "Hardwired...To Self-Destruct",
+ "artist": "Metallica",
+ "year": 2016,
+ "category_scores": {
+ "cells": [
+ { "address" : { "cat" : "pop" }, "value": 0 },
+ { "address" : { "cat" : "rock" }, "value": 1 },
+ { "address" : { "cat" : "jazz" }, "value": 0 }
+ ]
+ }
+ }
+}
+
diff --git a/tests/testapps/production-deployment-with-tests-java/src/test/resources/Love-Is-Here-To-Stay.json b/tests/testapps/production-deployment-with-tests-java/src/test/resources/Love-Is-Here-To-Stay.json
new file mode 100644
index 00000000..5d51044a
--- /dev/null
+++ b/tests/testapps/production-deployment-with-tests-java/src/test/resources/Love-Is-Here-To-Stay.json
@@ -0,0 +1,16 @@
+{
+ "put": "id:mynamespace:music::love-id-here-to-stay",
+ "fields": {
+ "album": "Love Is Here To Stay",
+ "artist": "Diana Krall",
+ "year": 2018,
+ "category_scores": {
+ "cells": [
+ { "address" : { "cat" : "pop" }, "value": 0.4 },
+ { "address" : { "cat" : "rock" }, "value": 0 },
+ { "address" : { "cat" : "jazz" }, "value": 0.8 }
+ ]
+ }
+ }
+}
+