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 th...@apache.org on 2026/08/07 13:25:26 UTC
(jackrabbit-oak) branch trunk updated: OAK-12340 Simplified index management improvements (#3068)
This is an automated email from the ASF dual-hosted git repository.
thomasmueller pushed a commit to branch trunk
in repository https://gitbox.apache.org/repos/asf/jackrabbit-oak.git
The following commit(s) were added to refs/heads/trunk by this push:
new 4984c4cf26 OAK-12340 Simplified index management improvements (#3068)
4984c4cf26 is described below
commit 4984c4cf26a7ca58ae9ce12c63190b7f492bda78
Author: Thomas Mueller <th...@apache.org>
AuthorDate: Fri Aug 7 15:25:21 2026 +0200
OAK-12340 Simplified index management improvements (#3068)
* OAK-12340 Simplified index management improvements
* OAK-12340 Simplified index management improvements
---
.../oak/plugins/index/diff/DiffIndexMerger.java | 103 +-
.../plugins/index/diff/DiffIndexMergerTest.java | 1412 ++++++++++++++++++++
2 files changed, 1505 insertions(+), 10 deletions(-)
diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/diff/DiffIndexMerger.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/diff/DiffIndexMerger.java
index 25346fc9e6..24a49e1e70 100644
--- a/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/diff/DiffIndexMerger.java
+++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/diff/DiffIndexMerger.java
@@ -88,7 +88,7 @@ public class DiffIndexMerger {
// set of properties that are allowed to be changed if the property already exists
private static final Set<String> ALLOW_CHANGING_IN_EXISTING_PROPERTY = Set.of(
- "boost", "weight");
+ "boost", "weight", "secure");
// set of properties that allow multi-valued string that might be merged
private static final Set<String> MERGE_MULTI_VALUES_STRINGS = Set.of(
@@ -163,6 +163,64 @@ public class DiffIndexMerger {
mergeDiff(newImageLuceneDefinitions, combined);
}
+ /**
+ * Remove "diff.index" and/or "diff.index.optimizer" from
+ * newImageLuceneDefinitions, if their content is unchanged compared to what
+ * is already stored in the writable repository. This avoids writing (and
+ * later re-committing, see ReindexCmd) an index definition that didn't
+ * actually change.
+ *
+ * @param newImageLuceneDefinitions
+ * the new Lucene definitions
+ * (input + output)
+ * @param repositoryNodeStore
+ * the writable repository, or null if not available
+ * (eg. in dry-run mode or tests)
+ */
+ public void removeUnchangedDiffIndexEntries(JsonObject newImageLuceneDefinitions, NodeStore repositoryNodeStore) {
+ if (repositoryNodeStore == null) {
+ // can't compare, so keep the entries as-is
+ return;
+ }
+ removeIfUnchanged(newImageLuceneDefinitions, repositoryNodeStore, DIFF_INDEX);
+ removeIfUnchanged(newImageLuceneDefinitions, repositoryNodeStore, DIFF_INDEX_OPTIMIZER);
+ }
+
+ private void removeIfUnchanged(JsonObject newImageLuceneDefinitions, NodeStore repositoryNodeStore, String name) {
+ String path = "/oak:index/" + name;
+ JsonObject newEntry = newImageLuceneDefinitions.getChildren().get(path);
+ if (newEntry == null) {
+ return;
+ }
+ Map<String, JsonObject> currentInRepo = readDiffIndex(repositoryNodeStore, name);
+ JsonObject currentEntry = currentInRepo.get(path);
+ if (currentEntry == null) {
+ // nothing stored yet in the repository: keep the new entry
+ return;
+ }
+ JsonObject cleanedNewEntry = cleanedAndNormalized(newEntry);
+ if (isSameIgnorePropertyOrder(cleanedNewEntry, currentEntry)) {
+ log("{} unchanged from repository, skipping", path);
+ newImageLuceneDefinitions.getChildren().remove(path);
+ }
+ }
+
+ /**
+ * Remove "diff.index" and "diff.index.optimizer" from the given
+ * definitions, if present. These are not real index definitions: they are
+ * processed and persisted only by the Lucene ReindexCmd (see
+ * ReindexCmd.storeIndexDiffInRepository), and must never be handed to any
+ * other pipeline (eg. the Elastic reindex/import jobs, which don't know
+ * how to handle a "lucene"-typed index definition and would fail).
+ *
+ * @param definitions the definitions to remove the entries from (eg. the
+ * Elastic definitions)
+ */
+ public static void removeDiffIndexEntries(JsonObject definitions) {
+ definitions.getChildren().remove("/oak:index/" + DIFF_INDEX);
+ definitions.getChildren().remove("/oak:index/" + DIFF_INDEX_OPTIMIZER);
+ }
+
/**
* If there is a diff index (hardcoded node "/oak:index/diff.index" or
* "/oak:index/diff.index.optimizer"), then iterate over all entries and create new
@@ -278,6 +336,15 @@ public class DiffIndexMerger {
if (key.indexOf("-custom-") < 0 || !value.getProperties().containsKey("mergeInfo")) {
continue;
}
+ String type = JsonNodeUpdater.oakStringValue(value, "type");
+ if (type != null && "disabled".equals(type)) {
+ // ignore disabled indexes:
+ // only if the version that is in use contains the "mergeInfo" property,
+ // then simplified index management is currently used
+ // (otherwise we create a new merged info even for environments that no longer use it,
+ // if there are indexes that are not yet deleted)
+ continue;
+ }
String baseName = IndexName.parse(key.substring("/oak:index/".length())).getBaseName();
if (!target.containsKey(baseName)) {
// if there is no entry yet for this key,
@@ -405,7 +472,6 @@ public class DiffIndexMerger {
} else {
merged = processMerge(indexName, latestProductIndex, indexDiff);
}
-
// compare to the latest version of the this index
JsonObject latestIndexVersion = new JsonObject();
if (latestCustomized == null) {
@@ -424,7 +490,10 @@ public class DiffIndexMerger {
} else {
String latestMergeChecksum = JsonNodeUpdater.oakStringValue(latestIndexVersion, "mergeChecksum");
JsonObject latestDef = cleanedAndNormalized(switchToLuceneIfNeeded(latestIndexVersion));
- if (isSameIgnorePropertyOrder(mergedDef, latestDef)) {
+ if (latestMergeChecksum == null && latestCustomized != null) {
+ log("mergeChecksum is missing in the old customized version, so this is a migration: {}", indexName);
+ // continue
+ } else if (isSameIgnorePropertyOrder(mergedDef, latestDef)) {
// normal case: no change
// (even if checksums do not match: checksums might be missing or manipulated)
log("Latest index matches");
@@ -448,9 +517,15 @@ public class DiffIndexMerger {
// a new merged index definition
if (latestProduct == null) {
// fully custom index: increment version
- key = prefix + indexName +
- "-" + latestCustomized.getProductVersion() +
- "-custom-" + (latestCustomized.getCustomerVersion() + 1);
+ if (latestCustomized == null) {
+ // in reality, this would mean latestIndexVersion,
+ // but the additional protection can't hurt
+ key = prefix + indexName + "-1-custom-1";
+ } else {
+ key = prefix + indexName +
+ "-" + latestCustomized.getProductVersion() +
+ "-custom-" + (latestCustomized.getCustomerVersion() + 1);
+ }
} else {
// customized OOTB index: use the latest product as the base
int productVersion = latestProduct.getProductVersion();
@@ -466,14 +541,22 @@ public class DiffIndexMerger {
}
}
}
+ if (merged == null) {
+ // this is only a theoretical case (doesn't happen with the current code)
+ // but the additional protection can't hurt
+ logAndCollectWarn("Logic error: no merged index " + indexName);
+ return false;
+ }
merged.getProperties().put("mergeInfo", JsopBuilder.encode(MERGE_INFO));
merged.getProperties().put("mergeChecksum", JsopBuilder.encode(mergeChecksum));
merged.getProperties().put("merges", "[" + JsopBuilder.encode("/oak:index/" + indexName) + "]");
merged.getProperties().remove("reindexCount");
merged.getProperties().remove("reindex");
- if (!deleteCopiesOutOfTheBoxIndex && indexDiff.toString().equals("{}")) {
- merged.getProperties().put("type", "\"disabled\"");
- merged.getProperties().put("mergeComment", "\"This index is superseded and can be removed\"");
+ if (indexDiff != null && indexDiff.toString().equals("{}")) {
+ if (!deleteCopiesOutOfTheBoxIndex && !deleteCreatesDummyIndex) {
+ merged.getProperties().put("type", "\"disabled\"");
+ merged.getProperties().put("mergeComment", "\"This index is superseded and can be removed\"");
+ }
}
newImageLuceneDefinitions.getChildren().put(key, merged);
return true;
@@ -802,7 +885,7 @@ public class DiffIndexMerger {
}
mergeInto(indexName, path + "/" + targetChildName, diff.getChildren().get(c), target.getChildren().get(targetChildName), childIsNew);
}
- if (target.getProperties().isEmpty() && target.getChildren().isEmpty()) {
+ if (path.isEmpty() && target.getProperties().isEmpty() && target.getChildren().isEmpty()) {
if (deleteCreatesDummyIndex) {
// dummy index
target.getProperties().put("async", "\"async\"");
diff --git a/oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/index/diff/DiffIndexMergerTest.java b/oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/index/diff/DiffIndexMergerTest.java
new file mode 100644
index 0000000000..9d0d559f33
--- /dev/null
+++ b/oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/index/diff/DiffIndexMergerTest.java
@@ -0,0 +1,1412 @@
+/*
+ * 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.plugins.index.diff;
+
+import static org.junit.Assert.assertEquals;
+
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.util.Base64;
+
+import org.apache.jackrabbit.oak.api.CommitFailedException;
+import org.apache.jackrabbit.oak.commons.json.JsonObject;
+import org.apache.jackrabbit.oak.plugins.memory.MemoryNodeStore;
+import org.apache.jackrabbit.oak.spi.commit.CommitInfo;
+import org.apache.jackrabbit.oak.spi.commit.EmptyHook;
+import org.apache.jackrabbit.oak.spi.state.NodeBuilder;
+import org.apache.jackrabbit.oak.spi.state.NodeStore;
+import org.junit.Test;
+
+public class DiffIndexMergerTest {
+
+ public static DiffIndexMerger getMerger() {
+ DiffIndexMerger merger = new DiffIndexMerger().
+ setDeleteCopiesOutOfTheBoxIndex(true).
+ setDeleteCreatesDummyIndex(true).
+ setLogAtInfoLevel(true).
+ setUnsupportedIncludedPaths(new String[] {"/apps", "/libs"});
+ return merger;
+ }
+
+ @Test
+ public void noCustomization() {
+ JsonObject newImageLuceneDefinitions = JsonObject.fromJson("{}", true);
+ JsonObject repositoryDefinitions = JsonObject.fromJson("""
+ {
+ "/oak:index/damAssetLucene-12": {
+ "jcr:primaryType": "oak:IndexDefinition",
+ "type": "lucene",
+ "async": ["async", "nrt"],
+ "tags": ["abc"],
+ "indexRules": {
+ "dam:Asset": {
+ "properties": {
+ "x": {
+ "name": "x",
+ "propertyIndex": true
+ }
+ }
+ }
+ }
+ },
+ "/oak:index/diff.index": {
+ "jcr:primaryType": "nt:unstructured",
+ "type": "lucene", "includedPaths": "/a", "queryPaths": "/a"
+ }
+ }
+ """, true);
+
+ getMerger().merge(newImageLuceneDefinitions, repositoryDefinitions, null);
+ assertEquals("{}", newImageLuceneDefinitions.toString());
+ }
+
+ // an index was customized in the past, but not any longer
+ @Test
+ public void noCustomizationAnyLonger() {
+ JsonObject newImageLuceneDefinitions = JsonObject.fromJson("{}", true);
+ JsonObject repositoryDefinitions = JsonObject.fromJson("""
+ {
+ "/oak:index/damAssetLucene-12": {
+ "jcr:primaryType": "oak:IndexDefinition",
+ "type": "lucene",
+ "async": ["async", "nrt"],
+ "includedPaths": "/content/dam",
+ "tags": ["abc"],
+ "indexRules": {
+ "dam:Asset": {
+ "properties": {
+ "x": {
+ "name": "x",
+ "propertyIndex": true
+ }
+ }
+ }
+ }
+ },
+ "/oak:index/damAssetLucene-12-custom-1": {
+ "jcr:primaryType": "oak:IndexDefinition",
+ "type": "lucene",
+ "async": ["async", "nrt"],
+ "tags": ["abc"],
+ "includedPaths": "/content/dam",
+ "mergeInfo": "This index was auto-merged. See also https://thomasmueller.github.io/oakTools/simplified.html",
+ "mergeChecksum": "ef0312b8bf7cc97bb8efd0faeeb4e4b1094268694a2df749b73fac08987b3264",
+ "merges": ["/oak:index/damAssetLucene"],
+ "indexRules": {
+ "jcr:primaryType": "nam:nt:unstructured",
+ "dam:Asset": {
+ "jcr:primaryType": "nam:nt:unstructured",
+ "properties": {
+ "jcr:primaryType": "nam:nt:unstructured",
+ "x": {
+ "name": "x",
+ "propertyIndex": true,
+ "jcr:primaryType": "nam:nt:unstructured"
+ },
+ "y": {
+ "name": "y",
+ "propertyIndex": true,
+ "jcr:primaryType": "nam:nt:unstructured"
+ }
+ }
+ }
+ }
+ },
+ "/oak:index/diff.index": {
+ "jcr:primaryType": "nt:unstructured",
+ "type": "lucene", "includedPaths": "/a", "queryPaths": "/a"
+ }
+ }
+ """, true);
+ // we can not just move back to /oak:index/damAssetLucene-12, because
+ // that index has a lower version and so is then not use for queries.
+ // we need to explicitly create a new version.
+ // once damAssetLucene-13 is rolled out, then no "-custom-" is created however
+ getMerger().merge(newImageLuceneDefinitions, repositoryDefinitions, null);
+ assertEquals("""
+ {
+ "/oak:index/damAssetLucene-12-custom-2": {
+ "jcr:primaryType": "oak:IndexDefinition",
+ "type": "lucene",
+ "async": ["async", "nrt"],
+ "includedPaths": "/content/dam",
+ "tags": ["abc"],
+ "mergeInfo": "This index was auto-merged. See also https://oak-indexing.github.io/oakTools/simplified.html",
+ "mergeChecksum": "aa02b93e0ba4ff4bcffb8247d38c5cc039be5afa915f822e18df7a578fabff76",
+ "merges": ["/oak:index/damAssetLucene"],
+ "indexRules": {
+ "jcr:primaryType": "nam:nt:unstructured",
+ "dam:Asset": {
+ "jcr:primaryType": "nam:nt:unstructured",
+ "properties": {
+ "jcr:primaryType": "nam:nt:unstructured",
+ "x": {
+ "name": "x",
+ "propertyIndex": true,
+ "jcr:primaryType": "nam:nt:unstructured"
+ }
+ }
+ }
+ }
+ }
+ }""", newImageLuceneDefinitions.toString());
+ }
+
+ @Test
+ public void firstCustomization() {
+ JsonObject newImageLuceneDefinitions = JsonObject.fromJson("{}", true);
+ JsonObject indexDiff = JsonObject.fromJson("""
+ {
+ "damAssetLucene": {
+ "indexRules": {
+ "dam:Asset": {
+ "properties": {
+ "y": {
+ "name": "y",
+ "propertyIndex": true
+ }
+ }
+ }
+ }
+ }
+ }
+ """, true);
+ String indexDiffString = indexDiff.toString();
+ String base64Prop =
+ "\":blobId:" + Base64.getEncoder().encodeToString(indexDiffString.getBytes(StandardCharsets.UTF_8)) + "\"";
+ JsonObject repositoryDefinitions = JsonObject.fromJson("""
+ {
+ "/oak:index/damAssetLucene-12": {
+ "jcr:primaryType": "oak:IndexDefinition",
+ "type": "lucene",
+ "async": ["async", "nrt"],
+ "tags": ["abc"],
+ "includedPaths": "/content/dam",
+ "indexRules": {
+ "dam:Asset": {
+ "properties": {
+ "x": {
+ "name": "x",
+ "propertyIndex": true
+ }
+ }
+ }
+ }
+ },
+ "/oak:index/diff.index": {
+ "jcr:primaryType": "nt:unstructured",
+ "type": "lucene", "includedPaths": "/same", "queryPaths": "/same",
+ "diff.json": {
+ "jcr:primaryType": "nam:nt:file",
+ "jcr:content": {
+ "jcr:primaryType": "nam:nt:resource",
+ "jcr:mimeType": "application/json",
+ "jcr:data":
+ """ + base64Prop + """
+ }
+ }
+ }
+ }
+ """, true);
+
+ getMerger().merge(newImageLuceneDefinitions, repositoryDefinitions, null);
+ assertEquals("""
+ {
+ "/oak:index/damAssetLucene-12-custom-1": {
+ "jcr:primaryType": "oak:IndexDefinition",
+ "type": "lucene",
+ "async": ["async", "nrt"],
+ "tags": ["abc"],
+ "includedPaths": "/content/dam",
+ "mergeInfo": "This index was auto-merged. See also https://oak-indexing.github.io/oakTools/simplified.html",
+ "mergeChecksum": "ef0312b8bf7cc97bb8efd0faeeb4e4b1094268694a2df749b73fac08987b3264",
+ "merges": ["/oak:index/damAssetLucene"],
+ "indexRules": {
+ "jcr:primaryType": "nam:nt:unstructured",
+ "dam:Asset": {
+ "jcr:primaryType": "nam:nt:unstructured",
+ "properties": {
+ "jcr:primaryType": "nam:nt:unstructured",
+ "x": {
+ "name": "x",
+ "propertyIndex": true,
+ "jcr:primaryType": "nam:nt:unstructured"
+ },
+ "y": {
+ "name": "y",
+ "propertyIndex": true,
+ "jcr:primaryType": "nam:nt:unstructured"
+ }
+ }
+ }
+ }
+ }
+ }""", newImageLuceneDefinitions.toString());
+ }
+
+ @Test
+ public void firstOptimizerCustomization() {
+ JsonObject newImageLuceneDefinitions = JsonObject.fromJson("{}", true);
+ // notice the the diff.index.optimizer is already stored in the repo,
+ // and so does not need to appear in the newImageLuceneDefinitions
+ JsonObject repositoryDefinitions = JsonObject.fromJson("""
+ {
+ "/oak:index/damAssetLucene-12": {
+ "jcr:primaryType": "oak:IndexDefinition",
+ "type": "lucene",
+ "async": ["async", "nrt"],
+ "tags": ["abc"],
+ "includedPaths": "/content/dam",
+ "indexRules": {
+ "dam:Asset": {
+ "properties": {
+ "x": {
+ "name": "x",
+ "propertyIndex": true
+ }
+ }
+ }
+ }
+ },
+ "/oak:index/diff.index.optimizer": {
+ "jcr:primaryType": "nt:unstructured",
+ "type": "lucene", "includedPaths": "/same", "queryPaths": "/same",
+ "diff": {
+ "damAssetLucene": {
+ "indexRules": {
+ "dam:Asset": {
+ "properties": {
+ "y": {
+ "name": "y",
+ "propertyIndex": true
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ """, true);
+
+ getMerger().merge(newImageLuceneDefinitions, repositoryDefinitions, null);
+ assertEquals("""
+ {
+ "/oak:index/damAssetLucene-12-custom-1": {
+ "jcr:primaryType": "oak:IndexDefinition",
+ "type": "lucene",
+ "async": ["async", "nrt"],
+ "tags": ["abc"],
+ "includedPaths": "/content/dam",
+ "mergeInfo": "This index was auto-merged. See also https://oak-indexing.github.io/oakTools/simplified.html",
+ "mergeChecksum": "ef0312b8bf7cc97bb8efd0faeeb4e4b1094268694a2df749b73fac08987b3264",
+ "merges": ["/oak:index/damAssetLucene"],
+ "indexRules": {
+ "jcr:primaryType": "nam:nt:unstructured",
+ "dam:Asset": {
+ "jcr:primaryType": "nam:nt:unstructured",
+ "properties": {
+ "jcr:primaryType": "nam:nt:unstructured",
+ "x": {
+ "name": "x",
+ "propertyIndex": true,
+ "jcr:primaryType": "nam:nt:unstructured"
+ },
+ "y": {
+ "name": "y",
+ "propertyIndex": true,
+ "jcr:primaryType": "nam:nt:unstructured"
+ }
+ }
+ }
+ }
+ }
+ }""", newImageLuceneDefinitions.toString());
+ }
+
+ @Test
+ public void newIndexAndNewCustomization() {
+ JsonObject repositoryDefinitions = JsonObject.fromJson("""
+ {
+ "/oak:index/damAssetLucene-11": {
+ "jcr:primaryType": "nam:oak:IndexDefinition",
+ "type": "lucene",
+ "async": ["async", "nrt"],
+ "tags": ["abc"],
+ "includedPaths": "/content/dam",
+ "indexRules": {
+ "dam:Asset": {
+ "properties": {
+ "a": {
+ "name": "a",
+ "propertyIndex": true
+ }
+ }
+ }
+ }
+ }
+ }
+ """, true);
+ // notice the the diff.index is not yet stored in the repo,
+ // and so _does_ need to appear in the newImageLuceneDefinitions
+ JsonObject newImageLuceneDefinitions = JsonObject.fromJson("""
+ {
+ "/oak:index/damAssetLucene-12": {
+ "jcr:primaryType": "nam:oak:IndexDefinition",
+ "type": "lucene",
+ "async": ["async", "nrt"],
+ "tags": ["abc"],
+ "includedPaths": "/content/dam",
+ "indexRules": {
+ "dam:Asset": {
+ "properties": {
+ "x": {
+ "name": "x",
+ "propertyIndex": true
+ }
+ }
+ }
+ }
+ },
+ "/oak:index/diff.index": {
+ "jcr:primaryType": "nam:nt:unstructured",
+ "type": "lucene", "includedPaths": "/same", "queryPaths": "/same",
+ "diff": {
+ "damAssetLucene": {
+ "indexRules": {
+ "dam:Asset": {
+ "properties": {
+ "y": {
+ "name": "y",
+ "propertyIndex": true
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ """, true);
+
+ getMerger().merge(newImageLuceneDefinitions, repositoryDefinitions, null);
+ assertEquals("""
+ {
+ "/oak:index/damAssetLucene-12": {
+ "jcr:primaryType": "nam:oak:IndexDefinition",
+ "type": "lucene",
+ "async": ["async", "nrt"],
+ "tags": ["abc"],
+ "includedPaths": "/content/dam",
+ "indexRules": {
+ "dam:Asset": {
+ "properties": {
+ "x": {
+ "name": "x",
+ "propertyIndex": true
+ }
+ }
+ }
+ }
+ },
+ "/oak:index/diff.index": {
+ "jcr:primaryType": "nam:nt:unstructured",
+ "type": "lucene",
+ "includedPaths": "/same",
+ "queryPaths": "/same",
+ "diff": {
+ "damAssetLucene": {
+ "indexRules": {
+ "dam:Asset": {
+ "properties": {
+ "y": {
+ "name": "y",
+ "propertyIndex": true
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "/oak:index/damAssetLucene-12-custom-1": {
+ "jcr:primaryType": "nam:oak:IndexDefinition",
+ "type": "lucene",
+ "async": ["async", "nrt"],
+ "tags": ["abc"],
+ "includedPaths": "/content/dam",
+ "mergeInfo": "This index was auto-merged. See also https://oak-indexing.github.io/oakTools/simplified.html",
+ "mergeChecksum": "ef0312b8bf7cc97bb8efd0faeeb4e4b1094268694a2df749b73fac08987b3264",
+ "merges": ["/oak:index/damAssetLucene"],
+ "indexRules": {
+ "jcr:primaryType": "nam:nt:unstructured",
+ "dam:Asset": {
+ "jcr:primaryType": "nam:nt:unstructured",
+ "properties": {
+ "jcr:primaryType": "nam:nt:unstructured",
+ "x": {
+ "name": "x",
+ "propertyIndex": true,
+ "jcr:primaryType": "nam:nt:unstructured"
+ },
+ "y": {
+ "name": "y",
+ "propertyIndex": true,
+ "jcr:primaryType": "nam:nt:unstructured"
+ }
+ }
+ }
+ }
+ }
+ }""", newImageLuceneDefinitions.toString());
+ }
+
+ @Test
+ public void mergeBothCustomAndAdobeProvidedDiff() {
+ JsonObject repositoryDefinitions = JsonObject.fromJson("""
+ {
+ "/oak:index/damAssetLucene-12": {
+ "jcr:primaryType": "oak:IndexDefinition",
+ "type": "lucene",
+ "async": ["async", "nrt"],
+ "tags": ["abc"],
+ "includedPaths": "/content/dam",
+ "indexRules": {
+ "dam:Asset": {
+ "properties": {
+ "x": {
+ "name": "x",
+ "propertyIndex": true
+ }
+ }
+ }
+ }
+ },
+ "/oak:index/diff.index.optimizer": {
+ "jcr:primaryType": "nt:unstructured",
+ "type": "lucene",
+ "includedPaths": "/same",
+ "queryPaths": "/same",
+ "diff": {
+ "damAssetLucene": {
+ "indexRules": {
+ "dam:Asset": {
+ "properties": {
+ "y": {
+ "name": "y",
+ "propertyIndex": true
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ """, true);
+ JsonObject newImageLuceneDefinitions = JsonObject.fromJson("""
+ {
+ "/oak:index/diff.index": {
+ "jcr:primaryType": "nt:unstructured",
+ "type": "lucene",
+ "includedPaths": "/same",
+ "queryPaths": "/same",
+ "diff": {
+ "damAssetLucene": {
+ "indexRules": {
+ "dam:Asset": {
+ "properties": {
+ "z": {
+ "name": "z",
+ "propertyIndex": true
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ """, true);
+ getMerger().merge(newImageLuceneDefinitions, repositoryDefinitions, null);
+ assertEquals("""
+ {
+ "/oak:index/diff.index": {
+ "jcr:primaryType": "nt:unstructured",
+ "type": "lucene",
+ "includedPaths": "/same",
+ "queryPaths": "/same",
+ "diff": {
+ "damAssetLucene": {
+ "indexRules": {
+ "dam:Asset": {
+ "properties": {
+ "z": {
+ "name": "z",
+ "propertyIndex": true
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "/oak:index/damAssetLucene-12-custom-1": {
+ "jcr:primaryType": "oak:IndexDefinition",
+ "type": "lucene",
+ "async": ["async", "nrt"],
+ "tags": ["abc"],
+ "includedPaths": "/content/dam",
+ "mergeInfo": "This index was auto-merged. See also https://oak-indexing.github.io/oakTools/simplified.html",
+ "mergeChecksum": "9957cea378122a0b88216de81274b13eed3746e953b91340c51bd322dea2e353",
+ "merges": ["/oak:index/damAssetLucene"],
+ "indexRules": {
+ "jcr:primaryType": "nam:nt:unstructured",
+ "dam:Asset": {
+ "jcr:primaryType": "nam:nt:unstructured",
+ "properties": {
+ "jcr:primaryType": "nam:nt:unstructured",
+ "x": {
+ "name": "x",
+ "propertyIndex": true,
+ "jcr:primaryType": "nam:nt:unstructured"
+ },
+ "z": {
+ "name": "z",
+ "propertyIndex": true,
+ "jcr:primaryType": "nam:nt:unstructured"
+ },
+ "y": {
+ "name": "y",
+ "propertyIndex": true,
+ "jcr:primaryType": "nam:nt:unstructured"
+ }
+ }
+ }
+ }
+ }
+ }""", newImageLuceneDefinitions.toString());
+ }
+
+ @Test
+ public void similarName() {
+ JsonObject repositoryDefinitions = JsonObject.fromJson("""
+ {
+ "/oak:index/damAssetLucene-12": {
+ "jcr:primaryType": "oak:IndexDefinition",
+ "type": "lucene",
+ "async": ["async", "nrt"],
+ "tags": ["abc"],
+ "includedPaths": "/content/dam",
+ "indexRules": {
+ "dam:Asset": {
+ "properties": {
+ "x": {
+ "name": "x",
+ "propertyIndex": true
+ }
+ }
+ }
+ }
+ },
+ "/oak:index/damAsset-12": {
+ "jcr:primaryType": "oak:IndexDefinition",
+ "type": "lucene",
+ "async": ["async", "nrt"],
+ "tags": ["abc"],
+ "includedPaths": "/content/dam",
+ "indexRules": {
+ "dam:Asset": {
+ "properties": {
+ "x": {
+ "name": "x",
+ "propertyIndex": true
+ }
+ }
+ }
+ }
+ }
+ }
+ """, true);
+ JsonObject newImageLuceneDefinitions = JsonObject.fromJson("""
+ {
+ "/oak:index/diff.index": {
+ "jcr:primaryType": "nt:unstructured",
+ "type": "lucene",
+ "includedPaths": "/same",
+ "queryPaths": "/same",
+ "diff": {
+ "damAsset": {
+ "indexRules": {
+ "dam:Asset": {
+ "properties": {
+ "z": {
+ "name": "z",
+ "propertyIndex": true
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ """, true);
+ getMerger().merge(newImageLuceneDefinitions, repositoryDefinitions, null);
+ assertEquals("""
+ {
+ "/oak:index/diff.index": {
+ "jcr:primaryType": "nt:unstructured",
+ "type": "lucene",
+ "includedPaths": "/same",
+ "queryPaths": "/same",
+ "diff": {
+ "damAsset": {
+ "indexRules": {
+ "dam:Asset": {
+ "properties": {
+ "z": {
+ "name": "z",
+ "propertyIndex": true
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "/oak:index/damAsset-12-custom-1": {
+ "jcr:primaryType": "oak:IndexDefinition",
+ "type": "lucene",
+ "async": ["async", "nrt"],
+ "tags": ["abc"],
+ "includedPaths": "/content/dam",
+ "mergeInfo": "This index was auto-merged. See also https://oak-indexing.github.io/oakTools/simplified.html",
+ "mergeChecksum": "09d85a55873fbf763bbf122bbd61198ea03e4ddd658d24a9dc0c0ac48ff1c977",
+ "merges": ["/oak:index/damAsset"],
+ "indexRules": {
+ "jcr:primaryType": "nam:nt:unstructured",
+ "dam:Asset": {
+ "jcr:primaryType": "nam:nt:unstructured",
+ "properties": {
+ "jcr:primaryType": "nam:nt:unstructured",
+ "x": {
+ "name": "x",
+ "propertyIndex": true,
+ "jcr:primaryType": "nam:nt:unstructured"
+ },
+ "z": {
+ "name": "z",
+ "propertyIndex": true,
+ "jcr:primaryType": "nam:nt:unstructured"
+ }
+ }
+ }
+ }
+ }
+ }""", newImageLuceneDefinitions.toString());
+ }
+
+ private static NodeStore storeInRepository(String path, String json) throws CommitFailedException, IOException {
+ NodeStore ns = new MemoryNodeStore();
+ NodeBuilder builder = ns.getRoot().builder();
+ JsonNodeUpdater.addOrReplace(builder, ns, path, "nt:unstructured", json);
+ ns.merge(builder, EmptyHook.INSTANCE, CommitInfo.EMPTY);
+ return ns;
+ }
+
+ @Test
+ public void removeUnchangedDiffIndex() throws Exception {
+ NodeStore repositoryNodeStore = storeInRepository("/oak:index/diff.index", """
+ {
+ "jcr:primaryType": "nt:unstructured",
+ "type": "lucene", "includedPaths": "/a", "queryPaths": "/a"
+ }""");
+
+ // same content, just properties in a different order: should still be considered unchanged
+ JsonObject newImageLuceneDefinitions = JsonObject.fromJson("""
+ {
+ "/oak:index/diff.index": {
+ "jcr:primaryType": "nt:unstructured",
+ "queryPaths": "/a", "includedPaths": "/a", "type": "lucene"
+ }
+ }
+ """, true);
+
+ getMerger().removeUnchangedDiffIndexEntries(newImageLuceneDefinitions, repositoryNodeStore);
+ assertEquals("{}", newImageLuceneDefinitions.toString());
+ }
+
+ @Test
+ public void keepChangedDiffIndex() throws Exception {
+ NodeStore repositoryNodeStore = storeInRepository("/oak:index/diff.index", """
+ {
+ "jcr:primaryType": "nt:unstructured",
+ "type": "lucene", "includedPaths": "/a", "queryPaths": "/a"
+ }""");
+
+ JsonObject newImageLuceneDefinitions = JsonObject.fromJson("""
+ {
+ "/oak:index/diff.index": {
+ "jcr:primaryType": "nt:unstructured",
+ "type": "lucene", "includedPaths": "/b", "queryPaths": "/b"
+ }
+ }
+ """, true);
+
+ getMerger().removeUnchangedDiffIndexEntries(newImageLuceneDefinitions, repositoryNodeStore);
+ assertEquals(1, newImageLuceneDefinitions.getChildren().size());
+ }
+
+ @Test
+ public void keepDiffIndexNotYetInRepository() throws Exception {
+ NodeStore repositoryNodeStore = new MemoryNodeStore();
+
+ JsonObject newImageLuceneDefinitions = JsonObject.fromJson("""
+ {
+ "/oak:index/diff.index": {
+ "jcr:primaryType": "nt:unstructured",
+ "type": "lucene", "includedPaths": "/a", "queryPaths": "/a"
+ }
+ }
+ """, true);
+
+ getMerger().removeUnchangedDiffIndexEntries(newImageLuceneDefinitions, repositoryNodeStore);
+ assertEquals(1, newImageLuceneDefinitions.getChildren().size());
+ }
+
+ @Test
+ public void keepDiffIndexWhenRepositoryNodeStoreIsNull() {
+ JsonObject newImageLuceneDefinitions = JsonObject.fromJson("""
+ {
+ "/oak:index/diff.index": {
+ "jcr:primaryType": "nt:unstructured",
+ "type": "lucene", "includedPaths": "/a", "queryPaths": "/a"
+ }
+ }
+ """, true);
+
+ getMerger().removeUnchangedDiffIndexEntries(newImageLuceneDefinitions, null);
+ assertEquals(1, newImageLuceneDefinitions.getChildren().size());
+ }
+
+ @Test
+ public void diffIndexAndDiffIndexOptimizerAreIndependent() throws Exception {
+ NodeStore repositoryNodeStore = storeInRepository("/oak:index/diff.index", """
+ {
+ "jcr:primaryType": "nt:unstructured",
+ "type": "lucene", "includedPaths": "/a", "queryPaths": "/a"
+ }""");
+ NodeBuilder builder = repositoryNodeStore.getRoot().builder();
+ JsonNodeUpdater.addOrReplace(builder, repositoryNodeStore, "/oak:index/diff.index.optimizer", "nt:unstructured", """
+ {
+ "jcr:primaryType": "nt:unstructured",
+ "type": "lucene", "includedPaths": "/x", "queryPaths": "/x"
+ }""");
+ repositoryNodeStore.merge(builder, EmptyHook.INSTANCE, CommitInfo.EMPTY);
+
+ JsonObject newImageLuceneDefinitions = JsonObject.fromJson("""
+ {
+ "/oak:index/diff.index": {
+ "jcr:primaryType": "nt:unstructured",
+ "type": "lucene", "includedPaths": "/a", "queryPaths": "/a"
+ },
+ "/oak:index/diff.index.optimizer": {
+ "jcr:primaryType": "nt:unstructured",
+ "type": "lucene", "includedPaths": "/y", "queryPaths": "/y"
+ }
+ }
+ """, true);
+
+ getMerger().removeUnchangedDiffIndexEntries(newImageLuceneDefinitions, repositoryNodeStore);
+ // diff.index is unchanged (removed), diff.index.optimizer changed (kept)
+ assertEquals("[/oak:index/diff.index.optimizer]", newImageLuceneDefinitions.getChildren().keySet().toString());
+ }
+
+ @Test
+ public void removeDiffIndexEntriesRemovesBothUnconditionally() {
+ JsonObject definitions = JsonObject.fromJson("""
+ {
+ "/oak:index/diff.index": {
+ "jcr:primaryType": "nt:unstructured",
+ "type": "lucene", "includedPaths": "/a", "queryPaths": "/a"
+ },
+ "/oak:index/diff.index.optimizer": {
+ "jcr:primaryType": "nt:unstructured",
+ "type": "lucene", "includedPaths": "/b", "queryPaths": "/b"
+ },
+ "/oak:index/damAssetLucene-8": {
+ "jcr:primaryType": "oak:IndexDefinition",
+ "type": "elasticsearch"
+ }
+ }
+ """, true);
+
+ DiffIndexMerger.removeDiffIndexEntries(definitions);
+ assertEquals("[/oak:index/damAssetLucene-8]", definitions.getChildren().keySet().toString());
+ }
+
+ // when migrating from the old way to configure indexes
+ // to simplified index management, a new version of the index
+ // needs to be created _even if the definition exactly matches_
+ // because the old configuration might be removed.
+ // the flag to check is "mergeChecksum"
+ @Test
+ public void migrateFromCustomized() {
+ JsonObject repositoryDefinitions = JsonObject.fromJson("""
+ {
+ "/oak:index/acme.test-1-custom-1": {
+ "type": "lucene",
+ "async": ["async", "nrt"],
+ "includedPaths": "/content/dam",
+ "queryPaths": "/content/dam",
+ "jcr:primaryType": "nam:oak:QueryIndexDefinition",
+ "indexRules": {
+ "jcr:primaryType": "nam:nt:unstructured",
+ "dam:Asset": {
+ "jcr:primaryType": "nam:nt:unstructured",
+ "properties": {
+ "jcr:primaryType": "nam:nt:unstructured",
+ "x": {
+ "name": "x",
+ "propertyIndex": true,
+ "jcr:primaryType": "nam:nt:unstructured"
+ }
+ }
+ }
+ }
+ },
+ "/oak:index/diff.index": {
+ "jcr:primaryType": "nt:unstructured",
+ "type": "lucene", "includedPaths": "/a", "queryPaths": "/a",
+ "diff": {
+ "acme.test": {
+ "type": "lucene",
+ "async": ["async", "nrt"],
+ "includedPaths": "/content/dam",
+ "queryPaths": "/content/dam",
+ "jcr:primaryType": "nam:oak:QueryIndexDefinition",
+ "mergeInfo": "This index was auto-merged. See also https://oak-indexing.github.io/oakTools/simplified.html",
+ "mergeChecksum": "cfa1e6e503fdb74a9fb87bbbc46603bb06fb8caa426abb192ded47276c763404",
+ "merges": ["/oak:index/acme.test"],
+ "indexRules": {
+ "jcr:primaryType": "nam:nt:unstructured",
+ "dam:Asset": {
+ "jcr:primaryType": "nam:nt:unstructured",
+ "properties": {
+ "jcr:primaryType": "nam:nt:unstructured",
+ "x": {
+ "name": "x",
+ "propertyIndex": true,
+ "jcr:primaryType": "nam:nt:unstructured"
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ """, true);
+ JsonObject newImageLuceneDefinitions = JsonObject.fromJson("{}", true);
+ DiffIndexMerger merger = new DiffIndexMerger().
+ setDeleteCopiesOutOfTheBoxIndex(false).
+ setDeleteCreatesDummyIndex(true).
+ setLogAtInfoLevel(true);
+ merger.merge(newImageLuceneDefinitions, repositoryDefinitions, null);
+ JsonObject merged = newImageLuceneDefinitions;
+ assertEquals("""
+ {
+ "/oak:index/acme.test-1-custom-2": {
+ "type": "lucene",
+ "async": ["async", "nrt"],
+ "includedPaths": "/content/dam",
+ "queryPaths": "/content/dam",
+ "mergeInfo": "This index was auto-merged. See also https://oak-indexing.github.io/oakTools/simplified.html",
+ "mergeChecksum": "cfa1e6e503fdb74a9fb87bbbc46603bb06fb8caa426abb192ded47276c763404",
+ "merges": ["/oak:index/acme.test"],
+ "jcr:primaryType": "nam:oak:QueryIndexDefinition",
+ "indexRules": {
+ "jcr:primaryType": "nam:nt:unstructured",
+ "dam:Asset": {
+ "jcr:primaryType": "nam:nt:unstructured",
+ "properties": {
+ "jcr:primaryType": "nam:nt:unstructured",
+ "x": {
+ "name": "x",
+ "propertyIndex": true,
+ "jcr:primaryType": "nam:nt:unstructured"
+ }
+ }
+ }
+ }
+ }
+ }""", merged.toString());
+ }
+
+ // GRANITE-68995
+ // Support indexes without version, eg. commerceLucene
+ @Test
+ public void unversionedMerge() {
+ JsonObject repositoryDefinitions = JsonObject.fromJson("""
+ {
+ "/oak:index/commerceLucene": {
+ "jcr:primaryType": "oak:IndexDefinition",
+ "type": "lucene",
+ "async": ["async", "nrt"],
+ "tags": ["abc"],
+ "indexRules": {
+ "dam:Asset": {
+ "properties": {
+ "x": {
+ "name": "x",
+ "propertyIndex": true
+ }
+ }
+ }
+ }
+ },
+ "/oak:index/diff.index": {
+ "jcr:primaryType": "nt:unstructured",
+ "type": "lucene", "includedPaths": "/same", "queryPaths": "/same",
+ "diff": {
+ "commerceLucene": {
+ "indexRules": {
+ "dam:Asset": {
+ "properties": {
+ "y": {
+ "name": "y",
+ "propertyIndex": true
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ """, true);
+ JsonObject newImageLuceneDefinitions = JsonObject.fromJson("{}", true);
+ DiffIndexMerger merger = new DiffIndexMerger().
+ setDeleteCopiesOutOfTheBoxIndex(false).
+ setDeleteCreatesDummyIndex(true).
+ setLogAtInfoLevel(true);
+ merger.merge(newImageLuceneDefinitions, repositoryDefinitions, null);
+ JsonObject merged = newImageLuceneDefinitions;
+ assertEquals("""
+ {
+ "/oak:index/commerceLucene-custom-1": {
+ "jcr:primaryType": "oak:IndexDefinition",
+ "type": "lucene",
+ "async": ["async", "nrt"],
+ "tags": ["abc"],
+ "mergeInfo": "This index was auto-merged. See also https://oak-indexing.github.io/oakTools/simplified.html",
+ "mergeChecksum": "138dd3bf22232a733b868b8d463a196aaf708993f06008a08c9e8813c4acaa65",
+ "merges": ["/oak:index/commerceLucene"],
+ "indexRules": {
+ "jcr:primaryType": "nam:nt:unstructured",
+ "dam:Asset": {
+ "jcr:primaryType": "nam:nt:unstructured",
+ "properties": {
+ "jcr:primaryType": "nam:nt:unstructured",
+ "x": {
+ "name": "x",
+ "propertyIndex": true,
+ "jcr:primaryType": "nam:nt:unstructured"
+ },
+ "y": {
+ "name": "y",
+ "propertyIndex": true,
+ "jcr:primaryType": "nam:nt:unstructured"
+ }
+ }
+ }
+ }
+ }
+ }""", merged.toString());
+ }
+
+ // if there is a matching index with a "mergeChecksum",
+ // that matches exactly with the current diff.index configuration
+ // then no new index needs to be created (the checksum itself is ignored)
+ @Test
+ public void withExistingMergeChecksum() {
+ JsonObject repositoryDefinitions = JsonObject.fromJson("""
+ {
+ "/oak:index/acme.test-1-custom-1": {
+ "type": "lucene",
+ "async": ["async", "nrt"],
+ "includedPaths": "/content/dam",
+ "queryPaths": "/content/dam",
+ "mergeChecksum": "test",
+ "jcr:primaryType": "nam:oak:QueryIndexDefinition",
+ "indexRules": {
+ "jcr:primaryType": "nam:nt:unstructured",
+ "dam:Asset": {
+ "jcr:primaryType": "nam:nt:unstructured",
+ "properties": {
+ "jcr:primaryType": "nam:nt:unstructured",
+ "x": {
+ "name": "x",
+ "propertyIndex": true,
+ "jcr:primaryType": "nam:nt:unstructured"
+ }
+ }
+ }
+ }
+ },
+ "/oak:index/diff.index": {
+ "jcr:primaryType": "nt:unstructured",
+ "type": "lucene", "includedPaths": "/a", "queryPaths": "/a",
+ "diff": {
+ "acme.test": {
+ "type": "lucene",
+ "async": ["async", "nrt"],
+ "includedPaths": "/content/dam",
+ "queryPaths": "/content/dam",
+ "jcr:primaryType": "nam:oak:QueryIndexDefinition",
+ "mergeInfo": "This index was auto-merged. See also https://oak-indexing.github.io/oakTools/simplified.html",
+ "mergeChecksum": "cfa1e6e503fdb74a9fb87bbbc46603bb06fb8caa426abb192ded47276c763404",
+ "merges": ["/oak:index/acme.test"],
+ "indexRules": {
+ "jcr:primaryType": "nam:nt:unstructured",
+ "dam:Asset": {
+ "jcr:primaryType": "nam:nt:unstructured",
+ "properties": {
+ "jcr:primaryType": "nam:nt:unstructured",
+ "x": {
+ "name": "x",
+ "propertyIndex": true,
+ "jcr:primaryType": "nam:nt:unstructured"
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ """, true);
+ JsonObject newImageLuceneDefinitions = JsonObject.fromJson("{}", true);
+ DiffIndexMerger merger = new DiffIndexMerger().
+ setDeleteCopiesOutOfTheBoxIndex(false).
+ setDeleteCreatesDummyIndex(true).
+ setLogAtInfoLevel(true);
+ merger.merge(newImageLuceneDefinitions, repositoryDefinitions, null);
+ JsonObject merged = newImageLuceneDefinitions;
+ assertEquals("""
+ {}""", merged.toString());
+ }
+
+ // when migrating from simplified index management
+ // to the legacy mode (for example, in an emergency case,
+ // or when the index requires apps + libs)
+ // then we need to ignore disabled indexes that have the mergeInfo
+ // property
+ @Test
+ public void migrateBackToLegacyMode() {
+ JsonObject repositoryDefinitions = JsonObject.fromJson("""
+ {
+ "/oak:index/acme.test-1-custom-1": {
+ "type": "disabled",
+ "mergeInfo": "This index was created with simplified index management",
+ "async": ["async", "nrt"],
+ "includedPaths": "/content/dam",
+ "queryPaths": "/content/dam",
+ "jcr:primaryType": "nam:oak:QueryIndexDefinition",
+ "indexRules": {
+ "jcr:primaryType": "nam:nt:unstructured",
+ "dam:Asset": {
+ "jcr:primaryType": "nam:nt:unstructured",
+ "properties": {
+ "jcr:primaryType": "nam:nt:unstructured",
+ "x": {
+ "name": "x",
+ "propertyIndex": true,
+ "jcr:primaryType": "nam:nt:unstructured"
+ }
+ }
+ }
+ }
+ },
+ "/oak:index/diff.index": {
+ "jcr:primaryType": "nt:unstructured",
+ "type": "lucene", "includedPaths": "/a", "queryPaths": "/a",
+ "diff": {
+ }
+ }
+ }
+ """, true);
+ JsonObject newImageLuceneDefinitions = JsonObject.fromJson("{}", true);
+ DiffIndexMerger merger = new DiffIndexMerger().
+ setDeleteCopiesOutOfTheBoxIndex(false).
+ setDeleteCreatesDummyIndex(true).
+ setLogAtInfoLevel(true);
+ merger.merge(newImageLuceneDefinitions, repositoryDefinitions, null);
+ JsonObject merged = newImageLuceneDefinitions;
+ assertEquals("{}", merged.toString());
+ }
+
+ // GRANITE-69761: when a fully custom index is removed from diff.index, and
+ // deleteCreatesDummyIndex is enabled, the resulting index must stay a
+ // working "lucene" dummy index (with includedPaths "/dummy"), not be
+ // overridden back to type "disabled"
+ @Test
+ public void fullyCustomIndexRemovedCreatesDummyLuceneIndex() {
+ JsonObject repositoryDefinitions = JsonObject.fromJson("""
+ {
+ "/oak:index/damAssetStateIndex.rightsManagement-1-custom-1": {
+ "jcr:primaryType": "oak:IndexDefinition",
+ "type": "lucene",
+ "async": ["async", "nrt"],
+ "includedPaths": "/content/dam",
+ "mergeInfo": "This index was auto-merged. See also https://oak-indexing.github.io/oakTools/simplified.html",
+ "mergeChecksum": "abc",
+ "merges": ["/oak:index/damAssetStateIndex.rightsManagement"],
+ "indexRules": {
+ "dam:Asset": {
+ "properties": {
+ "x": {
+ "name": "x",
+ "propertyIndex": true
+ }
+ }
+ }
+ }
+ },
+ "/oak:index/diff.index": {
+ "jcr:primaryType": "nt:unstructured",
+ "type": "lucene", "includedPaths": "/a", "queryPaths": "/a"
+ }
+ }
+ """, true);
+ JsonObject newImageLuceneDefinitions = JsonObject.fromJson("{}", true);
+ DiffIndexMerger merger = new DiffIndexMerger().
+ setDeleteCopiesOutOfTheBoxIndex(false).
+ setDeleteCreatesDummyIndex(true).
+ setLogAtInfoLevel(true);
+ merger.merge(newImageLuceneDefinitions, repositoryDefinitions, null);
+ JsonObject merged = newImageLuceneDefinitions;
+ assertEquals("""
+ {
+ "/oak:index/damAssetStateIndex.rightsManagement-1-custom-2": {
+ "async": "async",
+ "includedPaths": "/dummy",
+ "queryPaths": "/dummy",
+ "type": "lucene",
+ "jcr:primaryType": "nam:oak:QueryIndexDefinition",
+ "mergeInfo": "This index was auto-merged. See also https://oak-indexing.github.io/oakTools/simplified.html",
+ "mergeChecksum": "ea20eb28050ff4cd8d270298656ff7b80c7322aa7964c5c30c32756ef4de228a",
+ "merges": ["/oak:index/damAssetStateIndex.rightsManagement"],
+ "indexRules": {
+ "jcr:primaryType": "nam:nt:unstructured",
+ "properties": {
+ "jcr:primaryType": "nam:nt:unstructured",
+ "dummy": {
+ "name": "dummy",
+ "propertyIndex": true,
+ "jcr:primaryType": "nam:nt:unstructured"
+ }
+ }
+ }
+ }
+ }""", merged.toString());
+ }
+
+ // GRANITE-69761: when a customization is removed from diff.index, and
+ // setDeleteCopiesOutOfTheBoxIndex is enabled, then the original
+ // (out-of-the-box) index must be copied
+ @Test
+ public void customizationRemovedCreatesCopyOfOriginalIndex() {
+ JsonObject repositoryDefinitions = JsonObject.fromJson("""
+ {
+ "/oak:index/damAssetStateIndex-1": {
+ "async": [ "async", "nrt" ],
+ "includedPaths": "/content/dam",
+ "jcr:primaryType": "oak:IndexDefinition",
+ "type": "lucene",
+ "indexRules": {
+ "dam:Asset": {
+ "properties": {
+ "x": {
+ "name": "x",
+ "propertyIndex": true
+ }
+ }
+ }
+ }
+ },
+ "/oak:index/damAssetStateIndex-1-custom-1": {
+ "async": [ "async", "nrt" ],
+ "includedPaths": "/content/dam",
+ "jcr:primaryType": "oak:IndexDefinition",
+ "mergeChecksum": "cf1177ccdf1b404eeb7787ba63c15fefa3fcaacdad38605f91cf00fa1aed0409",
+ "mergeInfo": "This index was auto-merged. See also https://oak-indexing.github.io/oakTools/simplified.html",
+ "merges": [ "/oak:index/damAssetStateIndex" ],
+ "type": "lucene",
+ "indexRules": {
+ "jcr:primaryType": "nam:nt:unstructured",
+ "dam:Asset": {
+ "jcr:primaryType": "nam:nt:unstructured",
+ "properties": {
+ "jcr:primaryType": "nam:nt:unstructured",
+ "x": {
+ "jcr:primaryType": "nam:nt:unstructured",
+ "name": "x",
+ "propertyIndex": true
+ },
+ "y": {
+ "jcr:primaryType": "nam:nt:unstructured",
+ "name": "y",
+ "propertyIndex": true
+ }
+ }
+ }
+ }
+ },
+ "/oak:index/diff.index": {
+ "includedPaths": "/same",
+ "jcr:primaryType": "nt:unstructured",
+ "queryPaths": "/same",
+ "type": "lucene",
+ "diff": {}
+ }
+ }
+ """, true);
+ JsonObject newImageLuceneDefinitions = JsonObject.fromJson("{}", true);
+ DiffIndexMerger merger = new DiffIndexMerger().
+ setDeleteCopiesOutOfTheBoxIndex(false).
+ setDeleteCreatesDummyIndex(true).
+ setLogAtInfoLevel(true);
+ merger.merge(newImageLuceneDefinitions, repositoryDefinitions, null);
+ JsonObject merged = newImageLuceneDefinitions;
+ assertEquals("""
+ {
+ "/oak:index/damAssetStateIndex-1-custom-2": {
+ "async": ["async", "nrt"],
+ "includedPaths": "/content/dam",
+ "jcr:primaryType": "oak:IndexDefinition",
+ "type": "lucene",
+ "mergeInfo": "This index was auto-merged. See also https://oak-indexing.github.io/oakTools/simplified.html",
+ "mergeChecksum": "43402360ba9021820657ef60e2c5fcece9ac9a06e03848817cba6779c189f947",
+ "merges": ["/oak:index/damAssetStateIndex"],
+ "indexRules": {
+ "jcr:primaryType": "nam:nt:unstructured",
+ "dam:Asset": {
+ "jcr:primaryType": "nam:nt:unstructured",
+ "properties": {
+ "jcr:primaryType": "nam:nt:unstructured",
+ "x": {
+ "name": "x",
+ "propertyIndex": true,
+ "jcr:primaryType": "nam:nt:unstructured"
+ }
+ }
+ }
+ }
+ }
+ }""", merged.toString());
+ }
+
+ // GRANITE-69361: an empty child node in the diff (eg. an analyzer filter
+ // without any properties, such as "LowerCase": {}) must be kept as an
+ // empty node - just getting a "jcr:primaryType" added - and not be turned
+ // into a disabled/dummy index structure. That special handling (the
+ // "target is empty" fallback in mergeInto()) is only meant for the
+ // top-level index definition itself, not for arbitrary nested empty nodes.
+ @Test
+ public void emptyChildNodeIsPreservedNotDisabled() {
+ JsonObject diff = JsonObject.fromJson("""
+ {
+ "async": "async",
+ "compatVersion": 2,
+ "includedPaths": ["/content"],
+ "queryPaths": "/content",
+ "type": "lucene",
+ "analyzers": {
+ "default": {
+ "filters": {
+ "LowerCase": {}
+ }
+ }
+ },
+ "indexRules": {
+ "nt:unstructured": {
+ "properties": {
+ "code": {
+ "name": "code",
+ "propertyIndex": true
+ }
+ }
+ }
+ }
+ }
+ """, true);
+
+ JsonObject merged = new DiffIndexMerger().processMerge("custom.test", null, diff);
+ assertEquals("""
+ {
+ "async": "async",
+ "compatVersion": 2,
+ "includedPaths": ["/content"],
+ "queryPaths": "/content",
+ "type": "lucene",
+ "jcr:primaryType": "nam:oak:QueryIndexDefinition",
+ "analyzers": {
+ "jcr:primaryType": "nam:nt:unstructured",
+ "default": {
+ "jcr:primaryType": "nam:nt:unstructured",
+ "filters": {
+ "jcr:primaryType": "nam:nt:unstructured",
+ "LowerCase": {
+ "jcr:primaryType": "nam:nt:unstructured"
+ }
+ }
+ }
+ },
+ "indexRules": {
+ "jcr:primaryType": "nam:nt:unstructured",
+ "nt:unstructured": {
+ "jcr:primaryType": "nam:nt:unstructured",
+ "properties": {
+ "jcr:primaryType": "nam:nt:unstructured",
+ "code": {
+ "name": "code",
+ "propertyIndex": true,
+ "jcr:primaryType": "nam:nt:unstructured"
+ }
+ }
+ }
+ }
+ }""", merged.toString());
+ }
+
+ @Test
+ public void removeDiffIndexEntriesNoOpWhenAbsent() {
+ JsonObject definitions = JsonObject.fromJson("""
+ {
+ "/oak:index/damAssetLucene-8": {
+ "jcr:primaryType": "oak:IndexDefinition",
+ "type": "elasticsearch"
+ }
+ }
+ """, true);
+
+ DiffIndexMerger.removeDiffIndexEntries(definitions);
+ assertEquals("[/oak:index/damAssetLucene-8]", definitions.getChildren().keySet().toString());
+ }
+}