search.mjs
60 lines and 7 functions in DrDustinEdwards/prelum, at commit e389900 on 2026-10-24. This file on GitHub.
Functions
| Function | Lines | Complexity | Covered | CRAP | Mutation |
|---|---|---|---|---|---|
| ftsLiteral | 11 to 13 | 1 | 100% | OkCRAP 1 | No data: mutation testing has not run on this repo yet |
| toMatchExpression | 23 to 33 | 4 | 100% | OkCRAP 4 | No data: mutation testing has not run on this repo yet |
| <anonymous:27> | 27 to 30 | 3 | 100% | OkCRAP 3 | No data: mutation testing has not run on this repo yet |
| fuse | 44 to 65 | 1 | 100% | OkCRAP 1 | No data: mutation testing has not run on this repo yet |
| <anonymous:48> | 48 to 62 | 1 | 100% | OkCRAP 1 | No data: mutation testing has not run on this repo yet |
| <anonymous:49> | 49 to 61 | 2 | 100% | OkCRAP 2 | No data: mutation testing has not run on this repo yet |
| <anonymous:64> | 64 to 64 | 1 | 100% | OkCRAP 1 | No data: mutation testing has not run on this repo yet |
Imports
It imports no other module of this repo. Imported by index.mjs.
Source
/** Reciprocal rank fusion constant. See fuse(). */
export const RRF_K = 60;
/**
* Every token is quoted: fts5 has its own query language, so unquoted input breaks on a hyphen and NEAR silently changes the
* meaning. Doubling embedded quotes is the whole escape.
*
* @param {string} token
* @returns {string}
*/
export function ftsLiteral(token) {
return `"${token.replace(/"/g, '""')}"`;
}
/**
* An FTS5 MATCH expression from already-split words and phrases, every one a literal, all ANDed.
*
* @param {{ terms: string[], phrases?: string[] }} parsed
* @param {boolean} [prefix] when true the LAST term also matches as a prefix, so "cloudf" finds "cloudflare" while someone is
* still typing. Only the last term, because the earlier ones are complete words the moment a space follows them.
* @returns {string | null} null when there is nothing to match on
*/
export function toMatchExpression(parsed, prefix = false) {
/** @type {string[]} */
const parts = [];
for (const phrase of parsed.phrases ?? []) parts.push(ftsLiteral(phrase));
parsed.terms.forEach((term, i) => {
const isLast = i === parsed.terms.length - 1;
parts.push(prefix && isLast ? `${ftsLiteral(term)}*` : ftsLiteral(term));
});
if (parts.length === 0) return null;
return parts.join(" AND ");
}
/**
* Rank based, never score based: bm25 from two indexes with different tokenizers is not comparable on value. k = 60 is the
* original RRF paper's constant. ranks and contributions are parallel to sources.
*
* @template {{ uid: string }} T
* @param {T[][]} lists
* @param {number} [k]
* @returns {Array<{ item: T, score: number, sources: number[], ranks: number[], contributions: number[] }>}
*/
export function fuse(lists, k = RRF_K) {
/** @type {Map<string, { item: T, score: number, sources: number[], ranks: number[], contributions: number[] }>} */
const scores = new Map();
lists.forEach((list, listIndex) => {
list.forEach((item, i) => {
const rank = i + 1;
const contribution = 1 / (k + rank);
const existing = scores.get(item.uid);
if (existing) {
existing.score += contribution;
existing.sources.push(listIndex);
existing.ranks.push(rank);
existing.contributions.push(contribution);
} else {
scores.set(item.uid, { item, score: contribution, sources: [listIndex], ranks: [rank], contributions: [contribution] });
}
});
});
return [...scores.values()].sort((a, b) => b.score - a.score);
}