-
Notifications
You must be signed in to change notification settings - Fork 0
Query Language
A QQL query is a compact reference to Islamic texts. The shortest useful one is a single number:
1
The whole of Surah al-Fatihah.
This page is the complete language reference. Every example on it has been run against the real data — the counts and addresses shown are what QQL actually returns.
- The shape of a reference
- Selecting items
- Order and duplicates
- Groups: several chapters at once
- Leaving out the source
- The source carries forward
- Book-wide numbering
- Searching
- Whitespace and case
- Grammar
- Common mistakes
SOURCE : PRIMARY : SELECTOR
| Part | Means | Example |
|---|---|---|
| source | which collection |
Q, B, HM
|
| primary | first-level division | Surah for Quran, chapter for hadith |
| selector | which items inside it |
255, 1-5, 1-5,255
|
Only the primary is required. Drop the selector to take everything in it:
Q:1 → 7 records the whole of Surah 1
Q:2:255 → 1 record Surah 2, ayah 255
Every part of a query is optional except the numbers — see Leaving out the source.
A selector is a comma-separated list. Each item is a single number or an inclusive range.
Q:2:255 1 record a single ayah
Q:2:1-5 5 records ayat 1, 2, 3, 4, 5
Q:2:1-5,255 6 records the range, then ayah 255
Q:2:1-5,10,20-25 ranges and singles mix freely
Ranges are inclusive at both ends: 1-5 is five items, not four.
QQL returns records in the order you wrote them. It never sorts for you.
Q:2:255,1-3 → 2:255, 2:1, 2:2, 2:3
Duplicates are dropped within one reference:
Q:2:1-5,3,4 → 2:1, 2:2, 2:3, 2:4, 2:5 (3 and 4 already there)
…but kept across references, because asking twice is a deliberate act:
Q:2:255;Q:2:255; → 2 records, both Ayat al-Kursi
The one exception to written order is ranked search, which comes back sorted by score and says so.
One source can address several chapters in a single reference. The rule:
An integer followed by
:starts a new group.
So the number before a colon is a primary, not another selector item:
q:1:2,3,2:3,4-6
Surah 1 ayat 2 and 3, then Surah 2 ayat 3, 4, 5, 6 — 6 records.
More:
Q:1,2:255 8 records all of Surah 1, then 2:255
Q:1,2,3 three whole Surahs
B:1:1,2:5 Bukhari chapter 1 hadith 1, then chapter 2 hadith 5
A range is never a primary, so Q:1:1-5:3 is a syntax error rather than a
second reading. Both things it might have meant are writable, and they differ:
Q:1:1-5;3 Surah 1 ayat 1–5, then all of Surah 3
Q:1:1-5,3 Surah 1 ayat 1–5, plus ayah 3 — already in the range, so deduped
No source code means the Quran:
1 7 records the whole of Surah 1
2:255 1 record Ayat al-Kursi
1,2:255 8 records all of Surah 1, then 2:255
1:2,3,2:3,4-6 groups work exactly the same
Once a query names a collection, everything after it belongs to that collection until another code says otherwise:
b:1:1;3 Bukhari 1:1, then Bukhari chapter 3 → 77 records
b:1:1;q:3 Bukhari 1:1, then Surah 3 → 201 records
b:1:1;3;q:1;2 Bukhari 1:1 and 3, then Surah 1 and 2
The Quran default applies only when nothing has been named yet:
1:1;b:1:1 Quran 1:1, then Bukhari 1:1
; separates references and is only needed to switch collection or start a
new primary. A trailing one is always optional — q:1;b:1 and q:1;b:1; are
the same query.
SOURCE::N skips the chapter and uses the canonical citation numbers —
'Abd al-Baqi's numbering for Bukhari (1–7563), Dar-us-Salam's for Muslim, the
sunnah.com reference numbers for the rest. B::6403 returns what the world
cites as Bukhari 6403.
B::6403 the "la ilaha illallah a hundred times" hadith
B::7563 the famous closing hadith of Sahih al-Bukhari
Q::100 the 100th ayah of the mushaf → Surah 2, ayah 93
HM::75 the 75th supplication → chapter 27
B::1-10,255 ranges and lists work here too
For hadith this resolves through small committed maps
(sources/canonical/*.json), built from the public-domain
fawazahmed0/hadith-api dataset and validated against the local text. The
canonical space has holes: front matter owns some numbers (Muslim's
Muqaddima is canonical 1–92, Ibn Majah's introduction 1–266), and lettered
variants like 1771.5 are not integers. A hole asked for alone is an error; a
range simply skips it, so B::1-7563 walks the whole book cleanly.
Not every collection has this axis. Six of them — DA, RS, BM, AM,
MK, SM — have no citation numbering QQL can source, so DA::1 is refused
with QQL_UNSUPPORTED rather than guessed at. See Sources.
Records from this form carry "numbering": "book", so a query mixing both
forms stays unambiguous:
b::100;3 hadith 100 book-wide, then all of chapter 3
Inheriting a source never carries the :: along: the 3 above is a chapter.
Bounds are the whole collection — 1–6236 for the Quran, 1–7563 for Bukhari, 1–267 for Hisnul Muslim.
Note.
B:3:100is the 100th hadith of chapter 3;B::100is canonical number 100 of the book. Where the numbered body of a collection starts, the two agree — Bukhari's canonical 1 isB:1:1— but for Muslim the first canonical number after the Muqaddima is 93, soM::93 == M:1:1.
Three ways to search. The spelling picks the engine, so a build flag never changes what a query means.
| Form | Matches | Order | Needs |
|---|---|---|---|
"term" 'term'
|
folded substring | positional | nothing |
?"term" ?'term'
|
words, stemmed, BM25 | ranked |
fulltext feature |
*"term" *'term'
|
vector similarity | ranked |
vector feature |
The reference in front of the term says where to look:
"text" the Quran — the default source
q:"text" the whole Quran
q:1:"text" Surah 1
q:1:3~5:"text" ayat 3–5 of Surah 1
b:1:"text" Bukhari chapter 1
b:"text" all of Bukhari
Note ~ here, not -. A search scope is written 3~5 so it cannot be
confused with the 3-5 of an ordinary selector.
A marker takes either quote, exactly as a bare term does — so *'mercy' and
*"mercy" are the same query, and each quote can carry the other verbatim:
?'"straight path"' a phrase: inner quotes need the outer ones to differ
*"Allah's" an apostrophe needs the double quotes
q:1:"الحمد" 1 hit Surah 1, ayah 2
q:2:"prayer" 14 hits
q:1:'Allah' 2 hits either quote delimits a term
b:1:"Allah's" 7 hits the other quote carries an apostrophe
q:1:3~5:"You" 1 hit Surah 1, ayah 5
Arabic is matched with the marks folded away. The text is fully
diacritized, so a typed الحمد shares no substring with the stored
ٱلْحَمْدُ. For comparison only, QQL drops harakat, sukun, the superscript
alef and the Quranic annotation marks, folds the alef seats (أ إ آ ٱ → ا),
ى → ي, ة → ه, and lowercases ASCII. Returned text keeps every mark.
Matching is plain substring — not words, not stems:
q:1:"mercy" 0 hits "Merciful" does not contain "mercy"
It covers ar and en, not metadata, so "Al-Fatihah" finds nothing —
Surah names are not part of the verse text.
An inverted index with an English stemmer. This is what fixes the case above:
q:1:?"mercy" 2 hits stemming reaches "Merciful"
The term carries the engine's own syntax:
q:?"prayer AND charity"~3 1 hit
q:?"mercy OR forgiveness"~3 3 hits
q:?"prayer -charity"~3 exclusion
q:?'"straight path"'~3 3 hits — a phrase, so the term takes '
Ranked by vector similarity rather than by words:
q:*"worship"~3 109:2, 109:3, 109:4 Surah al-Kafirun
q:1:*"الحمد" 1:2
b:*"intentions"~3 9:29, 81:88, 90:3
~N after a ranked term caps the results; the default is 20.
q:?"mercy"~5 at most 5, best first
q:*"worship"~3 at most 3
Ranked hits carry two extra fields:
{ "surah": 1, "ayah": 3, "score": 12.017, "ranked": true, "ar": "…", "en": "…" }Weak matches are dropped, so a ranked search can return fewer than the cap, or nothing at all.
If a feature or its index is missing, the query is refused with
QQL_UNSUPPORTED naming the fix. It never silently falls back to substring
matching.
Whitespace around tokens is legal, and source codes are case-insensitive:
Q : 2 : 1-5, 255 same as Q:2:1-5,255
q:2:255 same as Q:2:255
hm:27 same as HM:27
Inside a quoted term, spaces are part of the term.
query := reference (';' reference)* ';'?
reference := (source ':')? body
body := text search, whole collection
| ':' selector book-wide numbering, B::100
| group (',' group)*
group := primary ':' text search inside a primary
| primary ':' scope ':' text search inside a range
| primary (':' selector)?
scope := integer '~' integer
selector := item (',' item)*
item := integer | integer '-' integer
text := quoted exact substring
| '*' quoted ('~' integer)? similarity, optionally capped
| '?' quoted ('~' integer)? full text, optionally capped
quoted := '"' ... '"' | "'" ... "'"
source := [A-Za-z][A-Za-z0-9_]*
integer := [0-9]+
There are no escape sequences inside a term: the first matching close ends it. That is why both quotes exist — each can carry the other verbatim.
Your shell eats the quotes. This is the most common one by far:
qql q:2:"book" # bash strips the quotes → qql sees q:2:book → error
qql 'q:2:"book"' # correct
qql "q:2:'book'" # also correctThe markers * and ? are glob characters too, so *"book" unquoted can be
rewritten by the shell. Always wrap the whole query in single quotes.
Q:1:1-5:3 is an error. A range cannot be followed by a group colon; see
Groups for the two things you might mean.
Q: and Q:: are errors, not "everything". The flat form needs a
selector: Q::1-10.
A bare number after ; inherits the source. b:1:1;3 is Bukhari chapter
3, not Surah 3. Write b:1:1;q:3 to switch.
~ is not -. Q:1:3-5 selects three ayat; Q:1:3~5:"x" searches them.
See Errors for the full list of codes and what they mean.
Using QQL
Interfaces
Extending