Query Schema

The top-level fields of a search query.

A query is a single JSON object with these fields (or an array of them, see Combining several queries below):

FieldTypeRequiredDefaultDescription
idstringnoits indexOnly used when combining queries, see below.
featurestringyes—The driving feature, see below.
voicesstringno"all"Which voices to search.
patternarray of objectsyes—The sequence to search for. See Patterns.
choraleFilterobjectnoallWhich chorales are searched. See Chorale Filter.
simultaneousWitharray of objectsno[]Additional patterns that must match at the same time. See Simultaneous.
mintStartAtPreviousTokenbooleannofalseSee Options.
mintAllowIntervalComplementationstring or arrayno—See Options.
fbCompareExactChordbooleannofalseSee Options.
kernIgnoreOctavebooleannofalseSee Options.
hintReduceCompoundbooleannofalseSee Options.
durationAllowSplitNotesbooleannofalseSee Options.
durationAllowMergedNotesbooleannofalseSee Options.
metweightSkipUnclassifiedbooleannofalseSee Options.
simultaneousAlignmentstringno"start"See Options.
limitnumbernounlimitedStop 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 deg pattern spans 3 consecutive scale-degree changes; the same 3-position pattern driven by mint spans 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.

SelectorMeaning
"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.