diff --git a/Directory.Packages.props b/Directory.Packages.props
index deb03b526..40a57e8b4 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -29,7 +29,7 @@
     
     
     
-    
+    
     
     
     
diff --git a/src/Elastic.Documentation/AppliesTo/ApplicableTo.cs b/src/Elastic.Documentation/AppliesTo/ApplicableTo.cs
index 2aba29981..978a1f1fe 100644
--- a/src/Elastic.Documentation/AppliesTo/ApplicableTo.cs
+++ b/src/Elastic.Documentation/AppliesTo/ApplicableTo.cs
@@ -3,6 +3,7 @@
 // See the LICENSE file in the project root for more information
 
 using System.Collections;
+using System.Text;
 using System.Text.Json.Serialization;
 using Elastic.Documentation.Diagnostics;
 using YamlDotNet.Serialization;
@@ -34,6 +35,7 @@ public interface IApplicableToElement
 }
 
 [YamlSerializable]
+[JsonConverter(typeof(ApplicableToJsonConverter))]
 public record ApplicableTo
 {
 	[YamlMember(Alias = "stack")]
@@ -61,6 +63,58 @@ public record ApplicableTo
 		Deployment = DeploymentApplicability.All,
 		Product = AppliesCollection.GenerallyAvailable
 	};
+
+	public static ApplicableTo Default { get; } = new()
+	{
+		Stack = new AppliesCollection([new Applicability { Version = new SemVersion(9, 0, 0), Lifecycle = ProductLifecycle.GenerallyAvailable }]),
+		Serverless = ServerlessProjectApplicability.All
+	};
+
+	/// 
+	public override string ToString()
+	{
+		var sb = new StringBuilder();
+		var hasContent = false;
+
+		if (Stack is not null)
+		{
+			_ = sb.Append("stack: ").Append(Stack);
+			hasContent = true;
+		}
+
+		if (Deployment is not null)
+		{
+			if (hasContent)
+				_ = sb.Append(", ");
+			_ = sb.Append("deployment: ").Append(Deployment);
+			hasContent = true;
+		}
+
+		if (Serverless is not null)
+		{
+			if (hasContent)
+				_ = sb.Append(", ");
+			_ = sb.Append("serverless: ").Append(Serverless);
+			hasContent = true;
+		}
+
+		if (Product is not null)
+		{
+			if (hasContent)
+				_ = sb.Append(", ");
+			_ = sb.Append("product: ").Append(Product);
+			hasContent = true;
+		}
+
+		if (ProductApplicability is not null)
+		{
+			if (hasContent)
+				_ = sb.Append(", ");
+			_ = sb.Append("products: ").Append(ProductApplicability);
+		}
+
+		return sb.ToString();
+	}
 }
 
 [YamlSerializable]
@@ -85,6 +139,44 @@ public record DeploymentApplicability
 		Ess = AppliesCollection.GenerallyAvailable,
 		Self = AppliesCollection.GenerallyAvailable
 	};
+
+	/// 
+	public override string ToString()
+	{
+		var sb = new StringBuilder();
+		var hasContent = false;
+
+		if (Self is not null)
+		{
+			_ = sb.Append("self=").Append(Self);
+			hasContent = true;
+		}
+
+		if (Ece is not null)
+		{
+			if (hasContent)
+				_ = sb.Append(", ");
+			_ = sb.Append("ece=").Append(Ece);
+			hasContent = true;
+		}
+
+		if (Eck is not null)
+		{
+			if (hasContent)
+				_ = sb.Append(", ");
+			_ = sb.Append("eck=").Append(Eck);
+			hasContent = true;
+		}
+
+		if (Ess is not null)
+		{
+			if (hasContent)
+				_ = sb.Append(", ");
+			_ = sb.Append("ess=").Append(Ess);
+		}
+
+		return sb.ToString();
+	}
 }
 
 [YamlSerializable]
@@ -113,6 +205,36 @@ public record ServerlessProjectApplicability
 		Observability = AppliesCollection.GenerallyAvailable,
 		Security = AppliesCollection.GenerallyAvailable
 	};
+
+	/// 
+	public override string ToString()
+	{
+		var sb = new StringBuilder();
+		var hasContent = false;
+
+		if (Elasticsearch is not null)
+		{
+			_ = sb.Append("elasticsearch=").Append(Elasticsearch);
+			hasContent = true;
+		}
+
+		if (Observability is not null)
+		{
+			if (hasContent)
+				_ = sb.Append(", ");
+			_ = sb.Append("observability=").Append(Observability);
+			hasContent = true;
+		}
+
+		if (Security is not null)
+		{
+			if (hasContent)
+				_ = sb.Append(", ");
+			_ = sb.Append("security=").Append(Security);
+		}
+
+		return sb.ToString();
+	}
 }
 
 [YamlSerializable]
@@ -183,4 +305,46 @@ public record ProductApplicability
 
 	[YamlMember(Alias = "edot-collector")]
 	public AppliesCollection? EdotCollector { get; set; }
+
+	/// 
+	public override string ToString()
+	{
+		var sb = new StringBuilder();
+		var hasContent = false;
+
+		void AppendProduct(string name, AppliesCollection? value)
+		{
+			if (value is null)
+				return;
+			if (hasContent)
+				_ = sb.Append(", ");
+			_ = sb.Append(name).Append('=').Append(value);
+			hasContent = true;
+		}
+
+		AppendProduct("ecctl", Ecctl);
+		AppendProduct("curator", Curator);
+		AppendProduct("apm-agent-android", ApmAgentAndroid);
+		AppendProduct("apm-agent-dotnet", ApmAgentDotnet);
+		AppendProduct("apm-agent-go", ApmAgentGo);
+		AppendProduct("apm-agent-ios", ApmAgentIos);
+		AppendProduct("apm-agent-java", ApmAgentJava);
+		AppendProduct("apm-agent-node", ApmAgentNode);
+		AppendProduct("apm-agent-php", ApmAgentPhp);
+		AppendProduct("apm-agent-python", ApmAgentPython);
+		AppendProduct("apm-agent-ruby", ApmAgentRuby);
+		AppendProduct("apm-agent-rum-js", ApmAgentRumJs);
+		AppendProduct("edot-ios", EdotIos);
+		AppendProduct("edot-android", EdotAndroid);
+		AppendProduct("edot-dotnet", EdotDotnet);
+		AppendProduct("edot-java", EdotJava);
+		AppendProduct("edot-node", EdotNode);
+		AppendProduct("edot-php", EdotPhp);
+		AppendProduct("edot-python", EdotPython);
+		AppendProduct("edot-cf-aws", EdotCfAws);
+		AppendProduct("edot-cf-azure", EdotCfAzure);
+		AppendProduct("edot-collector", EdotCollector);
+
+		return sb.ToString();
+	}
 }
