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

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


The following commit(s) were added to refs/heads/trunk by this push:
     new b03171acb6 OAK-11714: Add jmx to expose inferenceConfig (#2290)
b03171acb6 is described below

commit b03171acb632f8fbe6414215d54c1030a3a43db6
Author: Mohit Kataria <mkata...@apache.org>
AuthorDate: Sat May 10 12:00:04 2025 +0530

    OAK-11714: Add jmx to expose inferenceConfig (#2290)
    
    * OAK-11714: Add jmx to expose inferenceConfig
    
    * OAK-11714: added more tests
---
 .../jackrabbit/oak/api/jmx/InferenceMBean.java     |  40 +++
 .../index/elastic/ElasticIndexProviderService.java |  13 +
 .../elastic/query/inference/EnricherStatus.java    |  26 ++
 .../elastic/query/inference/InferenceConfig.java   |  59 +++-
 .../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          | 365 +++++++++++++++++++++
 .../query/inference/InferenceConfigTest.java       | 311 ++++++++++++++++--
 11 files changed, 887 insertions(+), 62 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..21d2c312f7 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(InferenceConstants.ENRICHER_STATUS_MAPPING).value(enricherStatusJsonMapping);
+
+        // Add enricher status data
+        builder.key(InferenceConstants.ENRICHER_STATUS_DATA).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..167dea0f67 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,23 @@ 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();
+        // Serialize enricherStatus
+        
builder.key(":enrich").encodedValue(enricherStatus.toString()).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..503b685ef1
--- /dev/null
+++ 
b/oak-search-elastic/src/test/java/org/apache/jackrabbit/oak/plugins/index/elastic/query/inference/InferenceConfigSerializationTest.java
@@ -0,0 +1,365 @@
+/*
+ * 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 enricherStatusMapping", 
node.has(InferenceConstants.ENRICHER_STATUS_MAPPING));
+        JsonNode mappingNode = 
MAPPER.readTree(node.get(InferenceConstants.ENRICHER_STATUS_MAPPING).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());
+    }
+
+    /**
+     * More comprehensive test for InferenceConfig.toString() to verify all 
fields
+     */
+    @Test
+    public void testComprehensiveInferenceConfigToString() 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);
+
+        // Add enricher status
+        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);
+
+        // 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 includes all fields from the toString method
+        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 'inferenceConfigPath' key", 
node.has("inferenceConfigPath"));
+        assertEquals("inferenceConfigPath should match", DEFAULT_CONFIG_PATH, 
node.get("inferenceConfigPath").asText());
+
+        assertTrue("JSON should contain 'currentInferenceConfig' key", 
node.has("currentInferenceConfig"));
+        assertTrue("currentInferenceConfig should not be empty", 
!node.get("currentInferenceConfig").asText().isEmpty());
+
+        assertTrue("JSON should contain 'activeInferenceConfig' key", 
node.has("activeInferenceConfig"));
+        assertTrue("activeInferenceConfig should not be empty", 
!node.get("activeInferenceConfig").asText().isEmpty());
+
+        assertTrue("JSON should contain 'isInferenceEnabled' key", 
node.has("isInferenceEnabled"));
+        assertTrue("isInferenceEnabled should be true", 
node.get("isInferenceEnabled").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));
+
+        assertTrue("JSON should contain ':enrich' key", node.has(":enrich"));
+        JsonNode enrichNode2 = node.get(":enrich");
+        assertTrue("enrichNode should contain 'enricherStatusMapping'", 
enrichNode2.has(InferenceConstants.ENRICHER_STATUS_MAPPING));
+        assertTrue("enrichNode should contain 'enricherStatusData'", 
enrichNode2.has(InferenceConstants.ENRICHER_STATUS_DATA));
+    }
+
+    /**
+     * 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
diff --git 
a/oak-search-elastic/src/test/java/org/apache/jackrabbit/oak/plugins/index/elastic/query/inference/InferenceConfigTest.java
 
b/oak-search-elastic/src/test/java/org/apache/jackrabbit/oak/plugins/index/elastic/query/inference/InferenceConfigTest.java
index 790426c10a..0af97b2428 100644
--- 
a/oak-search-elastic/src/test/java/org/apache/jackrabbit/oak/plugins/index/elastic/query/inference/InferenceConfigTest.java
+++ 
b/oak-search-elastic/src/test/java/org/apache/jackrabbit/oak/plugins/index/elastic/query/inference/InferenceConfigTest.java
@@ -19,6 +19,8 @@
 package org.apache.jackrabbit.oak.plugins.index.elastic.query.inference;
 
 import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
 import joptsimple.internal.Strings;
 import org.apache.jackrabbit.oak.api.CommitFailedException;
 import org.apache.jackrabbit.oak.commons.PathUtils;
@@ -36,6 +38,7 @@ import org.junit.Test;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
+import java.io.IOException;
 import java.util.Map;
 
 import static org.junit.Assert.assertEquals;
@@ -87,7 +90,7 @@ public class InferenceConfigTest {
     }
 
     /**
-     * Test 1: Basic test - Disabled InferenceConfig
+     * Basic test - Disabled InferenceConfig
      * Verifies that when inference config is created but disabled, the 
InferenceConfig object reflects this state
      */
     @Test
