How to Implement Custom Score Functions for Relevance Tuning in FlexSearch
FlexSearch allows you to control search result ordering by providing a custom score function during index creation that maps documents to numeric relevance buckets where lower values indicate higher priority.
The nextapps-de/flexsearch library stores this scoring function on the Index instance and invokes it during the indexing process for every term, partial match, and contextual match. This architecture lets you inject domain-specific relevance signals—such as document priority, category hierarchies, or external popularity metrics—directly into the inverted index structure.
How Custom Scoring Works Internally
When you initialize an index with a custom score function, FlexSearch binds the function to this.score in the Index constructor (src/index.js at line 100). During document insertion (src/index/add.js), the library checks for the presence of this function and calls it for each token processed.
The scoring mechanism operates at three specific insertion points:
- Full terms (lines 59–61):
this.score(content, term, term_index, null, 0) - Partial matches (lines 100–102):
this.score(content, term, term_index, token, partial_index) - Contextual matches (lines 167–169):
this.score(content, keyword, term_index, term, context_index)
The numeric value returned must fall within the range 0 to (resolution - 1), where 0 represents the highest possible relevance. FlexSearch uses these values as keys in internal keystore buckets, iterating from lowest to highest during result retrieval to produce relevance-sorted output.
Score Function Signature and Type Definitions
According to the type definitions in src/type.js (around line 19), the custom score callback must conform to this signature:
score?: (
content: string[],
term: string,
term_index: number,
partial: string | null,
partial_index: number
) => number | undefined
Parameter breakdown:
content: The array of tokens produced by the internal tokenizerterm: The current term being indexedterm_index: The position of the term within the content arraypartial: The partial string (for fuzzy/partial matching) ornullfor exact matchespartial_index: The index of the partial match
The function executes in the context of the Index instance, allowing access to this.resolution and this.doc (the current document being indexed).
Practical Implementation Examples
Priority-Based Document Ranking
Map a numeric priority field (1–5) onto the resolution range to ensure high-priority documents surface first:
const index = new FlexSearch.Index({
resolution: 10,
score: function(content, term, term_index, partial, partial_index) {
// Access the current document via context
const priority = this.doc?.priority ?? 3;
const maxScore = this.resolution - 1;
// Map priority 1 (high) → 0, priority 5 (low) → 9
return Math.round(((priority - 1) / 4) * maxScore);
}
});
// Add documents with priority metadata
index.add({ id: 1, title: "Critical alert", priority: 1 });
index.add({ id: 2, title: "General notice", priority: 4 });
// Search results will prioritize id:1 over id:2
index.search("alert").then(console.log);
Label-Driven Relevance Ordering
Use categorical labels (e.g., "high", "medium", "low") to deterministically rank results without arithmetic calculations:
const labelOrder = { high: 0, medium: 1, low: 2 };
const index = new FlexSearch.Index({
resolution: 3,
score: function(content, term, term_index, partial, partial_index) {
const label = this.doc?.label ?? "medium";
return labelOrder[label] ?? 1;
}
});
index.add({ id: 1, title: "Security patch", label: "high" });
index.add({ id: 2, title: "Typos fix", label: "low" });
Async Scoring with External Metrics
Enable async: true to incorporate live data such as click-through rates or popularity scores:
const index = new FlexSearch.Index({
resolution: 20,
async: true,
score: async function(content, term, term_index, partial, partial_index) {
const popularity = await fetchPopularityScore(this.doc.id);
// Map 0-100 popularity to 0-19 score (inverted so popular = lower score)
return Math.floor((100 - popularity) / 5);
}
});
await index.add({ id: 1, title: "Trending topic" });
const results = await index.search("topic");
Token-Level Relevance Tuning
Distinguish between exact matches and partial matches to boost precision:
const index = new FlexSearch.Index({
resolution: 5,
tokenize: "full",
score: (content, term, termIndex, partial, partialIdx) => {
if (!partial) return 0; // Exact match gets highest relevance
// Penalize longer partials slightly
return Math.min(partial.length, 4);
}
});
Summary
- Custom score functions in FlexSearch are defined via the
scoreoption in the Index constructor and stored atthis.score(src/index.js). - The function receives five parameters (
content,term,term_index,partial,partial_index) and must return a number between0andresolution - 1, where0indicates maximum relevance. - FlexSearch invokes the scorer during term insertion (
src/index/add.js) for full terms, partials, and contextual matches, using the result to build sorted internal buckets. - Async scoring is supported when the
async: trueoption is enabled, allowing integration with external APIs or databases. - You can access the current document via
this.docand resolution settings viathis.resolutionto implement dynamic, context-aware relevance calculations.
Frequently Asked Questions
What is the valid range for custom score values?
Custom score functions must return integers between 0 and (resolution - 1). The resolution option defaults to 9 in FlexSearch, meaning valid scores are 0 through 8. Value 0 represents the highest relevance, while higher numbers indicate decreasing relevance. If your function returns values outside this range, the indexing behavior becomes unpredictable.
Can I use async operations in my custom score function?
Yes, provided you set async: true in the Index configuration. When enabled, FlexSearch will await Promise-based score calculations during the indexing process. This allows you to query external databases, fetch popularity metrics from APIs, or perform asynchronous computations. Without the async flag, returning a Promise will cause incorrect numeric coercion.
How does FlexSearch handle documents with the same score?
Documents receiving identical scores are grouped into the same internal bucket. During result retrieval, FlexSearch iterates buckets from lowest score (most relevant) to highest. Within a single bucket, document order typically follows insertion sequence, though this is implementation-dependent. For deterministic ordering, ensure unique scores or implement secondary sorting in your application layer.
Where is the score function stored in the FlexSearch source code?
The score function is stored as this.score on the Index instance, assigned in src/index.js at line 100. The actual invocation occurs in src/index/add.js at three specific locations: lines 59–61 for full terms, lines 100–102 for partial matches, and lines 167–169 for contextual matches. Type definitions specifying the expected function signature are documented in src/type.js around line 19.
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 →