Skip to content

Query Language

Mazhar Ahmed edited this page Aug 28, 2026 · 4 revisions

The QQL 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.

Contents

The shape of a reference

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.

Selecting items

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.

Order and duplicates

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.

Groups: several chapters at once

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

Leaving out the source

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

The source carries forward

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.

Book-wide numbering

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:100 is the 100th hadith of chapter 3; B::100 is canonical number 100 of the book. Where the numbered body of a collection starts, the two agree — Bukhari's canonical 1 is B:1:1 — but for Muslim the first canonical number after the Muqaddima is 93, so M::93 == M:1:1.

Searching

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

Scoping a search

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

Exact search — always available

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.

Full-text search — ?"term"

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 '

Similarity search — *"term"

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

Capping and scoring

~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 and case

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.

Grammar

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.

Common mistakes

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 correct

The 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.

Clone this wiki locally