You are viewing a plain text version of this content. The canonical link for it is here.
Posted to oak-commits@jackrabbit.apache.org by mk...@apache.org on 2025/05/05 08:28:16 UTC

(jackrabbit-oak) branch OAK-11693 created (now 5f64fe20d6)

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

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


      at 5f64fe20d6 OAK-11693: Parse inference query and extract out inference config and text query

This branch includes the following new commits:

     new 5f64fe20d6 OAK-11693: Parse inference query and extract out inference config and text query

The 1 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.



(jackrabbit-oak) 01/01: OAK-11693: Parse inference query and extract out inference config and text query

Posted by mk...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

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

commit 5f64fe20d65e369202c673a8c75d40adaf190cf9
Author: Mohit Kataria <ti...@gmail.com>
AuthorDate: Mon May 5 13:51:50 2025 +0530

    OAK-11693: Parse inference query and extract out inference config and text query
---
 .../oak/api/jmx/QueryEngineSettingsMBean.java      |  14 +
 .../jackrabbit/oak/api/jmx/package-info.java       |   2 +-
 .../main/java/org/apache/jackrabbit/oak/Oak.java   |  11 +
 .../jackrabbit/oak/query/QueryEngineSettings.java  |  19 +-
 .../oak/query/QueryEngineSettingsService.java      |  16 ++
 .../oak/query/ast/FullTextSearchImpl.java          |  49 ++--
 .../jackrabbit/oak/query/index/FilterImpl.java     |   5 +
 .../org/apache/jackrabbit/oak/osgi/OSGiIT.java     |   5 +
 oak-query-spi/pom.xml                              |   9 +
 .../apache/jackrabbit/oak/spi/query/Filter.java    |   8 +
 .../jackrabbit/oak/spi/query/QueryLimits.java      |   4 +
 .../oak/spi/query/fulltext/InferenceQuery.java     | 100 ++++++++
 .../spi/query/fulltext/InferenceQueryConfig.java   |  54 ++++
 .../oak/spi/query/fulltext/package-info.java       |   2 +-
 .../jackrabbit/oak/spi/query/package-info.java     |   2 +-
 .../query/fulltext/InferenceQueryConfigTest.java   |  53 ++++
 .../oak/spi/query/fulltext/InferenceQueryTest.java |  92 +++++++
 oak-store-spi/pom.xml                              |   5 +
 .../org/apache/jackrabbit/oak/json/JsonUtils.java  | 281 +++++++++++++++++++++
 .../apache/jackrabbit/oak/json/JsonUtilsTest.java  | 270 ++++++++++++++++++++
 20 files changed, 978 insertions(+), 23 deletions(-)