diff --git a/src/Elastic.Documentation/AppliesTo/ApplicableToJsonConverter.cs b/src/Elastic.Documentation/AppliesTo/ApplicableToJsonConverter.cs
new file mode 100644
index 000000000..d3779e525
--- /dev/null
+++ b/src/Elastic.Documentation/AppliesTo/ApplicableToJsonConverter.cs
@@ -0,0 +1,263 @@
+// 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.Reflection;
+using System.Text.Json;
+using System.Text.Json.Serialization;
+using YamlDotNet.Serialization;
+
+namespace Elastic.Documentation.AppliesTo;
+
+/// 
+/// JSON converter for ApplicableTo that serializes to a flat array of objects with:
+/// - type: stack, deployment, serverless, or product
+/// - sub-type: the property name (e.g., "self", "ece", "elasticsearch", "ecctl")
+/// - lifecycle: the lifecycle value (if applicable)
+/// - version: the version value (if applicable)
+/// 
+public class ApplicableToJsonConverter : JsonConverter
+{
+	public override ApplicableTo? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
+	{
+		if (reader.TokenType == JsonTokenType.Null)
+			return null;
+
+		if (reader.TokenType != JsonTokenType.StartArray)
+			throw new JsonException("Expected array");
+
+		var result = new ApplicableTo();
+		var deploymentProps = new Dictionary>();
+		var serverlessProps = new Dictionary>();
+		var productProps = new Dictionary>();
+		var stackItems = new List();
+		var productItems = new List();
+
+		while (reader.Read())
+		{
+			if (reader.TokenType == JsonTokenType.EndArray)
+				break;
+
+			if (reader.TokenType != JsonTokenType.StartObject)
+				throw new JsonException("Expected object");
+
+			string? type = null;
+			string? subType = null;
+			var lifecycle = ProductLifecycle.GenerallyAvailable;
+			SemVersion? version = null;
+
+			while (reader.Read())
+			{
+				if (reader.TokenType == JsonTokenType.EndObject)
+					break;
+
+				if (reader.TokenType != JsonTokenType.PropertyName)
+					throw new JsonException("Expected property name");
+
+				var propertyName = reader.GetString();
+				_ = reader.Read();
+
+				switch (propertyName)
+				{
+					case "type":
+						type = reader.GetString();
+						break;
+					case "sub_type":
+						subType = reader.GetString();
+						break;
+					case "lifecycle":
+						var lifecycleStr = reader.GetString();
+						if (lifecycleStr != null)
+							lifecycle = ParseLifecycle(lifecycleStr);
+						break;
+					case "version":
+						var versionStr = reader.GetString();
+						if (versionStr != null && SemVersionConverter.TryParse(versionStr, out var v))
+							version = v;
+						break;
+				}
+			}
+
+			if (string.IsNullOrEmpty(type) || string.IsNullOrEmpty(subType))
+				throw new JsonException("Missing type or sub-type");
+
+			var applicability = new Applicability { Lifecycle = lifecycle, Version = version };
+
+			switch (type)
+			{
+				case "stack":
+					stackItems.Add(applicability);
+					break;
+				case "deployment":
+					if (!deploymentProps.ContainsKey(subType))
+						deploymentProps[subType] = [];
+					deploymentProps[subType].Add(applicability);
+					break;
+				case "serverless":
+					if (!serverlessProps.ContainsKey(subType))
+						serverlessProps[subType] = [];
+					serverlessProps[subType].Add(applicability);
+					break;
+				case "product" when subType == "product":
+					productItems.Add(applicability);
+					break;
+				case "product":
+					if (!productProps.ContainsKey(subType))
+						productProps[subType] = [];
+					productProps[subType].Add(applicability);
+					break;
+			}
+		}
+
+		// Create Stack collection
+		if (stackItems.Count > 0)
+			result.Stack = new AppliesCollection(stackItems.ToArray());
+
+		// Create Product collection
+		if (productItems.Count > 0)
+			result.Product = new AppliesCollection(productItems.ToArray());
+
+		// Reconstruct DeploymentApplicability
+		if (deploymentProps.Count > 0)
+		{
+			result.Deployment = new DeploymentApplicability
+			{
+				Self = deploymentProps.TryGetValue("self", out var self) ? new AppliesCollection(self.ToArray()) : null,
+				Ece = deploymentProps.TryGetValue("ece", out var ece) ? new AppliesCollection(ece.ToArray()) : null,
+				Eck = deploymentProps.TryGetValue("eck", out var eck) ? new AppliesCollection(eck.ToArray()) : null,
+				Ess = deploymentProps.TryGetValue("ess", out var ess) ? new AppliesCollection(ess.ToArray()) : null
+			};
+		}
+
+		// Reconstruct ServerlessProjectApplicability
+		if (serverlessProps.Count > 0)
+		{
+			result.Serverless = new ServerlessProjectApplicability
+			{
+				Elasticsearch = serverlessProps.TryGetValue("elasticsearch", out var es) ? new AppliesCollection(es.ToArray()) : null,
+				Observability = serverlessProps.TryGetValue("observability", out var obs) ? new AppliesCollection(obs.ToArray()) : null,
+				Security = serverlessProps.TryGetValue("security", out var sec) ? new AppliesCollection(sec.ToArray()) : null
+			};
+		}
+
+		// Reconstruct ProductApplicability
+		if (productProps.Count > 0)
+		{
+			var productApplicability = new ProductApplicability();
+			var productType = typeof(ProductApplicability);
+
+			foreach (var (key, items) in productProps)
+			{
+				// Find the property by YamlMember alias
+				var property = productType.GetProperties()
+					.FirstOrDefault(p => p.GetCustomAttribute()?.Alias == key);
+
+				property?.SetValue(productApplicability, new AppliesCollection(items.ToArray()));
+			}
+
+			result.ProductApplicability = productApplicability;
+		}
+
+		return result;
+	}
+
+	public override void Write(Utf8JsonWriter writer, ApplicableTo value, JsonSerializerOptions options)
+	{
+		writer.WriteStartArray();
+
+		// Stack
+		if (value.Stack != null)
+			WriteApplicabilityEntries(writer, "stack", "stack", value.Stack);
+
+		// Deployment
+		if (value.Deployment != null)
+		{
+			if (value.Deployment.Self != null)
+				WriteApplicabilityEntries(writer, "deployment", "self", value.Deployment.Self);
+			if (value.Deployment.Ece != null)
+				WriteApplicabilityEntries(writer, "deployment", "ece", value.Deployment.Ece);
+			if (value.Deployment.Eck != null)
+				WriteApplicabilityEntries(writer, "deployment", "eck", value.Deployment.Eck);
+			if (value.Deployment.Ess != null)
+				WriteApplicabilityEntries(writer, "deployment", "ess", value.Deployment.Ess);
+		}
+
+		// Serverless
+		if (value.Serverless != null)
+		{
+			if (value.Serverless.Elasticsearch != null)
+				WriteApplicabilityEntries(writer, "serverless", "elasticsearch", value.Serverless.Elasticsearch);
+			if (value.Serverless.Observability != null)
+				WriteApplicabilityEntries(writer, "serverless", "observability", value.Serverless.Observability);
+			if (value.Serverless.Security != null)
+				WriteApplicabilityEntries(writer, "serverless", "security", value.Serverless.Security);
+		}
+
+		// Product (simple)
+		if (value.Product != null)
+			WriteApplicabilityEntries(writer, "product", "product", value.Product);
+
+		// ProductApplicability (specific products)
+		if (value.ProductApplicability != null)
+		{
+			var productType = typeof(ProductApplicability);
+			foreach (var property in productType.GetProperties())
+			{
+				var yamlAlias = property.GetCustomAttribute()?.Alias;
+				if (yamlAlias != null)
+				{
+					if (property.GetValue(value.ProductApplicability) is AppliesCollection propertyValue)
+						WriteApplicabilityEntries(writer, "product", yamlAlias, propertyValue);
+				}
+			}
+		}
+
+		writer.WriteEndArray();
+	}
+
+	private static ProductLifecycle ParseLifecycle(string lifecycleStr) => lifecycleStr.ToLowerInvariant() switch
+	{
+		"preview" => ProductLifecycle.TechnicalPreview,
+		"beta" => ProductLifecycle.Beta,
+		"ga" => ProductLifecycle.GenerallyAvailable,
+		"deprecated" => ProductLifecycle.Deprecated,
+		"removed" => ProductLifecycle.Removed,
+		"unavailable" => ProductLifecycle.Unavailable,
+		"development" => ProductLifecycle.Development,
+		"planned" => ProductLifecycle.Planned,
+		"discontinued" => ProductLifecycle.Discontinued,
+		_ => ProductLifecycle.GenerallyAvailable
+	};
+
+	private static void WriteApplicabilityEntries(Utf8JsonWriter writer, string type, string subType, AppliesCollection collection)
+	{
+		foreach (var applicability in collection)
+		{
+			writer.WriteStartObject();
+			writer.WriteString("type", type);
+			writer.WriteString("sub_type", subType);
+
+			// Write lifecycle
+			var lifecycleName = applicability.Lifecycle switch
+			{
+				ProductLifecycle.TechnicalPreview => "preview",
+				ProductLifecycle.Beta => "beta",
+				ProductLifecycle.GenerallyAvailable => "ga",
+				ProductLifecycle.Deprecated => "deprecated",
+				ProductLifecycle.Removed => "removed",
+				ProductLifecycle.Unavailable => "unavailable",
+				ProductLifecycle.Development => "development",
+				ProductLifecycle.Planned => "planned",
+				ProductLifecycle.Discontinued => "discontinued",
+				_ => "ga"
+			};
+			writer.WriteString("lifecycle", lifecycleName);
+
+			// Write the version
+			if (applicability.Version is not null)
+				writer.WriteString("version", applicability.Version.ToString());
+
+			writer.WriteEndObject();
+		}
+	}
+}
diff --git a/src/Elastic.Documentation/Search/DocumentationDocument.cs b/src/Elastic.Documentation/Search/DocumentationDocument.cs
index 8a0eb6781..8470fc3b5 100644
--- a/src/Elastic.Documentation/Search/DocumentationDocument.cs
+++ b/src/Elastic.Documentation/Search/DocumentationDocument.cs
@@ -4,7 +4,6 @@
 
 using System.Text.Json.Serialization;
 using Elastic.Documentation.AppliesTo;
-using Elastic.Documentation.Extensions;
 
 namespace Elastic.Documentation.Search;
 
diff --git a/src/Elastic.Markdown/Exporters/Elasticsearch/ElasticsearchExporter.cs b/src/Elastic.Markdown/Exporters/Elasticsearch/ElasticsearchExporter.cs
index 199724929..3068e8f59 100644
--- a/src/Elastic.Markdown/Exporters/Elasticsearch/ElasticsearchExporter.cs
+++ b/src/Elastic.Markdown/Exporters/Elasticsearch/ElasticsearchExporter.cs
@@ -93,8 +93,8 @@ Func createOptions
 		options.ExportBufferCallback = () =>
 		{
 			var count = Interlocked.Increment(ref i);
-			_logger.LogInformation("Exported {Count} documents to Elasticsearch index {Format}",
-				count * endpoint.BufferSize, string.Format(options.IndexFormat, "latest"));
+			_logger.LogInformation("Exported {Count} documents to Elasticsearch index {IndexName}",
+				count * endpoint.BufferSize, Channel?.IndexName ?? string.Format(options.IndexFormat, "latest"));
 		};
 		options.ExportExceptionCallback = e =>
 		{
@@ -103,7 +103,7 @@ Func createOptions
 		};
 		options.ServerRejectionCallback = items => _logger.LogInformation("Server rejection: {Rejection}", items.First().Item2);
 		Channel = createChannel(options);
-		_logger.LogInformation($"Bootstrapping {nameof(SemanticIndexChannel)} Elasticsearch target for indexing");
+		_logger.LogInformation("Created {Channel} Elasticsearch target for indexing", typeof(TChannel).Name);
 	}
 
 	public async ValueTask StopAsync(Cancel ctx = default)
@@ -193,6 +193,34 @@ protected static string CreateMapping(string? inferenceId) =>
 		          "prefix": { "type": "text", "analyzer" : "hierarchy_analyzer" }
 		        }
 		      },
