diff --git a/marklogic-client-api/src/main/java/com/marklogic/client/expression/PlanBuilderBase.java b/marklogic-client-api/src/main/java/com/marklogic/client/expression/PlanBuilderBase.java index 669730712..b1321c575 100644 --- a/marklogic-client-api/src/main/java/com/marklogic/client/expression/PlanBuilderBase.java +++ b/marklogic-client-api/src/main/java/com/marklogic/client/expression/PlanBuilderBase.java @@ -43,17 +43,25 @@ public interface PlanBuilderBase { */ PlanBuilder.AccessPlan fromSearchDocs(CtsQueryExpr query, String qualifierName); /** - * Provides a convenience for matching documents and constructing rows with the score, - * document URI, and document content. The convenience is equivalent to chaining + * Matches documents and constructs rows with the score, document URI, and document content, + * with control over which fragment types are searched via {@link PlanSearchOptions#withFragment(PlanSearchOptions.Fragment)}. + *
When no {@code fragment} option is set, behavior is equivalent to chaining * {@link PlanBuilder#fromSearch(CtsQueryExpr)}, * {@link PlanBuilder.ModifyPlan#joinDocUri(String, String)}, * and {@link PlanBuilder.ModifyPlan#joinDoc(String, String)}. - *
The documents can be ordered by the score and limited for the most relevant - * documents.
+ * When a non-default fragment type such as {@link PlanSearchOptions.Fragment#LOCKS} or + * {@link PlanSearchOptions.Fragment#PROPERTIES} is specified, the search targets those fragment + * types and the returned {@code uri} column resolves to the URI of the associated document. + *The documents can be ordered by the score and limited for the most relevant documents.
* @param query The cts.query expression for matching the documents. * @param qualifierName Specifies a name for qualifying the column names similar to a view name. - * @return a ModifyPlan object - * @since 7.0.0; requires MarkLogic 12 or higher. + * @param options Specifies scoring options and the fragment type to search. Use + * {@link PlanBuilder#searchOptions()} to create the options. Support for + * controlling fragment scope with + * {@link PlanSearchOptions#withFragment(PlanSearchOptions.Fragment)} was added + * in 8.2.0 and requires MarkLogic 12.1 or higher. + * @return an AccessPlan object + * @since 7.0.0 */ PlanBuilder.AccessPlan fromSearchDocs(CtsQueryExpr query, String qualifierName, PlanSearchOptions options); /** @@ -131,7 +139,9 @@ public interface PlanBuilderBase { * @param query The cts.query expression for matching the documents. * @param columns The columns to project for the documents. See {@link PlanBuilder#colSeq(String...)} * @param qualifierName Specifies a name for qualifying the column names similar to a view name. - * @param options Specifies how to calculate the score for the matching documents. See {@link PlanBuilder#searchOptions()} + * @param options Specifies how to calculate the score for the matching documents and which fragment + * types to return. Use {@link PlanBuilder#searchOptions()} with + * {@link PlanSearchOptions#withFragment(PlanSearchOptions.Fragment)} to control the fragment scope. * @return an AccessPlan object */ PlanBuilder.AccessPlan fromSearch(CtsQueryExpr query, PlanExprColSeq columns, XsStringVal qualifierName, PlanSearchOptions options); diff --git a/marklogic-client-api/src/main/java/com/marklogic/client/impl/PlanBuilderBaseImpl.java b/marklogic-client-api/src/main/java/com/marklogic/client/impl/PlanBuilderBaseImpl.java index 7c2e624cb..3d307c9ef 100644 --- a/marklogic-client-api/src/main/java/com/marklogic/client/impl/PlanBuilderBaseImpl.java +++ b/marklogic-client-api/src/main/java/com/marklogic/client/impl/PlanBuilderBaseImpl.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2010-2025 Progress Software Corporation and/or its subsidiaries or affiliates. All Rights Reserved. + * Copyright (c) 2010-2026 Progress Software Corporation and/or its subsidiaries or affiliates. All Rights Reserved. */ package com.marklogic.client.impl; @@ -51,17 +51,20 @@ static class PlanSearchOptionsImpl implements PlanSearchOptions { private PlanBuilderBaseImpl pb; private XsFloatVal qualityWeight; private ScoreMethod scoreMethod; - private XsDoubleVal bm25LengthWeight; + private XsDoubleVal bm25LengthWeight; + private Fragment fragment; PlanSearchOptionsImpl(PlanBuilderBaseImpl pb) { this.pb = pb; } - PlanSearchOptionsImpl(PlanBuilderBaseImpl pb, XsFloatVal qualityWeight, - ScoreMethod scoreMethod, XsDoubleVal bm25LengthWeight) { - this(pb); - this.qualityWeight = qualityWeight; - this.scoreMethod = scoreMethod; - this.bm25LengthWeight = bm25LengthWeight; - } + PlanSearchOptionsImpl(PlanBuilderBaseImpl pb, XsFloatVal qualityWeight, + ScoreMethod scoreMethod, XsDoubleVal bm25LengthWeight, + Fragment fragment) { + this(pb); + this.qualityWeight = qualityWeight; + this.scoreMethod = scoreMethod; + this.bm25LengthWeight = bm25LengthWeight; + this.fragment = fragment; + } @Override public XsFloatVal getQualityWeight() { @@ -71,43 +74,55 @@ public XsFloatVal getQualityWeight() { public ScoreMethod getScoreMethod() { return scoreMethod; } - @Override - public XsDoubleVal getBm25LengthWeight() { - return bm25LengthWeight; - } + @Override + public XsDoubleVal getBm25LengthWeight() { + return bm25LengthWeight; + } + @Override + public Fragment getFragment() { + return fragment; + } @Override public PlanSearchOptions withQualityWeight(float qualityWeight) { return withQualityWeight(pb.xs.floatVal(qualityWeight)); } @Override public PlanSearchOptions withQualityWeight(XsFloatVal qualityWeight) { - return new PlanSearchOptionsImpl(pb, qualityWeight, getScoreMethod(), getBm25LengthWeight()); + return new PlanSearchOptionsImpl(pb, qualityWeight, getScoreMethod(), getBm25LengthWeight(), getFragment()); } @Override public PlanSearchOptions withScoreMethod(ScoreMethod scoreMethod) { - return new PlanSearchOptionsImpl(pb, getQualityWeight(), scoreMethod, getBm25LengthWeight()); - } - - @Override - public PlanSearchOptions withBm25LengthWeight(double bm25LengthWeight) { - return new PlanSearchOptionsImpl(pb, getQualityWeight(), getScoreMethod(), pb.xs.doubleVal(bm25LengthWeight)); - } - - MapFragment scope support was added in release 8.2.0 and requires MarkLogic 12.1 or higher. + * Scoring and weighting options apply to all supported MarkLogic versions.
*/ public interface PlanSearchOptions { @@ -38,6 +42,34 @@ public interface PlanSearchOptions { */ PlanSearchOptions withBm25LengthWeight(double bm25LengthWeight); + /** + * @since 8.2.0; requires MarkLogic 12.1 or higher. + */ + Fragment getFragment(); + + /** + * Specifies the type of fragment to search and return. Defaults to {@link Fragment#DOCUMENT} when no option + * is specified. Applies to both {@code fromSearch()} and {@code fromSearchDocs()}. + * + * @param fragment the fragment scope to select + * @return a new PlanSearchOptions with the fragment set + * @since 8.2.0; requires MarkLogic 12.1 or higher. + */ + PlanSearchOptions withFragment(Fragment fragment); + + /** + * Controls which type of fragments are searched and returned by {@code fromSearch()} and + * {@code fromSearchDocs()}. + * + * @since 8.2.0; requires MarkLogic 12.1 or higher. + */ + enum Fragment { + DOCUMENT, + ANY, + PROPERTIES, + LOCKS + } + enum ScoreMethod { LOGTFIDF, LOGTF, diff --git a/marklogic-client-api/src/test/java/com/marklogic/client/test/junit5/RequiresML12Dot1.java b/marklogic-client-api/src/test/java/com/marklogic/client/test/junit5/RequiresML12Dot1.java new file mode 100644 index 000000000..9f2dc9f80 --- /dev/null +++ b/marklogic-client-api/src/test/java/com/marklogic/client/test/junit5/RequiresML12Dot1.java @@ -0,0 +1,28 @@ +/* + * Copyright (c) 2010-2026 Progress Software Corporation and/or its subsidiaries or affiliates. All Rights Reserved. + */ +package com.marklogic.client.test.junit5; + +import com.marklogic.client.test.Common; +import com.marklogic.client.test.MarkLogicVersion; +import org.junit.jupiter.api.extension.ConditionEvaluationResult; +import org.junit.jupiter.api.extension.ExecutionCondition; +import org.junit.jupiter.api.extension.ExtensionContext; + +public class RequiresML12Dot1 implements ExecutionCondition { + + private static MarkLogicVersion markLogicVersion; + + @Override + public ConditionEvaluationResult evaluateExecutionCondition(ExtensionContext context) { + if (markLogicVersion == null) { + markLogicVersion = Common.getMarkLogicVersion(); + } + boolean supported = + (markLogicVersion.getMajor() == 12 && markLogicVersion.getMinor() != null && markLogicVersion.getMinor() >= 1) || + markLogicVersion.getMajor() > 12; + return supported ? + ConditionEvaluationResult.enabled("MarkLogic is version 12.1 or higher") : + ConditionEvaluationResult.disabled("MarkLogic is version 12.0.x or lower"); + } +} diff --git a/marklogic-client-api/src/test/java/com/marklogic/client/test/rows/AbstractFromSearchFragmentTest.java b/marklogic-client-api/src/test/java/com/marklogic/client/test/rows/AbstractFromSearchFragmentTest.java new file mode 100644 index 000000000..bec5c7f59 --- /dev/null +++ b/marklogic-client-api/src/test/java/com/marklogic/client/test/rows/AbstractFromSearchFragmentTest.java @@ -0,0 +1,54 @@ +/* + * Copyright (c) 2010-2026 Progress Software Corporation and/or its subsidiaries or affiliates. All Rights Reserved. + */ +package com.marklogic.client.test.rows; + +import com.marklogic.client.test.Common; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; + +import java.util.List; + +/** + * Shared test fixture for {@link FromSearchWithFragmentTest} and + * {@link FromSearchDocsWithFragmentTest}. Holds the XQuery setup/teardown scripts and + * expected URI constants so that changes to the test documents only need to be made in + * one place. + */ +abstract class AbstractFromSearchFragmentTest extends AbstractOpticUpdateTest { + + static final String SETUP_XQUERY = + "xquery version '1.0-ml';" + + "let $jsondoc1 := object-node {'AllDataTypes': array-node {object-node {'word':'dog'}, object-node {'rank':1}, object-node {'score':4}}}" + + "let $jsondoc2 := object-node {'AllDataTypes': array-node {object-node {'word':'cat'}, object-node {'rank':2}, object-node {'score':5}}}" + + "let $jsondoc3 := object-node {'AllDataTypes': array-node {object-node {'word':'duck'}, object-node {'rank':3}, object-node {'score':6}}}" + + "return (" + + "xdmp:document-insert('range-prop-1.json', $jsondoc1, xdmp:default-permissions(), ('elemCol','jsondoc-range','from-search-fragment-test'))," + + "xdmp:document-insert('range-prop-2.json', $jsondoc2, xdmp:default-permissions(), ('elemCol','jsondoc-range','from-search-fragment-test'))," + + "xdmp:document-insert('range-prop-3.json', $jsondoc3, xdmp:default-permissions(), ('elemCol','jsondoc-range','from-search-fragment-test'))," + + "xdmp:document-set-properties('range-prop-1.json', (Note: {@code op.fromSearchDocs()} returns rows with {@code uri} and {@code doc}
+ * columns directly — URI assertions are possible for all fragment types without requiring
+ * a separate {@code joinDocUri()} call.
+ *
+ * @see FromSearchWithFragmentTest for equivalent tests using {@code op.fromSearch()}.
+ */
+@ExtendWith(RequiresML12Dot1.class)
+class FromSearchDocsWithFragmentTest extends AbstractFromSearchFragmentTest {
+
+ /**
+ * Test case: Verifies the default {@code fromSearchDocs} behavior when no {@code fragment} option
+ * is specified. On MarkLogic 12.1, the default is to search document fragments only. "duck"
+ * appears in the document content of range-prop-3.json ({@code {"word":"duck"}}), so exactly
+ * one row is returned with {@code uri} equal to range-prop-3.json. Properties and lock fragments
+ * are not searched by default.
+ */
+ @Test
+ void fromSearchDocsDefaultFragment() {
+ List Note: {@code op.fromSearch()} returns rows with {@code fragmentId} and {@code score}
+ * columns only — there is no {@code uri} column. {@code joinDocUri()} can resolve document
+ * fragment IDs to URIs, but does not support lock or properties fragment IDs on MarkLogic 12.1.
+ *
+ * @see FromSearchDocsWithFragmentTest for equivalent tests using {@code op.fromSearchDocs()}.
+ */
+@ExtendWith(RequiresML12Dot1.class)
+class FromSearchWithFragmentTest extends AbstractFromSearchFragmentTest {
+
+ /**
+ * Test case: Verifies the default {@code fromSearch} behavior when no {@code fragment} option
+ * is specified. On MarkLogic 12.1, the default is to search document fragments only. "duck"
+ * appears in the document content of range-prop-3.json ({@code {"word":"duck"}}), so exactly
+ * one row is returned. Properties and lock fragments are not searched by default.
+ */
+ @Test
+ void fromSearchDefaultFragment() {
+ List