# How Context Hub Ranks and Scores Search Results: A Deep Dive into the Three-Layer Scoring Model

> Discover how Context Hub ranks search results using its three-layer scoring model. Learn about BM25 lexical matching, description quality, and source authority for superior search.

- Repository: [Andrew Ng/context-hub](https://github.com/andrewyng/context-hub)
- Tags: deep-dive
- Published: 2026-03-20

---

**Context Hub uses a three-layer scoring model that combines field-weighted BM25 lexical matching, description-quality multipliers, and source-authority boosts to rank search results.**

The `andrewyng/context-hub` repository implements a sophisticated search ranking system designed to surface the most relevant context entries. Unlike simple keyword matching, the system evaluates term relevance, document quality, and source credibility to produce a composite score that determines result ordering.

## The Three-Layer Scoring Architecture

Context Hub's ranking pipeline processes queries through sequential scoring layers, each adding precision to the final ranking.

### Layer 1: Lexical and BM25 Scoring

The foundation of the ranking system resides in `searchEntries()` within [`cli/src/lib/registry.js`](https://github.com/andrewyng/context-hub/blob/main/cli/src/lib/registry.js). When a query is executed, the engine first normalizes the input and determines whether to use the pre-built BM25 index or a fallback lexical matcher.

**BM25 Index Path:**
If an index exists (created during `chub build`), the engine invokes `bm25Search()` in [`cli/src/lib/bm25.js`](https://github.com/andrewyng/context-hub/blob/main/cli/src/lib/bm25.js). This implementation applies a **field-weighted BM25 algorithm** where each document is tokenized into four distinct fields with assigned weights:

- `id`: **4.0** (highest priority)
- `name`: **3.0**
- `tags`: **2.0**
- `description`: **1.0** (lowest priority)

For every query term, the BM25 formula is calculated per field, and the resulting score is multiplied by the field weight (see the `runSearch` loop, lines 62-68 in [`bm25.js`](https://github.com/andrewyng/context-hub/blob/main/bm25.js)). Results are then sorted by the composite BM25 score.

**Fallback Lexical Matcher:**
When no index is present, the system falls back to a heuristic scoring method (lines 70-84 in [`registry.js`](https://github.com/andrewyng/context-hub/blob/main/registry.js)) that assigns points based on match precision:

- Exact ID match: **+100**
- ID contains query: **+50**
- Exact name match: **+80**
- Name contains query: **+40**
- Per-word boosts: ID (+10), name (+10), description (+5), tags (+15)

Both paths populate a `Map` structure mapping `resultByKey` to objects containing the entry and its base `_score`.

### Layer 2: Lexical Boost Layer

After establishing the base candidate set, Context Hub applies an additional **lexical-variant scan** via `scoreEntryLexicalBoost` in [`cli/src/lib/registry.js`](https://github.com/andrewyng/context-hub/blob/main/cli/src/lib/registry.js) (lines 42-52). This layer handles compacted comparisons where the query is normalized (lowercased with non-alphanumeric characters stripped) and matched against compacted `id` and `name` variants using `scoreCompactCandidate` (lines 56-80).

This step provides finer-grained scoring adjustments for partial matches, prefixes, and fuzzy matches, adding incremental boosts to the existing BM25 or keyword base scores.

### Layer 3: Quality and Authority Multipliers

The final ranking adjustments apply multipliers based on document quality and source credibility.

**Description-Quality Multiplier:**
During the build step ([`cli/src/commands/build.js`](https://github.com/andrewyng/context-hub/blob/main/cli/src/commands/build.js)), each entry receives a deterministic `_qualityScore` ranging from 0 to 10 based on a rubric evaluating description length, presence of code blocks, and tag count. The final ranking formula applies this as a multiplier:

```

final_score = term_relevance * (1 + _qualityScore / 20) * source_boost

```

This calculation is documented in the design specification at [`docs/features/search-ranking.md`](https://github.com/andrewyng/context-hub/blob/main/docs/features/search-ranking.md) (lines 71-73).

**Source-Authority Boost:**
Entries receive additional multipliers based on their source classification, as defined in the ranking design:

| Source | Multiplier |
|--------|------------|
| `maintainer` (library author) | 1.3 |
| `official` | 1.2 |
| `community` | 1.0 |

The `searchEntries()` function applies this multiplier in [`registry.js`](https://github.com/andrewyng/context-hub/blob/main/registry.js) after quality scoring (line 91).

## How BM25 Indexing Works in Context Hub

The BM25 implementation in [`cli/src/lib/bm25.js`](https://github.com/andrewyng/context-hub/blob/main/cli/src/lib/bm25.js) creates an inverted index during the build process, tokenizing each entry's fields and calculating inverse document frequencies (IDF) and term frequencies (TF). The search phase uses these pre-computed statistics to calculate relevance scores in milliseconds.

The field-weighting system prioritizes matches in identifiers and names over description text, ensuring that a query matching an entry's ID receives significantly more weight than the same word appearing in a lengthy description.

## Inspecting Search Scores via CLI and API

Context Hub exposes the internal scoring mechanics through both command-line and programmatic interfaces.

### CLI Search with Score Inspection

View the raw composite scores and component signals using the `--json` flag:

```bash
chub search "api client" --json | jq '.[] | {id: .id, score: ._score, quality: ._qualityScore, source: .source}'

```

Inspect a specific entry's stored metrics:

```bash
chub get my-lib/api-client --json | jq '{id: .id, score: ._score, quality: ._qualityScore, source: .source}'

```

### Programmatic Search (Node.js)

Access the ranking engine directly in Node.js applications:

```javascript
import { searchEntries } from './cli/src/lib/registry.js';

const query = 'streaming auth';
const filters = { tags: 'node' };

const results = searchEntries(query, filters);

results.forEach(r => {
  console.log(`${r.id}\tScore:${r._score}\tQuality:${r._qualityScore || 0}\tSource:${r.source}`);
});

```

## Summary

- Context Hub implements a **three-layer scoring model** in [`cli/src/lib/registry.js`](https://github.com/andrewyng/context-hub/blob/main/cli/src/lib/registry.js) that combines lexical relevance, quality metrics, and source authority.
- **BM25 field-weighted search** in [`cli/src/lib/bm25.js`](https://github.com/andrewyng/context-hub/blob/main/cli/src/lib/bm25.js) prioritizes ID and name matches over description text with weights of 4.0, 3.0, 2.0, and 1.0 respectively.
- A **fallback lexical matcher** provides heuristic scoring when no BM25 index exists, assigning point values for exact and partial matches.
- **Description-quality multipliers** derived from `_qualityScore` (0-10) boost well-documented entries by up to 50% using the formula `term_relevance * (1 + _qualityScore / 20)`.
- **Source-authority boosts** tier results by provenance: maintainer (1.3×), official (1.2×), and community (1.0×).
- The CLI exposes raw scores via `--json` output, enabling debugging and transparency in the ranking process.

## Frequently Asked Questions

### How does Context Hub handle searches when no BM25 index is built?

When the BM25 index is unavailable, Context Hub falls back to a **lexical matching heuristic** in `searchEntries()` (lines 70-84 of [`cli/src/lib/registry.js`](https://github.com/andrewyng/context-hub/blob/main/cli/src/lib/registry.js)). This method assigns static point values for exact matches (+100 for ID, +80 for name) and partial containment matches (+50 for ID, +40 for name), plus per-word boosts across all fields. While less sophisticated than BM25, this ensures search functionality works immediately without requiring a build step.

### What is the formula for calculating the final relevance score?

The final composite score follows the formula documented in [`docs/features/search-ranking.md`](https://github.com/andrewyng/context-hub/blob/main/docs/features/search-ranking.md) (lines 71-73):

```

final_score = term_relevance * (1 + _qualityScore / 20) * source_boost

```

Here, `term_relevance` represents the base BM25 or lexical score, `(1 + _qualityScore / 20)` applies a quality multiplier ranging from 1.0 to 1.5, and `source_boost` applies the authority tier (1.3, 1.2, or 1.0).

### Why do ID and name matches receive higher weights than description matches?

Context Hub prioritizes **identifier and name fields** (weights 4.0 and 3.0 respectively) over descriptions (weight 1.0) in the BM25 implementation ([`cli/src/lib/bm25.js`](https://github.com/andrewyng/context-hub/blob/main/cli/src/lib/bm25.js), lines 62-68) because matches in structured identifiers indicate higher semantic relevance. A query matching an entry's exact ID or name typically signals precise intent, whereas the same term appearing in a lengthy description may constitute incidental word overlap. This weighting strategy reduces noise and surfaces the most definitionally relevant entries first.

### Can I customize the field weights or source boost multipliers?

Currently, the **field weights** (4.0, 3.0, 2.0, 1.0) and **source boosts** (1.3, 1.2, 1.0) are hardcoded constants in [`cli/src/lib/bm25.js`](https://github.com/andrewyng/context-hub/blob/main/cli/src/lib/bm25.js) and [`cli/src/lib/registry.js`](https://github.com/andrewyng/context-hub/blob/main/cli/src/lib/registry.js) respectively. The architecture does not expose runtime configuration for these parameters, as they are tuned to balance precision and recall across the context library ecosystem. However, the open-source nature of the repository allows developers to modify these constants in the source code and rebuild the CLI if specific weighting schemes are required for specialized deployments.