Visual query builder
A point-and-click UI for constructing Arkime search expressions without hand-writing expression syntax — field / operator / value rows, AND/OR/NOT grouping, parentheses — with lossless round-trip to/from the existing text expression bar.
Part of the Arkime 7 UI epic (#3926). Large. Scoping below is settled; remaining open questions are small and listed at the end.
Decisions (settled)
| Area | Decision |
|---|---|
| Round-trip | Full bidirectional — parse any valid expression into an editable tree and regenerate. Tree is the source of truth while the builder is open. |
| Surface | Popover anchored under the search bar (page + bar stay visible). |
| Grammar coverage | Full grammar — every construct gets first-class UI (see checklist). |
| Autocomplete | Full reuse — each row gets field typeahead + type-aware operator list + live value autocomplete (api/unique, country codes, EXISTS!, $shortcuts), same as the bar. |
| Parser strategy | One shared grammar that emits an AST. Backend derives the ES query from the AST (refactoring today's direct text→ES path); frontend uses the same AST for the builder + an AST→text serializer. Single source of truth. |
| Round-trip fidelity | Semantic, formatting normalized — meaning is preserved; the builder may tidy syntax it rewrites (=→==, &→&&, spacing, redundant parens). Untouched-but-reserialized text may be normalized. |
| Sync model | Builder→bar on Apply. Open = parse bar into builder once; edit in builder only; write back to bar + run search on Apply. No live reparse of bar keystrokes while open. |
| v1 reach | Viewer Sessions search bar only. Views editor, cron/periodic queries, hunt, SPIView/SPIGraph keep the plain bar for now (shared component lands later). |
Architecture — the AST becomes the center
Today: expression text → arkimeparser.js (jison) → ES query, server-side only. There is no representation that is editable or that can be turned back into text.
Target:
┌──────────────────────────────┐
grammar ─────▶ AST │ (one source of truth)
(one .jison) └──────────────────────────────┘
│ │ │
AST→ES ────┘ AST→UI ──┘ AST→text ┘
(backend) (builder rows) (serializer)Three consumers of the AST:
AST → ES query(backend refactor). Replace the inline jison semantic actions that build ES directly. The leaf builders (formatQuery,formatExists,parseIpPort,stringQuery,termOrTermsInt, …) already exist as standalone functions and are reused verbatim. Only the four combinators move into an AST walker:&&→bool.filter,||→bool.should,!→bool.must_not,()→passthrough.parser.yycontext (fieldsMap, views, shortcuts, prefix, requiredRight, emailSearch) is threaded into the walker unchanged.AST → builder UI. Render the tree as nested group/rule rows in the popover.AST → textserializer (new). Pretty-print the tree back to expression text with normalized formatting; re-insert parentheses wherever child precedence requires it to preserve meaning.
Proposed AST node model
Logical { type: 'and' | 'or', children: Node[] }
Not { type: 'not', child: Node }
Term { type: 'term', field: string, op: 'eq'|'ne'|'lt'|'lte'|'gt'|'gte', value: Value }
Value (tagged so the row UI can pick the right editor):
{ kind: 'scalar', text } // bare / quoted string, wildcard *
{ kind: 'regex', pattern } // /.../
{ kind: 'list', mode: 'or'|'and', items: Value[] } // [a,b] vs ]a,b[
{ kind: 'exists' } // EXISTS!
{ kind: 'shortcut', name } // $name
{ kind: 'ip', text } // CIDR / :port shorthand (ip builder)
{ kind: 'range', text } // integer/float range 1-5 (range UI)
{ kind: 'view', name } // view embed (viewand)(Group nesting is explicit in the tree via and/or children; the serializer adds parens from precedence. The db: field prefix is carried on Term.field.)
Grammar coverage checklist (full grammar)
Every construct below must parse → edit → serialize and survive text→AST→ES byte-for-byte against the golden tests:
- Comparison ops:
== != < <= > >=(and lexer aliases=, normalized to==) - Boolean:
&& || !(and aliases& |), correct precedence/associativity - Parentheses / nested groups
- Quoted strings, escaped quotes
- Regex values
/.../(with the back-slash-forward-slash rule) - Wildcards
*inside string values - OR lists
[a,b,c]and AND lists]a,b,c[ -
EXISTS! - Shortcuts
$name(incl. inside lists, with*/?glob expansion) - IP field shorthand: CIDR,
/8 /16 /24short forms,:port(v4) /.port(v6),::ipv6, arrays of these - Integer/float ranges
1-5 - View embedding (
viewand—view == name) -
db:field prefix - Negative-number production (
- e)
Backend refactor — blast radius & safety net
Touches the proven query path — handle with care.
Consumers of arkimeparser.parse() that must keep producing identical ES:
viewer/buildQuery.js— requestexpression, view expansion (buildQuery.js:129), user forced-expression +emailSearch.viewer/apiCrons.js— periodic/cron query expression + user expression.
Safety net (must stay green, unchanged):
tests/api-buildquery.t(~292 lines of exactesquerysnapshot assertions)tests/api-views.t,tests/api-cron.t,tests/api-spiview.t
Plan: land the text→AST→ES refactor first, prove byte-identical output via the snapshot suite, then build the frontend on top. The grammar is jison-generated (arkimeparser.js from arkimeparser.jison, jison 0.4.18) — regeneration step must be documented/wired so the checked-in artifact can't drift from the grammar source.
Frontend components (new)
QueryBuilderPopover.vue— anchored dropdown off the search bar (new⊞button next to the existing expand/save/clear buttons inExpressionTypeahead.vue). Owns open state; parses bar text → AST on open; serializes AST → bar + emitsapplyExpressionon Apply.QueryBuilderGroup.vue— recursive: renders a group'sand/ortoggle, its child rows/sub-groups,+ rule/+ group, and group delete/negate. Self-references for nesting.QueryBuilderRow.vue— oneterm: field typeahead → type-aware operator select → value editor.ValueEditor.vue— switches editor byValue.kind: scalar typeahead (liveapi/unique), list chips, regex toggle,EXISTS!toggle, ip builder, range inputs,$shortcutpicker, view picker.- Autocomplete extraction — pull the field/op/value resolution + value-fetch logic out of
ExpressionTypeahead.vueinto a reusable composable/service so both the bar and every builder row share it (no duplicated typeahead logic).FieldServicealready centralizes field metadata + value fetch.
Reuse: BigExpressionModal placement pattern, TypeaheadResults dropdown, teleport-positioning approach, arkime-input-group styling.
UX
- Launch: new icon button in the search-bar input group (alongside expand / save / clear).
- Popover shows the parsed tree as nested rule rows; page and bar remain visible behind it.
- Footer:
Apply(write text → bar + search),Cancel(discard),Clear. - If the bar text fails to parse on open: don't silently drop it — show a non-blocking notice ("expression too complex / invalid to edit visually") and offer to start fresh; leave the text bar untouched. (Decide exact treatment — see open questions.)
Phasing
- Backend AST refactor — one grammar → AST, AST→ES walker, golden snapshot suite green & byte-identical. (No UI yet.)
AST→textserializer + atext→AST→textround-trip test harness over theapi-buildquery.tcorpus (normalization-aware).- Builder shell — popover, recursive group/row rendering, AST→UI binding, Apply path, parse-on-open.
- Value editors — wire each
Value.kindeditor + full autocomplete reuse. - Polish — invalid-expression handling, keyboard nav, a11y, i18n strings, UI review.
Out of scope for v1
- Builder in views editor / cron / hunt / SPIView / SPIGraph (shared-component rollout = follow-up).
- Live two-way sync while the popover is open (Apply-only for v1).
- Byte-exact preservation of untouched formatting (we normalize).
- Drag-to-reorder / live result-count preview pane (dedicated-editor ideas, not this surface).
Remaining open questions (small)
- Exact treatment when the bar text won't parse on open: hard-disable the builder button vs. open-with-notice vs. best-effort partial parse.
- Serializer paren policy: minimal parens (precedence-driven only) vs. always-parenthesize groups for readability.
- Where the shared autocomplete logic should live (
common/vueappcomposable vs. viewersearch/) given the future cross-app rollout. - Whether
db:-prefixed fields are surfaced in the field typeahead or only preserved on round-trip.
Source: arkime/arkime