This is an automated email from the ASF dual-hosted git repository.

mkataria pushed a commit to branch OAK-11714
in repository https://gitbox.apache.org/repos/asf/jackrabbit-oak.git

commit 07ee6995edd028d3e536ca5506b76d0c8ab1c5fe
Author: Mohit Kataria <[email protected]>
AuthorDate: Sat May 10 00:11:24 2025 +0530

    OAK-11714: Add jmx to expose inferenceConfig
---
 .../jackrabbit/oak/api/jmx/InferenceMBean.java     |  40 +++
 .../index/elastic/ElasticIndexProviderService.java |  13 +
 .../elastic/query/inference/EnricherStatus.java    |  26 ++
 .../elastic/query/inference/InferenceConfig.java   |  57 +++-
 .../query/inference/InferenceHeaderPayload.java    |   8 +-
 .../query/inference/InferenceIndexConfig.java      |  27 +-
 .../query/inference/InferenceMBeanImpl.java        |  49 ++++
 .../query/inference/InferenceModelConfig.java      |  30 +-
 .../elastic/query/inference/InferencePayload.java  |  21 ++
 .../InferenceConfigSerializationTest.java          | 301 +++++++++++++++++++++
 10 files changed, 541 insertions(+), 31 deletions(-)

diff --git 
a/oak-api/src/main/java/org/apache/jackrabbit/oak/api/jmx/InferenceMBean.java 
b/oak-api/src/main/java/org/apache/jackrabbit/oak/api/jmx/InferenceMBean.java
new file mode 100644
index 0000000000..2690b6b64d
--- /dev/null
+++ 
b/oak-api/src/main/java/org/apache/jackrabbit/oak/api/jmx/InferenceMBean.java
@@ -0,0 +1,40 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.jackrabbit.oak.api.jmx;
+
+import org.osgi.annotation.versioning.ProviderType;
+
+/**
+ * An MBean that provides the inference configuration.
+ */
+@ProviderType
+public interface InferenceMBean {
+
+    String TYPE = "Inference";
+
+    /**
+     * Get the inference configuration as a Json string.
+     */
+    String getConfigJson();
+
+    /**
+     * Get the inference configuration as a Json string.
+     */
+    String getConfigNodeStateJson();
+}
diff --git 
a/oak-search-elastic/src/main/java/org/apache/jackrabbit/oak/plugins/index/elastic/ElasticIndexProviderService.java
 
b/oak-search-elastic/src/main/java/org/apache/jackrabbit/oak/plugins/index/elastic/ElasticIndexProviderService.java
index 43565495dd..a53af22e10 100644
--- 
a/oak-search-elastic/src/main/java/org/apache/jackrabbit/oak/plugins/index/elastic/ElasticIndexProviderService.java
+++ 
b/oak-search-elastic/src/main/java/org/apache/jackrabbit/oak/plugins/index/elastic/ElasticIndexProviderService.java
@@ -16,6 +16,7 @@
  */
 package org.apache.jackrabbit.oak.plugins.index.elastic;
 
+import org.apache.jackrabbit.oak.api.jmx.InferenceMBean;
 import org.apache.jackrabbit.oak.commons.IOUtils;
 import org.apache.jackrabbit.oak.osgi.OsgiWhiteboard;
 import org.apache.jackrabbit.oak.plugins.index.AsyncIndexInfoService;
@@ -25,6 +26,7 @@ import 
org.apache.jackrabbit.oak.plugins.index.elastic.index.ElasticIndexEditorP
 import 
org.apache.jackrabbit.oak.plugins.index.elastic.query.ElasticIndexProvider;
 import 
org.apache.jackrabbit.oak.plugins.index.elastic.query.inference.InferenceConfig;
 import 
org.apache.jackrabbit.oak.plugins.index.elastic.query.inference.InferenceConstants;
+import 
org.apache.jackrabbit.oak.plugins.index.elastic.query.inference.InferenceMBeanImpl;
 import 
org.apache.jackrabbit.oak.plugins.index.fulltext.PreExtractedTextProvider;
 import org.apache.jackrabbit.oak.plugins.index.search.ExtractedTextCache;
 import org.apache.jackrabbit.oak.query.QueryEngineSettings;
@@ -209,6 +211,13 @@ public class ElasticIndexProviderService {
                 ElasticIndexMBean.TYPE,
                 "Elastic Index statistics"));
 
+        InferenceMBeanImpl inferenceMBean = new InferenceMBeanImpl();
+        oakRegs.add(registerMBean(whiteboard,
+            InferenceMBean.class,
+            inferenceMBean,
+            InferenceMBean.TYPE,
+            "Inference"));
+
         LOG.info("Registering Index and Editor providers with connection {}", 
elasticConnection);
 
         registerIndexProvider(bundleContext);
@@ -284,4 +293,8 @@ public class ElasticIndexProviderService {
                 .withApiKeys(apiKeyId, apiSecretId)
                 .build();
     }
+
+    public InferenceConfig getInferenceConfig() {
+        return InferenceConfig.getInstance();
+    }
 }
diff --git 
a/oak-search-elastic/src/main/java/org/apache/jackrabbit/oak/plugins/index/elastic/query/inference/EnricherStatus.java
 
b/oak-search-elastic/src/main/java/org/apache/jackrabbit/oak/plugins/index/elastic/query/inference/EnricherStatus.java
index 678b3daaa7..77121f2419 100644
--- 
a/oak-search-elastic/src/main/java/org/apache/jackrabbit/oak/plugins/index/elastic/query/inference/EnricherStatus.java
+++ 
b/oak-search-elastic/src/main/java/org/apache/jackrabbit/oak/plugins/index/elastic/query/inference/EnricherStatus.java
@@ -18,9 +18,11 @@
  */
 package org.apache.jackrabbit.oak.plugins.index.elastic.query.inference;
 
