From 11111e2d3daa9d3b54c956cde316766b8d7e2fdd Mon Sep 17 00:00:00 2001 From: Jan Calanog Date: Mon, 30 Mar 2026 18:25:25 +0200 Subject: [PATCH 1/2] Search: Include synonym ID in synonym rule values The synonym dictionary uses the first term as the key and remaining terms as values. When publishing to Elasticsearch, only the values were included in the synonym string, omitting the key term itself. This meant e.g. "ilm" was the rule ID but not part of the synonym set, so it wouldn't match "index lifecycle management". Co-Authored-By: Claude Opus 4.6 (1M context) --- .../Exporters/Elasticsearch/ElasticsearchMarkdownExporter.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Elastic.Markdown/Exporters/Elasticsearch/ElasticsearchMarkdownExporter.cs b/src/Elastic.Markdown/Exporters/Elasticsearch/ElasticsearchMarkdownExporter.cs index 2899fefba..748c2a683 100644 --- a/src/Elastic.Markdown/Exporters/Elasticsearch/ElasticsearchMarkdownExporter.cs +++ b/src/Elastic.Markdown/Exporters/Elasticsearch/ElasticsearchMarkdownExporter.cs @@ -238,7 +238,8 @@ private async Task PublishSynonymsAsync(Cancel ctx) var synonymRules = _synonyms.Aggregate(new List(), (acc, synonym) => { var id = synonym.Key; - acc.Add(new SynonymRule { Id = id, Synonyms = string.Join(", ", synonym.Value) }); + var synonyms = string.Join(", ", [id, .. synonym.Value]); + acc.Add(new SynonymRule { Id = id, Synonyms = synonyms }); return acc; }); From 5cea709f22f7bb6feba2ad6a623fcd70dfb020f9 Mon Sep 17 00:00:00 2001 From: Jan Calanog Date: Mon, 30 Mar 2026 18:25:39 +0200 Subject: [PATCH 2/2] Search: Change synonyms from dictionary to flat list The dictionary structure used the first term as the key and Skip(1) for values, which artificially separated one synonym from its group. This caused the key term to be omitted from synonym rules sent to Elasticsearch. Replace Dictionary with IReadOnlyList so all terms in a synonym group are treated equally. The first term is still used as the ES rule ID for readability but is no longer excluded from the synonym string. Co-Authored-By: Claude Opus 4.6 (1M context) --- .superset/config.json | 7 ++++++ .../Search/SearchConfiguration.cs | 17 +++++++------- .../ElasticsearchMarkdownExporter.cs | 22 +++++++------------ .../TestHelpers.cs | 2 +- .../McpToolsIntegrationTestsBase.cs | 2 +- .../Elastic.ApiExplorer.Tests/TestHelpers.cs | 2 +- .../Changelogs/ChangelogTestBase.cs | 2 +- .../TestHelpers.cs | 2 +- tests/Elastic.Markdown.Tests/TestHelpers.cs | 2 +- 9 files changed, 30 insertions(+), 28 deletions(-) create mode 100644 .superset/config.json diff --git a/.superset/config.json b/.superset/config.json new file mode 100644 index 000000000..2f285f8a8 --- /dev/null +++ b/.superset/config.json @@ -0,0 +1,7 @@ +{ + "setup": [ + "dotnet tool restore && dotnet husky install" + ], + "teardown": [], + "run": [] +} \ No newline at end of file diff --git a/src/Elastic.Documentation.Configuration/Search/SearchConfiguration.cs b/src/Elastic.Documentation.Configuration/Search/SearchConfiguration.cs index ae52726af..b75501bd7 100644 --- a/src/Elastic.Documentation.Configuration/Search/SearchConfiguration.cs +++ b/src/Elastic.Documentation.Configuration/Search/SearchConfiguration.cs @@ -9,9 +9,9 @@ namespace Elastic.Documentation.Configuration.Search; public record SearchConfiguration { - private readonly IReadOnlyDictionary _synonyms; + private readonly IReadOnlyList _synonyms; - public required IReadOnlyDictionary Synonyms + public required IReadOnlyList Synonyms { get => _synonyms; [MemberNotNull(nameof(_synonyms))] @@ -19,7 +19,6 @@ public required IReadOnlyDictionary Synonyms { _synonyms = value; SynonymBiDirectional = value - .Select(kv => kv.Value.Concat([kv.Key]).ToArray()) .SelectMany(a => { var targets = new List(); @@ -120,15 +119,17 @@ public static class SearchConfigurationExtensions public static SearchConfiguration CreateSearchConfiguration(this ConfigurationFileProvider provider) { var searchFile = provider.SearchFile; - var synonyms = new Dictionary(); if (!searchFile.Exists) - return new SearchConfiguration { Synonyms = synonyms, Rules = [], DiminishTerms = [] }; + return new SearchConfiguration { Synonyms = [], Rules = [], DiminishTerms = [] }; var searchDto = ConfigurationFileProvider.Deserializer.Deserialize(searchFile.OpenText()); - synonyms = searchDto.Synonyms + var synonyms = searchDto.Synonyms .Where(s => s.Count > 1) - .ToDictionary(k => k[0], sl => sl.Skip(1).ToArray(), StringComparer.OrdinalIgnoreCase); + .Select(s => s.ToArray()) + .GroupBy(s => s[0], StringComparer.OrdinalIgnoreCase) + .Select(g => g.First()) + .ToArray(); var rules = searchDto.Rules.Select(ParseRule).ToImmutableArray(); var diminishTerms = searchDto.DiminishTerms.ToImmutableArray(); return new SearchConfiguration { Synonyms = synonyms, Rules = rules, DiminishTerms = diminishTerms }; @@ -154,4 +155,4 @@ private static QueryRuleCriteria ParseCriteria(QueryRuleCriteriaDto dto) => Metadata = dto.Metadata, Values = dto.Values.ToImmutableArray() }; -} +} \ No newline at end of file diff --git a/src/Elastic.Markdown/Exporters/Elasticsearch/ElasticsearchMarkdownExporter.cs b/src/Elastic.Markdown/Exporters/Elasticsearch/ElasticsearchMarkdownExporter.cs index 748c2a683..b62d7be12 100644 --- a/src/Elastic.Markdown/Exporters/Elasticsearch/ElasticsearchMarkdownExporter.cs +++ b/src/Elastic.Markdown/Exporters/Elasticsearch/ElasticsearchMarkdownExporter.cs @@ -38,7 +38,7 @@ public partial class ElasticsearchMarkdownExporter : IMarkdownExporter, IDisposa private readonly ElasticsearchTypeContext _semanticTypeContext; private readonly VersionsConfiguration _versionsConfiguration; - private readonly IReadOnlyDictionary _synonyms; + private readonly IReadOnlyList _synonyms; private readonly IReadOnlyCollection _rules; private readonly string _fixedSynonymsHash; @@ -74,12 +74,10 @@ IDocumentationConfigurationContext context _operations = new ElasticsearchOperations(_transport, _logger, collector); string[] fixedSynonyms = ["esql", "data-stream", "data-streams", "machine-learning"]; - var indexTimeSynonyms = _synonyms.Aggregate(new List(), (acc, synonym) => - { - var id = synonym.Key; - acc.Add(new SynonymRule { Id = id, Synonyms = string.Join(", ", synonym.Value) }); - return acc; - }).Where(r => fixedSynonyms.Contains(r.Id)).Select(r => r.Synonyms).ToArray(); + var indexTimeSynonyms = _synonyms + .Where(s => s.Any(t => fixedSynonyms.Contains(t))) + .Select(s => string.Join(", ", s)) + .ToArray(); _fixedSynonymsHash = HashedBulkUpdate.CreateHash(string.Join(",", indexTimeSynonyms)); var synonymSetName = $"docs-{_buildType}-{_environment}"; @@ -235,13 +233,9 @@ private async Task PublishSynonymsAsync(Cancel ctx) var setName = $"docs-{_buildType}-{_environment}"; _logger.LogInformation("Publishing synonym set '{SetName}' to Elasticsearch", setName); - var synonymRules = _synonyms.Aggregate(new List(), (acc, synonym) => - { - var id = synonym.Key; - var synonyms = string.Join(", ", [id, .. synonym.Value]); - acc.Add(new SynonymRule { Id = id, Synonyms = synonyms }); - return acc; - }); + var synonymRules = _synonyms + .Select(s => new SynonymRule { Id = s[0], Synonyms = string.Join(", ", s) }) + .ToList(); var synonymsSet = new SynonymsSet { Synonyms = synonymRules }; await PutSynonyms(synonymsSet, setName, ctx); diff --git a/tests-integration/Elastic.Assembler.IntegrationTests/TestHelpers.cs b/tests-integration/Elastic.Assembler.IntegrationTests/TestHelpers.cs index 7de28f21b..473dfae09 100644 --- a/tests-integration/Elastic.Assembler.IntegrationTests/TestHelpers.cs +++ b/tests-integration/Elastic.Assembler.IntegrationTests/TestHelpers.cs @@ -57,7 +57,7 @@ public static IConfigurationContext CreateConfigurationContext( ProductDisplayNames = products.ToDictionary(p => p.Key, p => p.Value.DisplayName).ToFrozenDictionary() }; } - var search = new SearchConfiguration { Synonyms = new Dictionary(), Rules = [], DiminishTerms = [] }; + var search = new SearchConfiguration { Synonyms = [], Rules = [], DiminishTerms = [] }; return new ConfigurationContext { Endpoints = new DocumentationEndpoints diff --git a/tests-integration/Mcp.Remote.IntegrationTests/McpToolsIntegrationTestsBase.cs b/tests-integration/Mcp.Remote.IntegrationTests/McpToolsIntegrationTestsBase.cs index a494d6dff..d2a3b48ea 100644 --- a/tests-integration/Mcp.Remote.IntegrationTests/McpToolsIntegrationTestsBase.cs +++ b/tests-integration/Mcp.Remote.IntegrationTests/McpToolsIntegrationTestsBase.cs @@ -92,7 +92,7 @@ private static ElasticsearchClientAccessor CreateElasticsearchClientAccessor() var searchConfig = new SearchConfiguration { - Synonyms = new Dictionary(), + Synonyms = [], Rules = [], DiminishTerms = ["plugin", "client", "integration", "glossary"] }; diff --git a/tests/Elastic.ApiExplorer.Tests/TestHelpers.cs b/tests/Elastic.ApiExplorer.Tests/TestHelpers.cs index 25cbfb10c..eb7478d0f 100644 --- a/tests/Elastic.ApiExplorer.Tests/TestHelpers.cs +++ b/tests/Elastic.ApiExplorer.Tests/TestHelpers.cs @@ -53,7 +53,7 @@ public static IConfigurationContext CreateConfigurationContext(IFileSystem fileS ProductDisplayNames = products.ToDictionary(p => p.Key, p => p.Value.DisplayName).ToFrozenDictionary() }; } - var search = new SearchConfiguration { Synonyms = new Dictionary(), Rules = [], DiminishTerms = [] }; + var search = new SearchConfiguration { Synonyms = [], Rules = [], DiminishTerms = [] }; return new ConfigurationContext { Endpoints = new DocumentationEndpoints diff --git a/tests/Elastic.Changelog.Tests/Changelogs/ChangelogTestBase.cs b/tests/Elastic.Changelog.Tests/Changelogs/ChangelogTestBase.cs index 1bc0e1eb7..c6453ce1f 100644 --- a/tests/Elastic.Changelog.Tests/Changelogs/ChangelogTestBase.cs +++ b/tests/Elastic.Changelog.Tests/Changelogs/ChangelogTestBase.cs @@ -103,7 +103,7 @@ protected ChangelogTestBase(ITestOutputHelper output) ConfigurationFileProvider = new ConfigurationFileProvider(NullLoggerFactory.Instance, FileSystem), VersionsConfiguration = versionsConfiguration, ProductsConfiguration = productsConfiguration, - SearchConfiguration = new SearchConfiguration { Synonyms = new Dictionary(), Rules = [], DiminishTerms = [] }, + SearchConfiguration = new SearchConfiguration { Synonyms = [], Rules = [], DiminishTerms = [] }, LegacyUrlMappings = new LegacyUrlMappingConfiguration { Mappings = [] }, }; } diff --git a/tests/Elastic.Documentation.Build.Tests/TestHelpers.cs b/tests/Elastic.Documentation.Build.Tests/TestHelpers.cs index c36142c84..0a1bacfc2 100644 --- a/tests/Elastic.Documentation.Build.Tests/TestHelpers.cs +++ b/tests/Elastic.Documentation.Build.Tests/TestHelpers.cs @@ -62,7 +62,7 @@ public static IConfigurationContext CreateConfigurationContext( }; } - var search = new SearchConfiguration { Synonyms = new Dictionary(), Rules = [], DiminishTerms = [] }; + var search = new SearchConfiguration { Synonyms = [], Rules = [], DiminishTerms = [] }; return new ConfigurationContext { Endpoints = new DocumentationEndpoints diff --git a/tests/Elastic.Markdown.Tests/TestHelpers.cs b/tests/Elastic.Markdown.Tests/TestHelpers.cs index 92919b221..99adaad4b 100644 --- a/tests/Elastic.Markdown.Tests/TestHelpers.cs +++ b/tests/Elastic.Markdown.Tests/TestHelpers.cs @@ -106,7 +106,7 @@ public static IConfigurationContext CreateConfigurationContext(IFileSystem fileS ProductDisplayNames = products.ToDictionary(p => p.Key, p => p.Value.DisplayName).ToFrozenDictionary() }; } - var search = new SearchConfiguration { Synonyms = new Dictionary(), Rules = [], DiminishTerms = [] }; + var search = new SearchConfiguration { Synonyms = [], Rules = [], DiminishTerms = [] }; return new ConfigurationContext { Endpoints = new DocumentationEndpoints