@@ -109,7 +112,7 @@ public class InferenceConfigTest {
     }
 
     /**
-     * Test 2: Enabled InferenceConfig but no index configs
+     * Enabled InferenceConfig but no index configs
      * Verifies that when an empty inference config is enabled, the 
InferenceConfig object reflects this state
      */
     @Test
@@ -131,7 +134,7 @@ public class InferenceConfigTest {
     }
 
     /**
-     * Test 3: Basic InferenceIndexConfig creation
+     * Basic InferenceIndexConfig creation
      * Tests the creation of a simple InferenceIndexConfig within 
InferenceConfig
      */
     @Test
@@ -167,7 +170,7 @@ public class InferenceConfigTest {
     }
 
     /**
-     * Test 4: Disabled InferenceIndexConfig
+     * Disabled InferenceIndexConfig
      * Tests that a disabled InferenceIndexConfig is properly handled
      */
     @Test
@@ -201,7 +204,7 @@ public class InferenceConfigTest {
     }
 
     /**
-     * Test 5: Invalid InferenceIndexConfig (missing type)
+     * Invalid InferenceIndexConfig (missing type)
      * Tests that an invalid InferenceIndexConfig (missing type) is properly 
handled
      */
     @Test
@@ -234,7 +237,7 @@ public class InferenceConfigTest {
     }
 
     /**
-     * Test 6: Basic InferenceModelConfig
+     * Basic InferenceModelConfig
      * Tests the creation of an InferenceModelConfig within an 
InferenceIndexConfig
      */
     @Test
@@ -287,7 +290,7 @@ public class InferenceConfigTest {
     }
 
     /**
-     * Test 7: Multiple InferenceModelConfigs with one default
+     * Multiple InferenceModelConfigs with one default
      * Tests multiple InferenceModelConfigs within an InferenceIndexConfig, 
with one marked as default
      */
     @Test
@@ -353,7 +356,7 @@ public class InferenceConfigTest {
     }
 
     /**
-     * Test 8: Test EnricherStatus JSON Mapping
+     * Test EnricherStatus JSON Mapping
      * Tests that the EnricherStatus JSON mapping is properly stored and 
retrieved
      */
     @Test
@@ -384,7 +387,7 @@ public class InferenceConfigTest {
     }
 
     /**
-     * Test 9: Test Complete Integration with EnricherStatus
+     * Test Complete Integration with EnricherStatus
      * Tests the complete integration of InferenceConfig, 
InferenceIndexConfig, InferenceModelConfig, and EnricherStatus
      */
     @Test
@@ -470,18 +473,7 @@ public class InferenceConfigTest {
     }
 
     /**
-     * Utility method to verify enricher status fields
-     */
-    private void verifyEnricherStatusFields(Map<String, Object> status, String 
expectedStatus,
-                                            int expectedProcessingTime, String 
expectedError, int expectedErrorCount) {
-        assertEquals("Status should match", expectedStatus, 
status.get("status"));
-        assertEquals("Processing time should match", expectedProcessingTime, 
status.get("processingTimeMs"));
-        assertEquals("Latest error should match", expectedError, 
status.get("latestError"));
-        assertEquals("Error count should match", expectedErrorCount, 
status.get("errorCount"));
-    }
-
-    /**
-     * Test 10: Test EnricherStatus
+     * Test EnricherStatus
      * Tests that the EnricherStatus is properly loaded from the inference 
config
      */
     @Test
