From 8472f90212224cc92e0f4279423c4c0f919c44e1 Mon Sep 17 00:00:00 2001
From: Jonathan Miller 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.
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()} with + * {@link PlanSearchOptions#withFragment(PlanSearchOptions.Fragment)} to control + * the fragment scope. + * @return an AccessPlan object + * @since 8.2.0; requires MarkLogic 12.1 or higher. */ PlanBuilder.AccessPlan fromSearchDocs(CtsQueryExpr query, String qualifierName, PlanSearchOptions options); /** @@ -131,7 +138,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/FromSearchDocsWithFragmentTest.java b/marklogic-client-api/src/test/java/com/marklogic/client/test/rows/FromSearchDocsWithFragmentTest.java new file mode 100644 index 000000000..b58b61291 --- /dev/null +++ b/marklogic-client-api/src/test/java/com/marklogic/client/test/rows/FromSearchDocsWithFragmentTest.java @@ -0,0 +1,194 @@ +/* + * 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.io.JacksonHandle; +import com.marklogic.client.row.RowRecord; +import com.marklogic.client.test.Common; +import com.marklogic.client.test.junit5.RequiresML12Dot1; +import com.marklogic.client.type.PlanSearchOptions; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +import java.util.List; +import java.util.stream.Collectors; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * Tests for the {@link PlanSearchOptions.Fragment} option added by MLE-28334, using + * {@code op.fromSearchDocs()}. All tests require MarkLogic 12.1 or higher because the + * {@code fragment} option was introduced in MarkLogic 12.1. + * + *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 AbstractOpticUpdateTest {
+
+ private 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.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 AbstractOpticUpdateTest {
+
+ private 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', (