Skip to content

Commit ce67c68

Browse files
committed
OAK-11735 Index merge: merge aggregation definitions
1 parent 5d1e75f commit ce67c68

File tree

3 files changed

+184
-0
lines changed

3 files changed

+184
-0
lines changed

oak-run/src/main/java/org/apache/jackrabbit/oak/index/merge/IndexDefMergerUtils.java

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@
2828
import java.util.Map.Entry;
2929
import java.util.Objects;
3030
import java.util.TreeSet;
31+
import java.util.regex.Matcher;
32+
import java.util.regex.Pattern;
3133
import java.util.stream.Collectors;
3234

3335
import org.apache.jackrabbit.oak.commons.json.JsonObject;
@@ -270,6 +272,9 @@ public static String getStringOrStringFromSingleValueArray(String value) {
270272

271273
private static JsonObject mergeChild(String path, String child, int level, JsonObject ancestor, JsonObject custom, JsonObject product,
272274
ArrayList<String> conflicts) {
275+
if (level == 1 && path.indexOf("/aggregates/") >= 0) {
276+
return mergeAggregates(path, child, level, ancestor, custom, product, conflicts);
277+
}
273278
JsonObject a = ancestor.getChildren().get(child);
274279
JsonObject c = custom.getChildren().get(child);
275280
JsonObject p = product.getChildren().get(child);
@@ -287,6 +292,81 @@ private static JsonObject mergeChild(String path, String child, int level, JsonO
287292
}
288293
}
289294

295+
private static JsonObject mergeAggregates(String path, String child, int level, JsonObject ancestor, JsonObject custom, JsonObject product,
296+
ArrayList<String> conflicts) {
297+
298+
// merge, with level + 1 so that we don't recurse into this function again
299+
// conflicts are redirected to a new, temporary list
300+
ArrayList<String> aggregateConflicts = new ArrayList<>();
301+
JsonObject merged = mergeChild(path, child, level + 1, ancestor, custom, product, aggregateConflicts);
302+
303+
// if there were conflicts, resolve them
304+
if (!aggregateConflicts.isEmpty()) {
305+
306+
// list of "include" elements to move to the end
307+
ArrayList<JsonObject> elementToMove = new ArrayList<>();
308+
309+
// which is the next id for "include" (eg. 12)
310+
long nextIncludeId = getNextIncludeId(ancestor.getChildren().get(child));
311+
nextIncludeId = Math.max(nextIncludeId, getNextIncludeId(custom.getChildren().get(child)));
312+
nextIncludeId = Math.max(nextIncludeId, getNextIncludeId(product.getChildren().get(child)));
313+
314+
// loop over conflicts, and find + remove these
315+
// the aggregateConflicts will contain entries that look like this:
316+
// "Could not merge value; path=/oak:index/assets-11/aggregates/asset/include11
317+
// property=path; ancestor=null; custom=...; product=..."
318+
// and we need to extract the path
319+
for (String n : aggregateConflicts) {
320+
String regex = "path=([^\\s]+)\\sproperty=";
321+
Pattern pattern = Pattern.compile(regex);
322+
Matcher matcher = pattern.matcher(n);
323+
if (matcher.find()) {
324+
// the path of the conflicting aggregation node
325+
String extractedPath = matcher.group(1);
326+
String[] elements = extractedPath.split("/");
327+
String conflictElement = elements[elements.length - 1];
328+
329+
// remove from the custom list
330+
JsonObject conflict = custom.getChildren().get(child).getChildren().remove(conflictElement);
331+
332+
// remember the element, to put it back later
333+
elementToMove.add(conflict);
334+
}
335+
}
336+
337+
// merge again, with conflicts resolved now
338+
// (if there are other conflicts unrelated to aggregation,
339+
// those will not be resolved)
340+
merged = mergeChild(path, child, level + 1, ancestor, custom, product, conflicts);
341+
342+
// add the aggregation conflict at the end, with new ids
343+
// first we need to clone the merged object,
344+
// because it might be the same object as the product currently
345+
merged = JsonObject.fromJson(merged.toString(), true);
346+
for (JsonObject json : elementToMove) {
347+
merged.getChildren().put("include" + nextIncludeId, json);
348+
nextIncludeId++;
349+
}
350+
}
351+
return merged;
352+
}
353+
354+
private static long getNextIncludeId(JsonObject json) {
355+
long max = 0;
356+
for(String n : json.getChildren().keySet()) {
357+
if (n.startsWith("include")) {
358+
n = n.substring("include".length());
359+
try {
360+
long id = Long.parseLong(n);
361+
max = Math.max(max, id);
362+
} catch (NumberFormatException e) {
363+
// ignore
364+
}
365+
}
366+
}
367+
return max + 1;
368+
}
369+
290370
private static boolean isSameJson(JsonObject a, JsonObject b) {
291371
if (a == null || b == null) {
292372
return a == null && b == null;

oak-run/src/test/java/org/apache/jackrabbit/oak/index/merge/IndexDefMergerScenariosTest.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ public class IndexDefMergerScenariosTest extends ParameterizedMergingTestBase {
4848
public static Collection<Object[]> data() {
4949
return Arrays.asList(new Object[][] {
5050
testCase("should merge tags fully; and override type", "merge-override-tags-type.json"),
51+
testCase("should merge aggregates", "merge-aggregates.json"),
5152
testCase("should merge custom into new base index", "basic.json"),
5253
testCase("should use the latest base version for the base in merges", "merges-base.json"),
5354
testCase(
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
{
2+
"build": {
3+
"/oak:index/lucene-2": {
4+
"jcr:primaryType": "nam:oak:QueryIndexDefinition",
5+
":version": 2,
6+
"tags": ["similarity", "asset", "fragments"],
7+
"type": "elasticsearch",
8+
"async": "elastic-async",
9+
"reindex": false,
10+
"reindexCount": 1,
11+
"aggregates": {
12+
"asset": {
13+
"include0": {
14+
"path": "a"
15+
},
16+
"include1": {
17+
"path": "b"
18+
}
19+
}
20+
}
21+
}
22+
},
23+
24+
"run": {
25+
"/oak:index/lucene-1": {
26+
"jcr:primaryType": "nam:oak:QueryIndexDefinition",
27+
":version": 2,
28+
"tags": ["similarity", "asset"],
29+
"type": "disabled",
30+
"async": ["async", "nrt"],
31+
"reindex": false,
32+
"reindexCount": 1,
33+
"aggregates": {
34+
"asset": {
35+
"include0": {
36+
"path": "a"
37+
}
38+
}
39+
}
40+
},
41+
"/oak:index/lucene-1-custom-1": {
42+
"jcr:primaryType": "nam:oak:QueryIndexDefinition",
43+
":version": 2,
44+
"tags": "custom",
45+
"type": "lucene",
46+
"async": "async",
47+
"reindex": false,
48+
"reindexCount": 1,
49+
"aggregates": {
50+
"asset": {
51+
"include0": {
52+
"path": "a"
53+
},
54+
"include1": {
55+
"path": "xzy"
56+
}
57+
}
58+
}
59+
}
60+
},
61+
62+
"expected": {
63+
"/oak:index/lucene-2": {
64+
"jcr:primaryType": "nam:oak:QueryIndexDefinition",
65+
":version": 2,
66+
"tags": ["similarity", "asset", "fragments"],
67+
"type": "elasticsearch",
68+
"async": "elastic-async",
69+
"reindex": false,
70+
"reindexCount": 1,
71+
"aggregates": {
72+
"asset": {
73+
"include0": {
74+
"path": "a"
75+
},
76+
"include1": {
77+
"path": "b"
78+
}
79+
}
80+
}
81+
},
82+
"/oak:index/lucene-2-custom-1": {
83+
"jcr:primaryType": "nam:oak:QueryIndexDefinition",
84+
"tags": ["asset", "custom", "fragments", "similarity"],
85+
"type": "elasticsearch",
86+
"async": "elastic-async",
87+
"merges": ["/oak:index/lucene-2", "/oak:index/lucene-1-custom-1"],
88+
"aggregates": {
89+
"asset": {
90+
"include0": {
91+
"path": "a"
92+
},
93+
"include1": {
94+
"path": "b"
95+
},
96+
"include2": {
97+
"path": "xzy"
98+
}
99+
}
100+
}
101+
}
102+
}
103+
}

0 commit comments

Comments
 (0)