//#region src/core/hybrid-search.ts
function tokenize(s) {
	return s.toLowerCase().split(/[^a-z0-9]+/).filter((t) => t.length > 1);
}
/** Rank doc ids by query-term overlap (term frequency), dropping zero-overlap docs. */
function keywordRank(query, docs) {
	const qTokens = new Set(tokenize(query));
	if (qTokens.size === 0) return [];
	return docs.map((d) => {
		const docTokens = tokenize(d.text);
		let score = 0;
		for (const t of docTokens) if (qTokens.has(t)) score++;
		return {
			id: d.id,
			score
		};
	}).filter((s) => s.score > 0).sort((a, b) => b.score - a.score).map((s) => s.id);
}
/** Reciprocal Rank Fusion of multiple ranked id-lists. Higher score = better. */
function reciprocalRankFusion(rankings, k = 60) {
	const scores = /* @__PURE__ */ new Map();
	for (const ranking of rankings) ranking.forEach((id, rank) => {
		scores.set(id, (scores.get(id) ?? 0) + 1 / (k + rank));
	});
	return [...scores.entries()].map(([id, score]) => ({
		id,
		score
	})).sort((a, b) => b.score - a.score);
}
/** Hybrid search: fuse keyword ranking with an optional vector ranking. */
function hybridSearch(query, docs, opts = {}) {
	const rankings = [keywordRank(query, docs)];
	if (opts.vectorRanking && opts.vectorRanking.length > 0) rankings.push(opts.vectorRanking);
	const fused = reciprocalRankFusion(rankings, opts.k ?? 60);
	return opts.limit ? fused.slice(0, opts.limit) : fused;
}
//#endregion
export { hybridSearch as t };

//# sourceMappingURL=hybrid-search-BmHttLrR.js.map