Query Schema
A query is a single JSON object with these fields (or an array of them, see Combining several queries below):
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
id | string | no | its index | Only used when combining queries, see below. |
feature | string | yes | — | The driving feature, see below. |
voices | string | no | "all" | Which voices to search. |
pattern | array of objects | yes | — | The sequence to search for. See Patterns. |
choraleFilter | object | no | all | Which chorales are searched. See Chorale Filter. |
simultaneousWith | array of objects | no | [] | Additional patterns that must match at the same time. See Simultaneous. |
mintStartAtPreviousToken | boolean | no | false | See Options. |
mintAllowIntervalComplementation | string or array | no | — | See Options. |
fbCompareExactChord | boolean | no | false | See Options. |
kernIgnoreOctave | boolean | no | false | See Options. |
hintReduceCompound | boolean | no | false | See Options. |
durationAllowSplitNotes | boolean | no | false | See Options. |
durationAllowMergedNotes | boolean | no | false | See Options. |
metweightSkipUnclassified | boolean | no | false | See Options. |
simultaneousAlignment | string | no | "start" | See Options. |
limit | number | no | unlimited | Stop after this many results, across the whole corpus. |
The driving feature
feature picks which analysis spine the search actually walks through. The
engine collects every onset (every non-empty note/interval/harmony event) in
that spine, for the voices you asked for, and then slides your pattern — a
window of one or more positions — over consecutive onsets, checking each
position against one onset.
Only the spines listed on the Features page can be a driving
feature: kern, deg, mint, fb, metweight, and any concrete
hint-<pair> (e.g. hint-14), but not the hint-<voice> relative form (e.g.
hint-2), which only resolves against whichever voice a different driving
feature happens to be walking, see hint. A pattern
position can additionally check duration, fermata, phrase and any other feature
at each onset (see Checking several features at
once) but duration and
fermata cannot be the driving feature themselves, because they aren't
independent spines: they're read off the underlying **kern token at whatever
line the driving feature happens to land on. Setting feature to duration or
fermata (or any name that isn't a real spine) always returns zero results.
A set of voice pairs, hint-**, hint-*4 or hint-1*, can also be the driving
feature of a query (but not of a simultaneousWith entry): the query is then
searched for each pair of the set, see hint.
Two consequences of "the driving feature decides which onsets exist" that come up a lot in practice:
- The pattern's length is a length in onsets of that feature, not in beats
or notes of some other voice. A 3-position
degpattern spans 3 consecutive scale-degree changes; the same 3-position pattern driven bymintspans 3 consecutive melodic intervals. - Switching the driving feature changes where matches can start, even for an otherwise identical pattern, because it changes what counts as "the next onset".
voices
A case-insensitive selector for which voices to search. Voices are numbered 1‑4
from the bottom up: 1 = Bass, 2 = Tenor, 3 = Alto, 4 = Soprano.
| Selector | Meaning |
|---|---|
"all" or "" | every voice (default) |
"1" … "4" | a single voice by number |
"bass", "tenor", "alto", "soprano" | a single voice by name |
"b", "t", "a", "s" | a single voice by initial letter |
a run of digits, e.g. "124" | several voices at once: Bass, Tenor, Soprano |
comma-separated, e.g. "bass,3,s" | mix names/digits/letters freely |
Each matching voice is searched independently and reported separately in the
results (see the voice field returned per result).
When the query is driven by a set of voice pairs (hint-**), @lower and
@upper stand for the lower and the upper voice of the pair being searched. The
query's own voices is then all voices, which reads each pair once, or names
@lower and @upper only.
simultaneousWith
Additional patterns, typically in other voices, that must each have a match at
the same musical position as a match of the query's own pattern, for that
match to be kept. See the dedicated Simultaneous page.
Combining several queries
The top-level input can be a single query object, as above, or a JSON array of query objects. Each one runs independently across the whole corpus, and the results are combined into a single list:
[
{
"id": "green",
"feature": "mint",
"voices": "soprano",
"pattern": [
{ "mint": "*" },
{ "mint": "-M2", "fermata": true }
]
},
{
"id": "sky",
"feature": "mint",
"voices": "bass",
"pattern": [
{ "mint": "*" },
{ "mint": "-P5", "fermata": true }
]
}
]
Searched individually, these two queries cannot be composed into one, because
they search different voices. Combined, the ids (see id) color their
results in the score.
Every result gets a queryId field so you can tell which of the combined
queries produced it: the query's own id if it set one, otherwise its position
in the array, stringified ("0", "1", …). A single, non-array query never
gets a queryId at all, there's nothing to disambiguate. Each query keeps its
own independent limit, so combining a capped and an uncapped query doesn't let
the uncapped one cut the capped one off early, or vice versa.
Combining queries this way parses and analyzes each chorale only once, regardless of how many queries are combined, so it's meaningfully cheaper than sending the same queries one at a time.
id
id can be any string, but the chorale-webapp frontend gives one particular
choice special meaning: set it to the name of a standard Tailwind
color (e.g. "green", "sky",
"fuchsia") and the frontend highlights that query's matches in exactly that
color, instead of picking one automatically. Any other string still works as an
id, it just doesn't control the color.
Matcher options
The remaining fields (mintStartAtPreviousToken, fbCompareExactChord,
kernIgnoreOctave, …) tweak how matching itself works rather than what gets
matched. See the Options page for each one.
limit
Type: number · Default: unlimited
Caps the total number of results returned, counted across the entire corpus,
not per chorale. As soon as the cap is reached, the search stops scanning
further chorales; files are processed in sorted filename order, so limit
always returns the same first N results for a given query, deterministically.
{
"feature": "deg",
"pattern": [
{ "deg": "7" },
{ "deg": "1" }
],
"limit": 20
}
limit is mainly useful while you're iterating on a query: a pattern that's too
loose (or has a typo that makes it match almost everything) won't force you to
wait for all 370 chorales to be scanned before you notice.
Unlike the matcher options, limit doesn't change how
anything is matched, only how many of the matches you get back. It's also the
one field of this kind that a simultaneousWith group
never has: a group is a constraint on the primary pattern, not a search that
returns results of its own.