Documentation Index
Fetch the complete documentation index at: https://mintlify.com/adelpro/quran-search-engine/llms.txt
Use this file to discover all available pages before exploring further.
The Quran Search Engine uses a weighted scoring system to rank search results by relevance. Each match type receives a different point value based on its precision.
Score weights
The scoring system assigns points based on match quality:
| Match type | Score per match | Priority |
|---|
| Exact | +3 points | Highest |
| Lemma | +2 points | High |
| Root | +1 point | Medium |
| Fuzzy | +0.5 points | Lowest |
Scores accumulate for each matched token in a verse. A verse matching multiple tokens will have a higher total score.
How scoring works
The computeScore() function evaluates each verse against the search query:
export const computeScore = <TVerse extends VerseInput>(
verse: TVerse,
cleanQuery: string,
morphologyMap: Map<number, MorphologyAya>,
wordMap: WordMap,
options: AdvancedSearchOptions,
mapEntry?: { lemma?: string; root?: string },
fuseMatches?: readonly FuseResultMatch[],
): ScoredVerse<TVerse> => {
let score = 0;
let matchType: MatchType = 'none';
let matchedTokens: string[] = [];
const tokenTypes: Record<string, MatchType> = {};
const queryTokens = cleanQuery.split(/\s+/);
// Check each token...
};
Scoring process
For each token in the query:
1. Check for exact matches (weight: 3)
const textMatches = getPositiveTokens(
verse,
'text',
undefined,
undefined,
token,
morphologyMap,
);
if (textMatches.length > 0) {
score += textMatches.length * 3;
if (matchType === 'none') matchType = 'exact';
matchedTokens.push(...textMatches);
textMatches.forEach((t) => (tokenTypes[t] = 'exact'));
}
What happens:
- Search for the token in the verse text
- Add 3 points for each occurrence
- Upgrade
matchType to 'exact' if this is the first match
- Track matched tokens for highlighting
2. Check for lemma matches (weight: 2)
if (options.lemma && entry.lemma) {
const lemmaMatches = getPositiveTokens(
verse,
'lemma',
entry.lemma,
undefined,
token,
morphologyMap,
);
if (lemmaMatches.length > 0) {
score += lemmaMatches.length * 2;
if (matchType !== 'exact') matchType = 'lemma';
matchedTokens.push(...lemmaMatches);
lemmaMatches.forEach((t) => {
if (!tokenTypes[t]) tokenTypes[t] = 'lemma';
});
}
}
What happens:
- Look up the token’s lemma in the word map
- Search for that lemma in the verse’s morphology
- Add 2 points for each lemma match
- Set
matchType to 'lemma' only if no exact matches exist
3. Check for root matches (weight: 1)
if (options.root && entry.root) {
const rootMatches = getPositiveTokens(
verse,
'root',
undefined,
entry.root,
token,
morphologyMap,
wordMap,
);
if (rootMatches.length > 0) {
score += rootMatches.length * 1;
if (matchType !== 'exact' && matchType !== 'lemma') matchType = 'root';
matchedTokens.push(...rootMatches);
rootMatches.forEach((t) => {
if (!tokenTypes[t]) tokenTypes[t] = 'root';
});
}
}
What happens:
- Look up the token’s root in the word map
- Search for that root in the verse’s morphology
- Add 1 point for each root match
- Set
matchType to 'root' only if no exact or lemma matches exist
4. Fuzzy matches as fallback (weight: 0.5)
if (matchType === 'none' && fuseMatches && fuseMatches.length > 0) {
matchType = 'fuzzy';
const fuzzyTokens: string[] = [];
fuseMatches.forEach((match) => {
const { key, indices } = match;
if (!key || !indices) return;
const sourceText = (verse as Record<string, unknown>)[key];
if (typeof sourceText === 'string') {
indices.forEach(([start, end]) => {
const token = sourceText.substring(start, end + 1);
if (token) {
fuzzyTokens.push(token);
tokenTypes[token] = 'fuzzy';
}
});
}
});
if (fuzzyTokens.length > 0) {
matchedTokens = [...matchedTokens, ...fuzzyTokens];
score += fuzzyTokens.length * 0.5;
}
}
What happens:
- Only applied if no exact, lemma, or root matches were found
- Extract matched tokens from Fuse.js match indices
- Add 0.5 points for each fuzzy token
- Set
matchType to 'fuzzy'
Fuzzy matches only contribute to scoring when they’re the only type of match found. They serve as a fallback layer.
Match type hierarchy
The matchType field represents the best match quality for a verse:
let matchType: MatchType = 'none';
// Upgraded in order of quality:
if (exactMatches) matchType = 'exact'; // Highest
if (lemmaMatches && matchType !== 'exact') matchType = 'lemma';
if (rootMatches && matchType !== 'exact' && matchType !== 'lemma') matchType = 'root';
if (fuzzyMatches && matchType === 'none') matchType = 'fuzzy'; // Lowest
Priority order:
exact – Direct text match
lemma – Morphological form match
root – Linguistic root match
fuzzy – Approximate match
none – No match
Scoring examples
Example 1: Single exact match
Query: الله
Verse: Contains الله once in the text
Exact matches: 1
Score: 1 × 3 = 3
matchType: 'exact'
Example 2: Multiple exact matches
Query: الله الرحمن
Verse: Contains both الله (2 times) and الرحمن (1 time)
Exact matches: الله (2) + الرحمن (1) = 3 total
Score: 3 × 3 = 9
matchType: 'exact'
Example 3: Mixed match types
Query: الله الرحمن
Verse: Contains الله exactly (1 time) and a lemma match for الرحمن (1 time)
Exact matches: 1 × 3 = 3
Lemma matches: 1 × 2 = 2
Total score: 5
matchType: 'exact' (best type present)
Example 4: Lemma-only match
Query: صلى
Verse: Contains different forms of the lemma صلى (e.g., يصلون, صلاة) 2 times
Lemma matches: 2 × 2 = 4
Score: 4
matchType: 'lemma'
Example 5: Fuzzy fallback
Query: الرحمان (typo)
Verse: Contains الرحمن (fuzzy match)
Fuzzy matches: 1 × 0.5 = 0.5
Score: 0.5
matchType: 'fuzzy'
Higher scores appear first in search results. A verse with score: 9 ranks above one with score: 4.
Final output
Each result includes complete scoring metadata:
export type ScoredVerse<TVerse> = TVerse & {
matchScore: number; // Total accumulated score
matchType: MatchType; // Best match type (exact > lemma > root > fuzzy)
matchedTokens: string[]; // Deduplicated tokens for highlighting
tokenTypes: Record<string, MatchType>; // Match type per token
};
Example result:
{
gid: 1,
sura_id: 1,
aya_id: 1,
standard: "بسم الله الرحمن الرحيم",
matchScore: 6,
matchType: "exact",
matchedTokens: ["الله", "الرحمن"],
tokenTypes: {
"الله": "exact",
"الرحمن": "exact"
}
}
Using scores in your UI
You can use the scoring data to:
- Sort results by relevance (done automatically)
- Display match quality badges (exact, lemma, root, fuzzy)
- Highlight matched text using
matchedTokens and tokenTypes
- Filter by match type (e.g., show only exact matches)
- Show score values for debugging or transparency
The search response also includes aggregate counts by match type in the counts field.