Skip to content

refactor(search): consume Elastic.Internal.Search.* from NuGet#3364

Open
Mpdreamz wants to merge 5 commits into
mainfrom
feature/search-contract
Open

refactor(search): consume Elastic.Internal.Search.* from NuGet#3364
Mpdreamz wants to merge 5 commits into
mainfrom
feature/search-contract

Conversation

@Mpdreamz
Copy link
Copy Markdown
Member

Why

The search contract and Elasticsearch implementation were developed in a sibling repo (website-ai-search) and temporarily wired in via on-disk ProjectReference entries. Now that both packages ship to nuget.org as Elastic.Internal.Search.Contract and Elastic.Internal.Search.Elasticsearch, the temporary workaround can be removed.

What

  • Replaced on-disk ProjectReference entries in Elastic.Documentation.Search.csproj and Elastic.Documentation.csproj with proper PackageReference entries for Elastic.Internal.Search.Contract and Elastic.Internal.Search.Elasticsearch.
  • Removed the website-search-data-local local feed from NuGet.config; nuget.org is now the sole source.
  • Bumped both package version pins in Directory.Packages.props to 0.0.0-alpha.0.139 (.138 was no longer available on the feed).

Mpdreamz and others added 3 commits May 20, 2026 17:19
…te-search-data

Move all shared search types (SearchDocumentBase polymorphic root, document
records, mapping contexts, request/response DTOs, ISearchService<TDocument>)
to the new Elastic.Internal.Search.Contract package owned by website-search-data.
Reference it via on-disk ProjectReference for now; swap to a real NuGet feed
once published.

- Delete the docs-builder Elastic.Documentation.Search.Contract project; all
  three duplicate types (DocumentationDocument, IndexedProduct, AppliesToEntry)
  now come from the shared package.
- Bump Elastic.Mapping to 0.44.1 to match the package's transitive requirement.
- DocumentationMappingContext (with the docs-{type}.semantic-{env} index template)
  moves into the shared package too; docs-builder consumes it directly.
- Migrate code from settable Type = "doc"/"api" to ContentType assignments; the
  new Contract makes Type a JsonIgnore virtual discriminator. Existing indexed
  data keeps working via the content_type field already written by docs-builder.

dotnet build docs-builder.slnx: 0 errors.
dotnet test tests/Elastic.Markdown.Tests --filter '~Search': 16/16 pass.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Swap on-disk ProjectReferences (sibling website-ai-search checkout) for
PackageReferences to Elastic.Internal.Search.Contract and
Elastic.Internal.Search.Elasticsearch (0.0.0-alpha.0.139). Remove the
local NuGet feed from NuGet.config now that packages are on nuget.org.

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
dotnet format fixes import ordering and IDE0002 simplifications across
files that gained new using directives from the Elastic.Internal.Search.*
package swap.

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
@Mpdreamz Mpdreamz requested a review from a team as a code owner May 20, 2026 15:24
@Mpdreamz Mpdreamz added the chore label May 20, 2026
@Mpdreamz Mpdreamz requested a review from technige May 20, 2026 15:24
@Mpdreamz Mpdreamz added the chore label May 20, 2026
@Mpdreamz Mpdreamz had a problem deploying to integration-tests May 20, 2026 15:24 — with GitHub Actions Failure
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 20, 2026

Review Change Stack

📝 Walkthrough

Walkthrough

This PR externalizes the docs-builder's search implementation by migrating from a local Elastic.Documentation.Search.Contract project to shared Elastic.Internal.Search.* NuGet packages. The refactoring converts FullSearchService and NavigationSearchService into adapter layers delegating to a shared ISearchService<DocumentationDocument>. The document schema changes Type to ContentType, and ProductsConfiguration gains an IProductNameLookup implementation for product display name resolution. Local search utilities (SearchQueryBuilder, SearchResultProcessor, StringHighlightExtensions) are removed, and tests are updated to match new serialization contexts and property names.

Possibly related PRs

  • elastic/docs-builder#3313: Builds on this PR's external contract migration by updating document construction/serialization to use the new ContentType field rather than Type.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 29.55% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the main refactoring: consuming Elastic.Internal.Search packages from NuGet instead of on-disk ProjectReferences.
Description check ✅ Passed The description clearly explains the motivation (moving from temporary on-disk wiring to published NuGet packages), the changes made (PackageReference replacements, NuGet.config updates, version bumps), and the rationale.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
✨ Simplify code
  • Create PR with simplified code
  • Commit simplified code in branch feature/search-contract

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

