Skip to content

Class tree search highlights only leaf-level matches; intermediate ancestors with matching labels are missed #209

Description

@JohnRDOrazio

Problem

Searching the Class tree highlights only the leaf-level matches that the backend's search endpoint returned. If an ancestor also contains the query string in its label, that ancestor is not highlighted — even though it's just as much a valid match from the user's perspective.

Example reported by a user searching for document:

> Event
  > Conflict
    > Unarmed Conflict
      > Legal Dispute Events
        > Litigation Events
          > Discovery Events
            > Document Collection and Production Events    ← contains "Document", NOT highlighted
              * Document Collection Event                  ← contains "Document", highlighted

Both nodes contain "document" in their label, but only the leaf is rendered with the <mark> highlight + tree-search-match class. The user has to mentally re-scan every ancestor row to find any other matches — the whole point of search highlighting is to avoid that.

Where the bug is

lib/hooks/useFilteredTree.ts, function mergePathsIntoTree at line 125-164.

for (const path of paths) {
  matchIris.add(path.matchIri);  // only the leaf "matchIri" goes into matchIris
  ...
  for (let i = 0; i < fullPath.length; i++) {
    const item = fullPath[i];
    if (!nodeMap.has(item.iri)) {
      nodeMap.set(item.iri, {
        ...
        isSearchMatch: matchIris.has(item.iri),  // only true for leaves
      });
    } else {
      const existing = nodeMap.get(item.iri)!;
      ...
      if (matchIris.has(item.iri)) {
        existing.isSearchMatch = true;
      }
    }
    ...
  }
}

matchIris is populated only from path.matchIri (the deepest match for each search-result path). The ancestor IRIs in fullPath[0 .. n-1] are added to the tree as expanded context but never checked against the search query — even when their labels would match.

The downstream rendering in EntityTreeNode.tsx:233-235 correctly defers to node.isSearchMatch, so the fix is upstream in the merge.

Proposed fix

Pass the active search query (or its lowercase form) into mergePathsIntoTree, and mark isSearchMatch: true for any node whose label contains the query — not just the leaf-level matches the backend returned.

export function mergePathsIntoTree(
  paths: AncestorPath[],
  query: string,        // ← new param, e.g. "document"
): EntityTreeNode[] {
  const q = query.trim().toLowerCase();
  ...
  // When inserting / updating each node:
  const labelMatches = q.length > 0 && item.label.toLowerCase().includes(q);
  isSearchMatch: matchIris.has(item.iri) || labelMatches,
  ...
}

The caller (useFilteredTree) already has the query in scope via searchResults — though it's not exposed by name there. Either:

  • (A) Plumb the query down from useTreeSearch.searchQuery through to useFilteredTree, or
  • (B) Derive the query from searchResults (each EntitySearchResult was returned by a query that the hook can hold onto when it triggered the search). Cleaner to just pass it in explicitly.

I'd go with (A): add searchQuery: string | null to UseFilteredTreeOptions, pass it to mergePathsIntoTree, and let the merge highlight any label containing it.

Done criteria

  • Searching for document in a tree where both Document Collection Event (leaf) and Document Collection and Production Events (ancestor) exist highlights both nodes' labels.
  • Searching for a query that matches only a leaf still works as today (ancestors aren't falsely highlighted).
  • Searching for a query that matches a non-result ancestor (e.g., the backend search didn't return it because it ranked lower) still highlights it if it's in the rendered tree.
  • Case-insensitive matching is preserved (consistent with highlightMatch in EntityTreeNode.tsx:34-45).
  • Existing test suite for useFilteredTree (if any) is extended; new test asserts a multi-level match path highlights every matching ancestor.

Out of scope

  • Expanding the backend's searchEntities endpoint to return all transitive matches. The frontend fix is more robust because it stays in sync with whatever ranking / limit the backend applies, and works retroactively without an API change.
  • Highlighting matches in node labels that aren't in the rendered tree (e.g., siblings of an ancestor that didn't make the search results). Those nodes aren't visible anyway; nothing to highlight.
  • Fuzzy / regex / multi-token search semantics. The bug is just about case-insensitive substring containment, which is the existing behavior in highlightMatch.

Related

Other v0.5.0 polish items: #204, #205, #206, #207, #208. This one is purely in useFilteredTree and a small caller plumb-through; could ship in any of the chrome-polish PRs or its own.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions