Skip to main content

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 typeScore per matchPriority
Exact+3 pointsHighest
Lemma+2 pointsHigh
Root+1 pointMedium
Fuzzy+0.5 pointsLowest
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:
  1. exact – Direct text match
  2. lemma – Morphological form match
  3. root – Linguistic root match
  4. fuzzy – Approximate match
  5. 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.

Build docs developers (and LLMs) love