First published version on nuget.org; version scheme changed from
0.0.0-alpha.* to 0.1.0-canary.*.

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (1)
src/services/search/Elastic.Documentation.Search/ChangesService.cs (1)

112-120: ⚡ Quick win

Align _source includes with DocumentationDocument.ContentType
ChangesService’s _source filter still requests e => e.Type, but DocumentationDocumentSerializationTests show Type is the polymorphic discriminator (while the actual content type is ContentType, used in other search services). Keep the payload/contract consistent by including ContentType.

Proposed fix
-							e => e.Type,
+							e => e.ContentType,
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/services/search/Elastic.Documentation.Search/ChangesService.cs` around
lines 112 - 120, The _source filter in ChangesService is including e => e.Type
(the polymorphic discriminator) instead of the actual content type field; update
the .Source(...).Filter(...).Includes(...) call in ChangesService to remove e =>
e.Type and include e => e.ContentType so the payload matches
DocumentationDocument's contract (as used by other search services and tested by
DocumentationDocumentSerializationTests).
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@NuGet.config`:
- Around line 3-5: The NuGet.config's <packageSources> currently adds only
nuget.org but will inherit other feeds from higher-level configs; update the
<packageSources> block to include a <clear /> element before the <add
key="nuget.org" ... /> entry so that nuget.org becomes the sole package source
(i.e., insert <clear /> above the existing <add> line inside the packageSources
element).

In `@src/services/search/Elastic.Documentation.Search/FullSearchService.cs`:
- Around line 86-90: The RelatedProducts projection in FullSearchService (the
block that maps item.Document.RelatedProducts using MapProduct) can return null
when there are no related products; change it to normalize to an empty array
like Parents does — filter and map as before but ensure the final value is
non-null by returning Array.Empty<YourProductType>() (or use the equivalent
empty array construct) when the source is null or yields no items, keeping
MapProduct and the null-check on p.Id intact.

In `@src/services/search/Elastic.Documentation.Search/ServicesExtension.cs`:
- Around line 31-62: ElasticsearchClientAccessor is registered as a singleton
but NavigationSearchService and FullSearchService (the scoped adapters)
currently call clientAccessor.Dispose() in their Dispose methods, which will
tear down the shared singleton; remove that disposal call (or add an explicit
ownership flag so only the component that created the accessor disposes it) from
the Dispose/DisposeAsync implementations in NavigationSearchService and
FullSearchService (and any other scoped types that call Dispose on
ElasticsearchClientAccessor), ensuring only the owner of
ElasticsearchClientAccessor disposes it.

---

Nitpick comments:
In `@src/services/search/Elastic.Documentation.Search/ChangesService.cs`:
- Around line 112-120: The _source filter in ChangesService is including e =>
e.Type (the polymorphic discriminator) instead of the actual content type field;
update the .Source(...).Filter(...).Includes(...) call in ChangesService to
remove e => e.Type and include e => e.ContentType so the payload matches
DocumentationDocument's contract (as used by other search services and tested by
DocumentationDocumentSerializationTests).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: f2ae2ec0-bd5e-49aa-b22a-741a5d0f4d4d

📥 Commits

Reviewing files that changed from the base of the PR and between 9243c64 and 339e397.

