Skip to content

Commit 8b45385

Browse files
authored
MLE-28334 Implement Fragment Option (#1937)
* MLE-28334 Implement Fragment Option * MLE-28334 Copilot Suggested Fixes + AbstractFromSearchFragmentTest.java: Refactored FromSearchDocsWithFragmentTest.java and FromSearchWithFragmentTest.java + Changed JavaDoc for fromsearchDocs in PlanBuilderBase.java
1 parent 75c4014 commit 8b45385

8 files changed

Lines changed: 495 additions & 49 deletions

File tree

marklogic-client-api/src/main/java/com/marklogic/client/expression/PlanBuilderBase.java

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -43,17 +43,25 @@ public interface PlanBuilderBase {
4343
*/
4444
PlanBuilder.AccessPlan fromSearchDocs(CtsQueryExpr query, String qualifierName);
4545
/**
46-
* Provides a convenience for matching documents and constructing rows with the score,
47-
* document URI, and document content. The convenience is equivalent to chaining
46+
* Matches documents and constructs rows with the score, document URI, and document content,
47+
* with control over which fragment types are searched via {@link PlanSearchOptions#withFragment(PlanSearchOptions.Fragment)}.
48+
* <p>When no {@code fragment} option is set, behavior is equivalent to chaining
4849
* {@link PlanBuilder#fromSearch(CtsQueryExpr)},
4950
* {@link PlanBuilder.ModifyPlan#joinDocUri(String, String)},
5051
* and {@link PlanBuilder.ModifyPlan#joinDoc(String, String)}.
51-
* <p>The documents can be ordered by the score and limited for the most relevant
52-
* documents.</p>
52+
* When a non-default fragment type such as {@link PlanSearchOptions.Fragment#LOCKS} or
53+
* {@link PlanSearchOptions.Fragment#PROPERTIES} is specified, the search targets those fragment
54+
* types and the returned {@code uri} column resolves to the URI of the associated document.</p>
55+
* <p>The documents can be ordered by the score and limited for the most relevant documents.</p>
5356
* @param query The cts.query expression for matching the documents.
5457
* @param qualifierName Specifies a name for qualifying the column names similar to a view name.
55-
* @return a ModifyPlan object
56-
* @since 7.0.0; requires MarkLogic 12 or higher.
58+
* @param options Specifies scoring options and the fragment type to search. Use
59+
* {@link PlanBuilder#searchOptions()} to create the options. Support for
60+
* controlling fragment scope with
61+
* {@link PlanSearchOptions#withFragment(PlanSearchOptions.Fragment)} was added
62+
* in 8.2.0 and requires MarkLogic 12.1 or higher.
63+
* @return an AccessPlan object
64+
* @since 7.0.0
5765
*/
5866
PlanBuilder.AccessPlan fromSearchDocs(CtsQueryExpr query, String qualifierName, PlanSearchOptions options);
5967
/**
@@ -131,7 +139,9 @@ public interface PlanBuilderBase {
131139
* @param query The cts.query expression for matching the documents.
132140
* @param columns The columns to project for the documents. See {@link PlanBuilder#colSeq(String...)}
133141
* @param qualifierName Specifies a name for qualifying the column names similar to a view name.
134-
* @param options Specifies how to calculate the score for the matching documents. See {@link PlanBuilder#searchOptions()}
142+
* @param options Specifies how to calculate the score for the matching documents and which fragment
143+
* types to return. Use {@link PlanBuilder#searchOptions()} with
144+
* {@link PlanSearchOptions#withFragment(PlanSearchOptions.Fragment)} to control the fragment scope.
135145
* @return an AccessPlan object
136146
*/
137147
PlanBuilder.AccessPlan fromSearch(CtsQueryExpr query, PlanExprColSeq columns, XsStringVal qualifierName, PlanSearchOptions options);

marklogic-client-api/src/main/java/com/marklogic/client/impl/PlanBuilderBaseImpl.java

Lines changed: 52 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright (c) 2010-2025 Progress Software Corporation and/or its subsidiaries or affiliates. All Rights Reserved.
2+
* Copyright (c) 2010-2026 Progress Software Corporation and/or its subsidiaries or affiliates. All Rights Reserved.
33
*/
44
package com.marklogic.client.impl;
55

@@ -51,17 +51,20 @@ static class PlanSearchOptionsImpl implements PlanSearchOptions {
5151
private PlanBuilderBaseImpl pb;
5252
private XsFloatVal qualityWeight;
5353
private ScoreMethod scoreMethod;
54-
private XsDoubleVal bm25LengthWeight;
54+
private XsDoubleVal bm25LengthWeight;
55+
private Fragment fragment;
5556
PlanSearchOptionsImpl(PlanBuilderBaseImpl pb) {
5657
this.pb = pb;
5758
}
58-
PlanSearchOptionsImpl(PlanBuilderBaseImpl pb, XsFloatVal qualityWeight,
59-
ScoreMethod scoreMethod, XsDoubleVal bm25LengthWeight) {
60-
this(pb);
61-
this.qualityWeight = qualityWeight;
62-
this.scoreMethod = scoreMethod;
63-
this.bm25LengthWeight = bm25LengthWeight;
64-
}
59+
PlanSearchOptionsImpl(PlanBuilderBaseImpl pb, XsFloatVal qualityWeight,
60+
ScoreMethod scoreMethod, XsDoubleVal bm25LengthWeight,
61+
Fragment fragment) {
62+
this(pb);
63+
this.qualityWeight = qualityWeight;
64+
this.scoreMethod = scoreMethod;
65+
this.bm25LengthWeight = bm25LengthWeight;
66+
this.fragment = fragment;
67+
}
6568

6669
@Override
6770
public XsFloatVal getQualityWeight() {
@@ -71,43 +74,55 @@ public XsFloatVal getQualityWeight() {
7174
public ScoreMethod getScoreMethod() {
7275
return scoreMethod;
7376
}
74-
@Override
75-
public XsDoubleVal getBm25LengthWeight() {
76-
return bm25LengthWeight;
77-
}
77+
@Override
78+
public XsDoubleVal getBm25LengthWeight() {
79+
return bm25LengthWeight;
80+
}
81+
@Override
82+
public Fragment getFragment() {
83+
return fragment;
84+
}
7885
@Override
7986
public PlanSearchOptions withQualityWeight(float qualityWeight) {
8087
return withQualityWeight(pb.xs.floatVal(qualityWeight));
8188
}
8289
@Override
8390
public PlanSearchOptions withQualityWeight(XsFloatVal qualityWeight) {
84-
return new PlanSearchOptionsImpl(pb, qualityWeight, getScoreMethod(), getBm25LengthWeight());
91+
return new PlanSearchOptionsImpl(pb, qualityWeight, getScoreMethod(), getBm25LengthWeight(), getFragment());
8592
}
8693
@Override
8794
public PlanSearchOptions withScoreMethod(ScoreMethod scoreMethod) {
88-
return new PlanSearchOptionsImpl(pb, getQualityWeight(), scoreMethod, getBm25LengthWeight());
89-
}
90-
91-
@Override
92-
public PlanSearchOptions withBm25LengthWeight(double bm25LengthWeight) {
93-
return new PlanSearchOptionsImpl(pb, getQualityWeight(), getScoreMethod(), pb.xs.doubleVal(bm25LengthWeight));
94-
}
95-
96-
Map<String,Object> makeMap() {
97-
if (qualityWeight == null && scoreMethod == null && bm25LengthWeight == null) return null;
98-
99-
Map<String, Object> map = new HashMap<>();
100-
if (qualityWeight != null) {
101-
map.put("qualityWeight", qualityWeight);
102-
}
103-
if (scoreMethod != null) {
104-
map.put("scoreMethod", scoreMethod.name().toLowerCase());
105-
}
106-
if (bm25LengthWeight != null) {
107-
map.put("bm25LengthWeight", bm25LengthWeight);
108-
}
109-
return map;
110-
}
95+
return new PlanSearchOptionsImpl(pb, getQualityWeight(), scoreMethod, getBm25LengthWeight(), getFragment());
96+
}
97+
98+
@Override
99+
public PlanSearchOptions withBm25LengthWeight(double bm25LengthWeight) {
100+
return new PlanSearchOptionsImpl(pb, getQualityWeight(), getScoreMethod(), pb.xs.doubleVal(bm25LengthWeight), getFragment());
101+
}
102+
103+
@Override
104+
public PlanSearchOptions withFragment(Fragment fragment) {
105+
return new PlanSearchOptionsImpl(pb, getQualityWeight(), getScoreMethod(), getBm25LengthWeight(), fragment);
106+
}
107+
108+
Map<String,Object> makeMap() {
109+
if (qualityWeight == null && scoreMethod == null && bm25LengthWeight == null && fragment == null) return null;
110+
111+
Map<String, Object> map = new HashMap<>();
112+
if (qualityWeight != null) {
113+
map.put("qualityWeight", qualityWeight);
114+
}
115+
if (scoreMethod != null) {
116+
map.put("scoreMethod", scoreMethod.name().toLowerCase());
117+
}
118+
if (bm25LengthWeight != null) {
119+
map.put("bm25LengthWeight", bm25LengthWeight);
120+
}
121+
if (fragment != null) {
122+
map.put("fragment", fragment.name().toLowerCase());
123+
}
124+
return map;
125+
}
111126
}
112127

113128
static class PlanParamBase extends BaseTypeImpl.BaseCallImpl<XsValueImpl.StringValImpl> implements PlanParamExpr {

marklogic-client-api/src/main/java/com/marklogic/client/impl/PlanBuilderSubImpl.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ public PlanBuilder.AccessPlan fromSearchDocs(CtsQueryExpr query) {
4848
}
4949
@Override
5050
public PlanBuilder.AccessPlan fromSearchDocs(CtsQueryExpr query, String qualifierName) {
51-
return fromSearchDocs(query, null, null);
51+
return fromSearchDocs(query, qualifierName, null);
5252
}
5353
@Override
5454
public PlanBuilder.AccessPlan fromSearchDocs(CtsQueryExpr query, String qualifierName, PlanSearchOptions options) {
@@ -694,7 +694,7 @@ public PlanPrefixer prefixer(String base) {
694694
public PlanParamExpr param(String name) {
695695
return new PlanParamBase(name);
696696
}
697-
697+
698698
@Override
699699
public PlanParamExpr param(XsStringVal name) {
700700
if (name == null) {

marklogic-client-api/src/main/java/com/marklogic/client/type/PlanSearchOptions.java

Lines changed: 35 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,15 @@
11
/*
2-
* Copyright (c) 2010-2025 Progress Software Corporation and/or its subsidiaries or affiliates. All Rights Reserved.
2+
* Copyright (c) 2010-2026 Progress Software Corporation and/or its subsidiaries or affiliates. All Rights Reserved.
33
*/
44
package com.marklogic.client.type;
55

66
/**
7-
* An option controlling the scoring and weighting of fromSearch()
8-
* for a row pipeline.
7+
* Options controlling the scoring, weighting, and fragment scope for {@code fromSearch()} and
8+
* {@code fromSearchDocs()} in a row pipeline. Use {@link #withFragment(Fragment)} to select which
9+
* fragment types (document, properties, locks, or any) are searched and returned.
10+
*
11+
* <p>Fragment scope support was added in release 8.2.0 and requires MarkLogic 12.1 or higher.
12+
* Scoring and weighting options apply to all supported MarkLogic versions.</p>
913
*/
1014
public interface PlanSearchOptions {
1115

@@ -38,6 +42,34 @@ public interface PlanSearchOptions {
3842
*/
3943
PlanSearchOptions withBm25LengthWeight(double bm25LengthWeight);
4044

45+
/**
46+
* @since 8.2.0; requires MarkLogic 12.1 or higher.
47+
*/
48+
Fragment getFragment();
49+
50+
/**
51+
* Specifies the type of fragment to search and return. Defaults to {@link Fragment#DOCUMENT} when no option
52+
* is specified. Applies to both {@code fromSearch()} and {@code fromSearchDocs()}.
53+
*
54+
* @param fragment the fragment scope to select
55+
* @return a new PlanSearchOptions with the fragment set
56+
* @since 8.2.0; requires MarkLogic 12.1 or higher.
57+
*/
58+
PlanSearchOptions withFragment(Fragment fragment);
59+
60+
/**
61+
* Controls which type of fragments are searched and returned by {@code fromSearch()} and
62+
* {@code fromSearchDocs()}.
63+
*
64+
* @since 8.2.0; requires MarkLogic 12.1 or higher.
65+
*/
66+
enum Fragment {
67+
DOCUMENT,
68+
ANY,
69+
PROPERTIES,
70+
LOCKS
71+
}
72+
4173
enum ScoreMethod {
4274
LOGTFIDF,
4375
LOGTF,
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
/*
2+
* Copyright (c) 2010-2026 Progress Software Corporation and/or its subsidiaries or affiliates. All Rights Reserved.
3+
*/
4+
package com.marklogic.client.test.junit5;
5+
6+
import com.marklogic.client.test.Common;
7+
import com.marklogic.client.test.MarkLogicVersion;
8+
import org.junit.jupiter.api.extension.ConditionEvaluationResult;
9+
import org.junit.jupiter.api.extension.ExecutionCondition;
10+
import org.junit.jupiter.api.extension.ExtensionContext;
11+
12+
public class RequiresML12Dot1 implements ExecutionCondition {
13+
14+
private static MarkLogicVersion markLogicVersion;
15+
16+
@Override
17+
public ConditionEvaluationResult evaluateExecutionCondition(ExtensionContext context) {
18+
if (markLogicVersion == null) {
19+
markLogicVersion = Common.getMarkLogicVersion();
20+
}
21+
boolean supported =
22+
(markLogicVersion.getMajor() == 12 && markLogicVersion.getMinor() != null && markLogicVersion.getMinor() >= 1) ||
23+
markLogicVersion.getMajor() > 12;
24+
return supported ?
25+
ConditionEvaluationResult.enabled("MarkLogic is version 12.1 or higher") :
26+
ConditionEvaluationResult.disabled("MarkLogic is version 12.0.x or lower");
27+
}
28+
}
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
/*
2+
* Copyright (c) 2010-2026 Progress Software Corporation and/or its subsidiaries or affiliates. All Rights Reserved.
3+
*/
4+
package com.marklogic.client.test.rows;
5+
6+
import com.marklogic.client.test.Common;
7+
import org.junit.jupiter.api.AfterEach;
8+
import org.junit.jupiter.api.BeforeEach;
9+
10+
import java.util.List;
11+
12+
/**
13+
* Shared test fixture for {@link FromSearchWithFragmentTest} and
14+
* {@link FromSearchDocsWithFragmentTest}. Holds the XQuery setup/teardown scripts and
15+
* expected URI constants so that changes to the test documents only need to be made in
16+
* one place.
17+
*/
18+
abstract class AbstractFromSearchFragmentTest extends AbstractOpticUpdateTest {
19+
20+
static final String SETUP_XQUERY =
21+
"xquery version '1.0-ml';" +
22+
"let $jsondoc1 := object-node {'AllDataTypes': array-node {object-node {'word':'dog'}, object-node {'rank':1}, object-node {'score':4}}}" +
23+
"let $jsondoc2 := object-node {'AllDataTypes': array-node {object-node {'word':'cat'}, object-node {'rank':2}, object-node {'score':5}}}" +
24+
"let $jsondoc3 := object-node {'AllDataTypes': array-node {object-node {'word':'duck'}, object-node {'rank':3}, object-node {'score':6}}}" +
25+
"return (" +
26+
"xdmp:document-insert('range-prop-1.json', $jsondoc1, xdmp:default-permissions(), ('elemCol','jsondoc-range','from-search-fragment-test'))," +
27+
"xdmp:document-insert('range-prop-2.json', $jsondoc2, xdmp:default-permissions(), ('elemCol','jsondoc-range','from-search-fragment-test'))," +
28+
"xdmp:document-insert('range-prop-3.json', $jsondoc3, xdmp:default-permissions(), ('elemCol','jsondoc-range','from-search-fragment-test'))," +
29+
"xdmp:document-set-properties('range-prop-1.json', (<my-prop>opticfragmentpropvalue prop1value</my-prop>))," +
30+
"xdmp:document-set-properties('range-prop-2.json', (<my-prop>opticfragmentpropvalue prop2value</my-prop>))," +
31+
"xdmp:document-set-properties('range-prop-3.json', (<my-prop>opticfragmentpropvalue prop3value</my-prop>))," +
32+
"xdmp:lock-acquire('range-prop-1.json', 'exclusive', '0', 'dog rose', xs:unsignedLong(120))," +
33+
"xdmp:lock-acquire('range-prop-2.json', 'exclusive', '0', 'cat tulip', xs:unsignedLong(120))," +
34+
"xdmp:lock-acquire('range-prop-3.json', 'exclusive', '0', 'duck lily', xs:unsignedLong(120))" +
35+
")";
36+
37+
static final String TEARDOWN_XQUERY =
38+
"xquery version '1.0-ml';" +
39+
"for $uri in ('range-prop-1.json', 'range-prop-2.json', 'range-prop-3.json') return xdmp:document-delete($uri)";
40+
41+
static final List<String> EXPECTED_URIS = List.of(
42+
"range-prop-1.json", "range-prop-2.json", "range-prop-3.json");
43+
44+
@BeforeEach
45+
void setupTest() {
46+
rowManager.withUpdate(false);
47+
Common.newEvalClient().newServerEval().xquery(SETUP_XQUERY).evalAs(String.class);
48+
}
49+
50+
@AfterEach
51+
void teardownTest() {
52+
Common.newEvalClient().newServerEval().xquery(TEARDOWN_XQUERY).evalAs(String.class);
53+
}
54+
}

0 commit comments

Comments
 (0)