diff --git a/tests/Elastic.Documentation.Api.Infrastructure.Tests/.snapshots/Adapters/AskAi/StreamTransformerCommonBehaviorTests/NewConversationEmitsConversationStart.AgentBuilderStreamTransformer.084fb69f.verified.txt b/tests/Elastic.Documentation.Api.Infrastructure.Tests/.snapshots/Adapters/AskAi/StreamTransformerCommonBehaviorTests/NewConversationEmitsConversationStart.AgentBuilderStreamTransformer.084fb69f.verified.txt new file mode 100644 index 0000000000..30a469ae59 --- /dev/null +++ b/tests/Elastic.Documentation.Api.Infrastructure.Tests/.snapshots/Adapters/AskAi/StreamTransformerCommonBehaviorTests/NewConversationEmitsConversationStart.AgentBuilderStreamTransformer.084fb69f.verified.txt @@ -0,0 +1,3 @@ +data: {"type":"conversation_start","conversationId":"Guid_1","id":"Guid_2","timestamp":{now}} + +data: {"type":"message_chunk","content":"test","id":"Guid_3","timestamp":{now}} diff --git a/tests/Elastic.Documentation.Api.Infrastructure.Tests/.snapshots/Adapters/AskAi/StreamTransformerCommonBehaviorTests/NewConversationEmitsConversationStart.LlmGatewayStreamTransformer.5331b6a1.verified.txt b/tests/Elastic.Documentation.Api.Infrastructure.Tests/.snapshots/Adapters/AskAi/StreamTransformerCommonBehaviorTests/NewConversationEmitsConversationStart.LlmGatewayStreamTransformer.5331b6a1.verified.txt new file mode 100644 index 0000000000..391bc0933b --- /dev/null +++ b/tests/Elastic.Documentation.Api.Infrastructure.Tests/.snapshots/Adapters/AskAi/StreamTransformerCommonBehaviorTests/NewConversationEmitsConversationStart.LlmGatewayStreamTransformer.5331b6a1.verified.txt @@ -0,0 +1,3 @@ +data: {"type":"conversation_start","conversationId":"Guid_1","id":"Guid_2","timestamp":{now}} + +data: {"type":"message_chunk","content":"test","id":"1","timestamp":1234567890} diff --git a/tests/Elastic.Documentation.Api.Infrastructure.Tests/Adapters/AskAi/StreamTransformerTests.cs b/tests/Elastic.Documentation.Api.Infrastructure.Tests/Adapters/AskAi/StreamTransformerTests.cs index 743b267d9f..da96d7a1a4 100644 --- a/tests/Elastic.Documentation.Api.Infrastructure.Tests/Adapters/AskAi/StreamTransformerTests.cs +++ b/tests/Elastic.Documentation.Api.Infrastructure.Tests/Adapters/AskAi/StreamTransformerTests.cs @@ -5,10 +5,12 @@ using System.IO.Pipelines; using System.Text; using System.Text.Json; +using System.Text.RegularExpressions; using AwesomeAssertions; using Elastic.Documentation.Api.Adapters.AskAi; using Elastic.Documentation.Api.AskAi; using Microsoft.Extensions.Logging.Abstractions; +using Nullean.Snapshots; namespace Elastic.Documentation.Api.Infrastructure.Tests.Adapters.AskAi; @@ -33,6 +35,17 @@ public static async Task> ParseAskAiEventsAsync(Stream stream) } return events; } + + /// The transformer output exactly as the client receives it: SSE text. + public static async Task ReadSseAsync(Stream stream) + { + using var reader = new StreamReader(stream, Encoding.UTF8); + return await reader.ReadToEndAsync(); + } + + /// Replaces what changes per run: generated conversation ids and millisecond timestamps taken from the clock. + public static SnapshotOptions Scrub() => + new SnapshotOptions().Scrub(Scrubbers.Guids).Scrub(new Regex(@"""timestamp"":\d{13}"), "\"timestamp\":{now}"); } public class AgentBuilderStreamTransformerTests @@ -80,45 +93,30 @@ public async Task TransformAsyncWithRealAgentBuilderPayloadParsesAllEventTypes() // Act - No generated ID (Agent Builder handles IDs in stream) var outputStream = await _transformer.TransformAsync(inputStream, generatedConversationId: null, null, CancellationToken.None); - var events = await StreamTransformerTestHelpers.ParseAskAiEventsAsync(outputStream); - - // Assert - // Note: Due to async streaming, the final event might not be written before the input stream closes - // In production, real SSE streams stay open, so this isn't an issue - events.Should().HaveCountGreaterThanOrEqualTo(7); - - // Verify we got the key events (ConversationStart comes from stream for Agent Builder) - events.Should().ContainSingle(e => e is AskAiEvent.ConversationStart); - events.Should().ContainSingle(e => e is AskAiEvent.Reasoning); - events.Should().ContainSingle(e => e is AskAiEvent.SearchToolCall); - events.Should().ContainSingle(e => e is AskAiEvent.ToolResult); - events.Should().Contain(e => e is AskAiEvent.MessageChunk); - events.Should().ContainSingle(e => e is AskAiEvent.MessageComplete); - - // Verify specific content - var convStart = events.OfType().First(); - convStart.ConversationId.Should().Be("360222c5-76aa-405a-8316-703e1061b621"); - - var reasoning = events.OfType().First(); - reasoning.Message.Should().Contain("Searching"); - - // Tool call should be SearchToolCall type with extracted query - var searchToolCall = events.OfType().FirstOrDefault(); - searchToolCall.Should().NotBeNull(); - searchToolCall.ToolCallId.Should().Be("tooluse_abc123"); - searchToolCall.SearchQuery.Should().Be("semantic search"); - - var toolResult = events.OfType().First(); - toolResult.ToolCallId.Should().Be("tooluse_abc123"); - toolResult.Result.Should().Contain("semantic-docs-prod-latest"); - - var chunks = events.OfType().ToList(); - chunks.Should().HaveCount(2); - chunks[0].Content.Should().Be("Hello"); - chunks[1].Content.Should().Be(" world"); - - var complete = events.OfType().First(); - complete.FullContent.Should().Be("Hello world"); + var output = await StreamTransformerTestHelpers.ReadSseAsync(outputStream); + + // Assert - every event type of the Agent Builder stream, in order, with its content + // (ConversationStart comes from the stream for Agent Builder) + output.Should().MatchInlineSnapshot( + options: StreamTransformerTestHelpers.Scrub(), + expected: """ + data: {"type":"conversation_start","conversationId":"Guid_1","id":"Guid_2","timestamp":{now}} + + data: {"type":"reasoning","message":"Searching for relevant documents...","id":"Guid_3","timestamp":{now}} + + data: {"type":"search_tool_call","toolCallId":"tooluse_abc123","searchQuery":"semantic search","id":"Guid_4","timestamp":{now}} + + data: {"type":"tool_result","toolCallId":"tooluse_abc123","result":"[{\u0022type\u0022:\u0022query\u0022,\u0022data\u0022:{\u0022esql\u0022:\u0022FROM semantic-docs-prod-latest | WHERE MATCH(title.semantic_text, \\\u0022semantic search\\\u0022)\u0022},\u0022tool_result_id\u0022:\u0022result1\u0022}]","id":"Guid_5","timestamp":{now}} + + data: {"type":"message_chunk","content":"Hello","id":"Guid_6","timestamp":{now}} + + data: {"type":"message_chunk","content":" world","id":"Guid_7","timestamp":{now}} + + data: {"type":"message_complete","fullContent":"Hello world","id":"Guid_8","timestamp":{now}} + + data: {"type":"conversation_end","id":"Guid_9","timestamp":{now}} + """ + ); } [Test] @@ -143,11 +141,17 @@ public async Task TransformAsyncWithKeepAliveCommentsSkipsThem() // Act var outputStream = await _transformer.TransformAsync(inputStream, generatedConversationId: null, null, CancellationToken.None); - var events = await StreamTransformerTestHelpers.ParseAskAiEventsAsync(outputStream); + var output = await StreamTransformerTestHelpers.ReadSseAsync(outputStream); + + // Assert - the keepalive comments are dropped + output.Should().MatchInlineSnapshot( + options: StreamTransformerTestHelpers.Scrub(), + expected: """ + data: {"type":"message_chunk","content":"test","id":"Guid_1","timestamp":{now}} - // Assert - Should have at least 1 event (round_complete might not be written in time) - events.Should().HaveCountGreaterThanOrEqualTo(1); - events[0].Should().BeOfType(); + data: {"type":"conversation_end","id":"Guid_2","timestamp":{now}} + """ + ); } [Test] @@ -167,12 +171,14 @@ public async Task TransformAsyncWithMultilineDataFieldsAccumulatesCorrectly() // Act var outputStream = await _transformer.TransformAsync(inputStream, generatedConversationId: null, null, CancellationToken.None); - var events = await StreamTransformerTestHelpers.ParseAskAiEventsAsync(outputStream); + var output = await StreamTransformerTestHelpers.ReadSseAsync(outputStream); - // Assert - This test has malformed SSE data (missing proper blank line terminator) - // In a real scenario with proper SSE formatting, this would work - // For now, skip this test or mark as known limitation - events.Should().HaveCountGreaterThanOrEqualTo(0); + // This test has malformed SSE data (missing proper blank line terminator). The old assertion + // (HaveCountGreaterThanOrEqualTo(0)) could not fail; the snapshot pins what is actually emitted. + output.Should().MatchInlineSnapshot( + options: StreamTransformerTestHelpers.Scrub(), + expected: "data: {\"type\":\"message_chunk\",\"content\":\"multiline\",\"id\":\"Guid_1\",\"timestamp\":{now}}" + ); } } @@ -217,46 +223,28 @@ public async Task TransformAsyncWithRealLlmGatewayPayloadParsesAllEventTypes() // Act - Simulate new conversation to get ConversationStart event var testConversationId = Guid.NewGuid().ToString(); var outputStream = await _transformer.TransformAsync(inputStream, Guid.Parse(testConversationId), null, CancellationToken.None); - var events = await StreamTransformerTestHelpers.ParseAskAiEventsAsync(outputStream); - - // Assert - events.Should().HaveCount(7); - - // Event 1: ConversationStart (emitted by transformer for new conversation) - events[0].Should().BeOfType(); - var convStart = events[0] as AskAiEvent.ConversationStart; - convStart!.ConversationId.Should().Be(testConversationId); - - // Event 2: ai_message_chunk (first) - events[1].Should().BeOfType(); - var chunk1 = events[1] as AskAiEvent.MessageChunk; - chunk1!.Content.Should().Be("Hello"); - - // Event 3: ai_message_chunk (second) - events[2].Should().BeOfType(); - var chunk2 = events[2] as AskAiEvent.MessageChunk; - chunk2!.Content.Should().Be(" world"); - - // Event 4: tool_call -> Should be SearchToolCall with extracted query - events[3].Should().BeOfType(); - var searchToolCall = events[3] as AskAiEvent.SearchToolCall; - searchToolCall!.ToolCallId.Should().Be("tool1"); - searchToolCall.SearchQuery.Should().Be("Index Lifecycle Management (ILM) Elasticsearch documentation"); - - // Event 5: tool_message - events[4].Should().BeOfType(); - var toolResult = events[4] as AskAiEvent.ToolResult; - toolResult!.ToolCallId.Should().Be("tool1"); - // Result is intentionally empty - payload stripped to prevent CloudFront OAC buffering issues - toolResult.Result.Should().BeEmpty(); - - // Event 6: ai_message - events[5].Should().BeOfType(); - var complete = events[5] as AskAiEvent.MessageComplete; - complete!.FullContent.Should().Be("Hello world"); - - // Event 7: agent_end - events[6].Should().BeOfType(); + var output = await StreamTransformerTestHelpers.ReadSseAsync(outputStream); + + // Assert - ConversationStart (generated by the transformer), both chunks, the search tool call with its extracted query, + // the tool result with the payload stripped on purpose (CloudFront OAC buffering), the complete message and the end + output.Should().MatchInlineSnapshot( + options: StreamTransformerTestHelpers.Scrub(), + expected: """ + data: {"type":"conversation_start","conversationId":"Guid_1","id":"Guid_2","timestamp":{now}} + + data: {"type":"message_chunk","content":"Hello","id":"2","timestamp":1234567891} + + data: {"type":"message_chunk","content":" world","id":"3","timestamp":1234567892} + + data: {"type":"search_tool_call","toolCallId":"tool1","searchQuery":"Index Lifecycle Management (ILM) Elasticsearch documentation","id":"4","timestamp":1234567893} + + data: {"type":"tool_result","toolCallId":"tool1","result":"","id":"5","timestamp":1234567894} + + data: {"type":"message_complete","fullContent":"Hello world","id":"6","timestamp":1234567895} + + data: {"type":"conversation_end","id":"7","timestamp":1234567896} + """ + ); } [Test] @@ -284,12 +272,17 @@ public async Task TransformAsyncWithEmptyDataLinesSkipsThem() // Act - Simulate new conversation var testConversationId = Guid.NewGuid().ToString(); var outputStream = await _transformer.TransformAsync(inputStream, Guid.Parse(testConversationId), null, CancellationToken.None); - var events = await StreamTransformerTestHelpers.ParseAskAiEventsAsync(outputStream); + var output = await StreamTransformerTestHelpers.ReadSseAsync(outputStream); + + // Assert - the empty data lines are skipped: only ConversationStart and ConversationEnd remain + output.Should().MatchInlineSnapshot( + options: StreamTransformerTestHelpers.Scrub(), + expected: """ + data: {"type":"conversation_start","conversationId":"Guid_1","id":"Guid_2","timestamp":{now}} - // Assert - Should only have 2 events (ConversationStart + ConversationEnd) - events.Should().HaveCount(2); - events[0].Should().BeOfType(); - events[1].Should().BeOfType(); + data: {"type":"conversation_end","id":"2","timestamp":1234567891} + """ + ); } [Test] @@ -311,12 +304,17 @@ public async Task TransformAsyncSkipsModelLifecycleEvents() // Act - Simulate new conversation var testConversationId = Guid.NewGuid().ToString(); var outputStream = await _transformer.TransformAsync(inputStream, Guid.Parse(testConversationId), null, CancellationToken.None); - var events = await StreamTransformerTestHelpers.ParseAskAiEventsAsync(outputStream); + var output = await StreamTransformerTestHelpers.ReadSseAsync(outputStream); - // Assert - Should only have the ConversationStart and message chunk, model events skipped - events.Should().HaveCount(2); - events[0].Should().BeOfType(); - events[1].Should().BeOfType(); + // Assert - the model lifecycle events are skipped: only ConversationStart and the message chunk remain + output.Should().MatchInlineSnapshot( + options: StreamTransformerTestHelpers.Scrub(), + expected: """ + data: {"type":"conversation_start","conversationId":"Guid_1","id":"Guid_2","timestamp":{now}} + + data: {"type":"message_chunk","content":"test","id":"2","timestamp":1234567891} + """ + ); } } @@ -327,6 +325,8 @@ public async Task TransformAsyncSkipsModelLifecycleEvents() /// public class StreamTransformerCommonBehaviorTests { + // The SSE text is an argument of the test, and the snapshot file name carries a hash of the arguments. A checkout on Windows turns the + // line endings of this file into "\r\n", so the text is normalised, or the hash and the file name would differ per platform. public static IEnumerable> StreamTransformerTestCases() { yield return () => @@ -339,7 +339,9 @@ public class StreamTransformerCommonBehaviorTests event: message_chunk data: {"data":{"text_chunk":"test"}} - """); + """.ReplaceLineEndings( + "\n" + )); yield return () => ("LlmGatewayStreamTransformer", new LlmGatewayStreamTransformer(NullLogger.Instance), // LLM Gateway SSE format - minimal events @@ -347,91 +349,25 @@ public class StreamTransformerCommonBehaviorTests event: agent_stream_output data: [null, {"type":"ai_message_chunk","id":"1","timestamp":1234567890,"data":{"content":"test"}}] - """); - } - - [Test] - [MethodDataSource(nameof(StreamTransformerTestCases))] - public async Task TransformAsyncWhenIsNewConversationEmitsConversationStartEvent( - string transformerName, - IStreamTransformer transformer, - string sseData - ) - { - // Arrange - var inputStream = new MemoryStream(Encoding.UTF8.GetBytes(sseData)); - var testConversationId = Guid.NewGuid().ToString(); - - // Act - Pass isNewConversation: true to simulate new conversation - var outputStream = await transformer.TransformAsync(inputStream, Guid.Parse(testConversationId), null, CancellationToken.None); - var events = await StreamTransformerTestHelpers.ParseAskAiEventsAsync(outputStream); - - // Assert - Should have ConversationStart event - events.Should().ContainSingle( - e => e is AskAiEvent.ConversationStart, - $"{transformerName} should emit ConversationStart when isNewConversation is true" - ); - - var conversationStart = events.OfType().First(); - conversationStart - .ConversationId - .Should() - .NotBeNullOrEmpty($"{transformerName} should have a non-empty conversation ID in ConversationStart event"); + """.ReplaceLineEndings( + "\n" + )); } + /// + /// A new conversation starts with a ConversationStart event that has a conversation id, an event id and a clock + /// timestamp (scrubbed to {now}, which also fails on a zero timestamp), whatever the transformer. + /// [Test] [MethodDataSource(nameof(StreamTransformerTestCases))] - public async Task TransformAsyncConversationStartEventHasValidTimestamp( - string transformerName, - IStreamTransformer transformer, - string sseData - ) - { - // Arrange - var inputStream = new MemoryStream(Encoding.UTF8.GetBytes(sseData)); - var testConversationId = Guid.NewGuid().ToString(); - - // Act - var outputStream = await transformer.TransformAsync(inputStream, Guid.Parse(testConversationId), null, CancellationToken.None); - var events = new List(); - var reader = PipeReader.Create(outputStream); - await foreach (var sseEvent in SseParser.ParseAsync(reader, CancellationToken.None)) - { - var evt = JsonSerializer.Deserialize(sseEvent.Data, AskAiEventJsonContext.Default.AskAiEvent); - if (evt != null) - events.Add(evt); - } - - // Assert - var conversationStart = events.OfType().FirstOrDefault(); - conversationStart.Should().NotBeNull($"{transformerName} should emit ConversationStart event"); - - conversationStart.Timestamp.Should().BeGreaterThan(0, $"{transformerName} ConversationStart should have a valid timestamp"); - } - - [Test] - [MethodDataSource(nameof(StreamTransformerTestCases))] - public async Task TransformAsyncConversationStartEventHasValidId(string transformerName, IStreamTransformer transformer, string sseData) + public async Task NewConversationEmitsConversationStart(string transformerName, IStreamTransformer transformer, string sseData) { - // Arrange var inputStream = new MemoryStream(Encoding.UTF8.GetBytes(sseData)); var testConversationId = Guid.NewGuid().ToString(); - // Act var outputStream = await transformer.TransformAsync(inputStream, Guid.Parse(testConversationId), null, CancellationToken.None); - var events = new List(); - var reader = PipeReader.Create(outputStream); - await foreach (var sseEvent in SseParser.ParseAsync(reader, CancellationToken.None)) - { - var evt = JsonSerializer.Deserialize(sseEvent.Data, AskAiEventJsonContext.Default.AskAiEvent); - if (evt != null) - events.Add(evt); - } - - // Assert - var conversationStart = events.OfType().FirstOrDefault(); - conversationStart.Should().NotBeNull($"{transformerName} should emit ConversationStart event"); + var output = await StreamTransformerTestHelpers.ReadSseAsync(outputStream); - conversationStart.Id.Should().NotBeNullOrEmpty($"{transformerName} ConversationStart should have a non-empty event ID"); + output.Should().MatchSnapshot(name: transformerName, options: StreamTransformerTestHelpers.Scrub()); } } diff --git a/tests/Elastic.Documentation.Api.Tests/.snapshots/ApiPropertySearchTests/ApplyApiPropertyQuery_DocsWideMarkedEndpointAndField_RequiresFieldMatchBeforeEndpointBoost.c624ce26.verified.json b/tests/Elastic.Documentation.Api.Tests/.snapshots/ApiPropertySearchTests/ApplyApiPropertyQuery_DocsWideMarkedEndpointAndField_RequiresFieldMatchBeforeEndpointBoost.c624ce26.verified.json new file mode 100644 index 0000000000..9f677dc756 --- /dev/null +++ b/tests/Elastic.Documentation.Api.Tests/.snapshots/ApiPropertySearchTests/ApplyApiPropertyQuery_DocsWideMarkedEndpointAndField_RequiresFieldMatchBeforeEndpointBoost.c624ce26.verified.json @@ -0,0 +1,66 @@ +{ + "bool": { + "minimum_should_match": 1, + "should": [ + { + "match_none": {} + }, + { + "bool": { + "must": { + "nested": { + "ignore_unmapped": true, + "inner_hits": { + "name": "api_properties", + "size": 5, + "_source": { + "includes": "api.properties.*" + } + }, + "path": "api.properties", + "query": { + "dis_max": { + "queries": [ + { + "constant_score": { + "boost": 1, + "filter": { + "term": { + "api.properties.name": { + "case_insensitive": true, + "value": "track_total" + } + } + } + } + } + ] + } + }, + "score_mode": "max" + } + }, + "_name": "api_properties", + "should": { + "dis_max": { + "queries": [ + { + "constant_score": { + "boost": 5, + "filter": { + "terms": { + "api.endpoints": [ + "_search" + ] + } + } + } + } + ] + } + } + } + } + ] + } +} diff --git a/tests/Elastic.Documentation.Api.Tests/.snapshots/ApiPropertySearchTests/ApplyApiPropertyQuery_DocsWideMarkedEndpointAndField_RequiresFieldMatchBeforeEndpointBoost.d0302c76.verified.json b/tests/Elastic.Documentation.Api.Tests/.snapshots/ApiPropertySearchTests/ApplyApiPropertyQuery_DocsWideMarkedEndpointAndField_RequiresFieldMatchBeforeEndpointBoost.d0302c76.verified.json new file mode 100644 index 0000000000..9f677dc756 --- /dev/null +++ b/tests/Elastic.Documentation.Api.Tests/.snapshots/ApiPropertySearchTests/ApplyApiPropertyQuery_DocsWideMarkedEndpointAndField_RequiresFieldMatchBeforeEndpointBoost.d0302c76.verified.json @@ -0,0 +1,66 @@ +{ + "bool": { + "minimum_should_match": 1, + "should": [ + { + "match_none": {} + }, + { + "bool": { + "must": { + "nested": { + "ignore_unmapped": true, + "inner_hits": { + "name": "api_properties", + "size": 5, + "_source": { + "includes": "api.properties.*" + } + }, + "path": "api.properties", + "query": { + "dis_max": { + "queries": [ + { + "constant_score": { + "boost": 1, + "filter": { + "term": { + "api.properties.name": { + "case_insensitive": true, + "value": "track_total" + } + } + } + } + } + ] + } + }, + "score_mode": "max" + } + }, + "_name": "api_properties", + "should": { + "dis_max": { + "queries": [ + { + "constant_score": { + "boost": 5, + "filter": { + "terms": { + "api.endpoints": [ + "_search" + ] + } + } + } + } + ] + } + } + } + } + ] + } +} diff --git a/tests/Elastic.Documentation.Api.Tests/.snapshots/ApiPropertySearchTests/ApplyApiPropertyQuery_DocsWideTwoIdentifierWords_TriesBothReadingsWithUniqueInnerHitNames.verified.json b/tests/Elastic.Documentation.Api.Tests/.snapshots/ApiPropertySearchTests/ApplyApiPropertyQuery_DocsWideTwoIdentifierWords_TriesBothReadingsWithUniqueInnerHitNames.verified.json new file mode 100644 index 0000000000..b16777a44e --- /dev/null +++ b/tests/Elastic.Documentation.Api.Tests/.snapshots/ApiPropertySearchTests/ApplyApiPropertyQuery_DocsWideTwoIdentifierWords_TriesBothReadingsWithUniqueInnerHitNames.verified.json @@ -0,0 +1,181 @@ +{ + "bool": { + "minimum_should_match": 1, + "should": [ + { + "match_none": {} + }, + { + "bool": { + "minimum_should_match": 1, + "should": [ + { + "bool": { + "must": [ + { + "nested": { + "ignore_unmapped": true, + "inner_hits": { + "name": "api_properties", + "size": 5, + "_source": { + "includes": "api.properties.*" + } + }, + "path": "api.properties", + "query": { + "dis_max": { + "queries": [ + { + "constant_score": { + "boost": 1, + "filter": { + "term": { + "api.properties.name": { + "case_insensitive": true, + "value": "track_total" + } + } + } + } + } + ] + } + }, + "score_mode": "max" + } + }, + { + "dis_max": { + "queries": [ + { + "dis_max": { + "queries": [ + { + "constant_score": { + "boost": 5, + "filter": { + "terms": { + "api.endpoints": [ + "knn_field" + ] + } + } + } + } + ] + } + }, + { + "dis_max": { + "queries": [ + { + "constant_score": { + "boost": 5, + "filter": { + "terms": { + "api.endpoints": [ + "_knn_field" + ] + } + } + } + } + ] + } + } + ] + } + } + ], + "_name": "api_properties" + } + }, + { + "bool": { + "must": [ + { + "nested": { + "ignore_unmapped": true, + "inner_hits": { + "name": "api_properties_1", + "size": 5, + "_source": { + "includes": "api.properties.*" + } + }, + "path": "api.properties", + "query": { + "dis_max": { + "queries": [ + { + "constant_score": { + "boost": 1, + "filter": { + "term": { + "api.properties.name": { + "case_insensitive": true, + "value": "knn_field" + } + } + } + } + } + ] + } + }, + "score_mode": "max" + } + }, + { + "dis_max": { + "queries": [ + { + "dis_max": { + "queries": [ + { + "constant_score": { + "boost": 5, + "filter": { + "terms": { + "api.endpoints": [ + "track_total" + ] + } + } + } + } + ] + } + }, + { + "dis_max": { + "queries": [ + { + "constant_score": { + "boost": 5, + "filter": { + "terms": { + "api.endpoints": [ + "_track_total" + ] + } + } + } + } + ] + } + } + ] + } + } + ], + "_name": "api_properties_1" + } + } + ] + } + } + ] + } +} diff --git a/tests/Elastic.Documentation.Api.Tests/.snapshots/ApiPropertySearchTests/ApplyApiPropertyQuery_DocsWide_HasNoChildrenLift.verified.json b/tests/Elastic.Documentation.Api.Tests/.snapshots/ApiPropertySearchTests/ApplyApiPropertyQuery_DocsWide_HasNoChildrenLift.verified.json new file mode 100644 index 0000000000..db79f54ebf --- /dev/null +++ b/tests/Elastic.Documentation.Api.Tests/.snapshots/ApiPropertySearchTests/ApplyApiPropertyQuery_DocsWide_HasNoChildrenLift.verified.json @@ -0,0 +1,72 @@ +{ + "bool": { + "minimum_should_match": 1, + "should": [ + { + "match_none": {} + }, + { + "bool": { + "must": { + "nested": { + "ignore_unmapped": true, + "inner_hits": { + "name": "api_properties", + "size": 5, + "_source": { + "includes": "api.properties.*" + } + }, + "path": "api.properties", + "query": { + "dis_max": { + "queries": [ + { + "constant_score": { + "boost": 1, + "filter": { + "term": { + "api.properties.name": { + "case_insensitive": true, + "value": "retriever.standard" + } + } + } + } + }, + { + "bool": { + "filter": { + "match_phrase_prefix": { + "api.properties.path_words": { + "query": "retriever standard" + } + } + }, + "must": { + "constant_score": { + "boost": 1, + "filter": { + "term": { + "api.properties.name": { + "case_insensitive": true, + "value": "standard" + } + } + } + } + } + } + } + ] + } + }, + "score_mode": "max" + } + }, + "_name": "api_properties" + } + } + ] + } +} diff --git a/tests/Elastic.Documentation.Api.Tests/.snapshots/ApiPropertySearchTests/ApplyApiPropertyQuery_SingleReading_IsNamedAfterItsInnerHits.c624ce26.verified.json b/tests/Elastic.Documentation.Api.Tests/.snapshots/ApiPropertySearchTests/ApplyApiPropertyQuery_SingleReading_IsNamedAfterItsInnerHits.c624ce26.verified.json new file mode 100644 index 0000000000..9f677dc756 --- /dev/null +++ b/tests/Elastic.Documentation.Api.Tests/.snapshots/ApiPropertySearchTests/ApplyApiPropertyQuery_SingleReading_IsNamedAfterItsInnerHits.c624ce26.verified.json @@ -0,0 +1,66 @@ +{ + "bool": { + "minimum_should_match": 1, + "should": [ + { + "match_none": {} + }, + { + "bool": { + "must": { + "nested": { + "ignore_unmapped": true, + "inner_hits": { + "name": "api_properties", + "size": 5, + "_source": { + "includes": "api.properties.*" + } + }, + "path": "api.properties", + "query": { + "dis_max": { + "queries": [ + { + "constant_score": { + "boost": 1, + "filter": { + "term": { + "api.properties.name": { + "case_insensitive": true, + "value": "track_total" + } + } + } + } + } + ] + } + }, + "score_mode": "max" + } + }, + "_name": "api_properties", + "should": { + "dis_max": { + "queries": [ + { + "constant_score": { + "boost": 5, + "filter": { + "terms": { + "api.endpoints": [ + "_search" + ] + } + } + } + } + ] + } + } + } + } + ] + } +} diff --git a/tests/Elastic.Documentation.Api.Tests/.snapshots/ApiPropertySearchTests/ApplyApiPropertyQuery_SingleReading_IsNamedAfterItsInnerHits.edaac49a.verified.json b/tests/Elastic.Documentation.Api.Tests/.snapshots/ApiPropertySearchTests/ApplyApiPropertyQuery_SingleReading_IsNamedAfterItsInnerHits.edaac49a.verified.json new file mode 100644 index 0000000000..36830e6af6 --- /dev/null +++ b/tests/Elastic.Documentation.Api.Tests/.snapshots/ApiPropertySearchTests/ApplyApiPropertyQuery_SingleReading_IsNamedAfterItsInnerHits.edaac49a.verified.json @@ -0,0 +1,48 @@ +{ + "bool": { + "minimum_should_match": 1, + "should": [ + { + "match_none": {} + }, + { + "bool": { + "must": { + "nested": { + "ignore_unmapped": true, + "inner_hits": { + "name": "api_properties", + "size": 5, + "_source": { + "includes": "api.properties.*" + } + }, + "path": "api.properties", + "query": { + "dis_max": { + "queries": [ + { + "constant_score": { + "boost": 1, + "filter": { + "term": { + "api.properties.name": { + "case_insensitive": true, + "value": "track_total" + } + } + } + } + } + ] + } + }, + "score_mode": "max" + } + }, + "_name": "api_properties" + } + } + ] + } +} diff --git a/tests/Elastic.Documentation.Api.Tests/.snapshots/ApiPropertySearchTests/ApplyApiPropertyQuery_TwoPlainWordsDocsWide_KeepsTheEndpointRequiredForTheSurvivingReading.verified.json b/tests/Elastic.Documentation.Api.Tests/.snapshots/ApiPropertySearchTests/ApplyApiPropertyQuery_TwoPlainWordsDocsWide_KeepsTheEndpointRequiredForTheSurvivingReading.verified.json new file mode 100644 index 0000000000..68a0053676 --- /dev/null +++ b/tests/Elastic.Documentation.Api.Tests/.snapshots/ApiPropertySearchTests/ApplyApiPropertyQuery_TwoPlainWordsDocsWide_KeepsTheEndpointRequiredForTheSurvivingReading.verified.json @@ -0,0 +1,92 @@ +{ + "bool": { + "minimum_should_match": 1, + "should": [ + { + "match_none": {} + }, + { + "bool": { + "must": [ + { + "nested": { + "ignore_unmapped": true, + "inner_hits": { + "name": "api_properties", + "size": 5, + "_source": { + "includes": "api.properties.*" + } + }, + "path": "api.properties", + "query": { + "dis_max": { + "queries": [ + { + "constant_score": { + "boost": 1, + "filter": { + "term": { + "api.properties.name": { + "case_insensitive": true, + "value": "track_total_hits" + } + } + } + } + } + ] + } + }, + "score_mode": "max" + } + }, + { + "dis_max": { + "queries": [ + { + "dis_max": { + "queries": [ + { + "constant_score": { + "boost": 5, + "filter": { + "terms": { + "api.endpoints": [ + "bulk" + ] + } + } + } + } + ] + } + }, + { + "dis_max": { + "queries": [ + { + "constant_score": { + "boost": 5, + "filter": { + "terms": { + "api.endpoints": [ + "_bulk" + ] + } + } + } + } + ] + } + } + ] + } + } + ], + "_name": "api_properties" + } + } + ] + } +} diff --git a/tests/Elastic.Documentation.Api.Tests/.snapshots/ApiPropertySearchTests/BuildApiPropertyQuery_DocsWide_HasNoChildrenOption.verified.json b/tests/Elastic.Documentation.Api.Tests/.snapshots/ApiPropertySearchTests/BuildApiPropertyQuery_DocsWide_HasNoChildrenOption.verified.json new file mode 100644 index 0000000000..2495016d87 --- /dev/null +++ b/tests/Elastic.Documentation.Api.Tests/.snapshots/ApiPropertySearchTests/BuildApiPropertyQuery_DocsWide_HasNoChildrenOption.verified.json @@ -0,0 +1,57 @@ +{ + "nested": { + "ignore_unmapped": true, + "inner_hits": { + "name": "api_properties", + "size": 5, + "_source": { + "includes": "api.properties.*" + } + }, + "path": "api.properties", + "query": { + "dis_max": { + "queries": [ + { + "constant_score": { + "boost": 1, + "filter": { + "term": { + "api.properties.name": { + "case_insensitive": true, + "value": "retriever.standard" + } + } + } + } + }, + { + "bool": { + "filter": { + "match_phrase_prefix": { + "api.properties.path_words": { + "query": "retriever standard" + } + } + }, + "must": { + "constant_score": { + "boost": 1, + "filter": { + "term": { + "api.properties.name": { + "case_insensitive": true, + "value": "standard" + } + } + } + } + } + } + } + ] + } + }, + "score_mode": "max" + } +} diff --git a/tests/Elastic.Documentation.Api.Tests/.snapshots/ApiPropertySearchTests/BuildApiPropertyQuery_Identifier_ReturnsNestedInnerHits.3dd983dc.verified.json b/tests/Elastic.Documentation.Api.Tests/.snapshots/ApiPropertySearchTests/BuildApiPropertyQuery_Identifier_ReturnsNestedInnerHits.3dd983dc.verified.json new file mode 100644 index 0000000000..31bb0a8c53 --- /dev/null +++ b/tests/Elastic.Documentation.Api.Tests/.snapshots/ApiPropertySearchTests/BuildApiPropertyQuery_Identifier_ReturnsNestedInnerHits.3dd983dc.verified.json @@ -0,0 +1,33 @@ +{ + "nested": { + "ignore_unmapped": true, + "inner_hits": { + "name": "api_properties", + "size": 5, + "_source": { + "includes": "api.properties.*" + } + }, + "path": "api.properties", + "query": { + "dis_max": { + "queries": [ + { + "constant_score": { + "boost": 1, + "filter": { + "term": { + "api.properties.name": { + "case_insensitive": true, + "value": "track_total" + } + } + } + } + } + ] + } + }, + "score_mode": "max" + } +} diff --git a/tests/Elastic.Documentation.Api.Tests/.snapshots/ApiPropertySearchTests/BuildApiPropertyQuery_Identifier_ReturnsNestedInnerHits.ae232dbe.verified.json b/tests/Elastic.Documentation.Api.Tests/.snapshots/ApiPropertySearchTests/BuildApiPropertyQuery_Identifier_ReturnsNestedInnerHits.ae232dbe.verified.json new file mode 100644 index 0000000000..05ebb55d4d --- /dev/null +++ b/tests/Elastic.Documentation.Api.Tests/.snapshots/ApiPropertySearchTests/BuildApiPropertyQuery_Identifier_ReturnsNestedInnerHits.ae232dbe.verified.json @@ -0,0 +1,33 @@ +{ + "nested": { + "ignore_unmapped": true, + "inner_hits": { + "name": "api_properties", + "size": 5, + "_source": { + "includes": "api.properties.*" + } + }, + "path": "api.properties", + "query": { + "dis_max": { + "queries": [ + { + "constant_score": { + "boost": 1, + "filter": { + "term": { + "api.properties.name": { + "case_insensitive": true, + "value": "knn_field" + } + } + } + } + } + ] + } + }, + "score_mode": "max" + } +} diff --git a/tests/Elastic.Documentation.Api.Tests/.snapshots/ApiPropertySearchTests/BuildApiPropertyQuery_MarkedSingleWord_MatchesTheBareName.verified.json b/tests/Elastic.Documentation.Api.Tests/.snapshots/ApiPropertySearchTests/BuildApiPropertyQuery_MarkedSingleWord_MatchesTheBareName.verified.json new file mode 100644 index 0000000000..21b7ab17ad --- /dev/null +++ b/tests/Elastic.Documentation.Api.Tests/.snapshots/ApiPropertySearchTests/BuildApiPropertyQuery_MarkedSingleWord_MatchesTheBareName.verified.json @@ -0,0 +1,33 @@ +{ + "nested": { + "ignore_unmapped": true, + "inner_hits": { + "name": "api_properties", + "size": 5, + "_source": { + "includes": "api.properties.*" + } + }, + "path": "api.properties", + "query": { + "dis_max": { + "queries": [ + { + "constant_score": { + "boost": 1, + "filter": { + "term": { + "api.properties.name": { + "case_insensitive": true, + "value": "retriever" + } + } + } + } + } + ] + } + }, + "score_mode": "max" + } +} diff --git a/tests/Elastic.Documentation.Api.Tests/.snapshots/ApiPropertySearchTests/BuildApiPropertyQuery_PrefixMatch_OnlyInTheApiFilteredLookup.1b7c3761.verified.json b/tests/Elastic.Documentation.Api.Tests/.snapshots/ApiPropertySearchTests/BuildApiPropertyQuery_PrefixMatch_OnlyInTheApiFilteredLookup.1b7c3761.verified.json new file mode 100644 index 0000000000..31bb0a8c53 --- /dev/null +++ b/tests/Elastic.Documentation.Api.Tests/.snapshots/ApiPropertySearchTests/BuildApiPropertyQuery_PrefixMatch_OnlyInTheApiFilteredLookup.1b7c3761.verified.json @@ -0,0 +1,33 @@ +{ + "nested": { + "ignore_unmapped": true, + "inner_hits": { + "name": "api_properties", + "size": 5, + "_source": { + "includes": "api.properties.*" + } + }, + "path": "api.properties", + "query": { + "dis_max": { + "queries": [ + { + "constant_score": { + "boost": 1, + "filter": { + "term": { + "api.properties.name": { + "case_insensitive": true, + "value": "track_total" + } + } + } + } + } + ] + } + }, + "score_mode": "max" + } +} diff --git a/tests/Elastic.Documentation.Api.Tests/.snapshots/ApiPropertySearchTests/BuildApiPropertyQuery_PrefixMatch_OnlyInTheApiFilteredLookup.36054997.verified.json b/tests/Elastic.Documentation.Api.Tests/.snapshots/ApiPropertySearchTests/BuildApiPropertyQuery_PrefixMatch_OnlyInTheApiFilteredLookup.36054997.verified.json new file mode 100644 index 0000000000..31bb0a8c53 --- /dev/null +++ b/tests/Elastic.Documentation.Api.Tests/.snapshots/ApiPropertySearchTests/BuildApiPropertyQuery_PrefixMatch_OnlyInTheApiFilteredLookup.36054997.verified.json @@ -0,0 +1,33 @@ +{ + "nested": { + "ignore_unmapped": true, + "inner_hits": { + "name": "api_properties", + "size": 5, + "_source": { + "includes": "api.properties.*" + } + }, + "path": "api.properties", + "query": { + "dis_max": { + "queries": [ + { + "constant_score": { + "boost": 1, + "filter": { + "term": { + "api.properties.name": { + "case_insensitive": true, + "value": "track_total" + } + } + } + } + } + ] + } + }, + "score_mode": "max" + } +} diff --git a/tests/Elastic.Documentation.Api.Tests/.snapshots/ApiPropertySearchTests/BuildApiPropertyQuery_QualifiedPath_RequiresMatchingLeafAndPathWords.1a97d7d5.verified.json b/tests/Elastic.Documentation.Api.Tests/.snapshots/ApiPropertySearchTests/BuildApiPropertyQuery_QualifiedPath_RequiresMatchingLeafAndPathWords.1a97d7d5.verified.json new file mode 100644 index 0000000000..856112c032 --- /dev/null +++ b/tests/Elastic.Documentation.Api.Tests/.snapshots/ApiPropertySearchTests/BuildApiPropertyQuery_QualifiedPath_RequiresMatchingLeafAndPathWords.1a97d7d5.verified.json @@ -0,0 +1,57 @@ +{ + "nested": { + "ignore_unmapped": true, + "inner_hits": { + "name": "api_properties", + "size": 5, + "_source": { + "includes": "api.properties.*" + } + }, + "path": "api.properties", + "query": { + "dis_max": { + "queries": [ + { + "constant_score": { + "boost": 1, + "filter": { + "term": { + "api.properties.name": { + "case_insensitive": true, + "value": "rrf.min_score" + } + } + } + } + }, + { + "bool": { + "filter": { + "match_phrase_prefix": { + "api.properties.path_words": { + "query": "rrf min_score" + } + } + }, + "must": { + "constant_score": { + "boost": 1, + "filter": { + "term": { + "api.properties.name": { + "case_insensitive": true, + "value": "min_score" + } + } + } + } + } + } + } + ] + } + }, + "score_mode": "max" + } +} diff --git a/tests/Elastic.Documentation.Api.Tests/.snapshots/ApiPropertySearchTests/BuildApiPropertyQuery_QualifiedPath_RequiresMatchingLeafAndPathWords.9d305c6d.verified.json b/tests/Elastic.Documentation.Api.Tests/.snapshots/ApiPropertySearchTests/BuildApiPropertyQuery_QualifiedPath_RequiresMatchingLeafAndPathWords.9d305c6d.verified.json new file mode 100644 index 0000000000..7671dc160d --- /dev/null +++ b/tests/Elastic.Documentation.Api.Tests/.snapshots/ApiPropertySearchTests/BuildApiPropertyQuery_QualifiedPath_RequiresMatchingLeafAndPathWords.9d305c6d.verified.json @@ -0,0 +1,57 @@ +{ + "nested": { + "ignore_unmapped": true, + "inner_hits": { + "name": "api_properties", + "size": 5, + "_source": { + "includes": "api.properties.*" + } + }, + "path": "api.properties", + "query": { + "dis_max": { + "queries": [ + { + "constant_score": { + "boost": 1, + "filter": { + "term": { + "api.properties.name": { + "case_insensitive": true, + "value": "retriever.rrf.min_score" + } + } + } + } + }, + { + "bool": { + "filter": { + "match_phrase_prefix": { + "api.properties.path_words": { + "query": "retriever rrf min_score" + } + } + }, + "must": { + "constant_score": { + "boost": 1, + "filter": { + "term": { + "api.properties.name": { + "case_insensitive": true, + "value": "min_score" + } + } + } + } + } + } + } + ] + } + }, + "score_mode": "max" + } +} diff --git a/tests/Elastic.Documentation.Api.Tests/.snapshots/ApiPropertySearchTests/BuildApiPropertyQuery_QualifiedPath_RequiresMatchingLeafAndPathWords.9f803851.verified.json b/tests/Elastic.Documentation.Api.Tests/.snapshots/ApiPropertySearchTests/BuildApiPropertyQuery_QualifiedPath_RequiresMatchingLeafAndPathWords.9f803851.verified.json new file mode 100644 index 0000000000..b546e4f8a2 --- /dev/null +++ b/tests/Elastic.Documentation.Api.Tests/.snapshots/ApiPropertySearchTests/BuildApiPropertyQuery_QualifiedPath_RequiresMatchingLeafAndPathWords.9f803851.verified.json @@ -0,0 +1,57 @@ +{ + "nested": { + "ignore_unmapped": true, + "inner_hits": { + "name": "api_properties", + "size": 5, + "_source": { + "includes": "api.properties.*" + } + }, + "path": "api.properties", + "query": { + "dis_max": { + "queries": [ + { + "constant_score": { + "boost": 1, + "filter": { + "term": { + "api.properties.name": { + "case_insensitive": true, + "value": "labels..value" + } + } + } + } + }, + { + "bool": { + "filter": { + "match_phrase_prefix": { + "api.properties.path_words": { + "query": "labels value" + } + } + }, + "must": { + "constant_score": { + "boost": 1, + "filter": { + "term": { + "api.properties.name": { + "case_insensitive": true, + "value": "value" + } + } + } + } + } + } + } + ] + } + }, + "score_mode": "max" + } +} diff --git a/tests/Elastic.Documentation.Api.Tests/.snapshots/ApiPropertySearchTests/BuildApiPropertyQuery_QualifiedPath_RequiresMatchingLeafAndPathWords.f466369f.verified.json b/tests/Elastic.Documentation.Api.Tests/.snapshots/ApiPropertySearchTests/BuildApiPropertyQuery_QualifiedPath_RequiresMatchingLeafAndPathWords.f466369f.verified.json new file mode 100644 index 0000000000..f99af68ffb --- /dev/null +++ b/tests/Elastic.Documentation.Api.Tests/.snapshots/ApiPropertySearchTests/BuildApiPropertyQuery_QualifiedPath_RequiresMatchingLeafAndPathWords.f466369f.verified.json @@ -0,0 +1,57 @@ +{ + "nested": { + "ignore_unmapped": true, + "inner_hits": { + "name": "api_properties", + "size": 5, + "_source": { + "includes": "api.properties.*" + } + }, + "path": "api.properties", + "query": { + "dis_max": { + "queries": [ + { + "constant_score": { + "boost": 1, + "filter": { + "term": { + "api.properties.name": { + "case_insensitive": true, + "value": "[].retriever.standard" + } + } + } + } + }, + { + "bool": { + "filter": { + "match_phrase_prefix": { + "api.properties.path_words": { + "query": "retriever standard" + } + } + }, + "must": { + "constant_score": { + "boost": 1, + "filter": { + "term": { + "api.properties.name": { + "case_insensitive": true, + "value": "standard" + } + } + } + } + } + } + } + ] + } + }, + "score_mode": "max" + } +} diff --git a/tests/Elastic.Documentation.Api.Tests/.snapshots/ApiPropertySearchTests/Mapping_Api.2f0e22ee.verified.json b/tests/Elastic.Documentation.Api.Tests/.snapshots/ApiPropertySearchTests/Mapping_Api.2f0e22ee.verified.json new file mode 100644 index 0000000000..09282a0c34 --- /dev/null +++ b/tests/Elastic.Documentation.Api.Tests/.snapshots/ApiPropertySearchTests/Mapping_Api.2f0e22ee.verified.json @@ -0,0 +1,121 @@ +{ + "key": { + "type": "keyword" + }, + "trees": { + "type": "object", + "properties": { + "tree": { + "type": "keyword" + }, + "root": { + "type": "keyword", + "index": false + } + } + }, + "properties": { + "type": "nested", + "properties": { + "name": { + "type": "keyword" + }, + "path": { + "type": "keyword", + "fields": { + "match": { + "type": "text" + } + } + }, + "path_words": { + "type": "search_as_you_type", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 256 + } + } + }, + "type": { + "type": "keyword" + }, + "location": { + "type": "keyword" + }, + "anchor": { + "type": "keyword" + }, + "description": { + "type": "text", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 256 + } + } + }, + "parent": { + "type": "keyword" + } + } + }, + "routes": { + "type": "object", + "properties": { + "path": { + "type": "keyword", + "index": false + }, + "methods": { + "type": "keyword", + "index": false + } + } + }, + "endpoints": { + "type": "keyword" + }, + "endpoints_bare": { + "type": "keyword" + }, + "method": { + "type": "keyword" + }, + "path": { + "type": "keyword" + }, + "operation_id": { + "type": "keyword" + }, + "words": { + "type": "text", + "fields": { + "completion": { + "type": "search_as_you_type", + "analyzer": "synonyms_fixed_analyzer", + "search_analyzer": "synonyms_analyzer" + } + }, + "analyzer": "synonyms_fixed_analyzer", + "search_analyzer": "synonyms_analyzer" + }, + "group": { + "type": "text", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 256 + } + }, + "analyzer": "synonyms_fixed_analyzer", + "search_analyzer": "synonyms_analyzer" + }, + "id_words": { + "type": "rank_feature", + "positive_score_impact": false + }, + "deprecated": { + "type": "boolean" + } +} diff --git a/tests/Elastic.Documentation.Api.Tests/.snapshots/ApiPropertySearchTests/Mapping_Api.54b63aa5.verified.json b/tests/Elastic.Documentation.Api.Tests/.snapshots/ApiPropertySearchTests/Mapping_Api.54b63aa5.verified.json new file mode 100644 index 0000000000..09282a0c34 --- /dev/null +++ b/tests/Elastic.Documentation.Api.Tests/.snapshots/ApiPropertySearchTests/Mapping_Api.54b63aa5.verified.json @@ -0,0 +1,121 @@ +{ + "key": { + "type": "keyword" + }, + "trees": { + "type": "object", + "properties": { + "tree": { + "type": "keyword" + }, + "root": { + "type": "keyword", + "index": false + } + } + }, + "properties": { + "type": "nested", + "properties": { + "name": { + "type": "keyword" + }, + "path": { + "type": "keyword", + "fields": { + "match": { + "type": "text" + } + } + }, + "path_words": { + "type": "search_as_you_type", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 256 + } + } + }, + "type": { + "type": "keyword" + }, + "location": { + "type": "keyword" + }, + "anchor": { + "type": "keyword" + }, + "description": { + "type": "text", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 256 + } + } + }, + "parent": { + "type": "keyword" + } + } + }, + "routes": { + "type": "object", + "properties": { + "path": { + "type": "keyword", + "index": false + }, + "methods": { + "type": "keyword", + "index": false + } + } + }, + "endpoints": { + "type": "keyword" + }, + "endpoints_bare": { + "type": "keyword" + }, + "method": { + "type": "keyword" + }, + "path": { + "type": "keyword" + }, + "operation_id": { + "type": "keyword" + }, + "words": { + "type": "text", + "fields": { + "completion": { + "type": "search_as_you_type", + "analyzer": "synonyms_fixed_analyzer", + "search_analyzer": "synonyms_analyzer" + } + }, + "analyzer": "synonyms_fixed_analyzer", + "search_analyzer": "synonyms_analyzer" + }, + "group": { + "type": "text", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 256 + } + }, + "analyzer": "synonyms_fixed_analyzer", + "search_analyzer": "synonyms_analyzer" + }, + "id_words": { + "type": "rank_feature", + "positive_score_impact": false + }, + "deprecated": { + "type": "boolean" + } +} diff --git a/tests/Elastic.Documentation.Api.Tests/.snapshots/SearchQueryBuilderTests/BuildLexicalQuery_EndpointQuery_IncludesApiEndpointsTerms.verified.json b/tests/Elastic.Documentation.Api.Tests/.snapshots/SearchQueryBuilderTests/BuildLexicalQuery_EndpointQuery_IncludesApiEndpointsTerms.verified.json new file mode 100644 index 0000000000..6671182316 --- /dev/null +++ b/tests/Elastic.Documentation.Api.Tests/.snapshots/SearchQueryBuilderTests/BuildLexicalQuery_EndpointQuery_IncludesApiEndpointsTerms.verified.json @@ -0,0 +1,116 @@ +{ + "bool": { + "must": { + "bool": { + "should": [ + { + "constant_score": { + "boost": 3, + "filter": { + "multi_match": { + "analyzer": "synonyms_analyzer", + "fields": [ + "search_title.completion", + "search_title.completion._2gram", + "search_title.completion._3gram" + ], + "operator": "and", + "query": "_search", + "type": "bool_prefix" + } + } + } + }, + { + "multi_match": { + "analyzer": "synonyms_analyzer", + "boost": 0.1, + "fields": "body", + "operator": "and", + "query": "_search", + "type": "best_fields" + } + }, + { + "term": { + "title.keyword": { + "value": "_search" + } + } + }, + { + "dis_max": { + "queries": [ + { + "constant_score": { + "boost": 5, + "filter": { + "terms": { + "api.endpoints": [ + "_search" + ] + } + } + } + } + ] + } + }, + { + "constant_score": { + "filter": { + "term": { + "title.starts_with": { + "value": "_search" + } + } + } + } + }, + { + "constant_score": { + "boost": 0.3, + "filter": { + "match": { + "path.match": { + "query": "_search" + } + } + } + } + } + ] + } + }, + "should": [ + { + "rank_feature": { + "boost": 0.8, + "field": "navigation.depth" + } + }, + { + "rank_feature": { + "boost": 0.8, + "field": "navigation.table_of_contents" + } + }, + { + "term": { + "section": { + "boost": 0.15, + "value": "reference" + } + } + }, + { + "term": { + "section": { + "boost": 0.1, + "value": "getting-started" + } + } + } + ] + } +} diff --git a/tests/Elastic.Documentation.Api.Tests/ApiPropertySearchTests.cs b/tests/Elastic.Documentation.Api.Tests/ApiPropertySearchTests.cs index f045e6da91..5c372114b7 100644 --- a/tests/Elastic.Documentation.Api.Tests/ApiPropertySearchTests.cs +++ b/tests/Elastic.Documentation.Api.Tests/ApiPropertySearchTests.cs @@ -14,6 +14,7 @@ using Elastic.Documentation.Search.Highlighting; using Elastic.Transport; using Elastic.Transport.Extensions; +using Nullean.Snapshots.Json; namespace Elastic.Documentation.Api.Tests; @@ -21,171 +22,55 @@ public class ApiPropertySearchTests { private static readonly ElasticsearchClient Client = new(new ElasticsearchClientSettings(new InMemoryRequestInvoker())); - private static JsonDocument Json(Query? query) => JsonDocument.Parse(Client.RequestResponseSerializer.SerializeToString(query)); + private static string Json(Query? query) => Client.RequestResponseSerializer.SerializeToString(query); [Test] - [Arguments("track_total", null, 5)] - [Arguments("knn_field search", null, 5)] - public void BuildApiPropertyQuery_Identifier_ReturnsNestedInnerHits(string input, string? filter, int size) - { - using var json = Json(SearchQueryBuilder.BuildApiPropertyQuery(input, filter)); - var nested = json.RootElement.GetProperty("nested"); - nested.GetProperty("path").GetString().Should().Be("api.properties"); - nested.GetProperty("inner_hits").GetProperty("size").GetInt32().Should().Be(size); - } + [Arguments("track_total", null)] + [Arguments("knn_field search", null)] + public void BuildApiPropertyQuery_Identifier_ReturnsNestedInnerHits(string input, string? filter) => + Json(SearchQueryBuilder.BuildApiPropertyQuery(input, filter)).Should().MatchJsonSnapshot(); [Test] - [Arguments("retriever.rrf.min_score", "min_score", "retriever rrf min_score")] - [Arguments("rrf.min_score", "min_score", "rrf min_score")] - [Arguments("[].retriever.standard", "standard", "retriever standard")] - [Arguments("labels..value", "value", "labels value")] - public void BuildApiPropertyQuery_QualifiedPath_RequiresMatchingLeafAndPathWords(string input, string leaf, string phrase) - { - using var json = Json(SearchQueryBuilder.BuildApiPropertyQuery(input, null)); - var qualified = json - .RootElement - .GetProperty("nested") - .GetProperty("query") - .GetProperty("dis_max") - .GetProperty("queries")[1] - .GetProperty("bool"); - qualified - .GetProperty("must") - .GetProperty("constant_score") - .GetProperty("filter") - .GetProperty("term") - .GetProperty("api.properties.name") - .GetProperty("value") - .GetString() - .Should() - .Be(leaf); - qualified - .GetProperty("filter") - .GetProperty("match_phrase_prefix") - .GetProperty("api.properties.path_words") - .GetProperty("query") - .GetString() - .Should() - .Be(phrase); - } + [Arguments("retriever.rrf.min_score")] + [Arguments("rrf.min_score")] + [Arguments("[].retriever.standard")] + [Arguments("labels..value")] + public void BuildApiPropertyQuery_QualifiedPath_RequiresMatchingLeafAndPathWords(string input) => + Json(SearchQueryBuilder.BuildApiPropertyQuery(input, null)).Should().MatchJsonSnapshot(); [Test] - public void BuildApiPropertyQuery_MarkedSingleWord_MatchesTheBareName() - { - using var json = Json(SearchQueryBuilder.BuildApiPropertyQuery("[].retriever", null)); - var options = json.RootElement.GetProperty("nested").GetProperty("query").GetProperty("dis_max").GetProperty("queries"); - options.GetArrayLength().Should().Be(1); - options[0] - .GetProperty("constant_score") - .GetProperty("filter") - .GetProperty("term") - .GetProperty("api.properties.name") - .GetProperty("value") - .GetString() - .Should() - .Be("retriever"); - } + public void BuildApiPropertyQuery_MarkedSingleWord_MatchesTheBareName() => + Json(SearchQueryBuilder.BuildApiPropertyQuery("[].retriever", null)).Should().MatchJsonSnapshot(); [Test] - [Arguments(null, false)] - [Arguments("all", false)] - public void BuildApiPropertyQuery_PrefixMatch_OnlyInTheApiFilteredLookup(string? filter, bool prefix) - { - using var json = Json(SearchQueryBuilder.BuildApiPropertyQuery("track_total", filter)); - var options = json.RootElement.GetProperty("nested").GetProperty("query").GetProperty("dis_max").GetProperty("queries"); - options.GetArrayLength().Should().Be(1); - var name = options[0].TryGetProperty("dis_max", out var names) - ? names.GetProperty("queries").EnumerateArray().ToArray() - : [options[0]]; - name.Should().HaveCount(prefix ? 2 : 1); - name[0].GetRawText().Should().Contain("\"term\""); - } + [Arguments(null)] + [Arguments("all")] + public void BuildApiPropertyQuery_PrefixMatch_OnlyInTheApiFilteredLookup(string? filter) => + Json(SearchQueryBuilder.BuildApiPropertyQuery("track_total", filter)).Should().MatchJsonSnapshot(); [Test] [Arguments("track_total")] [Arguments("/_search track_total")] - public void ApplyApiPropertyQuery_SingleReading_IsNamedAfterItsInnerHits(string input) - { - using var json = Json(SearchQueryBuilder.ApplyApiPropertyQuery(new MatchNoneQuery(), input, null)); - var reading = json.RootElement.GetProperty("bool").GetProperty("should")[1].GetProperty("bool"); - reading.GetProperty("_name").GetString().Should().Be("api_properties"); - reading - .GetProperty("must") - .GetProperty("nested") - .GetProperty("inner_hits") - .GetProperty("name") - .GetString() - .Should() - .Be("api_properties"); - } + public void ApplyApiPropertyQuery_SingleReading_IsNamedAfterItsInnerHits(string input) => + Json(SearchQueryBuilder.ApplyApiPropertyQuery(new MatchNoneQuery(), input, null)).Should().MatchJsonSnapshot(); [Test] - public void ApplyApiPropertyQuery_TwoPlainWordsDocsWide_KeepsTheEndpointRequiredForTheSurvivingReading() - { - using var json = Json(SearchQueryBuilder.ApplyApiPropertyQuery(new MatchNoneQuery(), "bulk track_total_hits", null)); - var reading = json.RootElement.GetProperty("bool").GetProperty("should")[1].GetProperty("bool"); - var must = reading.GetProperty("must"); - must.GetArrayLength().Should().Be(2); - must[0].GetProperty("nested").GetProperty("query").GetRawText().Should().Contain("\"value\":\"track_total_hits\""); - must[1].GetRawText().Should().Contain("_bulk"); - reading - .TryGetProperty("should", out _) - .Should() - .BeFalse("the plain word names no field docs-wide, and the reading left must still hold its endpoint"); - } + public void ApplyApiPropertyQuery_TwoPlainWordsDocsWide_KeepsTheEndpointRequiredForTheSurvivingReading() => + Json(SearchQueryBuilder.ApplyApiPropertyQuery(new MatchNoneQuery(), "bulk track_total_hits", null)).Should().MatchJsonSnapshot(); [Test] [Arguments("GET /_search track_total")] [Arguments("/_search track_total")] - public void ApplyApiPropertyQuery_DocsWideMarkedEndpointAndField_RequiresFieldMatchBeforeEndpointBoost(string input) - { - using var json = Json(SearchQueryBuilder.ApplyApiPropertyQuery(new MatchNoneQuery(), input, null)); - var fieldBranch = json.RootElement.GetProperty("bool").GetProperty("should")[1].GetProperty("bool"); - fieldBranch - .GetProperty("must") - .GetProperty("nested") - .GetProperty("query") - .GetRawText() - .Should() - .Contain("\"value\":\"track_total\""); - var endpoint = fieldBranch.GetProperty("should").GetProperty("dis_max").GetProperty("queries")[0].GetProperty("constant_score"); - endpoint.GetProperty("filter").GetProperty("terms").GetProperty("api.endpoints")[0].GetString().Should().Be("_search"); - } + public void ApplyApiPropertyQuery_DocsWideMarkedEndpointAndField_RequiresFieldMatchBeforeEndpointBoost(string input) => + Json(SearchQueryBuilder.ApplyApiPropertyQuery(new MatchNoneQuery(), input, null)).Should().MatchJsonSnapshot(); [Test] - public void ApplyApiPropertyQuery_DocsWideTwoIdentifierWords_TriesBothReadingsWithUniqueInnerHitNames() - { - using var json = Json(SearchQueryBuilder.ApplyApiPropertyQuery(new MatchNoneQuery(), "knn_field track_total", null)); - var readings = json.RootElement.GetProperty("bool").GetProperty("should")[1].GetProperty("bool").GetProperty("should"); - readings.GetArrayLength().Should().Be(2); - var trackTotal = readings.EnumerateArray().Single(r => r.GetRawText().Contains("\"value\":\"track_total\"")).GetProperty("bool"); - trackTotal.GetProperty("must")[1].GetRawText().Should().Contain("\"api.endpoints\":[\"_knn_field\""); - var readingNames = readings.EnumerateArray().Select(r => r.GetProperty("bool").GetProperty("_name").GetString()).ToArray(); - var innerHitNames = readings - .EnumerateArray() - .Select( - r => r - .GetProperty("bool") - .GetProperty("must")[0] - .GetProperty("nested") - .GetProperty("inner_hits") - .GetProperty("name") - .GetString() - ) - .ToArray(); - readingNames.Should().OnlyHaveUniqueItems(); - innerHitNames.Should().OnlyHaveUniqueItems(); - readingNames.Concat(innerHitNames).Should().AllSatisfy(n => SearchQueryBuilder.IsApiPropertyHits(n).Should().BeTrue()); - } + public void ApplyApiPropertyQuery_DocsWideTwoIdentifierWords_TriesBothReadingsWithUniqueInnerHitNames() => + Json(SearchQueryBuilder.ApplyApiPropertyQuery(new MatchNoneQuery(), "knn_field track_total", null)).Should().MatchJsonSnapshot(); [Test] - public void ApplyApiPropertyQuery_DocsWide_HasNoChildrenLift() - { - var json = Client.RequestResponseSerializer.SerializeToString( - SearchQueryBuilder.ApplyApiPropertyQuery(new MatchNoneQuery(), "retriever.standard", null) - ); - json.Should().Contain("\"api.properties.name\"").And.NotContain("api.properties.parent"); - } + public void ApplyApiPropertyQuery_DocsWide_HasNoChildrenLift() => + Json(SearchQueryBuilder.ApplyApiPropertyQuery(new MatchNoneQuery(), "retriever.standard", null)).Should().MatchJsonSnapshot(); [Test] public void ProcessHit_InnerHitsFromBothReadings_AreMerged() @@ -234,105 +119,15 @@ public void ProcessHit_HitThatMatchedNoReading_KeepsNoProperties() public void BuildApiPropertyQuery_ExcludedQuery_ReturnsNull(string input, string? filter) => SearchQueryBuilder.BuildApiPropertyQuery(input, filter).Should().BeNull(); + /// + /// The api.* mapping, for the lexical and the semantic variant: boolean deprecated, synonym text words and group, + /// negative-impact rank_feature id_words, plain keyword endpoints, nested properties with keyword names and parent + /// (and no children field), and unindexed keyword routes. + /// [Test] [Arguments(false)] [Arguments(true)] - public void Mapping_ApiDeprecated_IsBoolean(bool semantic) - { - var api = ApiMapping(semantic); - api.GetProperty("deprecated").GetProperty("type").GetString().Should().Be("boolean"); - } - - [Test] - [Arguments(false)] - [Arguments(true)] - public void Mapping_ApiWordsAndGroup_AreSynonymText(bool semantic) - { - var api = ApiMapping(semantic); - var words = api.GetProperty("words"); - words.GetProperty("type").GetString().Should().Be("text"); - words.GetProperty("analyzer").GetString().Should().Be("synonyms_fixed_analyzer"); - words.GetProperty("search_analyzer").GetString().Should().Be("synonyms_analyzer"); - var completion = words.GetProperty("fields").GetProperty("completion"); - completion.GetProperty("type").GetString().Should().Be("search_as_you_type"); - completion.GetProperty("analyzer").GetString().Should().Be("synonyms_fixed_analyzer"); - completion.GetProperty("search_analyzer").GetString().Should().Be("synonyms_analyzer"); - completion.TryGetProperty("index_options", out _).Should().BeFalse(); - - var group = api.GetProperty("group"); - group.GetProperty("type").GetString().Should().Be("text"); - group.GetProperty("analyzer").GetString().Should().Be("synonyms_fixed_analyzer"); - group.GetProperty("search_analyzer").GetString().Should().Be("synonyms_analyzer"); - } - - [Test] - [Arguments(false)] - [Arguments(true)] - public void Mapping_ApiIdWords_IsRankFeatureWithNegativeImpact(bool semantic) - { - var idWords = ApiMapping(semantic).GetProperty("id_words"); - idWords.GetProperty("type").GetString().Should().Be("rank_feature"); - idWords.GetProperty("positive_score_impact").GetBoolean().Should().BeFalse(); - } - - [Test] - [Arguments(false)] - [Arguments(true)] - public void Mapping_ApiEndpointsBare_IsKeyword(bool semantic) - { - var api = ApiMapping(semantic); - var endpoints = api.GetProperty("endpoints"); - endpoints.GetProperty("type").GetString().Should().Be("keyword"); - endpoints.TryGetProperty("fields", out _).Should().BeFalse(); - api.GetProperty("endpoints_bare").GetProperty("type").GetString().Should().Be("keyword"); - } - - [Test] - [Arguments(false)] - [Arguments(true)] - public void Mapping_ApiProperties_IsNestedWithKeywordNames(bool semantic) - { - var context = semantic - ? DocumentationMappingContext.DocumentationDocumentSemantic.CreateContext(type: "assembler", env: "test") - : DocumentationMappingContext.DocumentationDocument.CreateContext(type: "assembler", env: "test"); - using var json = JsonDocument.Parse(context.GetMappingsJson()); - var field = json.RootElement.GetProperty("properties").GetProperty("api").GetProperty("properties").GetProperty("properties"); - field.GetProperty("type").GetString().Should().Be("nested"); - var fields = field.GetProperty("properties"); - fields.GetProperty("name").GetProperty("type").GetString().Should().Be("keyword"); - fields.GetProperty("anchor").GetProperty("type").GetString().Should().Be("keyword"); - fields.GetProperty("path_words").GetProperty("type").GetString().Should().Be("search_as_you_type"); - - var routes = json - .RootElement - .GetProperty("properties") - .GetProperty("api") - .GetProperty("properties") - .GetProperty("routes") - .GetProperty("properties"); - routes.GetProperty("path").GetProperty("type").GetString().Should().Be("keyword"); - routes.GetProperty("path").GetProperty("index").GetBoolean().Should().BeFalse(); - routes.GetProperty("methods").GetProperty("type").GetString().Should().Be("keyword"); - } - - [Test] - [Arguments(false)] - [Arguments(true)] - public void Mapping_ApiProperties_HasNoChildrenField(bool semantic) - { - var context = semantic - ? DocumentationMappingContext.DocumentationDocumentSemantic.CreateContext(type: "assembler", env: "test") - : DocumentationMappingContext.DocumentationDocument.CreateContext(type: "assembler", env: "test"); - using var json = JsonDocument.Parse(context.GetMappingsJson()); - var fields = json - .RootElement - .GetProperty("properties") - .GetProperty("api") - .GetProperty("properties") - .GetProperty("properties") - .GetProperty("properties"); - fields.TryGetProperty("children", out _).Should().BeFalse(); - } + public void Mapping_Api(bool semantic) => ApiMapping(semantic).GetRawText().Should().MatchJsonSnapshot(); [Test] public void BuildApiPropertyQuery_OnlyADot_ReturnsNull() => SearchQueryBuilder.BuildApiPropertyQuery(".", null).Should().BeNull(); @@ -360,13 +155,7 @@ public void ProcessHit_ManyInnerHits_KeepsTheThreeBestDistinctAnchorsInScoreOrde [Test] public void BuildApiPropertyQuery_DocsWide_HasNoChildrenOption() => - Client - .RequestResponseSerializer - .SerializeToString(SearchQueryBuilder.BuildApiPropertyQuery("retriever.standard", null)) - .Should() - .NotContain("api.properties.parent") - .And - .NotContain("_name"); + Json(SearchQueryBuilder.BuildApiPropertyQuery("retriever.standard", null)).Should().MatchJsonSnapshot(); [Test] public void ProcessHit_InnerHitsWithoutMatchedQueries_AreAllNameMatches() @@ -382,27 +171,6 @@ public void ProcessHit_InnerHitsWithoutMatchedQueries_AreAllNameMatches() result.Properties.Select(p => p.Path).Should().Equal("a", "b", "c"); } - [Test] - [Arguments(false)] - [Arguments(true)] - public void Mapping_ApiPropertiesParent_IsKeyword(bool semantic) - { - var context = semantic - ? DocumentationMappingContext.DocumentationDocumentSemantic.CreateContext(type: "assembler", env: "test") - : DocumentationMappingContext.DocumentationDocument.CreateContext(type: "assembler", env: "test"); - using var json = JsonDocument.Parse(context.GetMappingsJson()); - var parent = json - .RootElement - .GetProperty("properties") - .GetProperty("api") - .GetProperty("properties") - .GetProperty("properties") - .GetProperty("properties") - .GetProperty("parent"); - parent.GetProperty("type").GetString().Should().Be("keyword"); - parent.TryGetProperty("index", out _).Should().BeFalse(); - } - private static string SearchResponseJson( string innerHits, string moreInnerHitGroups = "", diff --git a/tests/Elastic.Documentation.Api.Tests/SearchQueryBuilderTests.cs b/tests/Elastic.Documentation.Api.Tests/SearchQueryBuilderTests.cs index 6ed3b86ef6..e40209c699 100644 --- a/tests/Elastic.Documentation.Api.Tests/SearchQueryBuilderTests.cs +++ b/tests/Elastic.Documentation.Api.Tests/SearchQueryBuilderTests.cs @@ -2,12 +2,13 @@ // Elasticsearch B.V licenses this file to you under the Apache 2.0 License. // See the LICENSE file in the project root for more information -using System.Text.Json; using AwesomeAssertions; using Elastic.Clients.Elasticsearch; using Elastic.Documentation.Search; using Elastic.Transport; using Elastic.Transport.Extensions; +using Nullean.Snapshots; +using Nullean.Snapshots.Json; namespace Elastic.Documentation.Api.Tests; @@ -24,7 +25,9 @@ public void BuildApiEndpointQuery_SingleSegment_ReturnsDisMaxOfOneFullBoostTerms .RequestResponseSerializer .SerializeToString(SearchQueryBuilder.BuildApiEndpointQuery(input)) .Should() - .Be("""{"dis_max":{"queries":[{"constant_score":{"boost":5,"filter":{"terms":{"api.endpoints":["_search"]}}}}]}}"""); + .MatchInlineJsonSnapshot( + """{"dis_max":{"queries":[{"constant_score":{"boost":5,"filter":{"terms":{"api.endpoints":["_search"]}}}}]}}""" + ); [Test] public void BuildApiEndpointQuery_ValueThenEndpoint_ScoresFullTermAboveWildcardTemplates() => @@ -32,38 +35,85 @@ public void BuildApiEndpointQuery_ValueThenEndpoint_ScoresFullTermAboveWildcardT .RequestResponseSerializer .SerializeToString(SearchQueryBuilder.BuildApiEndpointQuery("my-index/_search")) .Should() - .Be( + .MatchInlineJsonSnapshot( """{"dis_max":{"queries":[{"constant_score":{"boost":5,"filter":{"terms":{"api.endpoints":["my-index/_search"]}}}},{"constant_score":{"boost":3.3333333,"filter":{"terms":{"api.endpoints":["*/_search","my-index/*"]}}}}]}}""" ); [Test] - public void BuildApiEndpointQuery_InteriorValue_MatchesTemplateAndNeverTheParentPrefix() - { - var json = Client.RequestResponseSerializer.SerializeToString( - SearchQueryBuilder.BuildApiEndpointQuery("_security/user/alice/_password") - ); - using var doc = JsonDocument.Parse(json); - var clauses = doc - .RootElement - .GetProperty("dis_max") - .GetProperty("queries") - .EnumerateArray() - .Select(q => q.GetProperty("constant_score")) - .ToArray(); - var terms = clauses.Select( - c => c - .GetProperty("filter") - .GetProperty("terms") - .GetProperty("api.endpoints") - .EnumerateArray() - .Select(t => t.GetString()) - .ToArray() - ).ToArray(); - - clauses.Select(c => c.GetProperty("boost").GetSingle()).Should().Equal(5f, 4f, 3f, 2f); - terms[1].Should().Contain("_security/user/*/_password"); - terms.SelectMany(t => t).Should().NotContain("_security/user"); - } + public void BuildApiEndpointQuery_InteriorValue_MatchesTemplateAndNeverTheParentPrefix() => + Client + .RequestResponseSerializer + .SerializeToString(SearchQueryBuilder.BuildApiEndpointQuery("_security/user/alice/_password")) + .Should() + .MatchInlineJsonSnapshot( + """ + { + "dis_max": { + "queries": [ + { + "constant_score": { + "boost": 5, + "filter": { + "terms": { + "api.endpoints": [ + "_security/user/alice/_password" + ] + } + } + } + }, + { + "constant_score": { + "boost": 4, + "filter": { + "terms": { + "api.endpoints": [ + "*/user/alice/_password", + "_security/*/alice/_password", + "_security/user/*/_password", + "_security/user/alice/*" + ] + } + } + } + }, + { + "constant_score": { + "boost": 3, + "filter": { + "terms": { + "api.endpoints": [ + "*/*/alice/_password", + "*/user/*/_password", + "*/user/alice/*", + "_security/*/*/_password", + "_security/*/alice/*", + "_security/user/*/*" + ] + } + } + } + }, + { + "constant_score": { + "boost": 2, + "filter": { + "terms": { + "api.endpoints": [ + "*/*/*/_password", + "*/*/alice/*", + "*/user/*/*", + "_security/*/*/*" + ] + } + } + } + } + ] + } + } + """ + ); [Test] [Arguments(" ")] @@ -80,7 +130,9 @@ public void BuildApiEndpointQuery_RootRoute_ReturnsTheRootTerm(string input) => .RequestResponseSerializer .SerializeToString(SearchQueryBuilder.BuildApiEndpointQuery(input)) .Should() - .Be("""{"dis_max":{"queries":[{"constant_score":{"boost":5,"filter":{"terms":{"api.endpoints":["/"]}}}}]}}"""); + .MatchInlineJsonSnapshot( + """{"dis_max":{"queries":[{"constant_score":{"boost":5,"filter":{"terms":{"api.endpoints":["/"]}}}}]}}""" + ); [Test] [Arguments("search")] @@ -90,7 +142,9 @@ public void BuildBareWordEndpointQuery_Word_TargetsUnderscoreEndpoint(string inp .RequestResponseSerializer .SerializeToString(SearchQueryBuilder.BuildBareWordEndpointQuery(input)) .Should() - .Be("""{"dis_max":{"queries":[{"constant_score":{"boost":5,"filter":{"terms":{"api.endpoints":["_search"]}}}}]}}"""); + .MatchInlineJsonSnapshot( + """{"dis_max":{"queries":[{"constant_score":{"boost":5,"filter":{"terms":{"api.endpoints":["_search"]}}}}]}}""" + ); [Test] [Arguments("_search")] @@ -102,12 +156,10 @@ public void BuildBareWordEndpointQuery_NotABareWord_ReturnsNull(string input) => SearchQueryBuilder.BuildBareWordEndpointQuery(input).Should().BeNull(); [Test] - public void BuildLexicalQuery_EndpointQuery_IncludesApiEndpointsTerms() - { - var query = SearchQueryBuilder.BuildLexicalQuery("_search", new Dictionary(), [], null); - - var json = Client.RequestResponseSerializer.SerializeToString(query); - - json.Should().Contain("""{"terms":{"api.endpoints":["_search"]}}"""); - } + public void BuildLexicalQuery_EndpointQuery_IncludesApiEndpointsTerms() => + Client + .RequestResponseSerializer + .SerializeToString(SearchQueryBuilder.BuildLexicalQuery("_search", new Dictionary(), [], null)) + .Should() + .MatchJsonSnapshot(); } diff --git a/tests/Elastic.Documentation.Build.Tests/SitemapTests.cs b/tests/Elastic.Documentation.Build.Tests/SitemapTests.cs index 12eaba7b39..bba6cecd59 100644 --- a/tests/Elastic.Documentation.Build.Tests/SitemapTests.cs +++ b/tests/Elastic.Documentation.Build.Tests/SitemapTests.cs @@ -3,10 +3,10 @@ // See the LICENSE file in the project root for more information using System.IO.Abstractions.TestingHelpers; -using System.Text.Json; -using System.Xml.Linq; using AwesomeAssertions; using Elastic.Documentation.Assembler.Building; +using Nullean.Snapshots; +using Nullean.Snapshots.Json; namespace Elastic.Documentation.Build.Tests; @@ -28,23 +28,27 @@ public void Generate_WritesValidSitemapXml_WithCorrectLastModDates() // Act SitemapBuilder.Generate(entries, fs, outputDir); - // Assert - var sitemapPath = fs.Path.Join("/output", "sitemap.xml"); - fs.File.Exists(sitemapPath).Should().BeTrue(); - - var content = fs.File.ReadAllText(sitemapPath); - var doc = XDocument.Parse(content); - XNamespace ns = "http://www.sitemaps.org/schemas/sitemap/0.9"; - - var urls = doc.Descendants(ns + "url").ToList(); - urls.Should().HaveCount(2); - - // Entries are ordered by URL - urls[0].Element(ns + "loc")!.Value.Should().Be("https://www.elastic.co/docs/elasticsearch/getting-started"); - urls[0].Element(ns + "lastmod")!.Value.Should().Contain("2025-06-15"); - - urls[1].Element(ns + "loc")!.Value.Should().Be("https://www.elastic.co/docs/kibana/dashboard"); - urls[1].Element(ns + "lastmod")!.Value.Should().Contain("2025-07-20"); + // Assert - entries are ordered by URL, lastmod comes from the entry + fs.File.Exists(fs.Path.Join("/output", "sitemap.xml")).Should().BeTrue(); + fs + .File + .ReadAllText(fs.Path.Join("/output", "sitemap.xml")) + .Should() + .MatchInlineSnapshot( + """ + + + + https://www.elastic.co/docs/elasticsearch/getting-started + 2025-06-15T10:30:00.0000000+00:00 + + + https://www.elastic.co/docs/kibana/dashboard + 2025-07-20T14:00:00.0000000+00:00 + + + """ + ); } [Test] @@ -71,19 +75,36 @@ public void Generate_OrdersUrlsAlphabetically() var outputDir = fs.DirectoryInfo.New("/output"); fs.Directory.CreateDirectory("/output"); - var now = DateTimeOffset.UtcNow; + var now = new DateTimeOffset(2025, 6, 15, 10, 30, 0, TimeSpan.Zero); var entries = new Dictionary { ["/docs/z-last"] = now, ["/docs/a-first"] = now, ["/docs/m-middle"] = now, }; // Act SitemapBuilder.Generate(entries, fs, outputDir); // Assert - var content = fs.File.ReadAllText(fs.Path.Join("/output", "sitemap.xml")); - var doc = XDocument.Parse(content); - XNamespace ns = "http://www.sitemaps.org/schemas/sitemap/0.9"; - - var locs = doc.Descendants(ns + "loc").Select(e => e.Value).ToList(); - locs.Should().BeInAscendingOrder(); + fs + .File + .ReadAllText(fs.Path.Join("/output", "sitemap.xml")) + .Should() + .MatchInlineSnapshot( + """ + + + + https://www.elastic.co/docs/a-first + 2025-06-15T10:30:00.0000000+00:00 + + + https://www.elastic.co/docs/m-middle + 2025-06-15T10:30:00.0000000+00:00 + + + https://www.elastic.co/docs/z-last + 2025-06-15T10:30:00.0000000+00:00 + + + """ + ); } [Test] @@ -142,7 +163,7 @@ public void Generate_ExcludesApiDocsFromSitemap() var fs = new MockFileSystem(); var outputDir = fs.DirectoryInfo.New("/output"); - var now = DateTimeOffset.UtcNow; + var now = new DateTimeOffset(2025, 6, 15, 10, 30, 0, TimeSpan.Zero); var entries = new Dictionary { ["/docs/elasticsearch/getting-started"] = now, @@ -154,16 +175,26 @@ public void Generate_ExcludesApiDocsFromSitemap() // Act SitemapBuilder.Generate(entries, fs, outputDir); - // Assert - var content = fs.File.ReadAllText(fs.Path.Join("/output", "sitemap.xml")); - var doc = XDocument.Parse(content); - XNamespace ns = "http://www.sitemaps.org/schemas/sitemap/0.9"; - - var locs = doc.Descendants(ns + "loc").Select(e => e.Value).ToList(); - locs.Should().HaveCount(2); - locs.Should().NotContain(l => l.Contains("/docs/api/")); - locs.Should().Contain("https://www.elastic.co/docs/elasticsearch/getting-started"); - locs.Should().Contain("https://www.elastic.co/docs/kibana/dashboard"); + // Assert - the /docs/api/ entries are left out + fs + .File + .ReadAllText(fs.Path.Join("/output", "sitemap.xml")) + .Should() + .MatchInlineSnapshot( + """ + + + + https://www.elastic.co/docs/elasticsearch/getting-started + 2025-06-15T10:30:00.0000000+00:00 + + + https://www.elastic.co/docs/kibana/dashboard + 2025-06-15T10:30:00.0000000+00:00 + + + """ + ); } [Test] @@ -173,11 +204,37 @@ public void BuildSearchBody_FirstPage_HasPitButNoSearchAfter() var json = EsSitemapReader.BuildSearchBody("test-pit-id", null); // Assert - json.Should().Contain("\"pit\""); - json.Should().Contain("\"test-pit-id\""); - json.Should().Contain("\"must_not\""); - json.Should().Contain("\"hidden\":true"); - json.Should().NotContain("search_after"); + json.Should().MatchInlineJsonSnapshot( + """ + { + "size": 1000, + "_source": [ + "url", + "content_last_updated" + ], + "query": { + "bool": { + "must_not": [ + { + "term": { + "hidden": true + } + } + ] + } + }, + "pit": { + "id": "test-pit-id", + "keep_alive": "2m" + }, + "sort": [ + { + "url": "asc" + } + ] + } + """ + ); } [Test] @@ -187,8 +244,40 @@ public void BuildSearchBody_SubsequentPage_IncludesSearchAfter() var json = EsSitemapReader.BuildSearchBody("test-pit-id", ["/docs/last-url"]); // Assert - json.Should().Contain("\"search_after\""); - json.Should().Contain("/docs/last-url"); + json.Should().MatchInlineJsonSnapshot( + """ + { + "size": 1000, + "_source": [ + "url", + "content_last_updated" + ], + "query": { + "bool": { + "must_not": [ + { + "term": { + "hidden": true + } + } + ] + } + }, + "pit": { + "id": "test-pit-id", + "keep_alive": "2m" + }, + "sort": [ + { + "url": "asc" + } + ], + "search_after": [ + "/docs/last-url" + ] + } + """ + ); } [Test] @@ -196,9 +285,38 @@ public void BuildSearchBody_EscapesSpecialCharactersInPitId() { // Act var json = EsSitemapReader.BuildSearchBody("pit-with-\"quotes\"", null); - var doc = JsonDocument.Parse(json); - // Assert — verify the value round-trips correctly through serialization - doc.RootElement.GetProperty("pit").GetProperty("id").GetString().Should().Be("pit-with-\"quotes\""); + // Assert - the quotes are escaped, so the value round-trips through serialization + json.Should().MatchInlineJsonSnapshot( + """ + { + "size": 1000, + "_source": [ + "url", + "content_last_updated" + ], + "query": { + "bool": { + "must_not": [ + { + "term": { + "hidden": true + } + } + ] + } + }, + "pit": { + "id": "pit-with-\"quotes\"", + "keep_alive": "2m" + }, + "sort": [ + { + "url": "asc" + } + ] + } + """ + ); } } diff --git a/tests/Elastic.LegacyDocs.Migration.Tests/EmitterTests.cs b/tests/Elastic.LegacyDocs.Migration.Tests/EmitterTests.cs index 0062672ae6..2a1a76ddf1 100644 --- a/tests/Elastic.LegacyDocs.Migration.Tests/EmitterTests.cs +++ b/tests/Elastic.LegacyDocs.Migration.Tests/EmitterTests.cs @@ -4,6 +4,7 @@ using AwesomeAssertions; using Elastic.LegacyDocs.Migration.Asciidoc; +using Nullean.Snapshots; namespace Elastic.LegacyDocs.Migration.Tests; @@ -23,8 +24,11 @@ public void UndefinedAttribute_EmittedAsSingleBraces_NotDouble() { var md = Emit("= T\n\n{undefined-attr} text\n"); // Should be {undefined-attr}, NOT {{undefined-attr}} - md.Should().Contain("{undefined-attr}"); - md.Should().NotContain("{{undefined-attr}}"); + md.Should().MatchInlineSnapshot(""" + # T + + {undefined-attr} text + """); } [Test] @@ -32,15 +36,22 @@ public void ProductNameAttribute_EmittedAsDoubleBraces() { // {es} is a ProductNames key — should remain {{es}} for docs-builder substitution var md = Emit("= T\n\n{es} cluster\n"); - md.Should().Contain("{{es}}"); + md.Should().MatchInlineSnapshot(""" + # T + + {{es}} cluster + """); } [Test] public void DefinedAttribute_IsSubstituted_NotEmittedAsRef() { var md = Emit("= T\n\n:myattr: hello world\n\n{myattr}\n"); - md.Should().Contain("hello world"); - md.Should().NotContain("{myattr}"); + md.Should().MatchInlineSnapshot(""" + # T + + hello world + """); } // ── Step 5: callout list in output ─────────────────────────────────────── @@ -49,8 +60,19 @@ public void DefinedAttribute_IsSubstituted_NotEmittedAsRef() public void CodeBlock_WithCallouts_EmitsOrderedList() { var md = Emit("= T\n\n[source,python]\n----\nfoo() # <1>\nbar() # <2>\n----\n<1> Call foo\n<2> Call bar\n"); - md.Should().Contain("1. Call foo"); - md.Should().Contain("2. Call bar"); + md.Should().MatchInlineSnapshot( + """ + # T + + ```python + foo() # <1> + bar() # <2> + ``` + + 1. Call foo + 2. Call bar + """ + ); } // ── Step 7: xref with > in text ────────────────────────────────────────── @@ -59,24 +81,35 @@ public void CodeBlock_WithCallouts_EmitsOrderedList() public void CrossRef_Simple_IsEmittedCorrectly() { var md = Emit("= T\n\nSee <>.\n"); - md.Should().NotContain("<<"); - md.Should().Contain("[my-anchor](#my-anchor)"); + md.Should().MatchInlineSnapshot(""" + # T + + See [my-anchor](#my-anchor). + """); } [Test] public void CrossRef_WithText_IsEmittedCorrectly() { var md = Emit("= T\n\nSee <>.\n"); - md.Should().NotContain("<<"); - md.Should().Contain("[Click here](#my-anchor)"); + md.Should().MatchInlineSnapshot(""" + # T + + See [Click here](#my-anchor). + """); } [Test] public void CrossRef_WithBacktickInText_IsEmittedCorrectly() { var md = Emit("= T\n\nfilters like <> to normalise.\n"); - md.Should().NotContain("<<"); - md.Should().Contain("[`lowercase`]"); + md.Should().MatchInlineSnapshot( + """ + # T + + filters like [`lowercase`](#analysis-lowercase-tokenfilter) to normalise. + """ + ); } [Test] @@ -86,8 +119,15 @@ public void CrossRef_InMultiLineParagraph_WithBacktick_IsEmittedCorrectly() var asciidoc = "= T\n\nIt can be combined\nwith token filters like <> to\nnormalise the analysed terms.\n"; var md = Emit(asciidoc); - md.Should().NotContain("<<"); - md.Should().Contain("[`lowercase`]"); + md.Should().MatchInlineSnapshot( + """ + # T + + It can be combined + with token filters like [`lowercase`](#analysis-lowercase-tokenfilter) to + normalise the analysed terms. + """ + ); } [Test] @@ -97,8 +137,17 @@ public void CrossRef_AfterDLItemWithBlankLineSeparator_IsEmittedCorrectly() var asciidoc = "= T\n\n<>::\n\nIt can be combined with token filters like <> to\nnormalise the analysed terms.\n"; var md = Emit(asciidoc); - md.Should().NotContain("<<"); - md.Should().Contain("[`lowercase`]"); + md.Should().MatchInlineSnapshot( + """ + # T + + [Term](#term-anchor) + + + It can be combined with token filters like [`lowercase`](#analysis-lowercase-tokenfilter) to + normalise the analysed terms. + """ + ); } [Test] @@ -108,8 +157,15 @@ public void CrossRef_InMultiLineParagraph_WithCurlyQuotes_IsEmittedCorrectly() var asciidoc = "= T\n\nIn order to use scrolling, the initial search request should specify the\n`scroll` parameter in the query string, which tells Elasticsearch how long it\nshould keep the ``search context'' alive (see <>), eg `?scroll=1m`.\n"; var md = Emit(asciidoc); - md.Should().NotContain("<<"); - md.Should().Contain("[scroll-search-context]"); + md.Should().MatchInlineSnapshot( + """ + # T + + In order to use scrolling, the initial search request should specify the + `scroll` parameter in the query string, which tells Elasticsearch how long it + should keep the "search context" alive (see [scroll-search-context](#scroll-search-context)), eg `?scroll=1m`. + """ + ); } [Test] @@ -117,8 +173,13 @@ public void CrossRef_InOrderedListItem_WithBacktickText_IsEmittedCorrectly() { // Ordered list item with xref that has backtick-wrapped text (space after comma) var md = Emit("= T\n\n. Define a runtime field using the <> queries.\n"); - md.Should().NotContain("<<"); - md.Should().Contain("[`term`]"); + md.Should().MatchInlineSnapshot( + """ + # T + + 1. Define a runtime field using the [`term`](#query-dsl-term-query) queries. + """ + ); } [Test] @@ -126,8 +187,13 @@ public void CrossRef_InOrderedListItem_WithPrecedingCode_IsEmittedCorrectly() { // Ordered list item where a `code` span precedes the xref var md = Emit("= T\n\n. A type of `lookup` that uses the <> query.\n"); - md.Should().NotContain("<<"); - md.Should().Contain("[`term`]"); + md.Should().MatchInlineSnapshot( + """ + # T + + 1. A type of `lookup` that uses the [`term`](#query-dsl-term-query) query. + """ + ); } [Test] @@ -135,8 +201,13 @@ public void CrossRef_WithAsteriskInLinkText_IsEmittedCorrectly() { // Bold marker * between text and xref text must not consume the << var md = Emit("= T\n\nSemantic*text field can be target of <>.\n"); - md.Should().NotContain("<<"); - md.Should().Contain("[copy*to fields]"); + md.Should().MatchInlineSnapshot( + """ + # T + + Semantic*text field can be target of [copy*to fields](#copy-to). + """ + ); } [Test] @@ -144,7 +215,11 @@ public void CrossRef_WithAngleBracketInText_IsEmittedCorrectly() { // < b>> — text contains a literal > var md = Emit("= T\n\nSee < 7.0>>\n"); - md.Should().Contain("[version > 7.0]"); + md.Should().MatchInlineSnapshot(""" + # T + + See [version > 7.0](#setup) + """); } // ── Passthrough inline (constrained +..+) ───────────────────────────────── @@ -153,7 +228,11 @@ public void CrossRef_WithAngleBracketInText_IsEmittedCorrectly() public void PassthroughInline_BasicCase_EmitsCodeSpan() { var md = Emit("= T\n\nDefaults to +max(1, 10)+.\n"); - md.Should().Contain("`max(1, 10)`"); + md.Should().MatchInlineSnapshot(""" + # T + + Defaults to `max(1, 10)`. + """); } [Test] @@ -161,9 +240,13 @@ public void PassthroughInline_VersionSuffix_NotTreatedAsPassthrough() { // "8.0+" is a version indicator, NOT a passthrough delimiter — must not swallow following text var md = Emit("= T\n\nES 8.0+ does not (see <>). ES 8.0+ returns error.\n"); - md.Should().NotContain("<>"); - md.Should().Contain("[removal-of-types](#removal-of-types)"); - md.Should().Contain("8.0+"); + md.Should().MatchInlineSnapshot( + """ + # T + + ES 8.0+ does not (see [removal-of-types](#removal-of-types)). ES 8.0+ returns error. + """ + ); } // ── Verbatim include resolution (Step 6) ────────────────────────────────── @@ -183,9 +266,16 @@ public void VerbatimBlock_StandardTaggedInclude_IsResolved() }); var doc = parser.Parse("= T\n\n[source,esql]\n----\ninclude::{specs}/string.csv-spec[tag=my-example]\n----\n", ""); var md = new MarkdownEmitter(new MarkdownEmitterOptions()).Emit(doc); - md.Should().Contain("row1"); - md.Should().Contain("row2"); - md.Should().NotContain("include::"); + md.Should().MatchInlineSnapshot( + """ + # T + + ```esql + row1 + row2 + ``` + """ + ); } [Test] @@ -194,9 +284,17 @@ public void VerbatimBlock_IfevalConditionMarkers_AreStripped() // ifeval/endif markers inside verbatim blocks should be stripped; content is kept var adoc = "= T\n\n----\n{\nifeval::[\"{trust}\"==\"api-key\"]\n \"credentials\": \"secret\",\nendif::[]\n}\n----\n"; var md = Emit(adoc); - md.Should().NotContain("ifeval::"); - md.Should().NotContain("endif::"); - md.Should().Contain("\"credentials\": \"secret\","); + md.Should().MatchInlineSnapshot( + """ + # T + + ``` + { + "credentials": "secret", + } + ``` + """ + ); } // ── Conditional blocks (ifeval) ──────────────────────────────────────────── @@ -207,9 +305,13 @@ public void Ifeval_FalseCondition_ExcludesContent() // ifeval::[expr] — condition false, block content must be excluded var adoc = "= T\n\nBefore.\n\nifeval::[\"{stack}\"==\"something-else\"]\nHidden text.\nendif::[]\n\nAfter.\n"; var md = Emit(adoc, new Dictionary { ["stack"] = "elastic" }); - md.Should().Contain("Before."); - md.Should().Contain("After."); - md.Should().NotContain("Hidden text."); + md.Should().MatchInlineSnapshot(""" + # T + + Before. + + After. + """); } [Test] @@ -218,9 +320,15 @@ public void Ifeval_TrueCondition_IncludesContent() // ifeval::[expr] — condition true, block content must be included var adoc = "= T\n\nBefore.\n\nifeval::[\"{stack}\"==\"elastic\"]\nVisible text.\nendif::[]\n\nAfter.\n"; var md = Emit(adoc, new Dictionary { ["stack"] = "elastic" }); - md.Should().Contain("Before."); - md.Should().Contain("After."); - md.Should().Contain("Visible text."); + md.Should().MatchInlineSnapshot(""" + # T + + Before. + + Visible text. + + After. + """); } [Test] @@ -230,10 +338,18 @@ public void CodeBlock_TrailingSpaceOnClosingDelimiter_StillClosesBlock() // The lexer must treat this as a valid closing delimiter. var adoc = "= T\n\n[source,json]\n----\n{\"hello\":\"world\"}\n---- \n\n==== Next section\n"; var md = Emit(adoc); - md.Should().Contain("```json"); - md.Should().Contain(/*lang=json,strict*/ "{\"hello\":\"world\"}"); // If the block closes, Next section becomes a real heading; if not, it's inside code block - md.Should().NotContain("==== Next section"); // raw AsciiDoc must not appear + md.Should().MatchInlineSnapshot( + """ + # T + + ```json + {"hello":"world"} + ``` + + #### Next section + """ + ); } [Test] @@ -243,8 +359,11 @@ public void Ifeval_ContentAfterBlock_NotLeaked_WhenConditionFalse() // causing all content after the ifeval block to leak through unconditionally. var adoc = "= T\n\nifeval::[\"{trust}\"==\"api-key\"]\nSecret\nendif::[]\n\nPublic paragraph.\n"; var md = Emit(adoc, new Dictionary { ["trust"] = "cert" }); - md.Should().NotContain("Secret"); - md.Should().Contain("Public paragraph."); + md.Should().MatchInlineSnapshot(""" + # T + + Public paragraph. + """); } // ── Long dash delimiter (50 dashes) ────────────────────────────────────── @@ -256,10 +375,17 @@ public void CodeBlock_LongDashDelimiter_IsTreatedAsCodeFence() var adoc = "= T\n\n[source,js]\n--------------------------------------------------\n\"input\": {}\n--------------------------------------------------\n\nNormal paragraph.\n"; var md = Emit(adoc); - md.Should().Contain("```"); - md.Should().Contain("\"input\": {}"); - md.Should().NotContain("--------------------------------------------------"); - md.Should().Contain("Normal paragraph."); + md.Should().MatchInlineSnapshot( + """ + # T + + ```js + "input": {} + ``` + + Normal paragraph. + """ + ); } [Test] @@ -290,10 +416,25 @@ public void CodeBlock_LongDashDelimiter_InNestedSection_IsTreatedAsCodeFence() "" ); var md = Emit(adoc); - md.Should().Contain("```"); - md.Should().Contain("\"key\": \"value\""); - md.Should().NotContain("--------------------------------------------------"); - md.Should().Contain("Normal paragraph."); + md.Should().MatchInlineSnapshot( + """ + # Doc + + ## Level1 [#top] + + ### Level2 [#sec2] + + #### Level3 [#sec3] + + Some text: + + ```js + "key": "value" + ``` + + Normal paragraph. + """ + ); } // ── Open block delimiter trailing whitespace ─────────────────────────────── @@ -305,9 +446,18 @@ public void OpenBlock_TrailingSpaceOnOpeningDelimiter_StillClosesBlock() // The parser must not leave the block unclosed because openingDelim = "-- " ≠ "--". var adoc = "= T\n\n.Requirements\n[sidebar]\n-- \n* req one\n--\n\nNormal paragraph.\n"; var md = Emit(adoc); - md.Should().Contain("req one"); - md.Should().Contain("Normal paragraph."); // The trailing `--` must NOT appear raw in the output - md.Should().NotContain("\n--\n"); + md.Should().MatchInlineSnapshot( + """ + # T + + :::{admonition} Sidebar + - req one + + ::: + + Normal paragraph. + """ + ); } } diff --git a/tests/Elastic.SiteSearch.Tests/.snapshots/ContentStackMappingTests/BlogLegacy_Maps_BodyAndMetadata.verified.json b/tests/Elastic.SiteSearch.Tests/.snapshots/ContentStackMappingTests/BlogLegacy_Maps_BodyAndMetadata.verified.json new file mode 100644 index 0000000000..4157131a88 --- /dev/null +++ b/tests/Elastic.SiteSearch.Tests/.snapshots/ContentStackMappingTests/BlogLegacy_Maps_BodyAndMetadata.verified.json @@ -0,0 +1,34 @@ +{ + "$type": "site", + "locale": "en", + "published_date": "2023-01-16T09:00:00+00:00", + "modified_date": "2023-06-20T14:45:00+00:00", + "og": { + "title": "Getting Started with Kibana Dashboards | Elastic Blog", + "description": "Learn how to create interactive dashboards in Kibana." + }, + "twitter": {}, + "http": {}, + "title": "Getting Started with Kibana Dashboards", + "search_title": "Blog: Getting Started with Kibana Dashboards", + "content_type": "site", + "path": "/blog/getting-started-with-kibana-dashboards", + "url": "/blog/getting-started-with-kibana-dashboards", + "hash": "1fbad85a92ea0c15", + "last_updated": "2023-06-20T14:45:00+00:00", + "description": "Learn how to create interactive dashboards in Kibana to visualize your Elasticsearch data effectively.", + "headings": [ + "Creating Your First Dashboard", + "Adding Visualizations" + ], + "body": "Kibana is a powerful visualization tool that lets you explore your Elasticsearch data. Creating Your First Dashboard Start by navigating to the Dashboard section in Kibana. You can add visualizations, saved searches, and maps to build comprehensive views of your data. Adding Visualizations Click the Add panel button to select from existing visualizations or create new ones directly within the dashboard.", + "summary": "Learn how to create interactive dashboards in Kibana to visualize your Elasticsearch data effectively.", + "parents": [], + "translated": true, + "section": "blog", + "content_tier": "reference", + "navigation": { + "depth": 3, + "table_of_contents": 100 + } +} diff --git a/tests/Elastic.SiteSearch.Tests/.snapshots/ContentStackMappingTests/BlogModern_Maps_ModularBlocksBody.verified.json b/tests/Elastic.SiteSearch.Tests/.snapshots/ContentStackMappingTests/BlogModern_Maps_ModularBlocksBody.verified.json new file mode 100644 index 0000000000..e5d9bf2126 --- /dev/null +++ b/tests/Elastic.SiteSearch.Tests/.snapshots/ContentStackMappingTests/BlogModern_Maps_ModularBlocksBody.verified.json @@ -0,0 +1,31 @@ +{ + "$type": "site", + "locale": "en", + "published_date": "2024-03-12T10:00:00+00:00", + "modified_date": "2024-04-15T16:30:00+00:00", + "og": { + "title": "Building Search Applications | Elastic Blog", + "description": "A practical guide to building search applications with Elasticsearch." + }, + "twitter": {}, + "http": {}, + "title": "Building Search Applications with Elasticsearch", + "search_title": "Blog: Building Search Applications with Elasticsearch", + "content_type": "site", + "path": "/blog/building-search-applications-with-elasticsearch", + "url": "/blog/building-search-applications-with-elasticsearch", + "hash": "a1702fc1327b9904", + "last_updated": "2024-04-15T16:30:00+00:00", + "description": "Explore how to build production-ready search applications using Elasticsearch and its ecosystem of tools.", + "headings": [], + "body": "Search is at the heart of modern applications. In this guide, we walk through building a search application from scratch. First, define your index mappings to match your data model. Consider using dynamic templates for flexible schemas. The Query DSL provides a rich set of operators for full-text search, filtering, and aggregations.", + "summary": "Explore how to build production-ready search applications using Elasticsearch and its ecosystem of tools.", + "parents": [], + "translated": true, + "section": "blog", + "content_tier": "reference", + "navigation": { + "depth": 3, + "table_of_contents": 100 + } +} diff --git a/tests/Elastic.SiteSearch.Tests/.snapshots/ContentStackMappingTests/BlogV3_Maps_NestedRichTextBodyAndSummary.verified.json b/tests/Elastic.SiteSearch.Tests/.snapshots/ContentStackMappingTests/BlogV3_Maps_NestedRichTextBodyAndSummary.verified.json new file mode 100644 index 0000000000..6edd5841c0 --- /dev/null +++ b/tests/Elastic.SiteSearch.Tests/.snapshots/ContentStackMappingTests/BlogV3_Maps_NestedRichTextBodyAndSummary.verified.json @@ -0,0 +1,31 @@ +{ + "$type": "site", + "locale": "en", + "published_date": "2026-06-01T00:00:00+00:00", + "og": { + "title": "Hybrid Search With Elasticsearch", + "description": "" + }, + "twitter": {}, + "http": {}, + "title": "Hybrid Search With Elasticsearch", + "search_title": "Hybrid Search With Elasticsearch", + "content_type": "site", + "path": "/search-labs/blog/hybrid-search-with-elasticsearch", + "url": "/search-labs/blog/hybrid-search-with-elasticsearch", + "hash": "c04874ba5dc0d16d", + "description": "Combine lexical and semantic search for better relevance.", + "headings": [ + "Why hybrid search" + ], + "body": "Why hybrid search Hybrid search combines BM25 lexical scoring with dense vector search to improve relevance.", + "summary": "Combine lexical and semantic search for better relevance.", + "parents": [], + "translated": true, + "section": "search-labs", + "content_tier": "reference", + "navigation": { + "depth": 4, + "table_of_contents": 100 + } +} diff --git a/tests/Elastic.SiteSearch.Tests/.snapshots/ContentStackMappingTests/CustomerTile_Maps_ShortContent.verified.json b/tests/Elastic.SiteSearch.Tests/.snapshots/ContentStackMappingTests/CustomerTile_Maps_ShortContent.verified.json new file mode 100644 index 0000000000..b2eb62b05e --- /dev/null +++ b/tests/Elastic.SiteSearch.Tests/.snapshots/ContentStackMappingTests/CustomerTile_Maps_ShortContent.verified.json @@ -0,0 +1,27 @@ +{ + "$type": "site", + "locale": "ko", + "modified_date": "2022-10-01T08:00:00+00:00", + "og": {}, + "twitter": {}, + "http": {}, + "title": "TechCo Industries", + "search_title": "Customer Story: TechCo Industries", + "content_type": "site", + "path": "/kr/videos/techco-elasticsearch-case-study", + "url": "/kr/videos/techco-elasticsearch-case-study", + "hash": "9f3e869e9b215109", + "last_updated": "2022-10-01T08:00:00+00:00", + "description": null, + "headings": [], + "body": "TechCo uses Elasticsearch to power search across their global e-commerce platform.", + "summary": "TechCo uses Elasticsearch to power search across their global e-commerce platform.", + "parents": [], + "translated": true, + "section": "customer-story", + "content_tier": "supplementary", + "navigation": { + "depth": 4, + "table_of_contents": 100 + } +} diff --git a/tests/Elastic.SiteSearch.Tests/.snapshots/ContentStackMappingTests/DescriptionOnly_Maps_PlainDescription_NoBody.verified.json b/tests/Elastic.SiteSearch.Tests/.snapshots/ContentStackMappingTests/DescriptionOnly_Maps_PlainDescription_NoBody.verified.json new file mode 100644 index 0000000000..797c6296ba --- /dev/null +++ b/tests/Elastic.SiteSearch.Tests/.snapshots/ContentStackMappingTests/DescriptionOnly_Maps_PlainDescription_NoBody.verified.json @@ -0,0 +1,28 @@ +{ + "$type": "site", + "locale": "en", + "og": { + "title": "Cohere RAG and Rerank with Elasticsearch", + "description": "Learn how to build a RAG system using the Cohere Inference API." + }, + "twitter": {}, + "http": {}, + "title": "Using Cohere for RAG and Rerank in Elasticsearch", + "search_title": "Using Cohere for RAG and Rerank in Elasticsearch", + "content_type": "site", + "path": "/search-labs/tutorials/examples/cohere-rag-rerank-elasticsearch-tutorial", + "url": "/search-labs/tutorials/examples/cohere-rag-rerank-elasticsearch-tutorial", + "hash": "1ead94c577f4f8eb", + "description": "Learn how to build a RAG system using the Cohere Inference API and Elasticsearch with embeddings, hybrid search, and advanced reranking.", + "headings": [], + "body": "Learn how to build a RAG system using the Cohere Inference API and Elasticsearch with embeddings, hybrid search, and advanced reranking.", + "summary": "Learn how to build a RAG system using the Cohere Inference API and Elasticsearch with embeddings, hybrid search, and advanced reranking.", + "parents": [], + "translated": true, + "section": "search-labs", + "content_tier": "reference", + "navigation": { + "depth": 5, + "table_of_contents": 100 + } +} diff --git a/tests/Elastic.SiteSearch.Tests/.snapshots/ContentStackMappingTests/FaqPage_Maps_NestedTopics.verified.json b/tests/Elastic.SiteSearch.Tests/.snapshots/ContentStackMappingTests/FaqPage_Maps_NestedTopics.verified.json new file mode 100644 index 0000000000..1ab3229cf1 --- /dev/null +++ b/tests/Elastic.SiteSearch.Tests/.snapshots/ContentStackMappingTests/FaqPage_Maps_NestedTopics.verified.json @@ -0,0 +1,31 @@ +{ + "$type": "site", + "locale": "en", + "modified_date": "2024-05-20T16:00:00+00:00", + "og": { + "title": "What is Cloud Security? | Elastic", + "description": "Learn about cloud security fundamentals and best practices.", + "image": "https://images.contentstack.io/v3/assets/sample/cloud-security.png" + }, + "twitter": {}, + "http": {}, + "title": "What is Cloud Security?", + "search_title": "What is: What is Cloud Security?", + "content_type": "site", + "path": "/what-is/cloud-security", + "url": "/what-is/cloud-security", + "hash": "ffa05ad64187f8b4", + "last_updated": "2024-05-20T16:00:00+00:00", + "description": "Learn about cloud security fundamentals and best practices.", + "headings": [], + "body": "Cloud security encompasses the technologies, policies, and controls used to protect data, applications, and infrastructure in cloud computing environments. As organizations migrate workloads to the cloud, ensuring the security of cloud resources becomes critical to protecting sensitive data and maintaining compliance.", + "summary": "Learn about cloud security fundamentals and best practices.", + "parents": [], + "translated": true, + "section": "concept", + "content_tier": "primary", + "navigation": { + "depth": 3, + "table_of_contents": 100 + } +} diff --git a/tests/Elastic.SiteSearch.Tests/.snapshots/ContentStackMappingTests/LandingPage_Maps_SubheadingDescription_NoBody.verified.json b/tests/Elastic.SiteSearch.Tests/.snapshots/ContentStackMappingTests/LandingPage_Maps_SubheadingDescription_NoBody.verified.json new file mode 100644 index 0000000000..905e74d3fc --- /dev/null +++ b/tests/Elastic.SiteSearch.Tests/.snapshots/ContentStackMappingTests/LandingPage_Maps_SubheadingDescription_NoBody.verified.json @@ -0,0 +1,28 @@ +{ + "$type": "site", + "locale": "en", + "og": { + "title": "Elasticsearch tutorials", + "description": "" + }, + "twitter": {}, + "http": {}, + "title": "Tutorials", + "search_title": "Tutorials", + "content_type": "site", + "path": "/search-labs/tutorials", + "url": "/search-labs/tutorials", + "hash": "c27e667d2a905da9", + "description": "Long-form guides to get you started", + "headings": [], + "body": "Long-form guides to get you started", + "summary": "Long-form guides to get you started", + "parents": [], + "translated": true, + "section": "search-labs", + "content_tier": "reference", + "navigation": { + "depth": 3, + "table_of_contents": 100 + } +} diff --git a/tests/Elastic.SiteSearch.Tests/.snapshots/ContentStackMappingTests/MarkdownBodyPage_Maps_MarkdownAsHeadingsAndPlainBody.verified.json b/tests/Elastic.SiteSearch.Tests/.snapshots/ContentStackMappingTests/MarkdownBodyPage_Maps_MarkdownAsHeadingsAndPlainBody.verified.json new file mode 100644 index 0000000000..a7f8ba6bdd --- /dev/null +++ b/tests/Elastic.SiteSearch.Tests/.snapshots/ContentStackMappingTests/MarkdownBodyPage_Maps_MarkdownAsHeadingsAndPlainBody.verified.json @@ -0,0 +1,32 @@ +{ + "$type": "site", + "locale": "en", + "published_date": "2024-01-08T00:00:00+00:00", + "modified_date": "2024-01-10T11:00:00+00:00", + "og": {}, + "twitter": {}, + "http": {}, + "title": "Tracking a novel loader: PIPEDANCE", + "search_title": "Tracking a novel loader: PIPEDANCE", + "content_type": "site", + "path": "/security-labs/threat-command/tracking-a-novel-loader-pipedance", + "url": "/security-labs/threat-command/tracking-a-novel-loader-pipedance", + "hash": "b4c258bda492ff4e", + "last_updated": "2024-01-10T11:00:00+00:00", + "description": "An analysis of the PIPEDANCE loader observed in recent intrusions.", + "headings": [ + "Key takeaways", + "Analysis", + "API resolution" + ], + "body": "Key takeaways PIPEDANCE is a novel loader used to stage secondary payloads. It uses process injection to evade detection. Analysis The loader begins by resolving kernel32.dll exports via a custom PEB walk. API resolution Once resolved, the loader calls VirtualAlloc to stage the payload in memory.", + "summary": "An analysis of the PIPEDANCE loader observed in recent intrusions.", + "parents": [], + "translated": true, + "section": "security-labs", + "content_tier": "reference", + "navigation": { + "depth": 4, + "table_of_contents": 100 + } +} diff --git a/tests/Elastic.SiteSearch.Tests/.snapshots/ContentStackMappingTests/MinimalPage_Maps_TitleUrlSeo.verified.json b/tests/Elastic.SiteSearch.Tests/.snapshots/ContentStackMappingTests/MinimalPage_Maps_TitleUrlSeo.verified.json new file mode 100644 index 0000000000..666bd56f23 --- /dev/null +++ b/tests/Elastic.SiteSearch.Tests/.snapshots/ContentStackMappingTests/MinimalPage_Maps_TitleUrlSeo.verified.json @@ -0,0 +1,31 @@ +{ + "$type": "site", + "locale": "en", + "modified_date": "2024-06-01T12:00:00+00:00", + "og": { + "title": "Elastic Blog | News, Updates & Technical Content", + "description": "Read the latest from Elastic on search, observability, and security.", + "image": "https://images.contentstack.io/v3/assets/sample/blog-og.png" + }, + "twitter": {}, + "http": {}, + "title": "Elastic Blog", + "search_title": "Blog: Elastic Blog", + "content_type": "site", + "path": "/blog", + "url": "/blog", + "hash": "5e69b553831e39de", + "last_updated": "2024-06-01T12:00:00+00:00", + "description": "Read the latest from Elastic on search, observability, and security.", + "headings": [], + "body": "Read the latest from Elastic on search, observability, and security.", + "summary": "Read the latest from Elastic on search, observability, and security.", + "parents": [], + "translated": true, + "section": "blog", + "content_tier": "reference", + "navigation": { + "depth": 2, + "table_of_contents": 100 + } +} diff --git a/tests/Elastic.SiteSearch.Tests/.snapshots/ContentStackMappingTests/ModularPage_Maps_FlatModularBlocks.verified.json b/tests/Elastic.SiteSearch.Tests/.snapshots/ContentStackMappingTests/ModularPage_Maps_FlatModularBlocks.verified.json new file mode 100644 index 0000000000..d15edbcbad --- /dev/null +++ b/tests/Elastic.SiteSearch.Tests/.snapshots/ContentStackMappingTests/ModularPage_Maps_FlatModularBlocks.verified.json @@ -0,0 +1,31 @@ +{ + "$type": "site", + "locale": "en", + "modified_date": "2024-02-10T11:20:00+00:00", + "og": { + "title": "Cloud Workload Protection | Elastic Security", + "description": "Protect cloud workloads with Elastic Security.", + "image": "https://images.contentstack.io/v3/assets/sample/security-og.png" + }, + "twitter": {}, + "http": {}, + "title": "Elastic Security for Cloud Workloads", + "search_title": "Product: Elastic Security for Cloud Workloads", + "content_type": "site", + "path": "/security/cloud-workload-protection", + "url": "/security/cloud-workload-protection", + "hash": "55f67df05646caf7", + "last_updated": "2024-02-10T11:20:00+00:00", + "description": "Protect cloud workloads with Elastic Security.", + "headings": [], + "body": "Elastic Security provides comprehensive protection for your cloud workloads across AWS, Azure, and GCP. Detect threats in real-time with pre-built detection rules and custom analytics powered by machine learning.", + "summary": "Protect cloud workloads with Elastic Security.", + "parents": [], + "translated": true, + "section": "product", + "content_tier": "primary", + "navigation": { + "depth": 3, + "table_of_contents": 100 + } +} diff --git a/tests/Elastic.SiteSearch.Tests/.snapshots/ContentStackMappingTests/ParagraphPage_Maps_SingleBody.verified.json b/tests/Elastic.SiteSearch.Tests/.snapshots/ContentStackMappingTests/ParagraphPage_Maps_SingleBody.verified.json new file mode 100644 index 0000000000..15116a17d3 --- /dev/null +++ b/tests/Elastic.SiteSearch.Tests/.snapshots/ContentStackMappingTests/ParagraphPage_Maps_SingleBody.verified.json @@ -0,0 +1,33 @@ +{ + "$type": "site", + "locale": "en", + "modified_date": "2024-01-15T08:30:00+00:00", + "og": { + "title": "Cloud Terms of Service | Elastic", + "description": "Review the terms of service for Elastic Cloud." + }, + "twitter": {}, + "http": {}, + "title": "Elastic Cloud Terms of Service", + "search_title": "Legal: Elastic Cloud Terms of Service", + "content_type": "site", + "path": "/agreements/cloud-terms-of-service", + "url": "/agreements/cloud-terms-of-service", + "hash": "38ecdbc3e2f4198c", + "last_updated": "2024-01-15T08:30:00+00:00", + "description": "Review the terms of service for Elastic Cloud.", + "headings": [ + "1. Definitions", + "2. Account Registration" + ], + "body": "These Terms of Service govern your use of Elastic Cloud services. 1. Definitions \"Service\" means the Elastic Cloud hosted service provided by Elastic. 2. Account Registration You must register for an account to use the Service. You agree to provide accurate information.", + "summary": "Review the terms of service for Elastic Cloud.", + "parents": [], + "translated": true, + "section": "legal", + "content_tier": "peripheral", + "navigation": { + "depth": 3, + "table_of_contents": 100 + } +} diff --git a/tests/Elastic.SiteSearch.Tests/.snapshots/ContentStackMappingTests/PressRelease_Maps_CombinedParagraphs.verified.json b/tests/Elastic.SiteSearch.Tests/.snapshots/ContentStackMappingTests/PressRelease_Maps_CombinedParagraphs.verified.json new file mode 100644 index 0000000000..4ba4c3fc0f --- /dev/null +++ b/tests/Elastic.SiteSearch.Tests/.snapshots/ContentStackMappingTests/PressRelease_Maps_CombinedParagraphs.verified.json @@ -0,0 +1,28 @@ +{ + "$type": "site", + "locale": "en", + "published_date": "2023-09-01T00:00:00+00:00", + "modified_date": "2023-09-05T10:30:00+00:00", + "og": {}, + "twitter": {}, + "http": {}, + "title": "Elastic Announces New Security Analytics Features", + "search_title": "Press: Elastic Announces New Security Analytics Features", + "content_type": "site", + "path": "/about/press/elastic-announces-new-security-analytics", + "url": "/about/press/elastic-announces-new-security-analytics", + "hash": "1d2f23a5e401ef66", + "last_updated": "2023-09-05T10:30:00+00:00", + "description": null, + "headings": [], + "body": "Elastic today announced new security analytics features designed to help organizations detect and respond to cyber threats more effectively. The new features include advanced threat detection rules, improved investigation workflows, and enhanced integration with cloud security tools. These capabilities build on Elastic Security's unified approach to SIEM, endpoint security, and cloud security.", + "summary": "Elastic today announced new security analytics features designed to help organizations detect and respond to cyber threats more effectively. The new features include advanced threat detection rules, improved investigation workflows, and enhanced integration with cloud security tools. These capabilities build on Elastic Security's unified approach to SIEM, endpoint security, and cloud security.", + "parents": [], + "translated": true, + "section": "press", + "content_tier": "peripheral", + "navigation": { + "depth": 4, + "table_of_contents": 100 + } +} diff --git a/tests/Elastic.SiteSearch.Tests/.snapshots/ContentStackMappingTests/ProductRelease_Maps_ReleaseNotes.verified.json b/tests/Elastic.SiteSearch.Tests/.snapshots/ContentStackMappingTests/ProductRelease_Maps_ReleaseNotes.verified.json new file mode 100644 index 0000000000..afe6061a51 --- /dev/null +++ b/tests/Elastic.SiteSearch.Tests/.snapshots/ContentStackMappingTests/ProductRelease_Maps_ReleaseNotes.verified.json @@ -0,0 +1,31 @@ +{ + "$type": "site", + "locale": "en", + "published_date": "2024-01-10T00:00:00+00:00", + "modified_date": "2024-01-10T12:00:00+00:00", + "og": { + "title": "Download Elasticsearch 8.12.0", + "description": "Download Elasticsearch 8.12.0 for your platform." + }, + "twitter": {}, + "http": {}, + "title": "Elasticsearch 8.12.0", + "search_title": "Download: Elasticsearch 8.12.0", + "content_type": "site", + "path": "/downloads/past-releases/elasticsearch-8-12-0", + "url": "/downloads/past-releases/elasticsearch-8-12-0", + "hash": "f3b0ce116110a2f0", + "last_updated": "2024-01-10T12:00:00+00:00", + "description": "Download Elasticsearch 8.12.0 for your platform.", + "headings": [], + "body": "See the release notes for details.", + "summary": "Download Elasticsearch 8.12.0 for your platform.", + "parents": [], + "translated": true, + "section": "download", + "content_tier": "peripheral", + "navigation": { + "depth": 4, + "table_of_contents": 100 + } +} diff --git a/tests/Elastic.SiteSearch.Tests/.snapshots/ContentStackMappingTests/PropertyHomepage_Maps_PageDescription_NoBody.verified.json b/tests/Elastic.SiteSearch.Tests/.snapshots/ContentStackMappingTests/PropertyHomepage_Maps_PageDescription_NoBody.verified.json new file mode 100644 index 0000000000..d2bf52d1af --- /dev/null +++ b/tests/Elastic.SiteSearch.Tests/.snapshots/ContentStackMappingTests/PropertyHomepage_Maps_PageDescription_NoBody.verified.json @@ -0,0 +1,30 @@ +{ + "$type": "site", + "locale": "en", + "modified_date": "2022-05-02T10:00:00+00:00", + "og": { + "title": "Elastic Security Labs", + "description": "Research, tools, and insights from the Elastic Security Labs team." + }, + "twitter": {}, + "http": {}, + "title": "Security Labs", + "search_title": "Security Labs", + "content_type": "site", + "path": "/security-labs", + "url": "/security-labs", + "hash": "49957c356fce64d9", + "last_updated": "2022-05-02T10:00:00+00:00", + "description": "Research, tools, and insights from the Elastic Security Labs team.", + "headings": [], + "body": "Research, tools, and insights from the Elastic Security Labs team.", + "summary": "Research, tools, and insights from the Elastic Security Labs team.", + "parents": [], + "translated": true, + "section": "security-labs", + "content_tier": "reference", + "navigation": { + "depth": 2, + "table_of_contents": 100 + } +} diff --git a/tests/Elastic.SiteSearch.Tests/.snapshots/ContentStackMappingTests/RichTextPage_Maps_ProseMirrorBodyAndDescription.verified.json b/tests/Elastic.SiteSearch.Tests/.snapshots/ContentStackMappingTests/RichTextPage_Maps_ProseMirrorBodyAndDescription.verified.json new file mode 100644 index 0000000000..f48af0468f --- /dev/null +++ b/tests/Elastic.SiteSearch.Tests/.snapshots/ContentStackMappingTests/RichTextPage_Maps_ProseMirrorBodyAndDescription.verified.json @@ -0,0 +1,32 @@ +{ + "$type": "site", + "locale": "en", + "modified_date": "2026-07-01T00:00:00+00:00", + "og": { + "title": "Amazon Bedrock Integration", + "description": "Connect Amazon Bedrock to Elasticsearch." + }, + "twitter": {}, + "http": {}, + "title": "Amazon Bedrock", + "search_title": "Amazon Bedrock", + "content_type": "site", + "path": "/search-labs/integrations/amazon-bedrock", + "url": "/search-labs/integrations/amazon-bedrock", + "hash": "72b0323fe0a9b693", + "last_updated": "2026-07-01T00:00:00+00:00", + "description": "Amazon Bedrock is a fully managed service offering foundation models from leading AI companies.", + "headings": [ + "Blogs to get started" + ], + "body": "Amazon Bedrock makes leading foundation models available through a single API. Blogs to get started RAG using Bedrock", + "summary": "Amazon Bedrock is a fully managed service offering foundation models from leading AI companies.", + "parents": [], + "translated": true, + "section": "search-labs", + "content_tier": "reference", + "navigation": { + "depth": 4, + "table_of_contents": 100 + } +} diff --git a/tests/Elastic.SiteSearch.Tests/.snapshots/ContentStackMappingTests/TopLevelSubheadingLandingPage_Maps_SubheadingDescription_NoBody.verified.json b/tests/Elastic.SiteSearch.Tests/.snapshots/ContentStackMappingTests/TopLevelSubheadingLandingPage_Maps_SubheadingDescription_NoBody.verified.json new file mode 100644 index 0000000000..2a908e19b5 --- /dev/null +++ b/tests/Elastic.SiteSearch.Tests/.snapshots/ContentStackMappingTests/TopLevelSubheadingLandingPage_Maps_SubheadingDescription_NoBody.verified.json @@ -0,0 +1,30 @@ +{ + "$type": "site", + "locale": "en", + "modified_date": "2023-11-02T10:00:00+00:00", + "og": { + "title": "", + "description": "" + }, + "twitter": {}, + "http": {}, + "title": "Reports", + "search_title": "Reports", + "content_type": "site", + "path": "/security-labs/reports", + "url": "/security-labs/reports", + "hash": "f7cfede52f5f3ab7", + "last_updated": "2023-11-02T10:00:00+00:00", + "description": "In-depth research reports from the Elastic Security Labs team.", + "headings": [], + "body": "In-depth research reports from the Elastic Security Labs team.", + "summary": "In-depth research reports from the Elastic Security Labs team.", + "parents": [], + "translated": true, + "section": "security-labs", + "content_tier": "reference", + "navigation": { + "depth": 3, + "table_of_contents": 100 + } +} diff --git a/tests/Elastic.SiteSearch.Tests/.snapshots/ContentStackMappingTests/UseCase_Maps_IntroAndChallenges.verified.json b/tests/Elastic.SiteSearch.Tests/.snapshots/ContentStackMappingTests/UseCase_Maps_IntroAndChallenges.verified.json new file mode 100644 index 0000000000..6a39e6d4ed --- /dev/null +++ b/tests/Elastic.SiteSearch.Tests/.snapshots/ContentStackMappingTests/UseCase_Maps_IntroAndChallenges.verified.json @@ -0,0 +1,30 @@ +{ + "$type": "site", + "locale": "en", + "modified_date": "2023-08-15T14:30:00+00:00", + "og": { + "title": "Acme Corp Customer Story | Elastic", + "description": "Learn how Acme Corp uses Elastic for real-time analytics." + }, + "twitter": {}, + "http": {}, + "title": "Acme Corp", + "search_title": "Customer Story: Acme Corp", + "content_type": "site", + "path": "/customers/acme-corp", + "url": "/customers/acme-corp", + "hash": "4f1ea839a4d6cdea", + "last_updated": "2023-08-15T14:30:00+00:00", + "description": "Acme Corp processes millions of events per day and needed a scalable solution for real-time analytics and monitoring.", + "headings": [], + "body": "Acme Corp processes millions of events per day and needed a scalable solution for real-time analytics and monitoring. The existing solution could not handle the growing data volume and lacked real-time capabilities. By deploying Elastic, Acme Corp achieved sub-second query times across petabytes of data. The engineering team integrated Elasticsearch into their data pipeline within weeks. Elastic transformed how we approach observability and analytics.", + "summary": "Acme Corp processes millions of events per day and needed a scalable solution for real-time analytics and monitoring.", + "parents": [], + "translated": true, + "section": "customer-story", + "content_tier": "supplementary", + "navigation": { + "depth": 3, + "table_of_contents": 100 + } +} diff --git a/tests/Elastic.SiteSearch.Tests/.snapshots/ContentStackMappingTests/Video_Maps_ParagraphAndPresenter.verified.json b/tests/Elastic.SiteSearch.Tests/.snapshots/ContentStackMappingTests/Video_Maps_ParagraphAndPresenter.verified.json new file mode 100644 index 0000000000..0fd4b40028 --- /dev/null +++ b/tests/Elastic.SiteSearch.Tests/.snapshots/ContentStackMappingTests/Video_Maps_ParagraphAndPresenter.verified.json @@ -0,0 +1,32 @@ +{ + "$type": "site", + "locale": "en", + "published_date": "2023-06-15T17:00:00+00:00", + "modified_date": "2023-07-10T09:15:00+00:00", + "og": { + "title": "Ingesting Data into Elasticsearch | Elastic Webinar", + "description": "Learn about data ingestion patterns with Elasticsearch.", + "image": "https://images.contentstack.io/v3/assets/sample/seo-image.png" + }, + "twitter": {}, + "http": {}, + "title": "Ingesting Data into Elasticsearch", + "search_title": "Webinar: Ingesting Data into Elasticsearch", + "content_type": "site", + "path": "/webinars/ingesting-data-into-elasticsearch", + "url": "/webinars/ingesting-data-into-elasticsearch", + "hash": "4d31971fe7ddc1f9", + "last_updated": "2023-07-10T09:15:00+00:00", + "description": "Learn about data ingestion patterns with Elasticsearch.", + "headings": [], + "body": "Join us for a deep dive into data ingestion patterns with Elasticsearch. We cover Logstash, Beats, and the Elasticsearch ingest pipeline.", + "summary": "Learn about data ingestion patterns with Elasticsearch.", + "parents": [], + "translated": true, + "section": "webinar", + "content_tier": "supplementary", + "navigation": { + "depth": 3, + "table_of_contents": 100 + } +} diff --git a/tests/Elastic.SiteSearch.Tests/.snapshots/MappingStructureTests/DocumentationDocument_MappingJson.verified.json b/tests/Elastic.SiteSearch.Tests/.snapshots/MappingStructureTests/DocumentationDocument_MappingJson.verified.json new file mode 100644 index 0000000000..38cf5b4569 --- /dev/null +++ b/tests/Elastic.SiteSearch.Tests/.snapshots/MappingStructureTests/DocumentationDocument_MappingJson.verified.json @@ -0,0 +1,489 @@ +{ + "properties": { + "api": { + "type": "object", + "properties": { + "key": { + "type": "keyword" + }, + "trees": { + "type": "object", + "properties": { + "tree": { + "type": "keyword" + }, + "root": { + "type": "keyword", + "index": false + } + } + }, + "properties": { + "type": "nested", + "properties": { + "name": { + "type": "keyword" + }, + "path": { + "type": "keyword", + "fields": { + "match": { + "type": "text" + } + } + }, + "path_words": { + "type": "search_as_you_type", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 256 + } + } + }, + "type": { + "type": "keyword" + }, + "location": { + "type": "keyword" + }, + "anchor": { + "type": "keyword" + }, + "description": { + "type": "text", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 256 + } + } + }, + "parent": { + "type": "keyword" + } + } + }, + "routes": { + "type": "object", + "properties": { + "path": { + "type": "keyword", + "index": false + }, + "methods": { + "type": "keyword", + "index": false + } + } + }, + "endpoints": { + "type": "keyword" + }, + "endpoints_bare": { + "type": "keyword" + }, + "method": { + "type": "keyword" + }, + "path": { + "type": "keyword" + }, + "operation_id": { + "type": "keyword" + }, + "words": { + "type": "text", + "fields": { + "completion": { + "type": "search_as_you_type", + "analyzer": "synonyms_fixed_analyzer", + "search_analyzer": "synonyms_analyzer" + } + }, + "analyzer": "synonyms_fixed_analyzer", + "search_analyzer": "synonyms_analyzer" + }, + "group": { + "type": "text", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 256 + } + }, + "analyzer": "synonyms_fixed_analyzer", + "search_analyzer": "synonyms_analyzer" + }, + "id_words": { + "type": "rank_feature", + "positive_score_impact": false + }, + "deprecated": { + "type": "boolean" + } + } + }, + "product": { + "type": "keyword", + "normalizer": "keyword_normalizer" + }, + "related_products": { + "type": "object", + "properties": { + "id": { + "type": "keyword", + "normalizer": "keyword_normalizer" + }, + "repository": { + "type": "keyword", + "normalizer": "keyword_normalizer" + } + } + }, + "applies_to": { + "type": "nested", + "properties": { + "type": { + "type": "keyword", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 256 + } + }, + "normalizer": "keyword_normalizer" + }, + "sub_type": { + "type": "keyword", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 256 + } + }, + "normalizer": "keyword_normalizer" + }, + "lifecycle": { + "type": "keyword", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 256 + } + }, + "normalizer": "keyword_normalizer" + }, + "version": { + "type": "version", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 256 + } + } + } + } + }, + "links": { + "type": "text", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 256 + } + } + }, + "content_hash": { + "type": "keyword" + }, + "content_last_updated": { + "type": "date" + }, + "source_url": { + "type": "keyword" + }, + "title": { + "type": "text", + "fields": { + "keyword": { + "type": "keyword", + "normalizer": "keyword_normalizer" + }, + "starts_with": { + "type": "text", + "analyzer": "starts_with_analyzer", + "search_analyzer": "starts_with_analyzer_search" + }, + "completion": { + "type": "search_as_you_type", + "search_analyzer": "synonyms_analyzer" + } + }, + "search_analyzer": "synonyms_analyzer" + }, + "search_title": { + "type": "text", + "fields": { + "completion": { + "type": "search_as_you_type", + "analyzer": "synonyms_fixed_analyzer", + "search_analyzer": "synonyms_analyzer", + "index_options": "offsets" + } + }, + "analyzer": "synonyms_fixed_analyzer", + "search_analyzer": "synonyms_analyzer" + }, + "content_type": { + "type": "keyword", + "copy_to": "tags" + }, + "path": { + "type": "keyword", + "fields": { + "match": { + "type": "text" + }, + "prefix": { + "type": "text", + "analyzer": "hierarchy_analyzer" + } + } + }, + "url": { + "type": "keyword" + }, + "hash": { + "type": "keyword" + }, + "batch_index_date": { + "type": "date" + }, + "last_updated": { + "type": "date" + }, + "description": { + "type": "text", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 256 + } + } + }, + "headings": { + "type": "text", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 256 + } + }, + "analyzer": "synonyms_fixed_analyzer", + "search_analyzer": "synonyms_analyzer" + }, + "body": { + "type": "text", + "fields": { + "en": { + "type": "text", + "analyzer": "english" + }, + "de": { + "type": "text", + "analyzer": "german" + }, + "fr": { + "type": "text", + "analyzer": "french" + }, + "ja": { + "type": "text", + "analyzer": "cjk" + }, + "ko": { + "type": "text", + "analyzer": "cjk" + }, + "zh": { + "type": "text", + "analyzer": "cjk" + }, + "es": { + "type": "text", + "analyzer": "spanish" + }, + "pt": { + "type": "text", + "analyzer": "portuguese" + } + }, + "analyzer": "synonyms_fixed_analyzer", + "search_analyzer": "synonyms_analyzer", + "term_vector": "with_positions_offsets" + }, + "summary": { + "type": "text", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 256 + } + }, + "analyzer": "synonyms_fixed_analyzer", + "search_analyzer": "synonyms_analyzer" + }, + "parents": { + "type": "object", + "properties": { + "title": { + "type": "text", + "fields": { + "keyword": { + "type": "keyword" + } + }, + "search_analyzer": "synonyms_analyzer" + }, + "path": { + "type": "keyword", + "fields": { + "match": { + "type": "text" + }, + "prefix": { + "type": "text", + "analyzer": "hierarchy_analyzer" + } + } + }, + "url": { + "type": "alias", + "path": "parents.path" + } + } + }, + "hidden": { + "type": "boolean" + }, + "translated": { + "type": "boolean" + }, + "ai_rag_optimized_summary": { + "type": "text", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 256 + } + }, + "analyzer": "synonyms_fixed_analyzer", + "search_analyzer": "synonyms_analyzer" + }, + "ai_short_summary": { + "type": "text", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 256 + } + } + }, + "ai_search_query": { + "type": "keyword", + "fields": { + "completion": { + "type": "search_as_you_type" + } + } + }, + "ai_questions": { + "type": "text", + "fields": { + "completion": { + "type": "search_as_you_type", + "analyzer": "synonyms_fixed_analyzer", + "search_analyzer": "synonyms_analyzer", + "index_options": "offsets" + } + }, + "analyzer": "synonyms_fixed_analyzer", + "search_analyzer": "synonyms_analyzer" + }, + "ai_use_cases": { + "type": "text", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 256 + } + } + }, + "section": { + "type": "keyword", + "normalizer": "keyword_normalizer", + "copy_to": "tags" + }, + "content_tier": { + "type": "keyword" + }, + "navigation": { + "type": "object", + "properties": { + "depth": { + "type": "rank_feature", + "positive_score_impact": false + }, + "table_of_contents": { + "type": "rank_feature", + "positive_score_impact": false + } + } + }, + "ai_autocomplete_questions": { + "type": "text", + "fields": { + "completion": { + "type": "search_as_you_type", + "analyzer": "synonyms_fixed_analyzer", + "search_analyzer": "synonyms_analyzer", + "index_options": "offsets" + }, + "suggest": { + "type": "completion" + } + }, + "analyzer": "synonyms_fixed_analyzer", + "search_analyzer": "synonyms_analyzer" + }, + "tags": { + "type": "text", + "analyzer": "content_tags_analyzer" + }, + "abstract": { + "type": "alias", + "path": "summary" + }, + "navigation_section": { + "type": "alias", + "path": "section" + }, + "content_tags": { + "type": "alias", + "path": "tags" + }, + "stripped_body": { + "type": "alias", + "path": "body" + }, + "navigation_depth": { + "type": "alias", + "path": "navigation.depth" + }, + "navigation_table_of_contents": { + "type": "alias", + "path": "navigation.table_of_contents" + } + } +} diff --git a/tests/Elastic.SiteSearch.Tests/.snapshots/MappingStructureTests/GuideDocument_MappingJson.verified.json b/tests/Elastic.SiteSearch.Tests/.snapshots/MappingStructureTests/GuideDocument_MappingJson.verified.json new file mode 100644 index 0000000000..eeafe72c82 --- /dev/null +++ b/tests/Elastic.SiteSearch.Tests/.snapshots/MappingStructureTests/GuideDocument_MappingJson.verified.json @@ -0,0 +1,312 @@ +{ + "properties": { + "product": { + "type": "keyword" + }, + "version": { + "type": "keyword" + }, + "http": { + "type": "object", + "properties": { + "etag": { + "type": "keyword", + "index": false + }, + "last_modified": { + "type": "date" + } + } + }, + "title": { + "type": "text", + "fields": { + "keyword": { + "type": "keyword", + "normalizer": "keyword_normalizer" + }, + "starts_with": { + "type": "text", + "analyzer": "starts_with_analyzer", + "search_analyzer": "starts_with_analyzer_search" + }, + "completion": { + "type": "search_as_you_type", + "search_analyzer": "synonyms_analyzer" + } + }, + "search_analyzer": "synonyms_analyzer" + }, + "search_title": { + "type": "text", + "fields": { + "completion": { + "type": "search_as_you_type", + "analyzer": "synonyms_fixed_analyzer", + "search_analyzer": "synonyms_analyzer", + "index_options": "offsets" + } + }, + "analyzer": "synonyms_fixed_analyzer", + "search_analyzer": "synonyms_analyzer" + }, + "content_type": { + "type": "keyword", + "copy_to": "tags" + }, + "path": { + "type": "keyword", + "fields": { + "match": { + "type": "text" + }, + "prefix": { + "type": "text", + "analyzer": "hierarchy_analyzer" + } + } + }, + "url": { + "type": "keyword" + }, + "hash": { + "type": "keyword" + }, + "batch_index_date": { + "type": "date" + }, + "last_updated": { + "type": "date" + }, + "description": { + "type": "text", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 256 + } + } + }, + "headings": { + "type": "text", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 256 + } + }, + "analyzer": "synonyms_fixed_analyzer", + "search_analyzer": "synonyms_analyzer" + }, + "body": { + "type": "text", + "fields": { + "en": { + "type": "text", + "analyzer": "english" + }, + "de": { + "type": "text", + "analyzer": "german" + }, + "fr": { + "type": "text", + "analyzer": "french" + }, + "ja": { + "type": "text", + "analyzer": "cjk" + }, + "ko": { + "type": "text", + "analyzer": "cjk" + }, + "zh": { + "type": "text", + "analyzer": "cjk" + }, + "es": { + "type": "text", + "analyzer": "spanish" + }, + "pt": { + "type": "text", + "analyzer": "portuguese" + } + }, + "analyzer": "synonyms_fixed_analyzer", + "search_analyzer": "synonyms_analyzer", + "term_vector": "with_positions_offsets" + }, + "summary": { + "type": "text", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 256 + } + }, + "analyzer": "synonyms_fixed_analyzer", + "search_analyzer": "synonyms_analyzer" + }, + "parents": { + "type": "object", + "properties": { + "title": { + "type": "text", + "fields": { + "keyword": { + "type": "keyword" + } + }, + "search_analyzer": "synonyms_analyzer" + }, + "path": { + "type": "keyword", + "fields": { + "match": { + "type": "text" + }, + "prefix": { + "type": "text", + "analyzer": "hierarchy_analyzer" + } + } + }, + "url": { + "type": "alias", + "path": "parents.path" + } + } + }, + "hidden": { + "type": "boolean" + }, + "translated": { + "type": "boolean" + }, + "ai_rag_optimized_summary": { + "type": "text", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 256 + } + }, + "analyzer": "synonyms_fixed_analyzer", + "search_analyzer": "synonyms_analyzer" + }, + "ai_short_summary": { + "type": "text", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 256 + } + } + }, + "ai_search_query": { + "type": "keyword", + "fields": { + "completion": { + "type": "search_as_you_type" + } + } + }, + "ai_questions": { + "type": "text", + "fields": { + "completion": { + "type": "search_as_you_type", + "analyzer": "synonyms_fixed_analyzer", + "search_analyzer": "synonyms_analyzer", + "index_options": "offsets" + } + }, + "analyzer": "synonyms_fixed_analyzer", + "search_analyzer": "synonyms_analyzer" + }, + "ai_use_cases": { + "type": "text", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 256 + } + } + }, + "section": { + "type": "keyword", + "normalizer": "keyword_normalizer", + "copy_to": "tags" + }, + "content_tier": { + "type": "keyword" + }, + "navigation": { + "type": "object", + "properties": { + "depth": { + "type": "rank_feature", + "positive_score_impact": false + }, + "table_of_contents": { + "type": "rank_feature", + "positive_score_impact": false + } + } + }, + "ai_autocomplete_questions": { + "type": "text", + "fields": { + "completion": { + "type": "search_as_you_type", + "analyzer": "synonyms_fixed_analyzer", + "search_analyzer": "synonyms_analyzer", + "index_options": "offsets" + }, + "suggest": { + "type": "completion" + } + }, + "analyzer": "synonyms_fixed_analyzer", + "search_analyzer": "synonyms_analyzer" + }, + "tags": { + "type": "text", + "analyzer": "content_tags_analyzer" + }, + "abstract": { + "type": "alias", + "path": "summary" + }, + "navigation_section": { + "type": "alias", + "path": "section" + }, + "content_tags": { + "type": "alias", + "path": "tags" + }, + "stripped_body": { + "type": "alias", + "path": "body" + }, + "navigation_depth": { + "type": "alias", + "path": "navigation.depth" + }, + "navigation_table_of_contents": { + "type": "alias", + "path": "navigation.table_of_contents" + }, + "http_etag": { + "type": "alias", + "path": "http.etag" + }, + "http_last_modified": { + "type": "alias", + "path": "http.last_modified" + } + } +} diff --git a/tests/Elastic.SiteSearch.Tests/.snapshots/MappingStructureTests/SiteDocumentSemantic_MappingJson.verified.json b/tests/Elastic.SiteSearch.Tests/.snapshots/MappingStructureTests/SiteDocumentSemantic_MappingJson.verified.json new file mode 100644 index 0000000000..021a52ba40 --- /dev/null +++ b/tests/Elastic.SiteSearch.Tests/.snapshots/MappingStructureTests/SiteDocumentSemantic_MappingJson.verified.json @@ -0,0 +1,396 @@ +{ + "properties": { + "locale": { + "type": "keyword" + }, + "author": { + "type": "keyword" + }, + "published_date": { + "type": "date" + }, + "modified_date": { + "type": "date" + }, + "og": { + "type": "object", + "properties": { + "title": { + "type": "text", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 256 + } + } + }, + "description": { + "type": "text", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 256 + } + } + }, + "image": { + "type": "keyword", + "index": false + } + } + }, + "twitter": { + "type": "object", + "properties": { + "image": { + "type": "keyword", + "index": false + }, + "card": { + "type": "keyword" + } + } + }, + "http": { + "type": "object", + "properties": { + "etag": { + "type": "keyword", + "index": false + }, + "last_modified": { + "type": "date" + } + } + }, + "title": { + "type": "text", + "fields": { + "keyword": { + "type": "keyword", + "normalizer": "keyword_normalizer" + }, + "starts_with": { + "type": "text", + "analyzer": "starts_with_analyzer", + "search_analyzer": "starts_with_analyzer_search" + }, + "completion": { + "type": "search_as_you_type", + "search_analyzer": "synonyms_analyzer" + }, + "semantic_text": { + "type": "semantic_text" + } + }, + "search_analyzer": "synonyms_analyzer" + }, + "search_title": { + "type": "text", + "fields": { + "completion": { + "type": "search_as_you_type", + "analyzer": "synonyms_fixed_analyzer", + "search_analyzer": "synonyms_analyzer", + "index_options": "offsets" + } + }, + "analyzer": "synonyms_fixed_analyzer", + "search_analyzer": "synonyms_analyzer" + }, + "content_type": { + "type": "keyword", + "copy_to": "tags" + }, + "path": { + "type": "keyword", + "fields": { + "match": { + "type": "text" + }, + "prefix": { + "type": "text", + "analyzer": "hierarchy_analyzer" + } + } + }, + "url": { + "type": "keyword" + }, + "hash": { + "type": "keyword" + }, + "batch_index_date": { + "type": "date" + }, + "last_updated": { + "type": "date" + }, + "description": { + "type": "text", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 256 + } + } + }, + "headings": { + "type": "text", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 256 + } + }, + "analyzer": "synonyms_fixed_analyzer", + "search_analyzer": "synonyms_analyzer" + }, + "body": { + "type": "text", + "fields": { + "en": { + "type": "text", + "analyzer": "english" + }, + "de": { + "type": "text", + "analyzer": "german" + }, + "fr": { + "type": "text", + "analyzer": "french" + }, + "ja": { + "type": "text", + "analyzer": "cjk" + }, + "ko": { + "type": "text", + "analyzer": "cjk" + }, + "zh": { + "type": "text", + "analyzer": "cjk" + }, + "es": { + "type": "text", + "analyzer": "spanish" + }, + "pt": { + "type": "text", + "analyzer": "portuguese" + }, + "semantic_text": { + "type": "semantic_text" + } + }, + "analyzer": "synonyms_fixed_analyzer", + "search_analyzer": "synonyms_analyzer", + "term_vector": "with_positions_offsets" + }, + "summary": { + "type": "text", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 256 + }, + "semantic_text": { + "type": "semantic_text" + } + }, + "analyzer": "synonyms_fixed_analyzer", + "search_analyzer": "synonyms_analyzer" + }, + "parents": { + "type": "object", + "properties": { + "title": { + "type": "text", + "fields": { + "keyword": { + "type": "keyword" + } + }, + "search_analyzer": "synonyms_analyzer" + }, + "path": { + "type": "keyword", + "fields": { + "match": { + "type": "text" + }, + "prefix": { + "type": "text", + "analyzer": "hierarchy_analyzer" + } + } + }, + "url": { + "type": "alias", + "path": "parents.path" + } + } + }, + "hidden": { + "type": "boolean" + }, + "translated": { + "type": "boolean" + }, + "ai_rag_optimized_summary": { + "type": "text", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 256 + }, + "semantic_text": { + "type": "semantic_text" + } + }, + "analyzer": "synonyms_fixed_analyzer", + "search_analyzer": "synonyms_analyzer" + }, + "ai_short_summary": { + "type": "text", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 256 + } + } + }, + "ai_search_query": { + "type": "keyword", + "fields": { + "completion": { + "type": "search_as_you_type" + } + } + }, + "ai_questions": { + "type": "text", + "fields": { + "completion": { + "type": "search_as_you_type", + "analyzer": "synonyms_fixed_analyzer", + "search_analyzer": "synonyms_analyzer", + "index_options": "offsets" + }, + "semantic_text": { + "type": "semantic_text" + } + }, + "analyzer": "synonyms_fixed_analyzer", + "search_analyzer": "synonyms_analyzer" + }, + "ai_use_cases": { + "type": "text", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 256 + } + } + }, + "section": { + "type": "keyword", + "normalizer": "keyword_normalizer", + "copy_to": "tags" + }, + "content_tier": { + "type": "keyword" + }, + "navigation": { + "type": "object", + "properties": { + "depth": { + "type": "rank_feature", + "positive_score_impact": false + }, + "table_of_contents": { + "type": "rank_feature", + "positive_score_impact": false + } + } + }, + "ai_autocomplete_questions": { + "type": "text", + "fields": { + "completion": { + "type": "search_as_you_type", + "analyzer": "synonyms_fixed_analyzer", + "search_analyzer": "synonyms_analyzer", + "index_options": "offsets" + }, + "suggest": { + "type": "completion" + } + }, + "analyzer": "synonyms_fixed_analyzer", + "search_analyzer": "synonyms_analyzer" + }, + "tags": { + "type": "text", + "analyzer": "content_tags_analyzer" + }, + "abstract": { + "type": "alias", + "path": "summary" + }, + "navigation_section": { + "type": "alias", + "path": "section" + }, + "content_tags": { + "type": "alias", + "path": "tags" + }, + "stripped_body": { + "type": "alias", + "path": "body" + }, + "navigation_depth": { + "type": "alias", + "path": "navigation.depth" + }, + "navigation_table_of_contents": { + "type": "alias", + "path": "navigation.table_of_contents" + }, + "language": { + "type": "alias", + "path": "locale" + }, + "og_title": { + "type": "alias", + "path": "og.title" + }, + "og_description": { + "type": "alias", + "path": "og.description" + }, + "og_image": { + "type": "alias", + "path": "og.image" + }, + "twitter_image": { + "type": "alias", + "path": "twitter.image" + }, + "twitter_card": { + "type": "alias", + "path": "twitter.card" + }, + "http_etag": { + "type": "alias", + "path": "http.etag" + }, + "http_last_modified": { + "type": "alias", + "path": "http.last_modified" + } + } +} diff --git a/tests/Elastic.SiteSearch.Tests/.snapshots/MappingStructureTests/SiteDocument_MappingJson.verified.json b/tests/Elastic.SiteSearch.Tests/.snapshots/MappingStructureTests/SiteDocument_MappingJson.verified.json new file mode 100644 index 0000000000..07b68b390b --- /dev/null +++ b/tests/Elastic.SiteSearch.Tests/.snapshots/MappingStructureTests/SiteDocument_MappingJson.verified.json @@ -0,0 +1,381 @@ +{ + "properties": { + "locale": { + "type": "keyword" + }, + "author": { + "type": "keyword" + }, + "published_date": { + "type": "date" + }, + "modified_date": { + "type": "date" + }, + "og": { + "type": "object", + "properties": { + "title": { + "type": "text", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 256 + } + } + }, + "description": { + "type": "text", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 256 + } + } + }, + "image": { + "type": "keyword", + "index": false + } + } + }, + "twitter": { + "type": "object", + "properties": { + "image": { + "type": "keyword", + "index": false + }, + "card": { + "type": "keyword" + } + } + }, + "http": { + "type": "object", + "properties": { + "etag": { + "type": "keyword", + "index": false + }, + "last_modified": { + "type": "date" + } + } + }, + "title": { + "type": "text", + "fields": { + "keyword": { + "type": "keyword", + "normalizer": "keyword_normalizer" + }, + "starts_with": { + "type": "text", + "analyzer": "starts_with_analyzer", + "search_analyzer": "starts_with_analyzer_search" + }, + "completion": { + "type": "search_as_you_type", + "search_analyzer": "synonyms_analyzer" + } + }, + "search_analyzer": "synonyms_analyzer" + }, + "search_title": { + "type": "text", + "fields": { + "completion": { + "type": "search_as_you_type", + "analyzer": "synonyms_fixed_analyzer", + "search_analyzer": "synonyms_analyzer", + "index_options": "offsets" + } + }, + "analyzer": "synonyms_fixed_analyzer", + "search_analyzer": "synonyms_analyzer" + }, + "content_type": { + "type": "keyword", + "copy_to": "tags" + }, + "path": { + "type": "keyword", + "fields": { + "match": { + "type": "text" + }, + "prefix": { + "type": "text", + "analyzer": "hierarchy_analyzer" + } + } + }, + "url": { + "type": "keyword" + }, + "hash": { + "type": "keyword" + }, + "batch_index_date": { + "type": "date" + }, + "last_updated": { + "type": "date" + }, + "description": { + "type": "text", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 256 + } + } + }, + "headings": { + "type": "text", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 256 + } + }, + "analyzer": "synonyms_fixed_analyzer", + "search_analyzer": "synonyms_analyzer" + }, + "body": { + "type": "text", + "fields": { + "en": { + "type": "text", + "analyzer": "english" + }, + "de": { + "type": "text", + "analyzer": "german" + }, + "fr": { + "type": "text", + "analyzer": "french" + }, + "ja": { + "type": "text", + "analyzer": "cjk" + }, + "ko": { + "type": "text", + "analyzer": "cjk" + }, + "zh": { + "type": "text", + "analyzer": "cjk" + }, + "es": { + "type": "text", + "analyzer": "spanish" + }, + "pt": { + "type": "text", + "analyzer": "portuguese" + } + }, + "analyzer": "synonyms_fixed_analyzer", + "search_analyzer": "synonyms_analyzer", + "term_vector": "with_positions_offsets" + }, + "summary": { + "type": "text", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 256 + } + }, + "analyzer": "synonyms_fixed_analyzer", + "search_analyzer": "synonyms_analyzer" + }, + "parents": { + "type": "object", + "properties": { + "title": { + "type": "text", + "fields": { + "keyword": { + "type": "keyword" + } + }, + "search_analyzer": "synonyms_analyzer" + }, + "path": { + "type": "keyword", + "fields": { + "match": { + "type": "text" + }, + "prefix": { + "type": "text", + "analyzer": "hierarchy_analyzer" + } + } + }, + "url": { + "type": "alias", + "path": "parents.path" + } + } + }, + "hidden": { + "type": "boolean" + }, + "translated": { + "type": "boolean" + }, + "ai_rag_optimized_summary": { + "type": "text", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 256 + } + }, + "analyzer": "synonyms_fixed_analyzer", + "search_analyzer": "synonyms_analyzer" + }, + "ai_short_summary": { + "type": "text", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 256 + } + } + }, + "ai_search_query": { + "type": "keyword", + "fields": { + "completion": { + "type": "search_as_you_type" + } + } + }, + "ai_questions": { + "type": "text", + "fields": { + "completion": { + "type": "search_as_you_type", + "analyzer": "synonyms_fixed_analyzer", + "search_analyzer": "synonyms_analyzer", + "index_options": "offsets" + } + }, + "analyzer": "synonyms_fixed_analyzer", + "search_analyzer": "synonyms_analyzer" + }, + "ai_use_cases": { + "type": "text", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 256 + } + } + }, + "section": { + "type": "keyword", + "normalizer": "keyword_normalizer", + "copy_to": "tags" + }, + "content_tier": { + "type": "keyword" + }, + "navigation": { + "type": "object", + "properties": { + "depth": { + "type": "rank_feature", + "positive_score_impact": false + }, + "table_of_contents": { + "type": "rank_feature", + "positive_score_impact": false + } + } + }, + "ai_autocomplete_questions": { + "type": "text", + "fields": { + "completion": { + "type": "search_as_you_type", + "analyzer": "synonyms_fixed_analyzer", + "search_analyzer": "synonyms_analyzer", + "index_options": "offsets" + }, + "suggest": { + "type": "completion" + } + }, + "analyzer": "synonyms_fixed_analyzer", + "search_analyzer": "synonyms_analyzer" + }, + "tags": { + "type": "text", + "analyzer": "content_tags_analyzer" + }, + "abstract": { + "type": "alias", + "path": "summary" + }, + "navigation_section": { + "type": "alias", + "path": "section" + }, + "content_tags": { + "type": "alias", + "path": "tags" + }, + "stripped_body": { + "type": "alias", + "path": "body" + }, + "navigation_depth": { + "type": "alias", + "path": "navigation.depth" + }, + "navigation_table_of_contents": { + "type": "alias", + "path": "navigation.table_of_contents" + }, + "language": { + "type": "alias", + "path": "locale" + }, + "og_title": { + "type": "alias", + "path": "og.title" + }, + "og_description": { + "type": "alias", + "path": "og.description" + }, + "og_image": { + "type": "alias", + "path": "og.image" + }, + "twitter_image": { + "type": "alias", + "path": "twitter.image" + }, + "twitter_card": { + "type": "alias", + "path": "twitter.card" + }, + "http_etag": { + "type": "alias", + "path": "http.etag" + }, + "http_last_modified": { + "type": "alias", + "path": "http.last_modified" + } + } +} diff --git a/tests/Elastic.SiteSearch.Tests/.snapshots/MappingStructureTests/WebsiteSearchDocument_MappingJson.verified.json b/tests/Elastic.SiteSearch.Tests/.snapshots/MappingStructureTests/WebsiteSearchDocument_MappingJson.verified.json new file mode 100644 index 0000000000..1130a577ac --- /dev/null +++ b/tests/Elastic.SiteSearch.Tests/.snapshots/MappingStructureTests/WebsiteSearchDocument_MappingJson.verified.json @@ -0,0 +1,584 @@ +{ + "properties": { + "locale": { + "type": "keyword" + }, + "author": { + "type": "keyword" + }, + "published_date": { + "type": "date" + }, + "modified_date": { + "type": "date" + }, + "og": { + "type": "object", + "properties": { + "title": { + "type": "text", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 256 + } + } + }, + "description": { + "type": "text", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 256 + } + } + }, + "image": { + "type": "keyword", + "index": false + } + } + }, + "twitter": { + "type": "object", + "properties": { + "image": { + "type": "keyword", + "index": false + }, + "card": { + "type": "keyword" + } + } + }, + "http": { + "type": "object", + "properties": { + "etag": { + "type": "keyword", + "index": false + }, + "last_modified": { + "type": "date" + } + } + }, + "title": { + "type": "text", + "fields": { + "keyword": { + "type": "keyword", + "normalizer": "keyword_normalizer" + }, + "starts_with": { + "type": "text", + "analyzer": "starts_with_analyzer", + "search_analyzer": "starts_with_analyzer_search" + }, + "completion": { + "type": "search_as_you_type", + "search_analyzer": "synonyms_analyzer" + } + }, + "search_analyzer": "synonyms_analyzer" + }, + "search_title": { + "type": "text", + "fields": { + "completion": { + "type": "search_as_you_type", + "analyzer": "synonyms_fixed_analyzer", + "search_analyzer": "synonyms_analyzer", + "index_options": "offsets" + } + }, + "analyzer": "synonyms_fixed_analyzer", + "search_analyzer": "synonyms_analyzer" + }, + "content_type": { + "type": "keyword", + "copy_to": "tags" + }, + "path": { + "type": "keyword", + "fields": { + "match": { + "type": "text" + }, + "prefix": { + "type": "text", + "analyzer": "hierarchy_analyzer" + } + } + }, + "url": { + "type": "keyword" + }, + "hash": { + "type": "keyword" + }, + "batch_index_date": { + "type": "date" + }, + "last_updated": { + "type": "date" + }, + "description": { + "type": "text", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 256 + } + } + }, + "headings": { + "type": "text", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 256 + } + }, + "analyzer": "synonyms_fixed_analyzer", + "search_analyzer": "synonyms_analyzer" + }, + "body": { + "type": "text", + "fields": { + "en": { + "type": "text", + "analyzer": "english" + }, + "de": { + "type": "text", + "analyzer": "german" + }, + "fr": { + "type": "text", + "analyzer": "french" + }, + "ja": { + "type": "text", + "analyzer": "cjk" + }, + "ko": { + "type": "text", + "analyzer": "cjk" + }, + "zh": { + "type": "text", + "analyzer": "cjk" + }, + "es": { + "type": "text", + "analyzer": "spanish" + }, + "pt": { + "type": "text", + "analyzer": "portuguese" + } + }, + "analyzer": "synonyms_fixed_analyzer", + "search_analyzer": "synonyms_analyzer", + "term_vector": "with_positions_offsets" + }, + "summary": { + "type": "text", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 256 + } + }, + "analyzer": "synonyms_fixed_analyzer", + "search_analyzer": "synonyms_analyzer" + }, + "parents": { + "type": "object", + "properties": { + "title": { + "type": "text", + "fields": { + "keyword": { + "type": "keyword" + } + }, + "search_analyzer": "synonyms_analyzer" + }, + "path": { + "type": "keyword", + "fields": { + "match": { + "type": "text" + }, + "prefix": { + "type": "text", + "analyzer": "hierarchy_analyzer" + } + } + }, + "url": { + "type": "alias", + "path": "parents.path" + } + } + }, + "hidden": { + "type": "boolean" + }, + "translated": { + "type": "boolean" + }, + "ai_rag_optimized_summary": { + "type": "text", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 256 + } + }, + "analyzer": "synonyms_fixed_analyzer", + "search_analyzer": "synonyms_analyzer" + }, + "ai_short_summary": { + "type": "text", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 256 + } + } + }, + "ai_search_query": { + "type": "keyword", + "fields": { + "completion": { + "type": "search_as_you_type" + } + } + }, + "ai_questions": { + "type": "text", + "fields": { + "completion": { + "type": "search_as_you_type", + "analyzer": "synonyms_fixed_analyzer", + "search_analyzer": "synonyms_analyzer", + "index_options": "offsets" + } + }, + "analyzer": "synonyms_fixed_analyzer", + "search_analyzer": "synonyms_analyzer" + }, + "ai_use_cases": { + "type": "text", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 256 + } + } + }, + "section": { + "type": "keyword", + "normalizer": "keyword_normalizer", + "copy_to": "tags" + }, + "content_tier": { + "type": "keyword" + }, + "navigation": { + "type": "object", + "properties": { + "depth": { + "type": "rank_feature", + "positive_score_impact": false + }, + "table_of_contents": { + "type": "rank_feature", + "positive_score_impact": false + } + } + }, + "ai_autocomplete_questions": { + "type": "text", + "fields": { + "completion": { + "type": "search_as_you_type", + "analyzer": "synonyms_fixed_analyzer", + "search_analyzer": "synonyms_analyzer", + "index_options": "offsets" + }, + "suggest": { + "type": "completion" + } + }, + "analyzer": "synonyms_fixed_analyzer", + "search_analyzer": "synonyms_analyzer" + }, + "tags": { + "type": "text", + "analyzer": "content_tags_analyzer" + }, + "abstract": { + "type": "alias", + "path": "summary" + }, + "navigation_section": { + "type": "alias", + "path": "section" + }, + "content_tags": { + "type": "alias", + "path": "tags" + }, + "stripped_body": { + "type": "alias", + "path": "body" + }, + "navigation_depth": { + "type": "alias", + "path": "navigation.depth" + }, + "navigation_table_of_contents": { + "type": "alias", + "path": "navigation.table_of_contents" + }, + "language": { + "type": "alias", + "path": "locale" + }, + "og_title": { + "type": "alias", + "path": "og.title" + }, + "og_description": { + "type": "alias", + "path": "og.description" + }, + "og_image": { + "type": "alias", + "path": "og.image" + }, + "twitter_image": { + "type": "alias", + "path": "twitter.image" + }, + "twitter_card": { + "type": "alias", + "path": "twitter.card" + }, + "http_etag": { + "type": "alias", + "path": "http.etag" + }, + "http_last_modified": { + "type": "alias", + "path": "http.last_modified" + }, + "api": { + "type": "object", + "properties": { + "key": { + "type": "keyword" + }, + "trees": { + "type": "object", + "properties": { + "tree": { + "type": "keyword" + }, + "root": { + "type": "keyword", + "index": false + } + } + }, + "properties": { + "type": "nested", + "properties": { + "name": { + "type": "keyword" + }, + "path": { + "type": "keyword", + "fields": { + "match": { + "type": "text" + } + } + }, + "path_words": { + "type": "search_as_you_type", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 256 + } + } + }, + "type": { + "type": "keyword" + }, + "location": { + "type": "keyword" + }, + "anchor": { + "type": "keyword" + }, + "description": { + "type": "text", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 256 + } + } + }, + "parent": { + "type": "keyword" + } + } + }, + "routes": { + "type": "object", + "properties": { + "path": { + "type": "keyword", + "index": false + }, + "methods": { + "type": "keyword", + "index": false + } + } + }, + "endpoints": { + "type": "keyword" + }, + "endpoints_bare": { + "type": "keyword" + }, + "method": { + "type": "keyword" + }, + "path": { + "type": "keyword" + }, + "operation_id": { + "type": "keyword" + }, + "words": { + "type": "text", + "fields": { + "completion": { + "type": "search_as_you_type", + "analyzer": "synonyms_fixed_analyzer", + "search_analyzer": "synonyms_analyzer" + } + }, + "analyzer": "synonyms_fixed_analyzer", + "search_analyzer": "synonyms_analyzer" + }, + "group": { + "type": "text", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 256 + } + }, + "analyzer": "synonyms_fixed_analyzer", + "search_analyzer": "synonyms_analyzer" + }, + "id_words": { + "type": "rank_feature", + "positive_score_impact": false + }, + "deprecated": { + "type": "boolean" + } + } + }, + "product": { + "type": "keyword", + "normalizer": "keyword_normalizer" + }, + "related_products": { + "type": "object", + "properties": { + "id": { + "type": "keyword", + "normalizer": "keyword_normalizer" + }, + "repository": { + "type": "keyword", + "normalizer": "keyword_normalizer" + } + } + }, + "applies_to": { + "type": "nested", + "properties": { + "type": { + "type": "keyword", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 256 + } + }, + "normalizer": "keyword_normalizer" + }, + "sub_type": { + "type": "keyword", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 256 + } + }, + "normalizer": "keyword_normalizer" + }, + "lifecycle": { + "type": "keyword", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 256 + } + }, + "normalizer": "keyword_normalizer" + }, + "version": { + "type": "version", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 256 + } + } + } + } + }, + "links": { + "type": "text", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 256 + } + } + }, + "content_hash": { + "type": "keyword" + }, + "content_last_updated": { + "type": "date" + }, + "source_url": { + "type": "keyword" + } + } +} diff --git a/tests/Elastic.SiteSearch.Tests/ContentStackMappingTests.cs b/tests/Elastic.SiteSearch.Tests/ContentStackMappingTests.cs index b189c17a98..edfb3a655e 100644 --- a/tests/Elastic.SiteSearch.Tests/ContentStackMappingTests.cs +++ b/tests/Elastic.SiteSearch.Tests/ContentStackMappingTests.cs @@ -3,8 +3,11 @@ // See the LICENSE file in the project root for more information using System.Text.Json; +using System.Text.Json.Nodes; using AwesomeAssertions; +using Elastic.Documentation.Search.Contract; using Elastic.SiteSearch.Cli.ContentStack; +using Nullean.Snapshots.Json; namespace Elastic.SiteSearch.Tests; @@ -14,6 +17,18 @@ namespace Elastic.SiteSearch.Tests; /// public class ContentStackMappingTests { + private static readonly JsonSerializerOptions DocumentOptions = new() { TypeInfoResolver = SourceGenerationContext.Default }; + + /// The mapped document as JSON, without the fields stamped with the current time (index date, and last_updated when the entry has no modified date). + private static string ToJson(SiteDocument doc) + { + var node = JsonNode.Parse(JsonSerializer.Serialize(doc, DocumentOptions))!.AsObject(); + _ = node.Remove("batch_index_date"); + if (doc.ModifiedDate is null) + _ = node.Remove("last_updated"); + return node.ToJsonString(); + } + private static SyncItem LoadFixture(string fixtureName, string contentTypeUid) { var path = Path.Combine("Fixtures", "ContentStack", fixtureName); @@ -30,21 +45,7 @@ public void BlogLegacy_Maps_BodyAndMetadata() var doc = ContentStackMapper.ToSiteDocument(item); doc.Should().NotBeNull(); - doc.Path.Should().Be("/blog/getting-started-with-kibana-dashboards"); - doc.Title.Should().Be("Getting Started with Kibana Dashboards"); - doc.SearchTitle.Should().StartWith("Blog:"); - doc.Section.Should().Be("blog"); - doc.Locale.Should().Be("en"); - doc.Body.Should().Contain("Kibana is a powerful visualization tool"); - doc.Body.Should().NotContain("