📒 Files selected for processing (35)
  • Directory.Packages.props
  • NuGet.config
  • docs-builder.slnx
  • src/Elastic.ApiExplorer/Elasticsearch/OpenApiDocumentExporter.cs
  • src/Elastic.Documentation.Configuration/Products/Product.cs
  • src/Elastic.Documentation/AppliesTo/ApplicableTo.cs
  • src/Elastic.Documentation/Elastic.Documentation.csproj
  • src/Elastic.Documentation/Serialization/SourceGenerationContext.cs
  • src/Elastic.Markdown/Exporters/Elasticsearch/ElasticsearchMarkdownExporter.Export.cs
  • src/Elastic.Markdown/Exporters/Elasticsearch/ElasticsearchMarkdownExporter.cs
  • src/api/Elastic.Documentation.Mcp.Remote/Gateways/DocumentGateway.cs
  • src/services/Elastic.Documentation.Assembler/Building/AssemblerSitemapService.cs
  • src/services/search/Elastic.Documentation.Search.Contract/AppliesToEntry.cs
  • src/services/search/Elastic.Documentation.Search.Contract/DocumentationDocument.cs
  • src/services/search/Elastic.Documentation.Search.Contract/DocumentationMappingConfig.cs
  • src/services/search/Elastic.Documentation.Search.Contract/Elastic.Documentation.Search.Contract.csproj
  • src/services/search/Elastic.Documentation.Search.Contract/IndexedProduct.cs
  • src/services/search/Elastic.Documentation.Search/ChangesService.cs
  • src/services/search/Elastic.Documentation.Search/Common/ElasticsearchClientAccessor.cs
  • src/services/search/Elastic.Documentation.Search/Common/SearchQueryBuilder.cs
  • src/services/search/Elastic.Documentation.Search/Common/SearchResultProcessor.cs
  • src/services/search/Elastic.Documentation.Search/Elastic.Documentation.Search.csproj
  • src/services/search/Elastic.Documentation.Search/EsJsonContext.cs
  • src/services/search/Elastic.Documentation.Search/FullSearchService.cs
  • src/services/search/Elastic.Documentation.Search/NavigationSearchService.cs
  • src/services/search/Elastic.Documentation.Search/ServicesExtension.cs
  • src/services/search/Elastic.Documentation.Search/StringHighlightExtensions.cs
  • tests-integration/Elastic.ContentDateEnrichment.IntegrationTests/ContentDateEnrichmentTests.cs
  • tests-integration/Elastic.Documentation.IntegrationTests/Search/SearchBootstrapFixture.cs
  • tests-integration/Mcp.Remote.IntegrationTests/McpToolsIntegrationTestsBase.cs
  • tests-integration/Search.IntegrationTests/SearchRelevanceTests.cs
  • tests/Elastic.ApiExplorer.Tests/OpenApiDocumentExporterTests.cs
  • tests/Elastic.Changelog.Tests/Changelogs/ChangelogConfigurationTests.cs
  • tests/Elastic.Documentation.Api.Infrastructure.Tests/Adapters/Search/StringHighlightExtensionsTests.cs
  • tests/Elastic.Markdown.Tests/Search/DocumentationDocumentSerializationTests.cs
💤 Files with no reviewable changes (10)
  • src/services/search/Elastic.Documentation.Search.Contract/IndexedProduct.cs
  • src/services/search/Elastic.Documentation.Search.Contract/DocumentationDocument.cs
  • src/services/search/Elastic.Documentation.Search.Contract/AppliesToEntry.cs
  • src/services/search/Elastic.Documentation.Search.Contract/Elastic.Documentation.Search.Contract.csproj
  • src/services/search/Elastic.Documentation.Search/Common/SearchQueryBuilder.cs
  • src/services/search/Elastic.Documentation.Search/StringHighlightExtensions.cs
  • src/services/search/Elastic.Documentation.Search/Common/SearchResultProcessor.cs
  • src/services/search/Elastic.Documentation.Search.Contract/DocumentationMappingConfig.cs
  • docs-builder.slnx
  • tests/Elastic.Changelog.Tests/Changelogs/ChangelogConfigurationTests.cs

Comment thread NuGet.config
Comment on lines +3 to +5
<packageSources>
<add key="nuget.org" value="https://api.nuget.org/v3/index.json" protocolVersion="3" />
</packageSources>
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🌐 Web query:

In NuGet.config, if <packageSources> does not include <clear />, do sources from user-level or machine-level config remain active due to hierarchical merging?

💡 Result:

Yes, if the element is not included in the section of a NuGet.config file, the package sources defined in higher-level configuration files (such as user-level or machine-level configs) remain active due to the hierarchical nature of NuGet configuration [1][2][3]. NuGet resolves its configuration by merging files from multiple locations—typically starting from the project or solution directory and traversing up the directory tree, finally including user-level and machine-level configuration files [2][3]. By default, this process aggregates sources from all encountered configuration files [1]. Including the element within the section is the explicit mechanism to instruct NuGet to ignore (clear) all package sources defined in any previously loaded (higher-level) configuration files [4][1][5].

Citations:


Add <clear /> to make nuget.org the only NuGet package source
NuGet merges <packageSources> from higher-level configs unless <clear /> is present, so other feeds can remain active and conflict with the “sole source” intent. (NuGet.config, lines 3–5)

Suggested fix
 <configuration>
   <packageSources>
