You might want to consider the start of this tutorial.
Short introductions to other TF datasets:
or the
%load_ext autoreload
%autoreload 2
import collections
from tf.app import use
A = use("ETCBC/bhsa", hoist=globals())
Locating corpus resources ...
Name | # of nodes | # slots / node | % coverage |
---|---|---|---|
book | 39 | 10938.21 | 100 |
chapter | 929 | 459.19 | 100 |
lex | 9230 | 46.22 | 100 |
verse | 23213 | 18.38 | 100 |
half_verse | 45179 | 9.44 | 100 |
sentence | 63717 | 6.70 | 100 |
sentence_atom | 64514 | 6.61 | 100 |
clause | 88131 | 4.84 | 100 |
clause_atom | 90704 | 4.70 | 100 |
phrase | 253203 | 1.68 | 100 |
phrase_atom | 267532 | 1.59 | 100 |
subphrase | 113850 | 1.42 | 38 |
word | 426590 | 1.00 | 100 |
3
ETCBC/bhsa
/Users/me/text-fabric-data/github/ETCBC/bhsa/app
gb112c161cfd21eae403d51a2733740d8743460e7
''
<code>Genesis 1:1</code> (use <a href="https://github.com/{org}/{repo}/blob/master/tf/{version}/book%40en.tf" target="_blank">English book names</a>)
g_uvf_utf8
g_vbs
kq_hybrid
languageISO
g_nme
lex0
is_root
g_vbs_utf8
g_uvf
dist
root
suffix_person
g_vbe
dist_unit
suffix_number
distributional_parent
kq_hybrid_utf8
crossrefSET
instruction
g_prs
lexeme_count
rank_occ
g_pfm_utf8
freq_occ
crossrefLCS
functional_parent
g_pfm
g_nme_utf8
g_vbe_utf8
kind
g_prs_utf8
suffix_gender
mother_object_type
absent
n/a
none
unknown
NA
{docRoot}/{repo}
''
''
https://{org}.github.io
0_home
{}
True
local
/Users/me/text-fabric-data/github/ETCBC/bhsa/_temp
BHSA = Biblia Hebraica Stuttgartensia Amstelodamensis
10.5281/zenodo.1007624
ner
Phonetic Transcriptions
https://nbviewer.jupyter.org/github/etcbc/phono/blob/master/programs/phono.ipynb
10.5281/zenodo.1007636
ETCBC
/tf
phono
Parallel Passages
https://nbviewer.jupyter.org/github/ETCBC/parallels/blob/master/programs/parallels.ipynb
10.5281/zenodo.1007642
ETCBC
/tf
parallels
ETCBC
/tf
bhsa
2021
https://shebanq.ancient-data.org/hebrew
Show this on SHEBANQ
la
True
{webBase}/text?book=<1>&chapter=<2>&verse=<3>&version={version}&mr=m&qw=q&tp=txt_p&tr=hb&wget=v&qget=v&nget=vt
{webBase}/word?version={version}&id=<lid>
v1.8.1
{typ} {rela}
''
True
{code}
1
''
True
{label}
''
True
gloss
{voc_lex_utf8}
word
orig
{voc_lex_utf8}
{typ} {function}
''
True
{typ} {rela}
1
''
{number}
''
True
{number}
1
''
True
{number}
''
pdp vs vt
lex:gloss
hbo
So far we have seen search templates specifying feature conditions on nodes and a bit of nesting of those nodes, with an occasional extra constraint on their positions.
We show some more possibilities. An more thorough treatment is in relations.
We can refer to (spatial) relationships between nodes by means of extra constraints of the form
n relop m
where n
and m
are names of node parts in your template, and relop
is the name of a relational operator.
Text-Fabric comes with a fixed bunch of spatial relational operators, and your data set may contain edge-features, which correspond to additional relational operators.
You can get the list of all relational operators that you can currently use:
S.relationsLegend()
= left equal to right (as node) # left unequal to right (as node) < left before right (in canonical node ordering) > left after right (in canonical node ordering) == left occupies same slots as right && left has overlapping slots with right ## left and right do not have the same slot set || left and right do not have common slots [[ left embeds right ]] left embedded in right << left completely before right >> left completely after right =: left and right start at the same slot := left and right end at the same slot :: left and right start and end at the same slot <: left immediately before right :> left immediately after right =k: left and right start at k-nearly the same slot :k= left and right end at k-nearly the same slot :k: left and right start and end at k-near slots <k: left k-nearly before right :k> left k-nearly after right .f. left.f = right.f .f=g. left.f = right.g .f~r~g. left.f matches right.g .f#g. left.f # right.g .f>g. left.f > right.g .f<g. left.f < right.g -crossref> edge feature "crossref" with value specification allowed <crossref- edge feature "crossref" with value specification allowed (opposite direction) <crossref> edge feature "crossref" with value specification allowed (either direction) -crossrefLCS> edge feature "crossrefLCS" with value specification allowed <crossrefLCS- edge feature "crossrefLCS" with value specification allowed (opposite direction) <crossrefLCS> edge feature "crossrefLCS" with value specification allowed (either direction) -crossrefSET> edge feature "crossrefSET" with value specification allowed <crossrefSET- edge feature "crossrefSET" with value specification allowed (opposite direction) <crossrefSET> edge feature "crossrefSET" with value specification allowed (either direction) -distributional_parent> edge feature "distributional_parent" <distributional_parent- edge feature "distributional_parent" (opposite direction) <distributional_parent> edge feature "distributional_parent" (either direction) -functional_parent> edge feature "functional_parent" <functional_parent- edge feature "functional_parent" (opposite direction) <functional_parent> edge feature "functional_parent" (either direction) -mother> edge feature "mother" <mother- edge feature "mother" (opposite direction) <mother> edge feature "mother" (either direction) The warp feature "oslots" and omap features cannot be used in searches. One of the above relations on nodes and / or slots will suit you better.
Note the operators that are surrounded by . .
and have f
and/or g
and/or r
in them.
You can supply any node feature f
and g
in your dataset, and any regular expression r
.
We look for predicate - subject pairs where the subject is a single noun and agrees with the predicate in grammatical number.
Moreover, the noun must be part of the subject.
query = """
clause
phrase function=Pred
w1:word pdp=verb
phrase function=Subj
=: w2:word pdp=subs
:=
w1 .nu. w2
"""
results = A.search(query)
0.68s 3759 results
A.table(results, end=4)
n | p | clause | phrase | word | phrase | word |
---|---|---|---|---|---|---|
1 | Genesis 1:3 | יְהִ֣י אֹ֑ור | יְהִ֣י | יְהִ֣י | אֹ֑ור | אֹ֑ור |
2 | Genesis 1:3 | וַֽיְהִי־אֹֽור׃ | יְהִי־ | יְהִי־ | אֹֽור׃ | אֹֽור׃ |
3 | Genesis 1:5 | וַֽיְהִי־עֶ֥רֶב | יְהִי־ | יְהִי־ | עֶ֥רֶב | עֶ֥רֶב |
4 | Genesis 1:5 | וַֽיְהִי־בֹ֖קֶר | יְהִי־ | יְהִי־ | בֹ֖קֶר | בֹ֖קֶר |
A.show(results, condenseType="clause", end=4)
result 1
result 2
result 3
result 4
Now we want such pairs, but then where the grammatical number differs.
query = """
clause
phrase function=Pred
w1:word pdp=verb
phrase function=Subj
=: w2:word pdp=subs
:=
w1 .nu#nu. w2
"""
results = A.search(query)
0.65s 739 results
A.table(results, end=4)
n | p | clause | phrase | word | phrase | word |
---|---|---|---|---|---|---|
1 | Genesis 1:1 | בְּרֵאשִׁ֖ית בָּרָ֣א אֱלֹהִ֑ים אֵ֥ת הַשָּׁמַ֖יִם וְאֵ֥ת הָאָֽרֶץ׃ | בָּרָ֣א | בָּרָ֣א | אֱלֹהִ֑ים | אֱלֹהִ֑ים |
2 | Genesis 1:3 | וַיֹּ֥אמֶר אֱלֹהִ֖ים | יֹּ֥אמֶר | יֹּ֥אמֶר | אֱלֹהִ֖ים | אֱלֹהִ֖ים |
3 | Genesis 1:4 | וַיַּ֧רְא אֱלֹהִ֛ים אֶת־הָאֹ֖ור | יַּ֧רְא | יַּ֧רְא | אֱלֹהִ֛ים | אֱלֹהִ֛ים |
4 | Genesis 1:4 | וַיַּבְדֵּ֣ל אֱלֹהִ֔ים בֵּ֥ין הָאֹ֖ור וּבֵ֥ין הַחֹֽשֶׁךְ׃ | יַּבְדֵּ֣ל | יַּבְדֵּ֣ל | אֱלֹהִ֔ים | אֱלֹהִ֔ים |
A.show(results, condenseType="clause", end=4)
result 1
result 2
result 3
result 4
and now where the subject is not God(s).
query = """
clause
phrase function=Pred
w1:word pdp=verb
phrase function=Subj
=: w2:word pdp=subs lex#>LHJM/
:=
w1 .nu#nu. w2
"""
results = A.search(query)
0.77s 525 results
A.table(results, end=4)
n | p | clause | phrase | word | phrase | word |
---|---|---|---|---|---|---|
1 | Genesis 1:14 | יְהִ֤י מְאֹרֹת֙ בִּרְקִ֣יעַ הַשָּׁמַ֔יִם | יְהִ֤י | יְהִ֤י | מְאֹרֹת֙ | מְאֹרֹת֙ |
2 | Genesis 3:5 | וְנִפְקְח֖וּ עֵֽינֵיכֶ֑ם | נִפְקְח֖וּ | נִפְקְח֖וּ | עֵֽינֵיכֶ֑ם | עֵֽינֵיכֶ֑ם |
3 | Genesis 7:22 | כֹּ֡ל מִכֹּ֛ל מֵֽתוּ׃ | מֵֽתוּ׃ | מֵֽתוּ׃ | כֹּ֡ל | כֹּ֡ל |
4 | Genesis 18:32 | אוּלַ֛י יִמָּצְא֥וּן שָׁ֖ם עֲשָׂרָ֑ה | יִמָּצְא֥וּן | יִמָּצְא֥וּן | עֲשָׂרָ֑ה | עֲשָׂרָ֑ה |
A.show(results, condenseType="clause", end=4)
result 1
result 2
result 3
result 4
Note that all edge features in the dataset correspond to three relational operators.
For example, mother
gives rise to the operators -mother>
and <mother-
and <mother>
.
Here is an example: look for pairs of clauses of which one is the mother of the other.
In our dataset, there is an edge between the two clauses, and this edge is coded in the feature mother
.
The following query shows how to use the mother
edge information.
query = """
clause
-mother> clause
"""
results = A.search(query)
0.08s 13917 results
A.table(results, end=10)
n | p | clause | clause |
---|---|---|---|
1 | Genesis 1:4 | כִּי־טֹ֑וב | וַיַּ֧רְא אֱלֹהִ֛ים אֶת־הָאֹ֖ור |
2 | Genesis 1:10 | כִּי־טֹֽוב׃ | וַיַּ֥רְא אֱלֹהִ֖ים |
3 | Genesis 1:12 | כִּי־טֹֽוב׃ | וַיַּ֥רְא אֱלֹהִ֖ים |
4 | Genesis 1:14 | לְהַבְדִּ֕יל בֵּ֥ין הַיֹּ֖ום וּבֵ֣ין הַלָּ֑יְלָה | יְהִ֤י מְאֹרֹת֙ בִּרְקִ֣יעַ הַשָּׁמַ֔יִם |
5 | Genesis 1:15 | לְהָאִ֖יר עַל־הָאָ֑רֶץ | וְהָי֤וּ לִמְאֹורֹת֙ בִּרְקִ֣יעַ הַשָּׁמַ֔יִם |
6 | Genesis 1:17 | לְהָאִ֖יר עַל־הָאָֽרֶץ׃ | וַיִּתֵּ֥ן אֹתָ֛ם אֱלֹהִ֖ים בִּרְקִ֣יעַ הַשָּׁמָ֑יִם |
7 | Genesis 1:18 | וְלִמְשֹׁל֙ בַּיֹּ֣ום וּבַלַּ֔יְלָה | לְהָאִ֖יר עַל־הָאָֽרֶץ׃ |
8 | Genesis 1:18 | וּֽלֲהַבְדִּ֔יל בֵּ֥ין הָאֹ֖ור וּבֵ֣ין הַחֹ֑שֶׁךְ | וְלִמְשֹׁל֙ בַּיֹּ֣ום וּבַלַּ֔יְלָה |
9 | Genesis 1:18 | כִּי־טֹֽוב׃ | וַיַּ֥רְא אֱלֹהִ֖ים |
10 | Genesis 1:21 | כִּי־טֹֽוב׃ | וַיַּ֥רְא אֱלֹהִ֖ים |
The mother relation is not always between clause nodes. What if we are interested in all nodes between which the mother relation exists, irrespective of the type?
Use the .
in the query instead of clause
.
The .
stands for: any node type.
query = """
.
-mother> .
"""
results = A.search(query)
0.75s 182269 results
A.table(results, end=4, colorMap={1: "salmon", 2: "cyan"})
n | p | clause_atom (+1) | clause_atom (+1) |
---|---|---|---|
1 | Genesis 1:1 | אֵ֥ת הָאָֽרֶץ׃ | אֵ֥ת הַשָּׁמַ֖יִם |
2 | Genesis 1:2 | וְהָאָ֗רֶץ הָיְתָ֥ה תֹ֨הוּ֙ וָבֹ֔הוּ | בְּרֵאשִׁ֖ית בָּרָ֣א אֱלֹהִ֑ים אֵ֥ת הַשָּׁמַ֖יִם וְאֵ֥ת הָאָֽרֶץ׃ |
3 | Genesis 1:2 | בֹ֔הוּ | תֹ֨הוּ֙ |
4 | Genesis 1:2 | וְחֹ֖שֶׁךְ עַל־פְּנֵ֣י תְהֹ֑ום | וְהָאָ֗רֶץ הָיְתָ֥ה תֹ֨הוּ֙ וָבֹ֔הוּ |
We can show more of the edges.
Let's highlight all edges in the result in yellow.
A.show(
results,
end=1,
colorMap={1: "salmon", 2: "cyan"},
hiddenTypes={"half_verse"},
edgeHighlights=dict(mother={p: "yellow" for p in results}),
)
result 1
Now we color the edges between subphrases orange, the edges between clause atoms green, and the other edges yellow.
ehighlights = {p: "yellow" for p in results}
for (f, t) in results:
fType = F.otype.v(f)
tType = F.otype.v(t)
ehighlights[(f, t)] = (
(
"orange"
if fType == "subphrase"
else "lightgreen"
if fType == "clause_atom"
else "yellow"
)
if fType == tType
else "yellow"
)
A.show(
results,
end=1,
colorMap={1: "salmon", 2: "cyan"},
hiddenTypes={"half_verse"},
edgeHighlights=dict(mother=ehighlights),
)
result 1
Let's have a look at result 2:
A.show(
results,
start=2,
end=2,
colorMap={1: "salmon", 2: "cyan"},
hiddenTypes={"half_verse"},
edgeHighlights=dict(mother=ehighlights),
)
result 2
What about those yellow edges in the subphrases above? Didn't we say that those should be orange?
No, because they do not point to a subphrase, but to the word in the subphrase. To make that even more explicit, we show the node numbers:
A.show(
results,
start=2,
end=2,
colorMap={1: "salmon", 2: "cyan"},
withNodes=True,
hiddenTypes={"half_verse"},
edgeHighlights=dict(mother=ehighlights),
)
result 2
A clause and its mother do not have to be in the same verse. We are going to fetch are the cases where they are in different verses.
Note that we need a more flexible syntax here, where we specify a few templates, give names to a few positions in the template, and then constrain those positions by stipulating relationships between them.
Caution
Referring to verses is not as innocent as it seems. That will be addressed in gaps
query = """
v1:verse
c1:clause
v2:verse
c2:clause
c1 -mother> c2
v1 # v2
"""
results = A.search(query)
0.13s 710 results
A.table(results, end=1)
n | p | verse | clause | verse | clause |
---|---|---|---|---|---|
1 | Genesis 1:18 | וְלִמְשֹׁל֙ בַּיֹּ֣ום וּבַלַּ֔יְלָה | לְהָאִ֖יר עַל־הָאָֽרֶץ׃ |
We want to see the different verse references in the table.
We can skip the verse columns first:
A.table(results, end=1, skipCols="1 3")
n | p | verse | clause | verse | clause |
---|---|---|---|---|---|
1 | Genesis 1:18 | וְלִמְשֹׁל֙ בַּיֹּ֣ום וּבַלַּ֔יְלָה | לְהָאִ֖יר עַל־הָאָֽרֶץ׃ |
and then specify that the remaining columns (the clauses) show the passage:
A.table(results, end=7, skipCols="1 3", withPassage="1 2")
n | verse | clause | verse | clause |
---|---|---|---|---|
1 | Genesis 1:18 וְלִמְשֹׁל֙ בַּיֹּ֣ום וּבַלַּ֔יְלָה | לְהָאִ֖יר עַל־הָאָֽרֶץ׃ | ||
2 | Genesis 2:7 וַיִּיצֶר֩ יְהוָ֨ה אֱלֹהִ֜ים אֶת־הָֽאָדָ֗ם עָפָר֙ מִן־הָ֣אֲדָמָ֔ה | בְּיֹ֗ום | ||
3 | Genesis 7:3 לְחַיֹּ֥ות זֶ֖רַע עַל־פְּנֵ֥י כָל־הָאָֽרֶץ׃ | מִכֹּ֣ל׀ הַבְּהֵמָ֣ה הַטְּהֹורָ֗ה תִּֽקַּח־לְךָ֛ שִׁבְעָ֥ה שִׁבְעָ֖ה אִ֣ישׁ וְאִשְׁתֹּ֑ו | ||
4 | Genesis 22:17 כִּֽי־בָרֵ֣ךְ אֲבָרֶכְךָ֗ | כִּ֗י | ||
5 | Genesis 24:44 הִ֣וא הָֽאִשָּׁ֔ה | הָֽעַלְמָה֙ | ||
6 | Genesis 27:45 עַד־שׁ֨וּב אַף־אָחִ֜יךָ מִמְּךָ֗ | עַ֥ד אֲשֶׁר־תָּשׁ֖וּב חֲמַ֥ת אָחִֽיךָ׃ | ||
7 | Genesis 36:16 אַלּֽוּף־קֹ֛רַח אַלּ֥וּף גַּעְתָּ֖ם אַלּ֣וּף עֲמָלֵ֑ק | בְּנֵ֤י אֱלִיפַז֙ בְּכֹ֣ור עֵשָׂ֔ו אַלּ֤וּף תֵּימָן֙ אַלּ֣וּף אֹומָ֔ר אַלּ֥וּף צְפֹ֖ו אַלּ֥וּף קְנַֽז׃ |
There are also edge features that somehow qualify the relation between nodes they specify.
The edge feature crossref
in the
parallels
module specifies a relationship between verses: they are parallel if they are similar.
But crossref
also tells you how similar, in the form of a number that is the percentage of similarity
according to the measure used by the algorithm to detect the parallels.
This number is called the value of the crossref
edge.
In our search templates we make use of the values of edge features.
Not all edge features provide values. mother
does not. But crossref
does.
Here is how many cross-references we have. The crossref
edge feature is symmetric: if v
is parallel to w
, w
is parallel to v
. So in our query we stipulate that v
comes before w
:
query = """
v:verse
-crossref> w:verse
v < w
"""
results = A.search(query)
0.06s 15871 results
We get a quick overview of the similarity distribution of parallels by means of freqList()
:
E.crossref.freqList()
((100, 8456), (80, 7796), (84, 2874), (86, 2328), (76, 1274), (77, 1220), (78, 1170), (79, 844), (81, 844), (75, 836), (83, 754), (88, 730), (82, 720), (92, 250), (85, 248), (90, 240), (91, 216), (94, 160), (87, 148), (95, 148), (89, 142), (96, 90), (93, 88), (98, 76), (99, 58), (97, 32))
If we want the cases with a high similarity, we can say:
query = """
v:verse
-crossref>95> w:verse
v < w
"""
results = A.search(query)
A.table(results, end=10, withPassage="1 2")
0.04s 4356 results
We can also see the verses written out:
A.table(results, end=5, withPassage="1 2", full=True)
n | verse | verse |
---|---|---|
1 | Genesis 10:2 בְּנֵ֣י יֶ֔פֶת גֹּ֣מֶר וּמָגֹ֔וג וּמָדַ֖י וְיָוָ֣ן וְתֻבָ֑ל וּמֶ֖שֶׁךְ וְתִירָֽס׃ | 1_Chronicles 1:5 בְּנֵ֣י יֶ֔פֶת גֹּ֣מֶר וּמָגֹ֔וג וּמָדַ֖י וְיָוָ֣ן וְתֻבָ֑ל וּמֶ֖שֶׁךְ וְתִירָֽס׃ ס |
2 | Genesis 10:6 וּבְנֵ֖י חָ֑ם כּ֥וּשׁ וּמִצְרַ֖יִם וּפ֥וּט וּכְנָֽעַן׃ | 1_Chronicles 1:8 בְּנֵ֖י חָ֑ם כּ֥וּשׁ וּמִצְרַ֖יִם פּ֥וּט וּכְנָֽעַן׃ |
3 | Genesis 10:7 וּבְנֵ֣י כ֔וּשׁ סְבָא֙ וַֽחֲוִילָ֔ה וְסַבְתָּ֥ה וְרַעְמָ֖ה וְסַבְתְּכָ֑א וּבְנֵ֥י רַעְמָ֖ה שְׁבָ֥א וּדְדָֽן׃ | 1_Chronicles 1:9 וּבְנֵ֣י כ֔וּשׁ סְבָא֙ וַחֲוִילָ֔ה וְסַבְתָּ֥א וְרַעְמָ֖א וְסַבְתְּכָ֑א וּבְנֵ֥י רַעְמָ֖א שְׁבָ֥א וּדְדָֽן׃ ס |
4 | Genesis 10:8 וְכ֖וּשׁ יָלַ֣ד אֶת־נִמְרֹ֑ד ה֣וּא הֵחֵ֔ל לִֽהְיֹ֥ות גִּבֹּ֖ר בָּאָֽרֶץ׃ | 1_Chronicles 1:10 וְכ֖וּשׁ יָלַ֣ד אֶת־נִמְרֹ֑וד ה֣וּא הֵחֵ֔ל לִהְיֹ֥ות גִּבֹּ֖ור בָּאָֽרֶץ׃ ס |
5 | Genesis 10:13 וּמִצְרַ֡יִם יָלַ֞ד אֶת־לוּדִ֧ים וְאֶת־עֲנָמִ֛ים וְאֶת־לְהָבִ֖ים וְאֶת־נַפְתֻּחִֽים׃ | 1_Chronicles 1:11 וּמִצְרַ֡יִם יָלַ֞ד אֶת־לוּדִ֧ים וְאֶת־עֲנָמִ֛ים וְאֶת־לְהָבִ֖ים וְאֶת־נַפְתֻּחִֽים׃ |
If we want to inspect the cases with a lower similarity:
query = """
v:verse
-crossref<80> w:verse
v < w
"""
results = A.search(query)
A.table(results, end=3, withPassage="1 2", full=True)
0.03s 2672 results
n | verse | verse |
---|---|---|
1 | Genesis 1:15 וְהָי֤וּ לִמְאֹורֹת֙ בִּרְקִ֣יעַ הַשָּׁמַ֔יִם לְהָאִ֖יר עַל־הָאָ֑רֶץ וַֽיְהִי־כֵֽן׃ | Genesis 1:17 וַיִּתֵּ֥ן אֹתָ֛ם אֱלֹהִ֖ים בִּרְקִ֣יעַ הַשָּׁמָ֑יִם לְהָאִ֖יר עַל־הָאָֽרֶץ׃ |
2 | Genesis 5:4 וַיִּֽהְי֣וּ יְמֵי־אָדָ֗ם אַֽחֲרֵי֙ הֹולִידֹ֣ו אֶת־שֵׁ֔ת שְׁמֹנֶ֥ה מֵאֹ֖ת שָׁנָ֑ה וַיֹּ֥ולֶד בָּנִ֖ים וּבָנֹֽות׃ | Genesis 5:7 וַֽיְחִי־שֵׁ֗ת אַֽחֲרֵי֙ הֹולִידֹ֣ו אֶת־אֱנֹ֔ושׁ שֶׁ֣בַע שָׁנִ֔ים וּשְׁמֹנֶ֥ה מֵאֹ֖ות שָׁנָ֑ה וַיֹּ֥ולֶד בָּנִ֖ים וּבָנֹֽות׃ |
3 | Genesis 5:4 וַיִּֽהְי֣וּ יְמֵי־אָדָ֗ם אַֽחֲרֵי֙ הֹולִידֹ֣ו אֶת־שֵׁ֔ת שְׁמֹנֶ֥ה מֵאֹ֖ת שָׁנָ֑ה וַיֹּ֥ולֶד בָּנִ֖ים וּבָנֹֽות׃ | Genesis 5:13 וַיְחִ֣י קֵינָ֗ן אַחֲרֵי֙ הֹולִידֹ֣ו אֶת־מַֽהֲלַלְאֵ֔ל אַרְבָּעִ֣ים שָׁנָ֔ה וּשְׁמֹנֶ֥ה מֵאֹ֖ות שָׁנָ֑ה וַיֹּ֥ולֶד בָּנִ֖ים וּבָנֹֽות׃ |
This shows how all features in your data can be queried in search templates, even the features that give values to edges.
So far we have seen feature conditions in templates of these forms
node feature=value
But there is more.
You can say
node feature*
which selects all nodes, irrespective of the existence or value of feature.
This is a useless criterion in the sense that it does not influence the set of results.
But when some applications run queries for you, they might use the features mentioned in your query to decorate the results retrieved.
This is your way to tell such applications that you want the values of feature
included in your results.
The text fabric browser looks at the features when it exports your results to CSV.
query1 = """
word vt*
"""
query2 = """
word
"""
results = A.search(query1)
print(len(results))
results = A.search(query1)
print(len(results))
0.35s 426590 results 426590 0.34s 426590 results 426590
You can also say
node feature#value
which selects nodes where the feature does not have value
.
When stating a feature condition, such as chapter=1
,
you may also specify a list of alternative values:
chapter=1|2|3
You may list as many values as you wish, for every feature.
It also works with inequalities:
chapter#1|2|3
Let's find all verbally inflected words that are: not in the qal, not in the third person, not in the singular, not in the masculine.
query = """
word sp=verb vs#qal vt#infc|infa|ptca|ptcp ps#p3 nu#sg gn#m
"""
A.displaySetup(extraFeatures="vt ps nu gn")
results = A.search(query, shallow=True)
0.35s 271 results
for r in sorted(results)[0:5]:
A.pretty(r)
A.displayReset("extraFeatures")
If you are not interested in the particular value of a feature, but only in whether there is a value or not, you can express that.
We can ask for all words that have a qere.
Just leave out the =value
part.
word qere
Conversely, we can ask for words without a qere.
Just add a #
after the feature name.
word qere#
Let's test it.
query = """
word
"""
print("Words in total:")
results = A.search(query)
allWords = len(results)
print("Words with a qere:")
query = """
word qere
"""
results = A.search(query)
qereWords = len(results)
print("Words without a qere:")
query = """
word qere#
"""
results = A.search(query)
plainWords = len(results)
print(f"qereWords + plainWords == allWords ? {qereWords + plainWords == allWords}")
Words in total: 0.25s 426590 results Words with a qere: 0.12s 1892 results Words without a qere: 0.30s 424698 results qereWords + plainWords == allWords ? True
For features with numerical values, we may ask for values higher or lower than a given value.
The dist feature gives the distance between an object and its mother.
We want to see it values by means of freqList()
, but the feature is not yet loaded.
Let's do a query with it, after running it, the feature is loaded.
query = """
clause dist=1
"""
results = A.search(query)
1.67s 598 results
Now we can explore the frequencies:
F.dist.freqList()[0:10]
((0, 631151), (-1, 104911), (-2, 38188), (-3, 14986), (-4, 7665), (-5, 3657), (-6, 2145), (1, 1773), (-7, 1380), (-8, 918))
Let us say we are interested in clause only. The feature dist
is defined for multiple node types.
We can pass a set of node types to freqList()
in order to get the frequencies restricted to those types:
F.dist.freqList({"clause"})[0:10]
((0, 67340), (-1, 11593), (-2, 3265), (-3, 2437), (-4, 1384), (-5, 668), (1, 598), (-6, 329), (-7, 167), (-8, 70))
There are negative distances. In those cases the mother precedes the daughter. Let's get the mothers that precede their daughters by a large amount.
query = """
clause dist<-10
"""
results = A.search(query)
A.table(sorted(results), end=7)
0.03s 86 results
n | p | clause |
---|---|---|
1 | Genesis 25:12 | אֲשֶׁ֨ר יָלְדָ֜ה הָגָ֧ר הַמִּצְרִ֛ית שִׁפְחַ֥ת שָׂרָ֖ה לְאַבְרָהָֽם׃ |
2 | Genesis 30:33 | אֲשֶׁר־אֵינֶנּוּ֩ נָקֹ֨ד וְטָל֜וּא בָּֽעִזִּ֗ים וְחוּם֙ בַּכְּשָׂבִ֔ים |
3 | Genesis 49:11 | אֹסְרִ֤י לַגֶּ֨פֶן֙ עִירֹ֔ו |
4 | Genesis 50:13 | אֲשֶׁ֣ר קָנָה֩ אַבְרָהָ֨ם אֶת־הַשָּׂדֶ֜ה לַאֲחֻזַּת־קֶ֗בֶר מֵאֵ֛ת עֶפְרֹ֥ן הַחִתִּ֖י |
5 | Exodus 18:8 | אֲשֶׁ֨ר עָשָׂ֤ה יְהוָה֙ לְפַרְעֹ֣ה וּלְמִצְרַ֔יִם עַ֖ל אֹודֹ֣ת יִשְׂרָאֵ֑ל |
6 | Exodus 25:9 | אֲשֶׁ֤ר אֲנִי֙ מַרְאֶ֣ה אֹותְךָ֔ אֵ֚ת תַּבְנִ֣ית הַמִּשְׁכָּ֔ן וְאֵ֖ת תַּבְנִ֣ית כָּל־כֵּלָ֑יו |
7 | Exodus 38:26 | הָעֹבֵ֜ר עַל־הַפְּקֻדִ֗ים מִבֶּ֨ן עֶשְׂרִ֤ים שָׁנָה֙ וָמַ֔עְלָה |
An even more powerful way of specifying desired feature values is by regular expressions. You can do this for string-valued values features only.
Instead of specifying a feature condition like this
typ=WIm0
or
typ=WIm0|WImX
you can say
typ~WIm[0X]
Note that you do not use the =
between feature name and value specification,
but ~
.
The syntax and semantics of regular expressions are those as defined in the Python docs.
Note, that if you need to enter a \
in the regular expression, you have to double it.
Also, when you need a space in it, you have to put a \
in front of it.
If you search with regular expressions, then nodes without a value do not match any regular expression.
The regular expression .*
matches everything.
Not all words have a qere.
So we expect the following template to list all words that do have a qere and none of those that don't.
query = """
word qere~.*
"""
results = list(A.search(query))
matchWords = len(results)
print(
"Compare this with qere words: "
f'{qereWords}: {"Equal" if matchWords == qereWords else "Unequal"}'
)
0.14s 1892 results Compare this with qere words: 1892: Equal
query = """
word sp=subs g_cons~^>.$
"""
results = A.search(query, sort=True)
A.table(results, end=20)
0.21s 816 results
n | p | word |
---|---|---|
1 | Genesis 2:6 | אֵ֖ד |
2 | Genesis 3:20 | אֵ֥ם |
3 | Genesis 14:18 | אֵ֥ל |
4 | Genesis 14:19 | אֵ֣ל |
5 | Genesis 14:20 | אֵ֣ל |
6 | Genesis 14:22 | אֵ֣ל |
7 | Genesis 15:17 | אֵ֔שׁ |
8 | Genesis 16:13 | אֵ֣ל |
9 | Genesis 17:1 | אֵ֣ל |
10 | Genesis 17:4 | אַ֖ב |
11 | Genesis 17:5 | אַב־ |
12 | Genesis 19:24 | אֵ֑שׁ |
13 | Genesis 21:33 | אֵ֥ל |
14 | Genesis 22:6 | אֵ֖שׁ |
15 | Genesis 22:7 | אֵשׁ֙ |
16 | Genesis 24:29 | אָ֖ח |
17 | Genesis 27:45 | אַף־ |
18 | Genesis 28:3 | אֵ֤ל |
19 | Genesis 28:5 | אֵ֥ם |
20 | Genesis 30:2 | אַ֥ף |
Let us zoom in on one of the results. We want to know more about the lexeme in question.
There are several methods to do that.
First of all, let us show the nodes.
A.table(results, start=20, end=20, withNodes=True)
n | p | word |
---|---|---|
20 | Genesis 30:2 | 15621 אַ֥ף |
Now we can use pretty()
to get more info.
A.pretty(results[19][0])
Note that under the word is a link to its lexeme entry in SHEBANQ.
With a bit of TF juggling you could also have got this link programmatically:
lx = L.u(results[19][0], otype="lex")[0]
A.webLink(lx)
We can also add some context to the query. Since we are interested in the lexemes, let's add those to the query.
Every word lies embedded in a lexeme.
query = """
lex
word sp=subs g_cons~^>.$
"""
results = A.search(query)
A.table(results, end=10)
0.20s 816 results
n | p | lex | word |
---|---|---|---|
1 | Exodus 4:8 | אֹות | אֹ֣ת |
2 | Exodus 4:8 | אֹות | אֹ֥ת |
3 | Exodus 8:19 | אֹות | אֹ֥ת |
4 | Exodus 12:13 | אֹות | אֹ֗ת |
5 | Genesis 2:6 | אֵד | אֵ֖ד |
6 | Genesis 27:45 | אַף | אַף־ |
7 | Genesis 30:2 | אַף | אַ֥ף |
8 | Exodus 4:14 | אַף | אַ֨ף |
9 | Exodus 11:8 | אַף | אָֽף׃ ס |
10 | Exodus 32:19 | אַף | אַ֣ף |
Same amount of results, but the order is different.
We just use Python to get the lexemes only, together with their first occurrence.
We make a list of tuples, and feed that to A.table()
.
lexemes = set()
lexResults = []
for (lex, word) in results:
if lex not in lexemes:
lexemes.add(lex)
lexResults.append((lex, word))
A.table(lexResults)
n | p | lex | word |
---|---|---|---|
1 | Exodus 4:8 | אֹות | אֹ֣ת |
2 | Genesis 2:6 | אֵד | אֵ֖ד |
3 | Genesis 27:45 | אַף | אַף־ |
4 | Genesis 17:4 | אָב | אַ֖ב |
5 | Genesis 3:20 | אֵם | אֵ֥ם |
6 | Genesis 24:29 | אָח | אָ֖ח |
7 | Isaiah 20:6 | אִי | אִ֣י |
8 | Genesis 14:18 | אֵל | אֵ֥ל |
9 | Genesis 15:17 | אֵשׁ | אֵ֔שׁ |
10 | Genesis 31:29 | אֵל | אֵ֣ל |
11 | 2_Samuel 18:5 | אַט | אַט־ |
12 | 2_Samuel 14:19 | אִשׁ | אִ֣שׁ׀ |
13 | Ezekiel 40:48 | אַיִל | אֵ֣ל |
14 | Jeremiah 36:22 | אָח | אָ֖ח |
15 | Job 24:25 | אַל | אַ֗ל |
16 | Ezra 5:8 | אָע | אָ֖ע |
Observe how you can use a query to get an interesting node set,
which you can then massage using standard Python machinery,
after which you can display the results prettily with A.table()
or A.show()
.
The take-away lesson is: you can use A.table()
and A.show()
on arbitrary iterables of tuples of nodes,
whether or not they come from an executed query.
The headers of the tables are taken from the node types of all tuples, but it shows the most
frequent one only.
If there are more types in the same column, it will be indicated, and if you hover over the (+1)
you see which
types are also present.
tuples = (
(1, 1000000),
(1000001, 2),
)
A.table(tuples)
n | p | phrase_atom (+1) | phrase_atom (+1) |
---|---|---|---|
1 | Genesis 1:1 | בְּ | תֹּ֕אמֶר |
2 | Genesis 1:1 | בִּי־ | רֵאשִׁ֖ית |
Also A.show()
makes perfect sense in this case.
A.show(tuples, condensed=True, condenseType="clause")
clause 1
clause 2
clause 3
Everything that is part of a result, we see properly highlighted, but we can not discern what belongs to result 1 and what to result 2.
That becomes clear if we uncondense:
A.show(tuples, condensed=False, condenseType="clause")
result 1
result 2
If you look at the clause types
you see a lot of types indicating that the clause starts with we
:
Way0 Wayyiqtol-null clause
WayX Wayyiqtol-X clause
WIm0 We-imperative-null clause
WImX We-imperative-X clause
WQt0 We-qatal-null clause
WQtX We-qatal-X clause
WxI0 We-x-imperative-null clause
WXIm We-X-imperative clause
WxIX We-x-imperative-X clause
WxQ0 We-x-qatal-null clause
WXQt We-X-qatal clause
WxQX We-x-qatal-X clause
WxY0 We-x-yiqtol-null clause
WXYq We-X-yiqtol clause
WxYX We-x-yiqtol-X clause
WYq0 We-yiqtol-null clause
WYqX We-yiqtol-X clause
We are interested in the We-x
and We-X
clauses, so all clauses whose typ
starts with Wx
or WX
.
There are quite a number of verb stems. By means of a regular expression we can pick everything except qal
.
In the
Python docs on regular expressions
we see that we can check for that by ^(?:!qal)
.
query = """
clause typ~^W[xX]
word sp=verb vs#qal
"""
results = list(A.search(query))
A.table(results, end=10)
0.24s 3098 results
n | p | clause | word |
---|---|---|---|
1 | Genesis 1:20 | וְעֹוף֙ יְעֹופֵ֣ף עַל־הָאָ֔רֶץ עַל־פְּנֵ֖י רְקִ֥יעַ הַשָּׁמָֽיִם׃ | יְעֹופֵ֣ף |
2 | Genesis 2:10 | וּמִשָּׁם֙ יִפָּרֵ֔ד | יִפָּרֵ֔ד |
3 | Genesis 2:25 | וְלֹ֖א יִתְבֹּשָֽׁשׁוּ׃ | יִתְבֹּשָֽׁשׁוּ׃ |
4 | Genesis 3:18 | וְקֹ֥וץ וְדַרְדַּ֖ר תַּצְמִ֣יחַֽ לָ֑ךְ | תַּצְמִ֣יחַֽ |
5 | Genesis 4:4 | וְהֶ֨בֶל הֵבִ֥יא גַם־ה֛וּא מִבְּכֹרֹ֥ות צֹאנֹ֖ו וּמֵֽחֶלְבֵהֶ֑ן | הֵבִ֥יא |
6 | Genesis 4:7 | וְאִם֙ לֹ֣א תֵיטִ֔יב | תֵיטִ֔יב |
7 | Genesis 4:14 | וּמִפָּנֶ֖יךָ אֶסָּתֵ֑ר | אֶסָּתֵ֑ר |
8 | Genesis 4:26 | וּלְשֵׁ֤ת גַּם־הוּא֙ יֻלַּד־בֵּ֔ן | יֻלַּד־ |
9 | Genesis 6:1 | וּבָנֹ֖ות יֻלְּד֥וּ לָהֶֽם׃ | יֻלְּד֥וּ |
10 | Genesis 6:12 | וְהִנֵּ֣ה נִשְׁחָ֑תָה | נִשְׁחָ֑תָה |
query = r"""
lex gloss~[\ ] sp=subs
"""
results = list(A.search(query))
A.table(results, start=1, end=4)
A.show(results, condensed=False, start=1, end=4)
result 1
result 2
result 3
result 4
Eventually you reach cases where search templates are just not up to it.
Examples:
Before you dive head over heels into hand coding, here is an intermediate solution. You can create node sets by means of search, and then use those node sets in other search templates at the places where you have node types.
You can make custom sets with arbitrary nodes, not all of the same type. Let's collect all non-word, non-lex nodes that contain fairly frequent words only. We also collect a set of nodes that contain highly infrequent words.
There is a feature for that, rank_lex
.
Since we have not loaded it, we do so now.
TF.load("rank_lex", add=True)
True
We set a threshold COMMON_RANK
, and pick all objects with only high ranking words, their ranks between 0 and COMMON_RANK
.
We set a threshold RARE_RANK
, and pick all objects that contain at least one low ranking word, its rank higher than RARE_RANK
.
COMMON_RANK = 100
RARE_RANK = 500
frequent = set()
infrequent = set()
for n in N.walk():
nTp = F.otype.v(n)
if nTp == "lex":
continue
if nTp == "word":
ranks = [F.rank_lex.v(n)]
else:
ranks = [F.rank_lex.v(w) for w in L.d(n, otype="word")]
maxRank = max(ranks)
minRank = min(ranks)
if maxRank < COMMON_RANK:
frequent.add(n)
if maxRank > RARE_RANK:
infrequent.add(n)
print(f"{len(frequent):>6} members in set frequent")
print(f"{len(infrequent):>6} members in set infrequent")
669195 members in set frequent 425320 members in set infrequent
Now we can do all kinds of searches within the domain of frequent
and infrequent
things.
We give the names to all the sets and put them in a dictionary.
customSets = dict(
frequent=frequent,
infrequent=infrequent,
)
Then we pass it to A.search()
with a query to look for sentences with a rare word that have a clause with only frequent words:
query = """
infrequent otype=sentence
frequent otype=clause
"""
results = A.search(query, sets=customSets)
A.table(results, start=5, end=10)
0.43s 4311 results
n | p | sentence | clause |
---|---|---|---|
5 | Genesis 1:25 | וַיַּ֥רְא אֱלֹהִ֖ים כִּי־טֹֽוב׃ | וַיַּ֥רְא אֱלֹהִ֖ים |
6 | Genesis 1:29 | הִנֵּה֩ נָתַ֨תִּי לָכֶ֜ם אֶת־כָּל־עֵ֣שֶׂב׀ זֹרֵ֣עַ זֶ֗רַע אֲשֶׁר֙ עַל־פְּנֵ֣י כָל־הָאָ֔רֶץ וְאֶת־כָּל־הָעֵ֛ץ אֲשֶׁר־בֹּ֥ו פְרִי־עֵ֖ץ זֹרֵ֣עַ זָ֑רַע וּֽלְכָל־חַיַּ֣ת הָ֠אָרֶץ וּלְכָל־עֹ֨וף הַשָּׁמַ֜יִם וּלְכֹ֣ל׀ רֹומֵ֣שׂ עַל־הָאָ֗רֶץ אֲשֶׁר־בֹּו֙ נֶ֣פֶשׁ חַיָּ֔ה אֶת־כָּל־יֶ֥רֶק עֵ֖שֶׂב לְאָכְלָ֑ה | אֲשֶׁר֙ עַל־פְּנֵ֣י כָל־הָאָ֔רֶץ |
7 | Genesis 2:2 | וַיְכַ֤ל אֱלֹהִים֙ בַּיֹּ֣ום הַשְּׁבִיעִ֔י מְלַאכְתֹּ֖ו אֲשֶׁ֣ר עָשָׂ֑ה | אֲשֶׁ֣ר עָשָׂ֑ה |
8 | Genesis 2:2 | וַיִּשְׁבֹּת֙ בַּיֹּ֣ום הַשְּׁבִיעִ֔י מִכָּל־מְלַאכְתֹּ֖ו אֲשֶׁ֥ר עָשָֽׂה׃ | אֲשֶׁ֥ר עָשָֽׂה׃ |
9 | Genesis 2:3 | כִּ֣י בֹ֤ו שָׁבַת֙ מִכָּל־מְלַאכְתֹּ֔ו אֲשֶׁר־בָּרָ֥א אֱלֹהִ֖ים לַעֲשֹֽׂות׃ פ | לַעֲשֹֽׂות׃ פ |
10 | Genesis 2:4 | בְּיֹ֗ום עֲשֹׂ֛ות יְהוָ֥ה אֱלֹהִ֖ים אֶ֥רֶץ וְשָׁמָֽיִם׃ וַיִּיצֶר֩ יְהוָ֨ה אֱלֹהִ֜ים אֶת־הָֽאָדָ֗ם עָפָר֙ מִן־הָ֣אֲדָמָ֔ה | בְּיֹ֗ום |
We are going to show this really nice:
rank_lex
to the displayA.displaySetup(extraFeatures="rank_lex")
highlights = {}
for (sentence, clause) in results:
highlights[sentence] = "magenta"
highlights[clause] = "cyan"
for w in L.d(sentence, otype="word"):
if F.rank_lex.v(w) > RARE_RANK:
highlights[w] = "magenta"
for w in L.d(clause, otype="word"):
if F.rank_lex.v(w) < COMMON_RANK:
highlights[w] = "cyan"
A.show(
results,
condensed=False,
start=6,
end=7,
suppress={"sp", "vt", "vs", "function", "typ", "otype"},
highlights=highlights,
)
result 6
result 7
Now infrequent sentences ending in a frequent word:
query = """
infrequent otype=sentence
:= frequent otype=word
"""
results = A.search(query, sets=customSets)
A.table(results, start=5, end=10)
0.45s 10798 results
n | p | sentence | word |
---|---|---|---|
5 | Genesis 1:9 | יִקָּו֨וּ הַמַּ֜יִם מִתַּ֤חַת הַשָּׁמַ֨יִם֙ אֶל־מָקֹ֣ום אֶחָ֔ד | אֶחָ֔ד |
6 | Genesis 1:10 | וַיִּקְרָ֨א אֱלֹהִ֤ים׀ לַיַּבָּשָׁה֙ אֶ֔רֶץ | אֶ֔רֶץ |
7 | Genesis 1:11 | תַּֽדְשֵׁ֤א הָאָ֨רֶץ֙ דֶּ֔שֶׁא עֵ֚שֶׂב מַזְרִ֣יעַ זֶ֔רַע עֵ֣ץ פְּרִ֞י עֹ֤שֶׂה פְּרִי֙ לְמִינֹ֔ו אֲשֶׁ֥ר זַרְעֹו־בֹ֖ו עַל־הָאָ֑רֶץ | אָ֑רֶץ |
8 | Genesis 1:15 | וְהָי֤וּ לִמְאֹורֹת֙ בִּרְקִ֣יעַ הַשָּׁמַ֔יִם לְהָאִ֖יר עַל־הָאָ֑רֶץ | אָ֑רֶץ |
9 | Genesis 1:22 | וְהָעֹ֖וף יִ֥רֶב בָּאָֽרֶץ׃ | אָֽרֶץ׃ |
10 | Genesis 1:26 | וְיִרְדּוּ֩ בִדְגַ֨ת הַיָּ֜ם וּבְעֹ֣וף הַשָּׁמַ֗יִם וּבַבְּהֵמָה֙ וּבְכָל־הָאָ֔רֶץ וּבְכָל־הָרֶ֖מֶשׂ הָֽרֹמֵ֥שׂ עַל־הָאָֽרֶץ׃ | אָֽרֶץ׃ |
As a check, we replace the custom set frequent
by the ordinary type word
with a rank condition.
query = """
infrequent otype=sentence
:= word rank_lex<100
"""
results = A.search(query, sets=customSets)
A.table(results, start=5, end=10)
0.37s 10798 results
n | p | sentence | word |
---|---|---|---|
5 | Genesis 1:9 | יִקָּו֨וּ הַמַּ֜יִם מִתַּ֤חַת הַשָּׁמַ֨יִם֙ אֶל־מָקֹ֣ום אֶחָ֔ד | אֶחָ֔ד |
6 | Genesis 1:10 | וַיִּקְרָ֨א אֱלֹהִ֤ים׀ לַיַּבָּשָׁה֙ אֶ֔רֶץ | אֶ֔רֶץ |
7 | Genesis 1:11 | תַּֽדְשֵׁ֤א הָאָ֨רֶץ֙ דֶּ֔שֶׁא עֵ֚שֶׂב מַזְרִ֣יעַ זֶ֔רַע עֵ֣ץ פְּרִ֞י עֹ֤שֶׂה פְּרִי֙ לְמִינֹ֔ו אֲשֶׁ֥ר זַרְעֹו־בֹ֖ו עַל־הָאָ֑רֶץ | אָ֑רֶץ |
8 | Genesis 1:15 | וְהָי֤וּ לִמְאֹורֹת֙ בִּרְקִ֣יעַ הַשָּׁמַ֔יִם לְהָאִ֖יר עַל־הָאָ֑רֶץ | אָ֑רֶץ |
9 | Genesis 1:22 | וְהָעֹ֖וף יִ֥רֶב בָּאָֽרֶץ׃ | אָֽרֶץ׃ |
10 | Genesis 1:26 | וְיִרְדּוּ֩ בִדְגַ֨ת הַיָּ֜ם וּבְעֹ֣וף הַשָּׁמַ֗יִם וּבַבְּהֵמָה֙ וּבְכָל־הָאָ֔רֶץ וּבְכָל־הָרֶ֖מֶשׂ הָֽרֹמֵ֥שׂ עַל־הָאָֽרֶץ׃ | אָֽרֶץ׃ |
Note that no matter how expensive the construction of a set has been, once you have it, queries based on it are just fast. There is no penalty when you use given sets instead of the familiar node types.
advanced
You have seen how to filter on feature values, of nodes and of edges.
Now we want to set up sets for real.
sets relations quantifiers from MQL rough gaps
CC-BY Dirk Roorda