feat: include tap attributes in cmd-K full-text search
All checks were successful
CI / cleanup-branch (push) Has been skipped
CI / build (push) Successful in 1m4s
CI / docker (push) Successful in 1m13s
CI / deploy-feature (push) Has been skipped
CI / deploy (push) Successful in 36s

Add attributes_text flattened field to OpenSearch indexing for both
execution and processor levels. Include in full-text search queries,
wildcard matching, and highlighting. Merge processor-level attributes
into ExecutionSummary. Add 'attribute' category to CommandPalette
(design-system 0.1.17) with per-key-value results in the search UI.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
hsiegeln
2026-03-27 08:13:58 +01:00
parent ea88042ef5
commit 62709ce80b
4 changed files with 67 additions and 15 deletions

View File

@@ -130,8 +130,10 @@ public class OpenSearchIndex implements SearchIndex {
}
private static final List<String> HIGHLIGHT_FIELDS = List.of(
"error_message", "processors.input_body", "processors.output_body",
"processors.input_headers", "processors.output_headers");
"error_message", "attributes_text",
"processors.input_body", "processors.output_body",
"processors.input_headers", "processors.output_headers",
"processors.attributes_text");
private org.opensearch.client.opensearch.core.SearchRequest buildSearchRequest(
SearchRequest request, int size) {
@@ -197,11 +199,13 @@ public class OpenSearchIndex implements SearchIndex {
// Search top-level text fields (analyzed match + wildcard for substring)
textQueries.add(Query.of(q -> q.multiMatch(m -> m
.query(text)
.fields("error_message", "error_stacktrace"))));
.fields("error_message", "error_stacktrace", "attributes_text"))));
textQueries.add(Query.of(q -> q.wildcard(w -> w
.field("error_message").value(wildcard).caseInsensitive(true))));
textQueries.add(Query.of(q -> q.wildcard(w -> w
.field("error_stacktrace").value(wildcard).caseInsensitive(true))));
textQueries.add(Query.of(q -> q.wildcard(w -> w
.field("attributes_text").value(wildcard).caseInsensitive(true))));
// Search nested processor fields (analyzed match + wildcard)
textQueries.add(Query.of(q -> q.nested(n -> n
@@ -210,14 +214,16 @@ public class OpenSearchIndex implements SearchIndex {
.query(text)
.fields("processors.input_body", "processors.output_body",
"processors.input_headers", "processors.output_headers",
"processors.error_message", "processors.error_stacktrace"))))));
"processors.error_message", "processors.error_stacktrace",
"processors.attributes_text"))))));
textQueries.add(Query.of(q -> q.nested(n -> n
.path("processors")
.query(nq -> nq.bool(nb -> nb.should(
wildcardQuery("processors.input_body", wildcard),
wildcardQuery("processors.output_body", wildcard),
wildcardQuery("processors.input_headers", wildcard),
wildcardQuery("processors.output_headers", wildcard)
wildcardQuery("processors.output_headers", wildcard),
wildcardQuery("processors.attributes_text", wildcard)
).minimumShouldMatch("1"))))));
// Also try keyword fields for exact matches
@@ -319,7 +325,9 @@ public class OpenSearchIndex implements SearchIndex {
map.put("error_message", doc.errorMessage());
map.put("error_stacktrace", doc.errorStacktrace());
if (doc.attributes() != null) {
map.put("attributes", parseAttributesJson(doc.attributes()));
Map<String, String> attrs = parseAttributesJson(doc.attributes());
map.put("attributes", attrs);
map.put("attributes_text", flattenAttributes(attrs));
}
if (doc.processors() != null) {
map.put("processors", doc.processors().stream().map(p -> {
@@ -334,7 +342,9 @@ public class OpenSearchIndex implements SearchIndex {
pm.put("input_headers", p.inputHeaders());
pm.put("output_headers", p.outputHeaders());
if (p.attributes() != null) {
pm.put("attributes", parseAttributesJson(p.attributes()));
Map<String, String> pAttrs = parseAttributesJson(p.attributes());
pm.put("attributes", pAttrs);
pm.put("attributes_text", flattenAttributes(pAttrs));
}
return pm;
}).toList());
@@ -348,7 +358,20 @@ public class OpenSearchIndex implements SearchIndex {
if (src == null) return null;
@SuppressWarnings("unchecked")
Map<String, String> attributes = src.get("attributes") instanceof Map
? (Map<String, String>) src.get("attributes") : null;
? new LinkedHashMap<>((Map<String, String>) src.get("attributes")) : null;
// Merge processor-level attributes (execution-level takes precedence)
if (src.get("processors") instanceof List<?> procs) {
for (Object pObj : procs) {
if (pObj instanceof Map<?, ?> pm && pm.get("attributes") instanceof Map<?, ?> pa) {
if (attributes == null) attributes = new LinkedHashMap<>();
for (var entry : pa.entrySet()) {
attributes.putIfAbsent(
String.valueOf(entry.getKey()),
String.valueOf(entry.getValue()));
}
}
}
}
return new ExecutionSummary(
(String) src.get("execution_id"),
(String) src.get("route_id"),
@@ -384,4 +407,11 @@ public class OpenSearchIndex implements SearchIndex {
return null;
}
}
private static String flattenAttributes(Map<String, String> attrs) {
if (attrs == null || attrs.isEmpty()) return "";
return attrs.entrySet().stream()
.map(e -> e.getKey() + "=" + e.getValue())
.collect(Collectors.joining(" "));
}
}