+    <clear />
     <add key="nuget.org" value="https://api.nuget.org/v3/index.json" protocolVersion="3" />
   </packageSources>
 </configuration>
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<packageSources>
<add key="nuget.org" value="https://api.nuget.org/v3/index.json" protocolVersion="3" />
</packageSources>
<configuration>
<packageSources>
<clear />
<add key="nuget.org" value="https://api.nuget.org/v3/index.json" protocolVersion="3" />
</packageSources>
</configuration>
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@NuGet.config` around lines 3 - 5, The NuGet.config's <packageSources>
currently adds only nuget.org but will inherit other feeds from higher-level
configs; update the <packageSources> block to include a <clear /> element before
the <add key="nuget.org" ... /> entry so that nuget.org becomes the sole package
source (i.e., insert <clear /> above the existing <add> line inside the
packageSources element).

Comment on lines +86 to +90
RelatedProducts = item.Document.RelatedProducts?
.Where(p => p.Id is not null)
.Select(p => MapProduct(p)!)
.ToArray()
};
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Normalize RelatedProducts to an empty collection.

This path returns null when the document has no related products, which pushes a nullable collection into the public search response. Default it to [] the same way Parents does.

Proposed fix
 		RelatedProducts = item.Document.RelatedProducts?
 			.Where(p => p.Id is not null)
 			.Select(p => MapProduct(p)!)
-			.ToArray()
+			.ToArray() ?? []
 	};

As per coding guidelines "Collections should never return null; return an empty array/collection instead."

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
RelatedProducts = item.Document.RelatedProducts?
.Where(p => p.Id is not null)
.Select(p => MapProduct(p)!)
.ToArray()
};
RelatedProducts = item.Document.RelatedProducts?
.Where(p => p.Id is not null)
.Select(p => MapProduct(p)!)
.ToArray() ?? []
};
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/services/search/Elastic.Documentation.Search/FullSearchService.cs` around
lines 86 - 90, The RelatedProducts projection in FullSearchService (the block
that maps item.Document.RelatedProducts using MapProduct) can return null when
there are no related products; change it to normalize to an empty array like
Parents does — filter and map as before but ensure the final value is non-null
by returning Array.Empty<YourProductType>() (or use the equivalent empty array
construct) when the source is null or yields no items, keeping MapProduct and
the null-check on p.Id intact.

Comment on lines 31 to 62
_ = services.AddSingleton<ElasticsearchClientAccessor>();

// Navigation Search (autocomplete/navigation search)
_ = services.AddScoped<INavigationSearchService, NavigationSearchService>();
// ProductsConfiguration already implements IProductNameLookup; surface it via the
// shared interface so both the inner DefaultSearchService (for aggregation buckets) and
// the FullSearchService adapter (for per-hit display names) can resolve it from DI.
_ = services.AddScoped<IProductNameLookup>(sp => sp.GetRequiredService<ProductsConfiguration>());

// Inner search service: docs-builder pairs the typed contract with the docs index alias.
// docs-builder's SearchConfiguration (in Elastic.Documentation.Configuration.Search) is
// translated to the contract's shape — both carry synonyms+diminish-terms+ruleset, but
// the docs-builder type has richer QueryRule POCOs the shared service doesn't need.
_ = services.AddScoped<ISearchService<DocumentationDocument>>(sp =>
{
var acc = sp.GetRequiredService<ElasticsearchClientAccessor>();
var lookup = sp.GetRequiredService<IProductNameLookup>();
var innerLogger = sp.GetRequiredService<ILogger<DefaultSearchService<DocumentationDocument>>>();

var sharedConfig = new Internal.Search.Configuration.SearchConfiguration
{
SynonymBiDirectional = acc.SynonymBiDirectional,
DiminishTerms = acc.DiminishTerms.ToArray(),
RulesetName = acc.RulesetName,
SemanticEnabled = true
};

// FullSearch (full-page search with hybrid RRF)
return new DefaultSearchService<DocumentationDocument>(
acc.Client, acc.SearchIndex, sharedConfig, innerLogger, lookup);
});

// Docs-specific adapters preserve the existing API/MCP wire format.
_ = services.AddScoped<INavigationSearchService, NavigationSearchService>();
_ = services.AddScoped<IFullSearchService, FullSearchService>();
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Don't let scoped adapters tear down the singleton accessor.

ElasticsearchClientAccessor is registered as a singleton here, but both search adapters still call clientAccessor.Dispose() in their own Dispose() methods. The first scope that disposes either adapter will also dispose the shared accessor, and subsequent search requests will start failing.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/services/search/Elastic.Documentation.Search/ServicesExtension.cs` around
lines 31 - 62, ElasticsearchClientAccessor is registered as a singleton but
NavigationSearchService and FullSearchService (the scoped adapters) currently
call clientAccessor.Dispose() in their Dispose methods, which will tear down the
shared singleton; remove that disposal call (or add an explicit ownership flag
so only the component that created the accessor disposes it) from the
Dispose/DisposeAsync implementations in NavigationSearchService and
FullSearchService (and any other scoped types that call Dispose on
ElasticsearchClientAccessor), ensuring only the owner of
ElasticsearchClientAccessor disposes it.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants