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 @@ +