diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e622aa21d..69ee15caf 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -94,17 +94,7 @@ jobs: run: dotnet run --project build -c release -- unit-test - name: Publish AOT - if: ${{ matrix.os != 'ubuntu-latest' }} # publish containers already validates AOT build run: dotnet run --project build -c release -- publishbinaries - - - name: Publish Containers - if: ${{ matrix.os == 'ubuntu-latest' }} - run: dotnet run --project build -c release -- publishcontainers - - - name: Run Container - if: ${{ matrix.os == 'ubuntu-latest' }} - run: docker run elastic/docs-builder:ci-${{ github.event.pull_request.number }} --help - integration: runs-on: docs-builder-latest-16 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 596ef89bf..0cb2220b4 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -67,7 +67,7 @@ jobs: password: ${{ secrets.GITHUB_TOKEN }} - name: Publish Containers - run: ./build.sh publishcontainers + run: ./build.sh publishcontainers\ build-lambda: needs: diff --git a/Directory.Packages.props b/Directory.Packages.props index 969de380c..dcb97bed9 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -23,9 +23,8 @@ - - + diff --git a/build/Targets.fs b/build/Targets.fs index b82b6c7fe..47baaf75a 100644 --- a/build/Targets.fs +++ b/build/Targets.fs @@ -77,40 +77,32 @@ let private publishZip _ = let private publishContainers _ = let createImage project = - let ci = Environment.environVarOrNone "GITHUB_ACTIONS" - let pr = - match Environment.environVarOrNone "GITHUB_REF_NAME" with - | None -> None - | Some s when s.EndsWith "/merge" -> Some (s.Split('/') |> Seq.head) - | _ -> None let imageTag = match project with - | _ -> "9.0-noble-chiseled-aot" + | "docs-builder" -> "jammy-chiseled-aot" + | _ -> "jammy-chiseled-aot" let labels = let exitCode = exec { validExitCode (fun _ -> true) exit_code_of "git" "describe" "--tags" "--exact-match" "HEAD" } - match (exitCode, pr) with - | 0, _ -> "edge;latest" - | _, None -> "edge" - | _, Some pr -> $"ci-%s{pr}" + match exitCode with | 0 -> "edge;latest" | _ -> "edge" let args = ["publish"; $"src/tooling/%s{project}/%s{project}.csproj"] @ [ "/t:PublishContainer"; "-p"; "DebugType=none"; - "-p"; $"ContainerBaseImage=mcr.microsoft.com/dotnet/nightly/runtime-deps:%s{imageTag}"; + "-p"; $"ContainerBaseImage=mcr.microsoft.com/dotnet/nightly/runtime-deps:8.0-%s{imageTag}"; "-p"; $"ContainerImageTags=\"%s{labels};%s{Software.Version.Normalize()}\"" "-p"; $"ContainerRepository=elastic/%s{project}" ] let registry = - match (ci, pr) with - | Some _, None -> [ - "-p"; "ContainerRegistry=ghcr.io" - "-p"; "ContainerUser=1001:1001"; - ] - | _, _ -> [] + match Environment.environVarOrNone "GITHUB_ACTIONS" with + | None -> [] + | Some _ -> [ + "-p"; "ContainerRegistry=ghcr.io" + "-p"; "ContainerUser=1001:1001"; + ] exec { run "dotnet" (args @ registry) } createImage "docs-builder" createImage "docs-assembler" diff --git a/src/Elastic.Documentation.Site/Assets/web-components/SearchOrAskAi/AskAi/AskAiSuggestions.tsx b/src/Elastic.Documentation.Site/Assets/web-components/SearchOrAskAi/AskAi/AskAiSuggestions.tsx index 7399095dd..0af28a82c 100644 --- a/src/Elastic.Documentation.Site/Assets/web-components/SearchOrAskAi/AskAi/AskAiSuggestions.tsx +++ b/src/Elastic.Documentation.Site/Assets/web-components/SearchOrAskAi/AskAi/AskAiSuggestions.tsx @@ -1,11 +1,5 @@ import { useSearchActions, useSearchTerm } from '../search.store' -import { - EuiButton, - EuiIcon, - EuiSpacer, - EuiText, - useEuiTheme, -} from '@elastic/eui' +import { EuiButton, EuiSpacer, EuiText, useEuiTheme } from '@elastic/eui' import { css } from '@emotion/react' import * as React from 'react' @@ -32,16 +26,7 @@ export const AskAiSuggestions = (props: Props) => { ` return ( <> -
- - Ask Elastic Docs AI Assistant -
+ Ask Elastic Docs AI Assistant {searchTerm && ( { const searchTerm = useSearchTerm() - const [activePage, setActivePage] = useState(0) - const debouncedSearchTerm = useDebounce(searchTerm, 300) - useEffect(() => { - setActivePage(0) - }, [debouncedSearchTerm]) - const { data, error, isLoading, isFetching } = useSearchQuery({ - searchTerm, - pageNumber: activePage + 1, - }) + const { data, error, isLoading } = useSearchQuery() const { euiTheme } = useEuiTheme() if (!searchTerm) { @@ -37,193 +23,88 @@ export const SearchResults = () => { return
Error loading search results: {error.message}
} - return ( -
-
- {isLoading || isFetching ? ( - - ) : ( - - )} - - Search results for{' '} - - {searchTerm} - - + if (isLoading) { + return ( +
+ Loading search results...
- - {data && ( - <> -
    - {data.results.map((result) => ( - - ))} -
-
- - setActivePage(activePage) - } - /> -
- - )} -
- ) -} - -interface SearchResultListItemProps { - item: SearchResultItem -} - -function SearchResultListItem({ item: result }: SearchResultListItemProps) { - const { euiTheme } = useEuiTheme() - const searchTerm = useSearchTerm() - const highlightSearchTerms = useMemo( - () => searchTerm.toLowerCase().split(' '), - [searchTerm] - ) + ) + } - if (highlightSearchTerms.includes('esql')) { - highlightSearchTerms.push('es|ql') + if (!data || data.results.length === 0) { + return No results found for "{searchTerm}" } - if (highlightSearchTerms.includes('dotnet')) { - highlightSearchTerms.push('.net') + const buttonCss = css` + border: none; + vertical-align: top; + justify-content: flex-start; + block-size: 100%; + padding-block: 4px; + & > span { + justify-content: flex-start; + align-items: flex-start; + } + svg { + color: ${euiTheme.colors.textSubdued}; + } + .euiIcon { + margin-top: 4px; + } + ` + + const trimDescription = (description: string) => { + const limit = 200 + return description.length > limit + ? description.slice(0, limit) + '...' + : description } - return ( -
  • -
    - -
    - - - {result.title} - - - -
    -
    -
  • - ) -} -function Breadcrumbs({ - parents, - highlightSearchTerms, -}: { - parents: SearchResultItem['parents'] - highlightSearchTerms: string[] -}) { - const { euiTheme } = useEuiTheme() - const { fontSize: smallFontsize } = useEuiFontSize('xs') return ( -
      - {parents - .slice(1) // skip /docs - .map((parent) => ( -
    • - - Search Results for "{searchTerm}" + +
        + {data.results.map((result) => ( +
      • + +
        - + - {parent.title} - - - + {trimDescription(result.description)} + +
        +
        + {/**/} + {/*{result.title}*/}
      • ))} -
      +
    +
    ) } 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..d9cd3455f 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 @@ -1,56 +1,36 @@ -import { keepPreviousData, useQuery } from '@tanstack/react-query' +import { useSearchTerm } from '../search.store' +import { useQuery } from '@tanstack/react-query' import { useDebounce } from '@uidotdev/usehooks' import * as z from 'zod' -const SearchResultItemParent = z.object({ - url: z.string(), - title: z.string(), -}) - const SearchResultItem = z.object({ url: z.string(), title: z.string(), description: z.string(), score: z.number(), - parents: z.array(SearchResultItemParent), }) -export type SearchResultItem = z.infer - const SearchResponse = z.object({ results: z.array(SearchResultItem), totalResults: z.number(), - pageCount: z.number(), - pageNumber: z.number(), - pageSize: z.number(), }) -export type SearchResponse = z.infer - -type Props = { - searchTerm: string - pageNumber?: number -} +type SearchResponse = z.infer -export const useSearchQuery = ({ searchTerm, pageNumber = 1 }: Props) => { +export const useSearchQuery = () => { + const searchTerm = useSearchTerm() const trimmedSearchTerm = searchTerm.trim() const debouncedSearchTerm = useDebounce(trimmedSearchTerm, 300) return useQuery({ - queryKey: [ - 'search', - { searchTerm: debouncedSearchTerm.toLowerCase(), pageNumber }, - ], + queryKey: ['search', { searchTerm: debouncedSearchTerm }], queryFn: async () => { if (!debouncedSearchTerm || debouncedSearchTerm.length < 1) { return SearchResponse.parse({ results: [], totalResults: 0 }) } - const params = new URLSearchParams({ - q: debouncedSearchTerm, - page: pageNumber.toString(), - }) const response = await fetch( - '/docs/_api/v1/search?' + params.toString() + '/docs/_api/v1/search?q=' + + encodeURIComponent(debouncedSearchTerm) ) if (!response.ok) { throw new Error( @@ -62,7 +42,6 @@ export const useSearchQuery = ({ searchTerm, pageNumber = 1 }: Props) => { }, enabled: !!trimmedSearchTerm && trimmedSearchTerm.length >= 1, refetchOnWindowFocus: false, - placeholderData: keepPreviousData, - staleTime: 1000 * 60 * 5, // 5 minutes + staleTime: 1000 * 60 * 10, // 10 minutes }) } diff --git a/src/Elastic.Documentation/Search/DocumentationDocument.cs b/src/Elastic.Documentation/Search/DocumentationDocument.cs index 28c23ee3f..df036320d 100644 --- a/src/Elastic.Documentation/Search/DocumentationDocument.cs +++ b/src/Elastic.Documentation/Search/DocumentationDocument.cs @@ -7,15 +7,6 @@ namespace Elastic.Documentation.Search; -public record ParentDocument -{ - [JsonPropertyName("title")] - public string? Title { get; set; } - - [JsonPropertyName("url")] - public string? Url { get; set; } -} - public record DocumentationDocument { [JsonPropertyName("title")] @@ -39,12 +30,6 @@ public record DocumentationDocument [JsonPropertyName("body")] public string? Body { get; set; } - [JsonPropertyName("url_segment_count")] - public int? UrlSegmentCount { get; set; } - [JsonPropertyName("abstract")] public string? Abstract { get; set; } - - [JsonPropertyName("parents")] - public ParentDocument[] Parents { get; set; } = []; } diff --git a/src/Elastic.Markdown/DocumentationGenerator.cs b/src/Elastic.Markdown/DocumentationGenerator.cs index 49168cca1..723e8f32b 100644 --- a/src/Elastic.Markdown/DocumentationGenerator.cs +++ b/src/Elastic.Markdown/DocumentationGenerator.cs @@ -262,8 +262,7 @@ private async Task ProcessFile(HashSet offendingFiles, DocumentationFile Resolvers = DocumentationSet.MarkdownParser.Resolvers, Document = document, SourceFile = markdown, - DefaultOutputFile = outputFile, - DocumentationSet = DocumentationSet + DefaultOutputFile = outputFile }, ctx); } } diff --git a/src/Elastic.Markdown/Exporters/IMarkdownExporter.cs b/src/Elastic.Markdown/Exporters/IMarkdownExporter.cs index 492df8ced..570544141 100644 --- a/src/Elastic.Markdown/Exporters/IMarkdownExporter.cs +++ b/src/Elastic.Markdown/Exporters/IMarkdownExporter.cs @@ -18,7 +18,6 @@ public record MarkdownExportFileContext public required MarkdownFile SourceFile { get; init; } public required IFileInfo DefaultOutputFile { get; init; } public string? LLMText { get; set; } - public required DocumentationSet DocumentationSet { get; init; } } public interface IMarkdownExporter diff --git a/src/Elastic.Markdown/IO/DocumentationSet.cs b/src/Elastic.Markdown/IO/DocumentationSet.cs index e848ae8a6..7c8c5784d 100644 --- a/src/Elastic.Markdown/IO/DocumentationSet.cs +++ b/src/Elastic.Markdown/IO/DocumentationSet.cs @@ -78,9 +78,7 @@ INavigationItem[] GetParents(INavigationItem current) { if (parent is null) continue; - if (parents.All(i => i.Url != parent.Url)) - parents.Add(parent); - + parents.Add(parent); parent = parent.Parent; } while (parent != null); diff --git a/src/api/Elastic.Documentation.Api.Core/Search/SearchUsecase.cs b/src/api/Elastic.Documentation.Api.Core/Search/SearchUsecase.cs index 8114dd475..256ea1049 100644 --- a/src/api/Elastic.Documentation.Api.Core/Search/SearchUsecase.cs +++ b/src/api/Elastic.Documentation.Api.Core/Search/SearchUsecase.cs @@ -8,7 +8,6 @@ public class SearchUsecase(ISearchGateway searchGateway) { public async Task Search(SearchRequest request, Cancel ctx = default) { - // var validationResult = validator.Validate(request); // if (!validationResult.IsValid) // throw new ArgumentException(validationResult.Message); @@ -16,17 +15,13 @@ public async Task Search(SearchRequest request, Cancel ctx = def var (totalHits, results) = await searchGateway.SearchAsync( request.Query, request.PageNumber, - request.PageSize, - ctx + request.PageSize, ctx ); - return new SearchResponse { Results = results, - TotalResults = totalHits, - PageNumber = request.PageNumber, - PageSize = request.PageSize, + TotalResults = totalHits }; } } @@ -35,24 +30,13 @@ public record SearchRequest { public required string Query { get; init; } public int PageNumber { get; init; } = 1; - public int PageSize { get; init; } = 5; + public int PageSize { get; init; } = 10; } public record SearchResponse { public required IEnumerable Results { get; init; } public required int TotalResults { get; init; } - public required int PageNumber { get; init; } - public required int PageSize { get; init; } - public int PageCount => TotalResults > 0 - ? (int)Math.Ceiling((double)TotalResults / PageSize) - : 0; -} - -public record SearchResultItemParent -{ - public required string Title { get; init; } - public required string Url { get; init; } } public record SearchResultItem @@ -60,6 +44,5 @@ public record SearchResultItem public required string Url { get; init; } public required string Title { get; init; } public required string Description { get; init; } - public required SearchResultItemParent[] Parents { get; init; } - public float Score { get; init; } + public required double Score { get; init; } } diff --git a/src/api/Elastic.Documentation.Api.Infrastructure/Adapters/AskAi/LlmGatewayAskAiGateway.cs b/src/api/Elastic.Documentation.Api.Infrastructure/Adapters/AskAi/LlmGatewayAskAiGateway.cs index 3430ecec5..fb7236c72 100644 --- a/src/api/Elastic.Documentation.Api.Infrastructure/Adapters/AskAi/LlmGatewayAskAiGateway.cs +++ b/src/api/Elastic.Documentation.Api.Infrastructure/Adapters/AskAi/LlmGatewayAskAiGateway.cs @@ -40,7 +40,7 @@ string ThreadId public static LlmGatewayRequest CreateFromRequest(AskAiRequest request) => new( UserContext: new UserContext("elastic-docs-v3@invalid"), - PlatformContext: new PlatformContext("docs_site", "docs_assistant", []), + PlatformContext: new PlatformContext("support_portal", "support_assistant", []), Input: [ new ChatInput("user", AskAiRequest.SystemPrompt), diff --git a/src/api/Elastic.Documentation.Api.Infrastructure/Adapters/Search/ElasticsearchGateway.cs b/src/api/Elastic.Documentation.Api.Infrastructure/Adapters/Search/ElasticsearchGateway.cs deleted file mode 100644 index 0f14f4f2f..000000000 --- a/src/api/Elastic.Documentation.Api.Infrastructure/Adapters/Search/ElasticsearchGateway.cs +++ /dev/null @@ -1,196 +0,0 @@ -// 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.Serialization; -using Elastic.Clients.Elasticsearch; -using Elastic.Clients.Elasticsearch.QueryDsl; -using Elastic.Clients.Elasticsearch.Serialization; -using Elastic.Documentation.Api.Core.Search; -using Elastic.Transport; -using Microsoft.Extensions.Logging; - -namespace Elastic.Documentation.Api.Infrastructure.Adapters.Search; - -internal sealed record DocumentDto -{ - [JsonPropertyName("title")] - public required string Title { get; init; } - - [JsonPropertyName("url")] - public required string Url { get; init; } - - [JsonPropertyName("description")] - public string? Description { get; init; } - - [JsonPropertyName("body")] - public string? Body { get; init; } - - [JsonPropertyName("abstract")] - public required string Abstract { get; init; } - - [JsonPropertyName("url_segment_count")] - public int UrlSegmentCount { get; init; } - - [JsonPropertyName("parents")] - public ParentDocumentDto[] Parents { get; init; } = []; -} - -internal sealed record ParentDocumentDto -{ - [JsonPropertyName("title")] - public required string Title { get; init; } - - [JsonPropertyName("url")] - public required string Url { get; init; } -} - -public class ElasticsearchGateway : ISearchGateway -{ - private readonly ElasticsearchClient _client; - private readonly ElasticsearchOptions _elasticsearchOptions; - private readonly ILogger _logger; - - public ElasticsearchGateway(ElasticsearchOptions elasticsearchOptions, ILogger logger) - { - _logger = logger; - _elasticsearchOptions = elasticsearchOptions; - var nodePool = new SingleNodePool(new Uri(elasticsearchOptions.Url.Trim())); - var clientSettings = new ElasticsearchClientSettings( - nodePool, - sourceSerializer: (_, settings) => new DefaultSourceSerializer(settings, EsJsonContext.Default) - ) - .DefaultIndex(elasticsearchOptions.IndexName) - .Authentication(new ApiKey(elasticsearchOptions.ApiKey)); - - _client = new ElasticsearchClient(clientSettings); - } - - public async Task<(int TotalHits, List Results)> SearchAsync(string query, int pageNumber, int pageSize, Cancel ctx = default) => - await ExactSearchAsync(query, pageNumber, pageSize, ctx); - - public async Task<(int TotalHits, List Results)> ExactSearchAsync(string query, int pageNumber, int pageSize, Cancel ctx = default) - { - _logger.LogInformation("Starting search for '{Query}' with pageNumber={PageNumber}, pageSize={PageSize}", query, pageNumber, pageSize); - - var searchQuery = query.Replace("dotnet", "net", StringComparison.InvariantCultureIgnoreCase); - - try - { - 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); - - if (!response.IsValidResponse) - { - _logger.LogWarning("Elasticsearch search response was not valid. Reason: {Reason}", - response.ElasticsearchServerError?.Error?.Reason ?? "Unknown"); - } - else - { - _logger.LogInformation("Search completed for '{Query}'. Total hits: {TotalHits}", query, response.Total); - } - - 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 - { - Title = parent.Title, - Url = parent.Url - }).ToArray(), - Score = (float)(response.Hits.ElementAtOrDefault(index)?.Score ?? 0.0) - }).ToList(); - - return (totalHits, results); - } -} - -[JsonSerializable(typeof(DocumentDto))] -[JsonSerializable(typeof(ParentDocumentDto))] -internal sealed partial class EsJsonContext : JsonSerializerContext; diff --git a/src/api/Elastic.Documentation.Api.Infrastructure/Adapters/Search/ElasticsearchOptions.cs b/src/api/Elastic.Documentation.Api.Infrastructure/Adapters/Search/ElasticsearchOptions.cs deleted file mode 100644 index d30a3083a..000000000 --- a/src/api/Elastic.Documentation.Api.Infrastructure/Adapters/Search/ElasticsearchOptions.cs +++ /dev/null @@ -1,14 +0,0 @@ -// 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 Elastic.Documentation.Api.Infrastructure.Aws; - -namespace Elastic.Documentation.Api.Infrastructure.Adapters.Search; - -public class ElasticsearchOptions(IParameterProvider parameterProvider) -{ - public string Url { get; } = parameterProvider.GetParam("docs-elasticsearch-url").GetAwaiter().GetResult(); - public string ApiKey { get; } = parameterProvider.GetParam("docs-elasticsearch-apikey").GetAwaiter().GetResult(); - public string IndexName { get; } = parameterProvider.GetParam("docs-elasticsearch-index").GetAwaiter().GetResult() ?? "documentation-latest"; -} diff --git a/src/api/Elastic.Documentation.Api.Infrastructure/Adapters/Search/MockSearchGateway.cs b/src/api/Elastic.Documentation.Api.Infrastructure/Adapters/Search/MockSearchGateway.cs index 2e5705986..608b83201 100644 --- a/src/api/Elastic.Documentation.Api.Infrastructure/Adapters/Search/MockSearchGateway.cs +++ b/src/api/Elastic.Documentation.Api.Infrastructure/Adapters/Search/MockSearchGateway.cs @@ -16,14 +16,14 @@ public class MockSearchGateway : ISearchGateway Title = "Kibana: Explore, Visualize, Discover Data", Description = "Run data analytics at speed and scale for observability, security, and search with Kibana. Powerful analysis on any data from any source.", - Parents = [] + Score = 0.92 }, new SearchResultItem { Url = "https://www.elastic.co/docs/explore-analyze", Title = "Explore and analyze | Elastic Docs", Description = "Kibana provides a comprehensive suite of tools to help you search, interact with, explore, and analyze your data effectively.", - Parents = [] + Score = 0.86 }, new SearchResultItem { @@ -31,7 +31,7 @@ public class MockSearchGateway : ISearchGateway Title = "Install Kibana | Elastic Docs", Description = "Information on how to set up Kibana and get it running, including downloading, enrollment with Elasticsearch cluster, and configuration.", - Parents = [] + Score = 0.75 }, new SearchResultItem { @@ -39,7 +39,7 @@ public class MockSearchGateway : ISearchGateway Title = "Kibana Lens – Data visualization. Simply.", Description = "Kibana Lens simplifies the process of data visualization through a drag‑and‑drop experience, ideal for exploring logs, trends, and metrics.", - Parents = [] + Score = 0.70 }, new SearchResultItem { @@ -47,7 +47,7 @@ public class MockSearchGateway : ISearchGateway Title = "Elastic Docs – Elastic products, guides & reference", Description = "Official Elastic documentation. Explore guides for Elastic Cloud (hosted & on‑prem), product documentation, how‑to guides and API reference.", - Parents = [] + Score = 0.88 }, new SearchResultItem { @@ -55,14 +55,14 @@ public class MockSearchGateway : ISearchGateway Title = "Get started | Elastic Docs", Description = "Use Elasticsearch to search, index, store, and analyze data of all shapes and sizes in near real time. Kibana is the graphical user interface for Elasticsearch.", - Parents = [] + Score = 0.85 }, new SearchResultItem { Url = "https://www.elastic.co/docs/solutions/search/elasticsearch-basics-quickstart", Title = "Elasticsearch basics quickstart", Description = "Hands‑on introduction to fundamental Elasticsearch concepts: indices, documents, mappings, and search via Console syntax.", - Parents = [] + Score = 0.80 }, new SearchResultItem { @@ -70,26 +70,21 @@ public class MockSearchGateway : ISearchGateway Title = "Elasticsearch API documentation", Description = "Elastic provides REST APIs that are used by the UI components and can be called directly to configure and access Elasticsearch features.", - Parents = [] + Score = 0.78 } ]; public async Task<(int TotalHits, List Results)> SearchAsync(string query, int pageNumber, int pageSize, CancellationToken ctx = default) { var filteredResults = Results - .Where(item => - item.Title.Equals(query, StringComparison.OrdinalIgnoreCase) || - item.Description?.Equals(query, StringComparison.OrdinalIgnoreCase) == true) - .ToList(); - - var pagedResults = filteredResults + .Where(item => query.Split(' ') + .All(token => item.Title.Contains(token, StringComparison.OrdinalIgnoreCase) || + item.Description.Contains(token, StringComparison.OrdinalIgnoreCase))) .Skip((pageNumber - 1) * pageSize) .Take(pageSize) .ToList(); - Console.WriteLine($"MockSearchGateway: Paged results count: {pagedResults.Count}"); - return await Task.Delay(1000, ctx) - .ContinueWith(_ => (TotalHits: filteredResults.Count, Results: pagedResults), ctx); + .ContinueWith(_ => (TotalHits: filteredResults.Count, Results: filteredResults), ctx); } } diff --git a/src/api/Elastic.Documentation.Api.Infrastructure/Aws/LocalParameterProvider.cs b/src/api/Elastic.Documentation.Api.Infrastructure/Aws/LocalParameterProvider.cs index f4fea9f47..66d817d61 100644 --- a/src/api/Elastic.Documentation.Api.Infrastructure/Aws/LocalParameterProvider.cs +++ b/src/api/Elastic.Documentation.Api.Infrastructure/Aws/LocalParameterProvider.cs @@ -12,7 +12,10 @@ public async Task GetParam(string name, bool withDecryption = true, Canc { case "llm-gateway-service-account": { - var serviceAccountKeyPath = GetEnv("LLM_GATEWAY_SERVICE_ACCOUNT_KEY_PATH"); + const string envName = "LLM_GATEWAY_SERVICE_ACCOUNT_KEY_PATH"; + var serviceAccountKeyPath = Environment.GetEnvironmentVariable(envName); + if (string.IsNullOrEmpty(serviceAccountKeyPath)) + throw new ArgumentException($"Environment variable '{envName}' not found."); if (!File.Exists(serviceAccountKeyPath)) throw new ArgumentException($"Service account key file not found at '{serviceAccountKeyPath}'."); var serviceAccountKey = await File.ReadAllTextAsync(serviceAccountKeyPath, ctx); @@ -20,19 +23,11 @@ public async Task GetParam(string name, bool withDecryption = true, Canc } case "llm-gateway-function-url": { - return GetEnv("LLM_GATEWAY_FUNCTION_URL"); - } - case "docs-elasticsearch-url": - { - return GetEnv("DOCUMENTATION_ELASTIC_URL"); - } - case "docs-elasticsearch-apikey": - { - return GetEnv("DOCUMENTATION_ELASTIC_APIKEY"); - } - case "docs-elasticsearch-index": - { - return "semantic-documentation-latest"; + const string envName = "LLM_GATEWAY_FUNCTION_URL"; + var value = Environment.GetEnvironmentVariable(envName); + if (string.IsNullOrEmpty(value)) + throw new ArgumentException($"Environment variable '{envName}' not found."); + return value; } default: { @@ -40,12 +35,4 @@ public async Task GetParam(string name, bool withDecryption = true, Canc } } } - - private static string GetEnv(string name) - { - var value = Environment.GetEnvironmentVariable(name); - if (string.IsNullOrEmpty(value)) - throw new ArgumentException($"Environment variable '{name}' not found."); - return value; - } } 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 c7fc40d3a..effd23891 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 @@ -16,8 +16,7 @@ - - + diff --git a/src/api/Elastic.Documentation.Api.Infrastructure/MappingsExstension.cs b/src/api/Elastic.Documentation.Api.Infrastructure/MappingsExstension.cs index 1dcede6d9..fadc1afd5 100644 --- a/src/api/Elastic.Documentation.Api.Infrastructure/MappingsExstension.cs +++ b/src/api/Elastic.Documentation.Api.Infrastructure/MappingsExstension.cs @@ -32,21 +32,14 @@ private static void MapAskAiEndpoint(IEndpointRouteBuilder group) private static void MapSearchEndpoint(IEndpointRouteBuilder group) { var searchGroup = group.MapGroup("/search"); - _ = searchGroup.MapGet("/", - async ( - [FromQuery(Name = "q")] string query, - [FromQuery(Name = "page")] int? pageNumber, - SearchUsecase searchUsecase, - Cancel ctx - ) => + _ = searchGroup.MapGet("/", async ([FromQuery(Name = "q")] string query, SearchUsecase searchUsecase, Cancel ctx) => + { + var searchRequest = new SearchRequest { - var searchRequest = new SearchRequest - { - Query = query, - PageNumber = pageNumber ?? 1 - }; - var searchResponse = await searchUsecase.Search(searchRequest, ctx); - return Results.Ok(searchResponse); - }); + Query = query + }; + var searchResponse = await searchUsecase.Search(searchRequest, ctx); + return Results.Ok(searchResponse); + }); } } diff --git a/src/api/Elastic.Documentation.Api.Infrastructure/ServicesExtension.cs b/src/api/Elastic.Documentation.Api.Infrastructure/ServicesExtension.cs index 293547b92..43862f029 100644 --- a/src/api/Elastic.Documentation.Api.Infrastructure/ServicesExtension.cs +++ b/src/api/Elastic.Documentation.Api.Infrastructure/ServicesExtension.cs @@ -113,8 +113,7 @@ private static void AddSearchUsecase(IServiceCollection services, AppEnv appEnv) { var logger = GetLogger(services); logger?.LogInformation("Configuring Search use case for environment {AppEnvironment}", appEnv); - _ = services.AddScoped(); - _ = services.AddScoped(); + _ = services.AddScoped(); _ = services.AddScoped(); } } diff --git a/src/tooling/Elastic.Documentation.Tooling/Exporters/ElasticsearchMarkdownExporter.cs b/src/tooling/Elastic.Documentation.Tooling/Exporters/ElasticsearchMarkdownExporter.cs index 20499b6f9..e5bff6804 100644 --- a/src/tooling/Elastic.Documentation.Tooling/Exporters/ElasticsearchMarkdownExporter.cs +++ b/src/tooling/Elastic.Documentation.Tooling/Exporters/ElasticsearchMarkdownExporter.cs @@ -12,10 +12,8 @@ using Elastic.Ingest.Elasticsearch.Catalog; using Elastic.Ingest.Elasticsearch.Semantic; using Elastic.Markdown.Exporters; -using Elastic.Markdown.IO; using Elastic.Transport; using Elastic.Transport.Products.Elasticsearch; -using Markdig.Syntax; using Microsoft.Extensions.Logging; namespace Elastic.Documentation.Tooling.Exporters; @@ -35,7 +33,6 @@ public class ElasticsearchMarkdownExporter(ILoggerFactory logFactory, IDiagnosti /// protected override CatalogIndexChannel NewChannel(CatalogIndexChannelOptions options) => new(options); } - public class ElasticsearchMarkdownSemanticExporter(ILoggerFactory logFactory, IDiagnosticsCollector collector, DocumentationEndpoints endpoints) : ElasticsearchMarkdownExporterBase, SemanticIndexChannel> (logFactory, collector, endpoints) @@ -44,11 +41,10 @@ public class ElasticsearchMarkdownSemanticExporter(ILoggerFactory logFactory, ID protected override SemanticIndexChannelOptions NewOptions(DistributedTransport transport) => new(transport) { GetMapping = (inferenceId, _) => CreateMapping(inferenceId), - GetMappingSettings = (_, _) => CreateMappingSetting(), IndexFormat = "semantic-documentation-{0:yyyy.MM.dd.HHmmss}", ActiveSearchAlias = "semantic-documentation", IndexNumThreads = IndexNumThreads, - InferenceCreateTimeout = TimeSpan.FromMinutes(4) + InferenceCreateTimeout = TimeSpan.FromMinutes(4), }; /// @@ -71,87 +67,34 @@ public abstract class ElasticsearchMarkdownExporterBase 8; - protected static string CreateMappingSetting() => - // language=json - """ - { - "analysis": { - "analyzer": { - "synonyms_analyzer": { - "tokenizer": "whitespace", - "filter": [ - "lowercase", - "synonyms_filter" - ] - } - }, - "filter": { - "synonyms_filter": { - "type": "synonym", - "synonyms_set": "docs", - "updateable": true - } - } - } - } - """; - protected static string CreateMapping(string? inferenceId) => // langugage=json $$""" - { - "properties": { - "title": { - "type": "text", - "search_analyzer": "synonyms_analyzer", - "fields": { - "keyword": { - "type": "keyword" - } - {{(!string.IsNullOrWhiteSpace(inferenceId) ? $$""", "semantic_text": {{{InferenceMapping(inferenceId)}}}""" : "")}} - } - }, - "url": { - "type": "text", - "fields": { - "keyword": { - "type": "keyword" - } - } - }, - "url_segment_count": { - "type": "integer" - }, - "body": { - "type": "text" - } - {{(!string.IsNullOrWhiteSpace(inferenceId) ? AbstractInferenceMapping(inferenceId) : AbstractMapping())}} - } + { + "properties": { + "title": { "type": "text" }, + "body": { "type": "text" } + {{(!string.IsNullOrWhiteSpace(inferenceId) ? AbstractInferenceMapping(inferenceId) : AbstractMapping())}} } - """; + } + """; private static string AbstractMapping() => // langugage=json """ , "abstract": { - "type": "text" + "type": "text", } """; - private static string InferenceMapping(string inferenceId) => - // langugage=json - $""" - "type": "semantic_text", - "inference_id": "{inferenceId}" - """; - private static string AbstractInferenceMapping(string inferenceId) => // langugage=json $$""" - , "abstract": { - {{InferenceMapping(inferenceId)}} - } - """; + , "abstract": { + "type": "semantic_text", + "inference_id": "{{inferenceId}}" + } + """; public async ValueTask StartAsync(Cancel ctx = default) { @@ -169,7 +112,6 @@ public async ValueTask StartAsync(Cancel ctx = default) }; var transport = new DistributedTransport(configuration); - //The max num threads per allocated node, from testing its best to limit our max concurrency //producing to this number as well var options = NewOptions(transport); @@ -238,41 +180,18 @@ public async ValueTask ExportAsync(MarkdownExportFileContext fileContext, var file = fileContext.SourceFile; var url = file.Url; - if (url is "/docs" or "/docs/404") - { - // Skip the root and 404 pages - _logger.LogInformation("Skipping export for {Url}", url); - return true; - } - - 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(document, fileContext.BuildContext); - - var headings = fileContext.Document.Descendants() - .Select(h => (h.GetData("header") as string) ?? string.Empty) - .Where(text => !string.IsNullOrEmpty(text)) - .ToArray(); - + var body = fileContext.LLMText ??= LlmMarkdownExporter.ConvertToLlmMarkdown(fileContext.Document, fileContext.BuildContext); var doc = new DocumentationDocument { Title = file.Title, Url = url, Body = body, Description = fileContext.SourceFile.YamlFrontMatter?.Description, - Abstract = !string.IsNullOrEmpty(body) - ? body[..Math.Min(body.Length, 400)] + " " + string.Join(" \n- ", headings) + ? body[..Math.Min(body.Length, 400)] : string.Empty, Applies = fileContext.SourceFile.YamlFrontMatter?.AppliesTo, - UrlSegmentCount = url.Split('/', StringSplitOptions.RemoveEmptyEntries).Length, - Parents = navigation.GetParentsOfMarkdownFile(file).Select(i => new ParentDocument - { - Title = i.NavigationTitle, - Url = i.Url - }).Reverse().ToArray(), - Headings = headings }; return await TryWrite(doc, ctx); }