فهرست منبع

Synonyms API - Add refresh parameter to check synonyms index and reload analyzers (#126935)

* Add timeout to SynonymsManagementAPIService put synonyms

* Remove replicas 0, as that may impact serverless

* Add timeout to put synonyms action, fix tests

* Fix number of replicas

* Remove cluster.health checks for synonyms index

* Revert debugging

* Add integration test for timeouts

* Use TimeValue instead of an int

* Add YAML tests and REST API specs

* Fix a validation bug in put synonym rule

* Spotless

* Update docs/changelog/126314.yaml

* Remove unnecessary checks for null

* Fix equals / HashCode

* Checks that timeout is passed correctly to the check health method

* Use correctly the default timeout

* spotless

* Add monitor cluster privilege to internal synonyms user

* [CI] Auto commit changes from spotless

* Add capabilities to avoid failing on bwc tests

* Replace timeout for refresh param

* Add param to specs

* Add YAML tests

* Fix changelog

* [CI] Auto commit changes from spotless

* Use BWC serialization tests

* Fix bug in test parser

* Spotless

* Delete doesn't need reloading :facepalm: removing it

* Revert "Delete doesn't need reloading :facepalm: removing it"

This reverts commit 9c8e0b62beaba2f2894756c959cf9df9e20c4b0f.

* [CI] Auto commit changes from spotless

* Fix refresh for delete synonym rule

* Fix tests

* Update docs/changelog/126935.yaml

* Add reload analyzers test

* reload_analyzers is not available on serverless

---------

Co-authored-by: elasticsearchmachine <infra-root+elasticsearchmachine@elastic.co>
Carlos Delgado 6 ماه پیش
والد
کامیت
4d4b962fd1
35فایلهای تغییر یافته به همراه718 افزوده شده و 190 حذف شده
  1. 6 0
      docs/changelog/126314.yaml
  2. 6 0
      docs/changelog/126935.yaml
  3. 6 0
      rest-api-spec/src/main/resources/rest-api-spec/api/synonyms.delete_synonym_rule.json
  4. 6 0
      rest-api-spec/src/main/resources/rest-api-spec/api/synonyms.put_synonym.json
  5. 6 0
      rest-api-spec/src/main/resources/rest-api-spec/api/synonyms.put_synonym_rule.json
  6. 26 10
      rest-api-spec/src/yamlRestTest/resources/rest-api-spec/test/synonyms/10_synonyms_put.yml
  7. 0 19
      rest-api-spec/src/yamlRestTest/resources/rest-api-spec/test/synonyms/110_synonyms_invalid.yml
  8. 0 6
      rest-api-spec/src/yamlRestTest/resources/rest-api-spec/test/synonyms/20_synonyms_get.yml
  9. 0 7
      rest-api-spec/src/yamlRestTest/resources/rest-api-spec/test/synonyms/30_synonyms_delete.yml
  10. 0 6
      rest-api-spec/src/yamlRestTest/resources/rest-api-spec/test/synonyms/40_synonyms_sets_get.yml
  11. 65 6
      rest-api-spec/src/yamlRestTest/resources/rest-api-spec/test/synonyms/50_synonym_rule_put.yml
  12. 0 6
      rest-api-spec/src/yamlRestTest/resources/rest-api-spec/test/synonyms/60_synonym_rule_get.yml
  13. 21 6
      rest-api-spec/src/yamlRestTest/resources/rest-api-spec/test/synonyms/70_synonym_rule_delete.yml
  14. 0 6
      rest-api-spec/src/yamlRestTest/resources/rest-api-spec/test/synonyms/80_synonyms_from_index.yml
  15. 59 6
      rest-api-spec/src/yamlRestTest/resources/rest-api-spec/test/synonyms/90_synonyms_reloading_for_synset.yml
  16. 193 65
      server/src/internalClusterTest/java/org/elasticsearch/synonyms/SynonymsManagementAPIServiceIT.java
  17. 1 0
      server/src/main/java/org/elasticsearch/TransportVersions.java
  18. 16 2
      server/src/main/java/org/elasticsearch/action/synonyms/DeleteSynonymRuleAction.java
  19. 23 5
      server/src/main/java/org/elasticsearch/action/synonyms/PutSynonymRuleAction.java
  20. 22 4
      server/src/main/java/org/elasticsearch/action/synonyms/PutSynonymsAction.java
  21. 35 6
      server/src/main/java/org/elasticsearch/action/synonyms/SynonymUpdateResponse.java
  22. 1 0
      server/src/main/java/org/elasticsearch/action/synonyms/TransportDeleteSynonymRuleAction.java
  23. 2 2
      server/src/main/java/org/elasticsearch/action/synonyms/TransportPutSynonymRuleAction.java
  24. 1 0
      server/src/main/java/org/elasticsearch/action/synonyms/TransportPutSynonymsAction.java
  25. 8 1
      server/src/main/java/org/elasticsearch/rest/action/synonyms/RestDeleteSynonymRuleAction.java
  26. 7 0
      server/src/main/java/org/elasticsearch/rest/action/synonyms/RestPutSynonymRuleAction.java
  27. 7 0
      server/src/main/java/org/elasticsearch/rest/action/synonyms/RestPutSynonymsAction.java
  28. 26 0
      server/src/main/java/org/elasticsearch/rest/action/synonyms/SynonymCapabilities.java
  29. 95 16
      server/src/main/java/org/elasticsearch/synonyms/SynonymsManagementAPIService.java
  30. 2 2
      server/src/test/java/org/elasticsearch/action/admin/indices/analyze/ReloadAnalyzersResponseTests.java
  31. 1 1
      server/src/test/java/org/elasticsearch/action/synonyms/DeleteSynonymRuleActionRequestSerializingTests.java
  32. 1 1
      server/src/test/java/org/elasticsearch/action/synonyms/PutSynonymRuleActionRequestSerializingTests.java
  33. 1 1
      server/src/test/java/org/elasticsearch/action/synonyms/PutSynonymsActionRequestSerializingTests.java
  34. 74 5
      server/src/test/java/org/elasticsearch/action/synonyms/SynonymUpdateResponseSerializingTests.java
  35. 1 1
      x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/user/InternalUsers.java

+ 6 - 0
docs/changelog/126314.yaml

@@ -0,0 +1,6 @@
+pr: 126314
+summary: Add refresh to synonyms put / delete APIs to wait for synonyms to be accessible and reload analyzers
+area: Analysis
+type: bug
+issues:
+ - 121441

+ 6 - 0
docs/changelog/126935.yaml

@@ -0,0 +1,6 @@
+pr: 126935
+summary: Synonyms API - Add refresh parameter to check synonyms index and reload analyzers
+area: Analysis
+type: enhancement
+issues:
+ - 121441

+ 6 - 0
rest-api-spec/src/main/resources/rest-api-spec/api/synonyms.delete_synonym_rule.json

@@ -33,6 +33,12 @@
           }
         }
       ]
+    },
+    "params": {
+      "refresh": {
+        "type": "boolean",
+        "description": "Refresh search analyzers to update synonyms"
+      }
     }
   }
 }

+ 6 - 0
rest-api-spec/src/main/resources/rest-api-spec/api/synonyms.put_synonym.json

@@ -30,6 +30,12 @@
         }
       ]
     },
+    "params": {
+      "refresh": {
+        "type": "boolean",
+        "description": "Refresh search analyzers to update synonyms"
+      }
+    },
     "body": {
       "description": "Synonyms set rules",
       "required": true

+ 6 - 0
rest-api-spec/src/main/resources/rest-api-spec/api/synonyms.put_synonym_rule.json

@@ -34,6 +34,12 @@
         }
       ]
     },