@@ -519,7 +511,29 @@ public class InferenceConfigTest {
     }
 
     /**
-     * Test 11: Test EnricherStatus Refresh
+     * Utility method to verify enricher status fields
+     */
+    private void verifyEnricherStatusFields(Map<String, Object> status, String 
expectedStatus,
+                                            int expectedProcessingTime, String 
expectedError, int expectedErrorCount) {
+        assertEquals("Status should match", expectedStatus, 
status.get("status"));
+        assertEquals("Processing time should match", expectedProcessingTime, 
status.get("processingTimeMs"));
+        assertEquals("Latest error should match", expectedError, 
status.get("latestError"));
+        assertEquals("Error count should match", expectedErrorCount, 
status.get("errorCount"));
+    }
+
+    /**
+     * Utility method to create a path of nodes
+     */
+    private NodeBuilder createNodePath(NodeBuilder rootBuilder, String path) {
+        NodeBuilder builder = rootBuilder;
+        for (String elem : PathUtils.elements(path)) {
+            builder = builder.child(elem);
+        }
+        return builder;
+    }
+
+    /**
+     * Test EnricherStatus Refresh
      * Tests that the EnricherStatus is properly refreshed when the inference 
config is updated
      */
     @Test
@@ -571,7 +585,7 @@ public class InferenceConfigTest {
     }
 
     /**
-     * Test 12: Test EnricherStatus with Error Information
+     * Test EnricherStatus with Error Information
      * Tests that the EnricherStatus properly handles error information
      */
     @Test
@@ -603,7 +617,7 @@ public class InferenceConfigTest {
     }
 
     /**
-     * Test 13: Test Complete Configuration with Multiple Indexes and Models 
including EnricherStatus
+     * Test Complete Configuration with Multiple Indexes and Models including 
EnricherStatus
      * Tests a complex configuration with multiple indexes, models, and 
enricher status
      */
     @Test
@@ -738,13 +752,248 @@ public class InferenceConfigTest {
     }
 
     /**
-     * Utility method to create a path of nodes
+     * Test getInferenceConfigNodeState
+     * Comprehensively tests the getInferenceConfigNodeState method's 
functionality
+     * including normal operation, handling of non-existent paths, and complex 
JSON structures
      */