+		      "applies_to" : {
+		        "type" : "nested",
+		        "properties" : {
+		          "type" : { "type" : "keyword" },
+		          "sub-type" : { "type" : "keyword" },
+		          "lifecycle" : { "type" : "keyword" },
+		          "version" : { "type" : "version" }
+		        }
+		      },
+		      "parents" : {
+		        "type" : "object",
+		        "properties" : {
+		          "url" : {
+		            "type": "keyword",
+		            "fields": {
+		              "match": { "type": "text" },
+		              "prefix": { "type": "text", "analyzer" : "hierarchy_analyzer" }
+		            }
+		          },
+		          "title": {
+		            "type": "text",
+		            "search_analyzer": "synonyms_analyzer",
+		            "fields": {
+		              "keyword": { "type": "keyword" }
+		            }
+		          }
+		        }
+		      },
 		      "hash" : { "type" : "keyword" },
 		      "title": {
 		        "type": "text",
diff --git a/src/Elastic.Markdown/Exporters/Elasticsearch/ElasticsearchMarkdownExporter.cs b/src/Elastic.Markdown/Exporters/Elasticsearch/ElasticsearchMarkdownExporter.cs
index 64c3198e0..bba070ce4 100644
--- a/src/Elastic.Markdown/Exporters/Elasticsearch/ElasticsearchMarkdownExporter.cs
+++ b/src/Elastic.Markdown/Exporters/Elasticsearch/ElasticsearchMarkdownExporter.cs
@@ -3,6 +3,7 @@
 // See the LICENSE file in the project root for more information
 
 using System.IO.Abstractions;
+using Elastic.Documentation.AppliesTo;
 using Elastic.Documentation.Configuration;
 using Elastic.Documentation.Diagnostics;
 using Elastic.Documentation.Search;
@@ -33,6 +34,8 @@ public class ElasticsearchMarkdownExporter : IMarkdownExporter, IDisposable
 	private readonly DateTimeOffset _batchIndexDate = DateTimeOffset.UtcNow;
 	private readonly DistributedTransport _transport;
 	private IngestStrategy _indexStrategy;
+	private string _currentLexicalHash = string.Empty;
+	private string _currentSemanticHash = string.Empty;
 
 	public ElasticsearchMarkdownExporter(
 		ILoggerFactory logFactory,
@@ -80,26 +83,53 @@ string indexNamespace
 	/// 
 	public async ValueTask StartAsync(Cancel ctx = default)
 	{
+		_currentLexicalHash = await _lexicalChannel.Channel.GetIndexTemplateHashAsync(ctx) ?? string.Empty;
+		_currentSemanticHash = await _semanticChannel.Channel.GetIndexTemplateHashAsync(ctx) ?? string.Empty;
+
 		_ = await _lexicalChannel.Channel.BootstrapElasticsearchAsync(BootstrapMethod.Failure, null, ctx);
 
-		var semanticIndex = _semanticChannel.Channel.IndexName;
-		var semanticWriteAlias = string.Format(_semanticChannel.Channel.Options.IndexFormat, "latest");
-		var semanticIndexHead = await _transport.HeadAsync(semanticWriteAlias, ctx);
-		if (!semanticIndexHead.ApiCallDetails.HasSuccessfulStatusCode)
+		// if the previous hash does not match the current hash, we know already we want to multiplex to a new index
+		if (_currentLexicalHash != _lexicalChannel.Channel.ChannelHash)
+			_indexStrategy = IngestStrategy.Multiplex;
+
+		if (!_endpoint.NoSemantic)
 		{
-			_logger.LogInformation("No semantic index exists yet, creating index {Index} for semantic search", semanticIndex);
-			_ = await _semanticChannel.Channel.BootstrapElasticsearchAsync(BootstrapMethod.Failure, null, ctx);
-			var semanticIndexPut = await _transport.PutAsync(semanticIndex, PostData.String("{}"), ctx);
-			if (!semanticIndexPut.ApiCallDetails.HasSuccessfulStatusCode)
-				throw new Exception($"Failed to create index {semanticIndex}: {semanticIndexPut}");
-			_ = await _semanticChannel.Channel.ApplyAliasesAsync(ctx);
-			if (!_endpoint.ForceReindex)
+			var semanticWriteAlias = string.Format(_semanticChannel.Channel.Options.IndexFormat, "latest");
+			var semanticIndexAvailable = await _transport.HeadAsync(semanticWriteAlias, ctx);
+			if (!semanticIndexAvailable.ApiCallDetails.HasSuccessfulStatusCode && _endpoint is { ForceReindex: false, NoSemantic: false })
 			{
 				_indexStrategy = IngestStrategy.Multiplex;
-				_logger.LogInformation("Index strategy set to multiplex because {SemanticIndex} does not exist, pass --force-reindex to always use reindex", semanticIndex);
+				_logger.LogInformation("Index strategy set to multiplex because {SemanticIndex} does not exist, pass --force-reindex to always use reindex", semanticWriteAlias);
 			}
+
+			//try re-use index if we are re-indexing. Multiplex should always go to a new index
+			_semanticChannel.Channel.Options.TryReuseIndex = _indexStrategy == IngestStrategy.Reindex;
+			_ = await _semanticChannel.Channel.BootstrapElasticsearchAsync(BootstrapMethod.Failure, null, ctx);
 		}
+
+		var lexicalIndexExists = await IndexExists(_lexicalChannel.Channel.IndexName) ? "existing" : "new";
+		var semanticIndexExists = await IndexExists(_semanticChannel.Channel.IndexName) ? "existing" : "new";
+		if (_currentLexicalHash != _lexicalChannel.Channel.ChannelHash)
+		{
+			_indexStrategy = IngestStrategy.Multiplex;
+			_logger.LogInformation("Multiplexing lexical new index: '{Index}' since current hash on server '{HashCurrent}' does not match new '{HashNew}'",
+				_lexicalChannel.Channel.IndexName, _currentLexicalHash, _lexicalChannel.Channel.ChannelHash);
+		}
+		else
+			_logger.LogInformation("Targeting {State} lexical: '{Index}'", lexicalIndexExists, _lexicalChannel.Channel.IndexName);
+
+		if (!_endpoint.NoSemantic && _currentSemanticHash != _semanticChannel.Channel.ChannelHash)
+		{
+			_indexStrategy = IngestStrategy.Multiplex;
+			_logger.LogInformation("Multiplexing new index '{Index}' since current hash on server '{HashCurrent}' does not match new '{HashNew}'",
+				_semanticChannel.Channel.IndexName, _currentSemanticHash, _semanticChannel.Channel.ChannelHash);
+		}
+		else if (!_endpoint.NoSemantic)
+			_logger.LogInformation("Targeting {State} semantical: '{Index}'", semanticIndexExists, _semanticChannel.Channel.IndexName);
+
 		_logger.LogInformation("Using {IndexStrategy} to sync lexical index to semantic index", _indexStrategy.ToStringFast(true));
+
+		async ValueTask IndexExists(string name) => (await _transport.HeadAsync(name, ctx)).ApiCallDetails.HasSuccessfulStatusCode;
 	}
 
 	private async ValueTask CountAsync(string index, string body, Cancel ctx = default)
@@ -113,7 +143,6 @@ public async ValueTask StopAsync(Cancel ctx = default)
 	{
 		var semanticWriteAlias = string.Format(_semanticChannel.Channel.Options.IndexFormat, "latest");
 		var lexicalWriteAlias = string.Format(_lexicalChannel.Channel.Options.IndexFormat, "latest");
-		var semanticIndex = _semanticChannel.Channel.IndexName;
 
 		var stopped = await _lexicalChannel.StopAsync(ctx);
 		if (!stopped)
@@ -125,23 +154,28 @@ public async ValueTask StopAsync(Cancel ctx = default)
 		{
 			if (!_endpoint.NoSemantic)
 				_ = await _semanticChannel.StopAsync(ctx);
-			else
-				_logger.LogInformation("--no-semantic was specified when doing multiplex writes, not rolling over {SemanticIndex}", semanticIndex);
 
 			// cleanup lexical index of old data
 			await DoDeleteByQuery(lexicalWriteAlias, ctx);
+			// need to refresh the lexical index to ensure that the delete by query is available
 			_ = await _lexicalChannel.RefreshAsync(ctx);
-			_logger.LogInformation("Finish sync to semantic index using {IndexStrategy} strategy", _indexStrategy.ToStringFast(true));
 			await QueryDocumentCounts(ctx);
+			// ReSharper disable once ConvertIfStatementToConditionalTernaryExpression
+			if (_endpoint.NoSemantic)
+				_logger.LogInformation("Finish indexing {IndexStrategy} strategy", _indexStrategy.ToStringFast(true));
+			else
+				_logger.LogInformation("Finish syncing to semantic in {IndexStrategy} strategy", _indexStrategy.ToStringFast(true));
 			return;
 		}
 
 		if (_endpoint.NoSemantic)
 		{
-			_logger.LogInformation("--no-semantic was specified so exiting early before reindexing to {Index}", semanticIndex);
+			_logger.LogInformation("--no-semantic was specified so exiting early before reindexing to {Index}", lexicalWriteAlias);
 			return;
 		}
 
+		var semanticIndex = _semanticChannel.Channel.IndexName;
+		// check if the alias exists
 		var semanticIndexHead = await _transport.HeadAsync(semanticWriteAlias, ctx);
 		if (!semanticIndexHead.ApiCallDetails.HasSuccessfulStatusCode)
 		{
@@ -150,14 +184,14 @@ public async ValueTask StopAsync(Cancel ctx = default)
 			var semanticIndexPut = await _transport.PutAsync(semanticIndex, PostData.String("{}"), ctx);
 			if (!semanticIndexPut.ApiCallDetails.HasSuccessfulStatusCode)
 				throw new Exception($"Failed to create index {semanticIndex}: {semanticIndexPut}");
-			_ = await _semanticChannel.Channel.ApplyAliasesAsync(ctx);
 		}
+		var destinationIndex = _semanticChannel.Channel.IndexName;
 
-		_logger.LogInformation("_reindex updates: '{SourceIndex}' => '{DestinationIndex}'", lexicalWriteAlias, semanticWriteAlias);
+		_logger.LogInformation("_reindex updates: '{SourceIndex}' => '{DestinationIndex}'", lexicalWriteAlias, destinationIndex);
 		var request = PostData.String(@"
 		{
 			""dest"": {
-				""index"": """ + semanticWriteAlias + @"""
+				""index"": """ + destinationIndex + @"""
 			},
 			""source"": {
 				""index"": """ + lexicalWriteAlias + @""",
@@ -171,13 +205,13 @@ public async ValueTask StopAsync(Cancel ctx = default)
 				}
 			}
 		}");
-		await DoReindex(request, lexicalWriteAlias, semanticWriteAlias, "updates", ctx);
+		await DoReindex(request, lexicalWriteAlias, destinationIndex, "updates", ctx);
 
-		_logger.LogInformation("_reindex deletions: '{SourceIndex}' => '{DestinationIndex}'", lexicalWriteAlias, semanticWriteAlias);
+		_logger.LogInformation("_reindex deletions: '{SourceIndex}' => '{DestinationIndex}'", lexicalWriteAlias, destinationIndex);
 		request = PostData.String(@"
 		{
 			""dest"": {
-				""index"": """ + semanticWriteAlias + @"""
+				""index"": """ + destinationIndex + @"""
 			},
 			""script"": {
 				""source"": ""ctx.op = \""delete\""""
@@ -194,10 +228,13 @@ public async ValueTask StopAsync(Cancel ctx = default)
 				}
 			}
 		}");
-		await DoReindex(request, lexicalWriteAlias, semanticWriteAlias, "deletions", ctx);
+		await DoReindex(request, lexicalWriteAlias, destinationIndex, "deletions", ctx);
 
 		await DoDeleteByQuery(lexicalWriteAlias, ctx);
 
+		_ = await _lexicalChannel.Channel.ApplyLatestAliasAsync(ctx);
+		_ = await _semanticChannel.Channel.ApplyAliasesAsync(ctx);
+
 		_ = await _lexicalChannel.RefreshAsync(ctx);
 		_ = await _semanticChannel.RefreshAsync(ctx);
 
@@ -275,7 +312,7 @@ private async ValueTask DoDeleteByQuery(string lexicalWriteAlias, Cancel ctx)
 
 	private async ValueTask DoReindex(PostData request, string lexicalWriteAlias, string semanticWriteAlias, string typeOfSync, Cancel ctx)
 	{
-		var reindexUrl = "/_reindex?wait_for_completion=false&require_alias=true&scroll=10m";
+		var reindexUrl = "/_reindex?wait_for_completion=false&scroll=10m";
 		var reindexNewChanges = await _transport.PostAsync(reindexUrl, request, ctx);
 		var taskId = reindexNewChanges.Body.Get("task");
 		if (string.IsNullOrWhiteSpace(taskId))
@@ -336,6 +373,10 @@ public async ValueTask ExportAsync(MarkdownExportFileContext fileContext,
 			? body[..Math.Min(body.Length, 400)] + " " + string.Join(" \n- ", headings)
 			: string.Empty;
 
+		// this is temporary until https://github.com/elastic/docs-builder/pull/2070 lands
+		// this PR will add a service for us to resolve to a versioning scheme.
+		var appliesTo = fileContext.SourceFile.YamlFrontMatter?.AppliesTo ?? ApplicableTo.Default;
+
 		var doc = new DocumentationDocument
 		{
 			Url = url,
@@ -344,7 +385,7 @@ public async ValueTask ExportAsync(MarkdownExportFileContext fileContext,
 			StrippedBody = body.StripMarkdown(),
 			Description = fileContext.SourceFile.YamlFrontMatter?.Description,
 			Abstract = @abstract,
-			Applies = fileContext.SourceFile.YamlFrontMatter?.AppliesTo,
+			Applies = appliesTo,
 			UrlSegmentCount = url.Split('/', StringSplitOptions.RemoveEmptyEntries).Length,
 			Parents = navigation.GetParentsOfMarkdownFile(file).Select(i => new ParentDocument
 			{
@@ -357,7 +398,7 @@ public async ValueTask ExportAsync(MarkdownExportFileContext fileContext,
 		var semanticHash = _semanticChannel.Channel.ChannelHash;
 		var lexicalHash = _lexicalChannel.Channel.ChannelHash;
 		var hash = HashedBulkUpdate.CreateHash(semanticHash, lexicalHash,
-			doc.Url, doc.Body ?? string.Empty, string.Join(",", doc.Headings.OrderBy(h => h))
+			doc.Url, doc.Body ?? string.Empty, string.Join(",", doc.Headings.OrderBy(h => h)), doc.Url
 		);
 		doc.Hash = hash;
 		doc.LastUpdated = _batchIndexDate;
diff --git a/tests/Elastic.Markdown.Tests/AppliesTo/ApplicableToJsonConverterRoundTripTests.cs b/tests/Elastic.Markdown.Tests/AppliesTo/ApplicableToJsonConverterRoundTripTests.cs
new file mode 100644
index 000000000..3b22299f8
--- /dev/null
+++ b/tests/Elastic.Markdown.Tests/AppliesTo/ApplicableToJsonConverterRoundTripTests.cs
@@ -0,0 +1,380 @@
+// 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.Text.Json;
+using Elastic.Documentation;
+using Elastic.Documentation.AppliesTo;
+using FluentAssertions;
+
+namespace Elastic.Markdown.Tests.AppliesTo;
+
+public class ApplicableToJsonConverterRoundTripTests
+{
+	private readonly JsonSerializerOptions _options = new() { WriteIndented = true };
+
+	[Fact]
+	public void RoundTripStackSimple()
+	{
+		var original = new ApplicableTo
+		{
+			Stack = AppliesCollection.GenerallyAvailable
+		};
+
+		var json = JsonSerializer.Serialize(original, _options);
+		var deserialized = JsonSerializer.Deserialize(json, _options);
+
+		deserialized.Should().NotBeNull();
+		deserialized!.Stack.Should().NotBeNull();
+		deserialized.Stack.Should().BeEquivalentTo(original.Stack);
+	}
+
+	[Fact]
+	public void RoundTripStackWithVersion()
+	{
+		var original = new ApplicableTo
+		{
+			Stack = new AppliesCollection(
+			[
+				new Applicability { Lifecycle = ProductLifecycle.GenerallyAvailable, Version = new SemVersion(8, 0, 0) },
+				new Applicability { Lifecycle = ProductLifecycle.Beta, Version = new SemVersion(7, 17, 0) }
+			])
+		};
+
+		var json = JsonSerializer.Serialize(original, _options);
+		var deserialized = JsonSerializer.Deserialize(json, _options);
+
+		deserialized.Should().NotBeNull();
+		deserialized!.Stack.Should().NotBeNull();
+		deserialized.Stack.Should().BeEquivalentTo(original.Stack);
+	}
+
+	[Fact]
+	public void RoundTripDeploymentAllProperties()
+	{
+		var original = new ApplicableTo
+		{
+			Deployment = new DeploymentApplicability
+			{
+				Self = AppliesCollection.GenerallyAvailable,
+				Ece = new AppliesCollection([new Applicability { Lifecycle = ProductLifecycle.GenerallyAvailable, Version = (SemVersion)"3.0.0" }]),
+				Eck = new AppliesCollection([new Applicability { Lifecycle = ProductLifecycle.Beta, Version = (SemVersion)"2.0.0" }]),
+				Ess = AppliesCollection.GenerallyAvailable
+			}
+		};
+
+		var json = JsonSerializer.Serialize(original, _options);
+		var deserialized = JsonSerializer.Deserialize(json, _options);
+
+		deserialized.Should().NotBeNull();
+		deserialized!.Deployment.Should().NotBeNull();
+		deserialized.Deployment!.Self.Should().BeEquivalentTo(original.Deployment!.Self);
+		deserialized.Deployment.Ece.Should().BeEquivalentTo(original.Deployment.Ece);
+		deserialized.Deployment.Eck.Should().BeEquivalentTo(original.Deployment.Eck);
+		deserialized.Deployment.Ess.Should().BeEquivalentTo(original.Deployment.Ess);
+	}
+
+	[Fact]
+	public void RoundTripServerlessAllProperties()
+	{
+		var original = new ApplicableTo
+		{
+			Serverless = new ServerlessProjectApplicability
+			{
+				Elasticsearch = AppliesCollection.GenerallyAvailable,
+				Observability = new AppliesCollection([new Applicability { Lifecycle = ProductLifecycle.TechnicalPreview, Version = AllVersions.Instance }]),
+				Security = new AppliesCollection([new Applicability { Lifecycle = ProductLifecycle.Beta, Version = (SemVersion)"1.0.0" }])
+			}
+		};
+
+		var json = JsonSerializer.Serialize(original, _options);
+		var deserialized = JsonSerializer.Deserialize(json, _options);
+
+		deserialized.Should().NotBeNull();
+		deserialized!.Serverless.Should().NotBeNull();
+		deserialized.Serverless!.Elasticsearch.Should().BeEquivalentTo(original.Serverless!.Elasticsearch);
+		deserialized.Serverless.Observability.Should().BeEquivalentTo(original.Serverless.Observability);
+		deserialized.Serverless.Security.Should().BeEquivalentTo(original.Serverless.Security);
+	}
+
+	[Fact]
+	public void RoundTripProductSimple()
+	{
+		var original = new ApplicableTo
+		{
+			Product = AppliesCollection.GenerallyAvailable
+		};
+
+		var json = JsonSerializer.Serialize(original, _options);
+		var deserialized = JsonSerializer.Deserialize(json, _options);
+
+		deserialized.Should().NotBeNull();
+		deserialized!.Product.Should().NotBeNull();
+		deserialized.Product.Should().BeEquivalentTo(original.Product);
+	}
+
+	[Fact]
+	public void RoundTripProductApplicabilitySingleProduct()
+	{
+		var original = new ApplicableTo
+		{
+			ProductApplicability = new ProductApplicability
+			{
+				Ecctl = AppliesCollection.GenerallyAvailable
+			}
+		};
+
+		var json = JsonSerializer.Serialize(original, _options);
+		var deserialized = JsonSerializer.Deserialize(json, _options);
+
+		deserialized.Should().NotBeNull();
+		deserialized!.ProductApplicability.Should().NotBeNull();
+		deserialized.ProductApplicability!.Ecctl.Should().BeEquivalentTo(original.ProductApplicability!.Ecctl);
+	}
+
+	[Fact]
+	public void RoundTripProductApplicabilityMultipleProducts()
+	{
+		var original = new ApplicableTo
+		{
+			ProductApplicability = new ProductApplicability
+			{
+				Ecctl = AppliesCollection.GenerallyAvailable,
+				Curator = new AppliesCollection([new Applicability { Lifecycle = ProductLifecycle.Deprecated, Version = (SemVersion)"5.0.0" }]),
+				ApmAgentDotnet = new AppliesCollection([new Applicability { Lifecycle = ProductLifecycle.GenerallyAvailable, Version = (SemVersion)"1.2.0" }]),
+				EdotDotnet = new AppliesCollection([new Applicability { Lifecycle = ProductLifecycle.Beta, Version = (SemVersion)"0.9.0" }])
+			}
+		};
+
+		var json = JsonSerializer.Serialize(original, _options);
+		var deserialized = JsonSerializer.Deserialize(json, _options);
+
+		deserialized.Should().NotBeNull();
+		deserialized!.ProductApplicability.Should().NotBeNull();
+		deserialized.ProductApplicability!.Ecctl.Should().BeEquivalentTo(original.ProductApplicability!.Ecctl);
+		deserialized.ProductApplicability.Curator.Should().BeEquivalentTo(original.ProductApplicability.Curator);
+		deserialized.ProductApplicability.ApmAgentDotnet.Should().BeEquivalentTo(original.ProductApplicability.ApmAgentDotnet);
+		deserialized.ProductApplicability.EdotDotnet.Should().BeEquivalentTo(original.ProductApplicability.EdotDotnet);
+	}
+
+	[Fact]
+	public void RoundTripAllProductApplicabilityProperties()
+	{
+		var original = new ApplicableTo
+		{
+			ProductApplicability = new ProductApplicability
+			{
+				Ecctl = AppliesCollection.GenerallyAvailable,
+				Curator = new AppliesCollection([new Applicability { Lifecycle = ProductLifecycle.Deprecated, Version = (SemVersion)"5.0.0" }]),
+				ApmAgentAndroid = new AppliesCollection([new Applicability { Lifecycle = ProductLifecycle.Beta, Version = (SemVersion)"1.0.0" }]),
+				ApmAgentDotnet = new AppliesCollection([new Applicability { Lifecycle = ProductLifecycle.GenerallyAvailable, Version = (SemVersion)"1.2.0" }]),
+				ApmAgentGo = new AppliesCollection([new Applicability { Lifecycle = ProductLifecycle.GenerallyAvailable, Version = (SemVersion)"2.0.0" }]),
+				ApmAgentIos = new AppliesCollection([new Applicability { Lifecycle = ProductLifecycle.TechnicalPreview, Version = (SemVersion)"0.5.0" }]),
+				ApmAgentJava = new AppliesCollection([new Applicability { Lifecycle = ProductLifecycle.GenerallyAvailable, Version = (SemVersion)"1.30.0" }]),
+				ApmAgentNode = new AppliesCollection([new Applicability { Lifecycle = ProductLifecycle.GenerallyAvailable, Version = (SemVersion)"3.0.0" }]),
+				ApmAgentPhp = new AppliesCollection([new Applicability { Lifecycle = ProductLifecycle.GenerallyAvailable, Version = (SemVersion)"1.8.0" }]),
+				ApmAgentPython = new AppliesCollection([new Applicability { Lifecycle = ProductLifecycle.GenerallyAvailable, Version = (SemVersion)"6.0.0" }]),
+				ApmAgentRuby = new AppliesCollection([new Applicability { Lifecycle = ProductLifecycle.GenerallyAvailable, Version = (SemVersion)"4.0.0" }]),
+				ApmAgentRumJs = new AppliesCollection([new Applicability { Lifecycle = ProductLifecycle.GenerallyAvailable, Version = (SemVersion)"5.0.0" }]),
+				EdotIos = new AppliesCollection([new Applicability { Lifecycle = ProductLifecycle.Beta, Version = (SemVersion)"0.9.0" }]),
+				EdotAndroid = new AppliesCollection([new Applicability { Lifecycle = ProductLifecycle.Beta, Version = (SemVersion)"0.8.0" }]),
+				EdotDotnet = new AppliesCollection([new Applicability { Lifecycle = ProductLifecycle.Beta, Version = (SemVersion)"0.9.0" }]),
+				EdotJava = new AppliesCollection([new Applicability { Lifecycle = ProductLifecycle.Beta, Version = (SemVersion)"0.7.0" }]),
+				EdotNode = new AppliesCollection([new Applicability { Lifecycle = ProductLifecycle.Beta, Version = (SemVersion)"0.6.0" }]),
+				EdotPhp = new AppliesCollection([new Applicability { Lifecycle = ProductLifecycle.Beta, Version = (SemVersion)"0.5.0" }]),
+				EdotPython = new AppliesCollection([new Applicability { Lifecycle = ProductLifecycle.Beta, Version = (SemVersion)"0.4.0" }]),
+				EdotCfAws = new AppliesCollection([new Applicability { Lifecycle = ProductLifecycle.TechnicalPreview, Version = (SemVersion)"0.3.0" }]),
+				EdotCfAzure = new AppliesCollection([new Applicability { Lifecycle = ProductLifecycle.TechnicalPreview, Version = (SemVersion)"0.2.0" }]),
+				EdotCollector = new AppliesCollection([new Applicability { Lifecycle = ProductLifecycle.GenerallyAvailable, Version = (SemVersion)"1.0.0" }])
+			}
+		};
+
+		var json = JsonSerializer.Serialize(original, _options);
+		var deserialized = JsonSerializer.Deserialize(json, _options);
+
+		deserialized.Should().NotBeNull();
+		deserialized!.ProductApplicability.Should().NotBeNull();
+		deserialized.ProductApplicability!.Ecctl.Should().BeEquivalentTo(original.ProductApplicability!.Ecctl);
+		deserialized.ProductApplicability.Curator.Should().BeEquivalentTo(original.ProductApplicability.Curator);
+		deserialized.ProductApplicability.ApmAgentAndroid.Should().BeEquivalentTo(original.ProductApplicability.ApmAgentAndroid);
+		deserialized.ProductApplicability.ApmAgentDotnet.Should().BeEquivalentTo(original.ProductApplicability.ApmAgentDotnet);
+		deserialized.ProductApplicability.ApmAgentGo.Should().BeEquivalentTo(original.ProductApplicability.ApmAgentGo);
+		deserialized.ProductApplicability.ApmAgentIos.Should().BeEquivalentTo(original.ProductApplicability.ApmAgentIos);
+		deserialized.ProductApplicability.ApmAgentJava.Should().BeEquivalentTo(original.ProductApplicability.ApmAgentJava);
+		deserialized.ProductApplicability.ApmAgentNode.Should().BeEquivalentTo(original.ProductApplicability.ApmAgentNode);
+		deserialized.ProductApplicability.ApmAgentPhp.Should().BeEquivalentTo(original.ProductApplicability.ApmAgentPhp);
+		deserialized.ProductApplicability.ApmAgentPython.Should().BeEquivalentTo(original.ProductApplicability.ApmAgentPython);
+		deserialized.ProductApplicability.ApmAgentRuby.Should().BeEquivalentTo(original.ProductApplicability.ApmAgentRuby);
+		deserialized.ProductApplicability.ApmAgentRumJs.Should().BeEquivalentTo(original.ProductApplicability.ApmAgentRumJs);
+		deserialized.ProductApplicability.EdotIos.Should().BeEquivalentTo(original.ProductApplicability.EdotIos);
+		deserialized.ProductApplicability.EdotAndroid.Should().BeEquivalentTo(original.ProductApplicability.EdotAndroid);
+		deserialized.ProductApplicability.EdotDotnet.Should().BeEquivalentTo(original.ProductApplicability.EdotDotnet);
+		deserialized.ProductApplicability.EdotJava.Should().BeEquivalentTo(original.ProductApplicability.EdotJava);
+		deserialized.ProductApplicability.EdotNode.Should().BeEquivalentTo(original.ProductApplicability.EdotNode);
+		deserialized.ProductApplicability.EdotPhp.Should().BeEquivalentTo(original.ProductApplicability.EdotPhp);
+		deserialized.ProductApplicability.EdotPython.Should().BeEquivalentTo(original.ProductApplicability.EdotPython);
+		deserialized.ProductApplicability.EdotCfAws.Should().BeEquivalentTo(original.ProductApplicability.EdotCfAws);
+		deserialized.ProductApplicability.EdotCfAzure.Should().BeEquivalentTo(original.ProductApplicability.EdotCfAzure);
+		deserialized.ProductApplicability.EdotCollector.Should().BeEquivalentTo(original.ProductApplicability.EdotCollector);
+	}
+
+	[Fact]
+	public void RoundTripComplexAllFieldsPopulated()
+	{
+		var original = new ApplicableTo
+		{
+			Stack = new AppliesCollection(
+			[
+				new Applicability { Lifecycle = ProductLifecycle.GenerallyAvailable, Version = (SemVersion)"8.0.0" },
+				new Applicability { Lifecycle = ProductLifecycle.Beta, Version = (SemVersion)"7.17.0" }
+			]),
+			Deployment = new DeploymentApplicability
+			{
+				Self = AppliesCollection.GenerallyAvailable,
+				Ece = new AppliesCollection([new Applicability { Lifecycle = ProductLifecycle.GenerallyAvailable, Version = (SemVersion)"3.0.0" }]),
+				Eck = new AppliesCollection([new Applicability { Lifecycle = ProductLifecycle.Beta, Version = (SemVersion)"2.0.0" }]),
+				Ess = AppliesCollection.GenerallyAvailable
+			},
+			Serverless = new ServerlessProjectApplicability
+			{
+				Elasticsearch = AppliesCollection.GenerallyAvailable,
+				Observability = new AppliesCollection([new Applicability { Lifecycle = ProductLifecycle.TechnicalPreview, Version = AllVersions.Instance }]),
+				Security = new AppliesCollection([new Applicability { Lifecycle = ProductLifecycle.Beta, Version = (SemVersion)"1.0.0" }])
+			},
+			Product = AppliesCollection.GenerallyAvailable,
+			ProductApplicability = new ProductApplicability
+			{
+				Ecctl = AppliesCollection.GenerallyAvailable,
+				ApmAgentDotnet = new AppliesCollection([new Applicability { Lifecycle = ProductLifecycle.GenerallyAvailable, Version = (SemVersion)"1.2.0" }])
+			}
+		};
+
+		var json = JsonSerializer.Serialize(original, _options);
+		var deserialized = JsonSerializer.Deserialize(json, _options);
+
+		deserialized.Should().NotBeNull();
+		deserialized!.Stack.Should().BeEquivalentTo(original.Stack);
+		deserialized.Deployment.Should().NotBeNull();
+		deserialized.Deployment!.Self.Should().BeEquivalentTo(original.Deployment!.Self);
+		deserialized.Deployment.Ece.Should().BeEquivalentTo(original.Deployment.Ece);
+		deserialized.Deployment.Eck.Should().BeEquivalentTo(original.Deployment.Eck);
+		deserialized.Deployment.Ess.Should().BeEquivalentTo(original.Deployment.Ess);
+		deserialized.Serverless.Should().NotBeNull();
+		deserialized.Serverless!.Elasticsearch.Should().BeEquivalentTo(original.Serverless!.Elasticsearch);
+		deserialized.Serverless.Observability.Should().BeEquivalentTo(original.Serverless.Observability);
+		deserialized.Serverless.Security.Should().BeEquivalentTo(original.Serverless.Security);
+		deserialized.Product.Should().BeEquivalentTo(original.Product);
+		deserialized.ProductApplicability.Should().NotBeNull();
+		deserialized.ProductApplicability!.Ecctl.Should().BeEquivalentTo(original.ProductApplicability!.Ecctl);
+		deserialized.ProductApplicability.ApmAgentDotnet.Should().BeEquivalentTo(original.ProductApplicability.ApmAgentDotnet);
+	}
+
+	[Fact]
+	public void RoundTripAllLifecycles()
+	{
+		var lifecycles = Enum.GetValues();
+		var applicabilities = lifecycles.Select(lc =>
+			new Applicability { Lifecycle = lc, Version = (SemVersion)"1.0.0" }
+		).ToArray();
+
+		var original = new ApplicableTo
+		{
+			Stack = new AppliesCollection(applicabilities)
+		};
+
+		var json = JsonSerializer.Serialize(original, _options);
+		var deserialized = JsonSerializer.Deserialize(json, _options);
+
+		deserialized.Should().NotBeNull();
+		deserialized!.Stack.Should().NotBeNull();
+		deserialized.Stack.Should().BeEquivalentTo(original.Stack);
+	}
+
+	[Fact]
+	public void RoundTripMultipleApplicabilitiesInCollection()
+	{
+		var original = new ApplicableTo
+		{
+			Stack = new AppliesCollection(
+			[
+				new Applicability { Lifecycle = ProductLifecycle.GenerallyAvailable, Version = (SemVersion)"8.0.0" },
+				new Applicability { Lifecycle = ProductLifecycle.Beta, Version = (SemVersion)"7.17.0" },
+				new Applicability { Lifecycle = ProductLifecycle.TechnicalPreview, Version = (SemVersion)"7.16.0" },
+				new Applicability { Lifecycle = ProductLifecycle.Deprecated, Version = (SemVersion)"6.0.0" }
+			])
+		};
+
+		var json = JsonSerializer.Serialize(original, _options);
+		var deserialized = JsonSerializer.Deserialize(json, _options);
+
+		deserialized.Should().NotBeNull();
+		deserialized!.Stack.Should().NotBeNull();
+		deserialized.Stack.Should().HaveCount(4);
+		deserialized.Stack.Should().BeEquivalentTo(original.Stack);
+	}
+
+	[Fact]
+	public void RoundTripEmptyApplicableTo()
+	{
+		var original = new ApplicableTo();
+
+		var json = JsonSerializer.Serialize(original, _options);
+		var deserialized = JsonSerializer.Deserialize(json, _options);
+
+		deserialized.Should().NotBeNull();
+		deserialized!.Stack.Should().BeNull();
+		deserialized.Deployment.Should().BeNull();
+		deserialized.Serverless.Should().BeNull();
+		deserialized.Product.Should().BeNull();
+		deserialized.ProductApplicability.Should().BeNull();
+	}
+
+	[Fact]
+	public void RoundTripNullReturnsNull()
+	{
+		ApplicableTo? original = null;
+
+		var json = JsonSerializer.Serialize(original, _options);
+		var deserialized = JsonSerializer.Deserialize(json, _options);
+
+		deserialized.Should().BeNull();
+	}
+
+	[Fact]
+	public void RoundTripAllVersionsSerializesAsSemanticVersion()
+	{
+		var original = new ApplicableTo
+		{
+			Stack = new AppliesCollection([new Applicability { Lifecycle = ProductLifecycle.GenerallyAvailable, Version = AllVersions.Instance }])
+		};
+
+		var json = JsonSerializer.Serialize(original, _options);
+		json.Should().Contain("\"version\": \"9999.9999.9999\"");
+
+		var deserialized = JsonSerializer.Deserialize(json, _options);
+		deserialized.Should().NotBeNull();
+		deserialized!.Stack.Should().NotBeNull();
+		deserialized.Stack!.First().Version.Should().Be(AllVersions.Instance);
+	}
+
+	[Fact]
+	public void RoundTripProductAndProductApplicabilityBothPresent()
+	{
+		var original = new ApplicableTo
+		{
+			Product = AppliesCollection.GenerallyAvailable,
+			ProductApplicability = new ProductApplicability
+			{
+				Ecctl = new AppliesCollection([new Applicability { Lifecycle = ProductLifecycle.Beta, Version = (SemVersion)"1.0.0" }])
+			}
+		};
+
+		var json = JsonSerializer.Serialize(original, _options);
+		var deserialized = JsonSerializer.Deserialize(json, _options);
+
+		deserialized.Should().NotBeNull();
+		deserialized!.Product.Should().BeEquivalentTo(original.Product);
+		deserialized.ProductApplicability.Should().NotBeNull();
+		deserialized.ProductApplicability!.Ecctl.Should().BeEquivalentTo(original.ProductApplicability!.Ecctl);
+	}
+}
diff --git a/tests/Elastic.Markdown.Tests/AppliesTo/ApplicableToJsonConverterSerializationTests.cs b/tests/Elastic.Markdown.Tests/AppliesTo/ApplicableToJsonConverterSerializationTests.cs
new file mode 100644
index 000000000..fbff52703
--- /dev/null
+++ b/tests/Elastic.Markdown.Tests/AppliesTo/ApplicableToJsonConverterSerializationTests.cs
@@ -0,0 +1,394 @@
+// 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.Text.Json;
+using Elastic.Documentation;
+using Elastic.Documentation.AppliesTo;
+using FluentAssertions;
+
+namespace Elastic.Markdown.Tests.AppliesTo;
+
+public class ApplicableToJsonConverterSerializationTests
+{
+	private readonly JsonSerializerOptions _options = new()
+	{
+		WriteIndented = true
+	};
+
+	[Fact]
+	public void SerializeStackProducesCorrectJson()
+	{
+		var applicableTo = new ApplicableTo
+		{
+			Stack = AppliesCollection.GenerallyAvailable
+		};
+
+		var json = JsonSerializer.Serialize(applicableTo, _options);
+
+		// language=json
+		json.Should().Be(
+			"""
+			[
+			  {
+			    "type": "stack",
+			    "sub_type": "stack",
+			    "lifecycle": "ga",
+			    "version": "9999.9999.9999"
+			  }
+			]
+			""");
+	}
+
+	[Fact]
+	public void SerializeStackWithVersionProducesCorrectJson()
+	{
+		var applicableTo = new ApplicableTo
+		{
+			Stack = new AppliesCollection([
+				new Applicability
+				{
+					Lifecycle = ProductLifecycle.Beta,
+					Version = (SemVersion)"8.0.0"
+				}
+			])
+		};
+
+		var json = JsonSerializer.Serialize(applicableTo, _options);
+
+		// language=json
+		json.Should().Be(
+			"""
+			[
+			  {
+			    "type": "stack",
+			    "sub_type": "stack",
+			    "lifecycle": "beta",
+			    "version": "8.0.0"
+			  }
+			]
+			""");
+	}
+
+	[Fact]
+	public void SerializeMultipleApplicabilitiesProducesCorrectJson()
+	{
+		var applicableTo = new ApplicableTo
+		{
+			Stack = new AppliesCollection(
+			[
+				new Applicability
+				{
+					Lifecycle = ProductLifecycle.GenerallyAvailable,
+					Version = (SemVersion)"8.0.0"
+				},
+				new Applicability
+				{
+					Lifecycle = ProductLifecycle.Beta,
+					Version = (SemVersion)"7.17.0"
+				}
+			])
+		};
+
+		var json = JsonSerializer.Serialize(applicableTo, _options);
+
+		// language=json
+		json.Should().Be(
+			"""
+			[
+			  {
+			    "type": "stack",
+			    "sub_type": "stack",
+			    "lifecycle": "ga",
+			    "version": "8.0.0"
+			  },
+			  {
+			    "type": "stack",
+			    "sub_type": "stack",
+			    "lifecycle": "beta",
+			    "version": "7.17.0"
+			  }
+			]
+			""");
+	}
+
+	[Fact]
+	public void SerializeDeploymentProducesCorrectJson()
+	{
+		var applicableTo = new ApplicableTo
+		{
+			Deployment = new DeploymentApplicability
+			{
+				Ece = new AppliesCollection([
+					new Applicability
+					{
+						Lifecycle = ProductLifecycle.GenerallyAvailable,
+						Version = (SemVersion)"3.0.0"
+					}
+				]),
+				Ess = AppliesCollection.GenerallyAvailable
+			}
+		};
+
+		var json = JsonSerializer.Serialize(applicableTo, _options);
+
+		// language=json
+		json.Should().Be(
+			"""
+			[
+			  {
+			    "type": "deployment",
+			    "sub_type": "ece",
+			    "lifecycle": "ga",
+			    "version": "3.0.0"
+			  },
+			  {
+			    "type": "deployment",
+			    "sub_type": "ess",
+			    "lifecycle": "ga",
+			    "version": "9999.9999.9999"
+			  }
+			]
+			""");
+	}
+
+	[Fact]
+	public void SerializeServerlessProducesCorrectJson()
+	{
+		var applicableTo = new ApplicableTo
+		{
+			Serverless = new ServerlessProjectApplicability
+			{
+				Elasticsearch = new AppliesCollection([
+					new Applicability
+					{
+						Lifecycle = ProductLifecycle.Beta,
+						Version = (SemVersion)"1.0.0"
+					}
+				]),
+				Security = AppliesCollection.GenerallyAvailable
+			}
+		};
+
+		var json = JsonSerializer.Serialize(applicableTo, _options);
+
+		// language=json
+		json.Should().Be(
+			"""
+			[
+			  {
+			    "type": "serverless",
+			    "sub_type": "elasticsearch",
+			    "lifecycle": "beta",
+			    "version": "1.0.0"
+			  },
+			  {
+			    "type": "serverless",
+			    "sub_type": "security",
+			    "lifecycle": "ga",
+			    "version": "9999.9999.9999"
+			  }
+			]
+			""");
+	}
+
+	[Fact]
+	public void SerializeProductProducesCorrectJson()
+	{
+		var applicableTo = new ApplicableTo
+		{
+			Product = new AppliesCollection([
+				new Applicability
+				{
+					Lifecycle = ProductLifecycle.TechnicalPreview,
+					Version = (SemVersion)"0.5.0"
+				}
+			])
+		};
+
+		var json = JsonSerializer.Serialize(applicableTo, _options);
+
+		// language=json
+		json.Should().Be(
+			"""
+			[
+			  {
+			    "type": "product",
+			    "sub_type": "product",
+			    "lifecycle": "preview",
+			    "version": "0.5.0"
+			  }
+			]
+			""");
+	}
+
+	[Fact]
+	public void SerializeProductApplicabilityProducesCorrectJson()
+	{
+		var applicableTo = new ApplicableTo
+		{
+			ProductApplicability = new ProductApplicability
+			{
+				Ecctl = new AppliesCollection([
+					new Applicability
+					{
+						Lifecycle = ProductLifecycle.Deprecated,
+						Version = (SemVersion)"5.0.0"
+					}
+				]),
+				ApmAgentDotnet = AppliesCollection.GenerallyAvailable
+			}
+		};
+
+		var json = JsonSerializer.Serialize(applicableTo, _options);
+
+		// language=json
+		json.Should().Be(
+			"""
+			[
+			  {
+			    "type": "product",
+			    "sub_type": "ecctl",
+			    "lifecycle": "deprecated",
+			    "version": "5.0.0"
+			  },
+			  {
+			    "type": "product",
+			    "sub_type": "apm-agent-dotnet",
+			    "lifecycle": "ga",
+			    "version": "9999.9999.9999"
+			  }
+			]
+			""");
+	}
+
+	[Fact]
+	public void SerializeAllLifecyclesProducesCorrectJson()
+	{
+		var applicableTo = new ApplicableTo
+		{
+			Stack = new AppliesCollection(
+			[
+				new Applicability
+				{
+					Lifecycle = ProductLifecycle.TechnicalPreview,
+					Version = (SemVersion)"1.0.0"
+				},
+				new Applicability
+				{
+					Lifecycle = ProductLifecycle.Beta,
+					Version = (SemVersion)"1.0.0"
+				},
+				new Applicability
+				{
+					Lifecycle = ProductLifecycle.GenerallyAvailable,
+					Version = (SemVersion)"1.0.0"
+				},
+				new Applicability
+				{
+					Lifecycle = ProductLifecycle.Deprecated,
+					Version = (SemVersion)"1.0.0"
+				},
+				new Applicability
+				{
+					Lifecycle = ProductLifecycle.Removed,
+					Version = (SemVersion)"1.0.0"
+				}
+			])
+		};
+
+		var json = JsonSerializer.Serialize(applicableTo, _options);
+
+		json.Should().Contain("\"lifecycle\": \"preview\"");
+		json.Should().Contain("\"lifecycle\": \"beta\"");
+		json.Should().Contain("\"lifecycle\": \"ga\"");
+		json.Should().Contain("\"lifecycle\": \"deprecated\"");
+		json.Should().Contain("\"lifecycle\": \"removed\"");
+	}
+
+	[Fact]
+	public void SerializeComplexProducesCorrectJson()
+	{
+		var applicableTo = new ApplicableTo
+		{
+			Stack = new AppliesCollection([
+				new Applicability
+				{
+					Lifecycle = ProductLifecycle.GenerallyAvailable,
+					Version = (SemVersion)"8.0.0"
+				}
+			]),
+			Deployment = new DeploymentApplicability
+			{
+				Ece = AppliesCollection.GenerallyAvailable
+			},
+			Product = AppliesCollection.GenerallyAvailable
+		};
+
+		var json = JsonSerializer.Serialize(applicableTo, _options);
+
+		// Verify it's an array with 3 items
+		var jsonDoc = JsonDocument.Parse(json);
+		jsonDoc.RootElement.GetArrayLength().Should().Be(3);
+
+		// Verify each type is present
+		json.Should().Contain("\"type\": \"stack\"");
+		json.Should().Contain("\"type\": \"deployment\"");
+		json.Should().Contain("\"type\": \"product\"");
+
+		// Verify sub_types
+		json.Should().Contain("\"sub_type\": \"stack\"");
+		json.Should().Contain("\"sub_type\": \"ece\"");
+		json.Should().Contain("\"sub_type\": \"product\"");
+	}
+
+	[Fact]
+	public void SerializeEmptyProducesEmptyArray()
+	{
+		var applicableTo = new ApplicableTo();
+
+		var json = JsonSerializer.Serialize(applicableTo, _options);
+
+		json.Should().Be("[]");
+	}
+
+	[Fact]
+	public void SerializeValidatesJsonStructure()
+	{
+		var original = new ApplicableTo
+		{
+			Stack = AppliesCollection.GenerallyAvailable,
+			Deployment = new DeploymentApplicability
+			{
+				Ece = new AppliesCollection([
+					new Applicability
+					{
+						Lifecycle = ProductLifecycle.Beta,
+						Version = (SemVersion)"3.0.0"
+					}
+				])
+			}
+		};
+
+		var json = JsonSerializer.Serialize(original, _options);
+		var jsonDoc = JsonDocument.Parse(json);
+		var root = jsonDoc.RootElement;
+
+		root.ValueKind.Should().Be(JsonValueKind.Array);
+		var array = root.EnumerateArray().ToList();
+
+		array.Should().HaveCount(2); // Stack + Deployment.Ece
+
+		var stackEntry = array[0];
+		stackEntry.GetProperty("type").GetString().Should().Be("stack");
+		stackEntry.GetProperty("sub_type").GetString().Should().Be("stack");
+		stackEntry.GetProperty("lifecycle").GetString().Should().Be("ga");
+		stackEntry.GetProperty("version").GetString().Should().Be("9999.9999.9999");
+
+		var deploymentEntry = array[1];
+		deploymentEntry.GetProperty("type").GetString().Should().Be("deployment");
+		deploymentEntry.GetProperty("sub_type").GetString().Should().Be("ece");
+		deploymentEntry.GetProperty("lifecycle").GetString().Should().Be("beta");
+		deploymentEntry.GetProperty("version").GetString().Should().Be("3.0.0");
+	}
+}
diff --git a/tests/Elastic.Markdown.Tests/AppliesTo/ProductApplicabilityToStringTests.cs b/tests/Elastic.Markdown.Tests/AppliesTo/ProductApplicabilityToStringTests.cs
new file mode 100644
index 000000000..61dc00e80
--- /dev/null
+++ b/tests/Elastic.Markdown.Tests/AppliesTo/ProductApplicabilityToStringTests.cs
@@ -0,0 +1,112 @@
+// 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.Reflection;
+using Elastic.Documentation;
+using Elastic.Documentation.AppliesTo;
+using FluentAssertions;
+using YamlDotNet.Serialization;
+
+namespace Elastic.Markdown.Tests.AppliesTo;
+
+public class ProductApplicabilityToStringTests
+{
+	[Fact]
+	public void ProductApplicabilityToStringIncludesAllProperties()
+	{
+		// Create a ProductApplicability with all properties set
+		var productApplicability = new ProductApplicability();
+		var productType = typeof(ProductApplicability);
+		var properties = productType.GetProperties()
+			.Where(p => p.GetCustomAttribute() != null)
+			.ToList();
+
+		// Set all properties to a test value
+		var testValue = AppliesCollection.GenerallyAvailable;
+		foreach (var property in properties)
+		{
+			property.SetValue(productApplicability, testValue);
+		}
+
+		// Get the ToString output
+		var result = productApplicability.ToString();
+
+		// Verify that each property's YAML alias appears in the output
+		foreach (var property in properties)
+		{
+			var yamlAlias = property.GetCustomAttribute()!.Alias;
+			result.Should().Contain($"{yamlAlias}=",
+				$"ToString should include the property {property.Name} with alias '{yamlAlias}'");
+		}
+
+		// Verify we have the expected number of properties
+		properties.Should().HaveCount(22, "ProductApplicability should have exactly 22 product properties");
+	}
+
+	[Fact]
+	public void ProductApplicabilityToStringWithSomePropertiesOnlyIncludesSetProperties()
+	{
+		var productApplicability = new ProductApplicability
+		{
+			ApmAgentDotnet = AppliesCollection.GenerallyAvailable,
+			Ecctl = new AppliesCollection([new Applicability { Lifecycle = ProductLifecycle.Beta, Version = new SemVersion(1, 0, 0) }])
+		};
+
+		var result = productApplicability.ToString();
+
+		// Should include set properties
+		result.Should().Contain("apm-agent-dotnet=");
+		result.Should().Contain("ecctl=");
+
+		// Should not include unset properties
+		result.Should().NotContain("apm-agent-node=");
+		result.Should().NotContain("curator=");
+	}
+
+	[Fact]
+	public void ProductApplicabilityToStringEmptyReturnsEmptyString()
+	{
+		var productApplicability = new ProductApplicability();
+
+		var result = productApplicability.ToString();
+
+		result.Should().Be("");
+	}
+
+	[Fact]
+	public void ProductApplicabilityToStringPropertyOrderMatchesReflectionOrder()
+	{
+		// This test ensures that properties appear in the order they are defined
+		var productApplicability = new ProductApplicability
+		{
+			Ecctl = AppliesCollection.GenerallyAvailable,
+			Curator = AppliesCollection.GenerallyAvailable,
+			ApmAgentAndroid = AppliesCollection.GenerallyAvailable
+		};
+
+		var result = productApplicability.ToString();
+
+		// Get the properties in reflection order
+		var productType = typeof(ProductApplicability);
+		var properties = productType.GetProperties()
+			.Where(p => p.GetCustomAttribute() != null)
+			.Select(p => p.GetCustomAttribute()!.Alias)
+			.ToList();
+
+		// Find positions in the string
+		var positions = new Dictionary();
+		foreach (var alias in new[] { "ecctl", "curator", "apm-agent-android" })
+		{
+			var index = result.IndexOf($"{alias}=", StringComparison.Ordinal);
+			if (index >= 0)
+				positions[alias] = index;
+		}
+
+		// Verify that the properties appear in the correct order
+		positions["ecctl"].Should().BeLessThan(positions["curator"],
+			"ecctl should appear before curator");
+		positions["curator"].Should().BeLessThan(positions["apm-agent-android"],
+			"curator should appear before apm-agent-android");
+	}
+}
diff --git a/tests/Elastic.Markdown.Tests/Search/DocumentationDocumentSerializationTests.cs b/tests/Elastic.Markdown.Tests/Search/DocumentationDocumentSerializationTests.cs
new file mode 100644
index 000000000..b3258a994
--- /dev/null
+++ b/tests/Elastic.Markdown.Tests/Search/DocumentationDocumentSerializationTests.cs
@@ -0,0 +1,351 @@
+// 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.Globalization;
+using System.Text.Json;
+using Elastic.Documentation;
+using Elastic.Documentation.AppliesTo;
+using Elastic.Documentation.Search;
+using Elastic.Documentation.Serialization;
+using FluentAssertions;
+
+namespace Elastic.Markdown.Tests.Search;
+
+public class DocumentationDocumentSerializationTests
+{
+	private readonly JsonSerializerOptions _options = new(SourceGenerationContext.Default.Options);
+
+	[Fact]
+	public void SerializeDocumentWithStackAppliesToProducesCorrectJson()
+	{
+		var doc = new DocumentationDocument
+		{
+			Url = "/test/page",
+			Title = "Test Page",
+			Applies = new ApplicableTo
+			{
+				Stack = AppliesCollection.GenerallyAvailable
+			}
+		};
+
+		var json = JsonSerializer.Serialize(doc, _options);
+		var jsonDoc = JsonDocument.Parse(json);
+		var root = jsonDoc.RootElement;
+
+		// Verify applies_to existing
+		root.TryGetProperty("applies_to", out var appliesTo).Should().BeTrue();
+		appliesTo.ValueKind.Should().Be(JsonValueKind.Array);
+
+		// Verify structure
+		var appliesArray = appliesTo.EnumerateArray().ToList();
+		appliesArray.Should().HaveCount(1);
+
+		var stackEntry = appliesArray[0];
+		stackEntry.GetProperty("type").GetString().Should().Be("stack");
+		stackEntry.GetProperty("sub_type").GetString().Should().Be("stack");
+		stackEntry.GetProperty("lifecycle").GetString().Should().Be("ga");
+		stackEntry.GetProperty("version").GetString().Should().Be("9999.9999.9999");
+	}
+
+	[Fact]
+	public void SerializeDocumentWithDeploymentAppliesToProducesCorrectJson()
+	{
+		var doc = new DocumentationDocument
+		{
+			Url = "/test/deployment",
+			Title = "Deployment Test",
+			Applies = new ApplicableTo
+			{
+				Deployment = new DeploymentApplicability
+				{
+					Ess = AppliesCollection.GenerallyAvailable,
+					Ece = new AppliesCollection([new Applicability { Lifecycle = ProductLifecycle.Beta, Version = (SemVersion)"3.5.0" }])
+				}
+			}
+		};
+
+		var json = JsonSerializer.Serialize(doc, _options);
+		var jsonDoc = JsonDocument.Parse(json);
+		var root = jsonDoc.RootElement;
+
+		root.TryGetProperty("applies_to", out var appliesTo).Should().BeTrue();
+		var appliesArray = appliesTo.EnumerateArray().ToList();
+		appliesArray.Should().HaveCount(2);
+
+		// Verify ESS entry
+		var essEntry = appliesArray.FirstOrDefault(e => e.GetProperty("sub_type").GetString() == "ess");
+		essEntry.ValueKind.Should().NotBe(JsonValueKind.Undefined);
+		essEntry.GetProperty("type").GetString().Should().Be("deployment");
+		essEntry.GetProperty("lifecycle").GetString().Should().Be("ga");
+		essEntry.GetProperty("version").GetString().Should().Be("9999.9999.9999");
+
+		// Verify ECE entry
+		var eceEntry = appliesArray.FirstOrDefault(e => e.GetProperty("sub_type").GetString() == "ece");
+		eceEntry.ValueKind.Should().NotBe(JsonValueKind.Undefined);
+		eceEntry.GetProperty("type").GetString().Should().Be("deployment");
+		eceEntry.GetProperty("lifecycle").GetString().Should().Be("beta");
+		eceEntry.GetProperty("version").GetString().Should().Be("3.5.0");
+	}
+
+	[Fact]
+	public void SerializeDocumentWithServerlessAppliesToProducesCorrectJson()
+	{
+		var doc = new DocumentationDocument
+		{
+			Url = "/test/serverless",
+			Title = "Serverless Test",
+			Applies = new ApplicableTo
+			{
+				Serverless = new ServerlessProjectApplicability
+				{
+					Elasticsearch = new AppliesCollection([new Applicability { Lifecycle = ProductLifecycle.GenerallyAvailable, Version = (SemVersion)"8.0.0" }]),
+					Security = new AppliesCollection([new Applicability { Lifecycle = ProductLifecycle.TechnicalPreview, Version = (SemVersion)"1.0.0" }])
+				}
+			}
+		};
+
+		var json = JsonSerializer.Serialize(doc, _options);
+		var jsonDoc = JsonDocument.Parse(json);
+		var root = jsonDoc.RootElement;
+
+		root.TryGetProperty("applies_to", out var appliesTo).Should().BeTrue();
+		var appliesArray = appliesTo.EnumerateArray().ToList();
+		appliesArray.Should().HaveCount(2);
+
+		// Verify elasticsearch entry
+		var esEntry = appliesArray.FirstOrDefault(e => e.GetProperty("sub_type").GetString() == "elasticsearch");
+		esEntry.ValueKind.Should().NotBe(JsonValueKind.Undefined);
+		esEntry.GetProperty("type").GetString().Should().Be("serverless");
+		esEntry.GetProperty("lifecycle").GetString().Should().Be("ga");
+		esEntry.GetProperty("version").GetString().Should().Be("8.0.0");
+
+		// Verify security entry
+		var secEntry = appliesArray.FirstOrDefault(e => e.GetProperty("sub_type").GetString() == "security");
+		secEntry.ValueKind.Should().NotBe(JsonValueKind.Undefined);
+		secEntry.GetProperty("type").GetString().Should().Be("serverless");
+		secEntry.GetProperty("lifecycle").GetString().Should().Be("preview");
+		secEntry.GetProperty("version").GetString().Should().Be("1.0.0");
+	}
+
+	[Fact]
+	public void SerializeDocumentWithProductAppliesToProducesCorrectJson()
+	{
+		var doc = new DocumentationDocument
+		{
+			Url = "/test/product",
+			Title = "Product Test",
+			Applies = new ApplicableTo
+			{
+				Product = new AppliesCollection([new Applicability { Lifecycle = ProductLifecycle.Beta, Version = (SemVersion)"2.0.0" }])
+			}
+		};
+
+		var json = JsonSerializer.Serialize(doc, _options);
+		var jsonDoc = JsonDocument.Parse(json);
+		var root = jsonDoc.RootElement;
+
+		root.TryGetProperty("applies_to", out var appliesTo).Should().BeTrue();
+		var appliesArray = appliesTo.EnumerateArray().ToList();
+		appliesArray.Should().HaveCount(1);
+
+		var productEntry = appliesArray[0];
+		productEntry.GetProperty("type").GetString().Should().Be("product");
+		productEntry.GetProperty("sub_type").GetString().Should().Be("product");
+		productEntry.GetProperty("lifecycle").GetString().Should().Be("beta");
+		productEntry.GetProperty("version").GetString().Should().Be("2.0.0");
+	}
+
+	[Fact]
+	public void SerializeDocumentWithProductApplicabilityProducesCorrectJson()
+	{
+		var doc = new DocumentationDocument
+		{
+			Url = "/test/apm",
+			Title = "APM Test",
+			Applies = new ApplicableTo
+			{
+				ProductApplicability = new ProductApplicability
+				{
+					ApmAgentDotnet = new AppliesCollection([new Applicability { Lifecycle = ProductLifecycle.GenerallyAvailable, Version = (SemVersion)"1.5.0" }]),
+					ApmAgentNode = new AppliesCollection([new Applicability { Lifecycle = ProductLifecycle.Deprecated, Version = (SemVersion)"2.0.0" }])
+				}
+			}
+		};
+
+		var json = JsonSerializer.Serialize(doc, _options);
+		var jsonDoc = JsonDocument.Parse(json);
+		var root = jsonDoc.RootElement;
+
+		root.TryGetProperty("applies_to", out var appliesTo).Should().BeTrue();
+		var appliesArray = appliesTo.EnumerateArray().ToList();
+		appliesArray.Should().HaveCount(2);
+
+		// Verify apm-agent-dotnet entry
+		var dotnetEntry = appliesArray.FirstOrDefault(e => e.GetProperty("sub_type").GetString() == "apm-agent-dotnet");
+		dotnetEntry.ValueKind.Should().NotBe(JsonValueKind.Undefined);
+		dotnetEntry.GetProperty("type").GetString().Should().Be("product");
+		dotnetEntry.GetProperty("lifecycle").GetString().Should().Be("ga");
+		dotnetEntry.GetProperty("version").GetString().Should().Be("1.5.0");
+
+		// Verify apm-agent-node entry
+		var nodeEntry = appliesArray.FirstOrDefault(e => e.GetProperty("sub_type").GetString() == "apm-agent-node");
+		nodeEntry.ValueKind.Should().NotBe(JsonValueKind.Undefined);
+		nodeEntry.GetProperty("type").GetString().Should().Be("product");
+		nodeEntry.GetProperty("lifecycle").GetString().Should().Be("deprecated");
+		nodeEntry.GetProperty("version").GetString().Should().Be("2.0.0");
+	}
+
+	[Fact]
+	public void SerializeDocumentWithComplexAppliesToProducesCorrectJson()
+	{
+		var doc = new DocumentationDocument
+		{
+			Url = "/test/complex",
+			Title = "Complex Test",
+			Applies = new ApplicableTo
+			{
+				Stack = new AppliesCollection([new Applicability { Lifecycle = ProductLifecycle.GenerallyAvailable, Version = (SemVersion)"8.0.0" }]),
+				Deployment = new DeploymentApplicability
+				{
+					Ess = AppliesCollection.GenerallyAvailable
+				},
+				Serverless = new ServerlessProjectApplicability
+				{
+					Elasticsearch = AppliesCollection.GenerallyAvailable
+				}
+			}
+		};
+
+		var json = JsonSerializer.Serialize(doc, _options);
+		var jsonDoc = JsonDocument.Parse(json);
+		var root = jsonDoc.RootElement;
+
+		root.TryGetProperty("applies_to", out var appliesTo).Should().BeTrue();
+		var appliesArray = appliesTo.EnumerateArray().ToList();
+		appliesArray.Should().HaveCount(3);
+
+		// Verify we have all three types
+		appliesArray.Should().Contain(e => e.GetProperty("type").GetString() == "stack");
+		appliesArray.Should().Contain(e => e.GetProperty("type").GetString() == "deployment");
+		appliesArray.Should().Contain(e => e.GetProperty("type").GetString() == "serverless");
+	}
+
+	[Fact]
+	public void SerializeDocumentWithNullAppliesToOmitsField()
+	{
+		var doc = new DocumentationDocument
+		{
+			Url = "/test/no-applies",
+			Title = "No Applies Test",
+			Applies = null
+		};
+
+		var json = JsonSerializer.Serialize(doc, _options);
+		var jsonDoc = JsonDocument.Parse(json);
+		var root = jsonDoc.RootElement;
+
+		// With default JSON options, null values might be omitted or serialized as null
+		// Let's check both possibilities
+		if (root.TryGetProperty("applies_to", out var appliesTo))
+			appliesTo.ValueKind.Should().Be(JsonValueKind.Null);
+		else
+		{
+			// Field is omitted, which is also acceptable
+			true.Should().BeTrue();
+		}
+	}
+
+	[Fact]
+	public void SerializeDocumentWithEmptyAppliesToProducesEmptyArray()
+	{
+		var doc = new DocumentationDocument
+		{
+			Url = "/test/empty-applies",
+			Title = "Empty Applies Test",
+			Applies = new ApplicableTo()
+		};
+
+		var json = JsonSerializer.Serialize(doc, _options);
+		var jsonDoc = JsonDocument.Parse(json);
+		var root = jsonDoc.RootElement;
+
+		root.TryGetProperty("applies_to", out var appliesTo).Should().BeTrue();
+		appliesTo.ValueKind.Should().Be(JsonValueKind.Array);
+		appliesTo.GetArrayLength().Should().Be(0);
+	}
+
+	[Fact]
+	public void RoundTripDocumentWithAppliesToPreservesData()
+	{
+		var original = new DocumentationDocument
+		{
+			Url = "/test/roundtrip",
+			Title = "Round Trip Test",
+			Hash = "abc123",
+			BatchIndexDate = DateTimeOffset.Parse("2024-01-15T10:00:00Z", CultureInfo.InvariantCulture),
+			LastUpdated = DateTimeOffset.Parse("2024-01-15T09:00:00Z", CultureInfo.InvariantCulture),
+			Applies = new ApplicableTo
+			{
+				Stack = new AppliesCollection([new Applicability { Lifecycle = ProductLifecycle.GenerallyAvailable, Version = (SemVersion)"8.5.0" }]),
+				Deployment = new DeploymentApplicability
+				{
+					Ess = new AppliesCollection([new Applicability { Lifecycle = ProductLifecycle.Beta, Version = (SemVersion)"8.6.0" }])
+				}
+			},
+			Headings = ["Introduction", "Getting Started"],
+			Links = ["/link1", "/link2"],
+			Body = "Test body content",
+			StrippedBody = "Test body content",
+			Description = "Test description"
+		};
+
+		var json = JsonSerializer.Serialize(original, _options);
+		var deserialized = JsonSerializer.Deserialize(json, _options);
+
+		deserialized.Should().NotBeNull();
+		deserialized.Url.Should().Be(original.Url);
+		deserialized.Title.Should().Be(original.Title);
+		deserialized.Applies.Should().NotBeNull();
+		deserialized.Applies!.Stack.Should().BeEquivalentTo(original.Applies!.Stack);
+		deserialized.Applies.Deployment.Should().NotBeNull();
+		deserialized.Applies.Deployment!.Ess.Should().BeEquivalentTo(original.Applies.Deployment!.Ess);
+	}
+
+	[Fact]
+	public void SerializeDocumentWithMultipleApplicabilitiesPerTypeProducesMultipleArrayEntries()
+	{
+		var doc = new DocumentationDocument
+		{
+			Url = "/test/multiple",
+			Title = "Multiple Test",
+			Applies = new ApplicableTo
+			{
+				Stack = new AppliesCollection(
+				[
+					new Applicability { Lifecycle = ProductLifecycle.GenerallyAvailable, Version = (SemVersion)"8.0.0" },
+					new Applicability { Lifecycle = ProductLifecycle.Beta, Version = (SemVersion)"7.17.0" },
+					new Applicability { Lifecycle = ProductLifecycle.Deprecated, Version = (SemVersion)"7.0.0" }
+				])
+			}
+		};
+
+		var json = JsonSerializer.Serialize(doc, _options);
+		var jsonDoc = JsonDocument.Parse(json);
+		var root = jsonDoc.RootElement;
+
+		root.TryGetProperty("applies_to", out var appliesTo).Should().BeTrue();
+		var appliesArray = appliesTo.EnumerateArray().ToList();
+		appliesArray.Should().HaveCount(3);
+
+		// All should be stack type
+		appliesArray.Should().OnlyContain(e => e.GetProperty("type").GetString() == "stack");
+		appliesArray.Should().OnlyContain(e => e.GetProperty("sub_type").GetString() == "stack");
+
+		// Verify different lifecycle values
+		var lifecycles = appliesArray.Select(e => e.GetProperty("lifecycle").GetString()).ToList();
+		lifecycles.Should().Contain("ga");
+		lifecycles.Should().Contain("beta");
+		lifecycles.Should().Contain("deprecated");
+	}
+}