How Context Hub Ranks and Scores Search Results: A Deep Dive into the Three-Layer Scoring Model
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. 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. 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.0tags: 2.0description: 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). 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) 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 (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), 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 (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 after quality scoring (line 91).
How BM25 Indexing Works in Context Hub
The BM25 implementation in 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:
chub search "api client" --json | jq '.[] | {id: .id, score: ._score, quality: ._qualityScore, source: .source}'
Inspect a specific entry's stored metrics:
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:
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.jsthat combines lexical relevance, quality metrics, and source authority. - BM25 field-weighted search in
cli/src/lib/bm25.jsprioritizes 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 formulaterm_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
--jsonoutput, 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). 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 (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, 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 and 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.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →