Skip to content

Add API property search with direct links to matching fields - #4574

Merged
reakaleek merged 27 commits into
mainfrom
index-api-attributes-sear-a6c56a6b
Nov 3, 2026
Merged

reakaleek merged 27 commits into
mainfrom
index-api-attributes-sear-a6c56a6b

Conversation

@reakaleek

@reakaleek reakaleek commented Oct 24, 2026 •

Copy link
Copy Markdown
Member

API search finds parameter and schema field names and links directly to the matching row on an operation page.

Closes https://github.com/elastic/docs-eng-team/issues/967

Affects: Search, API reference, Site UI

Prompt summary: Make API fields discoverable through search while keeping operation and endpoint matches prominent. Show compact property rows with their JSON paths, clear page grouping, and yellow highlights that match the destination property.

Why

Readers who know a field name must find its operation and then locate the field manually. Operation text does not reliably identify nested schema fields or provide a link to their rendered anchors.

What

Searchable property metadata

Index rendered parameter, request, and response fields with their names, paths, types, descriptions, locations, and anchors. Indexing shares the page's property-tree construction so search links match rendered rows, including dictionaries and union variants, and two listings that differ only in their descriptions stay separate as they do on the page. Existing parameter text stays in the searchable operation body.

Operation and property matching

Match a field's own name or prefix in the API-filtered lookup, with qualified paths available for narrower searches. The docs-wide search tries only a word that looks like an identifier, containing _ or ., and matches a field by its exact name, so plain words such as index keep the pages about them on top. Ancestor paths and descriptions provide context without admitting unrelated child rows. Direct operation and endpoint matches retain priority, and combined searches such as /_search knn, knn /_search or knn search show the matching fields beneath their operations: a word with a slash, or an HTTP method word right before one, is the endpoint, and two plain words are tried in both orders with the endpoint required, so the index keeps the reading that holds. The endpoint half matches the path as typed or its underscore alias, and each reading is a named query, so a result carries only the field rows of the reading it matched.

Routes for a path hit

When a query word names a path, such as _search or /_search, the result shows one route in place of the description: the method the page leads with and the path with the match highlighted. The route whose whole path is the query word wins, so /_search shows /_search; otherwise the operation's primary route shows when it contains the word, and an alternate route only when the primary does not. A placeholder such as {index} never matches a query word. The index stores each operation's routes with their methods, taken from the same endpoint rows the operation page renders.

Compact results and direct navigation

Use a consistent bordered box for each result, with compact property rows beneath its operation header. Arrow keys traverse operations and properties together, and Enter opens the selected destination. The Enter badge on a row names where it leads: "Go to operation", "Go to property", or "Go to page" for a docs page, and the dropdown footer keeps a static "Go to page" hint. Field navigation reveals collapsed content and flashes the destination name using the same yellow tint as search matches. A click reports the result's rank as its telemetry position, with the property anchor as a separate attribute.

Verify

./build.sh unit-test
cd src/Elastic.Documentation.Site
npm run test
npm run compile:check
npm run lint

The full C# unit suite, 779 frontend tests, type checking, and lint pass. Local browser checks verify 32px property rows, 12px text, yellow matches, consistent boxes, and destination flashes.

Known limitation: A combined query of three or more plain words, with no slash and no HTTP method word, takes its last word as the field. Two plain words are tried in both orders with the endpoint required, so knn search and search knn read the same.

Risk: Indexing adds the nested api.properties mapping to existing semantic indices. A code revert leaves this additive mapping in place. Existing documents receive property metadata during the next indexing run.

Check on the preview

  • Elasticsearch search — search for search, _search, and /_search knn. Check operation priority, compact grouped fields, yellow matches, and keyboard navigation to a field. For _search, each result should show one route line, POST /_search on the search operation, in place of the description; search shows the primary POST /{index}/_search instead.
  • Nested property destination — check that navigation reveals the nested field and flashes its name.
  • Elasticsearch bulk — control page for existing operation rendering and the bulk operation lookup.

Property search checks require a search API and indexed metadata with this branch's property support.

Co-Authored-By: GPT-6 <noreply@anthropic.com>
@reakaleek
reakaleek requested a review from a team as a code owner October 24, 2026 00:53
@reakaleek
reakaleek requested a review from theletterf October 24, 2026 00:53
@github-actions

Copy link
Copy Markdown
Contributor

Docs preview (local build)

Handbook preview: https://docs-v3-preview.elastic.dev/elastic/docs-builder/pull/4574/

Sample changed pages:

