Problem
Several distinct UX gaps in tree search compound to make the feature feel inconsistent and partially broken. Filing as one issue because they share the same surface (EntityTreeToolbar + useTreeSearch) and the right design treats them together.
1. Expand / Collapse buttons stay visible (and active) during search
components/editor/shared/EntityTreeToolbar.tsx:102-150 renders the Expand and Collapse split-button groups regardless of showSearch. Once the user opens search and the tree switches to filtered-results mode (via useFilteredTree), Expand/Collapse no longer have any meaningful effect — the rendered tree is the filtered tree, not the live class hierarchy. The buttons just sit there occupying space and inviting confusion.
2. Search input renders below the buttons
The search input drops in below the toolbar row when active, leaving the now-meaningless Expand/Collapse buttons above it. The visual hierarchy reads as "primary controls on top, search secondary" — the opposite of reality once search is engaged.
3. The search behavior is opaque — same backend, very different results depending on query length
Reproducible behavior on the demo dataset:
| Query |
Result |
son of god |
Jesus (individual) is returned |
son of |
non-exact class matches plus Jesus (individual) |
son |
only class results — Jesus is gone |
son (trailing space) |
only class results |
The query passes straight to projectOntologyApi.searchEntities (useTreeSearch.ts:74-79) with no entity_types filter, so the backend is what's making the call. Whether it's switching ranking modes by query length, applying a relevance threshold, or splitting tokens differently, the user has no way to see what's happening — and the inconsistency is a real bug from the user's perspective.
4. Multi-field matching is unclear
The user expects to be able to search across rdfs:label, skos:prefLabel, skos:altLabel, rdfs:comment, and skos:definition — not just the one label that happens to be displayed as the tree-row's text. When a hit is via a non-displayed field, the user has no way to tell why a node was returned.
Proposal
Treat this as a single small redesign of the search mode rather than four separate tweaks.
Replace Expand/Collapse with a search-mode group while search is active
[ Add ] [ Search input.................. ] [ Exact | Partial | FTS | Semantic ] [ X ]
When showSearch is true:
- Hide the Expand and Collapse split-button groups entirely.
- Render the search input inline (full width of the toolbar minus the mode group and close button), instead of dropping it to a row below.
- Render a small grouped control (
role="group", similar shape to the existing ModeSwitcher) with Exact | Partial | FTS | Semantic segments. The active segment styles like the active mode in ModeSwitcher / the new ViewerEditorSwitcher.
When showSearch is false:
- Render the existing Expand/Collapse groups exactly as today.
This gives Expand/Collapse and the search-mode group "alternate uses for the same toolbar slot," which is the right metaphor — they're both about how to interpret the tree below.
Make search mode explicit and stable
Define four modes in a typed enum that maps to existing or new backend behavior:
| Mode |
Meaning |
Backend translation |
exact |
label equals query (case-insensitive, trimmed) |
entity_types=any&q=...&match=exact |
partial |
label contains query (substring) |
today's default — q=... |
fts |
full-text search across label + altLabel + comment + definition |
q=...&fields=label,altLabel,comment,definition (or backend FTS index if available) |
semantic |
embedding-based similarity (already wired in useSemanticSearch.ts) |
useSemanticSearch |
The default on first open is Partial (today's behavior). The selected mode persists per session in the same store as the editor mode (useEditorModeStore-style) so users don't have to re-pick it on every search session.
The token-length / query-shape inconsistency reported above goes away because the user explicitly picks the mode rather than the backend silently switching strategies.
Show why a node matched when the match isn't on the displayed label
When a result comes back and the displayed rdfs:label doesn't contain the query (because the match was on altLabel, definition, etc.), render the matching field's value as a small caption beneath the row, with the matching substring highlighted:
Document Collection Event
↳ also: alt — "doc collection ev." [matched on altLabel]
The backend would need to return matchedField + matchedValue per result (already easy if the search endpoint switches to FTS). The frontend just renders a single subdued line. When the match IS on rdfs:label, no caption is rendered — same as today.
This piece is independent of the toolbar redesign and could ship in a second PR if the backend work isn't ready in time.
Mixed entity-type results: opt-in via the existing entity tabs
The Classes tree should only return classes; the Properties tree only properties; the Individuals list only individuals. Today the call doesn't pass entity_types, so backends apply their own defaults — which explains why Jesus (individual) sometimes shows up in class-tree search results and sometimes doesn't.
Pass entity_types="class" from useTreeSearch when scoped to the Classes tree, entity_types="property" from PropertyTree's search, etc. Cross-tab results live in a separate global search affordance (out of scope for this issue).
Plan
1. Toolbar slot reuse
Refactor EntityTreeToolbar.tsx so the inner toolbar row branches on showSearch:
{showSearch ? (
<SearchToolbar
query={searchQuery}
onChange={setSearchQuery}
mode={searchMode}
onModeChange={setSearchMode}
onClose={onCloseSearch}
/>
) : (
<>
{/* existing Add / Expand / Collapse / Search-toggle */}
</>
)}
The new SearchToolbar is a small component with <input> + <SearchModeSwitcher> + close button. Same height as today's row so the layout doesn't shift.
2. Search-mode plumbing
- Add
SearchMode enum in lib/ontology/searchMode.ts.
- Extend
useTreeSearch with mode: SearchMode state and pass it (translated to backend args) to projectOntologyApi.searchEntities.
- Persist the chosen mode in a small Zustand store (
useSearchPreferenceStore) so it sticks per-session.
3. Per-tree entity-type scoping
Each caller of useTreeSearch (ClassTree, PropertyTree, IndividualList) passes its own entityType. Cross-tab search remains a separate concern.
4. Highlighting non-label matches
Two parts (can ship independently):
- Backend:
searchEntities returns matchedField: "label" | "altLabel" | "comment" | "definition" and matchedValue: string per result.
- Frontend: in
EntityTreeNodeRow, render an aria-describedby'd caption when matchedField is not "label". Styling consistent with the existing tree-search-match class.
Done criteria
Out of scope
- A global cross-tree search UI ("search everything"). Today's per-tree scope is correct; widening it is a separate feature.
- Saved searches / search history.
- Scoring & ranking improvements within each mode (e.g. boosting prefLabel matches over altLabel matches in Partial mode). The mode toggle gives the user the right knob; refining each mode's internal ranking is its own ticket.
- Replacing the native search input with a Combobox that shows suggestions while typing. The existing one-shot search-then-render-tree flow is fine for ontology-scale searches; suggestion-style autocomplete is a separate UX direction.
Related
Other v0.5.0 polish items in the editor chrome: #204, #205, #206, #207, #208, #209. This one is the largest of that set and probably warrants its own PR; #209 (search highlight on intermediate ancestors) is closely related and could land in the same change.
Problem
Several distinct UX gaps in tree search compound to make the feature feel inconsistent and partially broken. Filing as one issue because they share the same surface (
EntityTreeToolbar+useTreeSearch) and the right design treats them together.1. Expand / Collapse buttons stay visible (and active) during search
components/editor/shared/EntityTreeToolbar.tsx:102-150renders the Expand and Collapse split-button groups regardless ofshowSearch. Once the user opens search and the tree switches to filtered-results mode (viauseFilteredTree), Expand/Collapse no longer have any meaningful effect — the rendered tree is the filtered tree, not the live class hierarchy. The buttons just sit there occupying space and inviting confusion.2. Search input renders below the buttons
The search input drops in below the toolbar row when active, leaving the now-meaningless Expand/Collapse buttons above it. The visual hierarchy reads as "primary controls on top, search secondary" — the opposite of reality once search is engaged.
3. The search behavior is opaque — same backend, very different results depending on query length
Reproducible behavior on the demo dataset:
son of godson ofsonson(trailing space)The query passes straight to
projectOntologyApi.searchEntities(useTreeSearch.ts:74-79) with noentity_typesfilter, so the backend is what's making the call. Whether it's switching ranking modes by query length, applying a relevance threshold, or splitting tokens differently, the user has no way to see what's happening — and the inconsistency is a real bug from the user's perspective.4. Multi-field matching is unclear
The user expects to be able to search across
rdfs:label,skos:prefLabel,skos:altLabel,rdfs:comment, andskos:definition— not just the one label that happens to be displayed as the tree-row's text. When a hit is via a non-displayed field, the user has no way to tell why a node was returned.Proposal
Treat this as a single small redesign of the search mode rather than four separate tweaks.
Replace Expand/Collapse with a search-mode group while search is active
When
showSearchis true:role="group", similar shape to the existingModeSwitcher) with Exact | Partial | FTS | Semantic segments. The active segment styles like the active mode inModeSwitcher/ the newViewerEditorSwitcher.When
showSearchis false:This gives Expand/Collapse and the search-mode group "alternate uses for the same toolbar slot," which is the right metaphor — they're both about how to interpret the tree below.
Make search mode explicit and stable
Define four modes in a typed enum that maps to existing or new backend behavior:
exactentity_types=any&q=...&match=exactpartialq=...ftsq=...&fields=label,altLabel,comment,definition(or backend FTS index if available)semanticuseSemanticSearch.ts)useSemanticSearchThe default on first open is Partial (today's behavior). The selected mode persists per session in the same store as the editor mode (
useEditorModeStore-style) so users don't have to re-pick it on every search session.The token-length / query-shape inconsistency reported above goes away because the user explicitly picks the mode rather than the backend silently switching strategies.
Show why a node matched when the match isn't on the displayed label
When a result comes back and the displayed
rdfs:labeldoesn't contain the query (because the match was onaltLabel,definition, etc.), render the matching field's value as a small caption beneath the row, with the matching substring highlighted:The backend would need to return
matchedField+matchedValueper result (already easy if the search endpoint switches to FTS). The frontend just renders a single subdued line. When the match IS onrdfs:label, no caption is rendered — same as today.This piece is independent of the toolbar redesign and could ship in a second PR if the backend work isn't ready in time.
Mixed entity-type results: opt-in via the existing entity tabs
The Classes tree should only return classes; the Properties tree only properties; the Individuals list only individuals. Today the call doesn't pass
entity_types, so backends apply their own defaults — which explains why Jesus (individual) sometimes shows up in class-tree search results and sometimes doesn't.Pass
entity_types="class"fromuseTreeSearchwhen scoped to the Classes tree,entity_types="property"from PropertyTree's search, etc. Cross-tab results live in a separate global search affordance (out of scope for this issue).Plan
1. Toolbar slot reuse
Refactor
EntityTreeToolbar.tsxso the inner toolbar row branches onshowSearch:The new
SearchToolbaris a small component with<input>+<SearchModeSwitcher>+ close button. Same height as today's row so the layout doesn't shift.2. Search-mode plumbing
SearchModeenum inlib/ontology/searchMode.ts.useTreeSearchwithmode: SearchModestate and pass it (translated to backend args) toprojectOntologyApi.searchEntities.useSearchPreferenceStore) so it sticks per-session.3. Per-tree entity-type scoping
Each caller of
useTreeSearch(ClassTree, PropertyTree, IndividualList) passes its ownentityType. Cross-tab search remains a separate concern.4. Highlighting non-label matches
Two parts (can ship independently):
searchEntitiesreturnsmatchedField: "label" | "altLabel" | "comment" | "definition"andmatchedValue: stringper result.EntityTreeNodeRow, render anaria-describedby'd caption whenmatchedFieldis not"label". Styling consistent with the existingtree-search-matchclass.Done criteria
sonandson of godin Partial mode produces results consistent with each other (no silent "if your query has 3+ tokens we widen the search" backend tricks).altLabel/comment/definition, the row shows a small "matched on <field>" caption with the matching value highlighted.Ctrl+Kstill toggles search.Out of scope
Related
Other v0.5.0 polish items in the editor chrome: #204, #205, #206, #207, #208, #209. This one is the largest of that set and probably warrants its own PR; #209 (search highlight on intermediate ancestors) is closely related and could land in the same change.