-    private NodeBuilder createNodePath(NodeBuilder rootBuilder, String path) {
-        NodeBuilder builder = rootBuilder;
-        for (String elem : PathUtils.elements(path)) {
-            builder = builder.child(elem);
-        }
-        return builder;
+    @Test
+    public void testGetInferenceConfigNodeState() throws 
CommitFailedException, IOException {
+        // Part 1: Test with a complete configuration (happy path)
+        // ----------------------------------------------------------
+        // Create enabled inference config with complete configuration
+        NodeBuilder inferenceConfigBuilder = createNodePath(rootBuilder, 
DEFAULT_CONFIG_PATH);
+        inferenceConfigBuilder.setProperty(InferenceConstants.TYPE, 
InferenceConfig.TYPE);
+        inferenceConfigBuilder.setProperty(InferenceConstants.ENABLED, true);
+
+        // Add custom property to verify in JSON output
+        inferenceConfigBuilder.setProperty("customProperty", "customValue");
+
+        // Add enricher status node
+        NodeBuilder enrichBuilder = 
inferenceConfigBuilder.child(InferenceConstants.ENRICH_NODE);
+        enrichBuilder.setProperty(InferenceConstants.ENRICHER_STATUS_MAPPING, 
defaultEnricherStatusMapping);
+        enrichBuilder.setProperty(InferenceConstants.ENRICHER_STATUS_DATA, 
defaultEnricherStatusData);
+
+        // Add index config
+        String indexName = "testJsonIndex";
+        NodeBuilder indexConfigBuilder = 
inferenceConfigBuilder.child(indexName);
+        indexConfigBuilder.setProperty(InferenceConstants.TYPE, 
InferenceIndexConfig.TYPE);
+        indexConfigBuilder.setProperty(InferenceConstants.ENABLED, true);
+        indexConfigBuilder.setProperty(InferenceConstants.ENRICHER_CONFIG, 
ENRICHER_CONFIG);
+
+        // Add model config
+        String modelName = "testJsonModel";
+        NodeBuilder modelConfigBuilder = indexConfigBuilder.child(modelName);
+        modelConfigBuilder.setProperty(InferenceConstants.TYPE, 
InferenceModelConfig.TYPE);
+        modelConfigBuilder.setProperty(InferenceConstants.ENABLED, true);
+        modelConfigBuilder.setProperty(InferenceModelConfig.IS_DEFAULT, true);
+        modelConfigBuilder.setProperty(InferenceModelConfig.MODEL, 
"json-model");
+        
modelConfigBuilder.setProperty(InferenceModelConfig.EMBEDDING_SERVICE_URL, 
"http://localhost:8080/test";);
+        
modelConfigBuilder.setProperty(InferenceModelConfig.SIMILARITY_THRESHOLD, 0.8);
+        modelConfigBuilder.setProperty(InferenceModelConfig.MIN_TERMS, 3L);
+
+        // Add complex structure with various data types and special characters
+        NodeBuilder complexBuilder = 
inferenceConfigBuilder.child("complexNode");
+        complexBuilder.setProperty("string", "simple string value");
+        complexBuilder.setProperty("boolean", true);
+        complexBuilder.setProperty("number", 12345);
+        complexBuilder.setProperty("special", 
"test\"with\\quotes\nand\tnewlines");
+        complexBuilder.setProperty("unicode", "测试unicode字符");
+
+        // Add a child node to test nesting
+        NodeBuilder childBuilder = complexBuilder.child("childNode");
+        childBuilder.setProperty("childProp", "child value");
+
+        // Commit the changes
+        nodeStore.merge(rootBuilder, EmptyHook.INSTANCE, CommitInfo.EMPTY);
+
+        // Initialize InferenceConfig object
+        InferenceConfig.reInitialize(nodeStore, DEFAULT_CONFIG_PATH, true);
+        InferenceConfig inferenceConfig = InferenceConfig.getInstance();
+
+        // Get the node state as JSON
+        String nodeStateJson = inferenceConfig.getInferenceConfigNodeState();
+
+        // Parse the JSON
+        ObjectMapper mapper = new ObjectMapper();
+        JsonNode rootNode = mapper.readTree(nodeStateJson);
+
+        // Validate the complete config JSON
+        assertNotNull("Node state JSON should not be null", nodeStateJson);
+        assertFalse("Node state JSON should not be empty", 
nodeStateJson.isEmpty());
+        assertFalse("Node state JSON should not be {}", rootNode.isEmpty());
+
+        // Verify it contains expected elements using JsonNode structure
+        assertEquals("JSON should contain the correct type",
+            InferenceConfig.TYPE, rootNode.path("type").asText());
+
+        assertTrue("JSON should have enabled set to true",
+            rootNode.path("enabled").asBoolean());
+
+        assertEquals("JSON should contain the custom property",
+            "customValue", rootNode.path("customProperty").asText());
+
+        // Verify index node exists
+        JsonNode indexNode = rootNode.path(indexName);
+        assertTrue("JSON should contain the index node", indexNode.isObject());
+
+        // Verify model node exists within the index node
+        JsonNode modelNode = indexNode.path(modelName);
+        assertTrue("JSON should contain the model node", modelNode.isObject());
+
+        // Verify the model properties
+        assertEquals("Model should have correct type",
+            InferenceModelConfig.TYPE, modelNode.path("type").asText());
+        assertTrue("Model should be enabled",
+            modelNode.path("enabled").asBoolean());
+        assertTrue("Model should be default",
+            modelNode.path(InferenceModelConfig.IS_DEFAULT).asBoolean());
+        assertEquals("Model should have correct name",
+            "json-model", modelNode.path(InferenceModelConfig.MODEL).asText());
+
+        // Verify the enrich node exists
+        JsonNode enrichNode = rootNode.path(InferenceConstants.ENRICH_NODE);
+        assertTrue("JSON should contain the enrich node", 
enrichNode.isObject());
+
+        // Verify enrich status properties
+        assertTrue("Enrich node should contain status mapping",
+            enrichNode.has(InferenceConstants.ENRICHER_STATUS_MAPPING));
+        assertTrue("Enrich node should contain status data",
+            enrichNode.has(InferenceConstants.ENRICHER_STATUS_DATA));
+
+        // Verify complex node structure
+        JsonNode complexNode = rootNode.path("complexNode");
+        assertTrue("JSON should contain complex node", complexNode.isObject());
+
+        // Verify basic properties with different types
+        assertEquals("String property should match", "simple string value", 
complexNode.path("string").asText());
+        assertTrue("Boolean property should be true", 
complexNode.path("boolean").asBoolean());
+        assertEquals("Number property should match", 12345, 
complexNode.path("number").asInt());
+
+        // Verify special characters handling
+        String specialValue = complexNode.path("special").asText();
+        assertTrue("Special characters should be preserved",
+            specialValue.contains("test") &&
+                specialValue.contains("with") &&
+                specialValue.contains("quotes"));
+
+        // Verify unicode characters
+        assertEquals("Unicode characters should be preserved",
+            "测试unicode字符", complexNode.path("unicode").asText());
+
+        // Verify nested child node
+        JsonNode childNode = complexNode.path("childNode");
+        assertTrue("Child node should exist", childNode.isObject());
+        assertEquals("Child property should match", "child value", 
childNode.path("childProp").asText());
+
+        // Part 2: Test with a non-existent path
+        // -------------------------------------
+        String nonExistentPath = "/oak:index/nonExistentConfig";
+        InferenceConfig.reInitialize(nodeStore, nonExistentPath, true);
+        inferenceConfig = InferenceConfig.getInstance();
+
+        // Get JSON for non-existent path
+        String nonExistentJson = inferenceConfig.getInferenceConfigNodeState();
+        JsonNode nonExistentNode = mapper.readTree(nonExistentJson);
+
+        // Should return empty JSON object
+        assertTrue("Should return empty JSON object for non-existent path", 
nonExistentNode.isEmpty());
+
+        // Part 3: Test with disabled inference
+        // -----------------------------------
+        // Create config but disable it
+        NodeBuilder disabledConfigBuilder = createNodePath(rootBuilder, 
DEFAULT_CONFIG_PATH);
+        disabledConfigBuilder.setProperty(InferenceConstants.TYPE, 
InferenceConfig.TYPE);
+        disabledConfigBuilder.setProperty(InferenceConstants.ENABLED, false);
+        nodeStore.merge(rootBuilder, EmptyHook.INSTANCE, CommitInfo.EMPTY);
+
+        // Initialize with disabled config
+        InferenceConfig.reInitialize(nodeStore, DEFAULT_CONFIG_PATH, true);
+        inferenceConfig = InferenceConfig.getInstance();
+
+        // Get JSON for disabled inference
+        String disabledJson = inferenceConfig.getInferenceConfigNodeState();
+        JsonNode disabledNode = mapper.readTree(disabledJson);
+
+        // Should contain basic structure but with enabled=false
+        assertFalse("Disabled config should not be empty", 
disabledNode.isEmpty());
+        assertEquals("Disabled config should have type", InferenceConfig.TYPE, 
disabledNode.path("type").asText());
+        assertFalse("Disabled config should have enabled=false", 
disabledNode.path("enabled").asBoolean());
+
+        // Reset to the default path for other tests
+        inferenceConfigBuilder.setProperty(InferenceConstants.ENABLED, true);
+        nodeStore.merge(rootBuilder, EmptyHook.INSTANCE, CommitInfo.EMPTY);
+        InferenceConfig.reInitialize(nodeStore, DEFAULT_CONFIG_PATH, true);
+    }
+
+    /**
+     * Test getInferenceModelConfig
+     * Tests all paths of the getInferenceModelConfig method
+     */
+    @Test
+    public void testGetInferenceModelConfig() throws CommitFailedException {
+        // Create enabled inference config with an index config containing a 
model config
+        NodeBuilder inferenceConfigBuilder = createNodePath(rootBuilder, 
DEFAULT_CONFIG_PATH);
+        inferenceConfigBuilder.setProperty(InferenceConstants.TYPE, 
InferenceConfig.TYPE);
+        inferenceConfigBuilder.setProperty(InferenceConstants.ENABLED, true);
+
+        // Add index config
+        String indexName = "testModelLookupIndex";
+        NodeBuilder indexConfigBuilder = 
inferenceConfigBuilder.child(indexName);
+        indexConfigBuilder.setProperty(InferenceConstants.TYPE, 
InferenceIndexConfig.TYPE);
+        indexConfigBuilder.setProperty(InferenceConstants.ENABLED, true);
+        indexConfigBuilder.setProperty(InferenceConstants.ENRICHER_CONFIG, 
ENRICHER_CONFIG);
+
+        // Add default model config
+        String defaultModelName = "defaultModel";
+        NodeBuilder defaultModelConfigBuilder = 
indexConfigBuilder.child(defaultModelName);
+        defaultModelConfigBuilder.setProperty(InferenceConstants.TYPE, 
InferenceModelConfig.TYPE);
+        defaultModelConfigBuilder.setProperty(InferenceConstants.ENABLED, 
true);
+        defaultModelConfigBuilder.setProperty(InferenceModelConfig.IS_DEFAULT, 
true);
+        defaultModelConfigBuilder.setProperty(InferenceModelConfig.MODEL, 
"default-embedding-model");
+        
defaultModelConfigBuilder.setProperty(InferenceModelConfig.EMBEDDING_SERVICE_URL,
 "http://localhost:8080/default-embeddings";);
+        
defaultModelConfigBuilder.setProperty(InferenceModelConfig.SIMILARITY_THRESHOLD,
 0.8);
+        defaultModelConfigBuilder.setProperty(InferenceModelConfig.MIN_TERMS, 
3L);
+
+        // Add non-default model config
+        String nonDefaultModelName = "nonDefaultModel";
+        NodeBuilder nonDefaultModelConfigBuilder = 
indexConfigBuilder.child(nonDefaultModelName);
+        nonDefaultModelConfigBuilder.setProperty(InferenceConstants.TYPE, 
InferenceModelConfig.TYPE);
+        nonDefaultModelConfigBuilder.setProperty(InferenceConstants.ENABLED, 
true);
+        
nonDefaultModelConfigBuilder.setProperty(InferenceModelConfig.IS_DEFAULT, 
false);
+        nonDefaultModelConfigBuilder.setProperty(InferenceModelConfig.MODEL, 
"non-default-embedding-model");
+        
nonDefaultModelConfigBuilder.setProperty(InferenceModelConfig.EMBEDDING_SERVICE_URL,
 "http://localhost:8080/non-default-embeddings";);
+        
nonDefaultModelConfigBuilder.setProperty(InferenceModelConfig.SIMILARITY_THRESHOLD,
 0.7);
+        
nonDefaultModelConfigBuilder.setProperty(InferenceModelConfig.MIN_TERMS, 2L);
+
+        // Commit the changes
+        nodeStore.merge(rootBuilder, EmptyHook.INSTANCE, CommitInfo.EMPTY);
+
+        // Create InferenceConfig object
+        InferenceConfig.reInitialize(nodeStore, DEFAULT_CONFIG_PATH, true);
+        InferenceConfig inferenceConfig = InferenceConfig.getInstance();
+
+        // Test case 1: null model name should return NOOP
+        InferenceModelConfig resultForNullModelName = 
inferenceConfig.getInferenceModelConfig(indexName, null);
+        assertEquals("Null model name should return NOOP model config", 
InferenceModelConfig.NOOP, resultForNullModelName);
+
+        // Test case 2: empty model name should return default model
+        InferenceModelConfig resultForEmptyModelName = 
inferenceConfig.getInferenceModelConfig(indexName, "");
+        assertNotEquals("Empty model name should return default model", 
InferenceModelConfig.NOOP, resultForEmptyModelName);
+        assertEquals("Empty model name should return default model", 
defaultModelName, resultForEmptyModelName.getInferenceModelConfigName());
+        assertTrue("Empty model name should return default model that is 
marked as default", resultForEmptyModelName.isDefault());
+
+        // Test case 3: specific model name should return that model
+        InferenceModelConfig resultForSpecificModelName = 
inferenceConfig.getInferenceModelConfig(indexName, nonDefaultModelName);
+        assertNotEquals("Specific model name should return that model", 
InferenceModelConfig.NOOP, resultForSpecificModelName);
+        assertEquals("Specific model name should return that model", 
nonDefaultModelName, resultForSpecificModelName.getInferenceModelConfigName());
+        assertFalse("Specific model name should return that model with the 
correct default flag", resultForSpecificModelName.isDefault());
+
+        // Test case 4: non-existent model name should return NOOP
+        InferenceModelConfig resultForNonExistentModelName = 
inferenceConfig.getInferenceModelConfig(indexName, "nonExistentModel");
+        assertEquals("Non-existent model name should return NOOP", 
InferenceModelConfig.NOOP, resultForNonExistentModelName);
+
+        // Test case 5: non-existent index name should return NOOP
+        InferenceModelConfig resultForNonExistentIndexName = 
inferenceConfig.getInferenceModelConfig("nonExistentIndex", defaultModelName);
+        assertEquals("Non-existent index name should return NOOP", 
InferenceModelConfig.NOOP, resultForNonExistentIndexName);
     }
 } 
\ No newline at end of file

Reply via email to