+import com.fasterxml.jackson.core.JsonProcessingException;
 import com.fasterxml.jackson.core.type.TypeReference;
 import com.fasterxml.jackson.databind.ObjectMapper;
 import org.apache.jackrabbit.oak.commons.PathUtils;
+import org.apache.jackrabbit.oak.commons.json.JsopBuilder;
 import org.apache.jackrabbit.oak.spi.state.NodeState;
 import org.apache.jackrabbit.oak.spi.state.NodeStore;
 import org.slf4j.Logger;
@@ -85,4 +87,28 @@ public class EnricherStatus {
         return enricherStatusJsonMapping;
     }
 
+    @Override
+    public String toString() {
+        JsopBuilder builder = new JsopBuilder().object();
+        // Add the mapping data
+        
builder.key("enricherStatusJsonMapping").value(enricherStatusJsonMapping);
+
+        // Add enricher status data
+        builder.key("enricherStatusData").object();
+        for (Map.Entry<String, Object> entry : enricherStatusData.entrySet()) {
+            builder.key(entry.getKey());
+            if (entry.getValue() instanceof String) {
+                builder.value((String) entry.getValue());
+            } else {
+                try {
+                    
builder.encodedValue(MAPPER.writeValueAsString(entry.getValue()));
+                } catch (JsonProcessingException e) {
+                    LOG.warn("Failed to serialize value for key {}: {}", 
entry.getKey(), e.getMessage());
+                    builder.value(entry.getValue().toString());
+                }
+            }
+        }
+        builder.endObject().endObject();
+        return JsopBuilder.prettyPrint(builder.toString());
+    }
 } 
\ No newline at end of file
diff --git 
a/oak-search-elastic/src/main/java/org/apache/jackrabbit/oak/plugins/index/elastic/query/inference/InferenceConfig.java
 
b/oak-search-elastic/src/main/java/org/apache/jackrabbit/oak/plugins/index/elastic/query/inference/InferenceConfig.java
index 34730b7904..f5f251690a 100644
--- 
a/oak-search-elastic/src/main/java/org/apache/jackrabbit/oak/plugins/index/elastic/query/inference/InferenceConfig.java
+++ 
b/oak-search-elastic/src/main/java/org/apache/jackrabbit/oak/plugins/index/elastic/query/inference/InferenceConfig.java
@@ -18,7 +18,10 @@
  */
 package org.apache.jackrabbit.oak.plugins.index.elastic.query.inference;
 
+import com.fasterxml.jackson.core.JsonProcessingException;
 import org.apache.jackrabbit.oak.commons.PathUtils;
+import org.apache.jackrabbit.oak.commons.json.JsopBuilder;
+import org.apache.jackrabbit.oak.json.JsonUtils;
 import org.apache.jackrabbit.oak.plugins.index.IndexName;
 import org.apache.jackrabbit.oak.spi.state.NodeState;
 import org.apache.jackrabbit.oak.spi.state.NodeStore;
@@ -85,7 +88,7 @@ public class InferenceConfig {
         reInitialize(nodeStore, inferenceConfigPath, isInferenceEnabled, true);
     }
 
-    public static void reInitialize(){
+    public static void reInitialize() {
         reInitialize(INSTANCE.nodeStore, INSTANCE.inferenceConfigPath, 
INSTANCE.isInferenceEnabled, true);
     }
 
@@ -101,7 +104,7 @@ public class InferenceConfig {
         }
     }
 