"); - doc.Body.Should().Be(doc.Body); - doc.Headings.Should().Contain("Creating Your First Dashboard"); - doc.Description.Should().Contain("Learn how to create interactive dashboards"); - doc.PublishedDate.Should().NotBeNull(); - doc.ModifiedDate.Should().NotBeNull(); - doc.Og.Title.Should().Contain("Kibana Dashboards"); - doc.Og.Description.Should().NotBeNullOrEmpty(); - doc.Hash.Should().NotBeNullOrEmpty(); + ToJson(doc!).Should().MatchJsonSnapshot(); } ///

Covers: blog_v2 (modular blocks with nested title_text arrays) @@ -55,16 +56,7 @@ public void BlogModern_Maps_ModularBlocksBody() var doc = ContentStackMapper.ToSiteDocument(item); doc.Should().NotBeNull(); - doc.Path.Should().Be("/blog/building-search-applications-with-elasticsearch"); - doc.Title.Should().Be("Building Search Applications with Elasticsearch"); - doc.SearchTitle.Should().StartWith("Blog:"); - doc.Section.Should().Be("blog"); - doc.Body.Should().Contain("Search is at the heart of modern applications"); - doc.Body.Should().Contain("Query DSL"); - doc.Body.Should().NotContain("

"); - doc.Description.Should().Contain("Explore how to build production-ready"); - doc.PublishedDate.Should().NotBeNull(); - doc.Hash.Should().NotBeNullOrEmpty(); + ToJson(doc!).Should().MatchJsonSnapshot(); } ///

Covers: videos (paragraph_l10n body, presentation_date) @@ -75,15 +67,7 @@ public void Video_Maps_ParagraphAndPresenter() var doc = ContentStackMapper.ToSiteDocument(item); doc.Should().NotBeNull(); - doc.Path.Should().Be("/webinars/ingesting-data-into-elasticsearch"); - doc.Title.Should().Be("Ingesting Data into Elasticsearch"); - doc.SearchTitle.Should().StartWith("Webinar:"); - doc.Section.Should().Be("webinar"); - doc.Body.Should().Contain("data ingestion patterns"); - doc.Body.Should().NotContain("

"); - doc.PublishedDate.Should().NotBeNull(); - doc.Og.Image.Should().NotBeNullOrEmpty(); - doc.Hash.Should().NotBeNullOrEmpty(); + ToJson(doc!).Should().MatchJsonSnapshot(); } ///

Covers: press (intro_paragraph_l10n + paragraph_l10n, date field) @@ -94,15 +78,7 @@ public void PressRelease_Maps_CombinedParagraphs() var doc = ContentStackMapper.ToSiteDocument(item); doc.Should().NotBeNull(); - doc.Path.Should().Contain("/about/press/"); - doc.Title.Should().Contain("Security Analytics"); - doc.SearchTitle.Should().StartWith("Press:"); - doc.Section.Should().Be("press"); - doc.Body.Should().Contain("Elastic today announced"); - doc.Body.Should().Contain("advanced threat detection"); - doc.PublishedDate.Should().NotBeNull(); - doc.PublishedDate.Value.Year.Should().Be(2023); - doc.Hash.Should().NotBeNullOrEmpty(); + ToJson(doc!).Should().MatchJsonSnapshot(); } /// Covers: product_versions (release_notes body, date, version_number) @@ -113,14 +89,7 @@ public void ProductRelease_Maps_ReleaseNotes() var doc = ContentStackMapper.ToSiteDocument(item); doc.Should().NotBeNull(); - doc.Path.Should().Contain("/downloads/past-releases/"); - doc.Title.Should().Contain("Elasticsearch 8.12.0"); - doc.SearchTitle.Should().StartWith("Download:"); - doc.Section.Should().Be("download"); - doc.Body.Should().Contain("release notes"); - doc.PublishedDate.Should().NotBeNull(); - doc.Og.Title.Should().Contain("Download"); - doc.Hash.Should().NotBeNullOrEmpty(); + ToJson(doc!).Should().MatchJsonSnapshot(); } /// Covers: agreements, forms (paragraph_l10n as single body) @@ -131,15 +100,7 @@ public void ParagraphPage_Maps_SingleBody() var doc = ContentStackMapper.ToSiteDocument(item); doc.Should().NotBeNull(); - doc.Path.Should().Contain("/agreements/"); - doc.Title.Should().Contain("Terms of Service"); - doc.SearchTitle.Should().StartWith("Legal:"); - doc.Section.Should().Be("legal"); - doc.Body.Should().Contain("Terms of Service govern"); - doc.Body.Should().NotContain("

"); - doc.Headings.Should().Contain("1. Definitions"); - doc.Og.Title.Should().NotBeNullOrEmpty(); - doc.Hash.Should().NotBeNullOrEmpty(); + ToJson(doc!).Should().MatchJsonSnapshot(); } ///

Covers: default_detail, account_based_marketing, product_detail (modular_blocks with flat title_text) @@ -150,15 +111,7 @@ public void ModularPage_Maps_FlatModularBlocks() var doc = ContentStackMapper.ToSiteDocument(item); doc.Should().NotBeNull(); - doc.Path.Should().Be("/security/cloud-workload-protection"); - doc.Title.Should().Contain("Cloud Workloads"); - doc.SearchTitle.Should().StartWith("Product:"); - doc.Section.Should().Be("product"); - doc.Body.Should().Contain("comprehensive protection"); - doc.Body.Should().Contain("pre-built detection rules"); - doc.Body.Should().NotContain("

"); - doc.Og.Image.Should().NotBeNullOrEmpty(); - doc.Hash.Should().NotBeNullOrEmpty(); + ToJson(doc!).Should().MatchJsonSnapshot(); } ///

Covers: use_cases (introduction + challenge_solution + modular_blocks) @@ -169,15 +122,7 @@ public void UseCase_Maps_IntroAndChallenges() var doc = ContentStackMapper.ToSiteDocument(item); doc.Should().NotBeNull(); - doc.Path.Should().Be("/customers/acme-corp"); - doc.Title.Should().Be("Acme Corp"); - doc.SearchTitle.Should().StartWith("Customer Story:"); - doc.Section.Should().Be("customer-story"); - doc.Body.Should().Contain("millions of events per day"); - doc.Body.Should().Contain("could not handle the growing data volume"); - doc.Body.Should().Contain("sub-second query times"); - doc.Description.Should().Contain("scalable solution"); - doc.Hash.Should().NotBeNullOrEmpty(); + ToJson(doc!).Should().MatchJsonSnapshot(); } /// Covers: faq (topic[].subtopic[].paragraph_l10n) @@ -188,14 +133,7 @@ public void FaqPage_Maps_NestedTopics() var doc = ContentStackMapper.ToSiteDocument(item); doc.Should().NotBeNull(); - doc.Path.Should().Be("/what-is/cloud-security"); - doc.Title.Should().Contain("Cloud Security"); - doc.SearchTitle.Should().StartWith("What is:"); - doc.Section.Should().Be("concept"); - doc.Body.Should().Contain("technologies, policies, and controls"); - doc.Body.Should().Contain("organizations migrate workloads"); - doc.Body.Should().NotContain("

"); - doc.Hash.Should().NotBeNullOrEmpty(); + ToJson(doc!).Should().MatchJsonSnapshot(); } ///

Covers: customer_tile (short paragraph_l10n, partial URL coverage) @@ -206,12 +144,8 @@ public void CustomerTile_Maps_ShortContent() var doc = ContentStackMapper.ToSiteDocument(item); doc.Should().NotBeNull(); - doc.Path.Should().Contain("/kr/videos/"); - doc.Title.Should().Be("TechCo Industries"); // URL prefix wins over the entry's own (en-us) locale — see GetLanguageFromUrl. - doc.Locale.Should().Be("ko"); - doc.Body.Should().Contain("global e-commerce platform"); - doc.Hash.Should().NotBeNullOrEmpty(); + ToJson(doc!).Should().MatchJsonSnapshot(); } /// @@ -230,19 +164,9 @@ public void MinimalPage_Maps_TitleUrlSeo() var doc = ContentStackMapper.ToSiteDocument(item); doc.Should().NotBeNull(); - doc.Path.Should().Be("/blog"); - doc.Title.Should().Be("Elastic Blog"); // ContentTypeUid "blog_overview" is now mapped to "blog" via GetSectionFromContentType, // even though the URL alone ("/blog", no trailing slash) doesn't match the URL heuristic. - doc.Section.Should().Be("blog"); - doc.Locale.Should().Be("en"); - doc.Body.Should().Contain("latest from Elastic"); - doc.Body.Should().Be(doc.Body); - doc.Description.Should().Contain("latest from Elastic"); - doc.Og.Title.Should().NotBeNullOrEmpty(); - doc.Og.Description.Should().NotBeNullOrEmpty(); - doc.Og.Image.Should().NotBeNullOrEmpty(); - doc.Hash.Should().NotBeNullOrEmpty(); + ToJson(doc!).Should().MatchJsonSnapshot(); } /// Covers: tutorial, tutorial_page, tutorial_chapter, labs_integration (rich-text JSON AST body + description) @@ -253,15 +177,7 @@ public void RichTextPage_Maps_ProseMirrorBodyAndDescription() var doc = ContentStackMapper.ToSiteDocument(item); doc.Should().NotBeNull(); - doc.Path.Should().Be("/search-labs/integrations/amazon-bedrock"); - doc.Section.Should().Be("search-labs"); - doc.Body.Should().Contain("Amazon Bedrock makes leading foundation models available"); - doc.Body.Should().Contain("RAG using Bedrock"); - doc.Body.Should().NotContain("

"); - doc.Body.Should().NotContain("

"); - doc.Headings.Should().Contain("Blogs to get started"); - doc.Description.Should().Contain("fully managed service offering foundation models"); - doc.Hash.Should().NotBeNullOrEmpty(); + ToJson(doc!).Should().MatchJsonSnapshot(); } /// Covers: blog_v3 (rich-text body nested under main_content.body.content_l10n + summary_l10n description) @@ -272,13 +188,7 @@ public void BlogV3_Maps_NestedRichTextBodyAndSummary() var doc = ContentStackMapper.ToSiteDocument(item); doc.Should().NotBeNull(); - doc.Path.Should().Be("/search-labs/blog/hybrid-search-with-elasticsearch"); - doc.Section.Should().Be("search-labs"); - doc.Body.Should().Contain("Hybrid search combines BM25 lexical scoring"); - doc.Body.Should().NotContain("

"); - doc.Headings.Should().Contain("Why hybrid search"); - doc.Description.Should().Be("Combine lexical and semantic search for better relevance."); - doc.PublishedDate.Should().NotBeNull(); + ToJson(doc!).Should().MatchJsonSnapshot(); } ///

Covers: tutorials_landing, integrations_landing, blog_landing (page_info.subheading_l10n description, no body) @@ -289,11 +199,8 @@ public void LandingPage_Maps_SubheadingDescription_NoBody() var doc = ContentStackMapper.ToSiteDocument(item); doc.Should().NotBeNull(); - doc.Path.Should().Be("/search-labs/tutorials"); - doc.Section.Should().Be("search-labs"); - doc.Description.Should().Be("Long-form guides to get you started"); // No main_content on landing pages — body falls back to the description. - doc.Body.Should().Be(doc.Description); + ToJson(doc!).Should().MatchJsonSnapshot(); } /// Covers: notebook, series, labs_category, labs_homepage, glossary, examples_landing (plain description_l10n, no body) @@ -304,10 +211,7 @@ public void DescriptionOnly_Maps_PlainDescription_NoBody() var doc = ContentStackMapper.ToSiteDocument(item); doc.Should().NotBeNull(); - doc.Section.Should().Be("search-labs"); - doc.Description.Should().Contain("Learn how to build a RAG system"); - doc.Body.Should().Be(doc.Description); - doc.Headings.Should().BeEmpty(); + ToJson(doc!).Should().MatchJsonSnapshot(); } /// Covers: reports, threat_command (main_content.markdown_l10n plain-Markdown body + summary_l10n description) @@ -318,21 +222,10 @@ public void MarkdownBodyPage_Maps_MarkdownAsHeadingsAndPlainBody() var doc = ContentStackMapper.ToSiteDocument(item); doc.Should().NotBeNull(); - doc.Path.Should().Be("/security-labs/threat-command/tracking-a-novel-loader-pipedance"); - doc.Section.Should().Be("security-labs"); - doc.Headings.Should().BeEquivalentTo(["Key takeaways", "Analysis", "API resolution"]); - doc.Body.Should().Contain("PIPEDANCE is a novel loader used to stage secondary payloads"); // Links keep their visible text, drop the URL. - doc.Body.Should().Contain("process injection"); - doc.Body.Should().NotContain("example.com/injection"); // Bold/inline-code markers are stripped but the wrapped text is kept. - doc.Body.Should().Contain("kernel32.dll"); - doc.Body.Should().Contain("PEB"); - doc.Body.Should().NotContain("**"); - doc.Body.Should().NotContain("`"); // Fenced code blocks are dropped entirely. - doc.Body.Should().NotContain("walk_peb"); - doc.Description.Should().Be("An analysis of the PIPEDANCE loader observed in recent intrusions."); + ToJson(doc!).Should().MatchJsonSnapshot(); } /// Covers: reports_landing, threat_command_landing (top-level subheading_l10n description, no body) @@ -343,10 +236,7 @@ public void TopLevelSubheadingLandingPage_Maps_SubheadingDescription_NoBody() var doc = ContentStackMapper.ToSiteDocument(item); doc.Should().NotBeNull(); - doc.Path.Should().Be("/security-labs/reports"); - doc.Section.Should().Be("security-labs"); - doc.Description.Should().Be("In-depth research reports from the Elastic Security Labs team."); - doc.Body.Should().Be(doc.Description); + ToJson(doc!).Should().MatchJsonSnapshot(); } /// Covers: security_labs_homepage, observability_labs_homepage (page_description_l10n description, no body) @@ -357,10 +247,7 @@ public void PropertyHomepage_Maps_PageDescription_NoBody() var doc = ContentStackMapper.ToSiteDocument(item); doc.Should().NotBeNull(); - doc.Path.Should().Be("/security-labs"); - doc.Section.Should().Be("security-labs"); - doc.Description.Should().Be("Research, tools, and insights from the Elastic Security Labs team."); - doc.Body.Should().Be(doc.Description); + ToJson(doc!).Should().MatchJsonSnapshot(); } /// blog_description_l10n is used when page_description_l10n is absent. diff --git a/tests/Elastic.SiteSearch.Tests/MappingStructureTests.cs b/tests/Elastic.SiteSearch.Tests/MappingStructureTests.cs index 7b11afb319..577078c9a1 100644 --- a/tests/Elastic.SiteSearch.Tests/MappingStructureTests.cs +++ b/tests/Elastic.SiteSearch.Tests/MappingStructureTests.cs @@ -2,9 +2,9 @@ // Elasticsearch B.V licenses this file to you under the Apache 2.0 License. // See the LICENSE file in the project root for more information -using System.Text.Json; using AwesomeAssertions; using Elastic.Documentation.Search.Contract; +using Nullean.Snapshots.Json; namespace Elastic.SiteSearch.Tests; @@ -15,8 +15,6 @@ namespace Elastic.SiteSearch.Tests; ///

public class MappingStructureTests { - // ── [Id] resolves to Url for every document type ───────────────────────── - [Test] public void SiteDocument_GetId_ReturnsUrl() { @@ -38,109 +36,6 @@ public void GuideDocument_GetId_ReturnsUrl() ctx.GetId!(doc).Should().Be("https://www.elastic.co/guide/en/elasticsearch/reference/current/index.html"); } - // ── Base fields from SearchDocumentBase present in SiteDocument mapping ── - - [Test] - public void SiteDocument_MappingJson_ContainsBaseFields() - { - var json = SiteMappingContext.SiteDocument.GetMappingJson(); - using var doc = JsonDocument.Parse(json); - var props = doc.RootElement.GetProperty("properties"); - - props.TryGetProperty("path", out _).Should().BeTrue("path is declared on SearchDocumentBase"); - props.TryGetProperty("title", out _).Should().BeTrue("title is declared on SearchDocumentBase"); - props.TryGetProperty("content_type", out _).Should().BeTrue("content_type is declared on SearchDocumentBase"); - props.TryGetProperty("content_tier", out _).Should().BeTrue("content_tier is declared on SearchDocumentBase"); - props.TryGetProperty("hash", out _).Should().BeTrue("hash is declared on SearchDocumentBase"); - props.TryGetProperty("body", out _).Should().BeTrue("body is declared on SearchDocumentBase"); - props.TryGetProperty("headings", out _).Should().BeTrue("headings is declared on SearchDocumentBase"); - props.TryGetProperty("navigation", out _).Should().BeTrue("navigation is declared on SearchDocumentBase"); - } - - // ── Base fields from SearchDocumentBase present in GuideDocument mapping ─ - - [Test] - public void GuideDocument_MappingJson_ContainsBaseFields() - { - var json = GuideMappingContext.GuideDocument.GetMappingJson(); - using var doc = JsonDocument.Parse(json); - var props = doc.RootElement.GetProperty("properties"); - - props.TryGetProperty("path", out _).Should().BeTrue("path is declared on SearchDocumentBase"); - props.TryGetProperty("title", out _).Should().BeTrue("title is declared on SearchDocumentBase"); - props.TryGetProperty("content_type", out _).Should().BeTrue("content_type is declared on SearchDocumentBase"); - props.TryGetProperty("content_tier", out _).Should().BeTrue("content_tier is declared on SearchDocumentBase"); - props.TryGetProperty("hash", out _).Should().BeTrue("hash is declared on SearchDocumentBase"); - props.TryGetProperty("body", out _).Should().BeTrue("body is declared on SearchDocumentBase"); - props.TryGetProperty("navigation", out _).Should().BeTrue("navigation is declared on SearchDocumentBase"); - } - - // ── Url: [Id][Keyword] must map as keyword ──────────────────────────────── - - [Test] - public void SiteDocument_UrlField_IsKeyword() - { - var json = SiteMappingContext.SiteDocument.GetMappingJson(); - using var doc = JsonDocument.Parse(json); - doc.RootElement.GetProperty("properties").GetProperty("path").GetProperty("type").GetString().Should().Be("keyword"); - } - - [Test] - public void GuideDocument_UrlField_IsKeyword() - { - var json = GuideMappingContext.GuideDocument.GetMappingJson(); - using var doc = JsonDocument.Parse(json); - doc.RootElement.GetProperty("properties").GetProperty("path").GetProperty("type").GetString().Should().Be("keyword"); - } - - // ── content_type: [Keyword] on base ────────────────────────────────────── - - [Test] - public void SiteDocument_ContentTypeField_IsKeyword() - { - var json = SiteMappingContext.SiteDocument.GetMappingJson(); - using var doc = JsonDocument.Parse(json); - doc.RootElement.GetProperty("properties").GetProperty("content_type").GetProperty("type").GetString().Should().Be("keyword"); - } - - // ── tags: copy_to target for content_type/section ───── - - [Test] - public void SiteDocument_ContentTypeField_CopiesToContentTags() - { - var json = SiteMappingContext.SiteDocument.GetMappingJson(); - using var doc = JsonDocument.Parse(json); - doc.RootElement.GetProperty("properties").GetProperty("content_type").GetProperty("copy_to").GetString().Should().Be("tags"); - } - - [Test] - public void SiteDocument_NavigationSectionField_CopiesToContentTags() - { - var json = SiteMappingContext.SiteDocument.GetMappingJson(); - using var doc = JsonDocument.Parse(json); - doc.RootElement.GetProperty("properties").GetProperty("section").GetProperty("copy_to").GetString().Should().Be("tags"); - } - - [Test] - public void SiteDocument_ContentTagsField_IsTextWithContentTagsAnalyzer() - { - var json = SiteMappingContext.SiteDocument.GetMappingJson(); - using var doc = JsonDocument.Parse(json); - var contentTags = doc.RootElement.GetProperty("properties").GetProperty("tags"); - contentTags.GetProperty("type").GetString().Should().Be("text"); - contentTags.GetProperty("analyzer").GetString().Should().Be("content_tags_analyzer"); - } - - // ── content_tier: [Keyword] on base, neutral default ────────────────────── - - [Test] - public void SiteDocument_ContentTierField_IsKeyword() - { - var json = SiteMappingContext.SiteDocument.GetMappingJson(); - using var doc = JsonDocument.Parse(json); - doc.RootElement.GetProperty("properties").GetProperty("content_tier").GetProperty("type").GetString().Should().Be("keyword"); - } - [Test] public void SiteDocument_ContentTier_DefaultsToNeutralReference() { @@ -148,169 +43,6 @@ public void SiteDocument_ContentTier_DefaultsToNeutralReference() document.ContentTier.Should().Be(ContentTiers.Reference, "content_tier must default to a neutral tier, not a penalty"); } - // ── hash: [Keyword] on base ─────────────────────────────────────────────── - - [Test] - public void SiteDocument_HashField_IsKeyword() - { - var json = SiteMappingContext.SiteDocument.GetMappingJson(); - using var doc = JsonDocument.Parse(json); - doc.RootElement.GetProperty("properties").GetProperty("hash").GetProperty("type").GetString().Should().Be("keyword"); - } - - // ── Title multi-fields from AddSearchDocumentMappings ──────────────────── - - [Test] - public void SiteDocument_TitleField_HasKeywordNormalizedMultiField() - { - var json = SiteMappingContext.SiteDocument.GetMappingJson(); - using var doc = JsonDocument.Parse(json); - var titleFields = doc.RootElement.GetProperty("properties").GetProperty("title").GetProperty("fields"); - - var keyword = titleFields.GetProperty("keyword"); - keyword.GetProperty("type").GetString().Should().Be("keyword"); - keyword.GetProperty("normalizer").GetString().Should().Be("keyword_normalizer"); - } - - [Test] - public void SiteDocument_TitleField_HasStartsWithMultiField() - { - var json = SiteMappingContext.SiteDocument.GetMappingJson(); - using var doc = JsonDocument.Parse(json); - var titleFields = doc.RootElement.GetProperty("properties").GetProperty("title").GetProperty("fields"); - - titleFields.TryGetProperty("starts_with", out _).Should().BeTrue("starts_with is configured in AddSearchDocumentMappings"); - } - - [Test] - public void SiteDocument_TitleField_HasCompletionMultiField() - { - var json = SiteMappingContext.SiteDocument.GetMappingJson(); - using var doc = JsonDocument.Parse(json); - var titleFields = doc.RootElement.GetProperty("properties").GetProperty("title").GetProperty("fields"); - - titleFields.TryGetProperty("completion", out _).Should().BeTrue("completion is configured in AddSearchDocumentMappings"); - } - - // ── ai_search_query: search_as_you_type completion sub-field, no semantic_text ── - - [Test] - public void SiteDocument_AiSearchQueryField_IsKeywordWithCompletionMultiField() - { - var json = SiteMappingContext.SiteDocument.GetMappingJson(); - using var doc = JsonDocument.Parse(json); - var aiSearchQuery = doc.RootElement.GetProperty("properties").GetProperty("ai_search_query"); - aiSearchQuery.GetProperty("type").GetString().Should().Be("keyword"); - - var completion = aiSearchQuery.GetProperty("fields").GetProperty("completion"); - completion.GetProperty("type").GetString().Should().Be("search_as_you_type"); - } - - [Test] - public void SiteDocument_SemanticVariant_AiSearchQueryHasNoSemanticTextField() - { - var json = SiteMappingContext.SiteDocumentSemantic.GetMappingJson(); - using var doc = JsonDocument.Parse(json); - var aiSearchQuery = doc.RootElement.GetProperty("properties").GetProperty("ai_search_query"); - if (aiSearchQuery.TryGetProperty("fields", out var fields)) - fields.TryGetProperty("semantic_text", out _).Should().BeFalse("ai_search_query is typeahead-only — never semantic_text"); - } - - // ── Path multi-fields from AddCommonTitleMappings ────────────────────────── - - [Test] - public void SiteDocument_UrlField_HasMatchAndPrefixMultiFields() - { - var json = SiteMappingContext.SiteDocument.GetMappingJson(); - using var doc = JsonDocument.Parse(json); - var urlFields = doc.RootElement.GetProperty("properties").GetProperty("path").GetProperty("fields"); - - urlFields.TryGetProperty("match", out _).Should().BeTrue("path.match is configured in AddCommonTitleMappings"); - urlFields - .TryGetProperty("prefix", out _) - .Should() - .BeTrue("path.prefix (hierarchy_analyzer) is configured in AddCommonTitleMappings"); - urlFields.GetProperty("prefix").GetProperty("analyzer").GetString().Should().Be("hierarchy_analyzer"); - } - - // ── Navigation fields must be rank_feature with negative score impact ───── - - [Test] - public void SiteDocument_NavigationDepth_IsRankFeatureWithNegativeImpact() - { - var json = SiteMappingContext.SiteDocument.GetMappingJson(); - using var doc = JsonDocument.Parse(json); - var depth = doc.RootElement.GetProperty("properties").GetProperty("navigation").GetProperty("properties").GetProperty("depth"); - depth.GetProperty("type").GetString().Should().Be("rank_feature"); - depth.GetProperty("positive_score_impact").GetBoolean().Should().BeFalse(); - } - - [Test] - public void SiteDocument_NavigationTableOfContents_IsRankFeatureWithNegativeImpact() - { - var json = SiteMappingContext.SiteDocument.GetMappingJson(); - using var doc = JsonDocument.Parse(json); - var toc = doc - .RootElement - .GetProperty("properties") - .GetProperty("navigation") - .GetProperty("properties") - .GetProperty("table_of_contents"); - toc.GetProperty("type").GetString().Should().Be("rank_feature"); - toc.GetProperty("positive_score_impact").GetBoolean().Should().BeFalse(); - } - - // ── Body multi-language fields from AddSearchDocumentMappings ──────────── - - [Test] - public void SiteDocument_BodyField_HasLanguageMultiFields() - { - var json = SiteMappingContext.SiteDocument.GetMappingJson(); - using var doc = JsonDocument.Parse(json); - var bodyFields = doc.RootElement.GetProperty("properties").GetProperty("body").GetProperty("fields"); - - bodyFields.GetProperty("en").GetProperty("analyzer").GetString().Should().Be("english"); - bodyFields.GetProperty("de").GetProperty("analyzer").GetString().Should().Be("german"); - bodyFields.GetProperty("fr").GetProperty("analyzer").GetString().Should().Be("french"); - bodyFields.TryGetProperty("ja", out _).Should().BeTrue("ja (cjk) body multi-field is configured"); - bodyFields.TryGetProperty("ko", out _).Should().BeTrue("ko (cjk) body multi-field is configured"); - bodyFields.TryGetProperty("zh", out _).Should().BeTrue("zh (cjk) body multi-field is configured"); - bodyFields.TryGetProperty("es", out _).Should().BeTrue("es (spanish) body multi-field is configured"); - bodyFields.TryGetProperty("pt", out _).Should().BeTrue("pt (portuguese) body multi-field is configured"); - } - - // ── Semantic variant adds semantic_text multi-fields ───────────────────── - - [Test] - public void SiteDocument_SemanticVariant_TitleHasSemanticTextField() - { - var json = SiteMappingContext.SiteDocumentSemantic.GetMappingJson(); - using var doc = JsonDocument.Parse(json); - var titleFields = doc.RootElement.GetProperty("properties").GetProperty("title").GetProperty("fields"); - titleFields.GetProperty("semantic_text").GetProperty("type").GetString().Should().Be("semantic_text"); - } - - [Test] - public void SiteDocument_SemanticVariant_StrippedBodyHasSemanticTextField() - { - var json = SiteMappingContext.SiteDocumentSemantic.GetMappingJson(); - using var doc = JsonDocument.Parse(json); - var bodyFields = doc.RootElement.GetProperty("properties").GetProperty("body").GetProperty("fields"); - bodyFields.GetProperty("semantic_text").GetProperty("type").GetString().Should().Be("semantic_text"); - } - - [Test] - public void SiteDocument_LexicalVariant_DoesNotHaveSemanticTextField() - { - var json = SiteMappingContext.SiteDocument.GetMappingJson(); - using var doc = JsonDocument.Parse(json); - var title = doc.RootElement.GetProperty("properties").GetProperty("title"); - if (title.TryGetProperty("fields", out var titleFields)) - titleFields.TryGetProperty("semantic_text", out _).Should().BeFalse("lexical variant has no semantic_text"); - } - - // ── Field name constants match JSON property names ──────────────────────── - [Test] public void SiteDocument_Fields_UrlMatchesJsonPropertyName() => SiteMappingContext.SiteDocument.Fields.Path.Should().Be("path"); @@ -323,61 +55,26 @@ public void SiteDocument_LexicalVariant_DoesNotHaveSemanticTextField() [Test] public void GuideDocument_Fields_UrlMatchesJsonPropertyName() => GuideMappingContext.GuideDocument.Fields.Path.Should().Be("path"); - // ── parents: shared topology declared once in SharedMappingConfig ──────── + // ── Whole mapping per document type ────────────────────────────────────── + // The snapshots pin everything the old per-field checks did: the SearchDocumentBase fields, keyword and + // copy_to settings, the title/path/body/parents multi-fields, rank_feature navigation fields, the + // semantic_text sub-fields that only the semantic variant has, and the applies_to / api topology. [Test] - public void SiteDocument_ParentsPath_IsKeywordWithMatchAndPrefixMultiFields() - { - var json = SiteMappingContext.SiteDocument.GetMappingJson(); - using var doc = JsonDocument.Parse(json); - var path = doc.RootElement.GetProperty("properties").GetProperty("parents").GetProperty("properties").GetProperty("path"); - - path.GetProperty("type").GetString().Should().Be("keyword"); - var fields = path.GetProperty("fields"); - fields.TryGetProperty("match", out _).Should().BeTrue(); - fields.GetProperty("prefix").GetProperty("analyzer").GetString().Should().Be("hierarchy_analyzer"); - } - - // ── Unified index (WebsiteSearchDocument) merges in DocumentationDocument's - // applies_to topology, which it has no C# property for ───────────────────── + public void SiteDocument_MappingJson() => SiteMappingContext.SiteDocument.GetMappingJson().Should().MatchJsonSnapshot(); [Test] - public void WebsiteSearchDocument_MergesDocumentationAppliesTo() - { - var json = WebsiteSearchMappingContext.WebsiteSearchDocument.GetMappingJson(); - using var doc = JsonDocument.Parse(json); - var appliesTo = doc.RootElement.GetProperty("properties").GetProperty("applies_to"); - - appliesTo.GetProperty("type").GetString().Should().Be("nested"); - var props = appliesTo.GetProperty("properties"); - props.GetProperty("type").GetProperty("type").GetString().Should().Be("keyword"); - props.GetProperty("type").GetProperty("normalizer").GetString().Should().Be("keyword_normalizer"); - props.GetProperty("version").GetProperty("type").GetString().Should().Be("version"); - } + public void SiteDocumentSemantic_MappingJson() => SiteMappingContext.SiteDocumentSemantic.GetMappingJson().Should().MatchJsonSnapshot(); [Test] - public void WebsiteSearchDocument_ParentsPath_IsKeywordWithMatchAndPrefixMultiFields() - { - var json = WebsiteSearchMappingContext.WebsiteSearchDocument.GetMappingJson(); - using var doc = JsonDocument.Parse(json); - var path = doc.RootElement.GetProperty("properties").GetProperty("parents").GetProperty("properties").GetProperty("path"); + public void GuideDocument_MappingJson() => GuideMappingContext.GuideDocument.GetMappingJson().Should().MatchJsonSnapshot(); - path.GetProperty("type").GetString().Should().Be("keyword"); - var fields = path.GetProperty("fields"); - fields.TryGetProperty("match", out _).Should().BeTrue(); - fields.GetProperty("prefix").GetProperty("analyzer").GetString().Should().Be("hierarchy_analyzer"); - } + /// The unified index merges in DocumentationDocument's applies_to topology, which it has no C# property for. [Test] - public void DocumentationDocument_MappingJson_ApiEndpointsIsPlainKeyword() - { - var json = DocumentationMappingContext.DocumentationDocument.GetMappingJson(); - using var doc = JsonDocument.Parse(json); - var api = doc.RootElement.GetProperty("properties").GetProperty("api").GetProperty("properties"); + public void WebsiteSearchDocument_MappingJson() => + WebsiteSearchMappingContext.WebsiteSearchDocument.GetMappingJson().Should().MatchJsonSnapshot(); - foreach (var name in new[] { "endpoints", "method", "path", "operation_id" }) - { - api.GetProperty(name).GetProperty("type").GetString().Should().Be("keyword"); - api.GetProperty(name).TryGetProperty("normalizer", out _).Should().BeFalse(); - } - } + [Test] + public void DocumentationDocument_MappingJson() => + DocumentationMappingContext.DocumentationDocument.GetMappingJson().Should().MatchJsonSnapshot(); } diff --git a/tests/Elastic.SiteSearch.Tests/TestEnvironment.cs b/tests/Elastic.SiteSearch.Tests/TestEnvironment.cs new file mode 100644 index 0000000000..c93345657f --- /dev/null +++ b/tests/Elastic.SiteSearch.Tests/TestEnvironment.cs @@ -0,0 +1,19 @@ +// Licensed to Elasticsearch B.V under one or more agreements. +// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information + +using System.Runtime.CompilerServices; + +namespace Elastic.SiteSearch.Tests; + +internal static class TestEnvironment +{ + // ContentStackMapper.ParseDate reads a date without an offset ("2023-09-01") in the machine's time zone, so the mapped + // published_date of a press or product release carries the offset of whoever ran the test: +01:00 on a laptop in + // Amsterdam, +00:00 on a GitHub runner. Pin the zone so the verified files read the same everywhere. TZ is read by + // .NET on Linux and macOS; Windows ignores it, but its CI runners are on UTC. +#pragma warning disable CA2255 // a test assembly is application code + [ModuleInitializer] +#pragma warning restore CA2255 + internal static void PinTimeZone() => Environment.SetEnvironmentVariable("TZ", "UTC"); +} diff --git a/tests/Mcp.Remote.Tests/McpServerInstructionTests.cs b/tests/Mcp.Remote.Tests/McpServerInstructionTests.cs index f559f8183e..2ceba051ad 100644 --- a/tests/Mcp.Remote.Tests/McpServerInstructionTests.cs +++ b/tests/Mcp.Remote.Tests/McpServerInstructionTests.cs @@ -4,59 +4,12 @@ using AwesomeAssertions; using Elastic.Documentation.Mcp.Remote; +using Nullean.Snapshots; namespace Mcp.Remote.Tests; public class McpServerInstructionTests { - [Test] - public void PublicProfile_ContainsAllModuleGuidance() - { - var instructions = McpServerProfile.Public.ComposeServerInstructions(); - - instructions.Should().Contain("Use this server to search, retrieve, and analyze"); - instructions.Should().Contain("Elastic product documentation published at elastic.co/docs"); - instructions.Should().Contain(""); - instructions.Should().Contain("Use the server when the user:"); - instructions.Should().Contain(""); - instructions.Should().Contain("Prefer search_docs over a general web search"); - instructions.Should().Contain("Use get_document_by_url to retrieve a specific page"); - instructions.Should().Contain("Use find_related_docs when exploring what documentation exists"); - instructions.Should().Contain("Use check_docs_coherence or find_docs_inconsistencies when reviewing or auditing"); - } - - [Test] - public void InternalProfile_ContainsSearchAndDocumentGuidanceOnly() - { - var instructions = McpServerProfile.Internal.ComposeServerInstructions(); - - instructions.Should().Contain("Use this server to search and retrieve"); - instructions.Should().Contain("Elastic internal documentation: team processes, run books, architecture"); - instructions.Should().Contain("Prefer search_internal_docs over a general web search"); - instructions.Should().Contain("Use get_internal_document_by_url to retrieve a specific page"); - instructions.Should().Contain("Use find_related_internal_docs when exploring what documentation exists"); - instructions.Should().NotContain("check_coherence"); - instructions.Should().NotContain("find_inconsistencies"); - instructions.Should().NotContain("resolve_cross_link"); - instructions.Should().NotContain("list_content_types"); - instructions.Should().NotContain("generate_template"); - } - - [Test] - public void Triggers_AreProfileSpecific() - { - var publicInstructions = McpServerProfile.Public.ComposeServerInstructions(); - var internalInstructions = McpServerProfile.Internal.ComposeServerInstructions(); - - publicInstructions.Should().Contain("Elastic documentation pages"); - publicInstructions.Should().Contain("References Elastic product names"); - publicInstructions.Should().NotContain("internal team processes"); - - internalInstructions.Should().Contain("Elastic internal documentation pages"); - internalInstructions.Should().Contain("internal team processes"); - internalInstructions.Should().NotContain("Elastic product names"); - } - [Test] public void PublicProfile_ServiceName_IsDocsMcp() => McpServerProfile.Public.ServiceName.Should().Be("docs-mcp"); @@ -102,7 +55,7 @@ public void PublicProfile_ComposesExactInstructions() { var instructions = McpServerProfile.Public.ComposeServerInstructions(); - var expected = + instructions.Should().MatchInlineSnapshot( """ Use this server to search, retrieve, and analyze Elastic product documentation published at elastic.co/docs. All content covers version 9 and later. @@ -120,9 +73,8 @@ public void PublicProfile_ComposesExactInstructions() - Use get_document_by_url to retrieve a specific page when the user provides or you already know the URL. - Use check_docs_coherence or find_docs_inconsistencies when reviewing or auditing documentation quality. - """; - - instructions.Should().Be(expected.ReplaceLineEndings("\n")); + """ + ); } [Test] @@ -130,7 +82,7 @@ public void InternalProfile_ComposesExactInstructions() { var instructions = McpServerProfile.Internal.ComposeServerInstructions(); - var expected = + instructions.Should().MatchInlineSnapshot( """ Use this server to search and retrieve Elastic internal documentation: team processes, run books, architecture, and other internal knowledge. @@ -146,9 +98,8 @@ public void InternalProfile_ComposesExactInstructions() - Use find_related_internal_docs when exploring what documentation exists around a topic. - Use get_internal_document_by_url to retrieve a specific page when the user provides or you already know the URL. - """; - - instructions.Should().Be(expected.ReplaceLineEndings("\n")); + """ + ); } private static List ExtractBullets(string instructions) =>