Token-based querying
BlackLab started out as purely a token-based corpus engine. This section shows BCQL's token-based features.
Matching a token
With BCQL you can specify a pattern of tokens (i.e. words) you're looking for.
A simple such pattern is:
[word="man"]
This simply searches for all occurrences of the word man.
Each corpus has a default annotation; usually word. Using this fact, this query can be written even simpler:
"man"
NOTE: In BlackLab's CQL dialect, double and single quotes are interchangeable, but this is not true for all corpus engines. We will use the more standard double quotes in our examples.
Multiple annotations
If your corpus includes the per-word annotations lemma (i.e. headword) and pos (part-of-speech, i.e. noun, verb, etc.), you can query those as well.
For example:
[lemma="search" & pos="noun"]
This query would match search and searches where used as a noun. (your data may use different part-of-speech tags, of course)
Negation
You can use the "does not equal" operator (!=) to search for all words except nouns:
[pos != "noun"]
Regular expressions
The strings between quotes can also contain "wildcards", of sorts. To be precise, they are regular expressions, which provide a flexible way of matching strings of text. For example, to find man or woman (in the default annotation word), use:
"(wo)?man"
And to find lemmas starting with under, use:
[lemma="under.*"]
Explaining regular expression syntax is beyond the scope of this document, but for a complete overview, see regular-expressions.info for a general overview, or Lucene's regular expression syntax specifically, which has a few quirks.
Escaping and literal strings
To find characters with special meaning in a regular expression, such as the period, you need to escape them with a backslash:
[lemma="etc\."]
Alternatively, you can use a "literal string" by prefixing the string with an l
:
[lemma=l'etc.']
Note that some unexpected characters may be considered special regex characters, such as <
and >
. See the above link to Lucene's regex documentation for more details.
Matching any token
Sometimes you want to match any token, regardless of its value.
Of course, this is usually only useful in a larger query, as we will explore next. But we'll introduce the syntax here.
To match any token, use the match-all pattern, which is just a pair of empty square brackets:
[]
Case- and diacritics sensitivity
BlackLab defaults to (case and diacritics) insensitive search. That is, it ignores differences in upper- and lowercase, as well as diacritical marks (accented characters). So searching for "panama"
will also find Panama.
To match a pattern sensitively, prefix it with (?-i)
:
"(?-i)Panama"
Sequences
Simple sequences
You can search for sequences of words as well (i.e. phrase searches, but with many more possibilities). To search for the phrase the tall man, use this query:
"the" "tall" "man"
It might seem a bit clunky to separately quote each word, but this allows us the flexibility to specify exactly what kinds of words we're looking for.
For example, if you want to know all single adjectives used with man (not just tall), use this:
"an?|the" [pos="ADJ"] "man"
This would also match a wise man, an important man, the foolish man, etc.
If we don't care about the part of speech between the article and man, we can use the match-all pattern we showed before:
"an?|the" [] "man"
This way we might match something like the cable man as well as a wise man.
Repetitions
Really powerful token-based queries become possible when you use the regular expression operators on whole tokens as well. If we want to see not just single adjectives applied to man, but multiple as well:
[pos="ADJ"]+ "man"
This query matches little green man, for example. The plus sign after [pos="ADJ"]
says that the preceding part should occur one or more times (similarly, *
means "zero or more times", and ?
means "zero or once").
If you only want matches with exactly two or three adjectives, you can specify that too:
[pos="ADJ"]{2,3} "man"
Or, for two or more adjectives:
[pos="ADJ"]{2,} "man"
You can group sequences of tokens with parentheses and apply operators to the whole group as well. To search for a sequence of nouns, each optionally preceded by an article:
("an?|the"? [pos="NOU"])+
This would, for example, match the well-known palindrome a man, a plan, a canal: Panama! (provided the punctuation marks were not indexed as separate tokens)
Lookahead/lookbehind
Supported from v4.0
This feature will be supported from BlackLab 4.0 (and current development snapshots).
Just like most regular expressions engines, BlackLab supports lookahead and lookbehind assertions. These match a position in the text but do not consume any tokens. They are useful for matching a token only if it is followed or preceded by other token(s).
For example, to find the word cat only if it is followed by in the hat:
"cat" (?= "in" "the" "hat")
Similarly, to find the word dog, but only if it is preceded by very good:
(?<= "very" "good") "dog"
Negative lookahead is also supported. To only find cat if it is not followed by call:
"cat" (?! "call")
And negative lookbehind:
(?<! "bad") "dog"
Finding punctuation
(The following applies to corpora that index punctuation as the punct
property of the next word, not to corpora that index punctuation as a separate token)
Often in BlackLab, the punctuation and spaces between words will be indexed as a property named punct
. This property always contains the spaces and interpunction that occurs before the word where it is indexed.
Because of where it is indexed, it can be tricky to find specific punctuation after a certain word. To find the word dog
followed by a comma, you'd need to do something like this:
"dog" [punct=", *"]
Because spaces are also indexed with the punct
annotation, you need to include them in the regex as well.
BlackLab supports pseudo-annotations that can help with this. You can pretend that every corpus has a punctBefore
and punctAfter
annotation. So you can write the above query as:
[word="dog" & punctAfter=","]
Note that in special cases where more than one punctuation mark is indexed with a word, you may still need to tweak your regular expression. For example, if your input data contained the fragment "(white) dog, (black) cat", the above query would not work because the punct
annotation for the word after dog
would have the value , (
. You'd have to use a more general regular expression:
[word="dog" & punctAfter=",.*"]
Note that punctBefore
and punctAfter
look like annotations when used in the query, but are not; they will not be in the results and you cannot group on them. You can group on the punct
annotation they are based on, because that is actually a part of the index.
Spans
Your input data may contains "spans": marked regions of text, such as paragraphs, sentences, named entities, etc. If your input data is XML these may be XML elements, but they may also be marked in other ways. Non-XML formats may also define spans.
Finding text in relation to these spans is done using an XML-like syntax, regardless of the exact input data format.
Finding spans
If you want to find all the sentence spans in your data:
<s/>
Note that forward slash before the closing bracket. This way of referring to the span means "the whole span". Compare this to <s>
, which means "the start of the span", and </s>
, which means "the end of the span".
So to find only the starts of sentences, use:
<s>
This would find zero-length hits at the position before the first word. Similarly, </s>
finds the ends of sentences. Not very useful, but we can combine these with other queries.
Words at the start or end of a span
More useful might be to find the first word of each sentence:
<s> []
or sentences ending in that:
"that" </s>
(Note that this assumes the period at the end of the sentence is not indexed as a separate token - if it is, you would use "that" '.' </s>
instead)
Words inside a span
You can also search for words occurring inside a specific span. Say you've run named entity recognition on your data, so all names of people are tagged with the person
span. To find the word baker as part of a person's name, use:
"baker" within <person/>
The above query will just match the word baker as part of a person's name. But you're likely more interested in the entire name that contains the word baker. So, to find those full names, use:
<person/> containing 'baker'
Using a regular expression for the span name
You can match multiple span types (e.g. both <person/>
and <location/>
) using a regular expression:
"baker" within <"person|location" />
To match all spans in the corpus, use:
<".+" />
Capturing all overlapping spans
If you want to know all spans that overlap each of your hits (for example, the sentence, paragraph and chapter it occurs
in, if you've indexed those as spans), pass withspans=true
as a parameter.
Alternatively, you can adjust your query directly:
with-spans("baker")
or
with-spans("baker", <"person|location" />, "props")
The second example will capture a list of matching spans in the match info named props
.
Only the first parameter for with-spans
is required. The second parameter defaults to <".+"/>
(all tags); the third defaults to "with-spans"
.
Universal operators
As you might have guessed, you can use within
and containing
with any other query as well. For example:
([pos="ADJ"]+ containing "tall") "man"
will find adjectives applied to man, where one of those adjectives is tall.
Captures
Part of the match
Just like in regular expressions, it is possible to "capture" part of the match for your query as a named group. Everything you capture is returned with the hit in a response section called match info. You label each capture with a name.
Example:
"an?|the" A:[pos="ADJ"] "man"
The adjective part of the match will be captured in a group named A.
You can capture multiple words as well:
"an?|the" adjectives:[pos="ADJ"]+ "man"
This will capture the adjectives found for each match in a captured group named adjectives.
The capture name can also just be a number:
"an?|the" 1:[pos="ADJ"]+ "man"
Spans are captured automatically
If your query involves spans like <s/>
, it will automatically be captured under the span name (s
in this case). You can override the capture name by specifying it in the query, e.g. A:<s/>
.
Constraints
If you tag certain tokens with labels, you can also apply "capture constraints" (also known as "global constraints") on these tokens. This is a way of relating different tokens to one another, for example requiring that they correspond to the same word:
A:[] "by" B:[] :: A.word = B.word
This would match day by day, step by step, etc.
Multiple-value annotations and constraints
Unfortunately, capture constraints can only access the first value indexed for an annotation. If you need this kind of functionality in combination with multi-values constraints, you'll have to find a way around this limitation.
Some queries can be rewritten so they don't need a capture constraint. For example,
A:[word="some"] B:[word="queries"] :: A.lemma="some" & B.lemma="query"
can also be written as
A:[word="some" & lemma="some"] B:[word="queries" & lemma="query"]
, which does work with multiple annotation values.
But this is rare.
In other cases, you might be able to add extra annotations or use spans ("inline tags") to get around this limitation.
Constraint functions
You can also use a few special functions in capture constraints. For example, ensure that words occur in the right order:
(<s> containing A:"cat") containing B:"fluffy" :: start(B) < start(A)
Here we find sentences containing both cat and fluffy (in some order), but then require that fluffy occurs before cat.
Of course this particular query would be better expressed as <s/> containing "fluffy" []* "cat"
. As a general rule,
capture constraints can be a bit slower, so only use them when you need to.
Local capture constraints
Unlike most other corpus engines, BlackLab allows you to place capture constraints inside a parenthesized expression. Be careful that the constraint only refers to labels that are captured inside the parentheses, though!
This is valid and would match over and over again:
(A:[] "and" B:[] :: A.word = B.word) "again"
This is NOT valid (may not produce an error, but the results are undefined):
A:[] ("and" B:[] :: A.word = B.word) "again" # BAD