diff --git a/pom.xml b/pom.xml
index 3d8cd95d..e0b02816 100644
--- a/pom.xml
+++ b/pom.xml
@@ -163,6 +163,10 @@
io.quarkus
quarkus-hibernate-validator
+
+ io.quarkus
+ quarkus-cache
+
io.quarkiverse.helm
quarkus-helm
diff --git a/src/main/java/io/quarkus/search/app/QuarkusVersions.java b/src/main/java/io/quarkus/search/app/QuarkusVersions.java
index a42ee06e..551ac322 100644
--- a/src/main/java/io/quarkus/search/app/QuarkusVersions.java
+++ b/src/main/java/io/quarkus/search/app/QuarkusVersions.java
@@ -1,5 +1,7 @@
package io.quarkus.search.app;
+import java.util.Comparator;
+
public final class QuarkusVersions {
private QuarkusVersions() {
}
@@ -8,4 +10,24 @@ private QuarkusVersions() {
public static final String MAIN = "main";
public static final String V3_2 = "3.2";
+ public static final Comparator COMPARATOR = new Comparator() {
+ @Override
+ public int compare(String left, String right) {
+ if (left.equals(right)) {
+ return 0;
+ } else if (left.equals(MAIN)) {
+ return 1;
+ } else if (right.equals(MAIN)) {
+ return -1;
+ } else if (left.equals(LATEST)) {
+ // "latest" actually means "latest non-snapshot", so it's older than main.
+ return 1;
+ } else if (right.equals(LATEST)) {
+ return -1;
+ } else {
+ return left.compareTo(right);
+ }
+ }
+ };
+
}
diff --git a/src/main/java/io/quarkus/search/app/ReferenceService.java b/src/main/java/io/quarkus/search/app/ReferenceService.java
new file mode 100644
index 00000000..a54659f8
--- /dev/null
+++ b/src/main/java/io/quarkus/search/app/ReferenceService.java
@@ -0,0 +1,82 @@
+package io.quarkus.search.app;
+
+import java.util.Arrays;
+import java.util.Comparator;
+import java.util.List;
+import java.util.Map;
+
+import jakarta.enterprise.context.ApplicationScoped;
+import jakarta.inject.Inject;
+import jakarta.transaction.Transactional;
+import jakarta.ws.rs.GET;
+import jakarta.ws.rs.Path;
+import jakarta.ws.rs.Produces;
+import jakarta.ws.rs.core.MediaType;
+
+import io.quarkus.search.app.cache.MethodNameCacheKeyGenerator;
+import io.quarkus.search.app.entity.Guide;
+import io.quarkus.search.app.entity.Language;
+
+import io.quarkus.cache.Cache;
+import io.quarkus.cache.CacheName;
+import io.quarkus.cache.CacheResult;
+
+import org.hibernate.search.engine.search.aggregation.AggregationKey;
+import org.hibernate.search.mapper.orm.session.SearchSession;
+
+import org.eclipse.microprofile.openapi.annotations.Operation;
+
+@ApplicationScoped
+@Path("/")
+@Transactional
+@org.jboss.resteasy.reactive.Cache(maxAge = 120)
+public class ReferenceService {
+
+ private static final String REFERENCE_CACHE = "reference-cache";
+
+ @CacheName(REFERENCE_CACHE)
+ Cache cache;
+
+ @Inject
+ SearchSession session;
+
+ @GET
+ @Produces(MediaType.APPLICATION_JSON)
+ @Operation(summary = "List available versions")
+ @Path("/versions")
+ @CacheResult(cacheName = REFERENCE_CACHE, keyGenerator = MethodNameCacheKeyGenerator.class)
+ public List versions() {
+ return listAllValues("quarkusVersion", QuarkusVersions.COMPARATOR.reversed());
+ }
+
+ @GET
+ @Produces(MediaType.APPLICATION_JSON)
+ @Operation(summary = "List available languages")
+ @Path("/languages")
+ @CacheResult(cacheName = REFERENCE_CACHE, keyGenerator = MethodNameCacheKeyGenerator.class)
+ public List languages() {
+ return Arrays.stream(Language.values()).map(lang -> lang.code).toList();
+ }
+
+ @GET
+ @Produces(MediaType.APPLICATION_JSON)
+ @Operation(summary = "List available categories")
+ @Path("/categories")
+ @CacheResult(cacheName = REFERENCE_CACHE, keyGenerator = MethodNameCacheKeyGenerator.class)
+ public List categories() {
+ return listAllValues("categories", Comparator.naturalOrder());
+ }
+
+ public void invalidateCaches() {
+ cache.invalidateAll();
+ }
+
+ private List listAllValues(String fieldName, Comparator comparator) {
+ var aggKey = AggregationKey.