Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 41 additions & 2 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -277,8 +277,47 @@ what a reader wants is a sample, in another repository, on another deployment.
(`/docs/search-index.json`, built by `generate-search.mjs`): every page of the
manual with its headings and its words, and all ~770 entries of the three
sample catalogues with the titles, summaries and keywords those repositories
maintain. Results are grouped by area, documentation first, and a sample hit
opens that sample's own page in the catalogue.
maintain. Results are grouped by area, and a sample hit opens that sample's
own page in the catalogue. The groups come in the order of their best hit;
the documentation leads whenever it has a real answer (a hit within half of
the best hit anywhere), and a page that only MENTIONS the word comes after
the samples that are about it — "dialog" used to open on Value Help, Popup,
PDF and Lock above forty-seven samples that are dialogs.

**What the matcher does with the words, and why.** Each rule is a query that
was measured wrong on the real index before it, and each is pinned in
`test/search.test.mjs`:

- **Stop words are not search terms** while a real word is left. "how do i
install" was answered with Client API › check_on_init, and Installation
was not in the first four: `a` and `i` are a prefix of something in every
entry and each earned a title-hit's worth of points. A query that is
nothing but stop words still asks what it says.
- **The words next to each other earn a bonus.** "abap cloud" preferred
Toolchain › abap-cleaner (a title hit on `abap`, a mention of `cloud`)
over the heading that IS the two words typed.
- **A field with its spaces taken out is matched too**, at the weight of the
field, for terms of five letters and up: "messagebox" found the three
samples spelled that way and not the chapter whose heading is "Message
Box", nor "selectdialog" a single page. The index carries no compound
forms for this; the matcher looks (`joined( )`).
- **The plural is the singular with an `s` on it**, taken off before the
prefix match — "tables" found 33 entries and "table" 202, and all 169 it
missed were about tables. Letters only, never `ss`, never a class name.
- **Nothing found is not the end**: the word on the fewest entries is set
aside, then the next, until something answers; a single word that still
finds nothing is tried against every title word one edit away ("tabel" is
"table"; and the candidate that starts the way the reader started wins
over the one on more titles, or "tabel" would be "label"). Whatever
answered is on the result as `hits.relaxedTo` and both boxes say so above
the rows: a list that silently answers a different question than the one
typed is worse than an empty one.
- **Synonyms are data in the index, not code in the matcher**: `SYNONYMS` in
`scripts/lib/search-index.mjs` is `[what is typed, what the entry says]`
pairs ("readonly" for `editable`, "f4" for the value help), added to an
entry's `terms` — the long tail's weight, so the alias finds the entry and
the ranking stays with the words it really carries. One direction each,
so it is the reader's word that is mapped, not the project's.

**The box states the type it would otherwise inherit.** Every rule inside the
panel is identical to `.search-panel`'s in `catalogue.css` — and the two still
Expand Down
9 changes: 8 additions & 1 deletion docs/.vitepress/theme/SearchBox.vue
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,10 @@ function hide() {
* them is not measurable. */
const hits = computed(() => (entries.value.length ? search(entries.value, query.value, { limit: 500 }) : []));
const groups = computed(() => grouped(hits.value));
/* What answered, when it was not what was typed: a typo corrected or a word
* set aside (search-engine.js). The list says so above the results, and the
* marks in the rows are of the query that answered, not the one that did not. */
const relaxedTo = computed(() => hits.value.relaxedTo || '');

/* What is in the box, in the two numbers a reader recognises. Only once the
* index has arrived - before that the box says what it is, not how much. */
Expand Down Expand Up @@ -185,7 +189,7 @@ onMounted(() => document.addEventListener('keydown', onKey));
onUnmounted(() => document.removeEventListener('keydown', onKey));

const indexOf = (hit) => rows.value.indexOf(hit);
const parts = (text) => highlight(text, query.value);
const parts = (text) => highlight(text, relaxedTo.value || query.value);
</script>

<template>
Expand Down Expand Up @@ -252,6 +256,9 @@ const parts = (text) => highlight(text, query.value);
<p v-else-if="!rows.length" class="a2ui5-search-note">
Nothing matches <strong>{{ query }}</strong>.
</p>
<p v-else-if="relaxedTo" class="a2ui5-search-note">
Nothing matches <strong>{{ query }}</strong> — showing <strong>{{ relaxedTo }}</strong>.
</p>

<div v-for="group in groups" :key="group.label" class="a2ui5-search-group">
<div class="a2ui5-search-group-head">
Expand Down
Loading