akira28 and others added 4 commits October 27, 2026 10:45
Main replaced Jest with Vitest in
[#4588](#4588) while this
branch was open, so `tsc` no longer knows the `jest` global and the
`npm` CI job failed. The tests now use `vi.fn`, and the file-level
`htmx.org` mock goes away because `setupTests.ts` already mocks it for
every file. The property navigation test also looks for the "Explore
APIs" placeholder that
[#4565](#4565) introduced.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
reakaleek and others added 2 commits November 3, 2026 10:50
"Jump to API" no longer says where Enter leads once a result box holds
an operation and its matching fields. An operation row now says
"Go to operation", a property row says "Go to property", and a docs
page keeps "Jump to". The dropdown footer shows the label of the
selected row, so the hint changes as the arrow keys move between an
operation and its properties.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
A footer label that changes with the selected row draws the eye on
every arrow press. The footer now always says "Go to page", and a docs
page row uses the same words so one action has one name. Operation and
property rows keep "Go to operation" and "Go to property".

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The operation row insets its "Go to operation" badge 24px from the
box edge while the property row insets "Go to property" 16px, so the
two badges did not line up. The property row now uses the same
right padding as the operation row.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
These tests hold the contract steady for a refactor. The result
processor keeps the three best-scoring properties with distinct
anchors in score order. The navigation service links a property to
its anchor on the operation page. The document hash changes when a
property is added or its path changes, through a pure `DocumentHash`
split out of `AssignDocumentMetadata`. A property row shows its
location only when the same path appears in two locations, and the
mouse selects a property row unless the keyboard is driving.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Results are grouped once, in `groupSearchResults`, which numbers the
operation and its properties in keyboard order and decides where a
location chip is needed. The list only renders those groups, the
dropdown loses a one-caller wrapper, both row kinds share one click
telemetry path, and the Enter badge owns its own show-on-select rule.
The match tint lives in one `--search-highlight-tint` token that the
two result lists and the property flash all read. The anchor telemetry
attribute joins its siblings in `semconv.ts`, and the modal search
drops a `properties` field it never read.

On the server, the query builder parses the input once and reads the
nested field names from `QueryFieldNames`, the property walk is a
single pre-order `SelectMany`, and plain-text descriptions are cached
per spec so shared schemas are converted once per indexing run
instead of once per operation.

Verified against the same cluster: all 18 captured navigation-search
responses are byte-identical before and after, and the headless run
on localhost:4001 passes every functional check.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…sear-a6c56a6b

# Conflicts:
#	src/Elastic.Documentation.Indexing/Exporters/Elasticsearch/ElasticsearchMarkdownExporter.Export.cs
apiOperation,
new SchemaAnalyzer(openApiDocument),
new ParameterRowSource(builder, null, OperationEndpoint.Build(apiOperation, operation.Value.Description, []))
);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Severity: P3 (Low)

Pass the operation's supplemental documentation into property indexing instead of always supplying null. Rendering applies parameter and request-body description overrides, but these new search rows always retain the original spec descriptions, or remain blank when only the supplemental file documents the field. Editing an override also leaves the indexed property metadata unchanged, so search continues showing text that no longer matches the destination row.

Generated by Libra. React with 👍 or 👎 to give feedback on this comment.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

This is substantial enough to track separately. Created #4662 as a follow-up. Not blocking this PR.

A query such as `_search` matched an operation's path, but the result
only showed the match buried in the body snippet after "Method: POST
Path:". The index now stores each operation's routes with their
methods, taken from the same endpoint rows the operation page
renders, and the navigation search returns them. When a query word
names a path, the result replaces the description with the primary
route: the method the page leads with and the path with the match
highlighted. An alternate route takes its place only when the primary
one does not contain the word, so the hit is always explained without
listing every variant.

The autocomplete source filter now returns every required member of
the api object, since a hit that carries any part of it must
deserialize, and a test pins that rule. The mapping update sends the
whole api object so the existing semantic index gains the field.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Comment on lines +166 to +168
Routes = endpoint
.Rows
.Select(

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Severity: P2 (Medium)

Populate Routes from the merged page endpoint rather than the individual spec operation. This endpoint is built with an empty sibling list, but navigation can collapse interchangeable operations onto one page. For a GET/POST /_search and /{index}/_search family without a description-level route listing, each indexed hit therefore carries only its own route and method, while the destination page leads with POST /{index}/_search and lists both routes. Search can consequently show a different primary route or omit a matching alternate route entirely.

Generated by Libra. React with 👍 or 👎 to give feedback on this comment.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

This is substantial enough to track separately. Created #4663 as a follow-up. Not blocking this PR.

reakaleek and others added 2 commits November 3, 2026 12:28
Typing `/_search` showed `POST /{index}/_search`, the page's primary
route, which left the reader checking whether that was the route they
meant. A route whose whole path is a query word now wins, so
`/_search` and `_search` show `POST /_search`, while `search` or
`index` still show the primary route that contains them.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
A combined query took its last word as the field and the rest as the
endpoint, so `knn /_search` looked for a field named `/_search` and
returned the operation without its `knn` rows, while `knn search`
surfaced an unrelated operation whose fields contain "search". The
field is now the last word that cannot be part of an endpoint, one
that neither starts with a slash nor is an HTTP method, and the
endpoint is everything else in order. `knn /_search`, `/_search knn`
and `knn GET /_search` read the same. A query made only of endpoint
words skips the field branch instead of searching for a field named
after the path.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Without a slash or a method word nothing marks the endpoint in a
two-word query, so `knn search` took `search` as the field and
surfaced an unrelated operation. Such a query is now read both ways,
and each reading requires its endpoint half to match the stored
endpoint keys: `search` becomes `_search` and holds, `knn` becomes
`_knn` and matches nothing, so only the reading that holds returns
rows. `knn search`, `search knn`, `_search knn` and `knn _search`
now all return the search operation with its `knn` rows.

Each reading carries its own inner-hits name, because Elasticsearch
rejects two nested queries that share one, and the result processor
merges the groups.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
A property row highlighted only the last query word, a leftover of
the rule that read the last word as the field, so `knn search` marked
nothing on the `knn` rows that the query now returns. The row now
highlights every query word that can name a field, which excludes
paths and HTTP method words, so `knn search` and `search knn` both
mark `knn`.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
reakaleek and others added 7 commits November 3, 2026 13:36
A property row's click reported the keyboard row index, which counts
the property rows above it, so a page listed after an operation with
three fields was reported at position 4. The position is now the
result's rank among the results for the operation and for each of its
properties, and the anchor keeps identifying the property row.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
A query word was matched against the raw spec path, so `index` or `id`
matched `{index}` and `{id}` and a query such as `delete index` replaced
most descriptions with a route line even though it named no path. A
placeholder now stands as `*` when a path is compared, as the indexed
endpoint keys already do, so only a literal segment can match.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
A method word was never allowed to be the field, so `options` found no
property and `_bulk delete` looked for a field named `_bulk`, although
both are real schema fields. A method word now marks the endpoint only
when the next word starts with a slash, as in `GET /_search`, on both
the query builder and the result row's highlighting.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The nested field query also ran in the unfiltered docs search for any
single word, matching field names by prefix, so `s` or `index` lifted
hundreds of API operations above the pages about them and inflated the
type counts. The docs-wide search now tries only words that look like
identifiers, containing `_` or `.`, and matches a field by its exact
name; the prefix clause stays in the API-filtered lookup where the
bare-word endpoint boost already lives for the same reason.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The endpoint half of a combined query tried the underscore alias first
and, since `_deployments` always builds a query, never tried the
literal `deployments`. With the endpoint required in a two-word query,
`deployments status` could not reach the fields of `/deployments`. The
endpoint half now accepts the path as typed or its alias.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Elasticsearch computes a nested query's inner hits for every returned
document from that nested query alone, so a two-word query's rejected
reading still attached its field rows: for `knn search` the reading
that paired field `search` with endpoint `_knn` returned `search_*`
rows on the search operation, which could take two of the three slots.
Each reading is now a named query carrying its inner-hits name, and
the processor keeps a group only when the hit matched that reading.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The exporter rendered every property description as an empty string,
so two listings that differ only in their variant descriptions hashed
alike and the second was folded into the first. The page renders the
descriptions and keeps both, so its anchors had no index entries.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
reakaleek and others added 3 commits November 3, 2026 14:12
The guard looked at the whole query, so `elasticsearch index_name
settings` passed it on the underscore and then tried the plain word
`settings` as the field, lifting every operation with a `settings`
field into the docs-wide results. The check now applies to the field
word of each reading, which is what the docs describe.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Elasticsearch leaves matched_queries out of a hit when no named query
matched it, so a hit the lexical branch alone matched looked the same
as a hit from a query that named nothing, and kept the inner hits of
every reading. Every reading is now named, so the processor can
require the name and drop the rows of a reading the hit did not match.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The page and the index build the same property tree with different
markdown renderers, and listing identity hashed the rendered HTML, so
the two could disagree on which listings to share: `*Code it*` and
`_Code it_` are one listing on the page and two in the index, leaving
index entries whose anchors the page never renders. Identity now keys
on the markdown both builders have, and the index renders nothing.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…dings

Whether a reading must match its endpoint was taken from how many
readings were left, so once the docs-wide filter dropped the plain
reading of `bulk track_total_hits`, the one that remained had its
endpoint demoted to a lift and admitted every operation with the
field. The parse now records that two plain words require their
endpoints, and the filter keeps that.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@reakaleek
reakaleek merged commit 67cd731 into main Nov 3, 2026
36 checks passed
@reakaleek
reakaleek deleted the index-api-attributes-sear-a6c56a6b branch November 3, 2026 18:26
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