+    "params": {
+      "refresh": {
+        "type": "boolean",
+        "description": "Refresh search analyzers to update synonyms"
+      }
+    },
     "body": {
       "description": "Synonym rule",
       "required": true

+ 26 - 10
rest-api-spec/src/yamlRestTest/resources/rest-api-spec/test/synonyms/10_synonyms_put.yml

@@ -15,11 +15,6 @@ setup:
 
   - match: { result: "created" }
 
-  - do:
-      cluster.health:
-        index: .synonyms
-        wait_for_status: green
-
   - do:
       synonyms.get_synonym:
         id: test-update-synonyms
@@ -63,11 +58,6 @@ setup:
 
   - match: { result: "created" }
 
-  - do:
-      cluster.health:
-        index: .synonyms
-        wait_for_status: green
-
   - do:
       synonyms.get_synonym:
         id: test-empty-synonyms
@@ -75,6 +65,31 @@ setup:
   - match: { count: 0 }
   - match: { synonyms_set: [] }
 
+---
+"Refresh can be specified":
+
+  - requires:
+      test_runner_features: [ capabilities ]
+      capabilities:
+        - method: PUT
+          path: /_synonyms/{rule_id}
+          capabilities: [ synonyms_refresh_param ]
+      reason: "synonyms refresh param capability needed"
+
+  - do:
+      synonyms.put_synonym:
+        id: test-update-synonyms
+        refresh: false
+        body:
+          synonyms_set:
+            - synonyms: "hello, hi"
+            - synonyms: "bye => goodbye"
+              id: "test-id"
+
+  - match: { result: "created" }
+  # Reload analyzers info is not included
+  - not_exists: reload_analyzers_details
+
 ---
 "Validation fails tests":
   - do:
@@ -116,3 +131,4 @@ setup:
         body:
           synonyms_set:
             - synonyms: "bye, goodbye,  "
+

+ 0 - 19
rest-api-spec/src/yamlRestTest/resources/rest-api-spec/test/synonyms/110_synonyms_invalid.yml

@@ -11,12 +11,6 @@ setup:
           synonyms_set:
             synonyms: "foo => bar, baz"
 
-  # This is to ensure that all index shards (write and read) are available. In serverless this can take some time.
-  - do:
-      cluster.health:
-        index: .synonyms
-        wait_for_status: green
-
   - do:
       indices.create:
         index: test_index
@@ -372,13 +366,6 @@ setup:
           synonyms_set:
             synonyms: "foo => bar, baz"
 
-  # This is to ensure that all index shards (write and read) are available. In serverless this can take some time.
-  - do:
-      cluster.health:
-        index: .synonyms
-        wait_for_status: green
-
-
   - do:
       indices.stats: { index: test_index }
 
@@ -441,12 +428,6 @@ setup:
           synonyms_set:
             synonyms: "foo => bar, baz"
 
-  # This is to ensure that all index shards (write and read) are available. In serverless this can take some time.
-  - do:
-      cluster.health:
-        index: .synonyms
-        wait_for_status: green
-
   - do:
       # Warning issued in previous versions
       allowed_warnings:

+ 0 - 6
rest-api-spec/src/yamlRestTest/resources/rest-api-spec/test/synonyms/20_synonyms_get.yml

@@ -14,12 +14,6 @@ setup:
             - synonyms: "test => check"
               id: "test-id-3"
 
-  # This is to ensure that all index shards (write and read) are available. In serverless this can take some time.
-  - do:
-      cluster.health:
-        index: .synonyms
-        wait_for_status: green
-
 ---
 "Get synonyms set":
   - do:

+ 0 - 7
rest-api-spec/src/yamlRestTest/resources/rest-api-spec/test/synonyms/30_synonyms_delete.yml

@@ -12,12 +12,6 @@ setup:
             - synonyms: "bye => goodbye"
               id: "test-id-2"
 
-  # This is to ensure that all index shards (write and read) are available. In serverless this can take some time.
-  - do:
-      cluster.health:
-        index: .synonyms
-        wait_for_status: green
-
 ---
 "Delete synonyms set":
   - do:
@@ -77,7 +71,6 @@ setup:
           settings:
             index:
               number_of_shards: 1
-              number_of_replicas: 0
             analysis:
               filter:
                 my_synonym_filter:

+ 0 - 6
rest-api-spec/src/yamlRestTest/resources/rest-api-spec/test/synonyms/40_synonyms_sets_get.yml

@@ -10,12 +10,6 @@ setup:
             - synonyms: "hello, hi"
             - synonyms: "goodbye, bye"
 
-  # This is to ensure that all index shards (write and read) are available. In serverless this can take some time.
-  - do:
-      cluster.health:
-        index: .synonyms
-        wait_for_status: green
-
   - do:
       synonyms.put_synonym:
         id: test-synonyms-1

+ 65 - 6
rest-api-spec/src/yamlRestTest/resources/rest-api-spec/test/synonyms/50_synonym_rule_put.yml

@@ -14,12 +14,6 @@ setup:
             - synonyms: "test => check"
               id: "test-id-3"
 
-  # This is to ensure that all index shards (write and read) are available. In serverless this can take some time.
-  - do:
-      cluster.health:
-        index: .synonyms
-        wait_for_status: green
-
 ---
 "Update a synonyms rule":
   - do:
@@ -85,3 +79,68 @@ setup:
         rule_id: "test-id-0"
         body:
           synonyms: "i-phone, iphone"
+
+---
+"Refresh can be specified":
+
+  - requires:
+      test_runner_features: [ capabilities ]
+      capabilities:
+        - method: PUT
+          path: /_synonyms/{rule_id}
+          capabilities: [ synonyms_refresh_param ]
+      reason: "synonyms refresh param capability needed"
+
+  - do:
+      synonyms.put_synonym_rule:
+        refresh: false
+        set_id: "test-synonyms"
+        rule_id: "test-id-2"
+        body:
+          synonyms: "bye, goodbye, seeya"
+
+  - match: { result: "updated" }
+  # Reload analyzers info is not included
+  - not_exists: reload_analyzers_details
+
+---
+"Validation failure tests":
+  - do:
+      catch: /\[synonyms\] field can't be empty/
+      synonyms.put_synonym_rule:
+        set_id: "test-synonyms"
+        rule_id: "test-id-0"
+        body:
+          synonyms: ""
+
+  - do:
+      catch: /More than one explicit mapping specified in the same synonyms rule/
+      synonyms.put_synonym_rule:
+        set_id: "test-synonyms"
+        rule_id: "test-id-0"
+        body:
+          synonyms: "bye => => goodbye"
+
+  - do:
+      catch: /Incorrect syntax for \[synonyms\]/
+      synonyms.put_synonym_rule:
+        set_id: "test-synonyms"
+        rule_id: "test-id-0"
+        body:
+          synonyms: " => goodbye"
+
+  - do:
+      catch: /Incorrect syntax for \[synonyms\]/
+      synonyms.put_synonym_rule:
+        set_id: "test-synonyms"
+        rule_id: "test-id-0"
+        body:
+          synonyms: "bye => "
+
+  - do:
+      catch: /Incorrect syntax for \[synonyms\]/
+      synonyms.put_synonym_rule:
+        set_id: "test-synonyms"
+        rule_id: "test-id-0"
+        body:
+          synonyms: "bye, goodbye,  "

+ 0 - 6
rest-api-spec/src/yamlRestTest/resources/rest-api-spec/test/synonyms/60_synonym_rule_get.yml

@@ -14,12 +14,6 @@ setup:
             - synonyms: "test => check"
               id: "test-id-3"
 
-  # This is to ensure that all index shards (write and read) are available. In serverless this can take some time.
-  - do:
-      cluster.health:
-        index: .synonyms
-        wait_for_status: green
-
 ---
 "Get a synonym rule":
   - do:

+ 21 - 6
rest-api-spec/src/yamlRestTest/resources/rest-api-spec/test/synonyms/70_synonym_rule_delete.yml

@@ -14,12 +14,6 @@ setup:
             - synonyms: "test => check"
               id: "test-id-3"
 
-  # This is to ensure that all index shards (write and read) are available. In serverless this can take some time.
-  - do:
-      cluster.health:
-        index: .synonyms
-        wait_for_status: green
-
 ---
 "Delete synonym rule":
   - do:
@@ -50,6 +44,27 @@ setup:
         - synonyms: "test => check"
           id: "test-id-3"
 
+---
+"Refresh can be specified":
+
+  - requires:
+      test_runner_features: [ capabilities ]
+      capabilities:
+        - method: PUT
+          path: /_synonyms/{rule_id}
+          capabilities: [ synonyms_refresh_param ]
+      reason: "synonyms refresh param capability needed"
+
+  - do:
+      synonyms.delete_synonym_rule:
+        set_id: test-synonyms
+        rule_id: test-id-2
+        refresh: false
+
+  - match: { result: "deleted" }
+  # Reload analyzers info is not included
+  - not_exists: reload_analyzers_details
+
 ---
 "Delete synonym rule - missing synonym set":
   - do:

+ 0 - 6
rest-api-spec/src/yamlRestTest/resources/rest-api-spec/test/synonyms/80_synonyms_from_index.yml

@@ -13,12 +13,6 @@ setup:
             - synonyms: "bye => goodbye"
               id: "synonym-rule-2"
 
-  # This is to ensure that all index shards (write and read) are available. In serverless this can take some time.
-  - do:
-      cluster.health:
-        index: .synonyms
-        wait_for_status: green
-
   # Create an index with synonym_filter that uses that synonyms set
   - do:
       indices.create:

+ 59 - 6
rest-api-spec/src/yamlRestTest/resources/rest-api-spec/test/synonyms/90_synonyms_reloading_for_synset.yml

@@ -14,12 +14,6 @@ setup:
             - synonyms: "bye => goodbye"
               id: "synonym-rule-2"
 
-  # This is to ensure that all index shards (write and read) are available. In serverless this can take some time.
-  - do:
-      cluster.health:
-        index: .synonyms
-        wait_for_status: green
-
   # Create synonyms synonyms_set2
   - do:
       synonyms.put_synonym:
@@ -156,3 +150,62 @@ setup:
               my_field:
                 query: salute
   - match: { hits.total.value: 0 }
+
+---
+"Reload analyzers with refresh false":
+  - requires:
+      test_runner_features: [ capabilities ]
+      capabilities:
+        - method: PUT
+          path: /_synonyms/{rule_id}
+          capabilities: [ synonyms_refresh_param ]
+      reason: "synonyms refresh param capability needed"
+
+  - do:
+      synonyms.put_synonym:
+        id: synonyms_set1
+        refresh: false
+        body:
+          synonyms_set:
+            - synonyms: "hello, salute"
+
+  - match: { result: "updated" }
+  - not_exists: reload_analyzers_details
+
+  # Confirm that the index analyzers are not reloaded for my_index1
+  - do:
+      search:
+        index: my_index1
+        body:
+          query:
+            match:
+              my_field:
+                query: salute
+  - match: { hits.total.value: 0 }
+
+  # Reloading analyzers makes synonyms refresh
+  - do:
+      synonyms.put_synonym:
+        id: synonyms_set1
+        refresh: true
+        body:
+          synonyms_set:
+            - synonyms: "hello, salute"
+            - synonyms: "ciao => goodbye"
+
+  - match: { result: "updated" }
+  - gt: { reload_analyzers_details._shards.total: 0 }
+  - gt: { reload_analyzers_details._shards.successful: 0 }
+  - length: { reload_analyzers_details.reload_details: 1 }
+
+  - do:
+      search:
+        index: my_index1
+        body:
+          query:
+            match:
+              my_field:
+                query: salute
+
+  - match: { hits.total.value: 1 }
+

+ 193 - 65
server/src/internalClusterTest/java/org/elasticsearch/synonyms/SynonymsManagementAPIServiceIT.java

@@ -11,8 +11,12 @@ package org.elasticsearch.synonyms;
 
 import org.apache.logging.log4j.Logger;
 import org.elasticsearch.action.ActionListener;
+import org.elasticsearch.action.admin.cluster.health.ClusterHealthResponse;
 import org.elasticsearch.action.bulk.BulkResponse;
+import org.elasticsearch.cluster.ClusterName;
+import org.elasticsearch.cluster.ClusterState;
 import org.elasticsearch.index.mapper.extras.MapperExtrasPlugin;
+import org.elasticsearch.indices.IndexCreationException;
 import org.elasticsearch.plugins.Plugin;
 import org.elasticsearch.reindex.ReindexPlugin;
 import org.elasticsearch.test.ESIntegTestCase;
@@ -50,22 +54,29 @@ public class SynonymsManagementAPIServiceIT extends ESIntegTestCase {
     public void testCreateManySynonyms() throws Exception {
         CountDownLatch putLatch = new CountDownLatch(1);
         String synonymSetId = randomIdentifier();
+        boolean refresh = randomBoolean();
         int rulesNumber = randomIntBetween(maxSynonymSets / 2, maxSynonymSets);
-        synonymsManagementAPIService.putSynonymsSet(synonymSetId, randomSynonymsSet(rulesNumber, rulesNumber), new ActionListener<>() {
-            @Override
-            public void onResponse(SynonymsManagementAPIService.SynonymsReloadResult synonymsReloadResult) {
-                assertEquals(
-                    SynonymsManagementAPIService.UpdateSynonymsResultStatus.CREATED,
-                    synonymsReloadResult.synonymsOperationResult()
-                );
-                putLatch.countDown();
-            }
+        synonymsManagementAPIService.putSynonymsSet(
+            synonymSetId,
+            randomSynonymsSet(rulesNumber, rulesNumber),
+            refresh,
+            new ActionListener<>() {
+                @Override
+                public void onResponse(SynonymsManagementAPIService.SynonymsReloadResult synonymsReloadResult) {
+                    assertEquals(
+                        SynonymsManagementAPIService.UpdateSynonymsResultStatus.CREATED,
+                        synonymsReloadResult.synonymsOperationResult()
+                    );
+                    assertEquals(refresh, synonymsReloadResult.reloadAnalyzersResponse() != null);
+                    putLatch.countDown();
+                }
 
-            @Override
-            public void onFailure(Exception e) {
-                fail(e);
+                @Override
+                public void onFailure(Exception e) {
+                    fail(e);
+                }
             }
-        });
+        );
 
         putLatch.await(5, TimeUnit.SECONDS);
 
@@ -95,6 +106,7 @@ public class SynonymsManagementAPIServiceIT extends ESIntegTestCase {
         synonymsManagementAPIService.putSynonymsSet(
             randomIdentifier(),
             randomSynonymsSet(maxSynonymSets + 1, maxSynonymSets * 2),
+            randomBoolean(),
             new ActionListener<>() {
                 @Override
                 public void onResponse(SynonymsManagementAPIService.SynonymsReloadResult synonymsReloadResult) {
@@ -120,67 +132,73 @@ public class SynonymsManagementAPIServiceIT extends ESIntegTestCase {
         int rulesToUpdate = randomIntBetween(1, 10);
         int synonymsToCreate = maxSynonymSets - rulesToUpdate;
         String synonymSetId = randomIdentifier();
-        synonymsManagementAPIService.putSynonymsSet(synonymSetId, randomSynonymsSet(synonymsToCreate), new ActionListener<>() {
-            @Override
-            public void onResponse(SynonymsManagementAPIService.SynonymsReloadResult synonymsReloadResult) {
-                // Create as many rules as should fail
-                SynonymRule[] rules = randomSynonymsSet(atLeast(rulesToUpdate + 1));
-                CountDownLatch updatedRulesLatch = new CountDownLatch(rulesToUpdate);
-                for (int i = 0; i < rulesToUpdate; i++) {
-                    synonymsManagementAPIService.putSynonymRule(synonymSetId, rules[i], new ActionListener<>() {
-                        @Override
-                        public void onResponse(SynonymsManagementAPIService.SynonymsReloadResult synonymsReloadResult) {
-                            updatedRulesLatch.countDown();
-                        }
-
-                        @Override
-                        public void onFailure(Exception e) {
-                            fail(e);
-                        }
-                    });
-                }
-                try {
-                    updatedRulesLatch.await(5, TimeUnit.SECONDS);
-                } catch (InterruptedException e) {
-                    fail(e);
-                }
-
-                // Updating more rules fails
-                int rulesToInsert = rules.length - rulesToUpdate;
-                CountDownLatch insertRulesLatch = new CountDownLatch(rulesToInsert);
-                for (int i = rulesToUpdate; i < rulesToInsert; i++) {
-                    synonymsManagementAPIService.putSynonymRule(
-                        // Error here
-                        synonymSetId,
-                        rules[i],
-                        new ActionListener<>() {
+        synonymsManagementAPIService.putSynonymsSet(
+            synonymSetId,
+            randomSynonymsSet(synonymsToCreate),
+            randomBoolean(),
+            new ActionListener<>() {
+                @Override
+                public void onResponse(SynonymsManagementAPIService.SynonymsReloadResult synonymsReloadResult) {
+                    // Create as many rules as should fail
+                    SynonymRule[] rules = randomSynonymsSet(atLeast(rulesToUpdate + 1));
+                    CountDownLatch updatedRulesLatch = new CountDownLatch(rulesToUpdate);
+                    for (int i = 0; i < rulesToUpdate; i++) {
+                        synonymsManagementAPIService.putSynonymRule(synonymSetId, rules[i], true, new ActionListener<>() {
                             @Override
                             public void onResponse(SynonymsManagementAPIService.SynonymsReloadResult synonymsReloadResult) {
-                                fail("Shouldn't have been able to update a rule");
+                                updatedRulesLatch.countDown();
                             }
 
                             @Override
                             public void onFailure(Exception e) {
-                                if (e instanceof IllegalArgumentException == false) {
-                                    fail(e);
+                                fail(e);
+                            }
+                        });
+                    }
+                    try {
+                        updatedRulesLatch.await(5, TimeUnit.SECONDS);
+                    } catch (InterruptedException e) {
+                        fail(e);
+                    }
+
+                    // Updating more rules fails
+                    int rulesToInsert = rules.length - rulesToUpdate;
+                    CountDownLatch insertRulesLatch = new CountDownLatch(rulesToInsert);
+                    for (int i = rulesToUpdate; i < rulesToInsert; i++) {
+                        synonymsManagementAPIService.putSynonymRule(
+                            // Error here
+                            synonymSetId,
+                            rules[i],
+                            randomBoolean(),
+                            new ActionListener<>() {
+                                @Override
+                                public void onResponse(SynonymsManagementAPIService.SynonymsReloadResult synonymsReloadResult) {
+                                    fail("Shouldn't have been able to update a rule");
+                                }
+
+                                @Override
+                                public void onFailure(Exception e) {
+                                    if (e instanceof IllegalArgumentException == false) {
+                                        fail(e);
+                                    }
+                                    updatedRulesLatch.countDown();
                                 }
-                                updatedRulesLatch.countDown();
                             }
-                        }
-                    );
+                        );
+                    }
+                    try {
+                        insertRulesLatch.await(5, TimeUnit.SECONDS);
+                    } catch (InterruptedException e) {
+                        fail(e);
+                    }
                 }
-                try {
-                    insertRulesLatch.await(5, TimeUnit.SECONDS);
-                } catch (InterruptedException e) {
+
+                @Override
+                public void onFailure(Exception e) {
                     fail(e);
                 }
             }
-
-            @Override
-            public void onFailure(Exception e) {
-                fail(e);
-            }
-        });
+        );
 
         latch.await(5, TimeUnit.SECONDS);
     }
@@ -189,13 +207,14 @@ public class SynonymsManagementAPIServiceIT extends ESIntegTestCase {
         CountDownLatch latch = new CountDownLatch(1);
         String synonymSetId = randomIdentifier();
         SynonymRule[] synonymsSet = randomSynonymsSet(maxSynonymSets, maxSynonymSets);
-        synonymsManagementAPIService.putSynonymsSet(synonymSetId, synonymsSet, new ActionListener<>() {
+        synonymsManagementAPIService.putSynonymsSet(synonymSetId, synonymsSet, true, new ActionListener<>() {
             @Override
             public void onResponse(SynonymsManagementAPIService.SynonymsReloadResult synonymsReloadResult) {
                 // Updating a rule fails
                 synonymsManagementAPIService.putSynonymRule(
                     synonymSetId,
                     synonymsSet[randomIntBetween(0, maxSynonymSets - 1)],
+                    randomBoolean(),
                     new ActionListener<>() {
                         @Override
                         public void onResponse(SynonymsManagementAPIService.SynonymsReloadResult synonymsReloadResult) {
@@ -224,11 +243,11 @@ public class SynonymsManagementAPIServiceIT extends ESIntegTestCase {
         String synonymSetId = randomIdentifier();
         String ruleId = randomIdentifier();
         SynonymRule[] synonymsSet = randomSynonymsSet(maxSynonymSets, maxSynonymSets);
-        synonymsManagementAPIService.putSynonymsSet(synonymSetId, synonymsSet, new ActionListener<>() {
+        synonymsManagementAPIService.putSynonymsSet(synonymSetId, synonymsSet, randomBoolean(), new ActionListener<>() {
             @Override
             public void onResponse(SynonymsManagementAPIService.SynonymsReloadResult synonymsReloadResult) {
                 // Updating a rule fails
-                synonymsManagementAPIService.putSynonymRule(synonymSetId, randomSynonymRule(ruleId), new ActionListener<>() {
+                synonymsManagementAPIService.putSynonymRule(synonymSetId, randomSynonymRule(ruleId), true, new ActionListener<>() {
                     @Override
                     public void onResponse(SynonymsManagementAPIService.SynonymsReloadResult synonymsReloadResult) {
                         fail("Should not create a new rule that does not exist when at max capacity");
@@ -289,4 +308,113 @@ public class SynonymsManagementAPIServiceIT extends ESIntegTestCase {
         readLatch.await(5, TimeUnit.SECONDS);
         verify(logger).warn(anyString(), eq(synonymSetId));
     }
+
+    public void testCreateSynonymsWithYellowSynonymsIndex() throws Exception {
+
+        // Override health method check to simulate a timeout in checking the synonyms index
+        synonymsManagementAPIService = new SynonymsManagementAPIService(client()) {
+            @Override
+            void checkSynonymsIndexHealth(ActionListener<ClusterHealthResponse> listener) {
+                ClusterState clusterState = ClusterState.builder(ClusterName.DEFAULT).build();
+                ClusterHealthResponse response = new ClusterHealthResponse(
+                    randomIdentifier(),
+                    new String[] { SynonymsManagementAPIService.SYNONYMS_INDEX_CONCRETE_NAME },
+                    clusterState
+                );
+                response.setTimedOut(true);
+                listener.onResponse(response);
+            }
+        };
+
+        // Create a rule fails
+        CountDownLatch putLatch = new CountDownLatch(1);
+        String synonymSetId = randomIdentifier();
+        synonymsManagementAPIService.putSynonymsSet(synonymSetId, randomSynonymsSet(1, 1), true, new ActionListener<>() {
+            @Override
+            public void onResponse(SynonymsManagementAPIService.SynonymsReloadResult synonymsReloadResult) {
+                fail("Shouldn't have been able to create synonyms with refresh in synonyms index health");
+            }
+
+            @Override
+            public void onFailure(Exception e) {
+                // Expected
+                assertTrue(e instanceof IndexCreationException);
+                assertTrue(e.getMessage().contains("synonyms index [.synonyms] is not searchable"));
+                putLatch.countDown();
+            }
+        });
+
+        putLatch.await(5, TimeUnit.SECONDS);
+
+        // Update a rule fails
+        CountDownLatch updateLatch = new CountDownLatch(1);
+        String synonymRuleId = randomIdentifier();
+        synonymsManagementAPIService.putSynonymRule(synonymSetId, randomSynonymRule(synonymRuleId), true, new ActionListener<>() {
+            @Override
+            public void onResponse(SynonymsManagementAPIService.SynonymsReloadResult synonymsReloadResult) {
+                fail("Shouldn't have been able to update synonyms with refresh in synonyms index health");
+            }
+
+            @Override
+            public void onFailure(Exception e) {
+                // Expected
+                assertTrue(e instanceof IndexCreationException);
+                assertTrue(e.getMessage().contains("synonyms index [.synonyms] is not searchable"));
+                updateLatch.countDown();
+            }
+        });
+
+        updateLatch.await(5, TimeUnit.SECONDS);
+
+        // Delete a rule does not fail
+        CountDownLatch deleteLatch = new CountDownLatch(1);
+        synonymsManagementAPIService.deleteSynonymRule(synonymSetId, synonymRuleId, true, new ActionListener<>() {
+            @Override
+            public void onResponse(SynonymsManagementAPIService.SynonymsReloadResult synonymsReloadResult) {
+                updateLatch.countDown();
+            }
+
+            @Override
+            public void onFailure(Exception e) {
+                // Expected
+                fail("Should have been able to delete a synonym rule");
+            }
+        });
+
+        deleteLatch.await(5, TimeUnit.SECONDS);
+
+        // But, we can still create a synonyms set without refresh
+        CountDownLatch putNoRefreshLatch = new CountDownLatch(1);
+        synonymsManagementAPIService.putSynonymsSet(synonymSetId, randomSynonymsSet(1, 1), false, new ActionListener<>() {
+            @Override
+            public void onResponse(SynonymsManagementAPIService.SynonymsReloadResult synonymsReloadResult) {
+                // Expected
+                putLatch.countDown();
+            }
+
+            @Override
+            public void onFailure(Exception e) {
+                fail(e);
+            }
+        });
+
+        putNoRefreshLatch.await(5, TimeUnit.SECONDS);
+
+        // Same for update
+        CountDownLatch putRuleNoRefreshLatch = new CountDownLatch(1);
+        synonymsManagementAPIService.putSynonymRule(synonymSetId, randomSynonymRule(synonymRuleId), false, new ActionListener<>() {
+            @Override
+            public void onResponse(SynonymsManagementAPIService.SynonymsReloadResult synonymsReloadResult) {
+                // Expected
+                putRuleNoRefreshLatch.countDown();
+            }
+
+            @Override
+            public void onFailure(Exception e) {
+                fail(e);
+            }
+        });
+
+        putRuleNoRefreshLatch.await(5, TimeUnit.SECONDS);
+    }
 }

+ 1 - 0
server/src/main/java/org/elasticsearch/TransportVersions.java

@@ -232,6 +232,7 @@ public class TransportVersions {
     public static final TransportVersion BATCHED_QUERY_EXECUTION_DELAYABLE_WRITABLE = def(9_057_0_00);
     public static final TransportVersion SEARCH_INCREMENTAL_TOP_DOCS_NULL = def(9_058_0_00);
     public static final TransportVersion COMPRESS_DELAYABLE_WRITEABLE = def(9_059_0_00);
+    public static final TransportVersion SYNONYMS_REFRESH_PARAM = def(9_060_0_00);
 
     /*
      * STOP! READ THIS FIRST! No, really,

+ 16 - 2
server/src/main/java/org/elasticsearch/action/synonyms/DeleteSynonymRuleAction.java

@@ -9,6 +9,7 @@
 
 package org.elasticsearch.action.synonyms;
 
+import org.elasticsearch.TransportVersions;
 import org.elasticsearch.action.ActionRequest;
 import org.elasticsearch.action.ActionRequestValidationException;
 import org.elasticsearch.action.ActionType;
@@ -31,18 +32,24 @@ public class DeleteSynonymRuleAction extends ActionType<SynonymUpdateResponse> {
 
     public static class Request extends ActionRequest {
         private final String synonymsSetId;
-
         private final String synonymRuleId;
+        private final boolean refresh;
 
         public Request(StreamInput in) throws IOException {
             super(in);
             this.synonymsSetId = in.readString();
             this.synonymRuleId = in.readString();
+            if (in.getTransportVersion().onOrAfter(TransportVersions.SYNONYMS_REFRESH_PARAM)) {
+                this.refresh = in.readBoolean();
+            } else {
+                this.refresh = true;
+            }
         }
 
-        public Request(String synonymsSetId, String synonymRuleId) {
+        public Request(String synonymsSetId, String synonymRuleId, boolean refresh) {
             this.synonymsSetId = synonymsSetId;
             this.synonymRuleId = synonymRuleId;
+            this.refresh = refresh;
         }
 
         @Override
@@ -63,6 +70,9 @@ public class DeleteSynonymRuleAction extends ActionType<SynonymUpdateResponse> {
             super.writeTo(out);
             out.writeString(synonymsSetId);
             out.writeString(synonymRuleId);
+            if (out.getTransportVersion().onOrAfter(TransportVersions.SYNONYMS_REFRESH_PARAM)) {
+                out.writeBoolean(refresh);
+            }
         }
 
         public String synonymsSetId() {
@@ -73,6 +83,10 @@ public class DeleteSynonymRuleAction extends ActionType<SynonymUpdateResponse> {
             return synonymRuleId;
         }
 
+        public boolean refresh() {
+            return refresh;
+        }
+
         @Override
         public boolean equals(Object o) {
             if (this == o) return true;

+ 23 - 5
server/src/main/java/org/elasticsearch/action/synonyms/PutSynonymRuleAction.java

@@ -9,6 +9,7 @@
 
 package org.elasticsearch.action.synonyms;
 
+import org.elasticsearch.TransportVersions;
 import org.elasticsearch.action.ActionRequest;
 import org.elasticsearch.action.ActionRequestValidationException;
 import org.elasticsearch.action.ActionType;
@@ -40,8 +41,8 @@ public class PutSynonymRuleAction extends ActionType<SynonymUpdateResponse> {
 
     public static class Request extends ActionRequest {
         private final String synonymsSetId;
-
         private final SynonymRule synonymRule;
+        private final boolean refresh;
 
         public static final ParseField SYNONYMS_FIELD = new ParseField(SynonymsManagementAPIService.SYNONYMS_FIELD);
         private static final ConstructingObjectParser<SynonymRule, String> PARSER = new ConstructingObjectParser<>(
@@ -58,20 +59,28 @@ public class PutSynonymRuleAction extends ActionType<SynonymUpdateResponse> {
             super(in);
             this.synonymsSetId = in.readString();
             this.synonymRule = new SynonymRule(in);
+            if (in.getTransportVersion().onOrAfter(TransportVersions.SYNONYMS_REFRESH_PARAM)) {
+                this.refresh = in.readBoolean();
+            } else {
+                this.refresh = true;
+            }
         }
 
-        public Request(String synonymsSetId, String synonymRuleId, BytesReference content, XContentType contentType) throws IOException {
+        public Request(String synonymsSetId, String synonymRuleId, boolean refresh, BytesReference content, XContentType contentType)
+            throws IOException {
             this.synonymsSetId = synonymsSetId;
             try (XContentParser parser = XContentHelper.createParser(XContentParserConfiguration.EMPTY, content, contentType)) {
                 this.synonymRule = PARSER.apply(parser, synonymRuleId);
             } catch (Exception e) {
                 throw new IllegalArgumentException("Failed to parse: " + content.utf8ToString(), e);
             }
+            this.refresh = refresh;
         }
 
-        Request(String synonymsSetId, SynonymRule synonymRule) {
+        Request(String synonymsSetId, SynonymRule synonymRule, boolean refresh) {
             this.synonymsSetId = synonymsSetId;
             this.synonymRule = synonymRule;
+            this.refresh = refresh;
         }
 
         @Override
@@ -96,6 +105,9 @@ public class PutSynonymRuleAction extends ActionType<SynonymUpdateResponse> {
             super.writeTo(out);
             out.writeString(synonymsSetId);
             synonymRule.writeTo(out);
+            if (out.getTransportVersion().onOrAfter(TransportVersions.SYNONYMS_REFRESH_PARAM)) {
+                out.writeBoolean(refresh);
+            }
         }
 
         public String synonymsSetId() {
@@ -106,17 +118,23 @@ public class PutSynonymRuleAction extends ActionType<SynonymUpdateResponse> {
             return synonymRule;
         }
 
+        public boolean refresh() {
+            return refresh;
+        }
+
         @Override
         public boolean equals(Object o) {
             if (this == o) return true;
             if (o == null || getClass() != o.getClass()) return false;
             Request request = (Request) o;
-            return Objects.equals(synonymsSetId, request.synonymsSetId) && Objects.equals(synonymRule, request.synonymRule);
+            return Objects.equals(refresh, request.refresh)
+                && Objects.equals(synonymsSetId, request.synonymsSetId)
+                && Objects.equals(synonymRule, request.synonymRule);
         }
 
         @Override
         public int hashCode() {
-            return Objects.hash(synonymsSetId, synonymRule);
+            return Objects.hash(synonymsSetId, synonymRule, refresh);
         }
     }
 }

+ 22 - 4
server/src/main/java/org/elasticsearch/action/synonyms/PutSynonymsAction.java

@@ -9,6 +9,7 @@
 
 package org.elasticsearch.action.synonyms;
 
+import org.elasticsearch.TransportVersions;
 import org.elasticsearch.action.ActionRequest;
 import org.elasticsearch.action.ActionRequestValidationException;
 import org.elasticsearch.action.ActionType;
@@ -43,6 +44,7 @@ public class PutSynonymsAction extends ActionType<SynonymUpdateResponse> {
     public static class Request extends ActionRequest {
         private final String synonymsSetId;
         private final SynonymRule[] synonymRules;
+        private final boolean refresh;
 
         public static final ParseField SYNONYMS_SET_FIELD = new ParseField(SynonymsManagementAPIService.SYNONYMS_SET_FIELD);
         private static final ConstructingObjectParser<SynonymRule[], Void> PARSER = new ConstructingObjectParser<>("synonyms_set", args -> {
@@ -59,10 +61,16 @@ public class PutSynonymsAction extends ActionType<SynonymUpdateResponse> {
             super(in);
             this.synonymsSetId = in.readString();
             this.synonymRules = in.readArray(SynonymRule::new, SynonymRule[]::new);
+            if (in.getTransportVersion().onOrAfter(TransportVersions.SYNONYMS_REFRESH_PARAM)) {
+                this.refresh = in.readBoolean();
+            } else {
+                this.refresh = false;
+            }
         }
 
-        public Request(String synonymsSetId, BytesReference content, XContentType contentType) throws IOException {
+        public Request(String synonymsSetId, boolean refresh, BytesReference content, XContentType contentType) throws IOException {
             this.synonymsSetId = synonymsSetId;
+            this.refresh = refresh;
             try (XContentParser parser = XContentHelper.createParser(XContentParserConfiguration.EMPTY, content, contentType)) {
                 this.synonymRules = PARSER.apply(parser, null);
             } catch (Exception e) {
@@ -70,9 +78,10 @@ public class PutSynonymsAction extends ActionType<SynonymUpdateResponse> {
             }
         }
 
-        Request(String synonymsSetId, SynonymRule[] synonymRules) {
+        Request(String synonymsSetId, SynonymRule[] synonymRules, boolean refresh) {
             this.synonymsSetId = synonymsSetId;
             this.synonymRules = synonymRules;
+            this.refresh = refresh;
         }
 
         @Override
@@ -95,12 +104,19 @@ public class PutSynonymsAction extends ActionType<SynonymUpdateResponse> {
             super.writeTo(out);
             out.writeString(synonymsSetId);
             out.writeArray(synonymRules);
+            if (out.getTransportVersion().onOrAfter(TransportVersions.SYNONYMS_REFRESH_PARAM)) {
+                out.writeBoolean(refresh);
+            }
         }
 
         public String synonymsSetId() {
             return synonymsSetId;
         }
 
+        public boolean refresh() {
+            return refresh;
+        }
+
         public SynonymRule[] synonymRules() {
             return synonymRules;
         }
@@ -110,12 +126,14 @@ public class PutSynonymsAction extends ActionType<SynonymUpdateResponse> {
             if (this == o) return true;
             if (o == null || getClass() != o.getClass()) return false;
             Request request = (Request) o;
-            return Objects.equals(synonymsSetId, request.synonymsSetId) && Arrays.equals(synonymRules, request.synonymRules);
+            return Objects.equals(refresh, request.refresh)
+                && Objects.equals(synonymsSetId, request.synonymsSetId)
+                && Arrays.equals(synonymRules, request.synonymRules);
         }
 
         @Override
         public int hashCode() {
-            return Objects.hash(synonymsSetId, Arrays.hashCode(synonymRules));
+            return Objects.hash(synonymsSetId, Arrays.hashCode(synonymRules), refresh);
         }
     }
 }

+ 35 - 6
server/src/main/java/org/elasticsearch/action/synonyms/SynonymUpdateResponse.java

@@ -9,6 +9,7 @@
 
 package org.elasticsearch.action.synonyms;
 
+import org.elasticsearch.TransportVersions;
 import org.elasticsearch.action.ActionResponse;
 import org.elasticsearch.action.admin.indices.analyze.ReloadAnalyzersResponse;
 import org.elasticsearch.common.io.stream.StreamInput;
@@ -20,17 +21,27 @@ import org.elasticsearch.xcontent.ToXContentObject;
 import org.elasticsearch.xcontent.XContentBuilder;
 
 import java.io.IOException;
+import java.util.List;
 import java.util.Locale;
+import java.util.Map;
 import java.util.Objects;
 
 public class SynonymUpdateResponse extends ActionResponse implements ToXContentObject {
 
+    public static final String RESULT_FIELD = "result";
+    public static final String RELOAD_ANALYZERS_DETAILS_FIELD = "reload_analyzers_details";
+    static final ReloadAnalyzersResponse EMPTY_RELOAD_ANALYZER_RESPONSE = new ReloadAnalyzersResponse(0, 0, 0, List.of(), Map.of());
+
     private final UpdateSynonymsResultStatus updateStatus;
     private final ReloadAnalyzersResponse reloadAnalyzersResponse;
 
     public SynonymUpdateResponse(StreamInput in) throws IOException {
         this.updateStatus = in.readEnum(UpdateSynonymsResultStatus.class);
-        this.reloadAnalyzersResponse = new ReloadAnalyzersResponse(in);
+        if (in.getTransportVersion().onOrAfter(TransportVersions.SYNONYMS_REFRESH_PARAM)) {
+            this.reloadAnalyzersResponse = in.readOptionalWriteable(ReloadAnalyzersResponse::new);
+        } else {
+            this.reloadAnalyzersResponse = new ReloadAnalyzersResponse(in);
+        }
     }
 
     public SynonymUpdateResponse(SynonymsReloadResult synonymsReloadResult) {
@@ -38,7 +49,6 @@ public class SynonymUpdateResponse extends ActionResponse implements ToXContentO
         UpdateSynonymsResultStatus updateStatus = synonymsReloadResult.synonymsOperationResult();
         Objects.requireNonNull(updateStatus, "Update status must not be null");
         ReloadAnalyzersResponse reloadResponse = synonymsReloadResult.reloadAnalyzersResponse();
-        Objects.requireNonNull(reloadResponse, "Reload analyzers response must not be null");
 
         this.updateStatus = updateStatus;
         this.reloadAnalyzersResponse = reloadResponse;
@@ -48,9 +58,11 @@ public class SynonymUpdateResponse extends ActionResponse implements ToXContentO
     public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
         builder.startObject();
         {
-            builder.field("result", updateStatus.name().toLowerCase(Locale.ENGLISH));
-            builder.field("reload_analyzers_details");
-            reloadAnalyzersResponse.toXContent(builder, params);
+            builder.field(RESULT_FIELD, updateStatus.name().toLowerCase(Locale.ENGLISH));
+            if (reloadAnalyzersResponse != null) {
+                builder.field(RELOAD_ANALYZERS_DETAILS_FIELD);
+                reloadAnalyzersResponse.toXContent(builder, params);
+            }
         }
         builder.endObject();
 
@@ -60,7 +72,16 @@ public class SynonymUpdateResponse extends ActionResponse implements ToXContentO
     @Override
     public void writeTo(StreamOutput out) throws IOException {
         out.writeEnum(updateStatus);
-        reloadAnalyzersResponse.writeTo(out);
+        if (out.getTransportVersion().onOrAfter(TransportVersions.SYNONYMS_REFRESH_PARAM)) {
+            out.writeOptionalWriteable(reloadAnalyzersResponse);
+        } else {
+            if (reloadAnalyzersResponse == null) {
+                // Nulls will be written as empty reload analyzer responses for older versions
+                EMPTY_RELOAD_ANALYZER_RESPONSE.writeTo(out);
+            } else {
+                reloadAnalyzersResponse.writeTo(out);
+            }
+        }
     }
 
     public RestStatus status() {
@@ -70,6 +91,14 @@ public class SynonymUpdateResponse extends ActionResponse implements ToXContentO
         };
     }
 
+    UpdateSynonymsResultStatus updateStatus() {
+        return updateStatus;
+    }
+
+    ReloadAnalyzersResponse reloadAnalyzersResponse() {
+        return reloadAnalyzersResponse;
+    }
+
     @Override
     public boolean equals(Object o) {
         if (this == o) return true;

+ 1 - 0
server/src/main/java/org/elasticsearch/action/synonyms/TransportDeleteSynonymRuleAction.java

@@ -41,6 +41,7 @@ public class TransportDeleteSynonymRuleAction extends HandledTransportAction<Del
         synonymsManagementAPIService.deleteSynonymRule(
             request.synonymsSetId(),
             request.synonymRuleId(),
+            request.refresh(),
             listener.map(SynonymUpdateResponse::new)
         );
     }

+ 2 - 2
server/src/main/java/org/elasticsearch/action/synonyms/TransportPutSynonymRuleAction.java

@@ -41,8 +41,8 @@ public class TransportPutSynonymRuleAction extends HandledTransportAction<PutSyn
         synonymsManagementAPIService.putSynonymRule(
             request.synonymsSetId(),
             request.synonymRule(),
-            listener.map(updateResponse -> new SynonymUpdateResponse(updateResponse))
+            request.refresh(),
+            listener.map(SynonymUpdateResponse::new)
         );
-
     }
 }

+ 1 - 0
server/src/main/java/org/elasticsearch/action/synonyms/TransportPutSynonymsAction.java

@@ -35,6 +35,7 @@ public class TransportPutSynonymsAction extends HandledTransportAction<PutSynony
         synonymsManagementAPIService.putSynonymsSet(
             request.synonymsSetId(),
             request.synonymRules(),
+            request.refresh(),
             listener.map(SynonymUpdateResponse::new)
         );
     }

+ 8 - 1
server/src/main/java/org/elasticsearch/rest/action/synonyms/RestDeleteSynonymRuleAction.java

@@ -19,6 +19,7 @@ import org.elasticsearch.rest.action.RestToXContentListener;
 
 import java.io.IOException;
 import java.util.List;
+import java.util.Set;
 
 import static org.elasticsearch.rest.RestRequest.Method.DELETE;
 
@@ -39,8 +40,14 @@ public class RestDeleteSynonymRuleAction extends BaseRestHandler {
     protected RestChannelConsumer prepareRequest(RestRequest restRequest, NodeClient client) throws IOException {
         DeleteSynonymRuleAction.Request request = new DeleteSynonymRuleAction.Request(
             restRequest.param("synonymsSet"),
-            restRequest.param("synonymRuleId")
+            restRequest.param("synonymRuleId"),
+            restRequest.paramAsBoolean("refresh", true)
         );
         return channel -> client.execute(DeleteSynonymRuleAction.INSTANCE, request, new RestToXContentListener<>(channel));
     }
+
+    @Override
+    public Set<String> supportedCapabilities() {
+        return SynonymCapabilities.CAPABILITIES;
+    }
 }

+ 7 - 0
server/src/main/java/org/elasticsearch/rest/action/synonyms/RestPutSynonymRuleAction.java

@@ -20,6 +20,7 @@ import org.elasticsearch.rest.action.RestToXContentListener;
 
 import java.io.IOException;
 import java.util.List;
+import java.util.Set;
 
 import static org.elasticsearch.rest.RestRequest.Method.PUT;
 
@@ -41,6 +42,7 @@ public class RestPutSynonymRuleAction extends BaseRestHandler {
         PutSynonymRuleAction.Request request = new PutSynonymRuleAction.Request(
             restRequest.param("synonymsSet"),
             restRequest.param("synonymRuleId"),
+            restRequest.paramAsBoolean("refresh", true),
             restRequest.content(),
             restRequest.getXContentType()
         );
@@ -50,4 +52,9 @@ public class RestPutSynonymRuleAction extends BaseRestHandler {
             new RestToXContentListener<>(channel, SynonymUpdateResponse::status, r -> null)
         );
     }
+
+    @Override
+    public Set<String> supportedCapabilities() {
+        return SynonymCapabilities.CAPABILITIES;
+    }
 }

+ 7 - 0
server/src/main/java/org/elasticsearch/rest/action/synonyms/RestPutSynonymsAction.java

@@ -20,6 +20,7 @@ import org.elasticsearch.rest.action.RestToXContentListener;
 
 import java.io.IOException;
 import java.util.List;
+import java.util.Set;
 
 import static org.elasticsearch.rest.RestRequest.Method.PUT;
 
@@ -40,6 +41,7 @@ public class RestPutSynonymsAction extends BaseRestHandler {
     protected RestChannelConsumer prepareRequest(RestRequest restRequest, NodeClient client) throws IOException {
         PutSynonymsAction.Request request = new PutSynonymsAction.Request(
             restRequest.param("synonymsSet"),
+            restRequest.paramAsBoolean("refresh", true),
             restRequest.content(),
             restRequest.getXContentType()
         );
@@ -49,4 +51,9 @@ public class RestPutSynonymsAction extends BaseRestHandler {
             new RestToXContentListener<>(channel, SynonymUpdateResponse::status, r -> null)
         );
     }
+
+    @Override
+    public Set<String> supportedCapabilities() {
+        return SynonymCapabilities.CAPABILITIES;
+    }
 }

+ 26 - 0
server/src/main/java/org/elasticsearch/rest/action/synonyms/SynonymCapabilities.java

@@ -0,0 +1,26 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the "Elastic License
+ * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
+ * Public License v 1"; you may not use this file except in compliance with, at
+ * your election, the "Elastic License 2.0", the "GNU Affero General Public
+ * License v3.0 only", or the "Server Side Public License, v 1".
+ */
+
+package org.elasticsearch.rest.action.synonyms;
+
+import java.util.Set;
+
+/**
+ * A {@link Set} of "capabilities" supported by the {@link RestPutSynonymsAction} and {@link RestPutSynonymRuleAction}.
+ */
+public final class SynonymCapabilities {
+
+    private SynonymCapabilities() {
+        throw new UnsupportedOperationException("This is a utility class and cannot be instantiated");
+    }
+
+    private static final String SYNONYMS_REFRESH_PARAM = "synonyms_refresh_param";
+
+    public static final Set<String> CAPABILITIES = Set.of(SYNONYMS_REFRESH_PARAM);
+}

+ 95 - 16
server/src/main/java/org/elasticsearch/synonyms/SynonymsManagementAPIService.java

@@ -19,6 +19,9 @@ import org.elasticsearch.action.ActionListener;
 import org.elasticsearch.action.DelegatingActionListener;
 import org.elasticsearch.action.DocWriteRequest;
 import org.elasticsearch.action.DocWriteResponse;
+import org.elasticsearch.action.admin.cluster.health.ClusterHealthRequest;
+import org.elasticsearch.action.admin.cluster.health.ClusterHealthResponse;
+import org.elasticsearch.action.admin.cluster.health.TransportClusterHealthAction;
 import org.elasticsearch.action.admin.indices.analyze.ReloadAnalyzersRequest;
 import org.elasticsearch.action.admin.indices.analyze.ReloadAnalyzersResponse;
 import org.elasticsearch.action.admin.indices.analyze.TransportReloadAnalyzersAction;
@@ -36,12 +39,14 @@ import org.elasticsearch.client.internal.OriginSettingClient;
 import org.elasticsearch.cluster.metadata.IndexMetadata;
 import org.elasticsearch.cluster.routing.Preference;
 import org.elasticsearch.common.settings.Settings;
+import org.elasticsearch.core.TimeValue;
 import org.elasticsearch.index.IndexNotFoundException;
 import org.elasticsearch.index.query.BoolQueryBuilder;
 import org.elasticsearch.index.query.QueryBuilders;
 import org.elasticsearch.index.reindex.BulkByScrollResponse;
 import org.elasticsearch.index.reindex.DeleteByQueryAction;
 import org.elasticsearch.index.reindex.DeleteByQueryRequest;
+import org.elasticsearch.indices.IndexCreationException;
 import org.elasticsearch.indices.SystemIndexDescriptor;
 import org.elasticsearch.rest.RestStatus;
 import org.elasticsearch.search.aggregations.BucketOrder;
@@ -72,7 +77,7 @@ public class SynonymsManagementAPIService {
 
     private static final String SYNONYMS_INDEX_NAME_PATTERN = ".synonyms-*";
     private static final int SYNONYMS_INDEX_FORMAT = 2;
-    private static final String SYNONYMS_INDEX_CONCRETE_NAME = ".synonyms-" + SYNONYMS_INDEX_FORMAT;
+    static final String SYNONYMS_INDEX_CONCRETE_NAME = ".synonyms-" + SYNONYMS_INDEX_FORMAT;
     private static final String SYNONYMS_ALIAS_NAME = ".synonyms";
     public static final String SYNONYMS_FEATURE_NAME = "synonyms";
     // Stores the synonym set the rule belongs to
@@ -90,6 +95,7 @@ public class SynonymsManagementAPIService {
     private static final String SYNONYM_RULE_ID_FIELD = SynonymRule.ID_FIELD.getPreferredName();
     private static final String SYNONYM_SETS_AGG_NAME = "synonym_sets_aggr";
     private static final int SYNONYMS_INDEX_MAPPINGS_VERSION = 1;
+    public static final int INDEX_SEARCHABLE_TIMEOUT_SECONDS = 30;
     private final int maxSynonymsSets;
 
     // Package private for testing
@@ -301,7 +307,12 @@ public class SynonymsManagementAPIService {
         });
     }
 
-    public void putSynonymsSet(String synonymSetId, SynonymRule[] synonymsSet, ActionListener<SynonymsReloadResult> listener) {
+    public void putSynonymsSet(
+        String synonymSetId,
+        SynonymRule[] synonymsSet,
+        boolean refresh,
+        ActionListener<SynonymsReloadResult> listener
+    ) {
         if (synonymsSet.length > maxSynonymsSets) {
             listener.onFailure(
                 new IllegalArgumentException("The number of synonyms rules in a synonym set cannot exceed " + maxSynonymsSets)
@@ -343,7 +354,13 @@ public class SynonymsManagementAPIService {
                         ? UpdateSynonymsResultStatus.CREATED
                         : UpdateSynonymsResultStatus.UPDATED;
 
-                    reloadAnalyzers(synonymSetId, false, bulkInsertResponseListener, updateSynonymsResultStatus);
+                    checkIndexSearchableAndReloadAnalyzers(
+                        synonymSetId,
+                        refresh,
+                        false,
+                        updateSynonymsResultStatus,
+                        bulkInsertResponseListener
+                    );
                 })
             );
         }));
@@ -366,7 +383,12 @@ public class SynonymsManagementAPIService {
         bulkRequestBuilder.setRefreshPolicy(WriteRequest.RefreshPolicy.IMMEDIATE).execute(listener);
     }
 
-    public void putSynonymRule(String synonymsSetId, SynonymRule synonymRule, ActionListener<SynonymsReloadResult> listener) {
+    public void putSynonymRule(
+        String synonymsSetId,
+        SynonymRule synonymRule,
+        boolean refresh,
+        ActionListener<SynonymsReloadResult> listener
+    ) {
         checkSynonymSetExists(synonymsSetId, listener.delegateFailureAndWrap((l1, obj) -> {
             // Count synonym rules to check if we're at maximum
             BoolQueryBuilder queryFilter = QueryBuilders.boolQuery()
@@ -388,14 +410,18 @@ public class SynonymsManagementAPIService {
                             new IllegalArgumentException("The number of synonym rules in a synonyms set cannot exceed " + maxSynonymsSets)
                         );
                     } else {
-                        indexSynonymRule(synonymsSetId, synonymRule, searchListener);
+                        indexSynonymRule(synonymsSetId, synonymRule, refresh, searchListener);
                     }
                 }));
         }));
     }
 
-    private void indexSynonymRule(String synonymsSetId, SynonymRule synonymRule, ActionListener<SynonymsReloadResult> listener)
-        throws IOException {
+    private void indexSynonymRule(
+        String synonymsSetId,
+        SynonymRule synonymRule,
+        boolean refresh,
+        ActionListener<SynonymsReloadResult> listener
+    ) throws IOException {
         IndexRequest indexRequest = createSynonymRuleIndexRequest(synonymsSetId, synonymRule).setRefreshPolicy(
             WriteRequest.RefreshPolicy.IMMEDIATE
         );
@@ -404,7 +430,7 @@ public class SynonymsManagementAPIService {
                 ? UpdateSynonymsResultStatus.CREATED
                 : UpdateSynonymsResultStatus.UPDATED;
 
-            reloadAnalyzers(synonymsSetId, false, l2, updateStatus);
+            checkIndexSearchableAndReloadAnalyzers(synonymsSetId, refresh, false, updateStatus, l2);
         }));
     }
 
@@ -424,7 +450,12 @@ public class SynonymsManagementAPIService {
         );
     }
 
-    public void deleteSynonymRule(String synonymsSetId, String synonymRuleId, ActionListener<SynonymsReloadResult> listener) {
+    public void deleteSynonymRule(
+        String synonymsSetId,
+        String synonymRuleId,
+        boolean refresh,
+        ActionListener<SynonymsReloadResult> listener
+    ) {
         client.prepareDelete(SYNONYMS_ALIAS_NAME, internalSynonymRuleId(synonymsSetId, synonymRuleId))
             .setRefreshPolicy(WriteRequest.RefreshPolicy.IMMEDIATE)
             .execute(new DelegatingIndexNotFoundActionListener<>(synonymsSetId, listener, (l, deleteResponse) -> {
@@ -443,7 +474,11 @@ public class SynonymsManagementAPIService {
                     return;
                 }
 
-                reloadAnalyzers(synonymsSetId, false, listener, UpdateSynonymsResultStatus.DELETED);
+                if (refresh) {
+                    reloadAnalyzers(synonymsSetId, false, UpdateSynonymsResultStatus.DELETED, listener);
+                } else {
+                    listener.onResponse(new SynonymsReloadResult(UpdateSynonymsResultStatus.DELETED, null));
+                }
             }));
     }
 
@@ -501,7 +536,7 @@ public class SynonymsManagementAPIService {
     public void deleteSynonymsSet(String synonymSetId, ActionListener<AcknowledgedResponse> listener) {
 
         // Previews reloading the resource to understand its usage on indices
-        reloadAnalyzers(synonymSetId, true, listener.delegateFailure((reloadListener, reloadResult) -> {
+        reloadAnalyzers(synonymSetId, true, null, listener.delegateFailure((reloadListener, reloadResult) -> {
             Map<String, ReloadAnalyzersResponse.ReloadDetails> reloadDetails = reloadResult.reloadAnalyzersResponse.getReloadDetails();
             if (reloadDetails.isEmpty() == false) {
                 Set<String> indices = reloadDetails.entrySet()
@@ -538,14 +573,48 @@ public class SynonymsManagementAPIService {
 
                 deleteObjectsListener.onResponse(AcknowledgedResponse.of(true));
             }));
-        }), null);
+        }));
     }
 
-    private <T> void reloadAnalyzers(
+    private <T> void checkIndexSearchableAndReloadAnalyzers(
         String synonymSetId,
+        boolean refresh,
         boolean preview,
-        ActionListener<SynonymsReloadResult> listener,
-        UpdateSynonymsResultStatus synonymsOperationResult
+        UpdateSynonymsResultStatus synonymsOperationResult,
+        ActionListener<SynonymsReloadResult> listener
+    ) {
+
+        if (refresh == false) {
+            // If not refreshing, we don't need to reload analyzers
+            listener.onResponse(new SynonymsReloadResult(synonymsOperationResult, null));
+            return;
+        }
+
+        // Check synonyms index is searchable before reloading, to ensure analyzers are able to load the changed information
+        checkSynonymsIndexHealth(listener.delegateFailure((l, response) -> {
+            if (response.isTimedOut()) {
+                l.onFailure(
+                    new IndexCreationException(
+                        "synonyms index ["
+                            + SYNONYMS_ALIAS_NAME
+                            + "] is not searchable. "
+                            + response.getActiveShardsPercent()
+                            + "% shards are active",
+                        null
+                    )
+                );
+                return;
+            }
+
+            reloadAnalyzers(synonymSetId, preview, synonymsOperationResult, listener);
+        }));
+    }
+
+    private void reloadAnalyzers(
+        String synonymSetId,
+        boolean preview,
+        UpdateSynonymsResultStatus synonymsOperationResult,
+        ActionListener<SynonymsReloadResult> listener
     ) {
         // auto-reload all reloadable analyzers (currently only those that use updateable synonym or keyword_marker filters)
         ReloadAnalyzersRequest reloadAnalyzersRequest = new ReloadAnalyzersRequest(synonymSetId, preview, "*");
@@ -556,13 +625,23 @@ public class SynonymsManagementAPIService {
         );
     }
 
+    // Allows checking failures in tests
+    void checkSynonymsIndexHealth(ActionListener<ClusterHealthResponse> listener) {
+        ClusterHealthRequest healthRequest = new ClusterHealthRequest(
+            TimeValue.timeValueSeconds(INDEX_SEARCHABLE_TIMEOUT_SECONDS),
+            SYNONYMS_ALIAS_NAME
+        ).waitForGreenStatus();
+
+        client.execute(TransportClusterHealthAction.TYPE, healthRequest, listener);
+    }
+
     // Retrieves the internal synonym rule ID to store it in the index. As the same synonym rule ID
     // can be used in different synonym sets, we prefix the ID with the synonym set to avoid collisions
     private static String internalSynonymRuleId(String synonymsSetId, String synonymRuleId) {
         return synonymsSetId + SYNONYM_RULE_ID_SEPARATOR + synonymRuleId;
     }
 
-    static Settings settings() {
+    private static Settings settings() {
         return Settings.builder()
             .put(IndexMetadata.SETTING_NUMBER_OF_SHARDS, 1)
             .put(IndexMetadata.SETTING_AUTO_EXPAND_REPLICAS, "0-1")

+ 2 - 2
server/src/test/java/org/elasticsearch/action/admin/indices/analyze/ReloadAnalyzersResponseTests.java

@@ -31,7 +31,7 @@ import static org.elasticsearch.xcontent.ConstructingObjectParser.constructorArg
 public class ReloadAnalyzersResponseTests extends AbstractBroadcastResponseTestCase<ReloadAnalyzersResponse> {
 
     @SuppressWarnings({ "unchecked" })
-    private static final ConstructingObjectParser<ReloadAnalyzersResponse, Void> PARSER = new ConstructingObjectParser<>(
+    public static final ConstructingObjectParser<ReloadAnalyzersResponse, Void> PARSER = new ConstructingObjectParser<>(
         "reload_analyzer",
         true,
         arg -> {
@@ -67,8 +67,8 @@ public class ReloadAnalyzersResponseTests extends AbstractBroadcastResponseTestC
         declareBroadcastFields(PARSER);
         PARSER.declareObjectArray(constructorArg(), ENTRY_PARSER, ReloadAnalyzersResponse.RELOAD_DETAILS_FIELD);
         ENTRY_PARSER.declareString(constructorArg(), ReloadAnalyzersResponse.INDEX_FIELD);
-        ENTRY_PARSER.declareStringArray(constructorArg(), ReloadAnalyzersResponse.RELOADED_ANALYZERS_FIELD);
         ENTRY_PARSER.declareStringArray(constructorArg(), ReloadAnalyzersResponse.RELOADED_NODE_IDS_FIELD);
+        ENTRY_PARSER.declareStringArray(constructorArg(), ReloadAnalyzersResponse.RELOADED_ANALYZERS_FIELD);
     }
 
     @Override

+ 1 - 1
server/src/test/java/org/elasticsearch/action/synonyms/DeleteSynonymRuleActionRequestSerializingTests.java

@@ -23,7 +23,7 @@ public class DeleteSynonymRuleActionRequestSerializingTests extends AbstractWire
 
     @Override
     protected DeleteSynonymRuleAction.Request createTestInstance() {
-        return new DeleteSynonymRuleAction.Request(randomIdentifier(), randomIdentifier());
+        return new DeleteSynonymRuleAction.Request(randomIdentifier(), randomIdentifier(), randomBoolean());
     }
 
     @Override

+ 1 - 1
server/src/test/java/org/elasticsearch/action/synonyms/PutSynonymRuleActionRequestSerializingTests.java

@@ -22,7 +22,7 @@ public class PutSynonymRuleActionRequestSerializingTests extends AbstractWireSer
 
     @Override
     protected PutSynonymRuleAction.Request createTestInstance() {
-        return new PutSynonymRuleAction.Request(randomIdentifier(), SynonymsTestUtils.randomSynonymRule());
+        return new PutSynonymRuleAction.Request(randomIdentifier(), SynonymsTestUtils.randomSynonymRule(), randomBoolean());
     }
 
     @Override

+ 1 - 1
server/src/test/java/org/elasticsearch/action/synonyms/PutSynonymsActionRequestSerializingTests.java

@@ -25,7 +25,7 @@ public class PutSynonymsActionRequestSerializingTests extends AbstractWireSerial
 
     @Override
     protected PutSynonymsAction.Request createTestInstance() {
-        return new PutSynonymsAction.Request(randomIdentifier(), randomSynonymsSet());
+        return new PutSynonymsAction.Request(randomIdentifier(), randomSynonymsSet(), randomBoolean());
     }
 
     @Override

+ 74 - 5
server/src/test/java/org/elasticsearch/action/synonyms/SynonymUpdateResponseSerializingTests.java

@@ -9,23 +9,54 @@
 
 package org.elasticsearch.action.synonyms;
 
+import org.elasticsearch.TransportVersion;
+import org.elasticsearch.TransportVersions;
 import org.elasticsearch.action.admin.indices.analyze.ReloadAnalyzersResponse;
 import org.elasticsearch.action.admin.indices.analyze.ReloadAnalyzersResponseTests;
 import org.elasticsearch.common.Strings;
 import org.elasticsearch.common.io.stream.Writeable;
 import org.elasticsearch.common.xcontent.XContentHelper;
+import org.elasticsearch.synonyms.SynonymsManagementAPIService;
 import org.elasticsearch.synonyms.SynonymsManagementAPIService.SynonymsReloadResult;
-import org.elasticsearch.test.AbstractWireSerializingTestCase;
+import org.elasticsearch.test.AbstractBWCSerializationTestCase;
+import org.elasticsearch.xcontent.ConstructingObjectParser;
+import org.elasticsearch.xcontent.ParseField;
+import org.elasticsearch.xcontent.XContentParser;
 
 import java.io.IOException;
 import java.util.Collections;
+import java.util.Locale;
 import java.util.Map;
 
+import static org.elasticsearch.action.synonyms.SynonymUpdateResponse.EMPTY_RELOAD_ANALYZER_RESPONSE;
 import static org.elasticsearch.synonyms.SynonymsManagementAPIService.UpdateSynonymsResultStatus.CREATED;
 import static org.elasticsearch.synonyms.SynonymsManagementAPIService.UpdateSynonymsResultStatus.DELETED;
 import static org.elasticsearch.synonyms.SynonymsManagementAPIService.UpdateSynonymsResultStatus.UPDATED;
+import static org.elasticsearch.xcontent.ConstructingObjectParser.constructorArg;
+import static org.elasticsearch.xcontent.ConstructingObjectParser.optionalConstructorArg;
 
-public class SynonymUpdateResponseSerializingTests extends AbstractWireSerializingTestCase<SynonymUpdateResponse> {
+public class SynonymUpdateResponseSerializingTests extends AbstractBWCSerializationTestCase<SynonymUpdateResponse> {
+
+    private static final ConstructingObjectParser<SynonymUpdateResponse, Void> PARSER = new ConstructingObjectParser<>(
+        "synonyms_update_response",
+        true,
+        arg -> {
+            SynonymsManagementAPIService.UpdateSynonymsResultStatus status = SynonymsManagementAPIService.UpdateSynonymsResultStatus
+                .valueOf(((String) arg[0]).toUpperCase(Locale.ROOT));
+            ReloadAnalyzersResponse reloadAnalyzersResponse = (ReloadAnalyzersResponse) arg[1];
+            return new SynonymUpdateResponse(new SynonymsReloadResult(status, reloadAnalyzersResponse));
+        }
+    );
+
+    static {
+        PARSER.declareString(constructorArg(), new ParseField(SynonymUpdateResponse.RESULT_FIELD));
+        PARSER.declareObjectOrNull(
+            optionalConstructorArg(),
+            (p, c) -> ReloadAnalyzersResponseTests.PARSER.parse(p, null),
+            null,
+            new ParseField(SynonymUpdateResponse.RELOAD_ANALYZERS_DETAILS_FIELD)
+        );
+    }
 
     @Override
     protected Writeable.Reader<SynonymUpdateResponse> instanceReader() {
@@ -34,9 +65,22 @@ public class SynonymUpdateResponseSerializingTests extends AbstractWireSerializi
 
     @Override
     protected SynonymUpdateResponse createTestInstance() {
-        Map<String, ReloadAnalyzersResponse.ReloadDetails> reloadedIndicesDetails = ReloadAnalyzersResponseTests
-            .createRandomReloadDetails();
-        ReloadAnalyzersResponse reloadAnalyzersResponse = new ReloadAnalyzersResponse(10, 10, 0, null, reloadedIndicesDetails);
+        return createTestInstance(randomBoolean());
+    }
+
+    private SynonymUpdateResponse createTestInstance(boolean includeReloadInfo) {
+        ReloadAnalyzersResponse reloadAnalyzersResponse = null;
+        if (includeReloadInfo) {
+            Map<String, ReloadAnalyzersResponse.ReloadDetails> reloadedIndicesDetails = ReloadAnalyzersResponseTests
+                .createRandomReloadDetails();
+            reloadAnalyzersResponse = new ReloadAnalyzersResponse(
+                randomIntBetween(0, 10),
+                randomIntBetween(0, 10),
+                randomIntBetween(0, 5),
+                null,
+                reloadedIndicesDetails
+            );
+        }
         return new SynonymUpdateResponse(new SynonymsReloadResult(randomFrom(CREATED, UPDATED, DELETED), reloadAnalyzersResponse));
     }
 
@@ -45,6 +89,17 @@ public class SynonymUpdateResponseSerializingTests extends AbstractWireSerializi
         return randomValueOtherThan(instance, this::createTestInstance);
     }
 
+    @Override
+    protected SynonymUpdateResponse mutateInstanceForVersion(SynonymUpdateResponse instance, TransportVersion version) {
+
+        if (version.before(TransportVersions.SYNONYMS_REFRESH_PARAM) && instance.reloadAnalyzersResponse() == null) {
+            // Nulls will be written as empty reload analyzer responses for older versions
+            return new SynonymUpdateResponse(new SynonymsReloadResult(instance.updateStatus(), EMPTY_RELOAD_ANALYZER_RESPONSE));
+        }
+
+        return instance;
+    }
+
     public void testToXContent() throws IOException {
         Map<String, ReloadAnalyzersResponse.ReloadDetails> reloadedIndicesNodes = Collections.singletonMap(
             "index",
@@ -73,4 +128,18 @@ public class SynonymUpdateResponseSerializingTests extends AbstractWireSerializi
               }
             }"""), output);
     }
+
+    public void testToXContentWithNoReloadResult() throws IOException {
+        SynonymUpdateResponse response = new SynonymUpdateResponse(new SynonymsReloadResult(CREATED, null));
+        String output = Strings.toString(response);
+        assertEquals(XContentHelper.stripWhitespace("""
+            {
+              "result": "created"
+            }"""), output);
+    }
+
+    @Override
+    protected SynonymUpdateResponse doParseInstance(XContentParser parser) throws IOException {
+        return PARSER.apply(parser, null);
+    }
 }

+ 1 - 1
x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/user/InternalUsers.java

@@ -282,7 +282,7 @@ public class InternalUsers {
         UsernamesField.SYNONYMS_USER_NAME,
         new RoleDescriptor(
             UsernamesField.SYNONYMS_ROLE_NAME,
-            null,
+            new String[] { "monitor" },
             new RoleDescriptor.IndicesPrivileges[] {
                 RoleDescriptor.IndicesPrivileges.builder().indices(".synonyms*").privileges("all").allowRestrictedIndices(true).build(),
                 RoleDescriptor.IndicesPrivileges.builder().indices("*").privileges(TransportReloadAnalyzersAction.TYPE.name()).build(), },