diff --git a/src/Elastic.Documentation.Site/Assets/web-components/SearchOrAskAi/Search/SearchResults.tsx b/src/Elastic.Documentation.Site/Assets/web-components/SearchOrAskAi/Search/SearchResults.tsx
index 1752f8ade..38b17fc4e 100644
--- a/src/Elastic.Documentation.Site/Assets/web-components/SearchOrAskAi/Search/SearchResults.tsx
+++ b/src/Elastic.Documentation.Site/Assets/web-components/SearchOrAskAi/Search/SearchResults.tsx
@@ -2,7 +2,6 @@ import { useSearchTerm } from '../search.store'
import { SearchResultItem, useSearchQuery } from './useSearchQuery'
import {
useEuiFontSize,
- EuiHighlight,
EuiLink,
EuiLoadingSpinner,
EuiSpacer,
@@ -14,8 +13,8 @@ import {
} from '@elastic/eui'
import { css } from '@emotion/react'
import { useDebounce } from '@uidotdev/usehooks'
-import * as React from 'react'
-import { useEffect, useMemo, useState } from 'react'
+import DOMPurify from 'dompurify'
+import { useEffect, useMemo, useState, memo } from 'react'
export const SearchResults = () => {
const searchTerm = useSearchTerm()
@@ -103,25 +102,15 @@ interface SearchResultListItemProps {
function SearchResultListItem({ item: result }: SearchResultListItemProps) {
const { euiTheme } = useEuiTheme()
- const searchTerm = useSearchTerm()
- const highlightSearchTerms = useMemo(
- () =>
- searchTerm
- .toLowerCase()
- .split(' ')
- .filter((i) => i.length > 1),
- [searchTerm]
- )
-
- if (highlightSearchTerms.includes('esql')) {
- highlightSearchTerms.push('es|ql')
- }
-
- if (highlightSearchTerms.includes('dotnet')) {
- highlightSearchTerms.push('.net')
- }
+ const titleFontSize = useEuiFontSize('m')
return (
-
+
-
+
-
+ {result.title}
+
+
+
+
+
- {result.title}
-
-
-
+ {result.highlightedBody ? (
+
+ ) : (
+ {result.description}
+ )}
+
+
)
}
-function Breadcrumbs({
- parents,
- highlightSearchTerms,
-}: {
- parents: SearchResultItem['parents']
- highlightSearchTerms: string[]
-}) {
+function Breadcrumbs({ parents }: { parents: SearchResultItem['parents'] }) {
const { euiTheme } = useEuiTheme()
const { fontSize: smallFontsize } = useEuiFontSize('xs')
return (
@@ -224,12 +226,7 @@ function Breadcrumbs({
}
`}
>
-
- {parent.title}
-
+ {parent.title}
@@ -237,3 +234,32 @@ function Breadcrumbs({
)
}
+
+const SanitizedHtmlContent = memo(
+ ({ htmlContent }: { htmlContent: string }) => {
+ const processed = useMemo(() => {
+ if (!htmlContent) return ''
+
+ const sanitized = DOMPurify.sanitize(htmlContent, {
+ ALLOWED_TAGS: ['mark'],
+ ALLOWED_ATTR: [],
+ KEEP_CONTENT: true,
+ })
+
+ // Check if text starts mid-sentence (lowercase first letter)
+ const temp = document.createElement('div')
+ temp.innerHTML = sanitized
+ const text = temp.textContent || ''
+ const firstChar = text.trim()[0]
+
+ // Add leading ellipsis if starts with lowercase
+ if (firstChar && /[a-z]/.test(firstChar)) {
+ return '… ' + sanitized
+ }
+
+ return sanitized
+ }, [htmlContent])
+
+ return
+ }
+)
diff --git a/src/Elastic.Documentation.Site/Assets/web-components/SearchOrAskAi/Search/useSearchQuery.ts b/src/Elastic.Documentation.Site/Assets/web-components/SearchOrAskAi/Search/useSearchQuery.ts
index f3ab3ad5f..b379898b6 100644
--- a/src/Elastic.Documentation.Site/Assets/web-components/SearchOrAskAi/Search/useSearchQuery.ts
+++ b/src/Elastic.Documentation.Site/Assets/web-components/SearchOrAskAi/Search/useSearchQuery.ts
@@ -13,6 +13,7 @@ const SearchResultItem = z.object({
description: z.string(),
score: z.number(),
parents: z.array(SearchResultItemParent),
+ highlightedBody: z.string().nullish(),
})
export type SearchResultItem = z.infer
diff --git a/src/Elastic.Documentation.Site/Assets/web-components/SearchOrAskAi/SearchOrAskAiModal.tsx b/src/Elastic.Documentation.Site/Assets/web-components/SearchOrAskAi/SearchOrAskAiModal.tsx
index 2e064c260..592a83a6c 100644
--- a/src/Elastic.Documentation.Site/Assets/web-components/SearchOrAskAi/SearchOrAskAiModal.tsx
+++ b/src/Elastic.Documentation.Site/Assets/web-components/SearchOrAskAi/SearchOrAskAiModal.tsx
@@ -48,7 +48,7 @@ export const SearchOrAskAiModal = () => {
css={css`
flex-grow: 1;
overflow-y: scroll;
- max-height: 80vh;
+ max-height: 70vh;
${useEuiOverflowScroll('y')}
`}
>
diff --git a/src/Elastic.Documentation.Site/package-lock.json b/src/Elastic.Documentation.Site/package-lock.json
index a46ecf27e..a9f1d6d7b 100644
--- a/src/Elastic.Documentation.Site/package-lock.json
+++ b/src/Elastic.Documentation.Site/package-lock.json
@@ -17,6 +17,7 @@
"@tanstack/react-query": "^5.87.4",
"@uidotdev/usehooks": "2.4.1",
"clipboard": "2.0.11",
+ "dompurify": "3.2.7",
"highlight.js": "11.11.1",
"htmx-ext-head-support": "2.0.4",
"htmx-ext-preload": "2.1.1",
@@ -7855,6 +7856,12 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/@types/trusted-types": {
+ "version": "2.0.7",
+ "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz",
+ "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==",
+ "optional": true
+ },
"node_modules/@types/unist": {
"version": "2.0.11",
"resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.11.tgz",
@@ -9619,6 +9626,14 @@
"dev": true,
"peer": true
},
+ "node_modules/dompurify": {
+ "version": "3.2.7",
+ "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.2.7.tgz",
+ "integrity": "sha512-WhL/YuveyGXJaerVlMYGWhvQswa7myDG17P7Vu65EWC05o8vfeNbvNf4d/BOvH99+ZW+LlQsc1GDKMa1vNK6dw==",
+ "optionalDependencies": {
+ "@types/trusted-types": "^2.0.7"
+ }
+ },
"node_modules/dotenv": {
"version": "16.6.1",
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz",
diff --git a/src/Elastic.Documentation.Site/package.json b/src/Elastic.Documentation.Site/package.json
index ea4d92bf7..1b8e9d4e5 100644
--- a/src/Elastic.Documentation.Site/package.json
+++ b/src/Elastic.Documentation.Site/package.json
@@ -89,6 +89,7 @@
"@tanstack/react-query": "^5.87.4",
"@uidotdev/usehooks": "2.4.1",
"clipboard": "2.0.11",
+ "dompurify": "3.2.7",
"highlight.js": "11.11.1",
"htmx-ext-head-support": "2.0.4",
"htmx-ext-preload": "2.1.1",
diff --git a/src/Elastic.Documentation/Search/DocumentationDocument.cs b/src/Elastic.Documentation/Search/DocumentationDocument.cs
index 25660a76e..b6264a442 100644
--- a/src/Elastic.Documentation/Search/DocumentationDocument.cs
+++ b/src/Elastic.Documentation/Search/DocumentationDocument.cs
@@ -45,6 +45,10 @@ public record DocumentationDocument
[JsonPropertyName("body")]
public string? Body { get; set; }
+ // Stripped body is the body with markdown removed, suitable for search indexing
+ [JsonPropertyName("stripped_body")]
+ public string? StrippedBody { get; set; }
+
[JsonPropertyName("url_segment_count")]
public int? UrlSegmentCount { get; set; }
diff --git a/src/Elastic.Markdown/Exporters/ElasticsearchMarkdownExporter.cs b/src/Elastic.Markdown/Exporters/ElasticsearchMarkdownExporter.cs
index b253447f2..8c3367a95 100644
--- a/src/Elastic.Markdown/Exporters/ElasticsearchMarkdownExporter.cs
+++ b/src/Elastic.Markdown/Exporters/ElasticsearchMarkdownExporter.cs
@@ -12,6 +12,7 @@
using Elastic.Ingest.Elasticsearch;
using Elastic.Ingest.Elasticsearch.Catalog;
using Elastic.Ingest.Elasticsearch.Semantic;
+using Elastic.Markdown.Helpers;
using Elastic.Markdown.IO;
using Elastic.Transport;
using Elastic.Transport.Products.Elasticsearch;
@@ -90,6 +91,13 @@ protected static string CreateMappingSetting() =>
"synonyms_filter"
]
},
+ "highlight_analyzer": {
+ "tokenizer": "standard",
+ "filter": [
+ "lowercase",
+ "english_stop"
+ ]
+ },
"hierarchy_analyzer": { "tokenizer": "path_tokenizer" }
},
"filter": {
@@ -97,6 +105,10 @@ protected static string CreateMappingSetting() =>
"type": "synonym",
"synonyms_set": "docs",
"updateable": true
+ },
+ "english_stop": {
+ "type": "stop",
+ "stopwords": "_english_"
}
},
"tokenizer": {
@@ -136,6 +148,11 @@ protected static string CreateMapping(string? inferenceId) =>
},
"body": {
"type": "text"
+ },
+ "stripped_body": {
+ "type": "text",
+ "search_analyzer": "highlight_analyzer",
+ "term_vector": "with_positions_offsets"
}
{{(!string.IsNullOrWhiteSpace(inferenceId) ? AbstractInferenceMapping(inferenceId) : AbstractMapping())}}
}
@@ -277,11 +294,16 @@ public async ValueTask ExportAsync(MarkdownExportFileContext fileContext,
IPositionalNavigation navigation = fileContext.DocumentationSet;
- //use LLM text if it was already provided (because we run with both llm and elasticsearch output)
- var body = fileContext.LLMText ??= LlmMarkdownExporter.ConvertToLlmMarkdown(fileContext.Document, fileContext.BuildContext);
+ // Remove the first h1 because we already have the title
+ // and we don't want it to appear in the body
+ var h1 = fileContext.Document.Descendants().FirstOrDefault(h => h.Level == 1);
+ if (h1 is not null)
+ _ = fileContext.Document.Remove(h1);
+
+ var body = LlmMarkdownExporter.ConvertToLlmMarkdown(fileContext.Document, fileContext.BuildContext);
var headings = fileContext.Document.Descendants()
- .Select(h => h.GetData("header") as string ?? string.Empty)
+ .Select(h => h.GetData("header") as string ?? string.Empty) // TODO: Confirm that 'header' data is correctly set for all HeadingBlock instances and that this extraction is reliable.
.Where(text => !string.IsNullOrEmpty(text))
.ToArray();
@@ -295,6 +317,7 @@ public async ValueTask ExportAsync(MarkdownExportFileContext fileContext,
Hash = ShortId.Create(url, body),
Title = file.Title,
Body = body,
+ StrippedBody = body.StripMarkdown(),
Description = fileContext.SourceFile.YamlFrontMatter?.Description,
Abstract = @abstract,
Applies = fileContext.SourceFile.YamlFrontMatter?.AppliesTo,
@@ -318,4 +341,3 @@ public async ValueTask FinishExportAsync(IDirectoryInfo outputFolder, Canc
return await _channel.RefreshAsync(ctx);
}
}
-
diff --git a/src/Elastic.Markdown/Exporters/IMarkdownExporter.cs b/src/Elastic.Markdown/Exporters/IMarkdownExporter.cs
index 492df8ced..cd9e3c125 100644
--- a/src/Elastic.Markdown/Exporters/IMarkdownExporter.cs
+++ b/src/Elastic.Markdown/Exporters/IMarkdownExporter.cs
@@ -17,7 +17,6 @@ public record MarkdownExportFileContext
public required MarkdownDocument Document { get; init; }
public required MarkdownFile SourceFile { get; init; }
public required IFileInfo DefaultOutputFile { get; init; }
- public string? LLMText { get; set; }
public required DocumentationSet DocumentationSet { get; init; }
}
diff --git a/src/Elastic.Markdown/Exporters/LlmMarkdownExporter.cs b/src/Elastic.Markdown/Exporters/LlmMarkdownExporter.cs
index 365e1286f..8feaff33c 100644
--- a/src/Elastic.Markdown/Exporters/LlmMarkdownExporter.cs
+++ b/src/Elastic.Markdown/Exporters/LlmMarkdownExporter.cs
@@ -19,26 +19,26 @@ public class LlmMarkdownExporter : IMarkdownExporter
{
private const string LlmsTxtTemplate = """
# Elastic Documentation
-
+
> Elastic provides an open source search, analytics, and AI platform, and out-of-the-box solutions for observability and security. The Search AI platform combines the power of search and generative AI to provide near real-time search and analysis with relevance to reduce your time to value.
>
>Elastic offers the following solutions or types of projects:
>
- >* [Elasticsearch](https://www.elastic.co/docs/solutions/search): Build powerful search and RAG applications using Elasticsearch's vector database, AI toolkit, and advanced retrieval capabilities.
+ >* [Elasticsearch](https://www.elastic.co/docs/solutions/search): Build powerful search and RAG applications using Elasticsearch's vector database, AI toolkit, and advanced retrieval capabilities.
>* [Elastic Observability](https://www.elastic.co/docs/solutions/observability): Gain comprehensive visibility into applications, infrastructure, and user experience through logs, metrics, traces, and other telemetry data, all in a single interface.
>* [Elastic Security](https://www.elastic.co/docs/solutions/security): Combine SIEM, endpoint security, and cloud security to provide comprehensive tools for threat detection and prevention, investigation, and response.
-
+
The documentation is organized to guide you through your journey with Elastic, from learning the basics to deploying and managing complex solutions. Here is a detailed breakdown of the documentation structure:
-
- * [**Elastic fundamentals**](https://www.elastic.co/docs/get-started): Understand the basics about the deployment options, platform, and solutions, and features of the documentation.
- * [**Solutions and use cases**](https://www.elastic.co/docs/solutions): Learn use cases, evaluate, and implement Elastic's solutions: Observability, Search, and Security.
- * [**Manage data**](https://www.elastic.co/docs/manage-data): Learn about data store primitives, ingestion and enrichment, managing the data lifecycle, and migrating data.
- * [**Explore and analyze**](https://www.elastic.co/docs/explore-analyze): Get value from data through querying, visualization, machine learning, and alerting.
- * [**Deploy and manage**](https://www.elastic.co/docs/deploy-manage): Deploy and manage production-ready clusters. Covers deployment options and maintenance tasks.
- * [**Manage your Cloud account**](https://www.elastic.co/docs/cloud-account): A dedicated section for user-facing cloud account tasks like resetting passwords.
- * [**Troubleshoot**](https://www.elastic.co/docs/troubleshoot): Identify and resolve problems.
- * [**Extend and contribute**](https://www.elastic.co/docs/extend): How to contribute to or integrate with Elastic, from open source to plugins to integrations.
- * [**Release notes**](https://www.elastic.co/docs/release-notes): Contains release notes and changelogs for each new release.
+
+ * [**Elastic fundamentals**](https://www.elastic.co/docs/get-started): Understand the basics about the deployment options, platform, and solutions, and features of the documentation.
+ * [**Solutions and use cases**](https://www.elastic.co/docs/solutions): Learn use cases, evaluate, and implement Elastic's solutions: Observability, Search, and Security.
+ * [**Manage data**](https://www.elastic.co/docs/manage-data): Learn about data store primitives, ingestion and enrichment, managing the data lifecycle, and migrating data.
+ * [**Explore and analyze**](https://www.elastic.co/docs/explore-analyze): Get value from data through querying, visualization, machine learning, and alerting.
+ * [**Deploy and manage**](https://www.elastic.co/docs/deploy-manage): Deploy and manage production-ready clusters. Covers deployment options and maintenance tasks.
+ * [**Manage your Cloud account**](https://www.elastic.co/docs/cloud-account): A dedicated section for user-facing cloud account tasks like resetting passwords.
+ * [**Troubleshoot**](https://www.elastic.co/docs/troubleshoot): Identify and resolve problems.
+ * [**Extend and contribute**](https://www.elastic.co/docs/extend): How to contribute to or integrate with Elastic, from open source to plugins to integrations.
+ * [**Release notes**](https://www.elastic.co/docs/release-notes): Contains release notes and changelogs for each new release.
* [**Reference**](https://www.elastic.co/docs/reference): Reference material for core tasks and manuals for optional products.
""";
diff --git a/src/api/Elastic.Documentation.Api.Core/Search/SearchUsecase.cs b/src/api/Elastic.Documentation.Api.Core/Search/SearchUsecase.cs
index 8114dd475..2c81517f6 100644
--- a/src/api/Elastic.Documentation.Api.Core/Search/SearchUsecase.cs
+++ b/src/api/Elastic.Documentation.Api.Core/Search/SearchUsecase.cs
@@ -62,4 +62,5 @@ public record SearchResultItem
public required string Description { get; init; }
public required SearchResultItemParent[] Parents { get; init; }
public float Score { get; init; }
+ public string? HighlightedBody { get; init; }
}
diff --git a/src/api/Elastic.Documentation.Api.Infrastructure/Adapters/Search/ElasticsearchGateway.cs b/src/api/Elastic.Documentation.Api.Infrastructure/Adapters/Search/ElasticsearchGateway.cs
index 7a3134a80..615ac9567 100644
--- a/src/api/Elastic.Documentation.Api.Infrastructure/Adapters/Search/ElasticsearchGateway.cs
+++ b/src/api/Elastic.Documentation.Api.Infrastructure/Adapters/Search/ElasticsearchGateway.cs
@@ -4,9 +4,11 @@
using System.Text.Json.Serialization;
using Elastic.Clients.Elasticsearch;
+using Elastic.Clients.Elasticsearch.Core.Search;
using Elastic.Clients.Elasticsearch.QueryDsl;
using Elastic.Clients.Elasticsearch.Serialization;
using Elastic.Documentation.Api.Core.Search;
+using Elastic.Documentation.AppliesTo;
using Elastic.Transport;
using Microsoft.Extensions.Logging;
@@ -26,14 +28,20 @@ internal sealed record DocumentDto
[JsonPropertyName("body")]
public string? Body { get; init; }
+ [JsonPropertyName("stripped_body")]
+ public string? StrippedBody { get; init; }
+
[JsonPropertyName("abstract")]
- public required string Abstract { get; init; }
+ public string? Abstract { get; init; }
[JsonPropertyName("url_segment_count")]
public int UrlSegmentCount { get; init; }
[JsonPropertyName("parents")]
public ParentDocumentDto[] Parents { get; init; } = [];
+
+ [JsonPropertyName("applies_to")]
+ public ApplicableTo? Applies { get; init; }
}
internal sealed record ParentDocumentDto
@@ -45,7 +53,7 @@ internal sealed record ParentDocumentDto
public required string Url { get; init; }
}
-public class ElasticsearchGateway : ISearchGateway
+public partial class ElasticsearchGateway : ISearchGateway
{
private readonly ElasticsearchClient _client;
private readonly ElasticsearchOptions _elasticsearchOptions;
@@ -73,6 +81,9 @@ public ElasticsearchGateway(ElasticsearchOptions elasticsearchOptions, ILogger(f => f.Title), searchQuery) { Operator = Operator.And, Boost = 8.0f }
|| new MatchBoolPrefixQuery(Infer.Field(f => f.Title), searchQuery) { Boost = 6.0f }
|| new MatchQuery(Infer.Field(f => f.Abstract), searchQuery) { Boost = 4.0f }
+ || new MatchQuery(Infer.Field(f => f.StrippedBody), searchQuery) { Boost = 3.0f }
|| new MatchQuery(Infer.Field(f => f.Parents.First().Title), searchQuery) { Boost = 2.0f }
|| new MatchQuery(Infer.Field(f => f.Title), searchQuery) { Fuzziness = 1, Boost = 1.0f }
)
@@ -87,7 +99,7 @@ public ElasticsearchGateway(ElasticsearchOptions elasticsearchOptions, ILogger(f => f.Url.Suffix("keyword")),
new TermsQueryField(["/docs", "/docs/", "/docs/404", "/docs/404/"]))
@@ -106,10 +118,41 @@ public ElasticsearchGateway(ElasticsearchOptions elasticsearchOptions, ILogger ret.Standard(std => std.Query(semanticSearchRetriever))
)
.RankConstant(60) // Controls how much weight is given to document ranking
+ .RankWindowSize(100)
)
)
- .From((pageNumber - 1) * pageSize)
- .Size(pageSize), ctx);
+ .From((pageNumber - 1) * pageSize)
+ .Size(pageSize)
+ .Source(sf => sf
+ .Filter(f => f
+ .Includes(
+ e => e.Title,
+ e => e.Url,
+ e => e.Description,
+ e => e.Parents
+ )
+ )
+ )
+ .Highlight(h => h
+ .RequireFieldMatch(true)
+ .Fields(f => f
+ .Add(Infer.Field(d => d.StrippedBody), hf => hf
+ .FragmentSize(150)
+ .NumberOfFragments(3)
+ .NoMatchSize(150)
+ .BoundaryChars(":.!?\t\n")
+ .BoundaryScanner(BoundaryScanner.Sentence)
+ .BoundaryMaxScan(15)
+ .FragmentOffset(0)
+ .HighlightQuery(q => q.Match(m => m
+ .Field(d => d.StrippedBody)
+ .Query(searchQuery)
+ .Analyzer("highlight_analyzer")
+ ))
+ .PreTags(preTag)
+ .PostTags(postTag))
+ )
+ ), ctx);
if (!response.IsValidResponse)
{
@@ -128,122 +171,36 @@ public ElasticsearchGateway(ElasticsearchOptions elasticsearchOptions, ILogger Results)> ExactSearchAsync(string query, int pageNumber, int pageSize, Cancel ctx = default)
+ private static (int TotalHits, List Results) ProcessSearchResponse(SearchResponse response)
{
- _logger.LogInformation("Starting search for '{Query}' with pageNumber={PageNumber}, pageSize={PageSize}", query, pageNumber, pageSize);
-
- var searchQuery = query.Replace("dotnet", "net", StringComparison.InvariantCultureIgnoreCase);
+ var totalHits = (int)response.Total;
- try
+ var results = response.Documents.Select((doc, index) =>
{
- var response = await _client.SearchAsync(s => s
- .Indices(_elasticsearchOptions.IndexName)
- .Query(q => q
- .Bool(b => b
- .Should(
- // Tier 1: Exact/Prefix matches (highest boost)
- sh => sh.Prefix(p => p
- .Field("title.keyword")
- .Value(searchQuery)
- .CaseInsensitive(true)
- .Boost(300.0f)
- ),
-
- // Tier 2: Semantic search (combined into one clause)
- sh => sh.DisMax(dm => dm
- .Queries(
- dq => dq.Semantic(sem => sem
- .Field("title.semantic_text")
- .Query(searchQuery)
- ),
- dq => dq.Semantic(sem => sem
- .Field("abstract")
- .Query(searchQuery)
- )
- )
- .Boost(200.0f)
- ),
-
- // Tier 3: Standard text matching
- sh => sh.DisMax(dm => dm
- .Queries(
- dq => dq.MatchBoolPrefix(m => m
- .Field(f => f.Title)
- .Query(searchQuery)
- ),
- dq => dq.Match(m => m
- .Field(f => f.Title)
- .Query(searchQuery)
- .Operator(Operator.And)
- ),
- dq => dq.Match(m => m
- .Field(f => f.Abstract)
- .Query(searchQuery)
- )
- )
- .Boost(100.0f)
- ),
-
- // Tier 4: Parent matching
- sh => sh.Match(m => m
- .Field("parents.title")
- .Query(searchQuery)
- .Boost(75.0f)
- ),
-
- // Tier 5: Fuzzy fallback
- sh => sh.Match(m => m
- .Field(f => f.Title)
- .Query(searchQuery)
- .Fuzziness(1) // Reduced from 2
- .Boost(25.0f)
- )
- )
- .MustNot(mn => mn.Terms(t => t
- .Field("url.keyword")
- .Terms(factory => factory.Value("/docs", "/docs/", "/docs/404", "/docs/404/"))
- ))
- .MinimumShouldMatch(1)
- )
- )
- .From((pageNumber - 1) * pageSize)
- .Size(pageSize), ctx);
+ var hit = response.Hits.ElementAtOrDefault(index);
+ var highlights = hit?.Highlight;
- if (!response.IsValidResponse)
- {
- _logger.LogWarning("Elasticsearch search response was not valid. Reason: {Reason}",
- response.ElasticsearchServerError?.Error?.Reason ?? "Unknown");
- }
- else
+ string? highlightedBody = null;
+
+ if (highlights != null)
{
- _logger.LogInformation("Search completed for '{Query}'. Total hits: {TotalHits}", query, response.Total);
+ if (highlights.TryGetValue("stripped_body", out var bodyHighlights) && bodyHighlights.Count > 0)
+ highlightedBody = string.Join(". ", bodyHighlights.Select(h => h.TrimEnd('.')));
}
- return ProcessSearchResponse(response);
- }
- catch (Exception ex)
- {
- _logger.LogError(ex, "Error occurred during Elasticsearch search for '{Query}'", query);
- throw;
- }
- }
-
-
- private static (int TotalHits, List Results) ProcessSearchResponse(SearchResponse response)
- {
- var totalHits = (int)response.Total;
-
- var results = response.Documents.Select((doc, index) => new SearchResultItem
- {
- Url = doc.Url,
- Title = doc.Title,
- Description = doc.Description ?? string.Empty,
- Parents = doc.Parents.Select(parent => new SearchResultItemParent
+ return new SearchResultItem
{
- Title = parent.Title,
- Url = parent.Url
- }).ToArray(),
- Score = (float)(response.Hits.ElementAtOrDefault(index)?.Score ?? 0.0)
+ Url = doc.Url,
+ Title = doc.Title,
+ Description = doc.Description ?? string.Empty,
+ Parents = doc.Parents.Select(parent => new SearchResultItemParent
+ {
+ Title = parent.Title,
+ Url = parent.Url
+ }).ToArray(),
+ Score = (float)(hit?.Score ?? 0.0),
+ HighlightedBody = highlightedBody
+ };
}).ToList();
return (totalHits, results);
diff --git a/src/api/Elastic.Documentation.Api.Infrastructure/Aws/LocalParameterProvider.cs b/src/api/Elastic.Documentation.Api.Infrastructure/Aws/LocalParameterProvider.cs
index f4fea9f47..c86054c13 100644
--- a/src/api/Elastic.Documentation.Api.Infrastructure/Aws/LocalParameterProvider.cs
+++ b/src/api/Elastic.Documentation.Api.Infrastructure/Aws/LocalParameterProvider.cs
@@ -32,7 +32,7 @@ public async Task GetParam(string name, bool withDecryption = true, Canc
}
case "docs-elasticsearch-index":
{
- return "semantic-documentation-latest";
+ return GetEnv("DOCUMENTATION_ELASTIC_INDEX", "semantic-docs-dev-latest");
}
default:
{
@@ -41,11 +41,13 @@ public async Task GetParam(string name, bool withDecryption = true, Canc
}
}
- private static string GetEnv(string name)
+ private static string GetEnv(string name, string? defaultValue = null)
{
var value = Environment.GetEnvironmentVariable(name);
- if (string.IsNullOrEmpty(value))
- throw new ArgumentException($"Environment variable '{name}' not found.");
- return value;
+ if (!string.IsNullOrEmpty(value))
+ return value;
+ if (defaultValue != null)
+ return defaultValue;
+ throw new ArgumentException($"Environment variable '{name}' not found.");
}
}
diff --git a/src/api/Elastic.Documentation.Api.Infrastructure/Elastic.Documentation.Api.Infrastructure.csproj b/src/api/Elastic.Documentation.Api.Infrastructure/Elastic.Documentation.Api.Infrastructure.csproj
index 43cf5b65a..e9b64a3b9 100644
--- a/src/api/Elastic.Documentation.Api.Infrastructure/Elastic.Documentation.Api.Infrastructure.csproj
+++ b/src/api/Elastic.Documentation.Api.Infrastructure/Elastic.Documentation.Api.Infrastructure.csproj
@@ -11,6 +11,7 @@
+