diff --git a/oak-api/src/main/java/org/apache/jackrabbit/oak/api/jmx/QueryEngineSettingsMBean.java b/oak-api/src/main/java/org/apache/jackrabbit/oak/api/jmx/QueryEngineSettingsMBean.java
index df9a550029..93bfc9888c 100644
--- a/oak-api/src/main/java/org/apache/jackrabbit/oak/api/jmx/QueryEngineSettingsMBean.java
+++ b/oak-api/src/main/java/org/apache/jackrabbit/oak/api/jmx/QueryEngineSettingsMBean.java
@@ -107,6 +107,20 @@ public interface QueryEngineSettingsMBean {
      */
     void setFailTraversal(boolean failTraversal);
 
+    /**
+     * Get whether the query engine will parse the query and infers inference config
+     *
+     * @return true if inference is enabled
+     */
+    boolean isInferenceEnabled();
+
+    /**
+     * Set whether to parse for inference index config to use from query.
+     *
+     * @param isInferenceEnabled the new value for this setting
+     */
+    void setInferenceEnabled(boolean isInferenceEnabled);
+
     /**
      * Whether the query result size should return an estimation for large queries.
      *
diff --git a/oak-api/src/main/java/org/apache/jackrabbit/oak/api/jmx/package-info.java b/oak-api/src/main/java/org/apache/jackrabbit/oak/api/jmx/package-info.java
index 43d162d725..678163b257 100644
--- a/oak-api/src/main/java/org/apache/jackrabbit/oak/api/jmx/package-info.java
+++ b/oak-api/src/main/java/org/apache/jackrabbit/oak/api/jmx/package-info.java
@@ -15,7 +15,7 @@
  * limitations under the License.
  */
 
-@Version("4.13.0")
+@Version("4.14.0")
 package org.apache.jackrabbit.oak.api.jmx;
 
 import org.osgi.annotation.versioning.Version;
diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/Oak.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/Oak.java
index b1eb93ec9b..7009a89399 100644
--- a/oak-core/src/main/java/org/apache/jackrabbit/oak/Oak.java
+++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/Oak.java
@@ -411,6 +411,7 @@ public class Oak {
         this.queryEngineSettings.setLimitInMemory(settings.getLimitInMemory());
         this.queryEngineSettings.setLimitReads(settings.getLimitReads());
         this.queryEngineSettings.setStrictPathRestriction(settings.getStrictPathRestriction());
+        this.queryEngineSettings.setInferenceEnabled(settings.isInferenceEnabled());
         return this;
     }
 
@@ -957,6 +958,16 @@ public class Oak {
             settings.setFailTraversal(failQueriesWithoutIndex);
         }
 
+        @Override
+        public boolean isInferenceEnabled() {
+            return settings.isInferenceEnabled();
+        }
+
+        @Override
+        public void setInferenceEnabled(boolean isInferenceEnabled) {
+            settings.setInferenceEnabled(isInferenceEnabled);
+        }
+
         @Override
         public boolean isFastQuerySize() {
             return settings.isFastQuerySize();
diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/query/QueryEngineSettings.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/query/QueryEngineSettings.java
index bb90eb2ad9..9527ddb927 100644
--- a/oak-core/src/main/java/org/apache/jackrabbit/oak/query/QueryEngineSettings.java
+++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/query/QueryEngineSettings.java
@@ -75,7 +75,12 @@ public class QueryEngineSettings implements QueryEngineSettingsMBean, QueryLimit
 
     private static final boolean DEFAULT_FULL_TEXT_COMPARISON_WITHOUT_INDEX =
             Boolean.getBoolean("oak.queryFullTextComparisonWithoutIndex");
-    
+
+    public static final String OAK_INFERENCE_ENABLED = "oak.query.InferenceEnabled";
+    private static final boolean DEFAULT_INFERENCE_ENABLED =
+            Boolean.getBoolean(OAK_INFERENCE_ENABLED);
+
+
     private long limitInMemory = DEFAULT_QUERY_LIMIT_IN_MEMORY;
     
     private long limitReads = DEFAULT_QUERY_LIMIT_READS;
@@ -86,6 +91,8 @@ public class QueryEngineSettings implements QueryEngineSettingsMBean, QueryLimit
     
     private boolean fullTextComparisonWithoutIndex = 
             DEFAULT_FULL_TEXT_COMPARISON_WITHOUT_INDEX;
+
+    private boolean isInferenceEnabled = DEFAULT_INFERENCE_ENABLED;
     
     private boolean sql2Optimisation = 
             Boolean.parseBoolean(System.getProperty(SQL2_OPTIMISATION_FLAG, "true"));
@@ -200,6 +207,16 @@ public class QueryEngineSettings implements QueryEngineSettingsMBean, QueryLimit
         this.failTraversal = failTraversal;
     }
 
+    @Override
+    public boolean isInferenceEnabled() {
+        return this.isInferenceEnabled;
+    }
+
+    @Override
+    public void setInferenceEnabled(boolean isInferenceEnabled) {
+        this.isInferenceEnabled = isInferenceEnabled;
+    }
+
     @Override
     public boolean isFastQuerySize() {
         return fastQuerySize;
diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/query/QueryEngineSettingsService.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/query/QueryEngineSettingsService.java
index f52542a991..f30b3d5c3e 100644
--- a/oak-core/src/main/java/org/apache/jackrabbit/oak/query/QueryEngineSettingsService.java
+++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/query/QueryEngineSettingsService.java
@@ -86,6 +86,12 @@ public class QueryEngineSettingsService {
                 )
         String[] ignoredClassNamesInCallTrace() default {};
 
+        @AttributeDefinition(
+                name = "Enable inference query parsing",
+                description = "If enabled the query engine will parse the query, infers the inference index config from query"
+        )
+        boolean isInferenceEnabled() default DEFAULT_IS_INFERENCE_ENABLED;
+
     }
 
     // should be the same as QueryEngineSettings.DEFAULT_QUERY_LIMIT_IN_MEMORY
@@ -102,6 +108,9 @@ public class QueryEngineSettingsService {
     static final String QUERY_FAST_QUERY_SIZE = "fastQuerySize";
     static final String DISABLED_STRICT_PATH_RESTRICTION = "DISABLE";
 
+    private static final boolean DEFAULT_IS_INFERENCE_ENABLED = false;
+    static final String INFERENCE_ENABLED = "inferenceEnabled";
+
     private final Logger log = LoggerFactory.getLogger(getClass());
 
     @Reference
@@ -130,6 +139,13 @@ public class QueryEngineSettingsService {
             logMsg(QUERY_FAIL_TRAVERSAL, QueryEngineSettings.OAK_QUERY_FAIL_TRAVERSAL);
         }
 
+        if (System.getProperty(QueryEngineSettings.OAK_INFERENCE_ENABLED) == null) {
+            boolean isInferenceEnabled = config.isInferenceEnabled();
+            queryEngineSettings.setInferenceEnabled(isInferenceEnabled);
+        } else {
+            logMsg(INFERENCE_ENABLED, QueryEngineSettings.OAK_INFERENCE_ENABLED);
+        }
+
         queryEngineSettings.setIgnoredClassNamesInCallTrace(config.ignoredClassNamesInCallTrace());
 
         boolean fastQuerySizeSysProp = QueryEngineSettings.DEFAULT_FAST_QUERY_SIZE;
diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/query/ast/FullTextSearchImpl.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/query/ast/FullTextSearchImpl.java
index 6b3fa3f4e9..a630ea32f9 100644
--- a/oak-core/src/main/java/org/apache/jackrabbit/oak/query/ast/FullTextSearchImpl.java
+++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/query/ast/FullTextSearchImpl.java
@@ -18,24 +18,25 @@
  */
 package org.apache.jackrabbit.oak.query.ast;
 
-import static org.apache.jackrabbit.oak.api.Type.STRING;
-import static org.apache.jackrabbit.oak.api.Type.STRINGS;
-
-import java.text.ParseException;
-import java.util.Collections;
-import java.util.Set;
-
 import org.apache.jackrabbit.oak.api.PropertyState;
 import org.apache.jackrabbit.oak.api.PropertyValue;
 import org.apache.jackrabbit.oak.api.Tree;
 import org.apache.jackrabbit.oak.api.Type;
 import org.apache.jackrabbit.oak.commons.PathUtils;
+import org.apache.jackrabbit.oak.plugins.memory.PropertyValues;
+import org.apache.jackrabbit.oak.query.index.FilterImpl;
+import org.apache.jackrabbit.oak.spi.query.QueryIndex.FulltextQueryIndex;
 import org.apache.jackrabbit.oak.spi.query.fulltext.FullTextContains;
 import org.apache.jackrabbit.oak.spi.query.fulltext.FullTextExpression;
 import org.apache.jackrabbit.oak.spi.query.fulltext.FullTextParser;
-import org.apache.jackrabbit.oak.query.index.FilterImpl;
-import org.apache.jackrabbit.oak.plugins.memory.PropertyValues;
-import org.apache.jackrabbit.oak.spi.query.QueryIndex.FulltextQueryIndex;
+import org.apache.jackrabbit.oak.spi.query.fulltext.InferenceQuery;
+
+import java.text.ParseException;
+import java.util.Collections;
+import java.util.Set;
+
+import static org.apache.jackrabbit.oak.api.Type.STRING;
+import static org.apache.jackrabbit.oak.api.Type.STRINGS;
 
 /**
  * A fulltext "contains(...)" condition.
@@ -69,7 +70,7 @@ public class FullTextSearchImpl extends ConstraintImpl {
         } else {
             this.propertyName = propertyName;
         }
-        
+
         this.fullTextSearchExpression = fullTextSearchExpression;
     }
 
@@ -137,17 +138,27 @@ public class FullTextSearchImpl extends ConstraintImpl {
             }
             String p2 = normalizePropertyName(p);
             String rawText = getRawText(v);
-            FullTextExpression e = FullTextParser.parse(p2, rawText);
+            String queryText = rawText;
+            // To use inference we need to add inferenceconfig information in query. The format for the query is
+            // <inferenceprefix><json with inferenceModelConfig><inferencePrefix>
+            // e.g. ?{"inferenceModelConfig": "ada-test-model"}?little red fox
+            // So here we split the query into text part of query and inferenceConfig part of query.
+            // Afterwards we only parse text part of query as this part of query is what we want to search.
+            if (query.getSettings().isInferenceEnabled()) {
+                InferenceQuery inferenceQuery = new InferenceQuery(rawText);
+                queryText = inferenceQuery.getQueryText();
+            }
+            FullTextExpression e = FullTextParser.parse(p2, queryText);
             return new FullTextContains(p2, rawText, e);
         } catch (ParseException e) {
             throw new IllegalArgumentException("Invalid expression: " + fullTextSearchExpression, e);
         }
     }
-    
+
     String getRawText(PropertyValue v) {
         return v.getValue(Type.STRING);
     }
-    
+
     @Override
     public Set<SelectorImpl> getSelectors() {
         return Collections.singleton(selector);
@@ -155,9 +166,9 @@ public class FullTextSearchImpl extends ConstraintImpl {
 
     /**
      * verify that a property exists in the node. {@code property IS NOT NULL}
-     * 
+     *
      * @param propertyName the property to check
-     * @param selector the selector to work with
+     * @param selector     the selector to work with
      * @return true if the property is there, false otherwise.
      */
     boolean enforcePropertyExistence(String propertyName, SelectorImpl selector) {
@@ -167,7 +178,7 @@ public class FullTextSearchImpl extends ConstraintImpl {
         }
         return true;
     }
-    
+
     @Override
     public boolean evaluate() {
         // disable evaluation if a fulltext index is used,
@@ -225,7 +236,7 @@ public class FullTextSearchImpl extends ConstraintImpl {
         }
         return getFullTextConstraint(selector).evaluate(buff.toString());
     }
-    
+
     @Override
     public boolean evaluateStop() {
         // if a fulltext index is used, then we are fine
@@ -278,7 +289,7 @@ public class FullTextSearchImpl extends ConstraintImpl {
     /**
      * restrict the provided property to the property to the provided filter achieving so
      * {@code property IS NOT NULL}
-     * 
+     *
      * @param propertyName
      * @param f
      */
diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/query/index/FilterImpl.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/query/index/FilterImpl.java
index 6995c0e418..a0d41c1e6f 100644
--- a/oak-core/src/main/java/org/apache/jackrabbit/oak/query/index/FilterImpl.java
+++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/query/index/FilterImpl.java
@@ -257,6 +257,11 @@ public class FilterImpl implements Filter {
         return alwaysFalse;
     }
 
+    @Override
+    public boolean isInferenceEnabled() {
+        return getQueryLimits().isInferenceEnabled();
+    }
+
     public SelectorImpl getSelector() {
         return selector;
     }
diff --git a/oak-it-osgi/src/test/java/org/apache/jackrabbit/oak/osgi/OSGiIT.java b/oak-it-osgi/src/test/java/org/apache/jackrabbit/oak/osgi/OSGiIT.java
index 7d814496c0..cffaa55159 100644
--- a/oak-it-osgi/src/test/java/org/apache/jackrabbit/oak/osgi/OSGiIT.java
+++ b/oak-it-osgi/src/test/java/org/apache/jackrabbit/oak/osgi/OSGiIT.java
@@ -71,6 +71,11 @@ public class OSGiIT {
                 mavenBundle( "org.apache.felix", "org.apache.felix.configadmin", "1.9.20" ),
                 mavenBundle( "org.apache.felix", "org.apache.felix.fileinstall", "3.2.6" ),
                 mavenBundle( "org.ops4j.pax.logging", "pax-logging-api", "1.7.2" ),
+                // Jackson dependency for object serialisation.
+                mavenBundle().groupId("com.fasterxml.jackson.core").artifactId("jackson-core").version("2.17.2"),
+                mavenBundle().groupId("com.fasterxml.jackson.core").artifactId("jackson-annotations").version("2.17.2"),
+                mavenBundle().groupId("com.fasterxml.jackson.core").artifactId("jackson-databind").version("2.17.2"),
+
                 frameworkProperty("repository.home").value("target"),
                 systemProperties(new SystemPropertyOption("felix.fileinstall.dir").value(getConfigDir())),
                 jarBundles(),
diff --git a/oak-query-spi/pom.xml b/oak-query-spi/pom.xml
index d23ee1ed35..941d8a1906 100644
--- a/oak-query-spi/pom.xml
+++ b/oak-query-spi/pom.xml
@@ -82,6 +82,15 @@
       <artifactId>oak-store-spi</artifactId>
       <version>${project.version}</version>
     </dependency>
+    <dependency>
+      <groupId>com.fasterxml.jackson.core</groupId>
+      <artifactId>jackson-databind</artifactId>
+      <version>${jackson.version}</version>
+    </dependency>
+    <dependency>
+      <groupId>org.apache.commons</groupId>
+      <artifactId>commons-lang3</artifactId>
+    </dependency>
 
     <!-- Nullability annotations -->
     <dependency>
diff --git a/oak-query-spi/src/main/java/org/apache/jackrabbit/oak/spi/query/Filter.java b/oak-query-spi/src/main/java/org/apache/jackrabbit/oak/spi/query/Filter.java
index 83b4ff52c8..848b445822 100644
--- a/oak-query-spi/src/main/java/org/apache/jackrabbit/oak/spi/query/Filter.java
+++ b/oak-query-spi/src/main/java/org/apache/jackrabbit/oak/spi/query/Filter.java
@@ -179,6 +179,14 @@ public interface Filter {
      */
     boolean isAlwaysFalse();
 
+    /**
+     *
+     * @return true if inference is enabled.
+     */
+    default boolean isInferenceEnabled() {
+        return false;
+    }
+
     /**
      * A restriction for a property.
      */
diff --git a/oak-query-spi/src/main/java/org/apache/jackrabbit/oak/spi/query/QueryLimits.java b/oak-query-spi/src/main/java/org/apache/jackrabbit/oak/spi/query/QueryLimits.java
index d06edaf520..050e1bf9da 100644
--- a/oak-query-spi/src/main/java/org/apache/jackrabbit/oak/spi/query/QueryLimits.java
+++ b/oak-query-spi/src/main/java/org/apache/jackrabbit/oak/spi/query/QueryLimits.java
@@ -64,4 +64,8 @@ public interface QueryLimits {
         return new String[] {};
     }
 
+    default boolean isInferenceEnabled(){
+        return false;
+    };
+
 }
diff --git a/oak-query-spi/src/main/java/org/apache/jackrabbit/oak/spi/query/fulltext/InferenceQuery.java b/oak-query-spi/src/main/java/org/apache/jackrabbit/oak/spi/query/fulltext/InferenceQuery.java
new file mode 100644
index 0000000000..317fb86c9d
--- /dev/null
+++ b/oak-query-spi/src/main/java/org/apache/jackrabbit/oak/spi/query/fulltext/InferenceQuery.java
@@ -0,0 +1,100 @@
+/*
+ * 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.spi.query.fulltext;
+
+import org.apache.jackrabbit.oak.json.JsonUtils;
+import org.jetbrains.annotations.NotNull;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class InferenceQuery {
+    private static final Logger LOG = LoggerFactory.getLogger(InferenceQuery.class);
+    private static final String DEFAULT_INFERENCE_QUERY_CONFIG_PREFIX = "?";
+    private static final String INFERENCE_QUERY_CONFIG_PREFIX_KEY = "org.apache.jackrabbit.oak.search.inference.query.prefix";
+    public static final String INFERENCE_QUERY_CONFIG_PREFIX = System.getProperty(
+            INFERENCE_QUERY_CONFIG_PREFIX_KEY, DEFAULT_INFERENCE_QUERY_CONFIG_PREFIX);
+
+    private final String queryInferenceConfig;
+    private final String queryText;
+
+    public InferenceQuery(@NotNull String text) {
+        String[] components = parseText(text);
+        this.queryInferenceConfig = components[0];
+        this.queryText = components[1];
+    }
+
+    private String[] parseText(String inputText) {
+        String text = inputText.trim();
+        // Remove the first delimiter
+        if (text.startsWith(INFERENCE_QUERY_CONFIG_PREFIX) && text.charAt(INFERENCE_QUERY_CONFIG_PREFIX.length()) == '{') {
+            text = text.substring(INFERENCE_QUERY_CONFIG_PREFIX.length());
+
+            // Try to find the end of the JSON part by parsing incrementally
+            int possibleEndIndex = 0;
+            String jsonPart = null;
+            String queryTextPart;
+            int jsonEndDelimiterIndex = -1;
+
+            while (possibleEndIndex < text.length()) {
+                possibleEndIndex = text.indexOf(INFERENCE_QUERY_CONFIG_PREFIX, possibleEndIndex + 1);
+                if (possibleEndIndex == -1) {
+                    // If we reach here, it means we couldn't find a valid JSON part
+                    jsonPart = "";
+                    LOG.warn("Query starts with inference prefix {}, but without valid json part," +
+                                    " if case this prefix is a valid fulltext query prefix, please update system property {} with different prefix value",
+                            INFERENCE_QUERY_CONFIG_PREFIX, INFERENCE_QUERY_CONFIG_PREFIX_KEY);
+                    break;
+                }
+                String candidateJson = text.substring(0, possibleEndIndex);
+                // Verify if this is valid JSON using Oak's JsopTokenizer
+                if (JsonUtils.isValidJson(candidateJson, false)) {
+                    jsonPart = candidateJson;
+                    jsonEndDelimiterIndex = possibleEndIndex;
+                    break;
+                }
+            }
+            // If we found a valid JSON part, extract it
+            if (jsonPart == null) {
+                // If we reach here, it means we couldn't find a valid JSON part
+                jsonPart = "";
+                queryTextPart = text;
+                LOG.warn("Query starts with InferenceQueryPrefix: {}, but without valid json part," +
+                                " if case this prefix is a valid fulltext query prefix, please update {} with different prefix value",
+                        INFERENCE_QUERY_CONFIG_PREFIX, INFERENCE_QUERY_CONFIG_PREFIX_KEY);
+
+            } else {
+                // Extract query text part (everything after the JSON part delimiter)
+                queryTextPart = text.substring(jsonEndDelimiterIndex + 1).trim();
+
+            }
+            return new String[]{jsonPart, queryTextPart};
+        } else {
+            return new String[]{"", text};
+        }
+    }
+
+    public String getQueryInferenceConfig() {
+        return queryInferenceConfig;
+    }
+
+    public String getQueryText() {
+        return queryText;
+    }
+}
\ No newline at end of file
diff --git a/oak-query-spi/src/main/java/org/apache/jackrabbit/oak/spi/query/fulltext/InferenceQueryConfig.java b/oak-query-spi/src/main/java/org/apache/jackrabbit/oak/spi/query/fulltext/InferenceQueryConfig.java
new file mode 100644
index 0000000000..63ca7d6eab
--- /dev/null
+++ b/oak-query-spi/src/main/java/org/apache/jackrabbit/oak/spi/query/fulltext/InferenceQueryConfig.java
@@ -0,0 +1,54 @@
+/*
+ * 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.spi.query.fulltext;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+public class InferenceQueryConfig {
+    public static final String TYPE = "inferenceModelConfig";
+    @Nullable
+    private final String inferenceModelConfig;
+
+    private static final ObjectMapper objectMapper = new ObjectMapper();
+
+    public InferenceQueryConfig(@NotNull String queryConfig) {
+        if (queryConfig.isBlank()){
+            this.inferenceModelConfig = null;
+            return;
+        } else if (queryConfig.equals("{}")) {
+            // in this case a default inferenceModelConfig will be used.
+            this.inferenceModelConfig = "";
+        } else {
+            try {
+                JsonNode jsonNode1 = objectMapper.readTree(queryConfig);
+                inferenceModelConfig = jsonNode1.get(TYPE).asText();
+            } catch (JsonProcessingException e) {
+                throw new RuntimeException("Error parsing inference query config: "+ queryConfig  + "error message:" + e.getMessage());
+            }
+        }
+    }
+
+    public @Nullable String getInferenceModelConfig() {
+        return inferenceModelConfig;
+    }
+}
diff --git a/oak-query-spi/src/main/java/org/apache/jackrabbit/oak/spi/query/fulltext/package-info.java b/oak-query-spi/src/main/java/org/apache/jackrabbit/oak/spi/query/fulltext/package-info.java
index 324e2d104a..9e851fadb9 100644
--- a/oak-query-spi/src/main/java/org/apache/jackrabbit/oak/spi/query/fulltext/package-info.java
+++ b/oak-query-spi/src/main/java/org/apache/jackrabbit/oak/spi/query/fulltext/package-info.java
@@ -18,7 +18,7 @@
 /**
  * This package contains fulltext search condition implementations.
  */
-@Version("1.0.0")
+@Version("1.1.0")
 package org.apache.jackrabbit.oak.spi.query.fulltext;
 
 import org.osgi.annotation.versioning.Version;
diff --git a/oak-query-spi/src/main/java/org/apache/jackrabbit/oak/spi/query/package-info.java b/oak-query-spi/src/main/java/org/apache/jackrabbit/oak/spi/query/package-info.java
index 2cdf053fcc..200a10dc57 100644
--- a/oak-query-spi/src/main/java/org/apache/jackrabbit/oak/spi/query/package-info.java
+++ b/oak-query-spi/src/main/java/org/apache/jackrabbit/oak/spi/query/package-info.java
@@ -18,7 +18,7 @@
 /**
  * This package contains oak query index related classes.
  */
-@Version("3.1.0")
+@Version("3.2.0")
 package org.apache.jackrabbit.oak.spi.query;
 
 import org.osgi.annotation.versioning.Version;
diff --git a/oak-query-spi/src/test/java/org/apache/jackrabbit/oak/spi/query/fulltext/InferenceQueryConfigTest.java b/oak-query-spi/src/test/java/org/apache/jackrabbit/oak/spi/query/fulltext/InferenceQueryConfigTest.java
new file mode 100644
index 0000000000..4ac85ddfd1
--- /dev/null
+++ b/oak-query-spi/src/test/java/org/apache/jackrabbit/oak/spi/query/fulltext/InferenceQueryConfigTest.java
@@ -0,0 +1,53 @@
+/*
+ * 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.spi.query.fulltext;
+
+import org.junit.Test;
+import static org.junit.Assert.*;
+
+public class InferenceQueryConfigTest {
+
+    @Test
+    public void testEmptyConfig() {
+        InferenceQueryConfig config = new InferenceQueryConfig("");
+        assertNull(config.getInferenceModelConfig());
+    }
+
+    @Test
+    public void testEmptyJsonConfig() {
+        InferenceQueryConfig config = new InferenceQueryConfig("{}");
+        assertEquals("", config.getInferenceModelConfig());
+    }
+
+    @Test
+    public void testValidConfig() {
+        InferenceQueryConfig config = new InferenceQueryConfig("{\"inferenceModelConfig\":\"ada-test-model\"}");
+        assertEquals("ada-test-model", config.getInferenceModelConfig());
+    }
+
+    @Test(expected = RuntimeException.class)
+    public void testInvalidJsonConfig() {
+        new InferenceQueryConfig("{invalid json}");
+    }
+
+    @Test(expected = RuntimeException.class)
+    public void testMissingTypeConfig() {
+        new InferenceQueryConfig("{\"someOtherField\":\"value\"}");
+    }
+}
diff --git a/oak-query-spi/src/test/java/org/apache/jackrabbit/oak/spi/query/fulltext/InferenceQueryTest.java b/oak-query-spi/src/test/java/org/apache/jackrabbit/oak/spi/query/fulltext/InferenceQueryTest.java
new file mode 100644
index 0000000000..3017464b88
--- /dev/null
+++ b/oak-query-spi/src/test/java/org/apache/jackrabbit/oak/spi/query/fulltext/InferenceQueryTest.java
@@ -0,0 +1,92 @@
+/*
+ * 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.spi.query.fulltext;
+
+import org.junit.Test;
+
+import static org.junit.Assert.*;
+
+public class InferenceQueryTest {
+
+    @Test
+    public void testBasicQuery() {
+        InferenceQuery query = new InferenceQuery("simple query");
+        assertEquals("", query.getQueryInferenceConfig());
+        assertEquals("simple query", query.getQueryText());
+    }
+
+    @Test
+    public void testQueryWithInferenceConfig() {
+        InferenceQuery query = new InferenceQuery("?{\"model\":\"gpt-4\"}?search for oak trees");
+        assertEquals("{\"model\":\"gpt-4\"}", query.getQueryInferenceConfig());
+        assertEquals("search for oak trees", query.getQueryText());
+    }
+
+    @Test
+    public void testQueryWithComplexInferenceConfig() {
+        InferenceQuery query = new InferenceQuery(
+            "?{\"model\":\"gpt-4\",\"temperature\":0.7,\"options\":{\"filter\":true}}?oak trees");
+        assertEquals("{\"model\":\"gpt-4\",\"temperature\":0.7,\"options\":{\"filter\":true}}", 
+            query.getQueryInferenceConfig());
+        assertEquals("oak trees", query.getQueryText());
+    }
+
+    @Test
+    public void testQueryWithQuestionMarksInText() {
+        InferenceQuery query = new InferenceQuery("?{\"model\":\"gpt-4\"}?what are oak trees?");
+        assertEquals("{\"model\":\"gpt-4\"}", query.getQueryInferenceConfig());
+        assertEquals("what are oak trees?", query.getQueryText());
+    }
+
+    @Test
+    public void testQueryWithoutInferencePrefix() {
+        InferenceQuery query = new InferenceQuery("{\"model\":\"gpt-4\"}?query");
+        assertEquals("", query.getQueryInferenceConfig());
+        assertEquals("{\"model\":\"gpt-4\"}?query", query.getQueryText());
+    }
+
+    @Test
+    public void testQueryWithInvalidJson() {
+        InferenceQuery query = new InferenceQuery("?{invalid json}?query");
+        assertEquals("", query.getQueryInferenceConfig());
+        assertEquals("{invalid json}?query", query.getQueryText());
+    }
+
+    @Test
+    public void testQueryWithEmptyConfig() {
+        InferenceQuery query = new InferenceQuery("??query text");
+        assertEquals("", query.getQueryInferenceConfig());
+        assertEquals("??query text", query.getQueryText());
+    }
+
+    @Test
+    public void testQueryWithWhitespace() {
+        InferenceQuery query = new InferenceQuery("   ?{\"model\":\"gpt-4\"}?   search query   ");
+        assertEquals("{\"model\":\"gpt-4\"}", query.getQueryInferenceConfig());
+        assertEquals("search query", query.getQueryText());
+    }
+
+    @Test
+    public void testEmptyQuery() {
+        InferenceQuery query = new InferenceQuery("");
+        assertEquals("", query.getQueryInferenceConfig());
+        assertEquals("", query.getQueryText());
+    }
+
+}
\ No newline at end of file
diff --git a/oak-store-spi/pom.xml b/oak-store-spi/pom.xml
index 5245a5b121..ba34c07de9 100644
--- a/oak-store-spi/pom.xml
+++ b/oak-store-spi/pom.xml
@@ -117,6 +117,11 @@
       <groupId>org.apache.commons</groupId>
       <artifactId>commons-collections4</artifactId>
     </dependency>
+    <dependency>
+      <groupId>com.fasterxml.jackson.core</groupId>
+      <artifactId>jackson-databind</artifactId>
+      <version>${jackson.version}</version>
+    </dependency>
 
     <!-- Logging -->
     <dependency>
diff --git a/oak-store-spi/src/main/java/org/apache/jackrabbit/oak/json/JsonUtils.java b/oak-store-spi/src/main/java/org/apache/jackrabbit/oak/json/JsonUtils.java
new file mode 100644
index 0000000000..590403b8c2
--- /dev/null
+++ b/oak-store-spi/src/main/java/org/apache/jackrabbit/oak/json/JsonUtils.java
@@ -0,0 +1,281 @@
+/*
+ * 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.json;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.node.ArrayNode;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+import org.apache.jackrabbit.oak.api.PropertyState;
+import org.apache.jackrabbit.oak.api.Type;
+import org.apache.jackrabbit.oak.commons.json.JsopReader;
+import org.apache.jackrabbit.oak.commons.json.JsopTokenizer;
+import org.apache.jackrabbit.oak.spi.state.NodeState;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+public class JsonUtils {
+
+    private static final ObjectMapper mapper = new ObjectMapper();
+
+    /**
+     * Convert a NodeState to a Map representation
+     *
+     * @param nodeState The NodeState to convert
+     * @param maxDepth  Maximum depth to traverse
+     * @return Map representation of the NodeState
+     */
+    public static Map<String, Object> convertNodeStateToMap(NodeState nodeState, int maxDepth, boolean shouldSerializeHiddenNodesOrProperties) {
+        return convertNodeStateToMap(nodeState, maxDepth, -1, shouldSerializeHiddenNodesOrProperties);
+    }
+
+    /**
+     * Convert a NodeState to a Map representation
+     *
+     * @param nodeState    The NodeState to convert
+     * @param maxDepth     Maximum depth to traverse
+     * @param currentDepth Current traversal depth
+     * @return Map representation of the NodeState
+     */
+    private static Map<String, Object> convertNodeStateToMap(NodeState nodeState, int maxDepth, int currentDepth, boolean shouldSerializeHiddenNodesOrProperties) {
+        if (maxDepth >= 0 && currentDepth >= maxDepth) {
+            return null;
+        }
+
+        Map<String, Object> result = new HashMap<>();
+
+        // Convert properties
+        for (PropertyState property : nodeState.getProperties()) {
+            String name = property.getName();
+            Type<?> type = property.getType();
+            // Skip serializing hidden properties.
+            if (!shouldSerializeHiddenNodesOrProperties && name.startsWith(":")) {
+                continue;
+            }
+            if (property.isArray()) {
+                if (type == Type.STRINGS) {
+                    result.put(name, property.getValue(Type.STRINGS));
+                } else if (type == Type.LONGS) {
+                    result.put(name, property.getValue(Type.LONGS));
+                } else if (type == Type.DOUBLES) {
+                    result.put(name, property.getValue(Type.DOUBLES));
+                } else if (type == Type.BOOLEANS) {
+                    result.put(name, property.getValue(Type.BOOLEANS));
+                } else if (type == Type.DATES) {
+                    result.put(name, property.getValue(Type.DATES));
+                } else if (type == Type.DECIMALS) {
+                    result.put(name, property.getValue(Type.DECIMALS));
+                } else {
+                    // For other array types, convert to strings
+                    List<String> values = new ArrayList<>();
+                    for (int i = 0; i < property.count(); i++) {
+                        values.add(property.getValue(Type.STRING, i));
+                    }
+                    result.put(name, values);
+                }
+            } else {
+                if (type == Type.STRING) {
+                    result.put(name, property.getValue(Type.STRING));
+                } else if (type == Type.LONG) {
+                    result.put(name, property.getValue(Type.LONG));
+                } else if (type == Type.DOUBLE) {
+                    result.put(name, property.getValue(Type.DOUBLE));
+                } else if (type == Type.BOOLEAN) {
+                    result.put(name, property.getValue(Type.BOOLEAN));
+                } else if (type == Type.DATE) {
+                    result.put(name, property.getValue(Type.DATE));
+                } else if (type == Type.DECIMAL) {
+                    result.put(name, property.getValue(Type.DECIMAL));
+                } else {
+                    // For other types, convert to string
+                    result.put(name, property.getValue(Type.STRING));
+                }
+            }
+        }
+
+        // Convert child nodes recursively
+        for (String childName : nodeState.getChildNodeNames()) {
+            if (!shouldSerializeHiddenNodesOrProperties && childName.startsWith(":")) {
+                continue;
+            }
+            NodeState childNode = nodeState.getChildNode(childName);
+            Map<String, Object> childMap = convertNodeStateToMap(childNode, maxDepth, currentDepth + 1, shouldSerializeHiddenNodesOrProperties);
+            if (childMap != null) {
+                result.put(childName, childMap);
+            }
+        }
+        return result;
+    }
+
+    /**
+     * Converts a NodeState to JSON string with specified depth
+     *
+     * @param nodeState The NodeState to convert
+     * @param maxDepth  Maximum depth to traverse, use -1 for unlimited depth
+     * @return JSON string representation
+     * @throws JsonProcessingException if JSON processing fails
+     */
+    public static String nodeStateToJson(NodeState nodeState, int maxDepth) throws JsonProcessingException {
+        JsonNode jsonNode = convertNodeStateToJson(nodeState, maxDepth, 0);
+        return mapper.writerWithDefaultPrettyPrinter().writeValueAsString(jsonNode);
+    }
+
+    private static JsonNode convertNodeStateToJson(NodeState nodeState, int maxDepth, int currentDepth) {
+        ObjectNode result = mapper.createObjectNode();
+
+        // Return if max depth reached
+        if (maxDepth != -1 && currentDepth > maxDepth) {
+            return result;
+        }
+
+        // Convert properties
+        for (PropertyState property : nodeState.getProperties()) {
+            String name = property.getName();
+            Type<?> type = property.getType();
+
+            if (property.isArray()) {
+                ArrayNode arrayNode = mapper.createArrayNode();
+                if (type == Type.STRINGS) {
+                    property.getValue(Type.STRINGS).forEach(arrayNode::add);
+                } else if (type == Type.LONGS) {
+                    property.getValue(Type.LONGS).forEach(arrayNode::add);
+                } else if (type == Type.DOUBLES) {
+                    property.getValue(Type.DOUBLES).forEach(arrayNode::add);
+                } else if (type == Type.BOOLEANS) {
+                    property.getValue(Type.BOOLEANS).forEach(arrayNode::add);
+                } else if (type == Type.DATES) {
+                    property.getValue(Type.DATES).forEach(arrayNode::add);
+                } else if (type == Type.DECIMALS) {
+                    property.getValue(Type.DECIMALS).forEach(arrayNode::add);
+                } else {
+                    // For other array types, convert to strings
+                    for (int i = 0; i < property.count(); i++) {
+                        arrayNode.add(property.getValue(Type.STRING, i));
+                    }
+                }
+                result.set(name, arrayNode);
+            } else {
+                if (type == Type.STRING) {
+                    result.put(name, property.getValue(Type.STRING));
+                } else if (type == Type.LONG) {
+                    result.put(name, property.getValue(Type.LONG));
+                } else if (type == Type.DOUBLE) {
+                    result.put(name, property.getValue(Type.DOUBLE));
+                } else if (type == Type.BOOLEAN) {
+                    result.put(name, property.getValue(Type.BOOLEAN));
+                } else if (type == Type.DATE) {
+                    result.put(name, property.getValue(Type.DATE).toString());
+                } else if (type == Type.DECIMAL) {
+                    result.put(name, property.getValue(Type.DECIMAL).toString());
+                } else {
+                    // For other types, convert to string
+                    result.put(name, property.getValue(Type.STRING));
+                }
+            }
+        }
+
+        // Convert child nodes recursively
+        for (String childName : nodeState.getChildNodeNames()) {
+            NodeState childNode = nodeState.getChildNode(childName);
+            result.set(childName, convertNodeStateToJson(childNode, maxDepth, currentDepth + 1));
+        }
+
+        return result;
+    }
+
+    public static boolean isValidJson(String text, boolean isJsonArray) {
+        if (text == null) {
+            return false;
+        }
+
+        JsopReader reader = new JsopTokenizer(text);
+        return validateJson(reader, isJsonArray);
+    }
+
+    private static boolean validateJson(JsopReader reader, boolean isJsonArray) {
+        if (reader.matches('{')) {
+            return validateObject(reader) && reader.read() == JsopReader.END;
+        }
+        else if (reader.matches('[')) {
+            if (!isJsonArray) {
+                return false;
+            }
+            return validateArray(reader) && reader.read() == JsopReader.END;
+        }
+        else {
+            return false;// readJsonValue(reader) && reader.read() == JsopReader.END;
+        }
+    }
+
+    private static boolean validateObject(JsopReader reader) {
+        boolean first = true;
+        while (!reader.matches('}')) {
+            if (!first && !reader.matches(',')) {
+                return false;
+            }
+            if (!reader.matches(JsopReader.STRING)) {
+                return false;
+            }
+            if (!reader.matches(':')) {
+                return false;
+            }
+            if (!readJsonValue(reader)) {
+                return false;
+            }
+            first = false;
+        }
+        return true;
+    }
+
+    private static boolean validateArray(JsopReader reader) {
+        boolean first = true;
+        while (!reader.matches(']')) {
+            if (!first && !reader.matches(',')) {
+                return false;
+            }
+            if (!readJsonValue(reader)) {
+                return false;
+            }
+            first = false;
+        }
+        return true;
+    }
+
+    private static boolean readJsonValue(JsopReader reader) {
+        int token = reader.read();
+        switch (token) {
+            case JsopReader.STRING:
+            case JsopReader.NUMBER:
+            case JsopReader.TRUE:
+            case JsopReader.FALSE:
+            case JsopReader.NULL:
+                return true;
+            case '{':
+                return validateObject(reader);
+            case '[':
+                return validateArray(reader);
+            default:
+                return false;
+        }
+    }
+}
diff --git a/oak-store-spi/src/test/java/org/apache/jackrabbit/oak/json/JsonUtilsTest.java b/oak-store-spi/src/test/java/org/apache/jackrabbit/oak/json/JsonUtilsTest.java
new file mode 100644
index 0000000000..c3848bbea0
--- /dev/null
+++ b/oak-store-spi/src/test/java/org/apache/jackrabbit/oak/json/JsonUtilsTest.java
@@ -0,0 +1,270 @@
+/*
+ * 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.json;
+
+import org.apache.jackrabbit.oak.plugins.memory.EmptyNodeState;
+import org.apache.jackrabbit.oak.plugins.memory.MemoryNodeBuilder;
+import org.apache.jackrabbit.oak.spi.state.NodeBuilder;
+import org.apache.jackrabbit.oak.spi.state.NodeState;
+import org.junit.Test;
+
+import java.util.Map;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+
+public class JsonUtilsTest {
+
+    @Test
+    public void testNullInput() {
+        assertFalse(JsonUtils.isValidJson(null, false));
+        assertFalse(JsonUtils.isValidJson(null, true));
+    }
+
+    @Test
+    public void testEmptyString() {
+        assertFalse(JsonUtils.isValidJson("", false));
+        assertFalse(JsonUtils.isValidJson("", true));
+    }
+
+    @Test
+    public void testValidJsonObject() {
+        assertTrue(JsonUtils.isValidJson("{}", false));
+        assertTrue(JsonUtils.isValidJson("{\"key\":\"value\"}", false));
+        assertTrue(JsonUtils.isValidJson("{\"key\":123}", false));
+        assertTrue(JsonUtils.isValidJson("{\"key\":true}", false));
+        assertTrue(JsonUtils.isValidJson("{\"key\":null}", false));
+        assertTrue(JsonUtils.isValidJson("{\"key\":{\"nested\":\"value\"}}", false));
+    }
+
+    @Test
+    public void testInvalidJsonObject() {
+        assertFalse(JsonUtils.isValidJson("{", false));
+        assertFalse(JsonUtils.isValidJson("}", false));
+        assertFalse(JsonUtils.isValidJson("{key:value}", false));
+        assertFalse(JsonUtils.isValidJson("{\"key\":value}", false));
+        assertFalse(JsonUtils.isValidJson("{\"key\":\"value\"", false));
+        assertFalse(JsonUtils.isValidJson("{\"key\":\"value\",}", false));
+    }
+
+    @Test
+    public void testValidJsonArray() {
+        assertTrue(JsonUtils.isValidJson("[]", true));
+        assertTrue(JsonUtils.isValidJson("[1,2,3]", true));
+        assertTrue(JsonUtils.isValidJson("[\"a\",\"b\",\"c\"]", true));
+        assertTrue(JsonUtils.isValidJson("[true,false,null]", true));
+        assertTrue(JsonUtils.isValidJson("[{\"key\":\"value\"},{\"key2\":123}]", true));
+    }
+
+    @Test
+    public void testInvalidJsonArray() {
+        assertFalse(JsonUtils.isValidJson("[", true));
+        assertFalse(JsonUtils.isValidJson("]", true));
+        assertFalse(JsonUtils.isValidJson("[1,2,]", true));
+        assertFalse(JsonUtils.isValidJson("[1 2 3]", true));
+        assertFalse(JsonUtils.isValidJson("[\"unclosed]", true));
+    }
+
+    @Test
+    public void testArrayNotAllowedWhenFlagIsFalse() {
+        assertFalse(JsonUtils.isValidJson("[]", false));
+        assertFalse(JsonUtils.isValidJson("[1,2,3]", false));
+    }
+
+    @Test
+    public void testValidPrimitiveValues() {
+        assertFalse(JsonUtils.isValidJson("123", false));
+        assertFalse(JsonUtils.isValidJson("\"string\"", false));
+        assertFalse(JsonUtils.isValidJson("true", false));
+        assertFalse(JsonUtils.isValidJson("false", false));
+        assertFalse(JsonUtils.isValidJson("null", false));
+    }
+
+    @Test
+    public void testInvalidPrimitiveValues() {
+        assertFalse(JsonUtils.isValidJson("undefined", false));
+        assertFalse(JsonUtils.isValidJson("'string'", false));
+        assertFalse(JsonUtils.isValidJson("TRUE", false));
+        assertFalse(JsonUtils.isValidJson("False", false));
+        assertFalse(JsonUtils.isValidJson("NULL", false));
+    }
+
+    @Test
+    public void testComplexNestedStructures() {
+        assertTrue(JsonUtils.isValidJson("{\"array\":[1,2,{\"nested\":true}]}", false));
+        assertTrue(JsonUtils.isValidJson("[{\"obj\":{}},[[],{}]]", true));
+        assertTrue(JsonUtils.isValidJson("{\"a\":{\"b\":{\"c\":{\"d\":null}}}}", false));
+        assertFalse(JsonUtils.isValidJson("{\"array\":[1,2,{\"nested\":Undefined}]}", false));
+        assertFalse(JsonUtils.isValidJson("{\"array\":[1,2,{\"nested\":NaN}]}", false));
+        assertFalse(JsonUtils.isValidJson("{\"array\":[1,2,{\"nested\":Infinity}]}", false));
+        assertFalse(JsonUtils.isValidJson("{\"a\":{\"b\":{\"c\":{\"d\":void}}}}", false));
+    }
+
+    @Test
+    public void testConvertNodeStateToMap() {
+        MemoryNodeBuilder builder = new MemoryNodeBuilder(EmptyNodeState.EMPTY_NODE);
+        builder.setProperty("property1", "value1");
+        builder.setProperty("property2", 123);
+        NodeState nodeState = builder.getNodeState();
+
+        Map<String, Object> result = JsonUtils.convertNodeStateToMap(nodeState, 2, true);
+
+        assertNotNull(result);
+        assertEquals(2, result.size());
+        assertTrue(result.containsKey("property1"));
+        assertTrue(result.containsKey("property2"));
+        assertEquals("value1", result.get("property1"));
+        assertEquals(123L, result.get("property2"));
+    }
+
+    @Test
+    public void testConvertNodeStateToMapWithMaxDepth() {
+        MemoryNodeBuilder builder = new MemoryNodeBuilder(EmptyNodeState.EMPTY_NODE);
+        builder.setProperty("property1", "value1");
+        NodeState nodeState = builder.getNodeState();
+
+        Map<String, Object> result = JsonUtils.convertNodeStateToMap(nodeState, 0, true);
+        assertNotNull(result);
+    }
+
+    @Test
+    public void testConvertNodeStateToMapWithNestedNodes() {
+        NodeBuilder builder = new MemoryNodeBuilder(EmptyNodeState.EMPTY_NODE);
+        builder.setProperty("prop1", "val1");
+
+        NodeBuilder child = builder.child("child1");
+        child.setProperty("childProp1", "childVal1");
+
+        NodeBuilder grandChild = child.child("grandChild1");
+        grandChild.setProperty("grandChildProp1", "grandChildVal1");
+
+        NodeBuilder greatGrandChild = grandChild.child("greatGrandChild1");
+        greatGrandChild.setProperty("greatGrandChildProp1", "greatGrandChildVal1");
+
+        NodeState nodeState = builder.getNodeState();
+
+        // Test with maxDepth = 0 (should return only properties on current node)
+        Map<String, Object> result0 = JsonUtils.convertNodeStateToMap(nodeState, 0, true);
+        assertNotNull(result0);
+        assertEquals(1, result0.size());
+        assertTrue(result0.containsKey("prop1"));
+        assertEquals("val1", result0.get("prop1"));
+        assertNotNull(result0);
+        assertTrue(result0.containsKey("prop1"));
+        assertFalse(result0.containsKey("child1"));
+
+
+        // Test with maxDepth = 1 (should include child1)
+        Map<String, Object> result1 = JsonUtils.convertNodeStateToMap(nodeState, 1, true);
+        assertNotNull(result1);
+        assertTrue(result1.containsKey("child1"));
+        Map<String, Object> childMap1 = (Map<String, Object>) result1.get("child1");
+        assertEquals("childVal1", childMap1.get("childProp1"));
+        assertNotNull(result1);
+        assertTrue(result1.containsKey("prop1"));
+        assertTrue(result1.containsKey("child1"));
+        assertFalse(childMap1.containsKey("grandChild1"));
+
+
+        // Test with maxDepth = 2 (should include grandChild1)
+        Map<String, Object> result2 = JsonUtils.convertNodeStateToMap(nodeState, 2, true);
+        Map<String, Object> childMap2 = (Map<String, Object>) result2.get("child1");
+        assertTrue(childMap2.containsKey("grandChild1"));
+        Map<String, Object> grandChildMap2 = (Map<String, Object>) childMap2.get("grandChild1");
+        assertEquals("grandChildVal1", grandChildMap2.get("grandChildProp1"));
+        assertNotNull(result2);
+        assertTrue(result2.containsKey("prop1"));
+        assertTrue(result2.containsKey("child1"));
+        assertFalse(grandChildMap2.containsKey("greatGrandChild1"));
+
+        // Test with maxDepth = 3 (should include greatGrandChild1)
+        Map<String, Object> result3 = JsonUtils.convertNodeStateToMap(nodeState, 3, true);
+        Map<String, Object> childMap3 = (Map<String, Object>) result3.get("child1");
+        Map<String, Object> grandChildMap3 = (Map<String, Object>) childMap3.get("grandChild1");
+        Map<String, Object> greatGrandChildMap3 = (Map<String, Object>) grandChildMap3.get("greatGrandChild1");
+        assertEquals("greatGrandChildVal1", greatGrandChildMap3.get("greatGrandChildProp1"));
+        assertNotNull(result3);
+        assertTrue(result3.containsKey("prop1"));
+        assertTrue(result3.containsKey("child1"));
+
+        // Test with maxDepth = -1 (should return all nodes and properties)
+        Map<String, Object> resultNeg1 = JsonUtils.convertNodeStateToMap(nodeState, -1, true);
+        Map<String, Object> childMapNeg1 = (Map<String, Object>) resultNeg1.get("child1");
+        Map<String, Object> grandChildMapNeg1 = (Map<String, Object>) childMapNeg1.get("grandChild1");
+        assertTrue(grandChildMapNeg1.containsKey("greatGrandChild1"));
+        Map<String, Object> greatGrandChildMapNeg1 = (Map<String, Object>) grandChildMapNeg1.get("greatGrandChild1");
+        assertEquals("greatGrandChildVal1", greatGrandChildMapNeg1.get("greatGrandChildProp1"));
+        assertNotNull(resultNeg1);
+        assertTrue(resultNeg1.containsKey("prop1"));
+        assertTrue(resultNeg1.containsKey("child1"));
+
+
+    }
+
+    @Test
+    public void testConvertNodeStateToMapWithHiddenNodesAndProperties() {
+        NodeBuilder builder = new MemoryNodeBuilder(EmptyNodeState.EMPTY_NODE);
+
+        // Regular property
+        builder.setProperty("regularProp", "regularValue");
+
+        // Hidden property (starts with ":")
+        builder.setProperty(":hiddenProp", "hiddenValue");
+
+        // Regular child
+        NodeBuilder regularChild = builder.child("regularChild");
+        regularChild.setProperty("childProp", "childValue");
+
+        // Hidden child (starts with ":")
+        NodeBuilder hiddenChild = builder.child(":hiddenChild");
+        hiddenChild.setProperty("hiddenChildProp", "hiddenChildValue");
+
+        NodeState nodeState = builder.getNodeState();
+
+        // Test with shouldSerializeHiddenNodesOrProperties = true
+        // Hidden nodes and properties should be excluded
+        Map<String, Object> resultWithHiddenExcluded = JsonUtils.convertNodeStateToMap(nodeState, -1, false);
+        assertNotNull(resultWithHiddenExcluded);
+        assertEquals(2, resultWithHiddenExcluded.size());
+        assertTrue(resultWithHiddenExcluded.containsKey("regularProp"));
+        assertTrue(resultWithHiddenExcluded.containsKey("regularChild"));
+        assertFalse(resultWithHiddenExcluded.containsKey(":hiddenProp"));
+        assertFalse(resultWithHiddenExcluded.containsKey(":hiddenChild"));
+
+        // Test with shouldSerializeHiddenNodesOrProperties = false
+        // Hidden nodes and properties should be included
+        Map<String, Object> resultWithHiddenIncluded = JsonUtils.convertNodeStateToMap(nodeState, -1, true);
+        assertNotNull(resultWithHiddenIncluded);
+        assertEquals(4, resultWithHiddenIncluded.size());
+        assertTrue(resultWithHiddenIncluded.containsKey("regularProp"));
+        assertTrue(resultWithHiddenIncluded.containsKey("regularChild"));
+        assertTrue(resultWithHiddenIncluded.containsKey(":hiddenProp"));
+        assertTrue(resultWithHiddenIncluded.containsKey(":hiddenChild"));
+        assertEquals("hiddenValue", resultWithHiddenIncluded.get(":hiddenProp"));
+
+        // Verify nested content
+        Map<String, Object> regularChildMap = (Map<String, Object>) resultWithHiddenIncluded.get("regularChild");
+        assertEquals("childValue", regularChildMap.get("childProp"));
+
+        Map<String, Object> hiddenChildMap = (Map<String, Object>) resultWithHiddenIncluded.get(":hiddenChild");
+        assertEquals("hiddenChildValue", hiddenChildMap.get("hiddenChildProp"));
+    }
+}