-    private static void reInitialize(NodeStore nodeStore, String 
inferenceConfigPath, boolean isInferenceEnabled, boolean 
updateActiveInferenceConfig){
+    private static void reInitialize(NodeStore nodeStore, String 
inferenceConfigPath, boolean isInferenceEnabled, boolean 
updateActiveInferenceConfig) {
         lock.writeLock().lock();
         try {
             if (updateActiveInferenceConfig) {
@@ -156,11 +159,11 @@ public class InferenceConfig {
                 InferenceIndexConfig inferenceIndexConfig;
                 IndexName indexNameObject;
                 Function<String, InferenceIndexConfig> getInferenceIndexConfig 
= (iName) ->
-                        getIndexConfigs().getOrDefault(iName, 
InferenceIndexConfig.NOOP);
+                    getIndexConfigs().getOrDefault(iName, 
InferenceIndexConfig.NOOP);
                 if (!InferenceIndexConfig.NOOP.equals(inferenceIndexConfig = 
getInferenceIndexConfig.apply(indexName))) {
                     LOG.debug("InferenceIndexConfig for indexName: {} is: {}", 
indexName, inferenceIndexConfig);
                 } else if ((indexNameObject = IndexName.parse(indexName)) != 
null && indexNameObject.isLegal()
-                        && indexNameObject.getBaseName() != null
+                    && indexNameObject.getBaseName() != null
                 ) {
                     LOG.debug("InferenceIndexConfig is using baseIndexName {} 
and is: {}", indexNameObject.getBaseName(), inferenceIndexConfig);
                     inferenceIndexConfig = 
getInferenceIndexConfig.apply(indexNameObject.getBaseName());
@@ -175,7 +178,7 @@ public class InferenceConfig {
     public @NotNull InferenceModelConfig getInferenceModelConfig(String 
inferenceIndexName, String inferenceModelConfigName) {
         lock.readLock().lock();
         try {
-            if (inferenceModelConfigName == null){
+            if (inferenceModelConfigName == null) {
                 return InferenceModelConfig.NOOP;
             } else if (inferenceModelConfigName.isEmpty()) {
                 return 
getInferenceIndexConfig(inferenceIndexName).getDefaultEnabledModel();
@@ -188,7 +191,7 @@ public class InferenceConfig {
 
     }
 
-    public Map<String, Object> getEnricherStatus(){
+    public Map<String, Object> getEnricherStatus() {
         lock.readLock().lock();
         try {
             return INSTANCE.enricherStatus.getEnricherStatus();
@@ -197,7 +200,7 @@ public class InferenceConfig {
         }
     }
 
-    public String getEnricherStatusMapping(){
+    public String getEnricherStatusMapping() {
         lock.readLock().lock();
         try {
             return INSTANCE.enricherStatus.getEnricherStatusJsonMapping();
@@ -206,11 +209,32 @@ public class InferenceConfig {
         }
     }
 
+    public String getInferenceConfigNodeState() {
+        if (nodeStore != null) {
+            NodeState ns = nodeStore.getRoot();
+            for (String elem : PathUtils.elements(inferenceConfigPath)) {
+                ns = ns.getChildNode(elem);
+            }
+            if (!ns.exists()) {
+                LOG.warn("InferenceConfig: NodeState does not exist for path: 
" + inferenceConfigPath);
+                return "{}";
+            }
+            try {
+                return JsonUtils.nodeStateToJson(ns, 5);
+            } catch (JsonProcessingException e) {
+                throw new RuntimeException(e);
+            }
+        } else {
+            LOG.warn("InferenceConfig: NodeStore is null");
+            return "{}";
+        }
+    }
+
     private @NotNull Map<String, InferenceIndexConfig> getIndexConfigs() {
         lock.readLock().lock();
         try {
             return isEnabled() ?
-                    Collections.unmodifiableMap(indexConfigs) : Map.of();
+                Collections.unmodifiableMap(indexConfigs) : Map.of();
         } finally {
             lock.readLock().unlock();
         }
@@ -241,4 +265,21 @@ public class InferenceConfig {
         return UUID.randomUUID().toString();
     }
 
+    @Override
+    public String toString() {
+        JsopBuilder builder = new JsopBuilder().object().
+            key("type").value(TYPE).
+            key("enabled").value(enabled).
+            key("inferenceConfigPath").value(inferenceConfigPath).
+            key("currentInferenceConfig").value(currentInferenceConfig).
+            key("activeInferenceConfig").value(activeInferenceConfig).
+            key("isInferenceEnabled").value(isInferenceEnabled).
+            key("indexConfigs").object();
+        // Serialize each index config
+        for (Map.Entry<String, InferenceIndexConfig> e : 
indexConfigs.entrySet()) {
+            builder.key(e.getKey()).encodedValue(e.getValue().toString());
+        }
+        builder.endObject().endObject();
+        return JsopBuilder.prettyPrint(builder.toString());
+    }
 } 
\ No newline at end of file
diff --git 
a/oak-search-elastic/src/main/java/org/apache/jackrabbit/oak/plugins/index/elastic/query/inference/InferenceHeaderPayload.java
 
b/oak-search-elastic/src/main/java/org/apache/jackrabbit/oak/plugins/index/elastic/query/inference/InferenceHeaderPayload.java
index 53c387e20d..36e5e8c618 100644
--- 
a/oak-search-elastic/src/main/java/org/apache/jackrabbit/oak/plugins/index/elastic/query/inference/InferenceHeaderPayload.java
+++ 
b/oak-search-elastic/src/main/java/org/apache/jackrabbit/oak/plugins/index/elastic/query/inference/InferenceHeaderPayload.java
@@ -18,6 +18,7 @@
  */
 package org.apache.jackrabbit.oak.plugins.index.elastic.query.inference;
 
+import org.apache.jackrabbit.oak.commons.json.JsopBuilder;
 import org.apache.jackrabbit.oak.json.JsonUtils;
 import 
org.apache.jackrabbit.oak.plugins.index.elastic.util.EnvironmentVariableProcessorUtil;
 import org.apache.jackrabbit.oak.spi.state.NodeState;
@@ -64,7 +65,12 @@ public class InferenceHeaderPayload {
 
     @Override
     public String toString() {
-        return inferenceHeaderPayloadMap.toString();
+        JsopBuilder builder = new JsopBuilder().object();
+        for (Map.Entry<String, String> entry : 
inferenceHeaderPayloadMap.entrySet()) {
+            builder.key(entry.getKey()).value(entry.getValue());
+        }
+        builder.endObject();
+        return JsopBuilder.prettyPrint(builder.toString());
     }
 
 } 
\ No newline at end of file
diff --git 
a/oak-search-elastic/src/main/java/org/apache/jackrabbit/oak/plugins/index/elastic/query/inference/InferenceIndexConfig.java
 
b/oak-search-elastic/src/main/java/org/apache/jackrabbit/oak/plugins/index/elastic/query/inference/InferenceIndexConfig.java
index a7243655ed..5a2b78bb9d 100644
--- 
a/oak-search-elastic/src/main/java/org/apache/jackrabbit/oak/plugins/index/elastic/query/inference/InferenceIndexConfig.java
+++ 
b/oak-search-elastic/src/main/java/org/apache/jackrabbit/oak/plugins/index/elastic/query/inference/InferenceIndexConfig.java
@@ -18,6 +18,7 @@
  */
 package org.apache.jackrabbit.oak.plugins.index.elastic.query.inference;
 
+import org.apache.jackrabbit.oak.commons.json.JsopBuilder;
 import org.apache.jackrabbit.oak.json.JsonUtils;
 import org.apache.jackrabbit.oak.spi.state.NodeState;
 import org.slf4j.Logger;
@@ -79,7 +80,7 @@ public class InferenceIndexConfig {
             this.enricherConfig = getOptionalValue(nodeState, 
InferenceConstants.ENRICHER_CONFIG, DISABLED_ENRICHER_CONFIG);
             inferenceModelConfigs = Map.of();
             LOG.warn("inference index config for indexName: {} is not valid. 
Node: {}",
-                    indexName, nodeState);
+                indexName, nodeState);
         }
     }
 
@@ -108,18 +109,26 @@ public class InferenceIndexConfig {
      */
     public InferenceModelConfig getDefaultEnabledModel() {
         return inferenceModelConfigs.values().stream()
-                .filter(InferenceModelConfig::isDefault)
-                .filter(InferenceModelConfig::isEnabled)
-                .findFirst()
-                .orElse(InferenceModelConfig.NOOP);
+            .filter(InferenceModelConfig::isDefault)
+            .filter(InferenceModelConfig::isEnabled)
+            .findFirst()
+            .orElse(InferenceModelConfig.NOOP);
     }
 
     @Override
     public String toString() {
-        return TYPE + "{" +
-                ENRICHER_CONFIG + "='" + enricherConfig + '\'' +
-                ", " + InferenceModelConfig.TYPE + "=" + inferenceModelConfigs 
+
-                '}';
+        JsopBuilder builder = new JsopBuilder().object().
+            key("type").value(TYPE).
+            key(ENRICHER_CONFIG).value(enricherConfig).
+            key(InferenceConstants.ENABLED).value(isEnabled).
+            key("inferenceModelConfigs").object();
+
+        // Serialize each model config
+        for (Map.Entry<String, InferenceModelConfig> e : 
inferenceModelConfigs.entrySet()) {
+            builder.key(e.getKey()).encodedValue(e.getValue().toString());
+        }
+        builder.endObject().endObject();
+        return JsopBuilder.prettyPrint(builder.toString());
     }
 
 }
\ No newline at end of file
diff --git 
a/oak-search-elastic/src/main/java/org/apache/jackrabbit/oak/plugins/index/elastic/query/inference/InferenceMBeanImpl.java
 
b/oak-search-elastic/src/main/java/org/apache/jackrabbit/oak/plugins/index/elastic/query/inference/InferenceMBeanImpl.java
new file mode 100644
index 0000000000..bfd9f5f6fc
--- /dev/null
+++ 
b/oak-search-elastic/src/main/java/org/apache/jackrabbit/oak/plugins/index/elastic/query/inference/InferenceMBeanImpl.java
@@ -0,0 +1,49 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.jackrabbit.oak.plugins.index.elastic.query.inference;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import org.apache.jackrabbit.oak.api.jmx.InferenceMBean;
+import org.apache.jackrabbit.oak.commons.jmx.AnnotatedStandardMBean;
+import 
org.apache.jackrabbit.oak.plugins.index.elastic.ElasticIndexProviderService;
+import org.jetbrains.annotations.NotNull;
+
+import java.util.Objects;
+
+/**
+ * An MBean that provides the inference configuration.
+ */
+public class InferenceMBeanImpl extends AnnotatedStandardMBean implements 
InferenceMBean {
+    private static final ObjectMapper MAPPER = new ObjectMapper();
+
+    public InferenceMBeanImpl() {
+        super(InferenceMBean.class);
+    }
+
+    @Override
+    public String getConfigJson() {
+        return InferenceConfig.getInstance().toString();
+    }
+
+    @Override
+    public String getConfigNodeStateJson() {
+        return InferenceConfig.getInstance().getInferenceConfigNodeState();
+    }
+}
diff --git 
a/oak-search-elastic/src/main/java/org/apache/jackrabbit/oak/plugins/index/elastic/query/inference/InferenceModelConfig.java
 
b/oak-search-elastic/src/main/java/org/apache/jackrabbit/oak/plugins/index/elastic/query/inference/InferenceModelConfig.java
index b4f79d0a0f..04a825acd8 100644
--- 
a/oak-search-elastic/src/main/java/org/apache/jackrabbit/oak/plugins/index/elastic/query/inference/InferenceModelConfig.java
+++ 
b/oak-search-elastic/src/main/java/org/apache/jackrabbit/oak/plugins/index/elastic/query/inference/InferenceModelConfig.java
@@ -18,6 +18,7 @@
  */
 package org.apache.jackrabbit.oak.plugins.index.elastic.query.inference;
 
+import org.apache.jackrabbit.oak.commons.json.JsopBuilder;
 import 
org.apache.jackrabbit.oak.plugins.index.elastic.util.EnvironmentVariableProcessorUtil;
 import org.apache.jackrabbit.oak.spi.query.fulltext.VectorQueryConfig;
 import org.apache.jackrabbit.oak.spi.state.NodeState;
@@ -102,7 +103,7 @@ public class InferenceModelConfig {
         this.isDefault = getOptionalValue(nodeState, IS_DEFAULT, false);
         this.model = getOptionalValue(nodeState, MODEL, "");
         this.embeddingServiceUrl = 
EnvironmentVariableProcessorUtil.processEnvironmentVariable(
-                InferenceConstants.INFERENCE_ENVIRONMENT_VARIABLE_PREFIX, 
getOptionalValue(nodeState, EMBEDDING_SERVICE_URL, ""), 
DEFAULT_ENVIRONMENT_VARIABLE_VALUE);
+            InferenceConstants.INFERENCE_ENVIRONMENT_VARIABLE_PREFIX, 
getOptionalValue(nodeState, EMBEDDING_SERVICE_URL, ""), 
DEFAULT_ENVIRONMENT_VARIABLE_VALUE);
         this.similarityThreshold = getOptionalValue(nodeState, 
SIMILARITY_THRESHOLD, DEFAULT_SIMILARITY_THRESHOLD);
         this.minTerms = getOptionalValue(nodeState, MIN_TERMS, 
DEFAULT_MIN_TERMS);
         this.timeout = getOptionalValue(nodeState, TIMEOUT, 
DEFAULT_TIMEOUT_MILLIS);
@@ -112,18 +113,21 @@ public class InferenceModelConfig {
 
     @Override
     public String toString() {
-        return TYPE + "{" +
-                MODEL + "='" + model + '\'' +
-                ", " + EMBEDDING_SERVICE_URL + "='" + embeddingServiceUrl + 
'\'' +
-                ", " + SIMILARITY_THRESHOLD + similarityThreshold +
-                ", " + MIN_TERMS + "=" + minTerms +
-                ", " + IS_DEFAULT + "=" + isDefault +
-                ", " + ENABLED + "=" + enabled +
-                ", " + HEADER + "=" + header +
-                ", " + INFERENCE_PAYLOAD + "=" + payload +
-                ", " + TIMEOUT + "=" + timeout +
-                ", " + NUM_CANDIDATES + "=" + numCandidates +
-                "}";
+        JsopBuilder builder = new JsopBuilder().object().
+            key("type").value(TYPE).
+            key(MODEL).value(model).
+            key(EMBEDDING_SERVICE_URL).value(embeddingServiceUrl).
+            key(SIMILARITY_THRESHOLD).encodedValue("" + similarityThreshold).
+            key(MIN_TERMS).value(minTerms).
+            key(IS_DEFAULT).value(isDefault).
+            key(ENABLED).value(enabled).
+            key(HEADER).encodedValue(header.toString()).
+            key(INFERENCE_PAYLOAD).encodedValue(payload.toString()).
+            key(TIMEOUT).value(timeout).
+            key(NUM_CANDIDATES).value(numCandidates).
+            key(CACHE_SIZE).value(cacheSize);
+        builder.endObject();
+        return JsopBuilder.prettyPrint(builder.toString());
     }
 
     public String getInferenceModelConfigName() {
diff --git 
a/oak-search-elastic/src/main/java/org/apache/jackrabbit/oak/plugins/index/elastic/query/inference/InferencePayload.java
 
b/oak-search-elastic/src/main/java/org/apache/jackrabbit/oak/plugins/index/elastic/query/inference/InferencePayload.java
index ac230014a9..93a6065581 100644
--- 
a/oak-search-elastic/src/main/java/org/apache/jackrabbit/oak/plugins/index/elastic/query/inference/InferencePayload.java
+++ 
b/oak-search-elastic/src/main/java/org/apache/jackrabbit/oak/plugins/index/elastic/query/inference/InferencePayload.java
@@ -20,6 +20,7 @@ package 
org.apache.jackrabbit.oak.plugins.index.elastic.query.inference;
 
 import com.fasterxml.jackson.core.JsonProcessingException;
 import com.fasterxml.jackson.databind.ObjectMapper;
+import org.apache.jackrabbit.oak.commons.json.JsopBuilder;
 import org.apache.jackrabbit.oak.json.JsonUtils;
 import 
org.apache.jackrabbit.oak.plugins.index.elastic.util.EnvironmentVariableProcessorUtil;
 import org.apache.jackrabbit.oak.spi.state.NodeState;
@@ -58,6 +59,7 @@ public class InferencePayload {
         //replace current keys with swapped
         inferencePayloadMap.putAll(swappedEnvVarsMap);
     }
+
     /*
      * Get the inference payload as a json string
      *
@@ -76,4 +78,23 @@ public class InferencePayload {
         }
     }
 
+    @Override
+    public String toString() {
+        JsopBuilder builder = new JsopBuilder().object();
+        for (Map.Entry<String, Object> entry : inferencePayloadMap.entrySet()) 
{
+            builder.key(entry.getKey());
+            if (entry.getValue() instanceof String) {
+                builder.value((String) entry.getValue());
+            } else {
+                try {
+                    
builder.encodedValue(objectMapper.writeValueAsString(entry.getValue()));
+                } catch (JsonProcessingException e) {
+                    LOG.warn("Failed to serialize value for key {}: {}", 
entry.getKey(), e.getMessage());
+                    builder.value(entry.getValue().toString());
+                }
+            }
+        }
+        builder.endObject();
+        return JsopBuilder.prettyPrint(builder.toString());
+    }
 } 
\ No newline at end of file
diff --git 
a/oak-search-elastic/src/test/java/org/apache/jackrabbit/oak/plugins/index/elastic/query/inference/InferenceConfigSerializationTest.java
 
b/oak-search-elastic/src/test/java/org/apache/jackrabbit/oak/plugins/index/elastic/query/inference/InferenceConfigSerializationTest.java
new file mode 100644
index 0000000000..e68bd5f81c
--- /dev/null
+++ 
b/oak-search-elastic/src/test/java/org/apache/jackrabbit/oak/plugins/index/elastic/query/inference/InferenceConfigSerializationTest.java
@@ -0,0 +1,301 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.jackrabbit.oak.plugins.index.elastic.query.inference;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import org.apache.jackrabbit.oak.plugins.memory.EmptyNodeState;
+import org.apache.jackrabbit.oak.plugins.memory.MemoryNodeBuilder;
+import org.apache.jackrabbit.oak.plugins.memory.MemoryNodeStore;
+import org.apache.jackrabbit.oak.spi.commit.CommitInfo;
+import org.apache.jackrabbit.oak.spi.commit.EmptyHook;
+import org.apache.jackrabbit.oak.spi.state.NodeBuilder;
+import org.apache.jackrabbit.oak.spi.state.NodeStore;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+/**
+ * Tests for the toString() methods of the inference-related classes which use 
JsopBuilder
+ */
+public class InferenceConfigSerializationTest {
+
+    private static final ObjectMapper MAPPER = new ObjectMapper();
+    private static final String DEFAULT_CONFIG_PATH = 
InferenceConstants.DEFAULT_OAK_INDEX_INFERENCE_CONFIG_PATH;
+    private static final String ENRICHER_CONFIG = 
"{\"enricher\":{\"config\":{\"vectorSpaces\":{\"semantic\":{\"pipeline\":{\"steps\":[{\"inputFields\":{\"description\":\"STRING\",\"title\":\"STRING\"},\"chunkingConfig\":{\"enabled\":true},\"name\":\"sentence-embeddings\",\"model\":\"text-embedding-ada-002\",\"optional\":true,\"type\":\"embeddings\"}]},\"default\":false}},\"version\":\"0.0.1\"}}}";
+    private static final String DEFAULT_ENRICHER_STATUS_MAPPING = 
"{\"properties\":{\"processingTimeMs\":{\"type\":\"date\"},\"latestError\":{\"type\":\"keyword\",\"index\":false},\"errorCount\":{\"type\":\"short\"},\"status\":{\"type\":\"keyword\"}}}";
+    private static final String DEFAULT_ENRICHER_STATUS_DATA = 
"{\"processingTimeMs\":0,\"latestError\":\"\",\"errorCount\":0,\"status\":\"PENDING\"}";
+
+    private NodeBuilder rootBuilder;
+    private NodeStore nodeStore;
+
+    @Before
+    public void setup() {
+        // Initialize memory node store
+        rootBuilder = new MemoryNodeBuilder(EmptyNodeState.EMPTY_NODE);
+        nodeStore = new MemoryNodeStore(rootBuilder.getNodeState());
+    }
+
+    @After
+    public void tearDown() {
+        rootBuilder = null;
+        nodeStore = null;
+    }
+
+    /**
+     * Test for InferenceConfig.toString()
+     */
+    @Test
+    public void testInferenceConfigToString() throws Exception {
+        // Setup: Create a basic inference config
+        NodeBuilder inferenceConfigBuilder = createNodePath(rootBuilder, 
DEFAULT_CONFIG_PATH);
+        inferenceConfigBuilder.setProperty(InferenceConstants.TYPE, 
InferenceConfig.TYPE);
+        inferenceConfigBuilder.setProperty(InferenceConstants.ENABLED, true);
+
+        // Add index config
+        String indexName = "testIndex";
+        NodeBuilder indexConfigBuilder = 
inferenceConfigBuilder.child(indexName);
+        indexConfigBuilder.setProperty(InferenceConstants.TYPE, 
InferenceIndexConfig.TYPE);
+        indexConfigBuilder.setProperty(InferenceConstants.ENABLED, true);
+        indexConfigBuilder.setProperty(InferenceConstants.ENRICHER_CONFIG, 
ENRICHER_CONFIG);
+
+        // Commit the changes
+        nodeStore.merge(rootBuilder, EmptyHook.INSTANCE, CommitInfo.EMPTY);
+
+        // Initialize the inference config
+        InferenceConfig.reInitialize(nodeStore, DEFAULT_CONFIG_PATH, true);
+        InferenceConfig inferenceConfig = InferenceConfig.getInstance();
+
+        // Get the toString representation
+        String json = inferenceConfig.toString();
+
+        // Verify it's valid JSON 
+        JsonNode node = MAPPER.readTree(json);
+
+        // Verify the structure
+        assertTrue("JSON should contain 'type' key", node.has("type"));
+        assertEquals("Type should be inferenceConfig", InferenceConfig.TYPE, 
node.get("type").asText());
+        assertTrue("JSON should contain 'enabled' key", node.has("enabled"));
+        assertTrue("enabled should be true", node.get("enabled").asBoolean());
+        assertTrue("JSON should contain 'indexConfigs' key", 
node.has("indexConfigs"));
+        assertTrue("indexConfigs should be an object", 
node.get("indexConfigs").isObject());
+        assertTrue("indexConfigs should contain testIndex", 
node.get("indexConfigs").has(indexName));
+    }
+
+    /**
+     * Test for InferenceIndexConfig.toString()
+     */
+    @Test
+    public void testInferenceIndexConfigToString() throws Exception {
+        // Create a simple index config
+        NodeBuilder indexConfigBuilder = rootBuilder.child("testIndex");
+        indexConfigBuilder.setProperty(InferenceConstants.TYPE, 
InferenceIndexConfig.TYPE);
+        indexConfigBuilder.setProperty(InferenceConstants.ENABLED, true);
+        indexConfigBuilder.setProperty(InferenceConstants.ENRICHER_CONFIG, 
ENRICHER_CONFIG);
+
+        // Create the index config object
+        InferenceIndexConfig indexConfig = new 
InferenceIndexConfig("testIndex", indexConfigBuilder.getNodeState());
+
+        // Get the toString representation
+        String json = indexConfig.toString();
+
+        // Verify it's valid JSON
+        JsonNode node = MAPPER.readTree(json);
+
+        // Verify the structure
+        assertTrue("JSON should contain 'type' key", node.has("type"));
+        assertEquals("Type should be inferenceIndexConfig", 
InferenceIndexConfig.TYPE, node.get("type").asText());
+        assertTrue("JSON should contain 'enricherConfig' key", 
node.has(InferenceIndexConfig.ENRICHER_CONFIG));
+        assertEquals("Enricher config should match", ENRICHER_CONFIG, 
node.get(InferenceIndexConfig.ENRICHER_CONFIG).asText());
+        assertTrue("JSON should contain 'enabled' key", 
node.has(InferenceConstants.ENABLED));
+        assertTrue("enabled should be true", 
node.get(InferenceConstants.ENABLED).asBoolean());
+        assertTrue("JSON should contain 'inferenceModelConfigs' key", 
node.has("inferenceModelConfigs"));
+        assertTrue("inferenceModelConfigs should be an object", 
node.get("inferenceModelConfigs").isObject());
+    }
+
+    /**
+     * Test for InferenceModelConfig.toString()
+     */
+    @Test
+    public void testInferenceModelConfigToString() throws Exception {
+        // Create a model config with header and payload
+        NodeBuilder modelConfigBuilder = rootBuilder.child("testModel");
+        modelConfigBuilder.setProperty(InferenceConstants.TYPE, 
InferenceModelConfig.TYPE);
+        modelConfigBuilder.setProperty(InferenceConstants.ENABLED, true);
+        modelConfigBuilder.setProperty(InferenceModelConfig.IS_DEFAULT, true);
+        modelConfigBuilder.setProperty(InferenceModelConfig.MODEL, 
"test-model");
+        
modelConfigBuilder.setProperty(InferenceModelConfig.EMBEDDING_SERVICE_URL, 
"http://test-service";);
+        
modelConfigBuilder.setProperty(InferenceModelConfig.SIMILARITY_THRESHOLD, 0.85);
+        modelConfigBuilder.setProperty(InferenceModelConfig.MIN_TERMS, 3);
+        modelConfigBuilder.setProperty(InferenceModelConfig.TIMEOUT, 10000);
+        modelConfigBuilder.setProperty(InferenceModelConfig.NUM_CANDIDATES, 
50);
+        modelConfigBuilder.setProperty(InferenceModelConfig.CACHE_SIZE, 200);
+
+        // Create header node
+        NodeBuilder headerBuilder = 
modelConfigBuilder.child(InferenceModelConfig.HEADER);
+        headerBuilder.setProperty("Authorization", "Bearer test-token");
+        headerBuilder.setProperty("Content-Type", "application/json");
+
+        // Create payload node
+        NodeBuilder payloadBuilder = 
modelConfigBuilder.child(InferenceModelConfig.INFERENCE_PAYLOAD);
+        payloadBuilder.setProperty("model", "text-embedding-ada-002");
+        payloadBuilder.setProperty("dimensions", 1536);
+
+        // Create the model config object
+        InferenceModelConfig modelConfig = new 
InferenceModelConfig("testModel", modelConfigBuilder.getNodeState());
+
+        // Get the toString representation
+        String json = modelConfig.toString();
+
+        // Verify it's valid JSON
+        JsonNode node = MAPPER.readTree(json);
+
+        // Verify structure
+        assertTrue("JSON should contain 'TYPE' key", node.has("type"));
+        assertEquals("Type should match", InferenceModelConfig.TYPE, 
node.get("type").asText());
+        assertTrue("JSON should contain 'model' key", 
node.has(InferenceModelConfig.MODEL));
+        assertEquals("Model should match", "test-model", 
node.get(InferenceModelConfig.MODEL).asText());
+        assertTrue("JSON should contain 'embeddingServiceUrl' key", 
node.has(InferenceModelConfig.EMBEDDING_SERVICE_URL));
+        assertEquals("Service URL should match", "http://test-service";, 
node.get(InferenceModelConfig.EMBEDDING_SERVICE_URL).asText());
+        assertTrue("JSON should contain 'similarityThreshold' key", 
node.has(InferenceModelConfig.SIMILARITY_THRESHOLD));
+        assertEquals("Similarity threshold should match", 0.85, 
node.get(InferenceModelConfig.SIMILARITY_THRESHOLD).asDouble(), 0.001);
+        assertTrue("JSON should contain 'minTerms' key", 
node.has(InferenceModelConfig.MIN_TERMS));
+        assertEquals("Min terms should match", 3, 
node.get(InferenceModelConfig.MIN_TERMS).asInt());
+        assertTrue("JSON should contain 'isDefault' key", 
node.has(InferenceModelConfig.IS_DEFAULT));
+        assertTrue("isDefault should be true", 
node.get(InferenceModelConfig.IS_DEFAULT).asBoolean());
+        assertTrue("JSON should contain 'enabled' key", 
node.has(InferenceModelConfig.ENABLED));
+        assertTrue("enabled should be true", 
node.get(InferenceModelConfig.ENABLED).asBoolean());
+        assertTrue("JSON should contain 'header' key", 
node.has(InferenceModelConfig.HEADER));
+        assertTrue("JSON should contain 'inferencePayload' key", 
node.has(InferenceModelConfig.INFERENCE_PAYLOAD));
+        assertTrue("JSON should contain 'timeout' key", 
node.has(InferenceModelConfig.TIMEOUT));
+        assertEquals("Timeout should match", 10000, 
node.get(InferenceModelConfig.TIMEOUT).asInt());
+        assertTrue("JSON should contain 'numCandidates' key", 
node.has(InferenceModelConfig.NUM_CANDIDATES));
+        assertEquals("Num candidates should match", 50, 
node.get(InferenceModelConfig.NUM_CANDIDATES).asInt());
+        assertTrue("JSON should contain 'cacheSize' key", 
node.has(InferenceModelConfig.CACHE_SIZE));
+        assertEquals("Cache size should match", 200, 
node.get(InferenceModelConfig.CACHE_SIZE).asInt());
+    }
+
+    /**
+     * Test for InferenceHeaderPayload.toString()
+     */
+    @Test
+    public void testInferenceHeaderPayloadToString() throws Exception {
+        // Create a header payload
+        NodeBuilder headerBuilder = rootBuilder.child("header");
+        headerBuilder.setProperty("Authorization", "Bearer test-token");
+        headerBuilder.setProperty("Content-Type", "application/json");
+
+        // Create the header payload object
+        InferenceHeaderPayload headerPayload = new 
InferenceHeaderPayload(headerBuilder.getNodeState());
+
+        // Get the toString representation
+        String json = headerPayload.toString();
+
+        // Verify it's valid JSON
+        JsonNode node = MAPPER.readTree(json);
+
+        // Verify structure
+        assertTrue("JSON should contain Authorization", 
node.has("Authorization"));
+        assertEquals("Authorization should match", "Bearer test-token", 
node.get("Authorization").asText());
+        assertTrue("JSON should contain Content-Type", 
node.has("Content-Type"));
+        assertEquals("Content-Type should match", "application/json", 
node.get("Content-Type").asText());
+    }
+
+    /**
+     * Test for InferencePayload.toString()
+     */
+    @Test
+    public void testInferencePayloadToString() throws Exception {
+        // Create a payload
+        NodeBuilder payloadBuilder = rootBuilder.child("payload");
+        payloadBuilder.setProperty("model", "text-embedding-ada-002");
+        payloadBuilder.setProperty("dimensions", 1536);
+
+        // Create the payload object
+        InferencePayload payload = new InferencePayload("testModel", 
payloadBuilder.getNodeState());
+
+        // Get the toString representation
+        String json = payload.toString();
+
+        // Verify it's valid JSON
+        JsonNode node = MAPPER.readTree(json);
+
+        // Verify structure
+        assertTrue("JSON should contain model", node.has("model"));
+        assertEquals("Model should match", "text-embedding-ada-002", 
node.get("model").asText());
+        assertTrue("JSON should contain dimensions", node.has("dimensions"));
+        assertEquals("Dimensions should match", 1536, 
node.get("dimensions").asInt());
+    }
+
+    /**
+     * Test for EnricherStatus.toString()
+     */
+    @Test
+    public void testEnricherStatusToString() throws Exception {
+        // Setup: Create a node structure with enricher status data
+        NodeBuilder inferenceConfigBuilder = createNodePath(rootBuilder, 
DEFAULT_CONFIG_PATH);
+        NodeBuilder enrichNode = 
inferenceConfigBuilder.child(InferenceConstants.ENRICH_NODE);
+        enrichNode.setProperty(InferenceConstants.ENRICHER_STATUS_MAPPING, 
DEFAULT_ENRICHER_STATUS_MAPPING);
+        enrichNode.setProperty(InferenceConstants.ENRICHER_STATUS_DATA, 
DEFAULT_ENRICHER_STATUS_DATA);
+
+        // Commit the changes
+        nodeStore.merge(rootBuilder, EmptyHook.INSTANCE, CommitInfo.EMPTY);
+
+        // Create the enricher status object
+        EnricherStatus status = new EnricherStatus(nodeStore, 
DEFAULT_CONFIG_PATH);
+
+        // Get the toString representation
+        String json = status.toString();
+
+        // Verify it's valid JSON
+        JsonNode node = MAPPER.readTree(json);
+
+        // Verify structure
+        assertTrue("JSON should contain enricherStatusJsonMapping", 
node.has("enricherStatusJsonMapping"));
+        JsonNode mappingNode = 
MAPPER.readTree(node.get("enricherStatusJsonMapping").asText());
+        assertTrue("Mapping should contain properties", 
mappingNode.has("properties"));
+
+        assertTrue("JSON should contain enricherStatusData", 
node.has("enricherStatusData"));
+        JsonNode statusData = node.get("enricherStatusData");
+        assertTrue("Status data should contain processingTimeMs", 
statusData.has("processingTimeMs"));
+        assertEquals("Processing time should be 0", 0, 
statusData.get("processingTimeMs").asInt());
+        assertTrue("Status data should contain status", 
statusData.has("status"));
+        assertEquals("Status should be PENDING", "PENDING", 
statusData.get("status").asText());
+        assertTrue("Status data should contain errorCount", 
statusData.has("errorCount"));
+        assertEquals("Error count should be 0", 0, 
statusData.get("errorCount").asInt());
+        assertTrue("Status data should contain latestError", 
statusData.has("latestError"));
+        assertEquals("Latest error should be empty", "", 
statusData.get("latestError").asText());
+    }
+
+    /**
+     * Helper method to create node paths
+     */
+    private NodeBuilder createNodePath(NodeBuilder rootBuilder, String path) {
+        NodeBuilder currentBuilder = rootBuilder;
+        for (String element : path.split("/")) {
+            if (!element.isEmpty()) {
+                currentBuilder = currentBuilder.child(element);
+            }
+        }
+        return currentBuilder;
+    }
+} 
\ No newline at end of file


Reply via email to