@wcs-colab/plugin-fuzzy-phrase 3.1.16-custom.newbase.30 → 3.1.16-custom.newbase.32
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +55 -20
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +55 -20
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
package/dist/index.cjs
CHANGED
|
@@ -179,10 +179,7 @@ function filterCandidatesByScore(candidatesMap, minScore) {
|
|
|
179
179
|
}
|
|
180
180
|
|
|
181
181
|
// src/scoring.ts
|
|
182
|
-
function
|
|
183
|
-
const phrases = [];
|
|
184
|
-
const queryTokens = originalQueryTokens;
|
|
185
|
-
const wordMatches = [];
|
|
182
|
+
function buildCandidateLookup(candidatesMap) {
|
|
186
183
|
const candidateLookup = /* @__PURE__ */ new Map();
|
|
187
184
|
for (const [queryToken, candidates] of candidatesMap.entries()) {
|
|
188
185
|
for (const candidate of candidates) {
|
|
@@ -201,6 +198,19 @@ function findPhrasesInDocument(documentTokens, candidatesMap, config, documentFr
|
|
|
201
198
|
return b.candidate.score - a.candidate.score;
|
|
202
199
|
});
|
|
203
200
|
}
|
|
201
|
+
return candidateLookup;
|
|
202
|
+
}
|
|
203
|
+
function buildQueryTokenCounts(queryTokens) {
|
|
204
|
+
const queryTokenCounts = /* @__PURE__ */ new Map();
|
|
205
|
+
for (const token of queryTokens) {
|
|
206
|
+
queryTokenCounts.set(token, (queryTokenCounts.get(token) || 0) + 1);
|
|
207
|
+
}
|
|
208
|
+
return queryTokenCounts;
|
|
209
|
+
}
|
|
210
|
+
function findPhrasesInDocument(documentTokens, candidatesMap, config, documentFrequency, totalDocuments, originalQueryTokens, candidateLookup, queryTokenCounts) {
|
|
211
|
+
const phrases = [];
|
|
212
|
+
const queryTokens = originalQueryTokens;
|
|
213
|
+
const wordMatches = [];
|
|
204
214
|
for (let i = 0; i < documentTokens.length; i++) {
|
|
205
215
|
const docWord = documentTokens[i];
|
|
206
216
|
const matches = candidateLookup.get(docWord);
|
|
@@ -226,8 +236,9 @@ function findPhrasesInDocument(documentTokens, candidatesMap, config, documentFr
|
|
|
226
236
|
documentFrequency,
|
|
227
237
|
totalDocuments,
|
|
228
238
|
wordMatches,
|
|
229
|
-
documentTokens
|
|
230
|
-
|
|
239
|
+
documentTokens,
|
|
240
|
+
queryTokenCounts
|
|
241
|
+
// OPTIMIZATION B: Pass pre-built queryTokenCounts
|
|
231
242
|
);
|
|
232
243
|
if (phrase && phrase.words.length > 0) {
|
|
233
244
|
phrases.push(phrase);
|
|
@@ -237,13 +248,9 @@ function findPhrasesInDocument(documentTokens, candidatesMap, config, documentFr
|
|
|
237
248
|
const filteredPhrases = phrases.filter((p) => p.words.length >= minTokensRequired);
|
|
238
249
|
return deduplicatePhrases(filteredPhrases);
|
|
239
250
|
}
|
|
240
|
-
function buildPhraseFromPosition(wordMatches, startIndex, queryTokens, config, documentFrequency, totalDocuments, allWordMatches, documentTokens) {
|
|
251
|
+
function buildPhraseFromPosition(wordMatches, startIndex, queryTokens, config, documentFrequency, totalDocuments, allWordMatches, documentTokens, queryTokenCounts) {
|
|
241
252
|
const startMatch = wordMatches[startIndex];
|
|
242
253
|
const phraseWords = [startMatch];
|
|
243
|
-
const queryTokenCounts = /* @__PURE__ */ new Map();
|
|
244
|
-
for (const token of queryTokens) {
|
|
245
|
-
queryTokenCounts.set(token, (queryTokenCounts.get(token) || 0) + 1);
|
|
246
|
-
}
|
|
247
254
|
const matchedCounts = /* @__PURE__ */ new Map();
|
|
248
255
|
matchedCounts.set(startMatch.queryToken, 1);
|
|
249
256
|
const gapWords = [];
|
|
@@ -437,6 +444,19 @@ function normalizeText(text) {
|
|
|
437
444
|
function tokenize(text) {
|
|
438
445
|
return normalizeText(text).split(/\s+/).filter((token) => token.length > 0);
|
|
439
446
|
}
|
|
447
|
+
function buildReducedVocabularyFromDocs(candidateDocIds, docs) {
|
|
448
|
+
const reducedVocab = /* @__PURE__ */ new Set();
|
|
449
|
+
for (const docId of candidateDocIds) {
|
|
450
|
+
const doc = docs[docId];
|
|
451
|
+
if (!doc?.normalized_content)
|
|
452
|
+
continue;
|
|
453
|
+
const tokens = doc.normalized_content.split(/\s+/).filter((token) => token.length > 0);
|
|
454
|
+
for (const token of tokens) {
|
|
455
|
+
reducedVocab.add(token);
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
return reducedVocab;
|
|
459
|
+
}
|
|
440
460
|
function searchQPS(term, qpsIndex, tokenizer, properties, config, language) {
|
|
441
461
|
const tokens = tokenizer.tokenize(term, language);
|
|
442
462
|
if (tokens.length === 0) {
|
|
@@ -555,9 +575,17 @@ async function searchWithQPSPruning(orama, qpsIndex, pluginState, params, config
|
|
|
555
575
|
return { elapsed: { formatted: "0ms", raw: 0 }, hits: [], count: 0 };
|
|
556
576
|
}
|
|
557
577
|
}
|
|
578
|
+
let docs = {};
|
|
579
|
+
if (orama.data?.docs?.docs) {
|
|
580
|
+
docs = orama.data.docs.docs;
|
|
581
|
+
}
|
|
582
|
+
const vocabStartTime = performance.now();
|
|
583
|
+
const reducedVocabulary = buildReducedVocabularyFromDocs(candidateDocIds, docs);
|
|
584
|
+
const vocabTime = performance.now() - vocabStartTime;
|
|
585
|
+
console.log(`\u{1F4DA} Reduced vocabulary: ${reducedVocabulary.size} words (full: ${vocabulary.size}, reduction: ${(100 * (1 - reducedVocabulary.size / vocabulary.size)).toFixed(1)}%, built in ${vocabTime.toFixed(2)}ms)`);
|
|
558
586
|
const candidatesMap = findAllCandidates(
|
|
559
587
|
queryTokens,
|
|
560
|
-
|
|
588
|
+
reducedVocabulary,
|
|
561
589
|
tolerance,
|
|
562
590
|
pluginState.config.enableSynonyms ? pluginState.synonymMap : void 0,
|
|
563
591
|
pluginState.config.synonymMatchScore
|
|
@@ -565,11 +593,9 @@ async function searchWithQPSPruning(orama, qpsIndex, pluginState, params, config
|
|
|
565
593
|
const filteredFuzzyCandidates = tolerance === 0 ? candidatesMap : filterCandidatesByScore(candidatesMap, pluginState.config.minScore);
|
|
566
594
|
console.log(`\u{1F3AF} Fuzzy candidates: ${Array.from(filteredFuzzyCandidates.values()).reduce((sum, c) => sum + c.length, 0)} total`);
|
|
567
595
|
const phraseStartTime = performance.now();
|
|
596
|
+
const candidateLookup = buildCandidateLookup(filteredFuzzyCandidates);
|
|
597
|
+
const queryTokenCounts = buildQueryTokenCounts(queryTokens);
|
|
568
598
|
const documentMatches = [];
|
|
569
|
-
let docs = {};
|
|
570
|
-
if (orama.data?.docs?.docs) {
|
|
571
|
-
docs = orama.data.docs.docs;
|
|
572
|
-
}
|
|
573
599
|
let docsScored = 0;
|
|
574
600
|
for (const [docId, doc] of Object.entries(docs)) {
|
|
575
601
|
if (!candidateDocIds.has(docId)) {
|
|
@@ -584,7 +610,7 @@ async function searchWithQPSPruning(orama, qpsIndex, pluginState, params, config
|
|
|
584
610
|
if (tokenCache && tokenCache.has(docId)) {
|
|
585
611
|
docTokens = tokenCache.get(docId);
|
|
586
612
|
} else {
|
|
587
|
-
docTokens =
|
|
613
|
+
docTokens = text.split(/\s+/).filter((token) => token.length > 0);
|
|
588
614
|
}
|
|
589
615
|
const phrases = findPhrasesInDocument(
|
|
590
616
|
docTokens,
|
|
@@ -597,7 +623,11 @@ async function searchWithQPSPruning(orama, qpsIndex, pluginState, params, config
|
|
|
597
623
|
},
|
|
598
624
|
pluginState.documentFrequency,
|
|
599
625
|
pluginState.totalDocuments,
|
|
600
|
-
queryTokens
|
|
626
|
+
queryTokens,
|
|
627
|
+
candidateLookup,
|
|
628
|
+
// PHASE 1 OPTIMIZATION A: Pre-built candidate lookup
|
|
629
|
+
queryTokenCounts
|
|
630
|
+
// PHASE 1 OPTIMIZATION B: Pre-built query token counts
|
|
601
631
|
);
|
|
602
632
|
if (phrases.length > 0) {
|
|
603
633
|
const docScore = Math.max(...phrases.map((p) => p.score));
|
|
@@ -806,6 +836,8 @@ async function searchWithFuzzyPhrase(orama, params, language) {
|
|
|
806
836
|
);
|
|
807
837
|
const filteredCandidates = tolerance === 0 ? candidatesMap : filterCandidatesByScore(candidatesMap, state.config.minScore);
|
|
808
838
|
console.log(`\u{1F3AF} Found candidates: ${Array.from(filteredCandidates.values()).reduce((sum, c) => sum + c.length, 0)} total`);
|
|
839
|
+
const candidateLookup = buildCandidateLookup(filteredCandidates);
|
|
840
|
+
const queryTokenCounts = buildQueryTokenCounts(queryTokens);
|
|
809
841
|
const documentMatches = [];
|
|
810
842
|
console.log("\u{1F50D} DEBUG orama.data structure:", {
|
|
811
843
|
dataKeys: Object.keys(orama.data || {}),
|
|
@@ -862,8 +894,11 @@ async function searchWithFuzzyPhrase(orama, params, language) {
|
|
|
862
894
|
},
|
|
863
895
|
state.documentFrequency,
|
|
864
896
|
state.totalDocuments,
|
|
865
|
-
queryTokens
|
|
866
|
-
|
|
897
|
+
queryTokens,
|
|
898
|
+
candidateLookup,
|
|
899
|
+
// PHASE 1 OPTIMIZATION A: Pre-built candidate lookup
|
|
900
|
+
queryTokenCounts
|
|
901
|
+
// PHASE 1 OPTIMIZATION B: Pre-built query token counts
|
|
867
902
|
);
|
|
868
903
|
if (phrases.length > 0) {
|
|
869
904
|
const docScore = Math.max(...phrases.map((p) => p.score));
|
package/dist/index.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/fuzzy.ts","../src/candidates.ts","../src/scoring.ts","../src/optimized.ts","../src/index.ts"],"names":["tokenize","normalizeText"],"mappings":"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","sourcesContent":["/**\n * Fuzzy matching utilities using bounded Levenshtein distance\n * \n * This is the same algorithm used by Orama's match-highlight plugin\n * for consistent fuzzy matching behavior.\n */\n\n/**\n * Result of bounded Levenshtein distance calculation\n */\nexport interface BoundedLevenshteinResult {\n /** Whether the distance is within bounds */\n isBounded: boolean;\n /** The actual distance (only valid if isBounded is true) */\n distance: number;\n}\n\n/**\n * Calculate bounded Levenshtein distance between two strings\n * \n * Stops early if distance exceeds the bound for better performance.\n * This is the same algorithm as Orama's internal boundedLevenshtein.\n * \n * @param a - First string\n * @param b - Second string\n * @param bound - Maximum allowed distance\n * @returns Result indicating if strings are within bound and the distance\n */\nexport function boundedLevenshtein(\n a: string,\n b: string,\n bound: number\n): BoundedLevenshteinResult {\n // Quick checks\n if (a === b) {\n return { isBounded: true, distance: 0 };\n }\n\n const aLen = a.length;\n const bLen = b.length;\n\n // If length difference exceeds bound, no need to calculate\n if (Math.abs(aLen - bLen) > bound) {\n return { isBounded: false, distance: bound + 1 };\n }\n\n // Swap to ensure a is shorter (optimization)\n if (aLen > bLen) {\n [a, b] = [b, a];\n }\n\n const m = a.length;\n const n = b.length;\n\n // Use single array instead of matrix (memory optimization)\n let prevRow = new Array(n + 1);\n let currRow = new Array(n + 1);\n\n // Initialize first row\n for (let j = 0; j <= n; j++) {\n prevRow[j] = j;\n }\n\n for (let i = 1; i <= m; i++) {\n currRow[0] = i;\n let minInRow = i;\n\n for (let j = 1; j <= n; j++) {\n const cost = a[i - 1] === b[j - 1] ? 0 : 1;\n\n currRow[j] = Math.min(\n prevRow[j] + 1, // deletion\n currRow[j - 1] + 1, // insertion\n prevRow[j - 1] + cost // substitution\n );\n\n minInRow = Math.min(minInRow, currRow[j]);\n }\n\n // Early termination: if all values in row exceed bound, we're done\n if (minInRow > bound) {\n return { isBounded: false, distance: bound + 1 };\n }\n\n // Swap rows for next iteration\n [prevRow, currRow] = [currRow, prevRow];\n }\n\n const distance = prevRow[n];\n return {\n isBounded: distance <= bound,\n distance\n };\n}\n\n/**\n * Check if a word matches a query token with fuzzy matching\n * \n * @param word - Word from document\n * @param queryToken - Token from search query\n * @param tolerance - Maximum edit distance allowed\n * @returns Match result with score\n */\nexport function fuzzyMatch(\n word: string,\n queryToken: string,\n tolerance: number\n): { matches: boolean; distance: number; score: number } {\n // Exact match\n if (word === queryToken) {\n return { matches: true, distance: 0, score: 1.0 };\n }\n\n // NOTE: Prefix matching removed entirely\n // It was causing false positives (e.g., \"de\" matching \"dedain\", \"desert\")\n // and interfering with tolerance settings. Levenshtein-only is cleaner.\n\n // Fuzzy match with bounded Levenshtein distance\n const result = boundedLevenshtein(word, queryToken, tolerance);\n \n if (result.isBounded) {\n // Score decreases with distance\n // distance 1 = 0.8, distance 2 = 0.6, etc.\n const score = 1.0 - (result.distance * 0.2);\n return {\n matches: true,\n distance: result.distance,\n score: Math.max(0.1, score) // Minimum score of 0.1\n };\n }\n\n return { matches: false, distance: tolerance + 1, score: 0 };\n}\n\n/**\n * Calculate adaptive tolerance based on query length\n * \n * Longer queries get higher tolerance for better fuzzy matching.\n * \n * @param queryTokens - Array of query tokens\n * @param baseTolerance - Base tolerance value\n * @returns Calculated tolerance (always an integer)\n */\nexport function calculateAdaptiveTolerance(\n queryTokens: string[],\n baseTolerance: number\n): number {\n const queryLength = queryTokens.length;\n \n if (queryLength <= 2) {\n return baseTolerance;\n } else if (queryLength <= 4) {\n return baseTolerance + 1;\n } else if (queryLength <= 6) {\n return baseTolerance + 2;\n } else {\n return baseTolerance + 3;\n }\n}\n","/**\r\n * Candidate expansion: Find all possible matches for query tokens\r\n * including exact matches, fuzzy matches, and synonyms\r\n */\r\n\r\nimport { fuzzyMatch } from './fuzzy.js';\r\nimport type { Candidate, SynonymMap } from './types.js';\r\n\r\n/**\r\n * Extract all unique words from the radix tree index\r\n * \r\n * @param radixNode - Root node of the radix tree\r\n * @returns Set of all unique words in the index\r\n */\r\nexport function extractVocabularyFromRadixTree(radixNode: any): Set<string> {\r\n const vocabulary = new Set<string>();\r\n let nodesVisited = 0;\r\n let wordsFound = 0;\r\n \r\n function traverse(node: any, depth: number = 0) {\r\n if (!node) {\r\n return;\r\n }\r\n \r\n nodesVisited++;\r\n \r\n // Check if this node represents a complete word\r\n // e = true means it's an end of a word\r\n if (node.e && node.w && typeof node.w === 'string' && node.w.length > 0) {\r\n vocabulary.add(node.w);\r\n wordsFound++;\r\n }\r\n \r\n // Children can be Map, Array, or Object\r\n if (node.c) {\r\n if (node.c instanceof Map) {\r\n // Map format\r\n for (const [_key, childNode] of node.c) {\r\n traverse(childNode, depth + 1);\r\n }\r\n } else if (Array.isArray(node.c)) {\r\n // Array format: [[key, childNode], ...]\r\n for (const [_key, childNode] of node.c) {\r\n traverse(childNode, depth + 1);\r\n }\r\n } else if (typeof node.c === 'object') {\r\n // Object format: {key: childNode, ...}\r\n for (const childNode of Object.values(node.c)) {\r\n traverse(childNode, depth + 1);\r\n }\r\n }\r\n }\r\n }\r\n \r\n traverse(radixNode);\r\n console.log(`📚 Extracted ${vocabulary.size} words from ${nodesVisited} nodes visited`);\r\n return vocabulary;\r\n}\r\n\r\n/**\r\n * Find all candidate matches for a single query token\r\n * \r\n * @param queryToken - Token from search query\r\n * @param vocabulary - Set of all words in the index\r\n * @param tolerance - Fuzzy matching tolerance\r\n * @param synonyms - Synonym map (optional)\r\n * @param synonymScore - Score multiplier for synonym matches\r\n * @returns Array of candidate matches\r\n */\r\nexport function findCandidatesForToken(\r\n queryToken: string,\r\n vocabulary: Set<string>,\r\n tolerance: number,\r\n synonyms?: SynonymMap,\r\n synonymScore: number = 0.8\r\n): Candidate[] {\r\n const candidates: Candidate[] = [];\r\n const seen = new Set<string>();\r\n\r\n // 1. Check for exact match\r\n if (vocabulary.has(queryToken)) {\r\n candidates.push({\r\n word: queryToken,\r\n type: 'exact',\r\n queryToken,\r\n distance: 0,\r\n score: 1.0\r\n });\r\n seen.add(queryToken);\r\n }\r\n\r\n // 2. Check for fuzzy matches\r\n for (const word of vocabulary) {\r\n if (seen.has(word)) continue;\r\n\r\n const match = fuzzyMatch(word, queryToken, tolerance);\r\n if (match.matches) {\r\n candidates.push({\r\n word,\r\n type: 'fuzzy',\r\n queryToken,\r\n distance: match.distance,\r\n score: match.score\r\n });\r\n seen.add(word);\r\n }\r\n }\r\n\r\n // 3. Check for synonym matches\r\n if (synonyms && synonyms[queryToken]) {\r\n for (const synonym of synonyms[queryToken]) {\r\n if (seen.has(synonym)) continue;\r\n if (vocabulary.has(synonym)) {\r\n candidates.push({\r\n word: synonym,\r\n type: 'synonym',\r\n queryToken,\r\n distance: 0,\r\n score: synonymScore\r\n });\r\n seen.add(synonym);\r\n }\r\n }\r\n }\r\n\r\n return candidates;\r\n}\r\n\r\n/**\r\n * Find candidates for all query tokens\r\n * \r\n * @param queryTokens - Array of tokens from search query\r\n * @param vocabulary - Set of all words in the index\r\n * @param tolerance - Fuzzy matching tolerance\r\n * @param synonyms - Synonym map (optional)\r\n * @param synonymScore - Score multiplier for synonym matches\r\n * @returns Map of query tokens to their candidate matches\r\n */\r\nexport function findAllCandidates(\r\n queryTokens: string[],\r\n vocabulary: Set<string>,\r\n tolerance: number,\r\n synonyms?: SynonymMap,\r\n synonymScore: number = 0.8\r\n): Map<string, Candidate[]> {\r\n const candidatesMap = new Map<string, Candidate[]>();\r\n\r\n for (const token of queryTokens) {\r\n const tokenCandidates = findCandidatesForToken(\r\n token,\r\n vocabulary,\r\n tolerance,\r\n synonyms,\r\n synonymScore\r\n );\r\n candidatesMap.set(token, tokenCandidates);\r\n }\r\n\r\n return candidatesMap;\r\n}\r\n\r\n/**\r\n * Get total number of candidates across all tokens\r\n * \r\n * @param candidatesMap - Map of token to candidates\r\n * @returns Total count of all candidates\r\n */\r\nexport function getTotalCandidateCount(\r\n candidatesMap: Map<string, Candidate[]>\r\n): number {\r\n let total = 0;\r\n for (const candidates of candidatesMap.values()) {\r\n total += candidates.length;\r\n }\r\n return total;\r\n}\r\n\r\n/**\r\n * Filter candidates by minimum score threshold\r\n * \r\n * @param candidatesMap - Map of token to candidates\r\n * @param minScore - Minimum score threshold\r\n * @returns Filtered candidates map\r\n */\r\nexport function filterCandidatesByScore(\r\n candidatesMap: Map<string, Candidate[]>,\r\n minScore: number\r\n): Map<string, Candidate[]> {\r\n const filtered = new Map<string, Candidate[]>();\r\n\r\n for (const [token, candidates] of candidatesMap.entries()) {\r\n const filteredCandidates = candidates.filter(c => c.score >= minScore);\r\n if (filteredCandidates.length > 0) {\r\n filtered.set(token, filteredCandidates);\r\n }\r\n }\r\n\r\n return filtered;\r\n}\r\n","/**\n * Phrase scoring algorithm with semantic weighting\n */\n\nimport type { WordMatch, PhraseMatch, Candidate, GapWord } from './types.js';\n\n/**\n * Configuration for phrase scoring\n */\nexport interface ScoringConfig {\n weights: {\n exact: number;\n fuzzy: number;\n order: number;\n proximity: number;\n density: number;\n semantic: number;\n };\n maxGap: number;\n /** \n * Multiplier for proximity window calculation.\n * proximityWindow = queryTokens.length × proximitySpanMultiplier\n */\n proximitySpanMultiplier: number;\n /**\n * Fuzzy tolerance (Levenshtein distance). When 0, only exact matches.\n */\n tolerance: number;\n}\n\n/**\n * Find all phrase matches in a document\n * \n * @param documentTokens - Tokenized document content (needed to extract gap words)\n * @param candidatesMap - Map of query tokens to their candidates\n * @param config - Scoring configuration\n * @param documentFrequency - Document frequency map for TF-IDF\n * @param totalDocuments - Total number of documents\n * @param originalQueryTokens - Original query tokens array (preserves duplicates like \"de...de\")\n * @returns Array of phrase matches\n */\nexport function findPhrasesInDocument(\n documentTokens: string[],\n candidatesMap: Map<string, Candidate[]>,\n config: ScoringConfig,\n documentFrequency: Map<string, number>,\n totalDocuments: number,\n originalQueryTokens: string[]\n): PhraseMatch[] {\n const phrases: PhraseMatch[] = [];\n // Use original query tokens to preserve duplicates (e.g., \"de maux ... de\")\n const queryTokens = originalQueryTokens;\n\n // Find all word matches in document\n const wordMatches: WordMatch[] = [];\n \n // Build candidateLookup once, sorted by quality (exact first, higher score)\n // This ensures best matches come first when iterating - no per-document sort needed\n const candidateLookup = new Map<string, Array<{queryToken: string, candidate: Candidate}>>();\n for (const [queryToken, candidates] of candidatesMap.entries()) {\n for (const candidate of candidates) {\n if (!candidateLookup.has(candidate.word)) {\n candidateLookup.set(candidate.word, []);\n }\n candidateLookup.get(candidate.word)!.push({ queryToken, candidate });\n }\n }\n // Sort each word's matches by quality: exact first, then by score\n for (const entries of candidateLookup.values()) {\n entries.sort((a, b) => {\n if (a.candidate.type === 'exact' && b.candidate.type !== 'exact') return -1;\n if (b.candidate.type === 'exact' && a.candidate.type !== 'exact') return 1;\n return b.candidate.score - a.candidate.score;\n });\n }\n\n // Iterate documentTokens in position order (0, 1, 2...)\n // Matches at same position come out quality-sorted (from candidateLookup)\n for (let i = 0; i < documentTokens.length; i++) {\n const docWord = documentTokens[i];\n const matches = candidateLookup.get(docWord);\n if (matches) {\n for (const { queryToken, candidate } of matches) {\n wordMatches.push({\n word: docWord,\n queryToken,\n position: i,\n type: candidate.type,\n distance: candidate.distance,\n score: candidate.score\n });\n }\n }\n }\n\n // Build phrases from word matches using sliding window\n for (let i = 0; i < wordMatches.length; i++) {\n const phrase = buildPhraseFromPosition(\n wordMatches,\n i,\n queryTokens,\n config,\n documentFrequency,\n totalDocuments,\n wordMatches,\n documentTokens // Pass document tokens to extract gap words\n );\n \n if (phrase && phrase.words.length > 0) {\n phrases.push(phrase);\n }\n }\n\n // Filter out low-quality single-word matches for multi-word queries\n // This prevents noise from common words like \"de\", \"la\", \"des\" appearing as separate phrases\n const minTokensRequired = queryTokens.length >= 3 ? 2 : 1;\n const filteredPhrases = phrases.filter(p => p.words.length >= minTokensRequired);\n\n // Deduplicate and sort by score\n return deduplicatePhrases(filteredPhrases);\n}\n\n/**\n * Build a phrase starting from a specific word match position\n * \n * @param wordMatches - All word matches in document\n * @param startIndex - Starting index in wordMatches array\n * @param queryTokens - Original query tokens\n * @param config - Scoring configuration\n * @param documentFrequency - Document frequency map\n * @param totalDocuments - Total document count\n * @param allWordMatches - All word matches in document (for density calculation)\n * @param documentTokens - Original document tokens (for gap word extraction)\n * @returns Phrase match or null\n */\nfunction buildPhraseFromPosition(\n wordMatches: WordMatch[],\n startIndex: number,\n queryTokens: string[],\n config: ScoringConfig,\n documentFrequency: Map<string, number>,\n totalDocuments: number,\n allWordMatches: WordMatch[],\n documentTokens: string[]\n): PhraseMatch | null {\n const startMatch = wordMatches[startIndex];\n const phraseWords: WordMatch[] = [startMatch];\n \n // Count occurrences of each token in query (handles duplicate tokens like \"de ... de\")\n const queryTokenCounts = new Map<string, number>();\n for (const token of queryTokens) {\n queryTokenCounts.set(token, (queryTokenCounts.get(token) || 0) + 1);\n }\n \n // Track how many times we've matched each token\n const matchedCounts = new Map<string, number>();\n matchedCounts.set(startMatch.queryToken, 1);\n \n const gapWords: GapWord[] = [];\n let totalGapUsed = 0;\n let totalMatchedTokens = 1;\n\n // Look for nearby matches to complete the phrase\n for (let i = startIndex + 1; i < wordMatches.length; i++) {\n const match = wordMatches[i];\n const lastPos = phraseWords[phraseWords.length - 1].position;\n \n // Skip if same position (can happen with multiple fuzzy matches for same word)\n if (match.position <= lastPos) {\n continue;\n }\n \n const gap = match.position - lastPos - 1;\n\n // Stop if this gap alone exceeds maximum\n if (gap > config.maxGap) {\n break;\n }\n \n // Stop if adding this gap would exceed total maxGap budget\n if (totalGapUsed + gap > config.maxGap) {\n break;\n }\n\n // Check if we still need more of this token (handles duplicates)\n const neededCount = queryTokenCounts.get(match.queryToken) || 0;\n const currentCount = matchedCounts.get(match.queryToken) || 0;\n \n if (currentCount < neededCount) {\n // Track gap words between last match and current match\n for (let pos = lastPos + 1; pos < match.position; pos++) {\n totalGapUsed++;\n gapWords.push({\n word: documentTokens[pos],\n position: pos,\n gapIndex: totalGapUsed\n });\n }\n\n phraseWords.push(match);\n matchedCounts.set(match.queryToken, currentCount + 1);\n totalMatchedTokens++;\n\n // Stop if we have all query tokens (including duplicates)\n if (totalMatchedTokens === queryTokens.length) {\n break;\n }\n }\n }\n\n // Calculate phrase score\n if (phraseWords.length > 0) {\n const coverage = phraseWords.length / queryTokens.length;\n const span = phraseWords[phraseWords.length - 1].position - phraseWords[0].position + 1;\n \n const { score, breakdown } = calculatePhraseScore(\n phraseWords,\n queryTokens,\n config,\n documentFrequency,\n totalDocuments,\n allWordMatches,\n coverage\n );\n\n return {\n words: phraseWords,\n gapWords,\n gapUsed: totalGapUsed,\n coverage,\n startPosition: phraseWords[0].position,\n endPosition: phraseWords[phraseWords.length - 1].position,\n span,\n inOrder: isInOrder(phraseWords, queryTokens),\n score,\n scoreBreakdown: breakdown\n };\n }\n\n return null;\n}\n\n/**\n * Calculate overall phrase score\n * \n * @param phraseWords - Words in the phrase\n * @param queryTokens - Original query tokens\n * @param config - Scoring configuration\n * @param documentFrequency - Document frequency map\n * @param totalDocuments - Total document count\n * @param allWordMatches - All word matches in document (for density calculation)\n * @param coverage - Pre-calculated coverage ratio (phraseWords.length / queryTokens.length)\n * @returns Phrase score (0-1) and detailed component breakdown\n */\nfunction calculatePhraseScore(\n phraseWords: WordMatch[],\n queryTokens: string[],\n config: ScoringConfig,\n documentFrequency: Map<string, number>,\n totalDocuments: number,\n allWordMatches: WordMatch[],\n coverage: number\n): { score: number; breakdown: { base: number; order: number; proximity: number; density: number; semantic: number; coverage: number } } {\n // Base score from word matches\n // Each word contributes: matchScore × typeWeight\n let baseScore = 0;\n for (const word of phraseWords) {\n const weight = word.type === 'exact' ? config.weights.exact :\n word.type === 'fuzzy' ? config.weights.fuzzy : \n config.weights.fuzzy * 0.8; // synonym gets 80% of fuzzy weight\n baseScore += word.score * weight;\n }\n baseScore /= phraseWords.length;\n\n // Order bonus: 1.0 if words appear in query order, 0.5 otherwise\n const inOrder = isInOrder(phraseWords, queryTokens);\n const orderScore = inOrder ? 1.0 : 0.5;\n\n // Proximity bonus (closer words score higher)\n // Short-circuit: skip if maxGap=0, proximity weight is 0, or single-word query (proximity meaningless)\n let proximityScore = 0;\n if (config.maxGap > 0 && config.weights.proximity > 0 && queryTokens.length > 1) {\n const span = phraseWords[phraseWords.length - 1].position - phraseWords[0].position + 1;\n const proximityWindow = queryTokens.length * config.proximitySpanMultiplier;\n proximityScore = Math.max(0, 1.0 - (span / proximityWindow));\n }\n\n // Density: Only applies to single-word queries (measures word repetition in document)\n // For multi-word phrase queries, density is 0 (coverage handles completeness separately)\n let densityScore = 0;\n \n if (queryTokens.length === 1) {\n // Single-word query: reward repetition\n const totalOccurrences = allWordMatches.length;\n // Cap at reasonable maximum to avoid runaway scores\n densityScore = Math.min(1.0, totalOccurrences / 10);\n }\n // For multi-word queries: densityScore stays 0\n // Coverage is applied as a multiplier at the end instead\n\n // Semantic score (TF-IDF based)\n const semanticScore = calculateSemanticScore(\n phraseWords,\n documentFrequency,\n totalDocuments\n );\n\n // Weighted combination\n const weights = config.weights;\n \n // Calculate weighted components\n const weightedBase = baseScore;\n const weightedOrder = orderScore * weights.order;\n const weightedProximity = proximityScore * weights.proximity;\n const weightedDensity = densityScore * weights.density;\n const weightedSemantic = semanticScore * weights.semantic;\n \n const totalScore = weightedBase + weightedOrder + weightedProximity + weightedDensity + weightedSemantic;\n\n // Calculate max possible score\n // FIX: Use actual max base weight (highest of exact/fuzzy) instead of hardcoded 1.0\n // When tolerance=0 or fuzzy weight=0, only exact matches are possible\n const canHaveFuzzyMatches = config.tolerance > 0 && weights.fuzzy > 0;\n const maxBaseWeight = canHaveFuzzyMatches ? Math.max(weights.exact, weights.fuzzy) : weights.exact;\n // Only include proximity in max if it can actually contribute (avoids penalizing scores when maxGap=0 or single-word)\n const effectiveProximityWeight = (config.maxGap > 0 && weights.proximity > 0 && queryTokens.length > 1) ? weights.proximity : 0;\n const maxPossibleScore = maxBaseWeight + weights.order + effectiveProximityWeight + weights.density + weights.semantic;\n \n // Normalize to 0-1 range\n const normalizedScore = totalScore / maxPossibleScore;\n \n // FIX: Apply coverage as a MULTIPLIER for multi-word queries\n // This ensures incomplete matches (2/3) can never outscore complete matches (3/3)\n const coverageMultiplier = queryTokens.length > 1 ? coverage : 1.0;\n const score = normalizedScore * coverageMultiplier;\n\n // Component contributions to the final normalized score (before coverage multiplier)\n const base = weightedBase / maxPossibleScore;\n const order = weightedOrder / maxPossibleScore;\n const proximity = weightedProximity / maxPossibleScore;\n const density = weightedDensity / maxPossibleScore;\n const semantic = weightedSemantic / maxPossibleScore;\n\n return {\n score,\n breakdown: {\n base,\n order,\n proximity,\n density,\n semantic,\n coverage: coverageMultiplier // Show coverage multiplier in breakdown\n }\n };\n}\n\n/**\n * Check if phrase words appear in query order\n * Handles duplicate tokens (e.g., \"de ... de\") by tracking position consumption\n * \n * @param phraseWords - Words in the phrase\n * @param queryTokens - Original query tokens\n * @returns True if in order\n */\nfunction isInOrder(phraseWords: WordMatch[], queryTokens: string[]): boolean {\n // Build array of {token, index} to handle duplicates\n // e.g., [\"de\", \"maux\", ..., \"de\"] → [{token:\"de\", idx:0}, {token:\"maux\", idx:1}, ..., {token:\"de\", idx:7}]\n const tokenPositions = queryTokens.map((token, index) => ({ token, index }));\n \n let lastMatchedIndex = -1;\n \n for (const phraseWord of phraseWords) {\n // Find the first unused position for this token that's after lastMatchedIndex\n let foundIndex = -1;\n for (const pos of tokenPositions) {\n if (pos.token === phraseWord.queryToken && pos.index > lastMatchedIndex) {\n foundIndex = pos.index;\n break;\n }\n }\n \n if (foundIndex === -1) {\n // Token not found in expected position - out of order\n return false;\n }\n \n lastMatchedIndex = foundIndex;\n }\n \n return true;\n}\n\n/**\n * Calculate semantic score using TF-IDF\n * \n * @param phraseWords - Words in the phrase\n * @param documentFrequency - Document frequency map\n * @param totalDocuments - Total document count\n * @returns Semantic score (0-1)\n */\nfunction calculateSemanticScore(\n phraseWords: WordMatch[],\n documentFrequency: Map<string, number>,\n totalDocuments: number\n): number {\n // Handle edge case: no documents\n if (totalDocuments === 0) {\n return 0;\n }\n \n let tfidfSum = 0;\n \n for (const word of phraseWords) {\n const df = documentFrequency.get(word.word) || 1;\n const idf = Math.log(totalDocuments / df);\n tfidfSum += idf;\n }\n \n // Normalize by phrase length\n const avgTfidf = tfidfSum / phraseWords.length;\n \n // Normalize to 0-1 range (assuming max IDF of ~10)\n return Math.min(1.0, avgTfidf / 10);\n}\n\n/**\n * Deduplicate overlapping phrases, keeping highest scoring ones\n * \n * @param phrases - Array of phrase matches\n * @returns Deduplicated phrases sorted by score\n */\nfunction deduplicatePhrases(phrases: PhraseMatch[]): PhraseMatch[] {\n if (phrases.length === 0) return [];\n\n // Sort by score descending\n const sorted = phrases.slice().sort((a, b) => b.score - a.score);\n const result: PhraseMatch[] = [];\n const covered = new Set<number>();\n\n for (const phrase of sorted) {\n // Check if this phrase overlaps with already selected phrases\n let overlaps = false;\n for (let pos = phrase.startPosition; pos <= phrase.endPosition; pos++) {\n if (covered.has(pos)) {\n overlaps = true;\n break;\n }\n }\n\n if (!overlaps) {\n result.push(phrase);\n // Mark positions as covered\n for (let pos = phrase.startPosition; pos <= phrase.endPosition; pos++) {\n covered.add(pos);\n }\n }\n }\n\n return result.sort((a, b) => b.score - a.score);\n}\n","/**\r\n * Optimized Search: QPS Candidate Pruning + Fuzzy Phrase Scoring\r\n * \r\n * This module provides an algorithmic pruning path where:\r\n * 1. QPS (Quantum Proximity Scoring) finds candidate documents quickly (token-level, typo-tolerant)\r\n * 2. Fuzzy Phrase scoring runs only on those candidates (expensive phrase scoring on smaller set)\r\n * \r\n * This dramatically reduces the search space for phrase scoring while maintaining\r\n * the same result quality as full fuzzy phrase search.\r\n */\r\n\r\nimport type { AnyOrama, Results, TypedDocument } from '@wcs-colab/orama';\r\nimport type { FuzzyPhraseConfig, PluginState, DocumentMatch, Candidate } from './types.js';\r\nimport { calculateAdaptiveTolerance } from './fuzzy.js';\r\nimport { findAllCandidates, filterCandidatesByScore, extractVocabularyFromRadixTree } from './candidates.js';\r\nimport { findPhrasesInDocument } from './scoring.js';\r\n\r\n/**\r\n * QPS Index data structure (matches plugin-qps internal format)\r\n */\r\ninterface QPSIndexData {\r\n indexes: Record<string, {\r\n type: string;\r\n node: any; // RadixNode\r\n isArray: boolean;\r\n }>;\r\n stats: Record<string, {\r\n tokenQuantums: Record<string, Record<string, number>>;\r\n tokensLength: Map<string, number>;\r\n }>;\r\n searchableProperties: string[];\r\n searchablePropertiesWithTypes: Record<string, string>;\r\n vectorIndexes: Record<string, any>;\r\n}\r\n\r\n/**\r\n * Configuration for optimized search\r\n */\r\nexport interface OptimizedSearchConfig extends FuzzyPhraseConfig {\r\n /** Maximum number of QPS candidates to consider for phrase scoring */\r\n maxQPSCandidates?: number;\r\n /** Minimum QPS score to include a candidate (0-1, relative to best score) */\r\n minQPSScore?: number;\r\n /** Whether to use exact matching for QPS (faster but no typo tolerance) */\r\n qpsExact?: boolean;\r\n /** QPS tolerance (edit distance for fuzzy matching) */\r\n qpsTolerance?: number;\r\n /** Boost factors per property for QPS scoring */\r\n qpsBoostPerProp?: Record<string, number>;\r\n}\r\n\r\n/**\r\n * Default optimized search configuration\r\n */\r\nconst DEFAULT_OPTIMIZED_CONFIG: Required<Pick<OptimizedSearchConfig, \r\n 'maxQPSCandidates' | 'minQPSScore' | 'qpsExact' | 'qpsTolerance'>> = {\r\n maxQPSCandidates: 100, // Limit phrase scoring to top 100 candidates\r\n minQPSScore: 0.1, // Include candidates with 10%+ of best score\r\n qpsExact: false, // Use fuzzy matching by default\r\n qpsTolerance: 1 // Default tolerance of 1 edit distance\r\n};\r\n\r\n/**\r\n * Normalize text using the same rules as Orama's French tokenizer\r\n * \r\n * CRITICAL: This must match normalizeFrenchText() in Orama's tokenizer exactly\r\n */\r\nexport function normalizeText(text: string): string {\r\n return text\r\n .toLowerCase()\r\n .normalize('NFD')\r\n .replace(/[\\u0300-\\u036f]/g, '') // Remove diacritics\r\n // Replace French elisions (l', d', etc.) with space to preserve word boundaries\r\n .replace(/\\b[ldcjmnst][\\u2018\\u2019\\u201A\\u201B\\u2032\\u2035\\u0027\\u0060\\u00B4](?=\\w)/gi, ' ')\r\n .replace(/[\\u2018\\u2019\\u201A\\u201B\\u2032\\u2035\\u0027\\u0060\\u00B4]/g, '') // Remove remaining apostrophes\r\n .replace(/[\\u201c\\u201d]/g, '\"') // Normalize curly quotes to straight quotes\r\n .replace(/[.,;:!?()[\\]{}\\-—–«»\"\"]/g, ' ') // Remove punctuation (replace with space to preserve word boundaries)\r\n .replace(/\\s+/g, ' ') // Normalize multiple spaces to single space\r\n .trim();\r\n}\r\n\r\n/**\r\n * Tokenize text using normalization matching Orama's French tokenizer\r\n */\r\nexport function tokenize(text: string): string[] {\r\n return normalizeText(text)\r\n .split(/\\s+/)\r\n .filter(token => token.length > 0);\r\n}\r\n\r\n/**\r\n * Call QPS search to get candidate document IDs with scores\r\n * \r\n * This function interfaces directly with QPS's internal search function,\r\n * returning candidate documents ranked by token proximity.\r\n * \r\n * @param term - Search term\r\n * @param qpsIndex - QPS index data\r\n * @param tokenizer - Tokenizer instance from Orama\r\n * @param properties - Properties to search\r\n * @param config - Optimized search configuration\r\n * @returns Array of [docId, score] tuples\r\n */\r\nfunction searchQPS(\r\n term: string,\r\n qpsIndex: QPSIndexData,\r\n tokenizer: any,\r\n properties: string[],\r\n config: OptimizedSearchConfig,\r\n language?: string\r\n): Array<[string, number]> {\r\n const tokens = tokenizer.tokenize(term, language);\r\n \r\n if (tokens.length === 0) {\r\n return [];\r\n }\r\n\r\n // QPS search configuration\r\n const exact = config.qpsExact ?? DEFAULT_OPTIMIZED_CONFIG.qpsExact;\r\n const tolerance = config.qpsTolerance ?? DEFAULT_OPTIMIZED_CONFIG.qpsTolerance;\r\n const boostPerProp = config.qpsBoostPerProp ?? {};\r\n \r\n // Results map: docId -> [score, tokenBitmask]\r\n const resultMap = new Map<string, [number, number]>();\r\n \r\n // Search each property\r\n for (const prop of properties) {\r\n const indexEntry = qpsIndex.indexes[prop];\r\n if (!indexEntry || indexEntry.type !== 'Radix') {\r\n continue;\r\n }\r\n \r\n const radixNode = indexEntry.node;\r\n const stats = qpsIndex.stats[prop];\r\n \r\n if (!radixNode || !stats) {\r\n continue;\r\n }\r\n \r\n const boost = boostPerProp[prop] ?? 1;\r\n \r\n // Search the radix tree for each token\r\n for (let i = 0; i < tokens.length; i++) {\r\n const token = tokens[i];\r\n \r\n // Find matches in radix tree (exact or fuzzy)\r\n const matches = radixNode.find({\r\n term: token,\r\n exact,\r\n tolerance: exact ? 0 : tolerance\r\n });\r\n \r\n // Process matches\r\n for (const [matchedWord, docIds] of Object.entries(matches)) {\r\n if (!Array.isArray(docIds)) continue;\r\n \r\n const isExactMatch = matchedWord === token;\r\n \r\n for (const docId of docIds as string[]) {\r\n const tokensLength = stats.tokensLength.get(docId) || 1;\r\n const quantum = stats.tokenQuantums[docId]?.[matchedWord];\r\n \r\n // Extract occurrence count and position bitmask from quantum\r\n const occurrences = quantum ? (quantum >> 20) : 1;\r\n const posBitmask = quantum ? (quantum & 0xFFFFF) : 0;\r\n \r\n // Calculate score contribution\r\n const scoreContrib = ((occurrences * occurrences) / tokensLength + (isExactMatch ? 1 : 0)) * boost;\r\n \r\n if (!resultMap.has(docId)) {\r\n resultMap.set(docId, [scoreContrib, 1 << i]);\r\n } else {\r\n const [prevScore, prevMask] = resultMap.get(docId)!;\r\n // Add proximity bonus for adjacent tokens\r\n const adjacencyBonus = countSetBits((prevMask >> 1) & (1 << i)) * 2;\r\n resultMap.set(docId, [prevScore + scoreContrib + adjacencyBonus, prevMask | (1 << i)]);\r\n }\r\n }\r\n }\r\n }\r\n }\r\n \r\n // Convert to sorted array\r\n const results = Array.from(resultMap.entries())\r\n .map(([docId, [score]]) => [docId, score] as [string, number])\r\n .sort((a, b) => b[1] - a[1]);\r\n \r\n return results;\r\n}\r\n\r\n/**\r\n * Count set bits in a number (popcount)\r\n */\r\nfunction countSetBits(n: number): number {\r\n let count = 0;\r\n while (n) {\r\n count += n & 1;\r\n n >>= 1;\r\n }\r\n return count;\r\n}\r\n\r\n/**\r\n * Get plugin state from WeakMap\r\n * Note: This function accesses the internal pluginStates WeakMap\r\n */\r\nfunction getPluginState(orama: AnyOrama, pluginStates: WeakMap<AnyOrama, PluginState>): PluginState | undefined {\r\n return pluginStates.get(orama);\r\n}\r\n\r\n/**\r\n * Optimized search combining QPS candidate pruning with fuzzy phrase scoring\r\n * \r\n * This is the main entry point for optimized search. It:\r\n * 1. Uses QPS to quickly find candidate documents (token-level matching)\r\n * 2. Filters to top candidates by QPS score\r\n * 3. Runs full fuzzy phrase scoring only on those candidates\r\n * \r\n * @param orama - Orama instance\r\n * @param qpsIndex - QPS index data (from pluginQPS.getComponents().index)\r\n * @param pluginState - Fuzzy phrase plugin state\r\n * @param params - Search parameters\r\n * @param config - Optimized search configuration\r\n * @param language - Language for tokenization (default: french)\r\n * @returns Search results with fuzzy phrase scoring\r\n */\r\nexport async function searchWithQPSPruning<T extends AnyOrama>(\r\n orama: T,\r\n qpsIndex: QPSIndexData,\r\n pluginState: PluginState,\r\n params: {\r\n term?: string;\r\n properties?: string[];\r\n limit?: number;\r\n tokenCache?: Map<string, string[]>;\r\n },\r\n config: OptimizedSearchConfig = {},\r\n language: string = 'french'\r\n): Promise<Results<TypedDocument<T>>> {\r\n const startTime = performance.now();\r\n \r\n const { term, properties, tokenCache } = params;\r\n \r\n if (!term || typeof term !== 'string') {\r\n return { elapsed: { formatted: '0ms', raw: 0 }, hits: [], count: 0 };\r\n }\r\n\r\n // Use specified property or default from plugin config\r\n const textProperty = (properties && properties[0]) || pluginState.config.textProperty;\r\n const searchProperties = properties || [textProperty];\r\n\r\n // Tokenize query\r\n const queryTokens = tokenize(term);\r\n \r\n if (queryTokens.length === 0) {\r\n return { elapsed: { formatted: '0ms', raw: 0 }, hits: [], count: 0 };\r\n }\r\n\r\n // Calculate tolerance (adaptive or fixed)\r\n const tolerance = pluginState.config.adaptiveTolerance\r\n ? calculateAdaptiveTolerance(queryTokens, pluginState.config.tolerance)\r\n : pluginState.config.tolerance;\r\n\r\n console.log(`🚀 Optimized search: \"${term}\" (${queryTokens.length} tokens, tolerance: ${tolerance})`);\r\n\r\n // STEP 1: Use QPS to find candidate documents\r\n const qpsStartTime = performance.now();\r\n \r\n // Get tokenizer from Orama\r\n const tokenizer = (orama as any).tokenizer;\r\n \r\n const qpsCandidates = searchQPS(\r\n term,\r\n qpsIndex,\r\n tokenizer,\r\n searchProperties,\r\n config,\r\n language\r\n );\r\n \r\n const qpsTime = performance.now() - qpsStartTime;\r\n console.log(`⚡ QPS found ${qpsCandidates.length} candidates in ${qpsTime.toFixed(2)}ms`);\r\n\r\n if (qpsCandidates.length === 0) {\r\n return { elapsed: { formatted: '0ms', raw: 0 }, hits: [], count: 0 };\r\n }\r\n\r\n // STEP 2: Filter to top candidates\r\n const maxCandidates = config.maxQPSCandidates ?? DEFAULT_OPTIMIZED_CONFIG.maxQPSCandidates;\r\n const minScoreRatio = config.minQPSScore ?? DEFAULT_OPTIMIZED_CONFIG.minQPSScore;\r\n \r\n const bestScore = qpsCandidates[0][1];\r\n const minScore = bestScore * minScoreRatio;\r\n \r\n const filteredCandidates = qpsCandidates\r\n .filter(([, score]) => score >= minScore)\r\n .slice(0, maxCandidates);\r\n \r\n console.log(`📋 Filtered to ${filteredCandidates.length} candidates (min score: ${minScore.toFixed(2)})`);\r\n\r\n // STEP 3: Get candidate document IDs as a Set for fast lookup\r\n // Convert to strings since Object.entries returns string keys\r\n const candidateDocIds = new Set(filteredCandidates.map(([docId]) => String(docId)));\r\n\r\n // STEP 4: Find fuzzy candidates for phrase scoring\r\n // Vocabulary is lazily extracted on first search\r\n let vocabulary = pluginState.vocabulary;\r\n \r\n if (vocabulary.size === 0) {\r\n console.log('📚 Vocabulary not initialized - extracting from index...');\r\n try {\r\n const indexData = (orama as any).data?.index;\r\n let radixNode = null;\r\n \r\n if (indexData?.indexes?.[textProperty]?.node) {\r\n radixNode = indexData.indexes[textProperty].node;\r\n } else if (indexData?.[textProperty]?.node) {\r\n radixNode = indexData[textProperty].node;\r\n }\r\n \r\n if (radixNode) {\r\n pluginState.vocabulary = extractVocabularyFromRadixTree(radixNode);\r\n vocabulary = pluginState.vocabulary;\r\n console.log(`📚 Extracted ${vocabulary.size} vocabulary words`);\r\n } else {\r\n console.error('❌ Radix tree not found for vocabulary extraction');\r\n return { elapsed: { formatted: '0ms', raw: 0 }, hits: [], count: 0 };\r\n }\r\n } catch (error) {\r\n console.error('❌ Failed to extract vocabulary:', error);\r\n return { elapsed: { formatted: '0ms', raw: 0 }, hits: [], count: 0 };\r\n }\r\n }\r\n\r\n // Find candidates for all query tokens\r\n const candidatesMap = findAllCandidates(\r\n queryTokens,\r\n vocabulary,\r\n tolerance,\r\n pluginState.config.enableSynonyms ? pluginState.synonymMap : undefined,\r\n pluginState.config.synonymMatchScore\r\n );\r\n\r\n // Filter by minimum score (skip when tolerance=0 since all matches are exact with score 1.0)\r\n const filteredFuzzyCandidates = tolerance === 0\r\n ? candidatesMap\r\n : filterCandidatesByScore(candidatesMap, pluginState.config.minScore);\r\n\r\n console.log(`🎯 Fuzzy candidates: ${Array.from(filteredFuzzyCandidates.values()).reduce((sum, c) => sum + c.length, 0)} total`);\r\n\r\n // STEP 5: Run phrase scoring ONLY on QPS candidate documents\r\n const phraseStartTime = performance.now();\r\n const documentMatches: DocumentMatch[] = [];\r\n \r\n // Get documents from Orama\r\n let docs: Record<string, any> = {};\r\n if ((orama as any).data?.docs?.docs) {\r\n docs = (orama as any).data.docs.docs;\r\n }\r\n\r\n let docsScored = 0;\r\n \r\n for (const [docId, doc] of Object.entries(docs)) {\r\n // PRUNING: Only score documents that are in QPS candidates\r\n if (!candidateDocIds.has(docId)) {\r\n continue;\r\n }\r\n \r\n docsScored++;\r\n \r\n const text = doc[textProperty];\r\n if (!text || typeof text !== 'string') {\r\n continue;\r\n }\r\n\r\n // Use cached tokens if available, otherwise tokenize\r\n let docTokens: string[];\r\n if (tokenCache && tokenCache.has(docId)) {\r\n docTokens = tokenCache.get(docId)!;\r\n } else {\r\n docTokens = tokenize(text);\r\n }\r\n\r\n // Find phrases in this document\r\n const phrases = findPhrasesInDocument(\r\n docTokens,\r\n filteredFuzzyCandidates,\r\n {\r\n weights: pluginState.config.weights as { exact: number; fuzzy: number; order: number; proximity: number; density: number; semantic: number },\r\n maxGap: pluginState.config.maxGap,\r\n proximitySpanMultiplier: pluginState.config.proximitySpanMultiplier,\r\n tolerance\r\n },\r\n pluginState.documentFrequency,\r\n pluginState.totalDocuments,\r\n queryTokens\r\n );\r\n\r\n if (phrases.length > 0) {\r\n const docScore = Math.max(...phrases.map(p => p.score));\r\n documentMatches.push({\r\n id: docId,\r\n phrases,\r\n score: docScore,\r\n document: doc\r\n });\r\n }\r\n }\r\n\r\n const phraseTime = performance.now() - phraseStartTime;\r\n console.log(`📊 Phrase scored ${docsScored} documents in ${phraseTime.toFixed(2)}ms`);\r\n\r\n // Sort by score descending\r\n documentMatches.sort((a, b) => b.score - a.score);\r\n\r\n // Apply final score minimum filter if enabled\r\n let finalMatches = documentMatches;\r\n if (pluginState.config.enableFinalScoreMinimum && pluginState.config.finalScoreMinimum > 0) {\r\n const threshold = pluginState.config.finalScoreMinimum;\r\n const beforeCount = finalMatches.length;\r\n finalMatches = finalMatches.filter(m => m.score >= threshold);\r\n console.log(`🎚️ Final score filter: ${beforeCount} → ${finalMatches.length} (threshold: ${threshold})`);\r\n }\r\n\r\n // Apply limit if specified\r\n const limit = params.limit ?? finalMatches.length;\r\n const limitedMatches = finalMatches.slice(0, limit);\r\n\r\n // Convert to Orama results format\r\n const hits = limitedMatches.map(match => ({\r\n id: match.id,\r\n score: match.score,\r\n document: match.document,\r\n _phrases: match.phrases\r\n })) as any[];\r\n\r\n const elapsed = performance.now() - startTime;\r\n\r\n console.log(`✅ Optimized search: ${hits.length} results in ${elapsed.toFixed(2)}ms (QPS: ${qpsTime.toFixed(2)}ms, Phrase: ${phraseTime.toFixed(2)}ms)`);\r\n\r\n return {\r\n elapsed: {\r\n formatted: `${elapsed.toFixed(2)}ms`,\r\n raw: Math.floor(elapsed * 1000000),\r\n qpsTime,\r\n phraseTime\r\n },\r\n hits,\r\n count: hits.length\r\n } as any;\r\n}\r\n\r\n/**\r\n * Create an optimized search function bound to a specific Orama + QPS index\r\n * \r\n * This is a convenience wrapper that captures the Orama instance and QPS index,\r\n * returning a simpler search function.\r\n * \r\n * @param orama - Orama instance\r\n * @param qpsIndex - QPS index data\r\n * @param pluginState - Fuzzy phrase plugin state\r\n * @param config - Optimized search configuration\r\n * @returns Bound search function\r\n */\r\nexport function createOptimizedSearch<T extends AnyOrama>(\r\n orama: T,\r\n qpsIndex: QPSIndexData,\r\n pluginState: PluginState,\r\n config: OptimizedSearchConfig = {}\r\n) {\r\n return async (params: {\r\n term?: string;\r\n properties?: string[];\r\n limit?: number;\r\n tokenCache?: Map<string, string[]>;\r\n }, language: string = 'french'): Promise<Results<TypedDocument<T>>> => {\r\n return searchWithQPSPruning(orama, qpsIndex, pluginState, params, config, language);\r\n };\r\n}\r\n","/**\r\n * Fuzzy Phrase Plugin for Orama\r\n * \r\n * Advanced fuzzy phrase matching with semantic weighting and synonym expansion.\r\n * Completely independent from QPS - accesses Orama's radix tree directly.\r\n */\r\n\r\nimport type { AnyOrama, OramaPlugin, Results, TypedDocument } from '@wcs-colab/orama';\r\nimport type { FuzzyPhraseConfig, PluginState, SynonymMap, DocumentMatch } from './types.js';\r\nimport { calculateAdaptiveTolerance } from './fuzzy.js';\r\nimport { \r\n extractVocabularyFromRadixTree, \r\n findAllCandidates,\r\n filterCandidatesByScore \r\n} from './candidates.js';\r\nimport { findPhrasesInDocument } from './scoring.js';\r\n\r\n/**\r\n * Default configuration\r\n */\r\nconst DEFAULT_CONFIG: Required<FuzzyPhraseConfig> = {\r\n textProperty: 'normalized_content', // Must match server's field name\r\n tolerance: 1,\r\n adaptiveTolerance: true,\r\n enableSynonyms: false,\r\n supabase: undefined as any,\r\n synonymMatchScore: 0.8,\r\n weights: {\r\n exact: 1.0,\r\n fuzzy: 0.8,\r\n order: 0.3,\r\n proximity: 0.2,\r\n density: 0.2,\r\n semantic: 0.15\r\n },\r\n maxGap: 5,\r\n minScore: 0.1,\r\n enableFinalScoreMinimum: false,\r\n finalScoreMinimum: 0.3,\r\n proximitySpanMultiplier: 5\r\n};\r\n\r\n/**\r\n * Plugin state storage (keyed by Orama instance)\r\n */\r\nconst pluginStates = new WeakMap<AnyOrama, PluginState>();\r\n\r\n/**\r\n * Create the Fuzzy Phrase Plugin\r\n * \r\n * @param userConfig - User configuration options\r\n * @returns Orama plugin instance\r\n */\r\nexport function pluginFuzzyPhrase(userConfig: FuzzyPhraseConfig = {}): OramaPlugin {\r\n // Merge user config with defaults\r\n const config: Required<FuzzyPhraseConfig> = {\r\n textProperty: userConfig.textProperty ?? DEFAULT_CONFIG.textProperty,\r\n tolerance: userConfig.tolerance ?? DEFAULT_CONFIG.tolerance,\r\n adaptiveTolerance: userConfig.adaptiveTolerance ?? DEFAULT_CONFIG.adaptiveTolerance,\r\n enableSynonyms: userConfig.enableSynonyms ?? DEFAULT_CONFIG.enableSynonyms,\r\n supabase: userConfig.supabase || DEFAULT_CONFIG.supabase,\r\n synonymMatchScore: userConfig.synonymMatchScore ?? DEFAULT_CONFIG.synonymMatchScore,\r\n weights: {\r\n exact: userConfig.weights?.exact ?? DEFAULT_CONFIG.weights.exact,\r\n fuzzy: userConfig.weights?.fuzzy ?? DEFAULT_CONFIG.weights.fuzzy,\r\n order: userConfig.weights?.order ?? DEFAULT_CONFIG.weights.order,\r\n proximity: userConfig.weights?.proximity ?? DEFAULT_CONFIG.weights.proximity,\r\n density: userConfig.weights?.density ?? DEFAULT_CONFIG.weights.density,\r\n semantic: userConfig.weights?.semantic ?? DEFAULT_CONFIG.weights.semantic\r\n },\r\n maxGap: userConfig.maxGap ?? DEFAULT_CONFIG.maxGap,\r\n minScore: userConfig.minScore ?? DEFAULT_CONFIG.minScore,\r\n enableFinalScoreMinimum: userConfig.enableFinalScoreMinimum ?? DEFAULT_CONFIG.enableFinalScoreMinimum,\r\n finalScoreMinimum: userConfig.finalScoreMinimum ?? DEFAULT_CONFIG.finalScoreMinimum,\r\n proximitySpanMultiplier: userConfig.proximitySpanMultiplier ?? DEFAULT_CONFIG.proximitySpanMultiplier\r\n };\r\n\r\n const plugin: OramaPlugin = {\r\n name: 'fuzzy-phrase',\r\n\r\n /**\r\n * Initialize plugin after index is created\r\n */\r\n afterCreate: async (orama: AnyOrama) => {\r\n console.log('🔮 Initializing Fuzzy Phrase Plugin...');\r\n\r\n // Initialize state with empty vocabulary (will be populated below)\r\n const state: PluginState = {\r\n synonymMap: {},\r\n config,\r\n documentFrequency: new Map(),\r\n totalDocuments: 0,\r\n vocabulary: new Set()\r\n };\r\n\r\n // Load synonyms from Supabase if enabled\r\n if (config.enableSynonyms && config.supabase) {\r\n try {\r\n console.log('📖 Loading synonyms from Supabase...');\r\n state.synonymMap = await loadSynonymsFromSupabase(config.supabase);\r\n console.log(`✅ Loaded ${Object.keys(state.synonymMap).length} words with synonyms`);\r\n } catch (error) {\r\n console.error('⚠️ Failed to load synonyms:', error);\r\n // Continue without synonyms\r\n }\r\n }\r\n\r\n // Calculate document frequencies for TF-IDF from document store\r\n const docs = (orama.data as any)?.docs?.docs;\r\n if (docs) {\r\n state.totalDocuments = Object.keys(docs).length;\r\n state.documentFrequency = calculateDocumentFrequencies(docs, config.textProperty);\r\n console.log(`📊 Calculated document frequencies for ${state.totalDocuments} documents`);\r\n }\r\n\r\n // CACHE VOCABULARY: Extract from radix tree ONCE at startup\r\n // This avoids O(V) radix traversal on every query\r\n try {\r\n const indexData = (orama as any).data?.index;\r\n let radixNode = null;\r\n \r\n if (indexData?.indexes?.[config.textProperty]?.node) {\r\n radixNode = indexData.indexes[config.textProperty].node;\r\n } else if (indexData?.[config.textProperty]?.node) {\r\n radixNode = indexData[config.textProperty].node;\r\n }\r\n \r\n if (radixNode) {\r\n state.vocabulary = extractVocabularyFromRadixTree(radixNode);\r\n console.log(`📚 Cached ${state.vocabulary.size} vocabulary words (eliminates per-query radix traversal)`);\r\n } else {\r\n console.warn('⚠️ Could not find radix tree for vocabulary caching');\r\n }\r\n } catch (error) {\r\n console.error('⚠️ Failed to cache vocabulary:', error);\r\n }\r\n\r\n // Store state\r\n pluginStates.set(orama, state);\r\n console.log('✅ Fuzzy Phrase Plugin initialized');\r\n \r\n // Signal ready - emit a custom event that can be listened to\r\n // Use setImmediate to ensure this runs after the afterCreate hook completes\r\n setImmediate(() => {\r\n if (typeof (globalThis as any).fuzzyPhrasePluginReady === 'function') {\r\n console.log('📡 Signaling plugin ready...');\r\n (globalThis as any).fuzzyPhrasePluginReady();\r\n } else {\r\n console.warn('⚠️ fuzzyPhrasePluginReady callback not found');\r\n }\r\n });\r\n }\r\n };\r\n\r\n return plugin;\r\n}\r\n\r\n/**\r\n * Search with fuzzy phrase matching\r\n * \r\n * This function should be called instead of the regular search() function\r\n * to enable fuzzy phrase matching.\r\n */\r\nexport async function searchWithFuzzyPhrase<T extends AnyOrama>(\r\n orama: T, \r\n params: { \r\n term?: string; \r\n properties?: string[]; \r\n limit?: number;\r\n tokenCache?: Map<string, string[]>; // Pre-tokenized documents for fast search\r\n candidateIds?: Set<string> | string[]; // Optional: only score these doc IDs (for QPS pruning)\r\n },\r\n language?: string\r\n): Promise<Results<TypedDocument<T>>> {\r\n const startTime = performance.now();\r\n \r\n // Get plugin state\r\n const state = pluginStates.get(orama);\r\n \r\n if (!state) {\r\n console.error('❌ Plugin state not initialized');\r\n throw new Error('Fuzzy Phrase Plugin not properly initialized');\r\n }\r\n\r\n const { term, properties, tokenCache, candidateIds } = params;\r\n \r\n // Convert candidateIds to Set for fast lookup if provided\r\n const candidateIdSet: Set<string> | null = candidateIds \r\n ? (candidateIds instanceof Set ? candidateIds : new Set(candidateIds))\r\n : null;\r\n \r\n if (!term || typeof term !== 'string') {\r\n return { elapsed: { formatted: '0ms', raw: 0 }, hits: [], count: 0 };\r\n }\r\n\r\n // Use specified property or default\r\n const textProperty = (properties && properties[0]) || state.config.textProperty;\r\n\r\n // Tokenize query\r\n const queryTokens = tokenize(term);\r\n \r\n if (queryTokens.length === 0) {\r\n return { elapsed: { formatted: '0ms', raw: 0 }, hits: [], count: 0 };\r\n }\r\n\r\n // Calculate tolerance (adaptive or fixed)\r\n const tolerance = state.config.adaptiveTolerance\r\n ? calculateAdaptiveTolerance(queryTokens, state.config.tolerance)\r\n : state.config.tolerance;\r\n\r\n console.log(`🔍 Fuzzy phrase search: \"${term}\" (${queryTokens.length} tokens, tolerance: ${tolerance})`);\r\n\r\n // USE CACHED VOCABULARY - lazy initialization on first search\r\n // afterCreate runs before documents are inserted, so we extract on first search\r\n let vocabulary = state.vocabulary;\r\n \r\n if (vocabulary.size === 0) {\r\n console.log('📚 Vocabulary cache empty - extracting on first search...');\r\n try {\r\n const indexData = (orama as any).data?.index;\r\n let radixNode = null;\r\n \r\n if (indexData?.indexes?.[textProperty]?.node) {\r\n radixNode = indexData.indexes[textProperty].node;\r\n } else if (indexData?.[textProperty]?.node) {\r\n radixNode = indexData[textProperty].node;\r\n }\r\n \r\n if (radixNode) {\r\n state.vocabulary = extractVocabularyFromRadixTree(radixNode);\r\n vocabulary = state.vocabulary;\r\n console.log(`📚 Cached ${vocabulary.size} vocabulary words (will be reused for subsequent queries)`);\r\n } else {\r\n console.error('❌ Radix tree not found for vocabulary extraction');\r\n return { elapsed: { formatted: '0ms', raw: 0 }, hits: [], count: 0 };\r\n }\r\n } catch (error) {\r\n console.error('❌ Failed to extract vocabulary:', error);\r\n return { elapsed: { formatted: '0ms', raw: 0 }, hits: [], count: 0 };\r\n }\r\n } else {\r\n console.log(`📚 Using cached vocabulary (${vocabulary.size} words)`);\r\n }\r\n\r\n // Find candidates for all query tokens\r\n const candidatesMap = findAllCandidates(\r\n queryTokens,\r\n vocabulary,\r\n tolerance,\r\n state.config.enableSynonyms ? state.synonymMap : undefined,\r\n state.config.synonymMatchScore\r\n );\r\n\r\n // Filter by minimum score (skip when tolerance=0 since all matches are exact with score 1.0)\r\n const filteredCandidates = tolerance === 0\r\n ? candidatesMap // Skip filtering - all matches are exact\r\n : filterCandidatesByScore(candidatesMap, state.config.minScore);\r\n\r\n console.log(`🎯 Found candidates: ${Array.from(filteredCandidates.values()).reduce((sum, c) => sum + c.length, 0)} total`);\r\n\r\n // Search through all documents\r\n const documentMatches: DocumentMatch[] = [];\r\n \r\n console.log('🔍 DEBUG orama.data structure:', {\r\n dataKeys: Object.keys((orama as any).data || {}),\r\n hasDocs: !!((orama as any).data?.docs),\r\n docsType: (orama as any).data?.docs ? typeof (orama as any).data.docs : 'undefined'\r\n });\r\n \r\n // Try multiple possible document storage locations\r\n let docs: Record<string, any> = {};\r\n \r\n // Access the actual documents - they're nested in orama.data.docs.docs\r\n if ((orama as any).data?.docs?.docs) {\r\n docs = (orama as any).data.docs.docs;\r\n console.log('✅ Found docs at orama.data.docs.docs');\r\n }\r\n // Fallback: orama.data.docs (might be the correct structure in some cases)\r\n else if ((orama as any).data?.docs && typeof (orama as any).data.docs === 'object') {\r\n // Check if it has document-like properties (not sharedInternalDocumentStore, etc.)\r\n const firstKey = Object.keys((orama as any).data.docs)[0];\r\n if (firstKey && firstKey !== 'sharedInternalDocumentStore' && firstKey !== 'count') {\r\n docs = (orama as any).data.docs;\r\n console.log('✅ Found docs at orama.data.docs (direct)');\r\n }\r\n }\r\n \r\n if (Object.keys(docs).length === 0) {\r\n console.log('❌ Could not find documents - available structure:', {\r\n hasDataDocs: !!((orama as any).data?.docs),\r\n dataDocsKeys: (orama as any).data?.docs ? Object.keys((orama as any).data.docs) : 'none',\r\n hasDataDocsDocs: !!((orama as any).data?.docs?.docs),\r\n dataDocsDocsCount: (orama as any).data?.docs?.docs ? Object.keys((orama as any).data.docs.docs).length : 0\r\n });\r\n }\r\n \r\n const cacheHits = tokenCache ? tokenCache.size : 0;\r\n const docsToSearch = candidateIdSet ? candidateIdSet.size : Object.keys(docs).length;\r\n console.log(`📄 Searching through ${docsToSearch} documents${candidateIdSet ? ' (pruned by candidateIds)' : ''} (${cacheHits > 0 ? `${cacheHits} tokens cached` : 'no cache'})`);\r\n\r\n for (const [docId, doc] of Object.entries(docs)) {\r\n // PRUNING: Skip docs not in candidateIds if provided\r\n // candidateIds can contain user document IDs (doc.id) OR internal Orama keys\r\n if (candidateIdSet) {\r\n const userDocId = doc.id !== undefined ? String(doc.id) : docId;\r\n if (!candidateIdSet.has(userDocId) && !candidateIdSet.has(docId)) {\r\n continue;\r\n }\r\n }\r\n \r\n const text = doc[textProperty];\r\n \r\n if (!text || typeof text !== 'string') {\r\n continue;\r\n }\r\n\r\n // Use cached tokens if available, otherwise tokenize\r\n let docTokens: string[];\r\n if (tokenCache && tokenCache.has(docId)) {\r\n docTokens = tokenCache.get(docId) as string[];\r\n } else {\r\n docTokens = tokenize(text);\r\n }\r\n\r\n // Find phrases in this document\r\n // Note: state.config.weights is guaranteed to have all properties from default merge\r\n // Pass original queryTokens to preserve duplicates (e.g., \"de ... de\")\r\n const phrases = findPhrasesInDocument(\r\n docTokens,\r\n filteredCandidates,\r\n {\r\n weights: state.config.weights as { exact: number; fuzzy: number; order: number; proximity: number; density: number; semantic: number },\r\n maxGap: state.config.maxGap,\r\n proximitySpanMultiplier: state.config.proximitySpanMultiplier,\r\n tolerance\r\n },\r\n state.documentFrequency,\r\n state.totalDocuments,\r\n queryTokens // Original tokens with duplicates preserved\r\n );\r\n\r\n if (phrases.length > 0) {\r\n // Calculate overall document score (highest phrase score)\r\n const docScore = Math.max(...phrases.map(p => p.score));\r\n\r\n documentMatches.push({\r\n id: docId,\r\n phrases,\r\n score: docScore,\r\n document: doc\r\n });\r\n }\r\n }\r\n\r\n // Sort by score descending\r\n documentMatches.sort((a, b) => b.score - a.score);\r\n\r\n // Apply final score minimum filter if enabled\r\n let filteredMatches = documentMatches;\r\n if (state.config.enableFinalScoreMinimum && state.config.finalScoreMinimum > 0) {\r\n const threshold = state.config.finalScoreMinimum;\r\n const beforeCount = filteredMatches.length;\r\n filteredMatches = filteredMatches.filter(m => m.score >= threshold);\r\n console.log(`🎚️ Final score filter: ${beforeCount} → ${filteredMatches.length} (threshold: ${threshold})`);\r\n }\r\n\r\n // Apply limit if specified\r\n const limit = params.limit ?? filteredMatches.length;\r\n const limitedMatches = filteredMatches.slice(0, limit);\r\n\r\n // Convert to Orama results format\r\n const hits = limitedMatches.map(match => ({\r\n id: match.id,\r\n score: match.score,\r\n document: match.document,\r\n // Store phrases for highlighting\r\n _phrases: match.phrases\r\n })) as any[];\r\n\r\n const elapsed = performance.now() - startTime;\r\n\r\n console.log(`✅ Found ${hits.length} results in ${elapsed.toFixed(2)}ms (limit: ${limit})`);\r\n\r\n return {\r\n elapsed: {\r\n formatted: `${elapsed.toFixed(2)}ms`,\r\n raw: Math.floor(elapsed * 1000000) // nanoseconds\r\n },\r\n hits,\r\n count: hits.length\r\n } as any;\r\n}\r\n\r\n/**\r\n * Load synonyms from Supabase\r\n */\r\nasync function loadSynonymsFromSupabase(\r\n supabaseConfig: { url: string; serviceKey: string }\r\n): Promise<SynonymMap> {\r\n try {\r\n console.log('🔍 DEBUG: Calling Supabase RPC get_synonym_map...');\r\n \r\n // Dynamic import to avoid bundling Supabase client if not needed\r\n const { createClient } = await import('@supabase/supabase-js');\r\n \r\n const supabase = createClient(supabaseConfig.url, supabaseConfig.serviceKey);\r\n \r\n // Call the get_synonym_map function\r\n const { data, error } = await supabase.rpc('get_synonym_map');\r\n \r\n console.log('🔍 DEBUG: Supabase RPC response:', {\r\n hasError: !!error,\r\n errorMessage: error?.message,\r\n hasData: !!data,\r\n dataType: typeof data,\r\n dataKeys: data ? Object.keys(data).length : 0\r\n });\r\n \r\n if (error) {\r\n throw new Error(`Supabase error: ${error.message}`);\r\n }\r\n \r\n const synonymMap = data || {};\r\n console.log(`📚 Loaded ${Object.keys(synonymMap).length} synonym entries from Supabase`);\r\n \r\n return synonymMap;\r\n } catch (error) {\r\n console.error('❌ Failed to load synonyms from Supabase:', error);\r\n throw error;\r\n }\r\n}\r\n\r\n/**\r\n * Calculate document frequencies for TF-IDF\r\n */\r\nfunction calculateDocumentFrequencies(\r\n docs: Record<string, any>,\r\n textProperty: string\r\n): Map<string, number> {\r\n const df = new Map<string, number>();\r\n\r\n for (const doc of Object.values(docs)) {\r\n const text = doc[textProperty];\r\n \r\n if (!text || typeof text !== 'string') {\r\n continue;\r\n }\r\n\r\n // Get unique words in this document\r\n const words = new Set(tokenize(text));\r\n\r\n // Increment document frequency for each unique word\r\n for (const word of words) {\r\n df.set(word, (df.get(word) || 0) + 1);\r\n }\r\n }\r\n\r\n return df;\r\n}\r\n\r\n/**\r\n * Normalize text using the same rules as server-side\r\n * \r\n * CRITICAL: This must match the normalizeText() function in server/index.js exactly\r\n * PLUS we remove all punctuation to match Orama's French tokenizer behavior\r\n */\r\nfunction normalizeText(text: string): string {\r\n return text\r\n .toLowerCase()\r\n .normalize('NFD')\r\n .replace(/[\\u0300-\\u036f]/g, '') // Remove diacritics\r\n // Replace French elisions (l', d', etc.) with space to preserve word boundaries\r\n .replace(/\\b[ldcjmnst][\\u2018\\u2019\\u201A\\u201B\\u2032\\u2035\\u0027\\u0060\\u00B4](?=\\w)/gi, ' ')\r\n .replace(/[\\u2018\\u2019\\u201A\\u201B\\u2032\\u2035\\u0027\\u0060\\u00B4]/g, '') // Remove remaining apostrophes\r\n .replace(/[\\u201c\\u201d]/g, '\"') // Normalize curly quotes to straight quotes\r\n .replace(/[.,;:!?()[\\]{}\\-—–«»\"\"]/g, ' ') // Remove punctuation (replace with space to preserve word boundaries)\r\n .replace(/\\s+/g, ' ') // Normalize multiple spaces to single space\r\n .trim();\r\n}\r\n\r\n/**\r\n * Tokenization matching normalized text behavior\r\n * \r\n * Note: Text should already be normalized before indexing, so we normalize again\r\n * to ensure plugin tokenization matches index tokenization\r\n */\r\nfunction tokenize(text: string): string[] {\r\n // Normalize first (same as indexing), then split by whitespace\r\n return normalizeText(text)\r\n .split(/\\s+/)\r\n .filter(token => token.length > 0);\r\n}\r\n\r\n/**\r\n * Export types for external use\r\n */\r\nexport type {\r\n FuzzyPhraseConfig,\r\n WordMatch,\r\n PhraseMatch,\r\n DocumentMatch,\r\n SynonymMap,\r\n Candidate,\r\n PluginState\r\n} from './types.js';\r\n\r\n/**\r\n * Export optimized search (QPS + fuzzy phrase pruning)\r\n */\r\nexport {\r\n searchWithQPSPruning,\r\n createOptimizedSearch,\r\n normalizeText as normalizeTextOptimized,\r\n tokenize as tokenizeOptimized,\r\n type OptimizedSearchConfig\r\n} from './optimized.js';\r\n\r\n/**\r\n * Re-export plugin state accessor for optimized search integration\r\n */\r\nexport function getPluginState(orama: any): PluginState | undefined {\r\n return pluginStates.get(orama);\r\n}\r\n"]}
|
|
1
|
+
{"version":3,"sources":["../src/fuzzy.ts","../src/candidates.ts","../src/scoring.ts","../src/optimized.ts","../src/index.ts"],"names":["tokenize","normalizeText"],"mappings":"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","sourcesContent":["/**\n * Fuzzy matching utilities using bounded Levenshtein distance\n * \n * This is the same algorithm used by Orama's match-highlight plugin\n * for consistent fuzzy matching behavior.\n */\n\n/**\n * Result of bounded Levenshtein distance calculation\n */\nexport interface BoundedLevenshteinResult {\n /** Whether the distance is within bounds */\n isBounded: boolean;\n /** The actual distance (only valid if isBounded is true) */\n distance: number;\n}\n\n/**\n * Calculate bounded Levenshtein distance between two strings\n * \n * Stops early if distance exceeds the bound for better performance.\n * This is the same algorithm as Orama's internal boundedLevenshtein.\n * \n * @param a - First string\n * @param b - Second string\n * @param bound - Maximum allowed distance\n * @returns Result indicating if strings are within bound and the distance\n */\nexport function boundedLevenshtein(\n a: string,\n b: string,\n bound: number\n): BoundedLevenshteinResult {\n // Quick checks\n if (a === b) {\n return { isBounded: true, distance: 0 };\n }\n\n const aLen = a.length;\n const bLen = b.length;\n\n // If length difference exceeds bound, no need to calculate\n if (Math.abs(aLen - bLen) > bound) {\n return { isBounded: false, distance: bound + 1 };\n }\n\n // Swap to ensure a is shorter (optimization)\n if (aLen > bLen) {\n [a, b] = [b, a];\n }\n\n const m = a.length;\n const n = b.length;\n\n // Use single array instead of matrix (memory optimization)\n let prevRow = new Array(n + 1);\n let currRow = new Array(n + 1);\n\n // Initialize first row\n for (let j = 0; j <= n; j++) {\n prevRow[j] = j;\n }\n\n for (let i = 1; i <= m; i++) {\n currRow[0] = i;\n let minInRow = i;\n\n for (let j = 1; j <= n; j++) {\n const cost = a[i - 1] === b[j - 1] ? 0 : 1;\n\n currRow[j] = Math.min(\n prevRow[j] + 1, // deletion\n currRow[j - 1] + 1, // insertion\n prevRow[j - 1] + cost // substitution\n );\n\n minInRow = Math.min(minInRow, currRow[j]);\n }\n\n // Early termination: if all values in row exceed bound, we're done\n if (minInRow > bound) {\n return { isBounded: false, distance: bound + 1 };\n }\n\n // Swap rows for next iteration\n [prevRow, currRow] = [currRow, prevRow];\n }\n\n const distance = prevRow[n];\n return {\n isBounded: distance <= bound,\n distance\n };\n}\n\n/**\n * Check if a word matches a query token with fuzzy matching\n * \n * @param word - Word from document\n * @param queryToken - Token from search query\n * @param tolerance - Maximum edit distance allowed\n * @returns Match result with score\n */\nexport function fuzzyMatch(\n word: string,\n queryToken: string,\n tolerance: number\n): { matches: boolean; distance: number; score: number } {\n // Exact match\n if (word === queryToken) {\n return { matches: true, distance: 0, score: 1.0 };\n }\n\n // NOTE: Prefix matching removed entirely\n // It was causing false positives (e.g., \"de\" matching \"dedain\", \"desert\")\n // and interfering with tolerance settings. Levenshtein-only is cleaner.\n\n // Fuzzy match with bounded Levenshtein distance\n const result = boundedLevenshtein(word, queryToken, tolerance);\n \n if (result.isBounded) {\n // Score decreases with distance\n // distance 1 = 0.8, distance 2 = 0.6, etc.\n const score = 1.0 - (result.distance * 0.2);\n return {\n matches: true,\n distance: result.distance,\n score: Math.max(0.1, score) // Minimum score of 0.1\n };\n }\n\n return { matches: false, distance: tolerance + 1, score: 0 };\n}\n\n/**\n * Calculate adaptive tolerance based on query length\n * \n * Longer queries get higher tolerance for better fuzzy matching.\n * \n * @param queryTokens - Array of query tokens\n * @param baseTolerance - Base tolerance value\n * @returns Calculated tolerance (always an integer)\n */\nexport function calculateAdaptiveTolerance(\n queryTokens: string[],\n baseTolerance: number\n): number {\n const queryLength = queryTokens.length;\n \n if (queryLength <= 2) {\n return baseTolerance;\n } else if (queryLength <= 4) {\n return baseTolerance + 1;\n } else if (queryLength <= 6) {\n return baseTolerance + 2;\n } else {\n return baseTolerance + 3;\n }\n}\n","/**\r\n * Candidate expansion: Find all possible matches for query tokens\r\n * including exact matches, fuzzy matches, and synonyms\r\n */\r\n\r\nimport { fuzzyMatch } from './fuzzy.js';\r\nimport type { Candidate, SynonymMap } from './types.js';\r\n\r\n/**\r\n * Extract all unique words from the radix tree index\r\n * \r\n * @param radixNode - Root node of the radix tree\r\n * @returns Set of all unique words in the index\r\n */\r\nexport function extractVocabularyFromRadixTree(radixNode: any): Set<string> {\r\n const vocabulary = new Set<string>();\r\n let nodesVisited = 0;\r\n let wordsFound = 0;\r\n \r\n function traverse(node: any, depth: number = 0) {\r\n if (!node) {\r\n return;\r\n }\r\n \r\n nodesVisited++;\r\n \r\n // Check if this node represents a complete word\r\n // e = true means it's an end of a word\r\n if (node.e && node.w && typeof node.w === 'string' && node.w.length > 0) {\r\n vocabulary.add(node.w);\r\n wordsFound++;\r\n }\r\n \r\n // Children can be Map, Array, or Object\r\n if (node.c) {\r\n if (node.c instanceof Map) {\r\n // Map format\r\n for (const [_key, childNode] of node.c) {\r\n traverse(childNode, depth + 1);\r\n }\r\n } else if (Array.isArray(node.c)) {\r\n // Array format: [[key, childNode], ...]\r\n for (const [_key, childNode] of node.c) {\r\n traverse(childNode, depth + 1);\r\n }\r\n } else if (typeof node.c === 'object') {\r\n // Object format: {key: childNode, ...}\r\n for (const childNode of Object.values(node.c)) {\r\n traverse(childNode, depth + 1);\r\n }\r\n }\r\n }\r\n }\r\n \r\n traverse(radixNode);\r\n console.log(`📚 Extracted ${vocabulary.size} words from ${nodesVisited} nodes visited`);\r\n return vocabulary;\r\n}\r\n\r\n/**\r\n * Find all candidate matches for a single query token\r\n * \r\n * @param queryToken - Token from search query\r\n * @param vocabulary - Set of all words in the index\r\n * @param tolerance - Fuzzy matching tolerance\r\n * @param synonyms - Synonym map (optional)\r\n * @param synonymScore - Score multiplier for synonym matches\r\n * @returns Array of candidate matches\r\n */\r\nexport function findCandidatesForToken(\r\n queryToken: string,\r\n vocabulary: Set<string>,\r\n tolerance: number,\r\n synonyms?: SynonymMap,\r\n synonymScore: number = 0.8\r\n): Candidate[] {\r\n const candidates: Candidate[] = [];\r\n const seen = new Set<string>();\r\n\r\n // 1. Check for exact match\r\n if (vocabulary.has(queryToken)) {\r\n candidates.push({\r\n word: queryToken,\r\n type: 'exact',\r\n queryToken,\r\n distance: 0,\r\n score: 1.0\r\n });\r\n seen.add(queryToken);\r\n }\r\n\r\n // 2. Check for fuzzy matches\r\n for (const word of vocabulary) {\r\n if (seen.has(word)) continue;\r\n\r\n const match = fuzzyMatch(word, queryToken, tolerance);\r\n if (match.matches) {\r\n candidates.push({\r\n word,\r\n type: 'fuzzy',\r\n queryToken,\r\n distance: match.distance,\r\n score: match.score\r\n });\r\n seen.add(word);\r\n }\r\n }\r\n\r\n // 3. Check for synonym matches\r\n if (synonyms && synonyms[queryToken]) {\r\n for (const synonym of synonyms[queryToken]) {\r\n if (seen.has(synonym)) continue;\r\n if (vocabulary.has(synonym)) {\r\n candidates.push({\r\n word: synonym,\r\n type: 'synonym',\r\n queryToken,\r\n distance: 0,\r\n score: synonymScore\r\n });\r\n seen.add(synonym);\r\n }\r\n }\r\n }\r\n\r\n return candidates;\r\n}\r\n\r\n/**\r\n * Find candidates for all query tokens\r\n * \r\n * @param queryTokens - Array of tokens from search query\r\n * @param vocabulary - Set of all words in the index\r\n * @param tolerance - Fuzzy matching tolerance\r\n * @param synonyms - Synonym map (optional)\r\n * @param synonymScore - Score multiplier for synonym matches\r\n * @returns Map of query tokens to their candidate matches\r\n */\r\nexport function findAllCandidates(\r\n queryTokens: string[],\r\n vocabulary: Set<string>,\r\n tolerance: number,\r\n synonyms?: SynonymMap,\r\n synonymScore: number = 0.8\r\n): Map<string, Candidate[]> {\r\n const candidatesMap = new Map<string, Candidate[]>();\r\n\r\n for (const token of queryTokens) {\r\n const tokenCandidates = findCandidatesForToken(\r\n token,\r\n vocabulary,\r\n tolerance,\r\n synonyms,\r\n synonymScore\r\n );\r\n candidatesMap.set(token, tokenCandidates);\r\n }\r\n\r\n return candidatesMap;\r\n}\r\n\r\n/**\r\n * Get total number of candidates across all tokens\r\n * \r\n * @param candidatesMap - Map of token to candidates\r\n * @returns Total count of all candidates\r\n */\r\nexport function getTotalCandidateCount(\r\n candidatesMap: Map<string, Candidate[]>\r\n): number {\r\n let total = 0;\r\n for (const candidates of candidatesMap.values()) {\r\n total += candidates.length;\r\n }\r\n return total;\r\n}\r\n\r\n/**\r\n * Filter candidates by minimum score threshold\r\n * \r\n * @param candidatesMap - Map of token to candidates\r\n * @param minScore - Minimum score threshold\r\n * @returns Filtered candidates map\r\n */\r\nexport function filterCandidatesByScore(\r\n candidatesMap: Map<string, Candidate[]>,\r\n minScore: number\r\n): Map<string, Candidate[]> {\r\n const filtered = new Map<string, Candidate[]>();\r\n\r\n for (const [token, candidates] of candidatesMap.entries()) {\r\n const filteredCandidates = candidates.filter(c => c.score >= minScore);\r\n if (filteredCandidates.length > 0) {\r\n filtered.set(token, filteredCandidates);\r\n }\r\n }\r\n\r\n return filtered;\r\n}\r\n","/**\n * Phrase scoring algorithm with semantic weighting\n */\n\nimport type { WordMatch, PhraseMatch, Candidate, GapWord } from './types.js';\n\n/**\n * Type for the candidate lookup structure\n */\nexport type CandidateLookup = Map<string, Array<{ queryToken: string, candidate: Candidate }>>;\n\n/**\n * PHASE 1 OPTIMIZATION A: Build candidateLookup once per query instead of per document\n * \n * Creates a map from document words to their matching query candidates,\n * sorted by quality (exact matches first, then by score).\n * \n * @param candidatesMap - Map of query tokens to their candidates\n * @returns Lookup map for fast document word matching\n */\nexport function buildCandidateLookup(\n candidatesMap: Map<string, Candidate[]>\n): CandidateLookup {\n const candidateLookup: CandidateLookup = new Map();\n for (const [queryToken, candidates] of candidatesMap.entries()) {\n for (const candidate of candidates) {\n if (!candidateLookup.has(candidate.word)) {\n candidateLookup.set(candidate.word, []);\n }\n candidateLookup.get(candidate.word)!.push({ queryToken, candidate });\n }\n }\n // Sort each word's matches by quality: exact first, then by score\n for (const entries of candidateLookup.values()) {\n entries.sort((a, b) => {\n if (a.candidate.type === 'exact' && b.candidate.type !== 'exact') return -1;\n if (b.candidate.type === 'exact' && a.candidate.type !== 'exact') return 1;\n return b.candidate.score - a.candidate.score;\n });\n }\n return candidateLookup;\n}\n\n/**\n * PHASE 1 OPTIMIZATION B: Build queryTokenCounts once per query instead of per phrase\n * \n * Counts occurrences of each token in the query (handles duplicates like \"de ... de\").\n * \n * @param queryTokens - Original query tokens array\n * @returns Map of token to occurrence count\n */\nexport function buildQueryTokenCounts(queryTokens: string[]): Map<string, number> {\n const queryTokenCounts = new Map<string, number>();\n for (const token of queryTokens) {\n queryTokenCounts.set(token, (queryTokenCounts.get(token) || 0) + 1);\n }\n return queryTokenCounts;\n}\n\n/**\n * Configuration for phrase scoring\n */\nexport interface ScoringConfig {\n weights: {\n exact: number;\n fuzzy: number;\n order: number;\n proximity: number;\n density: number;\n semantic: number;\n };\n maxGap: number;\n /** \n * Multiplier for proximity window calculation.\n * proximityWindow = queryTokens.length × proximitySpanMultiplier\n */\n proximitySpanMultiplier: number;\n /**\n * Fuzzy tolerance (Levenshtein distance). When 0, only exact matches.\n */\n tolerance: number;\n}\n\n/**\n * Find all phrase matches in a document\n * \n * @param documentTokens - Tokenized document content (needed to extract gap words)\n * @param candidatesMap - Map of query tokens to their candidates\n * @param config - Scoring configuration\n * @param documentFrequency - Document frequency map for TF-IDF\n * @param totalDocuments - Total number of documents\n * @param originalQueryTokens - Original query tokens array (preserves duplicates like \"de...de\")\n * @param candidateLookup - PHASE 1 OPTIMIZATION A: Pre-built candidate lookup\n * @param queryTokenCounts - PHASE 1 OPTIMIZATION B: Pre-built query token counts\n * @returns Array of phrase matches\n */\nexport function findPhrasesInDocument(\n documentTokens: string[],\n candidatesMap: Map<string, Candidate[]>,\n config: ScoringConfig,\n documentFrequency: Map<string, number>,\n totalDocuments: number,\n originalQueryTokens: string[],\n candidateLookup: CandidateLookup,\n queryTokenCounts: Map<string, number>\n): PhraseMatch[] {\n const phrases: PhraseMatch[] = [];\n // Use original query tokens to preserve duplicates (e.g., \"de maux ... de\")\n const queryTokens = originalQueryTokens;\n\n // Find all word matches in document\n const wordMatches: WordMatch[] = [];\n\n // Iterate documentTokens in position order (0, 1, 2...)\n // Matches at same position come out quality-sorted (from candidateLookup)\n for (let i = 0; i < documentTokens.length; i++) {\n const docWord = documentTokens[i];\n const matches = candidateLookup.get(docWord);\n if (matches) {\n for (const { queryToken, candidate } of matches) {\n wordMatches.push({\n word: docWord,\n queryToken,\n position: i,\n type: candidate.type,\n distance: candidate.distance,\n score: candidate.score\n });\n }\n }\n }\n\n // Build phrases from word matches using sliding window\n for (let i = 0; i < wordMatches.length; i++) {\n const phrase = buildPhraseFromPosition(\n wordMatches,\n i,\n queryTokens,\n config,\n documentFrequency,\n totalDocuments,\n wordMatches,\n documentTokens,\n queryTokenCounts // OPTIMIZATION B: Pass pre-built queryTokenCounts\n );\n\n if (phrase && phrase.words.length > 0) {\n phrases.push(phrase);\n }\n }\n\n // Filter out low-quality single-word matches for multi-word queries\n // This prevents noise from common words like \"de\", \"la\", \"des\" appearing as separate phrases\n const minTokensRequired = queryTokens.length >= 3 ? 2 : 1;\n const filteredPhrases = phrases.filter(p => p.words.length >= minTokensRequired);\n\n // Deduplicate and sort by score\n return deduplicatePhrases(filteredPhrases);\n}\n\n/**\n * Build a phrase starting from a specific word match position\n * \n * @param wordMatches - All word matches in document\n * @param startIndex - Starting index in wordMatches array\n * @param queryTokens - Original query tokens\n * @param config - Scoring configuration\n * @param documentFrequency - Document frequency map\n * @param totalDocuments - Total document count\n * @param allWordMatches - All word matches in document (for density calculation)\n * @param documentTokens - Original document tokens (for gap word extraction)\n * @param queryTokenCounts - OPTIMIZATION B: Pre-built query token counts\n * @returns Phrase match or null\n */\nfunction buildPhraseFromPosition(\n wordMatches: WordMatch[],\n startIndex: number,\n queryTokens: string[],\n config: ScoringConfig,\n documentFrequency: Map<string, number>,\n totalDocuments: number,\n allWordMatches: WordMatch[],\n documentTokens: string[],\n queryTokenCounts: Map<string, number>\n): PhraseMatch | null {\n const startMatch = wordMatches[startIndex];\n const phraseWords: WordMatch[] = [startMatch];\n\n // Track how many times we've matched each token\n const matchedCounts = new Map<string, number>();\n matchedCounts.set(startMatch.queryToken, 1);\n\n const gapWords: GapWord[] = [];\n let totalGapUsed = 0;\n let totalMatchedTokens = 1;\n\n // Look for nearby matches to complete the phrase\n for (let i = startIndex + 1; i < wordMatches.length; i++) {\n const match = wordMatches[i];\n const lastPos = phraseWords[phraseWords.length - 1].position;\n\n // Skip if same position (can happen with multiple fuzzy matches for same word)\n if (match.position <= lastPos) {\n continue;\n }\n\n const gap = match.position - lastPos - 1;\n\n // Stop if this gap alone exceeds maximum\n if (gap > config.maxGap) {\n break;\n }\n\n // Stop if adding this gap would exceed total maxGap budget\n if (totalGapUsed + gap > config.maxGap) {\n break;\n }\n\n // Check if we still need more of this token (handles duplicates)\n const neededCount = queryTokenCounts.get(match.queryToken) || 0;\n const currentCount = matchedCounts.get(match.queryToken) || 0;\n\n if (currentCount < neededCount) {\n // Track gap words between last match and current match\n for (let pos = lastPos + 1; pos < match.position; pos++) {\n totalGapUsed++;\n gapWords.push({\n word: documentTokens[pos],\n position: pos,\n gapIndex: totalGapUsed\n });\n }\n\n phraseWords.push(match);\n matchedCounts.set(match.queryToken, currentCount + 1);\n totalMatchedTokens++;\n\n // Stop if we have all query tokens (including duplicates)\n if (totalMatchedTokens === queryTokens.length) {\n break;\n }\n }\n }\n\n // Calculate phrase score\n if (phraseWords.length > 0) {\n const coverage = phraseWords.length / queryTokens.length;\n const span = phraseWords[phraseWords.length - 1].position - phraseWords[0].position + 1;\n\n const { score, breakdown } = calculatePhraseScore(\n phraseWords,\n queryTokens,\n config,\n documentFrequency,\n totalDocuments,\n allWordMatches,\n coverage\n );\n\n return {\n words: phraseWords,\n gapWords,\n gapUsed: totalGapUsed,\n coverage,\n startPosition: phraseWords[0].position,\n endPosition: phraseWords[phraseWords.length - 1].position,\n span,\n inOrder: isInOrder(phraseWords, queryTokens),\n score,\n scoreBreakdown: breakdown\n };\n }\n\n return null;\n}\n\n/**\n * Calculate overall phrase score\n * \n * @param phraseWords - Words in the phrase\n * @param queryTokens - Original query tokens\n * @param config - Scoring configuration\n * @param documentFrequency - Document frequency map\n * @param totalDocuments - Total document count\n * @param allWordMatches - All word matches in document (for density calculation)\n * @param coverage - Pre-calculated coverage ratio (phraseWords.length / queryTokens.length)\n * @returns Phrase score (0-1) and detailed component breakdown\n */\nfunction calculatePhraseScore(\n phraseWords: WordMatch[],\n queryTokens: string[],\n config: ScoringConfig,\n documentFrequency: Map<string, number>,\n totalDocuments: number,\n allWordMatches: WordMatch[],\n coverage: number\n): { score: number; breakdown: { base: number; order: number; proximity: number; density: number; semantic: number; coverage: number } } {\n // Base score from word matches\n // Each word contributes: matchScore × typeWeight\n let baseScore = 0;\n for (const word of phraseWords) {\n const weight = word.type === 'exact' ? config.weights.exact :\n word.type === 'fuzzy' ? config.weights.fuzzy :\n config.weights.fuzzy * 0.8; // synonym gets 80% of fuzzy weight\n baseScore += word.score * weight;\n }\n baseScore /= phraseWords.length;\n\n // Order bonus: 1.0 if words appear in query order, 0.5 otherwise\n const inOrder = isInOrder(phraseWords, queryTokens);\n const orderScore = inOrder ? 1.0 : 0.5;\n\n // Proximity bonus (closer words score higher)\n // Short-circuit: skip if maxGap=0, proximity weight is 0, or single-word query (proximity meaningless)\n let proximityScore = 0;\n if (config.maxGap > 0 && config.weights.proximity > 0 && queryTokens.length > 1) {\n const span = phraseWords[phraseWords.length - 1].position - phraseWords[0].position + 1;\n const proximityWindow = queryTokens.length * config.proximitySpanMultiplier;\n proximityScore = Math.max(0, 1.0 - (span / proximityWindow));\n }\n\n // Density: Only applies to single-word queries (measures word repetition in document)\n // For multi-word phrase queries, density is 0 (coverage handles completeness separately)\n let densityScore = 0;\n\n if (queryTokens.length === 1) {\n // Single-word query: reward repetition\n const totalOccurrences = allWordMatches.length;\n // Cap at reasonable maximum to avoid runaway scores\n densityScore = Math.min(1.0, totalOccurrences / 10);\n }\n // For multi-word queries: densityScore stays 0\n // Coverage is applied as a multiplier at the end instead\n\n // Semantic score (TF-IDF based)\n const semanticScore = calculateSemanticScore(\n phraseWords,\n documentFrequency,\n totalDocuments\n );\n\n // Weighted combination\n const weights = config.weights;\n\n // Calculate weighted components\n const weightedBase = baseScore;\n const weightedOrder = orderScore * weights.order;\n const weightedProximity = proximityScore * weights.proximity;\n const weightedDensity = densityScore * weights.density;\n const weightedSemantic = semanticScore * weights.semantic;\n\n const totalScore = weightedBase + weightedOrder + weightedProximity + weightedDensity + weightedSemantic;\n\n // Calculate max possible score\n // FIX: Use actual max base weight (highest of exact/fuzzy) instead of hardcoded 1.0\n // When tolerance=0 or fuzzy weight=0, only exact matches are possible\n const canHaveFuzzyMatches = config.tolerance > 0 && weights.fuzzy > 0;\n const maxBaseWeight = canHaveFuzzyMatches ? Math.max(weights.exact, weights.fuzzy) : weights.exact;\n // Only include proximity in max if it can actually contribute (avoids penalizing scores when maxGap=0 or single-word)\n const effectiveProximityWeight = (config.maxGap > 0 && weights.proximity > 0 && queryTokens.length > 1) ? weights.proximity : 0;\n const maxPossibleScore = maxBaseWeight + weights.order + effectiveProximityWeight + weights.density + weights.semantic;\n\n // Normalize to 0-1 range\n const normalizedScore = totalScore / maxPossibleScore;\n\n // FIX: Apply coverage as a MULTIPLIER for multi-word queries\n // This ensures incomplete matches (2/3) can never outscore complete matches (3/3)\n const coverageMultiplier = queryTokens.length > 1 ? coverage : 1.0;\n const score = normalizedScore * coverageMultiplier;\n\n // Component contributions to the final normalized score (before coverage multiplier)\n const base = weightedBase / maxPossibleScore;\n const order = weightedOrder / maxPossibleScore;\n const proximity = weightedProximity / maxPossibleScore;\n const density = weightedDensity / maxPossibleScore;\n const semantic = weightedSemantic / maxPossibleScore;\n\n return {\n score,\n breakdown: {\n base,\n order,\n proximity,\n density,\n semantic,\n coverage: coverageMultiplier // Show coverage multiplier in breakdown\n }\n };\n}\n\n/**\n * Check if phrase words appear in query order\n * Handles duplicate tokens (e.g., \"de ... de\") by tracking position consumption\n * \n * @param phraseWords - Words in the phrase\n * @param queryTokens - Original query tokens\n * @returns True if in order\n */\nfunction isInOrder(phraseWords: WordMatch[], queryTokens: string[]): boolean {\n // Build array of {token, index} to handle duplicates\n // e.g., [\"de\", \"maux\", ..., \"de\"] → [{token:\"de\", idx:0}, {token:\"maux\", idx:1}, ..., {token:\"de\", idx:7}]\n const tokenPositions = queryTokens.map((token, index) => ({ token, index }));\n\n let lastMatchedIndex = -1;\n\n for (const phraseWord of phraseWords) {\n // Find the first unused position for this token that's after lastMatchedIndex\n let foundIndex = -1;\n for (const pos of tokenPositions) {\n if (pos.token === phraseWord.queryToken && pos.index > lastMatchedIndex) {\n foundIndex = pos.index;\n break;\n }\n }\n\n if (foundIndex === -1) {\n // Token not found in expected position - out of order\n return false;\n }\n\n lastMatchedIndex = foundIndex;\n }\n\n return true;\n}\n\n/**\n * Calculate semantic score using TF-IDF\n * \n * @param phraseWords - Words in the phrase\n * @param documentFrequency - Document frequency map\n * @param totalDocuments - Total document count\n * @returns Semantic score (0-1)\n */\nfunction calculateSemanticScore(\n phraseWords: WordMatch[],\n documentFrequency: Map<string, number>,\n totalDocuments: number\n): number {\n // Handle edge case: no documents\n if (totalDocuments === 0) {\n return 0;\n }\n\n let tfidfSum = 0;\n\n for (const word of phraseWords) {\n const df = documentFrequency.get(word.word) || 1;\n const idf = Math.log(totalDocuments / df);\n tfidfSum += idf;\n }\n\n // Normalize by phrase length\n const avgTfidf = tfidfSum / phraseWords.length;\n\n // Normalize to 0-1 range (assuming max IDF of ~10)\n return Math.min(1.0, avgTfidf / 10);\n}\n\n/**\n * Deduplicate overlapping phrases, keeping highest scoring ones\n * \n * @param phrases - Array of phrase matches\n * @returns Deduplicated phrases sorted by score\n */\nfunction deduplicatePhrases(phrases: PhraseMatch[]): PhraseMatch[] {\n if (phrases.length === 0) return [];\n\n // Sort by score descending\n const sorted = phrases.slice().sort((a, b) => b.score - a.score);\n const result: PhraseMatch[] = [];\n const covered = new Set<number>();\n\n for (const phrase of sorted) {\n // Check if this phrase overlaps with already selected phrases\n let overlaps = false;\n for (let pos = phrase.startPosition; pos <= phrase.endPosition; pos++) {\n if (covered.has(pos)) {\n overlaps = true;\n break;\n }\n }\n\n if (!overlaps) {\n result.push(phrase);\n // Mark positions as covered\n for (let pos = phrase.startPosition; pos <= phrase.endPosition; pos++) {\n covered.add(pos);\n }\n }\n }\n\n return result.sort((a, b) => b.score - a.score);\n}\n","/**\r\n * Optimized Search: QPS Candidate Pruning + Fuzzy Phrase Scoring\r\n * \r\n * This module provides an algorithmic pruning path where:\r\n * 1. QPS (Quantum Proximity Scoring) finds candidate documents quickly (token-level, typo-tolerant)\r\n * 2. Fuzzy Phrase scoring runs only on those candidates (expensive phrase scoring on smaller set)\r\n * \r\n * This dramatically reduces the search space for phrase scoring while maintaining\r\n * the same result quality as full fuzzy phrase search.\r\n */\r\n\r\nimport type { AnyOrama, Results, TypedDocument } from '@wcs-colab/orama';\r\nimport type { FuzzyPhraseConfig, PluginState, DocumentMatch, Candidate } from './types.js';\r\nimport { calculateAdaptiveTolerance } from './fuzzy.js';\r\nimport { findAllCandidates, filterCandidatesByScore, extractVocabularyFromRadixTree } from './candidates.js';\r\nimport { findPhrasesInDocument, buildCandidateLookup, buildQueryTokenCounts } from './scoring.js';\r\n\r\n/**\r\n * QPS Index data structure (matches plugin-qps internal format)\r\n */\r\ninterface QPSIndexData {\r\n indexes: Record<string, {\r\n type: string;\r\n node: any; // RadixNode\r\n isArray: boolean;\r\n }>;\r\n stats: Record<string, {\r\n tokenQuantums: Record<string, Record<string, number>>;\r\n tokensLength: Map<string, number>;\r\n }>;\r\n searchableProperties: string[];\r\n searchablePropertiesWithTypes: Record<string, string>;\r\n vectorIndexes: Record<string, any>;\r\n}\r\n\r\n/**\r\n * Configuration for optimized search\r\n */\r\nexport interface OptimizedSearchConfig extends FuzzyPhraseConfig {\r\n /** Maximum number of QPS candidates to consider for phrase scoring */\r\n maxQPSCandidates?: number;\r\n /** Minimum QPS score to include a candidate (0-1, relative to best score) */\r\n minQPSScore?: number;\r\n /** Whether to use exact matching for QPS (faster but no typo tolerance) */\r\n qpsExact?: boolean;\r\n /** QPS tolerance (edit distance for fuzzy matching) */\r\n qpsTolerance?: number;\r\n /** Boost factors per property for QPS scoring */\r\n qpsBoostPerProp?: Record<string, number>;\r\n}\r\n\r\n/**\r\n * Default optimized search configuration\r\n */\r\nconst DEFAULT_OPTIMIZED_CONFIG: Required<Pick<OptimizedSearchConfig,\r\n 'maxQPSCandidates' | 'minQPSScore' | 'qpsExact' | 'qpsTolerance'>> = {\r\n maxQPSCandidates: 100, // Limit phrase scoring to top 100 candidates\r\n minQPSScore: 0.1, // Include candidates with 10%+ of best score\r\n qpsExact: false, // Use fuzzy matching by default\r\n qpsTolerance: 1 // Default tolerance of 1 edit distance\r\n};\r\n\r\n/**\r\n * Normalize text using the same rules as Orama's French tokenizer\r\n * \r\n * CRITICAL: This must match normalizeFrenchText() in Orama's tokenizer exactly\r\n */\r\nexport function normalizeText(text: string): string {\r\n return text\r\n .toLowerCase()\r\n .normalize('NFD')\r\n .replace(/[\\u0300-\\u036f]/g, '') // Remove diacritics\r\n // Replace French elisions (l', d', etc.) with space to preserve word boundaries\r\n .replace(/\\b[ldcjmnst][\\u2018\\u2019\\u201A\\u201B\\u2032\\u2035\\u0027\\u0060\\u00B4](?=\\w)/gi, ' ')\r\n .replace(/[\\u2018\\u2019\\u201A\\u201B\\u2032\\u2035\\u0027\\u0060\\u00B4]/g, '') // Remove remaining apostrophes\r\n .replace(/[\\u201c\\u201d]/g, '\"') // Normalize curly quotes to straight quotes\r\n .replace(/[.,;:!?()[\\]{}\\-—–«»\"\"]/g, ' ') // Remove punctuation (replace with space to preserve word boundaries)\r\n .replace(/\\s+/g, ' ') // Normalize multiple spaces to single space\r\n .trim();\r\n}\r\n\r\n/**\r\n * Tokenize text using normalization matching Orama's French tokenizer\r\n */\r\nexport function tokenize(text: string): string[] {\r\n return normalizeText(text)\r\n .split(/\\s+/)\r\n .filter(token => token.length > 0);\r\n}\r\n\r\n/**\r\n * PHASE 2 OPTIMIZATION C: Build reduced vocabulary from candidate documents only\r\n * \r\n * When QPS returns N candidates, we only score those N docs.\r\n * Fuzzy-matching query tokens against the FULL vocabulary wastes work on words\r\n * that don't appear in any candidate doc. Build vocabulary from candidates only.\r\n * \r\n * @param candidateDocIds - Set of candidate document IDs from QPS\r\n * @param docs - Documents object from Orama\r\n * @returns Set of unique tokens from candidate documents only\r\n */\r\nfunction buildReducedVocabularyFromDocs(\r\n candidateDocIds: Set<string>,\r\n docs: Record<string, any>\r\n): Set<string> {\r\n const reducedVocab = new Set<string>();\r\n for (const docId of candidateDocIds) {\r\n const doc = docs[docId];\r\n if (!doc?.normalized_content) continue;\r\n // normalized_content is already normalized, just split into tokens\r\n const tokens = doc.normalized_content.split(/\\s+/).filter((token: string) => token.length > 0);\r\n for (const token of tokens) {\r\n reducedVocab.add(token);\r\n }\r\n }\r\n return reducedVocab;\r\n}\r\n\r\n/**\r\n * Call QPS search to get candidate document IDs with scores\r\n * \r\n * This function interfaces directly with QPS's internal search function,\r\n * returning candidate documents ranked by token proximity.\r\n * \r\n * @param term - Search term\r\n * @param qpsIndex - QPS index data\r\n * @param tokenizer - Tokenizer instance from Orama\r\n * @param properties - Properties to search\r\n * @param config - Optimized search configuration\r\n * @returns Array of [docId, score] tuples\r\n */\r\nfunction searchQPS(\r\n term: string,\r\n qpsIndex: QPSIndexData,\r\n tokenizer: any,\r\n properties: string[],\r\n config: OptimizedSearchConfig,\r\n language?: string\r\n): Array<[string, number]> {\r\n const tokens = tokenizer.tokenize(term, language);\r\n\r\n if (tokens.length === 0) {\r\n return [];\r\n }\r\n\r\n // QPS search configuration\r\n const exact = config.qpsExact ?? DEFAULT_OPTIMIZED_CONFIG.qpsExact;\r\n const tolerance = config.qpsTolerance ?? DEFAULT_OPTIMIZED_CONFIG.qpsTolerance;\r\n const boostPerProp = config.qpsBoostPerProp ?? {};\r\n\r\n // Results map: docId -> [score, tokenBitmask]\r\n const resultMap = new Map<string, [number, number]>();\r\n\r\n // Search each property\r\n for (const prop of properties) {\r\n const indexEntry = qpsIndex.indexes[prop];\r\n if (!indexEntry || indexEntry.type !== 'Radix') {\r\n continue;\r\n }\r\n\r\n const radixNode = indexEntry.node;\r\n const stats = qpsIndex.stats[prop];\r\n\r\n if (!radixNode || !stats) {\r\n continue;\r\n }\r\n\r\n const boost = boostPerProp[prop] ?? 1;\r\n\r\n // Search the radix tree for each token\r\n for (let i = 0; i < tokens.length; i++) {\r\n const token = tokens[i];\r\n\r\n // Find matches in radix tree (exact or fuzzy)\r\n const matches = radixNode.find({\r\n term: token,\r\n exact,\r\n tolerance: exact ? 0 : tolerance\r\n });\r\n\r\n // Process matches\r\n for (const [matchedWord, docIds] of Object.entries(matches)) {\r\n if (!Array.isArray(docIds)) continue;\r\n\r\n const isExactMatch = matchedWord === token;\r\n\r\n for (const docId of docIds as string[]) {\r\n const tokensLength = stats.tokensLength.get(docId) || 1;\r\n const quantum = stats.tokenQuantums[docId]?.[matchedWord];\r\n\r\n // Extract occurrence count and position bitmask from quantum\r\n const occurrences = quantum ? (quantum >> 20) : 1;\r\n const posBitmask = quantum ? (quantum & 0xFFFFF) : 0;\r\n\r\n // Calculate score contribution\r\n const scoreContrib = ((occurrences * occurrences) / tokensLength + (isExactMatch ? 1 : 0)) * boost;\r\n\r\n if (!resultMap.has(docId)) {\r\n resultMap.set(docId, [scoreContrib, 1 << i]);\r\n } else {\r\n const [prevScore, prevMask] = resultMap.get(docId)!;\r\n // Add proximity bonus for adjacent tokens\r\n const adjacencyBonus = countSetBits((prevMask >> 1) & (1 << i)) * 2;\r\n resultMap.set(docId, [prevScore + scoreContrib + adjacencyBonus, prevMask | (1 << i)]);\r\n }\r\n }\r\n }\r\n }\r\n }\r\n\r\n // Convert to sorted array\r\n const results = Array.from(resultMap.entries())\r\n .map(([docId, [score]]) => [docId, score] as [string, number])\r\n .sort((a, b) => b[1] - a[1]);\r\n\r\n return results;\r\n}\r\n\r\n/**\r\n * Count set bits in a number (popcount)\r\n */\r\nfunction countSetBits(n: number): number {\r\n let count = 0;\r\n while (n) {\r\n count += n & 1;\r\n n >>= 1;\r\n }\r\n return count;\r\n}\r\n\r\n/**\r\n * Get plugin state from WeakMap\r\n * Note: This function accesses the internal pluginStates WeakMap\r\n */\r\nfunction getPluginState(orama: AnyOrama, pluginStates: WeakMap<AnyOrama, PluginState>): PluginState | undefined {\r\n return pluginStates.get(orama);\r\n}\r\n\r\n/**\r\n * Optimized search combining QPS candidate pruning with fuzzy phrase scoring\r\n * \r\n * This is the main entry point for optimized search. It:\r\n * 1. Uses QPS to quickly find candidate documents (token-level matching)\r\n * 2. Filters to top candidates by QPS score\r\n * 3. Runs full fuzzy phrase scoring only on those candidates\r\n * \r\n * @param orama - Orama instance\r\n * @param qpsIndex - QPS index data (from pluginQPS.getComponents().index)\r\n * @param pluginState - Fuzzy phrase plugin state\r\n * @param params - Search parameters\r\n * @param config - Optimized search configuration\r\n * @param language - Language for tokenization (default: french)\r\n * @returns Search results with fuzzy phrase scoring\r\n */\r\nexport async function searchWithQPSPruning<T extends AnyOrama>(\r\n orama: T,\r\n qpsIndex: QPSIndexData,\r\n pluginState: PluginState,\r\n params: {\r\n term?: string;\r\n properties?: string[];\r\n limit?: number;\r\n tokenCache?: Map<string, string[]>;\r\n },\r\n config: OptimizedSearchConfig = {},\r\n language: string = 'french'\r\n): Promise<Results<TypedDocument<T>>> {\r\n const startTime = performance.now();\r\n\r\n const { term, properties, tokenCache } = params;\r\n\r\n if (!term || typeof term !== 'string') {\r\n return { elapsed: { formatted: '0ms', raw: 0 }, hits: [], count: 0 };\r\n }\r\n\r\n // Use specified property or default from plugin config\r\n const textProperty = (properties && properties[0]) || pluginState.config.textProperty;\r\n const searchProperties = properties || [textProperty];\r\n\r\n // Tokenize query\r\n const queryTokens = tokenize(term);\r\n\r\n if (queryTokens.length === 0) {\r\n return { elapsed: { formatted: '0ms', raw: 0 }, hits: [], count: 0 };\r\n }\r\n\r\n // Calculate tolerance (adaptive or fixed)\r\n const tolerance = pluginState.config.adaptiveTolerance\r\n ? calculateAdaptiveTolerance(queryTokens, pluginState.config.tolerance)\r\n : pluginState.config.tolerance;\r\n\r\n console.log(`🚀 Optimized search: \"${term}\" (${queryTokens.length} tokens, tolerance: ${tolerance})`);\r\n\r\n // STEP 1: Use QPS to find candidate documents\r\n const qpsStartTime = performance.now();\r\n\r\n // Get tokenizer from Orama\r\n const tokenizer = (orama as any).tokenizer;\r\n\r\n const qpsCandidates = searchQPS(\r\n term,\r\n qpsIndex,\r\n tokenizer,\r\n searchProperties,\r\n config,\r\n language\r\n );\r\n\r\n const qpsTime = performance.now() - qpsStartTime;\r\n console.log(`⚡ QPS found ${qpsCandidates.length} candidates in ${qpsTime.toFixed(2)}ms`);\r\n\r\n if (qpsCandidates.length === 0) {\r\n return { elapsed: { formatted: '0ms', raw: 0 }, hits: [], count: 0 };\r\n }\r\n\r\n // STEP 2: Filter to top candidates\r\n const maxCandidates = config.maxQPSCandidates ?? DEFAULT_OPTIMIZED_CONFIG.maxQPSCandidates;\r\n const minScoreRatio = config.minQPSScore ?? DEFAULT_OPTIMIZED_CONFIG.minQPSScore;\r\n\r\n const bestScore = qpsCandidates[0][1];\r\n const minScore = bestScore * minScoreRatio;\r\n\r\n const filteredCandidates = qpsCandidates\r\n .filter(([, score]) => score >= minScore)\r\n .slice(0, maxCandidates);\r\n\r\n console.log(`📋 Filtered to ${filteredCandidates.length} candidates (min score: ${minScore.toFixed(2)})`);\r\n\r\n // STEP 3: Get candidate document IDs as a Set for fast lookup\r\n // Convert to strings since Object.entries returns string keys\r\n const candidateDocIds = new Set(filteredCandidates.map(([docId]) => String(docId)));\r\n\r\n // STEP 4: Find fuzzy candidates for phrase scoring\r\n // Vocabulary is lazily extracted on first search (needed for fallback and logging)\r\n let vocabulary = pluginState.vocabulary;\r\n\r\n if (vocabulary.size === 0) {\r\n console.log('📚 Vocabulary not initialized - extracting from index...');\r\n try {\r\n const indexData = (orama as any).data?.index;\r\n let radixNode = null;\r\n\r\n if (indexData?.indexes?.[textProperty]?.node) {\r\n radixNode = indexData.indexes[textProperty].node;\r\n } else if (indexData?.[textProperty]?.node) {\r\n radixNode = indexData[textProperty].node;\r\n }\r\n\r\n if (radixNode) {\r\n pluginState.vocabulary = extractVocabularyFromRadixTree(radixNode);\r\n vocabulary = pluginState.vocabulary;\r\n console.log(`📚 Extracted ${vocabulary.size} vocabulary words`);\r\n } else {\r\n console.error('❌ Radix tree not found for vocabulary extraction');\r\n return { elapsed: { formatted: '0ms', raw: 0 }, hits: [], count: 0 };\r\n }\r\n } catch (error) {\r\n console.error('❌ Failed to extract vocabulary:', error);\r\n return { elapsed: { formatted: '0ms', raw: 0 }, hits: [], count: 0 };\r\n }\r\n }\r\n\r\n // PHASE 2 OPTIMIZATION C: Build reduced vocabulary from candidate documents only\r\n // Extract docs object early so we can build vocabulary from candidates only\r\n let docs: Record<string, any> = {};\r\n if ((orama as any).data?.docs?.docs) {\r\n docs = (orama as any).data.docs.docs;\r\n }\r\n\r\n const vocabStartTime = performance.now();\r\n const reducedVocabulary = buildReducedVocabularyFromDocs(candidateDocIds, docs);\r\n const vocabTime = performance.now() - vocabStartTime;\r\n console.log(`📚 Reduced vocabulary: ${reducedVocabulary.size} words (full: ${vocabulary.size}, reduction: ${(100 * (1 - reducedVocabulary.size / vocabulary.size)).toFixed(1)}%, built in ${vocabTime.toFixed(2)}ms)`);\r\n\r\n // Find candidates for all query tokens - using REDUCED vocabulary\r\n const candidatesMap = findAllCandidates(\r\n queryTokens,\r\n reducedVocabulary,\r\n tolerance,\r\n pluginState.config.enableSynonyms ? pluginState.synonymMap : undefined,\r\n pluginState.config.synonymMatchScore\r\n );\r\n\r\n // Filter by minimum score (skip when tolerance=0 since all matches are exact with score 1.0)\r\n const filteredFuzzyCandidates = tolerance === 0\r\n ? candidatesMap\r\n : filterCandidatesByScore(candidatesMap, pluginState.config.minScore);\r\n\r\n console.log(`🎯 Fuzzy candidates: ${Array.from(filteredFuzzyCandidates.values()).reduce((sum, c) => sum + c.length, 0)} total`);\r\n\r\n // STEP 5: Run phrase scoring ONLY on QPS candidate documents\r\n const phraseStartTime = performance.now();\r\n\r\n // PHASE 1 OPTIMIZATION: Build candidateLookup and queryTokenCounts once per query\r\n const candidateLookup = buildCandidateLookup(filteredFuzzyCandidates);\r\n const queryTokenCounts = buildQueryTokenCounts(queryTokens);\r\n\r\n const documentMatches: DocumentMatch[] = [];\r\n // Note: docs already extracted above for reduced vocabulary optimization\r\n\r\n let docsScored = 0;\r\n\r\n for (const [docId, doc] of Object.entries(docs)) {\r\n // PRUNING: Only score documents that are in QPS candidates\r\n if (!candidateDocIds.has(docId)) {\r\n continue;\r\n }\r\n\r\n docsScored++;\r\n\r\n const text = doc[textProperty];\r\n if (!text || typeof text !== 'string') {\r\n continue;\r\n }\r\n\r\n // Use cached tokens if available, otherwise split\r\n let docTokens: string[];\r\n if (tokenCache && tokenCache.has(docId)) {\r\n docTokens = tokenCache.get(docId)!;\r\n } else {\r\n // textProperty is 'normalized_content' which is already normalized, just split\r\n docTokens = text.split(/\\s+/).filter((token: string) => token.length > 0);\r\n }\r\n\r\n // Find phrases in this document\r\n const phrases = findPhrasesInDocument(\r\n docTokens,\r\n filteredFuzzyCandidates,\r\n {\r\n weights: pluginState.config.weights as { exact: number; fuzzy: number; order: number; proximity: number; density: number; semantic: number },\r\n maxGap: pluginState.config.maxGap,\r\n proximitySpanMultiplier: pluginState.config.proximitySpanMultiplier,\r\n tolerance\r\n },\r\n pluginState.documentFrequency,\r\n pluginState.totalDocuments,\r\n queryTokens,\r\n candidateLookup, // PHASE 1 OPTIMIZATION A: Pre-built candidate lookup\r\n queryTokenCounts // PHASE 1 OPTIMIZATION B: Pre-built query token counts\r\n );\r\n\r\n if (phrases.length > 0) {\r\n const docScore = Math.max(...phrases.map(p => p.score));\r\n documentMatches.push({\r\n id: docId,\r\n phrases,\r\n score: docScore,\r\n document: doc\r\n });\r\n }\r\n }\r\n\r\n const phraseTime = performance.now() - phraseStartTime;\r\n console.log(`📊 Phrase scored ${docsScored} documents in ${phraseTime.toFixed(2)}ms`);\r\n\r\n // Sort by score descending\r\n documentMatches.sort((a, b) => b.score - a.score);\r\n\r\n // Apply final score minimum filter if enabled\r\n let finalMatches = documentMatches;\r\n if (pluginState.config.enableFinalScoreMinimum && pluginState.config.finalScoreMinimum > 0) {\r\n const threshold = pluginState.config.finalScoreMinimum;\r\n const beforeCount = finalMatches.length;\r\n finalMatches = finalMatches.filter(m => m.score >= threshold);\r\n console.log(`🎚️ Final score filter: ${beforeCount} → ${finalMatches.length} (threshold: ${threshold})`);\r\n }\r\n\r\n // Apply limit if specified\r\n const limit = params.limit ?? finalMatches.length;\r\n const limitedMatches = finalMatches.slice(0, limit);\r\n\r\n // Convert to Orama results format\r\n const hits = limitedMatches.map(match => ({\r\n id: match.id,\r\n score: match.score,\r\n document: match.document,\r\n _phrases: match.phrases\r\n })) as any[];\r\n\r\n const elapsed = performance.now() - startTime;\r\n\r\n console.log(`✅ Optimized search: ${hits.length} results in ${elapsed.toFixed(2)}ms (QPS: ${qpsTime.toFixed(2)}ms, Phrase: ${phraseTime.toFixed(2)}ms)`);\r\n\r\n return {\r\n elapsed: {\r\n formatted: `${elapsed.toFixed(2)}ms`,\r\n raw: Math.floor(elapsed * 1000000),\r\n qpsTime,\r\n phraseTime\r\n },\r\n hits,\r\n count: hits.length\r\n } as any;\r\n}\r\n\r\n/**\r\n * Create an optimized search function bound to a specific Orama + QPS index\r\n * \r\n * This is a convenience wrapper that captures the Orama instance and QPS index,\r\n * returning a simpler search function.\r\n * \r\n * @param orama - Orama instance\r\n * @param qpsIndex - QPS index data\r\n * @param pluginState - Fuzzy phrase plugin state\r\n * @param config - Optimized search configuration\r\n * @returns Bound search function\r\n */\r\nexport function createOptimizedSearch<T extends AnyOrama>(\r\n orama: T,\r\n qpsIndex: QPSIndexData,\r\n pluginState: PluginState,\r\n config: OptimizedSearchConfig = {}\r\n) {\r\n return async (params: {\r\n term?: string;\r\n properties?: string[];\r\n limit?: number;\r\n tokenCache?: Map<string, string[]>;\r\n }, language: string = 'french'): Promise<Results<TypedDocument<T>>> => {\r\n return searchWithQPSPruning(orama, qpsIndex, pluginState, params, config, language);\r\n };\r\n}\r\n","/**\r\n * Fuzzy Phrase Plugin for Orama\r\n * \r\n * Advanced fuzzy phrase matching with semantic weighting and synonym expansion.\r\n * Completely independent from QPS - accesses Orama's radix tree directly.\r\n */\r\n\r\nimport type { AnyOrama, OramaPlugin, Results, TypedDocument } from '@wcs-colab/orama';\r\nimport type { FuzzyPhraseConfig, PluginState, SynonymMap, DocumentMatch } from './types.js';\r\nimport { calculateAdaptiveTolerance } from './fuzzy.js';\r\nimport {\r\n extractVocabularyFromRadixTree,\r\n findAllCandidates,\r\n filterCandidatesByScore\r\n} from './candidates.js';\r\nimport {\r\n findPhrasesInDocument,\r\n buildCandidateLookup,\r\n buildQueryTokenCounts,\r\n type CandidateLookup\r\n} from './scoring.js';\r\n\r\n/**\r\n * Default configuration\r\n */\r\nconst DEFAULT_CONFIG: Required<FuzzyPhraseConfig> = {\r\n textProperty: 'normalized_content', // Must match server's field name\r\n tolerance: 1,\r\n adaptiveTolerance: true,\r\n enableSynonyms: false,\r\n supabase: undefined as any,\r\n synonymMatchScore: 0.8,\r\n weights: {\r\n exact: 1.0,\r\n fuzzy: 0.8,\r\n order: 0.3,\r\n proximity: 0.2,\r\n density: 0.2,\r\n semantic: 0.15\r\n },\r\n maxGap: 5,\r\n minScore: 0.1,\r\n enableFinalScoreMinimum: false,\r\n finalScoreMinimum: 0.3,\r\n proximitySpanMultiplier: 5\r\n};\r\n\r\n/**\r\n * Plugin state storage (keyed by Orama instance)\r\n */\r\nconst pluginStates = new WeakMap<AnyOrama, PluginState>();\r\n\r\n/**\r\n * Create the Fuzzy Phrase Plugin\r\n * \r\n * @param userConfig - User configuration options\r\n * @returns Orama plugin instance\r\n */\r\nexport function pluginFuzzyPhrase(userConfig: FuzzyPhraseConfig = {}): OramaPlugin {\r\n // Merge user config with defaults\r\n const config: Required<FuzzyPhraseConfig> = {\r\n textProperty: userConfig.textProperty ?? DEFAULT_CONFIG.textProperty,\r\n tolerance: userConfig.tolerance ?? DEFAULT_CONFIG.tolerance,\r\n adaptiveTolerance: userConfig.adaptiveTolerance ?? DEFAULT_CONFIG.adaptiveTolerance,\r\n enableSynonyms: userConfig.enableSynonyms ?? DEFAULT_CONFIG.enableSynonyms,\r\n supabase: userConfig.supabase || DEFAULT_CONFIG.supabase,\r\n synonymMatchScore: userConfig.synonymMatchScore ?? DEFAULT_CONFIG.synonymMatchScore,\r\n weights: {\r\n exact: userConfig.weights?.exact ?? DEFAULT_CONFIG.weights.exact,\r\n fuzzy: userConfig.weights?.fuzzy ?? DEFAULT_CONFIG.weights.fuzzy,\r\n order: userConfig.weights?.order ?? DEFAULT_CONFIG.weights.order,\r\n proximity: userConfig.weights?.proximity ?? DEFAULT_CONFIG.weights.proximity,\r\n density: userConfig.weights?.density ?? DEFAULT_CONFIG.weights.density,\r\n semantic: userConfig.weights?.semantic ?? DEFAULT_CONFIG.weights.semantic\r\n },\r\n maxGap: userConfig.maxGap ?? DEFAULT_CONFIG.maxGap,\r\n minScore: userConfig.minScore ?? DEFAULT_CONFIG.minScore,\r\n enableFinalScoreMinimum: userConfig.enableFinalScoreMinimum ?? DEFAULT_CONFIG.enableFinalScoreMinimum,\r\n finalScoreMinimum: userConfig.finalScoreMinimum ?? DEFAULT_CONFIG.finalScoreMinimum,\r\n proximitySpanMultiplier: userConfig.proximitySpanMultiplier ?? DEFAULT_CONFIG.proximitySpanMultiplier\r\n };\r\n\r\n const plugin: OramaPlugin = {\r\n name: 'fuzzy-phrase',\r\n\r\n /**\r\n * Initialize plugin after index is created\r\n */\r\n afterCreate: async (orama: AnyOrama) => {\r\n console.log('🔮 Initializing Fuzzy Phrase Plugin...');\r\n\r\n // Initialize state with empty vocabulary (will be populated below)\r\n const state: PluginState = {\r\n synonymMap: {},\r\n config,\r\n documentFrequency: new Map(),\r\n totalDocuments: 0,\r\n vocabulary: new Set()\r\n };\r\n\r\n // Load synonyms from Supabase if enabled\r\n if (config.enableSynonyms && config.supabase) {\r\n try {\r\n console.log('📖 Loading synonyms from Supabase...');\r\n state.synonymMap = await loadSynonymsFromSupabase(config.supabase);\r\n console.log(`✅ Loaded ${Object.keys(state.synonymMap).length} words with synonyms`);\r\n } catch (error) {\r\n console.error('⚠️ Failed to load synonyms:', error);\r\n // Continue without synonyms\r\n }\r\n }\r\n\r\n // Calculate document frequencies for TF-IDF from document store\r\n const docs = (orama.data as any)?.docs?.docs;\r\n if (docs) {\r\n state.totalDocuments = Object.keys(docs).length;\r\n state.documentFrequency = calculateDocumentFrequencies(docs, config.textProperty);\r\n console.log(`📊 Calculated document frequencies for ${state.totalDocuments} documents`);\r\n }\r\n\r\n // CACHE VOCABULARY: Extract from radix tree ONCE at startup\r\n // This avoids O(V) radix traversal on every query\r\n try {\r\n const indexData = (orama as any).data?.index;\r\n let radixNode = null;\r\n\r\n if (indexData?.indexes?.[config.textProperty]?.node) {\r\n radixNode = indexData.indexes[config.textProperty].node;\r\n } else if (indexData?.[config.textProperty]?.node) {\r\n radixNode = indexData[config.textProperty].node;\r\n }\r\n\r\n if (radixNode) {\r\n state.vocabulary = extractVocabularyFromRadixTree(radixNode);\r\n console.log(`📚 Cached ${state.vocabulary.size} vocabulary words (eliminates per-query radix traversal)`);\r\n } else {\r\n console.warn('⚠️ Could not find radix tree for vocabulary caching');\r\n }\r\n } catch (error) {\r\n console.error('⚠️ Failed to cache vocabulary:', error);\r\n }\r\n\r\n // Store state\r\n pluginStates.set(orama, state);\r\n console.log('✅ Fuzzy Phrase Plugin initialized');\r\n\r\n // Signal ready - emit a custom event that can be listened to\r\n // Use setImmediate to ensure this runs after the afterCreate hook completes\r\n setImmediate(() => {\r\n if (typeof (globalThis as any).fuzzyPhrasePluginReady === 'function') {\r\n console.log('📡 Signaling plugin ready...');\r\n (globalThis as any).fuzzyPhrasePluginReady();\r\n } else {\r\n console.warn('⚠️ fuzzyPhrasePluginReady callback not found');\r\n }\r\n });\r\n }\r\n };\r\n\r\n return plugin;\r\n}\r\n\r\n/**\r\n * Search with fuzzy phrase matching\r\n * \r\n * This function should be called instead of the regular search() function\r\n * to enable fuzzy phrase matching.\r\n */\r\nexport async function searchWithFuzzyPhrase<T extends AnyOrama>(\r\n orama: T,\r\n params: {\r\n term?: string;\r\n properties?: string[];\r\n limit?: number;\r\n tokenCache?: Map<string, string[]>; // Pre-tokenized documents for fast search\r\n candidateIds?: Set<string> | string[]; // Optional: only score these doc IDs (for QPS pruning)\r\n },\r\n language?: string\r\n): Promise<Results<TypedDocument<T>>> {\r\n const startTime = performance.now();\r\n\r\n // Get plugin state\r\n const state = pluginStates.get(orama);\r\n\r\n if (!state) {\r\n console.error('❌ Plugin state not initialized');\r\n throw new Error('Fuzzy Phrase Plugin not properly initialized');\r\n }\r\n\r\n const { term, properties, tokenCache, candidateIds } = params;\r\n\r\n // Convert candidateIds to Set for fast lookup if provided\r\n const candidateIdSet: Set<string> | null = candidateIds\r\n ? (candidateIds instanceof Set ? candidateIds : new Set(candidateIds))\r\n : null;\r\n\r\n if (!term || typeof term !== 'string') {\r\n return { elapsed: { formatted: '0ms', raw: 0 }, hits: [], count: 0 };\r\n }\r\n\r\n // Use specified property or default\r\n const textProperty = (properties && properties[0]) || state.config.textProperty;\r\n\r\n // Tokenize query\r\n const queryTokens = tokenize(term);\r\n\r\n if (queryTokens.length === 0) {\r\n return { elapsed: { formatted: '0ms', raw: 0 }, hits: [], count: 0 };\r\n }\r\n\r\n // Calculate tolerance (adaptive or fixed)\r\n const tolerance = state.config.adaptiveTolerance\r\n ? calculateAdaptiveTolerance(queryTokens, state.config.tolerance)\r\n : state.config.tolerance;\r\n\r\n console.log(`🔍 Fuzzy phrase search: \"${term}\" (${queryTokens.length} tokens, tolerance: ${tolerance})`);\r\n\r\n // USE CACHED VOCABULARY - lazy initialization on first search\r\n // afterCreate runs before documents are inserted, so we extract on first search\r\n let vocabulary = state.vocabulary;\r\n\r\n if (vocabulary.size === 0) {\r\n console.log('📚 Vocabulary cache empty - extracting on first search...');\r\n try {\r\n const indexData = (orama as any).data?.index;\r\n let radixNode = null;\r\n\r\n if (indexData?.indexes?.[textProperty]?.node) {\r\n radixNode = indexData.indexes[textProperty].node;\r\n } else if (indexData?.[textProperty]?.node) {\r\n radixNode = indexData[textProperty].node;\r\n }\r\n\r\n if (radixNode) {\r\n state.vocabulary = extractVocabularyFromRadixTree(radixNode);\r\n vocabulary = state.vocabulary;\r\n console.log(`📚 Cached ${vocabulary.size} vocabulary words (will be reused for subsequent queries)`);\r\n } else {\r\n console.error('❌ Radix tree not found for vocabulary extraction');\r\n return { elapsed: { formatted: '0ms', raw: 0 }, hits: [], count: 0 };\r\n }\r\n } catch (error) {\r\n console.error('❌ Failed to extract vocabulary:', error);\r\n return { elapsed: { formatted: '0ms', raw: 0 }, hits: [], count: 0 };\r\n }\r\n } else {\r\n console.log(`📚 Using cached vocabulary (${vocabulary.size} words)`);\r\n }\r\n\r\n // Find candidates for all query tokens\r\n const candidatesMap = findAllCandidates(\r\n queryTokens,\r\n vocabulary,\r\n tolerance,\r\n state.config.enableSynonyms ? state.synonymMap : undefined,\r\n state.config.synonymMatchScore\r\n );\r\n\r\n // Filter by minimum score (skip when tolerance=0 since all matches are exact with score 1.0)\r\n const filteredCandidates = tolerance === 0\r\n ? candidatesMap // Skip filtering - all matches are exact\r\n : filterCandidatesByScore(candidatesMap, state.config.minScore);\r\n\r\n console.log(`🎯 Found candidates: ${Array.from(filteredCandidates.values()).reduce((sum, c) => sum + c.length, 0)} total`);\r\n\r\n // PHASE 1 OPTIMIZATION: Build candidateLookup and queryTokenCounts once per query\r\n const candidateLookup = buildCandidateLookup(filteredCandidates);\r\n const queryTokenCounts = buildQueryTokenCounts(queryTokens);\r\n\r\n\r\n // Search through all documents\r\n const documentMatches: DocumentMatch[] = [];\r\n\r\n console.log('🔍 DEBUG orama.data structure:', {\r\n dataKeys: Object.keys((orama as any).data || {}),\r\n hasDocs: !!((orama as any).data?.docs),\r\n docsType: (orama as any).data?.docs ? typeof (orama as any).data.docs : 'undefined'\r\n });\r\n\r\n // Try multiple possible document storage locations\r\n let docs: Record<string, any> = {};\r\n\r\n // Access the actual documents - they're nested in orama.data.docs.docs\r\n if ((orama as any).data?.docs?.docs) {\r\n docs = (orama as any).data.docs.docs;\r\n console.log('✅ Found docs at orama.data.docs.docs');\r\n }\r\n // Fallback: orama.data.docs (might be the correct structure in some cases)\r\n else if ((orama as any).data?.docs && typeof (orama as any).data.docs === 'object') {\r\n // Check if it has document-like properties (not sharedInternalDocumentStore, etc.)\r\n const firstKey = Object.keys((orama as any).data.docs)[0];\r\n if (firstKey && firstKey !== 'sharedInternalDocumentStore' && firstKey !== 'count') {\r\n docs = (orama as any).data.docs;\r\n console.log('✅ Found docs at orama.data.docs (direct)');\r\n }\r\n }\r\n\r\n if (Object.keys(docs).length === 0) {\r\n console.log('❌ Could not find documents - available structure:', {\r\n hasDataDocs: !!((orama as any).data?.docs),\r\n dataDocsKeys: (orama as any).data?.docs ? Object.keys((orama as any).data.docs) : 'none',\r\n hasDataDocsDocs: !!((orama as any).data?.docs?.docs),\r\n dataDocsDocsCount: (orama as any).data?.docs?.docs ? Object.keys((orama as any).data.docs.docs).length : 0\r\n });\r\n }\r\n\r\n const cacheHits = tokenCache ? tokenCache.size : 0;\r\n const docsToSearch = candidateIdSet ? candidateIdSet.size : Object.keys(docs).length;\r\n console.log(`📄 Searching through ${docsToSearch} documents${candidateIdSet ? ' (pruned by candidateIds)' : ''} (${cacheHits > 0 ? `${cacheHits} tokens cached` : 'no cache'})`);\r\n\r\n for (const [docId, doc] of Object.entries(docs)) {\r\n // PRUNING: Skip docs not in candidateIds if provided\r\n // candidateIds can contain user document IDs (doc.id) OR internal Orama keys\r\n if (candidateIdSet) {\r\n const userDocId = doc.id !== undefined ? String(doc.id) : docId;\r\n if (!candidateIdSet.has(userDocId) && !candidateIdSet.has(docId)) {\r\n continue;\r\n }\r\n }\r\n\r\n const text = doc[textProperty];\r\n\r\n if (!text || typeof text !== 'string') {\r\n continue;\r\n }\r\n\r\n // Use cached tokens if available, otherwise tokenize\r\n let docTokens: string[];\r\n if (tokenCache && tokenCache.has(docId)) {\r\n docTokens = tokenCache.get(docId) as string[];\r\n } else {\r\n docTokens = tokenize(text);\r\n }\r\n\r\n // Find phrases in this document\r\n // Note: state.config.weights is guaranteed to have all properties from default merge\r\n // Pass original queryTokens to preserve duplicates (e.g., \"de ... de\")\r\n const phrases = findPhrasesInDocument(\r\n docTokens,\r\n filteredCandidates,\r\n {\r\n weights: state.config.weights as { exact: number; fuzzy: number; order: number; proximity: number; density: number; semantic: number },\r\n maxGap: state.config.maxGap,\r\n proximitySpanMultiplier: state.config.proximitySpanMultiplier,\r\n tolerance\r\n },\r\n state.documentFrequency,\r\n state.totalDocuments,\r\n queryTokens,\r\n candidateLookup, // PHASE 1 OPTIMIZATION A: Pre-built candidate lookup\r\n queryTokenCounts // PHASE 1 OPTIMIZATION B: Pre-built query token counts\r\n );\r\n\r\n if (phrases.length > 0) {\r\n // Calculate overall document score (highest phrase score)\r\n const docScore = Math.max(...phrases.map(p => p.score));\r\n\r\n documentMatches.push({\r\n id: docId,\r\n phrases,\r\n score: docScore,\r\n document: doc\r\n });\r\n }\r\n }\r\n\r\n // Sort by score descending\r\n documentMatches.sort((a, b) => b.score - a.score);\r\n\r\n // Apply final score minimum filter if enabled\r\n let filteredMatches = documentMatches;\r\n if (state.config.enableFinalScoreMinimum && state.config.finalScoreMinimum > 0) {\r\n const threshold = state.config.finalScoreMinimum;\r\n const beforeCount = filteredMatches.length;\r\n filteredMatches = filteredMatches.filter(m => m.score >= threshold);\r\n console.log(`🎚️ Final score filter: ${beforeCount} → ${filteredMatches.length} (threshold: ${threshold})`);\r\n }\r\n\r\n // Apply limit if specified\r\n const limit = params.limit ?? filteredMatches.length;\r\n const limitedMatches = filteredMatches.slice(0, limit);\r\n\r\n // Convert to Orama results format\r\n const hits = limitedMatches.map(match => ({\r\n id: match.id,\r\n score: match.score,\r\n document: match.document,\r\n // Store phrases for highlighting\r\n _phrases: match.phrases\r\n })) as any[];\r\n\r\n const elapsed = performance.now() - startTime;\r\n\r\n console.log(`✅ Found ${hits.length} results in ${elapsed.toFixed(2)}ms (limit: ${limit})`);\r\n\r\n return {\r\n elapsed: {\r\n formatted: `${elapsed.toFixed(2)}ms`,\r\n raw: Math.floor(elapsed * 1000000) // nanoseconds\r\n },\r\n hits,\r\n count: hits.length\r\n } as any;\r\n}\r\n\r\n/**\r\n * Load synonyms from Supabase\r\n */\r\nasync function loadSynonymsFromSupabase(\r\n supabaseConfig: { url: string; serviceKey: string }\r\n): Promise<SynonymMap> {\r\n try {\r\n console.log('🔍 DEBUG: Calling Supabase RPC get_synonym_map...');\r\n\r\n // Dynamic import to avoid bundling Supabase client if not needed\r\n const { createClient } = await import('@supabase/supabase-js');\r\n\r\n const supabase = createClient(supabaseConfig.url, supabaseConfig.serviceKey);\r\n\r\n // Call the get_synonym_map function\r\n const { data, error } = await supabase.rpc('get_synonym_map');\r\n\r\n console.log('🔍 DEBUG: Supabase RPC response:', {\r\n hasError: !!error,\r\n errorMessage: error?.message,\r\n hasData: !!data,\r\n dataType: typeof data,\r\n dataKeys: data ? Object.keys(data).length : 0\r\n });\r\n\r\n if (error) {\r\n throw new Error(`Supabase error: ${error.message}`);\r\n }\r\n\r\n const synonymMap = data || {};\r\n console.log(`📚 Loaded ${Object.keys(synonymMap).length} synonym entries from Supabase`);\r\n\r\n return synonymMap;\r\n } catch (error) {\r\n console.error('❌ Failed to load synonyms from Supabase:', error);\r\n throw error;\r\n }\r\n}\r\n\r\n/**\r\n * Calculate document frequencies for TF-IDF\r\n */\r\nfunction calculateDocumentFrequencies(\r\n docs: Record<string, any>,\r\n textProperty: string\r\n): Map<string, number> {\r\n const df = new Map<string, number>();\r\n\r\n for (const doc of Object.values(docs)) {\r\n const text = doc[textProperty];\r\n\r\n if (!text || typeof text !== 'string') {\r\n continue;\r\n }\r\n\r\n // Get unique words in this document\r\n const words = new Set(tokenize(text));\r\n\r\n // Increment document frequency for each unique word\r\n for (const word of words) {\r\n df.set(word, (df.get(word) || 0) + 1);\r\n }\r\n }\r\n\r\n return df;\r\n}\r\n\r\n/**\r\n * Normalize text using the same rules as server-side\r\n * \r\n * CRITICAL: This must match the normalizeText() function in server/index.js exactly\r\n * PLUS we remove all punctuation to match Orama's French tokenizer behavior\r\n */\r\nfunction normalizeText(text: string): string {\r\n return text\r\n .toLowerCase()\r\n .normalize('NFD')\r\n .replace(/[\\u0300-\\u036f]/g, '') // Remove diacritics\r\n // Replace French elisions (l', d', etc.) with space to preserve word boundaries\r\n .replace(/\\b[ldcjmnst][\\u2018\\u2019\\u201A\\u201B\\u2032\\u2035\\u0027\\u0060\\u00B4](?=\\w)/gi, ' ')\r\n .replace(/[\\u2018\\u2019\\u201A\\u201B\\u2032\\u2035\\u0027\\u0060\\u00B4]/g, '') // Remove remaining apostrophes\r\n .replace(/[\\u201c\\u201d]/g, '\"') // Normalize curly quotes to straight quotes\r\n .replace(/[.,;:!?()[\\]{}\\-—–«»\"\"]/g, ' ') // Remove punctuation (replace with space to preserve word boundaries)\r\n .replace(/\\s+/g, ' ') // Normalize multiple spaces to single space\r\n .trim();\r\n}\r\n\r\n/**\r\n * Tokenization matching normalized text behavior\r\n * \r\n * Note: Text should already be normalized before indexing, so we normalize again\r\n * to ensure plugin tokenization matches index tokenization\r\n */\r\nfunction tokenize(text: string): string[] {\r\n // Normalize first (same as indexing), then split by whitespace\r\n return normalizeText(text)\r\n .split(/\\s+/)\r\n .filter(token => token.length > 0);\r\n}\r\n\r\n/**\r\n * Export types for external use\r\n */\r\nexport type {\r\n FuzzyPhraseConfig,\r\n WordMatch,\r\n PhraseMatch,\r\n DocumentMatch,\r\n SynonymMap,\r\n Candidate,\r\n PluginState\r\n} from './types.js';\r\n\r\n/**\r\n * Export optimized search (QPS + fuzzy phrase pruning)\r\n */\r\nexport {\r\n searchWithQPSPruning,\r\n createOptimizedSearch,\r\n normalizeText as normalizeTextOptimized,\r\n tokenize as tokenizeOptimized,\r\n type OptimizedSearchConfig\r\n} from './optimized.js';\r\n\r\n/**\r\n * Re-export plugin state accessor for optimized search integration\r\n */\r\nexport function getPluginState(orama: any): PluginState | undefined {\r\n return pluginStates.get(orama);\r\n}\r\n"]}
|
package/dist/index.js
CHANGED
|
@@ -177,10 +177,7 @@ function filterCandidatesByScore(candidatesMap, minScore) {
|
|
|
177
177
|
}
|
|
178
178
|
|
|
179
179
|
// src/scoring.ts
|
|
180
|
-
function
|
|
181
|
-
const phrases = [];
|
|
182
|
-
const queryTokens = originalQueryTokens;
|
|
183
|
-
const wordMatches = [];
|
|
180
|
+
function buildCandidateLookup(candidatesMap) {
|
|
184
181
|
const candidateLookup = /* @__PURE__ */ new Map();
|
|
185
182
|
for (const [queryToken, candidates] of candidatesMap.entries()) {
|
|
186
183
|
for (const candidate of candidates) {
|
|
@@ -199,6 +196,19 @@ function findPhrasesInDocument(documentTokens, candidatesMap, config, documentFr
|
|
|
199
196
|
return b.candidate.score - a.candidate.score;
|
|
200
197
|
});
|
|
201
198
|
}
|
|
199
|
+
return candidateLookup;
|
|
200
|
+
}
|
|
201
|
+
function buildQueryTokenCounts(queryTokens) {
|
|
202
|
+
const queryTokenCounts = /* @__PURE__ */ new Map();
|
|
203
|
+
for (const token of queryTokens) {
|
|
204
|
+
queryTokenCounts.set(token, (queryTokenCounts.get(token) || 0) + 1);
|
|
205
|
+
}
|
|
206
|
+
return queryTokenCounts;
|
|
207
|
+
}
|
|
208
|
+
function findPhrasesInDocument(documentTokens, candidatesMap, config, documentFrequency, totalDocuments, originalQueryTokens, candidateLookup, queryTokenCounts) {
|
|
209
|
+
const phrases = [];
|
|
210
|
+
const queryTokens = originalQueryTokens;
|
|
211
|
+
const wordMatches = [];
|
|
202
212
|
for (let i = 0; i < documentTokens.length; i++) {
|
|
203
213
|
const docWord = documentTokens[i];
|
|
204
214
|
const matches = candidateLookup.get(docWord);
|
|
@@ -224,8 +234,9 @@ function findPhrasesInDocument(documentTokens, candidatesMap, config, documentFr
|
|
|
224
234
|
documentFrequency,
|
|
225
235
|
totalDocuments,
|
|
226
236
|
wordMatches,
|
|
227
|
-
documentTokens
|
|
228
|
-
|
|
237
|
+
documentTokens,
|
|
238
|
+
queryTokenCounts
|
|
239
|
+
// OPTIMIZATION B: Pass pre-built queryTokenCounts
|
|
229
240
|
);
|
|
230
241
|
if (phrase && phrase.words.length > 0) {
|
|
231
242
|
phrases.push(phrase);
|
|
@@ -235,13 +246,9 @@ function findPhrasesInDocument(documentTokens, candidatesMap, config, documentFr
|
|
|
235
246
|
const filteredPhrases = phrases.filter((p) => p.words.length >= minTokensRequired);
|
|
236
247
|
return deduplicatePhrases(filteredPhrases);
|
|
237
248
|
}
|
|
238
|
-
function buildPhraseFromPosition(wordMatches, startIndex, queryTokens, config, documentFrequency, totalDocuments, allWordMatches, documentTokens) {
|
|
249
|
+
function buildPhraseFromPosition(wordMatches, startIndex, queryTokens, config, documentFrequency, totalDocuments, allWordMatches, documentTokens, queryTokenCounts) {
|
|
239
250
|
const startMatch = wordMatches[startIndex];
|
|
240
251
|
const phraseWords = [startMatch];
|
|
241
|
-
const queryTokenCounts = /* @__PURE__ */ new Map();
|
|
242
|
-
for (const token of queryTokens) {
|
|
243
|
-
queryTokenCounts.set(token, (queryTokenCounts.get(token) || 0) + 1);
|
|
244
|
-
}
|
|
245
252
|
const matchedCounts = /* @__PURE__ */ new Map();
|
|
246
253
|
matchedCounts.set(startMatch.queryToken, 1);
|
|
247
254
|
const gapWords = [];
|
|
@@ -435,6 +442,19 @@ function normalizeText(text) {
|
|
|
435
442
|
function tokenize(text) {
|
|
436
443
|
return normalizeText(text).split(/\s+/).filter((token) => token.length > 0);
|
|
437
444
|
}
|
|
445
|
+
function buildReducedVocabularyFromDocs(candidateDocIds, docs) {
|
|
446
|
+
const reducedVocab = /* @__PURE__ */ new Set();
|
|
447
|
+
for (const docId of candidateDocIds) {
|
|
448
|
+
const doc = docs[docId];
|
|
449
|
+
if (!doc?.normalized_content)
|
|
450
|
+
continue;
|
|
451
|
+
const tokens = doc.normalized_content.split(/\s+/).filter((token) => token.length > 0);
|
|
452
|
+
for (const token of tokens) {
|
|
453
|
+
reducedVocab.add(token);
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
return reducedVocab;
|
|
457
|
+
}
|
|
438
458
|
function searchQPS(term, qpsIndex, tokenizer, properties, config, language) {
|
|
439
459
|
const tokens = tokenizer.tokenize(term, language);
|
|
440
460
|
if (tokens.length === 0) {
|
|
@@ -553,9 +573,17 @@ async function searchWithQPSPruning(orama, qpsIndex, pluginState, params, config
|
|
|
553
573
|
return { elapsed: { formatted: "0ms", raw: 0 }, hits: [], count: 0 };
|
|
554
574
|
}
|
|
555
575
|
}
|
|
576
|
+
let docs = {};
|
|
577
|
+
if (orama.data?.docs?.docs) {
|
|
578
|
+
docs = orama.data.docs.docs;
|
|
579
|
+
}
|
|
580
|
+
const vocabStartTime = performance.now();
|
|
581
|
+
const reducedVocabulary = buildReducedVocabularyFromDocs(candidateDocIds, docs);
|
|
582
|
+
const vocabTime = performance.now() - vocabStartTime;
|
|
583
|
+
console.log(`\u{1F4DA} Reduced vocabulary: ${reducedVocabulary.size} words (full: ${vocabulary.size}, reduction: ${(100 * (1 - reducedVocabulary.size / vocabulary.size)).toFixed(1)}%, built in ${vocabTime.toFixed(2)}ms)`);
|
|
556
584
|
const candidatesMap = findAllCandidates(
|
|
557
585
|
queryTokens,
|
|
558
|
-
|
|
586
|
+
reducedVocabulary,
|
|
559
587
|
tolerance,
|
|
560
588
|
pluginState.config.enableSynonyms ? pluginState.synonymMap : void 0,
|
|
561
589
|
pluginState.config.synonymMatchScore
|
|
@@ -563,11 +591,9 @@ async function searchWithQPSPruning(orama, qpsIndex, pluginState, params, config
|
|
|
563
591
|
const filteredFuzzyCandidates = tolerance === 0 ? candidatesMap : filterCandidatesByScore(candidatesMap, pluginState.config.minScore);
|
|
564
592
|
console.log(`\u{1F3AF} Fuzzy candidates: ${Array.from(filteredFuzzyCandidates.values()).reduce((sum, c) => sum + c.length, 0)} total`);
|
|
565
593
|
const phraseStartTime = performance.now();
|
|
594
|
+
const candidateLookup = buildCandidateLookup(filteredFuzzyCandidates);
|
|
595
|
+
const queryTokenCounts = buildQueryTokenCounts(queryTokens);
|
|
566
596
|
const documentMatches = [];
|
|
567
|
-
let docs = {};
|
|
568
|
-
if (orama.data?.docs?.docs) {
|
|
569
|
-
docs = orama.data.docs.docs;
|
|
570
|
-
}
|
|
571
597
|
let docsScored = 0;
|
|
572
598
|
for (const [docId, doc] of Object.entries(docs)) {
|
|
573
599
|
if (!candidateDocIds.has(docId)) {
|
|
@@ -582,7 +608,7 @@ async function searchWithQPSPruning(orama, qpsIndex, pluginState, params, config
|
|
|
582
608
|
if (tokenCache && tokenCache.has(docId)) {
|
|
583
609
|
docTokens = tokenCache.get(docId);
|
|
584
610
|
} else {
|
|
585
|
-
docTokens =
|
|
611
|
+
docTokens = text.split(/\s+/).filter((token) => token.length > 0);
|
|
586
612
|
}
|
|
587
613
|
const phrases = findPhrasesInDocument(
|
|
588
614
|
docTokens,
|
|
@@ -595,7 +621,11 @@ async function searchWithQPSPruning(orama, qpsIndex, pluginState, params, config
|
|
|
595
621
|
},
|
|
596
622
|
pluginState.documentFrequency,
|
|
597
623
|
pluginState.totalDocuments,
|
|
598
|
-
queryTokens
|
|
624
|
+
queryTokens,
|
|
625
|
+
candidateLookup,
|
|
626
|
+
// PHASE 1 OPTIMIZATION A: Pre-built candidate lookup
|
|
627
|
+
queryTokenCounts
|
|
628
|
+
// PHASE 1 OPTIMIZATION B: Pre-built query token counts
|
|
599
629
|
);
|
|
600
630
|
if (phrases.length > 0) {
|
|
601
631
|
const docScore = Math.max(...phrases.map((p) => p.score));
|
|
@@ -804,6 +834,8 @@ async function searchWithFuzzyPhrase(orama, params, language) {
|
|
|
804
834
|
);
|
|
805
835
|
const filteredCandidates = tolerance === 0 ? candidatesMap : filterCandidatesByScore(candidatesMap, state.config.minScore);
|
|
806
836
|
console.log(`\u{1F3AF} Found candidates: ${Array.from(filteredCandidates.values()).reduce((sum, c) => sum + c.length, 0)} total`);
|
|
837
|
+
const candidateLookup = buildCandidateLookup(filteredCandidates);
|
|
838
|
+
const queryTokenCounts = buildQueryTokenCounts(queryTokens);
|
|
807
839
|
const documentMatches = [];
|
|
808
840
|
console.log("\u{1F50D} DEBUG orama.data structure:", {
|
|
809
841
|
dataKeys: Object.keys(orama.data || {}),
|
|
@@ -860,8 +892,11 @@ async function searchWithFuzzyPhrase(orama, params, language) {
|
|
|
860
892
|
},
|
|
861
893
|
state.documentFrequency,
|
|
862
894
|
state.totalDocuments,
|
|
863
|
-
queryTokens
|
|
864
|
-
|
|
895
|
+
queryTokens,
|
|
896
|
+
candidateLookup,
|
|
897
|
+
// PHASE 1 OPTIMIZATION A: Pre-built candidate lookup
|
|
898
|
+
queryTokenCounts
|
|
899
|
+
// PHASE 1 OPTIMIZATION B: Pre-built query token counts
|
|
865
900
|
);
|
|
866
901
|
if (phrases.length > 0) {
|
|
867
902
|
const docScore = Math.max(...phrases.map((p) => p.score));
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/fuzzy.ts","../src/candidates.ts","../src/scoring.ts","../src/optimized.ts","../src/index.ts"],"names":["tokenize","normalizeText"],"mappings":"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","sourcesContent":["/**\n * Fuzzy matching utilities using bounded Levenshtein distance\n * \n * This is the same algorithm used by Orama's match-highlight plugin\n * for consistent fuzzy matching behavior.\n */\n\n/**\n * Result of bounded Levenshtein distance calculation\n */\nexport interface BoundedLevenshteinResult {\n /** Whether the distance is within bounds */\n isBounded: boolean;\n /** The actual distance (only valid if isBounded is true) */\n distance: number;\n}\n\n/**\n * Calculate bounded Levenshtein distance between two strings\n * \n * Stops early if distance exceeds the bound for better performance.\n * This is the same algorithm as Orama's internal boundedLevenshtein.\n * \n * @param a - First string\n * @param b - Second string\n * @param bound - Maximum allowed distance\n * @returns Result indicating if strings are within bound and the distance\n */\nexport function boundedLevenshtein(\n a: string,\n b: string,\n bound: number\n): BoundedLevenshteinResult {\n // Quick checks\n if (a === b) {\n return { isBounded: true, distance: 0 };\n }\n\n const aLen = a.length;\n const bLen = b.length;\n\n // If length difference exceeds bound, no need to calculate\n if (Math.abs(aLen - bLen) > bound) {\n return { isBounded: false, distance: bound + 1 };\n }\n\n // Swap to ensure a is shorter (optimization)\n if (aLen > bLen) {\n [a, b] = [b, a];\n }\n\n const m = a.length;\n const n = b.length;\n\n // Use single array instead of matrix (memory optimization)\n let prevRow = new Array(n + 1);\n let currRow = new Array(n + 1);\n\n // Initialize first row\n for (let j = 0; j <= n; j++) {\n prevRow[j] = j;\n }\n\n for (let i = 1; i <= m; i++) {\n currRow[0] = i;\n let minInRow = i;\n\n for (let j = 1; j <= n; j++) {\n const cost = a[i - 1] === b[j - 1] ? 0 : 1;\n\n currRow[j] = Math.min(\n prevRow[j] + 1, // deletion\n currRow[j - 1] + 1, // insertion\n prevRow[j - 1] + cost // substitution\n );\n\n minInRow = Math.min(minInRow, currRow[j]);\n }\n\n // Early termination: if all values in row exceed bound, we're done\n if (minInRow > bound) {\n return { isBounded: false, distance: bound + 1 };\n }\n\n // Swap rows for next iteration\n [prevRow, currRow] = [currRow, prevRow];\n }\n\n const distance = prevRow[n];\n return {\n isBounded: distance <= bound,\n distance\n };\n}\n\n/**\n * Check if a word matches a query token with fuzzy matching\n * \n * @param word - Word from document\n * @param queryToken - Token from search query\n * @param tolerance - Maximum edit distance allowed\n * @returns Match result with score\n */\nexport function fuzzyMatch(\n word: string,\n queryToken: string,\n tolerance: number\n): { matches: boolean; distance: number; score: number } {\n // Exact match\n if (word === queryToken) {\n return { matches: true, distance: 0, score: 1.0 };\n }\n\n // NOTE: Prefix matching removed entirely\n // It was causing false positives (e.g., \"de\" matching \"dedain\", \"desert\")\n // and interfering with tolerance settings. Levenshtein-only is cleaner.\n\n // Fuzzy match with bounded Levenshtein distance\n const result = boundedLevenshtein(word, queryToken, tolerance);\n \n if (result.isBounded) {\n // Score decreases with distance\n // distance 1 = 0.8, distance 2 = 0.6, etc.\n const score = 1.0 - (result.distance * 0.2);\n return {\n matches: true,\n distance: result.distance,\n score: Math.max(0.1, score) // Minimum score of 0.1\n };\n }\n\n return { matches: false, distance: tolerance + 1, score: 0 };\n}\n\n/**\n * Calculate adaptive tolerance based on query length\n * \n * Longer queries get higher tolerance for better fuzzy matching.\n * \n * @param queryTokens - Array of query tokens\n * @param baseTolerance - Base tolerance value\n * @returns Calculated tolerance (always an integer)\n */\nexport function calculateAdaptiveTolerance(\n queryTokens: string[],\n baseTolerance: number\n): number {\n const queryLength = queryTokens.length;\n \n if (queryLength <= 2) {\n return baseTolerance;\n } else if (queryLength <= 4) {\n return baseTolerance + 1;\n } else if (queryLength <= 6) {\n return baseTolerance + 2;\n } else {\n return baseTolerance + 3;\n }\n}\n","/**\r\n * Candidate expansion: Find all possible matches for query tokens\r\n * including exact matches, fuzzy matches, and synonyms\r\n */\r\n\r\nimport { fuzzyMatch } from './fuzzy.js';\r\nimport type { Candidate, SynonymMap } from './types.js';\r\n\r\n/**\r\n * Extract all unique words from the radix tree index\r\n * \r\n * @param radixNode - Root node of the radix tree\r\n * @returns Set of all unique words in the index\r\n */\r\nexport function extractVocabularyFromRadixTree(radixNode: any): Set<string> {\r\n const vocabulary = new Set<string>();\r\n let nodesVisited = 0;\r\n let wordsFound = 0;\r\n \r\n function traverse(node: any, depth: number = 0) {\r\n if (!node) {\r\n return;\r\n }\r\n \r\n nodesVisited++;\r\n \r\n // Check if this node represents a complete word\r\n // e = true means it's an end of a word\r\n if (node.e && node.w && typeof node.w === 'string' && node.w.length > 0) {\r\n vocabulary.add(node.w);\r\n wordsFound++;\r\n }\r\n \r\n // Children can be Map, Array, or Object\r\n if (node.c) {\r\n if (node.c instanceof Map) {\r\n // Map format\r\n for (const [_key, childNode] of node.c) {\r\n traverse(childNode, depth + 1);\r\n }\r\n } else if (Array.isArray(node.c)) {\r\n // Array format: [[key, childNode], ...]\r\n for (const [_key, childNode] of node.c) {\r\n traverse(childNode, depth + 1);\r\n }\r\n } else if (typeof node.c === 'object') {\r\n // Object format: {key: childNode, ...}\r\n for (const childNode of Object.values(node.c)) {\r\n traverse(childNode, depth + 1);\r\n }\r\n }\r\n }\r\n }\r\n \r\n traverse(radixNode);\r\n console.log(`📚 Extracted ${vocabulary.size} words from ${nodesVisited} nodes visited`);\r\n return vocabulary;\r\n}\r\n\r\n/**\r\n * Find all candidate matches for a single query token\r\n * \r\n * @param queryToken - Token from search query\r\n * @param vocabulary - Set of all words in the index\r\n * @param tolerance - Fuzzy matching tolerance\r\n * @param synonyms - Synonym map (optional)\r\n * @param synonymScore - Score multiplier for synonym matches\r\n * @returns Array of candidate matches\r\n */\r\nexport function findCandidatesForToken(\r\n queryToken: string,\r\n vocabulary: Set<string>,\r\n tolerance: number,\r\n synonyms?: SynonymMap,\r\n synonymScore: number = 0.8\r\n): Candidate[] {\r\n const candidates: Candidate[] = [];\r\n const seen = new Set<string>();\r\n\r\n // 1. Check for exact match\r\n if (vocabulary.has(queryToken)) {\r\n candidates.push({\r\n word: queryToken,\r\n type: 'exact',\r\n queryToken,\r\n distance: 0,\r\n score: 1.0\r\n });\r\n seen.add(queryToken);\r\n }\r\n\r\n // 2. Check for fuzzy matches\r\n for (const word of vocabulary) {\r\n if (seen.has(word)) continue;\r\n\r\n const match = fuzzyMatch(word, queryToken, tolerance);\r\n if (match.matches) {\r\n candidates.push({\r\n word,\r\n type: 'fuzzy',\r\n queryToken,\r\n distance: match.distance,\r\n score: match.score\r\n });\r\n seen.add(word);\r\n }\r\n }\r\n\r\n // 3. Check for synonym matches\r\n if (synonyms && synonyms[queryToken]) {\r\n for (const synonym of synonyms[queryToken]) {\r\n if (seen.has(synonym)) continue;\r\n if (vocabulary.has(synonym)) {\r\n candidates.push({\r\n word: synonym,\r\n type: 'synonym',\r\n queryToken,\r\n distance: 0,\r\n score: synonymScore\r\n });\r\n seen.add(synonym);\r\n }\r\n }\r\n }\r\n\r\n return candidates;\r\n}\r\n\r\n/**\r\n * Find candidates for all query tokens\r\n * \r\n * @param queryTokens - Array of tokens from search query\r\n * @param vocabulary - Set of all words in the index\r\n * @param tolerance - Fuzzy matching tolerance\r\n * @param synonyms - Synonym map (optional)\r\n * @param synonymScore - Score multiplier for synonym matches\r\n * @returns Map of query tokens to their candidate matches\r\n */\r\nexport function findAllCandidates(\r\n queryTokens: string[],\r\n vocabulary: Set<string>,\r\n tolerance: number,\r\n synonyms?: SynonymMap,\r\n synonymScore: number = 0.8\r\n): Map<string, Candidate[]> {\r\n const candidatesMap = new Map<string, Candidate[]>();\r\n\r\n for (const token of queryTokens) {\r\n const tokenCandidates = findCandidatesForToken(\r\n token,\r\n vocabulary,\r\n tolerance,\r\n synonyms,\r\n synonymScore\r\n );\r\n candidatesMap.set(token, tokenCandidates);\r\n }\r\n\r\n return candidatesMap;\r\n}\r\n\r\n/**\r\n * Get total number of candidates across all tokens\r\n * \r\n * @param candidatesMap - Map of token to candidates\r\n * @returns Total count of all candidates\r\n */\r\nexport function getTotalCandidateCount(\r\n candidatesMap: Map<string, Candidate[]>\r\n): number {\r\n let total = 0;\r\n for (const candidates of candidatesMap.values()) {\r\n total += candidates.length;\r\n }\r\n return total;\r\n}\r\n\r\n/**\r\n * Filter candidates by minimum score threshold\r\n * \r\n * @param candidatesMap - Map of token to candidates\r\n * @param minScore - Minimum score threshold\r\n * @returns Filtered candidates map\r\n */\r\nexport function filterCandidatesByScore(\r\n candidatesMap: Map<string, Candidate[]>,\r\n minScore: number\r\n): Map<string, Candidate[]> {\r\n const filtered = new Map<string, Candidate[]>();\r\n\r\n for (const [token, candidates] of candidatesMap.entries()) {\r\n const filteredCandidates = candidates.filter(c => c.score >= minScore);\r\n if (filteredCandidates.length > 0) {\r\n filtered.set(token, filteredCandidates);\r\n }\r\n }\r\n\r\n return filtered;\r\n}\r\n","/**\n * Phrase scoring algorithm with semantic weighting\n */\n\nimport type { WordMatch, PhraseMatch, Candidate, GapWord } from './types.js';\n\n/**\n * Configuration for phrase scoring\n */\nexport interface ScoringConfig {\n weights: {\n exact: number;\n fuzzy: number;\n order: number;\n proximity: number;\n density: number;\n semantic: number;\n };\n maxGap: number;\n /** \n * Multiplier for proximity window calculation.\n * proximityWindow = queryTokens.length × proximitySpanMultiplier\n */\n proximitySpanMultiplier: number;\n /**\n * Fuzzy tolerance (Levenshtein distance). When 0, only exact matches.\n */\n tolerance: number;\n}\n\n/**\n * Find all phrase matches in a document\n * \n * @param documentTokens - Tokenized document content (needed to extract gap words)\n * @param candidatesMap - Map of query tokens to their candidates\n * @param config - Scoring configuration\n * @param documentFrequency - Document frequency map for TF-IDF\n * @param totalDocuments - Total number of documents\n * @param originalQueryTokens - Original query tokens array (preserves duplicates like \"de...de\")\n * @returns Array of phrase matches\n */\nexport function findPhrasesInDocument(\n documentTokens: string[],\n candidatesMap: Map<string, Candidate[]>,\n config: ScoringConfig,\n documentFrequency: Map<string, number>,\n totalDocuments: number,\n originalQueryTokens: string[]\n): PhraseMatch[] {\n const phrases: PhraseMatch[] = [];\n // Use original query tokens to preserve duplicates (e.g., \"de maux ... de\")\n const queryTokens = originalQueryTokens;\n\n // Find all word matches in document\n const wordMatches: WordMatch[] = [];\n \n // Build candidateLookup once, sorted by quality (exact first, higher score)\n // This ensures best matches come first when iterating - no per-document sort needed\n const candidateLookup = new Map<string, Array<{queryToken: string, candidate: Candidate}>>();\n for (const [queryToken, candidates] of candidatesMap.entries()) {\n for (const candidate of candidates) {\n if (!candidateLookup.has(candidate.word)) {\n candidateLookup.set(candidate.word, []);\n }\n candidateLookup.get(candidate.word)!.push({ queryToken, candidate });\n }\n }\n // Sort each word's matches by quality: exact first, then by score\n for (const entries of candidateLookup.values()) {\n entries.sort((a, b) => {\n if (a.candidate.type === 'exact' && b.candidate.type !== 'exact') return -1;\n if (b.candidate.type === 'exact' && a.candidate.type !== 'exact') return 1;\n return b.candidate.score - a.candidate.score;\n });\n }\n\n // Iterate documentTokens in position order (0, 1, 2...)\n // Matches at same position come out quality-sorted (from candidateLookup)\n for (let i = 0; i < documentTokens.length; i++) {\n const docWord = documentTokens[i];\n const matches = candidateLookup.get(docWord);\n if (matches) {\n for (const { queryToken, candidate } of matches) {\n wordMatches.push({\n word: docWord,\n queryToken,\n position: i,\n type: candidate.type,\n distance: candidate.distance,\n score: candidate.score\n });\n }\n }\n }\n\n // Build phrases from word matches using sliding window\n for (let i = 0; i < wordMatches.length; i++) {\n const phrase = buildPhraseFromPosition(\n wordMatches,\n i,\n queryTokens,\n config,\n documentFrequency,\n totalDocuments,\n wordMatches,\n documentTokens // Pass document tokens to extract gap words\n );\n \n if (phrase && phrase.words.length > 0) {\n phrases.push(phrase);\n }\n }\n\n // Filter out low-quality single-word matches for multi-word queries\n // This prevents noise from common words like \"de\", \"la\", \"des\" appearing as separate phrases\n const minTokensRequired = queryTokens.length >= 3 ? 2 : 1;\n const filteredPhrases = phrases.filter(p => p.words.length >= minTokensRequired);\n\n // Deduplicate and sort by score\n return deduplicatePhrases(filteredPhrases);\n}\n\n/**\n * Build a phrase starting from a specific word match position\n * \n * @param wordMatches - All word matches in document\n * @param startIndex - Starting index in wordMatches array\n * @param queryTokens - Original query tokens\n * @param config - Scoring configuration\n * @param documentFrequency - Document frequency map\n * @param totalDocuments - Total document count\n * @param allWordMatches - All word matches in document (for density calculation)\n * @param documentTokens - Original document tokens (for gap word extraction)\n * @returns Phrase match or null\n */\nfunction buildPhraseFromPosition(\n wordMatches: WordMatch[],\n startIndex: number,\n queryTokens: string[],\n config: ScoringConfig,\n documentFrequency: Map<string, number>,\n totalDocuments: number,\n allWordMatches: WordMatch[],\n documentTokens: string[]\n): PhraseMatch | null {\n const startMatch = wordMatches[startIndex];\n const phraseWords: WordMatch[] = [startMatch];\n \n // Count occurrences of each token in query (handles duplicate tokens like \"de ... de\")\n const queryTokenCounts = new Map<string, number>();\n for (const token of queryTokens) {\n queryTokenCounts.set(token, (queryTokenCounts.get(token) || 0) + 1);\n }\n \n // Track how many times we've matched each token\n const matchedCounts = new Map<string, number>();\n matchedCounts.set(startMatch.queryToken, 1);\n \n const gapWords: GapWord[] = [];\n let totalGapUsed = 0;\n let totalMatchedTokens = 1;\n\n // Look for nearby matches to complete the phrase\n for (let i = startIndex + 1; i < wordMatches.length; i++) {\n const match = wordMatches[i];\n const lastPos = phraseWords[phraseWords.length - 1].position;\n \n // Skip if same position (can happen with multiple fuzzy matches for same word)\n if (match.position <= lastPos) {\n continue;\n }\n \n const gap = match.position - lastPos - 1;\n\n // Stop if this gap alone exceeds maximum\n if (gap > config.maxGap) {\n break;\n }\n \n // Stop if adding this gap would exceed total maxGap budget\n if (totalGapUsed + gap > config.maxGap) {\n break;\n }\n\n // Check if we still need more of this token (handles duplicates)\n const neededCount = queryTokenCounts.get(match.queryToken) || 0;\n const currentCount = matchedCounts.get(match.queryToken) || 0;\n \n if (currentCount < neededCount) {\n // Track gap words between last match and current match\n for (let pos = lastPos + 1; pos < match.position; pos++) {\n totalGapUsed++;\n gapWords.push({\n word: documentTokens[pos],\n position: pos,\n gapIndex: totalGapUsed\n });\n }\n\n phraseWords.push(match);\n matchedCounts.set(match.queryToken, currentCount + 1);\n totalMatchedTokens++;\n\n // Stop if we have all query tokens (including duplicates)\n if (totalMatchedTokens === queryTokens.length) {\n break;\n }\n }\n }\n\n // Calculate phrase score\n if (phraseWords.length > 0) {\n const coverage = phraseWords.length / queryTokens.length;\n const span = phraseWords[phraseWords.length - 1].position - phraseWords[0].position + 1;\n \n const { score, breakdown } = calculatePhraseScore(\n phraseWords,\n queryTokens,\n config,\n documentFrequency,\n totalDocuments,\n allWordMatches,\n coverage\n );\n\n return {\n words: phraseWords,\n gapWords,\n gapUsed: totalGapUsed,\n coverage,\n startPosition: phraseWords[0].position,\n endPosition: phraseWords[phraseWords.length - 1].position,\n span,\n inOrder: isInOrder(phraseWords, queryTokens),\n score,\n scoreBreakdown: breakdown\n };\n }\n\n return null;\n}\n\n/**\n * Calculate overall phrase score\n * \n * @param phraseWords - Words in the phrase\n * @param queryTokens - Original query tokens\n * @param config - Scoring configuration\n * @param documentFrequency - Document frequency map\n * @param totalDocuments - Total document count\n * @param allWordMatches - All word matches in document (for density calculation)\n * @param coverage - Pre-calculated coverage ratio (phraseWords.length / queryTokens.length)\n * @returns Phrase score (0-1) and detailed component breakdown\n */\nfunction calculatePhraseScore(\n phraseWords: WordMatch[],\n queryTokens: string[],\n config: ScoringConfig,\n documentFrequency: Map<string, number>,\n totalDocuments: number,\n allWordMatches: WordMatch[],\n coverage: number\n): { score: number; breakdown: { base: number; order: number; proximity: number; density: number; semantic: number; coverage: number } } {\n // Base score from word matches\n // Each word contributes: matchScore × typeWeight\n let baseScore = 0;\n for (const word of phraseWords) {\n const weight = word.type === 'exact' ? config.weights.exact :\n word.type === 'fuzzy' ? config.weights.fuzzy : \n config.weights.fuzzy * 0.8; // synonym gets 80% of fuzzy weight\n baseScore += word.score * weight;\n }\n baseScore /= phraseWords.length;\n\n // Order bonus: 1.0 if words appear in query order, 0.5 otherwise\n const inOrder = isInOrder(phraseWords, queryTokens);\n const orderScore = inOrder ? 1.0 : 0.5;\n\n // Proximity bonus (closer words score higher)\n // Short-circuit: skip if maxGap=0, proximity weight is 0, or single-word query (proximity meaningless)\n let proximityScore = 0;\n if (config.maxGap > 0 && config.weights.proximity > 0 && queryTokens.length > 1) {\n const span = phraseWords[phraseWords.length - 1].position - phraseWords[0].position + 1;\n const proximityWindow = queryTokens.length * config.proximitySpanMultiplier;\n proximityScore = Math.max(0, 1.0 - (span / proximityWindow));\n }\n\n // Density: Only applies to single-word queries (measures word repetition in document)\n // For multi-word phrase queries, density is 0 (coverage handles completeness separately)\n let densityScore = 0;\n \n if (queryTokens.length === 1) {\n // Single-word query: reward repetition\n const totalOccurrences = allWordMatches.length;\n // Cap at reasonable maximum to avoid runaway scores\n densityScore = Math.min(1.0, totalOccurrences / 10);\n }\n // For multi-word queries: densityScore stays 0\n // Coverage is applied as a multiplier at the end instead\n\n // Semantic score (TF-IDF based)\n const semanticScore = calculateSemanticScore(\n phraseWords,\n documentFrequency,\n totalDocuments\n );\n\n // Weighted combination\n const weights = config.weights;\n \n // Calculate weighted components\n const weightedBase = baseScore;\n const weightedOrder = orderScore * weights.order;\n const weightedProximity = proximityScore * weights.proximity;\n const weightedDensity = densityScore * weights.density;\n const weightedSemantic = semanticScore * weights.semantic;\n \n const totalScore = weightedBase + weightedOrder + weightedProximity + weightedDensity + weightedSemantic;\n\n // Calculate max possible score\n // FIX: Use actual max base weight (highest of exact/fuzzy) instead of hardcoded 1.0\n // When tolerance=0 or fuzzy weight=0, only exact matches are possible\n const canHaveFuzzyMatches = config.tolerance > 0 && weights.fuzzy > 0;\n const maxBaseWeight = canHaveFuzzyMatches ? Math.max(weights.exact, weights.fuzzy) : weights.exact;\n // Only include proximity in max if it can actually contribute (avoids penalizing scores when maxGap=0 or single-word)\n const effectiveProximityWeight = (config.maxGap > 0 && weights.proximity > 0 && queryTokens.length > 1) ? weights.proximity : 0;\n const maxPossibleScore = maxBaseWeight + weights.order + effectiveProximityWeight + weights.density + weights.semantic;\n \n // Normalize to 0-1 range\n const normalizedScore = totalScore / maxPossibleScore;\n \n // FIX: Apply coverage as a MULTIPLIER for multi-word queries\n // This ensures incomplete matches (2/3) can never outscore complete matches (3/3)\n const coverageMultiplier = queryTokens.length > 1 ? coverage : 1.0;\n const score = normalizedScore * coverageMultiplier;\n\n // Component contributions to the final normalized score (before coverage multiplier)\n const base = weightedBase / maxPossibleScore;\n const order = weightedOrder / maxPossibleScore;\n const proximity = weightedProximity / maxPossibleScore;\n const density = weightedDensity / maxPossibleScore;\n const semantic = weightedSemantic / maxPossibleScore;\n\n return {\n score,\n breakdown: {\n base,\n order,\n proximity,\n density,\n semantic,\n coverage: coverageMultiplier // Show coverage multiplier in breakdown\n }\n };\n}\n\n/**\n * Check if phrase words appear in query order\n * Handles duplicate tokens (e.g., \"de ... de\") by tracking position consumption\n * \n * @param phraseWords - Words in the phrase\n * @param queryTokens - Original query tokens\n * @returns True if in order\n */\nfunction isInOrder(phraseWords: WordMatch[], queryTokens: string[]): boolean {\n // Build array of {token, index} to handle duplicates\n // e.g., [\"de\", \"maux\", ..., \"de\"] → [{token:\"de\", idx:0}, {token:\"maux\", idx:1}, ..., {token:\"de\", idx:7}]\n const tokenPositions = queryTokens.map((token, index) => ({ token, index }));\n \n let lastMatchedIndex = -1;\n \n for (const phraseWord of phraseWords) {\n // Find the first unused position for this token that's after lastMatchedIndex\n let foundIndex = -1;\n for (const pos of tokenPositions) {\n if (pos.token === phraseWord.queryToken && pos.index > lastMatchedIndex) {\n foundIndex = pos.index;\n break;\n }\n }\n \n if (foundIndex === -1) {\n // Token not found in expected position - out of order\n return false;\n }\n \n lastMatchedIndex = foundIndex;\n }\n \n return true;\n}\n\n/**\n * Calculate semantic score using TF-IDF\n * \n * @param phraseWords - Words in the phrase\n * @param documentFrequency - Document frequency map\n * @param totalDocuments - Total document count\n * @returns Semantic score (0-1)\n */\nfunction calculateSemanticScore(\n phraseWords: WordMatch[],\n documentFrequency: Map<string, number>,\n totalDocuments: number\n): number {\n // Handle edge case: no documents\n if (totalDocuments === 0) {\n return 0;\n }\n \n let tfidfSum = 0;\n \n for (const word of phraseWords) {\n const df = documentFrequency.get(word.word) || 1;\n const idf = Math.log(totalDocuments / df);\n tfidfSum += idf;\n }\n \n // Normalize by phrase length\n const avgTfidf = tfidfSum / phraseWords.length;\n \n // Normalize to 0-1 range (assuming max IDF of ~10)\n return Math.min(1.0, avgTfidf / 10);\n}\n\n/**\n * Deduplicate overlapping phrases, keeping highest scoring ones\n * \n * @param phrases - Array of phrase matches\n * @returns Deduplicated phrases sorted by score\n */\nfunction deduplicatePhrases(phrases: PhraseMatch[]): PhraseMatch[] {\n if (phrases.length === 0) return [];\n\n // Sort by score descending\n const sorted = phrases.slice().sort((a, b) => b.score - a.score);\n const result: PhraseMatch[] = [];\n const covered = new Set<number>();\n\n for (const phrase of sorted) {\n // Check if this phrase overlaps with already selected phrases\n let overlaps = false;\n for (let pos = phrase.startPosition; pos <= phrase.endPosition; pos++) {\n if (covered.has(pos)) {\n overlaps = true;\n break;\n }\n }\n\n if (!overlaps) {\n result.push(phrase);\n // Mark positions as covered\n for (let pos = phrase.startPosition; pos <= phrase.endPosition; pos++) {\n covered.add(pos);\n }\n }\n }\n\n return result.sort((a, b) => b.score - a.score);\n}\n","/**\r\n * Optimized Search: QPS Candidate Pruning + Fuzzy Phrase Scoring\r\n * \r\n * This module provides an algorithmic pruning path where:\r\n * 1. QPS (Quantum Proximity Scoring) finds candidate documents quickly (token-level, typo-tolerant)\r\n * 2. Fuzzy Phrase scoring runs only on those candidates (expensive phrase scoring on smaller set)\r\n * \r\n * This dramatically reduces the search space for phrase scoring while maintaining\r\n * the same result quality as full fuzzy phrase search.\r\n */\r\n\r\nimport type { AnyOrama, Results, TypedDocument } from '@wcs-colab/orama';\r\nimport type { FuzzyPhraseConfig, PluginState, DocumentMatch, Candidate } from './types.js';\r\nimport { calculateAdaptiveTolerance } from './fuzzy.js';\r\nimport { findAllCandidates, filterCandidatesByScore, extractVocabularyFromRadixTree } from './candidates.js';\r\nimport { findPhrasesInDocument } from './scoring.js';\r\n\r\n/**\r\n * QPS Index data structure (matches plugin-qps internal format)\r\n */\r\ninterface QPSIndexData {\r\n indexes: Record<string, {\r\n type: string;\r\n node: any; // RadixNode\r\n isArray: boolean;\r\n }>;\r\n stats: Record<string, {\r\n tokenQuantums: Record<string, Record<string, number>>;\r\n tokensLength: Map<string, number>;\r\n }>;\r\n searchableProperties: string[];\r\n searchablePropertiesWithTypes: Record<string, string>;\r\n vectorIndexes: Record<string, any>;\r\n}\r\n\r\n/**\r\n * Configuration for optimized search\r\n */\r\nexport interface OptimizedSearchConfig extends FuzzyPhraseConfig {\r\n /** Maximum number of QPS candidates to consider for phrase scoring */\r\n maxQPSCandidates?: number;\r\n /** Minimum QPS score to include a candidate (0-1, relative to best score) */\r\n minQPSScore?: number;\r\n /** Whether to use exact matching for QPS (faster but no typo tolerance) */\r\n qpsExact?: boolean;\r\n /** QPS tolerance (edit distance for fuzzy matching) */\r\n qpsTolerance?: number;\r\n /** Boost factors per property for QPS scoring */\r\n qpsBoostPerProp?: Record<string, number>;\r\n}\r\n\r\n/**\r\n * Default optimized search configuration\r\n */\r\nconst DEFAULT_OPTIMIZED_CONFIG: Required<Pick<OptimizedSearchConfig, \r\n 'maxQPSCandidates' | 'minQPSScore' | 'qpsExact' | 'qpsTolerance'>> = {\r\n maxQPSCandidates: 100, // Limit phrase scoring to top 100 candidates\r\n minQPSScore: 0.1, // Include candidates with 10%+ of best score\r\n qpsExact: false, // Use fuzzy matching by default\r\n qpsTolerance: 1 // Default tolerance of 1 edit distance\r\n};\r\n\r\n/**\r\n * Normalize text using the same rules as Orama's French tokenizer\r\n * \r\n * CRITICAL: This must match normalizeFrenchText() in Orama's tokenizer exactly\r\n */\r\nexport function normalizeText(text: string): string {\r\n return text\r\n .toLowerCase()\r\n .normalize('NFD')\r\n .replace(/[\\u0300-\\u036f]/g, '') // Remove diacritics\r\n // Replace French elisions (l', d', etc.) with space to preserve word boundaries\r\n .replace(/\\b[ldcjmnst][\\u2018\\u2019\\u201A\\u201B\\u2032\\u2035\\u0027\\u0060\\u00B4](?=\\w)/gi, ' ')\r\n .replace(/[\\u2018\\u2019\\u201A\\u201B\\u2032\\u2035\\u0027\\u0060\\u00B4]/g, '') // Remove remaining apostrophes\r\n .replace(/[\\u201c\\u201d]/g, '\"') // Normalize curly quotes to straight quotes\r\n .replace(/[.,;:!?()[\\]{}\\-—–«»\"\"]/g, ' ') // Remove punctuation (replace with space to preserve word boundaries)\r\n .replace(/\\s+/g, ' ') // Normalize multiple spaces to single space\r\n .trim();\r\n}\r\n\r\n/**\r\n * Tokenize text using normalization matching Orama's French tokenizer\r\n */\r\nexport function tokenize(text: string): string[] {\r\n return normalizeText(text)\r\n .split(/\\s+/)\r\n .filter(token => token.length > 0);\r\n}\r\n\r\n/**\r\n * Call QPS search to get candidate document IDs with scores\r\n * \r\n * This function interfaces directly with QPS's internal search function,\r\n * returning candidate documents ranked by token proximity.\r\n * \r\n * @param term - Search term\r\n * @param qpsIndex - QPS index data\r\n * @param tokenizer - Tokenizer instance from Orama\r\n * @param properties - Properties to search\r\n * @param config - Optimized search configuration\r\n * @returns Array of [docId, score] tuples\r\n */\r\nfunction searchQPS(\r\n term: string,\r\n qpsIndex: QPSIndexData,\r\n tokenizer: any,\r\n properties: string[],\r\n config: OptimizedSearchConfig,\r\n language?: string\r\n): Array<[string, number]> {\r\n const tokens = tokenizer.tokenize(term, language);\r\n \r\n if (tokens.length === 0) {\r\n return [];\r\n }\r\n\r\n // QPS search configuration\r\n const exact = config.qpsExact ?? DEFAULT_OPTIMIZED_CONFIG.qpsExact;\r\n const tolerance = config.qpsTolerance ?? DEFAULT_OPTIMIZED_CONFIG.qpsTolerance;\r\n const boostPerProp = config.qpsBoostPerProp ?? {};\r\n \r\n // Results map: docId -> [score, tokenBitmask]\r\n const resultMap = new Map<string, [number, number]>();\r\n \r\n // Search each property\r\n for (const prop of properties) {\r\n const indexEntry = qpsIndex.indexes[prop];\r\n if (!indexEntry || indexEntry.type !== 'Radix') {\r\n continue;\r\n }\r\n \r\n const radixNode = indexEntry.node;\r\n const stats = qpsIndex.stats[prop];\r\n \r\n if (!radixNode || !stats) {\r\n continue;\r\n }\r\n \r\n const boost = boostPerProp[prop] ?? 1;\r\n \r\n // Search the radix tree for each token\r\n for (let i = 0; i < tokens.length; i++) {\r\n const token = tokens[i];\r\n \r\n // Find matches in radix tree (exact or fuzzy)\r\n const matches = radixNode.find({\r\n term: token,\r\n exact,\r\n tolerance: exact ? 0 : tolerance\r\n });\r\n \r\n // Process matches\r\n for (const [matchedWord, docIds] of Object.entries(matches)) {\r\n if (!Array.isArray(docIds)) continue;\r\n \r\n const isExactMatch = matchedWord === token;\r\n \r\n for (const docId of docIds as string[]) {\r\n const tokensLength = stats.tokensLength.get(docId) || 1;\r\n const quantum = stats.tokenQuantums[docId]?.[matchedWord];\r\n \r\n // Extract occurrence count and position bitmask from quantum\r\n const occurrences = quantum ? (quantum >> 20) : 1;\r\n const posBitmask = quantum ? (quantum & 0xFFFFF) : 0;\r\n \r\n // Calculate score contribution\r\n const scoreContrib = ((occurrences * occurrences) / tokensLength + (isExactMatch ? 1 : 0)) * boost;\r\n \r\n if (!resultMap.has(docId)) {\r\n resultMap.set(docId, [scoreContrib, 1 << i]);\r\n } else {\r\n const [prevScore, prevMask] = resultMap.get(docId)!;\r\n // Add proximity bonus for adjacent tokens\r\n const adjacencyBonus = countSetBits((prevMask >> 1) & (1 << i)) * 2;\r\n resultMap.set(docId, [prevScore + scoreContrib + adjacencyBonus, prevMask | (1 << i)]);\r\n }\r\n }\r\n }\r\n }\r\n }\r\n \r\n // Convert to sorted array\r\n const results = Array.from(resultMap.entries())\r\n .map(([docId, [score]]) => [docId, score] as [string, number])\r\n .sort((a, b) => b[1] - a[1]);\r\n \r\n return results;\r\n}\r\n\r\n/**\r\n * Count set bits in a number (popcount)\r\n */\r\nfunction countSetBits(n: number): number {\r\n let count = 0;\r\n while (n) {\r\n count += n & 1;\r\n n >>= 1;\r\n }\r\n return count;\r\n}\r\n\r\n/**\r\n * Get plugin state from WeakMap\r\n * Note: This function accesses the internal pluginStates WeakMap\r\n */\r\nfunction getPluginState(orama: AnyOrama, pluginStates: WeakMap<AnyOrama, PluginState>): PluginState | undefined {\r\n return pluginStates.get(orama);\r\n}\r\n\r\n/**\r\n * Optimized search combining QPS candidate pruning with fuzzy phrase scoring\r\n * \r\n * This is the main entry point for optimized search. It:\r\n * 1. Uses QPS to quickly find candidate documents (token-level matching)\r\n * 2. Filters to top candidates by QPS score\r\n * 3. Runs full fuzzy phrase scoring only on those candidates\r\n * \r\n * @param orama - Orama instance\r\n * @param qpsIndex - QPS index data (from pluginQPS.getComponents().index)\r\n * @param pluginState - Fuzzy phrase plugin state\r\n * @param params - Search parameters\r\n * @param config - Optimized search configuration\r\n * @param language - Language for tokenization (default: french)\r\n * @returns Search results with fuzzy phrase scoring\r\n */\r\nexport async function searchWithQPSPruning<T extends AnyOrama>(\r\n orama: T,\r\n qpsIndex: QPSIndexData,\r\n pluginState: PluginState,\r\n params: {\r\n term?: string;\r\n properties?: string[];\r\n limit?: number;\r\n tokenCache?: Map<string, string[]>;\r\n },\r\n config: OptimizedSearchConfig = {},\r\n language: string = 'french'\r\n): Promise<Results<TypedDocument<T>>> {\r\n const startTime = performance.now();\r\n \r\n const { term, properties, tokenCache } = params;\r\n \r\n if (!term || typeof term !== 'string') {\r\n return { elapsed: { formatted: '0ms', raw: 0 }, hits: [], count: 0 };\r\n }\r\n\r\n // Use specified property or default from plugin config\r\n const textProperty = (properties && properties[0]) || pluginState.config.textProperty;\r\n const searchProperties = properties || [textProperty];\r\n\r\n // Tokenize query\r\n const queryTokens = tokenize(term);\r\n \r\n if (queryTokens.length === 0) {\r\n return { elapsed: { formatted: '0ms', raw: 0 }, hits: [], count: 0 };\r\n }\r\n\r\n // Calculate tolerance (adaptive or fixed)\r\n const tolerance = pluginState.config.adaptiveTolerance\r\n ? calculateAdaptiveTolerance(queryTokens, pluginState.config.tolerance)\r\n : pluginState.config.tolerance;\r\n\r\n console.log(`🚀 Optimized search: \"${term}\" (${queryTokens.length} tokens, tolerance: ${tolerance})`);\r\n\r\n // STEP 1: Use QPS to find candidate documents\r\n const qpsStartTime = performance.now();\r\n \r\n // Get tokenizer from Orama\r\n const tokenizer = (orama as any).tokenizer;\r\n \r\n const qpsCandidates = searchQPS(\r\n term,\r\n qpsIndex,\r\n tokenizer,\r\n searchProperties,\r\n config,\r\n language\r\n );\r\n \r\n const qpsTime = performance.now() - qpsStartTime;\r\n console.log(`⚡ QPS found ${qpsCandidates.length} candidates in ${qpsTime.toFixed(2)}ms`);\r\n\r\n if (qpsCandidates.length === 0) {\r\n return { elapsed: { formatted: '0ms', raw: 0 }, hits: [], count: 0 };\r\n }\r\n\r\n // STEP 2: Filter to top candidates\r\n const maxCandidates = config.maxQPSCandidates ?? DEFAULT_OPTIMIZED_CONFIG.maxQPSCandidates;\r\n const minScoreRatio = config.minQPSScore ?? DEFAULT_OPTIMIZED_CONFIG.minQPSScore;\r\n \r\n const bestScore = qpsCandidates[0][1];\r\n const minScore = bestScore * minScoreRatio;\r\n \r\n const filteredCandidates = qpsCandidates\r\n .filter(([, score]) => score >= minScore)\r\n .slice(0, maxCandidates);\r\n \r\n console.log(`📋 Filtered to ${filteredCandidates.length} candidates (min score: ${minScore.toFixed(2)})`);\r\n\r\n // STEP 3: Get candidate document IDs as a Set for fast lookup\r\n // Convert to strings since Object.entries returns string keys\r\n const candidateDocIds = new Set(filteredCandidates.map(([docId]) => String(docId)));\r\n\r\n // STEP 4: Find fuzzy candidates for phrase scoring\r\n // Vocabulary is lazily extracted on first search\r\n let vocabulary = pluginState.vocabulary;\r\n \r\n if (vocabulary.size === 0) {\r\n console.log('📚 Vocabulary not initialized - extracting from index...');\r\n try {\r\n const indexData = (orama as any).data?.index;\r\n let radixNode = null;\r\n \r\n if (indexData?.indexes?.[textProperty]?.node) {\r\n radixNode = indexData.indexes[textProperty].node;\r\n } else if (indexData?.[textProperty]?.node) {\r\n radixNode = indexData[textProperty].node;\r\n }\r\n \r\n if (radixNode) {\r\n pluginState.vocabulary = extractVocabularyFromRadixTree(radixNode);\r\n vocabulary = pluginState.vocabulary;\r\n console.log(`📚 Extracted ${vocabulary.size} vocabulary words`);\r\n } else {\r\n console.error('❌ Radix tree not found for vocabulary extraction');\r\n return { elapsed: { formatted: '0ms', raw: 0 }, hits: [], count: 0 };\r\n }\r\n } catch (error) {\r\n console.error('❌ Failed to extract vocabulary:', error);\r\n return { elapsed: { formatted: '0ms', raw: 0 }, hits: [], count: 0 };\r\n }\r\n }\r\n\r\n // Find candidates for all query tokens\r\n const candidatesMap = findAllCandidates(\r\n queryTokens,\r\n vocabulary,\r\n tolerance,\r\n pluginState.config.enableSynonyms ? pluginState.synonymMap : undefined,\r\n pluginState.config.synonymMatchScore\r\n );\r\n\r\n // Filter by minimum score (skip when tolerance=0 since all matches are exact with score 1.0)\r\n const filteredFuzzyCandidates = tolerance === 0\r\n ? candidatesMap\r\n : filterCandidatesByScore(candidatesMap, pluginState.config.minScore);\r\n\r\n console.log(`🎯 Fuzzy candidates: ${Array.from(filteredFuzzyCandidates.values()).reduce((sum, c) => sum + c.length, 0)} total`);\r\n\r\n // STEP 5: Run phrase scoring ONLY on QPS candidate documents\r\n const phraseStartTime = performance.now();\r\n const documentMatches: DocumentMatch[] = [];\r\n \r\n // Get documents from Orama\r\n let docs: Record<string, any> = {};\r\n if ((orama as any).data?.docs?.docs) {\r\n docs = (orama as any).data.docs.docs;\r\n }\r\n\r\n let docsScored = 0;\r\n \r\n for (const [docId, doc] of Object.entries(docs)) {\r\n // PRUNING: Only score documents that are in QPS candidates\r\n if (!candidateDocIds.has(docId)) {\r\n continue;\r\n }\r\n \r\n docsScored++;\r\n \r\n const text = doc[textProperty];\r\n if (!text || typeof text !== 'string') {\r\n continue;\r\n }\r\n\r\n // Use cached tokens if available, otherwise tokenize\r\n let docTokens: string[];\r\n if (tokenCache && tokenCache.has(docId)) {\r\n docTokens = tokenCache.get(docId)!;\r\n } else {\r\n docTokens = tokenize(text);\r\n }\r\n\r\n // Find phrases in this document\r\n const phrases = findPhrasesInDocument(\r\n docTokens,\r\n filteredFuzzyCandidates,\r\n {\r\n weights: pluginState.config.weights as { exact: number; fuzzy: number; order: number; proximity: number; density: number; semantic: number },\r\n maxGap: pluginState.config.maxGap,\r\n proximitySpanMultiplier: pluginState.config.proximitySpanMultiplier,\r\n tolerance\r\n },\r\n pluginState.documentFrequency,\r\n pluginState.totalDocuments,\r\n queryTokens\r\n );\r\n\r\n if (phrases.length > 0) {\r\n const docScore = Math.max(...phrases.map(p => p.score));\r\n documentMatches.push({\r\n id: docId,\r\n phrases,\r\n score: docScore,\r\n document: doc\r\n });\r\n }\r\n }\r\n\r\n const phraseTime = performance.now() - phraseStartTime;\r\n console.log(`📊 Phrase scored ${docsScored} documents in ${phraseTime.toFixed(2)}ms`);\r\n\r\n // Sort by score descending\r\n documentMatches.sort((a, b) => b.score - a.score);\r\n\r\n // Apply final score minimum filter if enabled\r\n let finalMatches = documentMatches;\r\n if (pluginState.config.enableFinalScoreMinimum && pluginState.config.finalScoreMinimum > 0) {\r\n const threshold = pluginState.config.finalScoreMinimum;\r\n const beforeCount = finalMatches.length;\r\n finalMatches = finalMatches.filter(m => m.score >= threshold);\r\n console.log(`🎚️ Final score filter: ${beforeCount} → ${finalMatches.length} (threshold: ${threshold})`);\r\n }\r\n\r\n // Apply limit if specified\r\n const limit = params.limit ?? finalMatches.length;\r\n const limitedMatches = finalMatches.slice(0, limit);\r\n\r\n // Convert to Orama results format\r\n const hits = limitedMatches.map(match => ({\r\n id: match.id,\r\n score: match.score,\r\n document: match.document,\r\n _phrases: match.phrases\r\n })) as any[];\r\n\r\n const elapsed = performance.now() - startTime;\r\n\r\n console.log(`✅ Optimized search: ${hits.length} results in ${elapsed.toFixed(2)}ms (QPS: ${qpsTime.toFixed(2)}ms, Phrase: ${phraseTime.toFixed(2)}ms)`);\r\n\r\n return {\r\n elapsed: {\r\n formatted: `${elapsed.toFixed(2)}ms`,\r\n raw: Math.floor(elapsed * 1000000),\r\n qpsTime,\r\n phraseTime\r\n },\r\n hits,\r\n count: hits.length\r\n } as any;\r\n}\r\n\r\n/**\r\n * Create an optimized search function bound to a specific Orama + QPS index\r\n * \r\n * This is a convenience wrapper that captures the Orama instance and QPS index,\r\n * returning a simpler search function.\r\n * \r\n * @param orama - Orama instance\r\n * @param qpsIndex - QPS index data\r\n * @param pluginState - Fuzzy phrase plugin state\r\n * @param config - Optimized search configuration\r\n * @returns Bound search function\r\n */\r\nexport function createOptimizedSearch<T extends AnyOrama>(\r\n orama: T,\r\n qpsIndex: QPSIndexData,\r\n pluginState: PluginState,\r\n config: OptimizedSearchConfig = {}\r\n) {\r\n return async (params: {\r\n term?: string;\r\n properties?: string[];\r\n limit?: number;\r\n tokenCache?: Map<string, string[]>;\r\n }, language: string = 'french'): Promise<Results<TypedDocument<T>>> => {\r\n return searchWithQPSPruning(orama, qpsIndex, pluginState, params, config, language);\r\n };\r\n}\r\n","/**\r\n * Fuzzy Phrase Plugin for Orama\r\n * \r\n * Advanced fuzzy phrase matching with semantic weighting and synonym expansion.\r\n * Completely independent from QPS - accesses Orama's radix tree directly.\r\n */\r\n\r\nimport type { AnyOrama, OramaPlugin, Results, TypedDocument } from '@wcs-colab/orama';\r\nimport type { FuzzyPhraseConfig, PluginState, SynonymMap, DocumentMatch } from './types.js';\r\nimport { calculateAdaptiveTolerance } from './fuzzy.js';\r\nimport { \r\n extractVocabularyFromRadixTree, \r\n findAllCandidates,\r\n filterCandidatesByScore \r\n} from './candidates.js';\r\nimport { findPhrasesInDocument } from './scoring.js';\r\n\r\n/**\r\n * Default configuration\r\n */\r\nconst DEFAULT_CONFIG: Required<FuzzyPhraseConfig> = {\r\n textProperty: 'normalized_content', // Must match server's field name\r\n tolerance: 1,\r\n adaptiveTolerance: true,\r\n enableSynonyms: false,\r\n supabase: undefined as any,\r\n synonymMatchScore: 0.8,\r\n weights: {\r\n exact: 1.0,\r\n fuzzy: 0.8,\r\n order: 0.3,\r\n proximity: 0.2,\r\n density: 0.2,\r\n semantic: 0.15\r\n },\r\n maxGap: 5,\r\n minScore: 0.1,\r\n enableFinalScoreMinimum: false,\r\n finalScoreMinimum: 0.3,\r\n proximitySpanMultiplier: 5\r\n};\r\n\r\n/**\r\n * Plugin state storage (keyed by Orama instance)\r\n */\r\nconst pluginStates = new WeakMap<AnyOrama, PluginState>();\r\n\r\n/**\r\n * Create the Fuzzy Phrase Plugin\r\n * \r\n * @param userConfig - User configuration options\r\n * @returns Orama plugin instance\r\n */\r\nexport function pluginFuzzyPhrase(userConfig: FuzzyPhraseConfig = {}): OramaPlugin {\r\n // Merge user config with defaults\r\n const config: Required<FuzzyPhraseConfig> = {\r\n textProperty: userConfig.textProperty ?? DEFAULT_CONFIG.textProperty,\r\n tolerance: userConfig.tolerance ?? DEFAULT_CONFIG.tolerance,\r\n adaptiveTolerance: userConfig.adaptiveTolerance ?? DEFAULT_CONFIG.adaptiveTolerance,\r\n enableSynonyms: userConfig.enableSynonyms ?? DEFAULT_CONFIG.enableSynonyms,\r\n supabase: userConfig.supabase || DEFAULT_CONFIG.supabase,\r\n synonymMatchScore: userConfig.synonymMatchScore ?? DEFAULT_CONFIG.synonymMatchScore,\r\n weights: {\r\n exact: userConfig.weights?.exact ?? DEFAULT_CONFIG.weights.exact,\r\n fuzzy: userConfig.weights?.fuzzy ?? DEFAULT_CONFIG.weights.fuzzy,\r\n order: userConfig.weights?.order ?? DEFAULT_CONFIG.weights.order,\r\n proximity: userConfig.weights?.proximity ?? DEFAULT_CONFIG.weights.proximity,\r\n density: userConfig.weights?.density ?? DEFAULT_CONFIG.weights.density,\r\n semantic: userConfig.weights?.semantic ?? DEFAULT_CONFIG.weights.semantic\r\n },\r\n maxGap: userConfig.maxGap ?? DEFAULT_CONFIG.maxGap,\r\n minScore: userConfig.minScore ?? DEFAULT_CONFIG.minScore,\r\n enableFinalScoreMinimum: userConfig.enableFinalScoreMinimum ?? DEFAULT_CONFIG.enableFinalScoreMinimum,\r\n finalScoreMinimum: userConfig.finalScoreMinimum ?? DEFAULT_CONFIG.finalScoreMinimum,\r\n proximitySpanMultiplier: userConfig.proximitySpanMultiplier ?? DEFAULT_CONFIG.proximitySpanMultiplier\r\n };\r\n\r\n const plugin: OramaPlugin = {\r\n name: 'fuzzy-phrase',\r\n\r\n /**\r\n * Initialize plugin after index is created\r\n */\r\n afterCreate: async (orama: AnyOrama) => {\r\n console.log('🔮 Initializing Fuzzy Phrase Plugin...');\r\n\r\n // Initialize state with empty vocabulary (will be populated below)\r\n const state: PluginState = {\r\n synonymMap: {},\r\n config,\r\n documentFrequency: new Map(),\r\n totalDocuments: 0,\r\n vocabulary: new Set()\r\n };\r\n\r\n // Load synonyms from Supabase if enabled\r\n if (config.enableSynonyms && config.supabase) {\r\n try {\r\n console.log('📖 Loading synonyms from Supabase...');\r\n state.synonymMap = await loadSynonymsFromSupabase(config.supabase);\r\n console.log(`✅ Loaded ${Object.keys(state.synonymMap).length} words with synonyms`);\r\n } catch (error) {\r\n console.error('⚠️ Failed to load synonyms:', error);\r\n // Continue without synonyms\r\n }\r\n }\r\n\r\n // Calculate document frequencies for TF-IDF from document store\r\n const docs = (orama.data as any)?.docs?.docs;\r\n if (docs) {\r\n state.totalDocuments = Object.keys(docs).length;\r\n state.documentFrequency = calculateDocumentFrequencies(docs, config.textProperty);\r\n console.log(`📊 Calculated document frequencies for ${state.totalDocuments} documents`);\r\n }\r\n\r\n // CACHE VOCABULARY: Extract from radix tree ONCE at startup\r\n // This avoids O(V) radix traversal on every query\r\n try {\r\n const indexData = (orama as any).data?.index;\r\n let radixNode = null;\r\n \r\n if (indexData?.indexes?.[config.textProperty]?.node) {\r\n radixNode = indexData.indexes[config.textProperty].node;\r\n } else if (indexData?.[config.textProperty]?.node) {\r\n radixNode = indexData[config.textProperty].node;\r\n }\r\n \r\n if (radixNode) {\r\n state.vocabulary = extractVocabularyFromRadixTree(radixNode);\r\n console.log(`📚 Cached ${state.vocabulary.size} vocabulary words (eliminates per-query radix traversal)`);\r\n } else {\r\n console.warn('⚠️ Could not find radix tree for vocabulary caching');\r\n }\r\n } catch (error) {\r\n console.error('⚠️ Failed to cache vocabulary:', error);\r\n }\r\n\r\n // Store state\r\n pluginStates.set(orama, state);\r\n console.log('✅ Fuzzy Phrase Plugin initialized');\r\n \r\n // Signal ready - emit a custom event that can be listened to\r\n // Use setImmediate to ensure this runs after the afterCreate hook completes\r\n setImmediate(() => {\r\n if (typeof (globalThis as any).fuzzyPhrasePluginReady === 'function') {\r\n console.log('📡 Signaling plugin ready...');\r\n (globalThis as any).fuzzyPhrasePluginReady();\r\n } else {\r\n console.warn('⚠️ fuzzyPhrasePluginReady callback not found');\r\n }\r\n });\r\n }\r\n };\r\n\r\n return plugin;\r\n}\r\n\r\n/**\r\n * Search with fuzzy phrase matching\r\n * \r\n * This function should be called instead of the regular search() function\r\n * to enable fuzzy phrase matching.\r\n */\r\nexport async function searchWithFuzzyPhrase<T extends AnyOrama>(\r\n orama: T, \r\n params: { \r\n term?: string; \r\n properties?: string[]; \r\n limit?: number;\r\n tokenCache?: Map<string, string[]>; // Pre-tokenized documents for fast search\r\n candidateIds?: Set<string> | string[]; // Optional: only score these doc IDs (for QPS pruning)\r\n },\r\n language?: string\r\n): Promise<Results<TypedDocument<T>>> {\r\n const startTime = performance.now();\r\n \r\n // Get plugin state\r\n const state = pluginStates.get(orama);\r\n \r\n if (!state) {\r\n console.error('❌ Plugin state not initialized');\r\n throw new Error('Fuzzy Phrase Plugin not properly initialized');\r\n }\r\n\r\n const { term, properties, tokenCache, candidateIds } = params;\r\n \r\n // Convert candidateIds to Set for fast lookup if provided\r\n const candidateIdSet: Set<string> | null = candidateIds \r\n ? (candidateIds instanceof Set ? candidateIds : new Set(candidateIds))\r\n : null;\r\n \r\n if (!term || typeof term !== 'string') {\r\n return { elapsed: { formatted: '0ms', raw: 0 }, hits: [], count: 0 };\r\n }\r\n\r\n // Use specified property or default\r\n const textProperty = (properties && properties[0]) || state.config.textProperty;\r\n\r\n // Tokenize query\r\n const queryTokens = tokenize(term);\r\n \r\n if (queryTokens.length === 0) {\r\n return { elapsed: { formatted: '0ms', raw: 0 }, hits: [], count: 0 };\r\n }\r\n\r\n // Calculate tolerance (adaptive or fixed)\r\n const tolerance = state.config.adaptiveTolerance\r\n ? calculateAdaptiveTolerance(queryTokens, state.config.tolerance)\r\n : state.config.tolerance;\r\n\r\n console.log(`🔍 Fuzzy phrase search: \"${term}\" (${queryTokens.length} tokens, tolerance: ${tolerance})`);\r\n\r\n // USE CACHED VOCABULARY - lazy initialization on first search\r\n // afterCreate runs before documents are inserted, so we extract on first search\r\n let vocabulary = state.vocabulary;\r\n \r\n if (vocabulary.size === 0) {\r\n console.log('📚 Vocabulary cache empty - extracting on first search...');\r\n try {\r\n const indexData = (orama as any).data?.index;\r\n let radixNode = null;\r\n \r\n if (indexData?.indexes?.[textProperty]?.node) {\r\n radixNode = indexData.indexes[textProperty].node;\r\n } else if (indexData?.[textProperty]?.node) {\r\n radixNode = indexData[textProperty].node;\r\n }\r\n \r\n if (radixNode) {\r\n state.vocabulary = extractVocabularyFromRadixTree(radixNode);\r\n vocabulary = state.vocabulary;\r\n console.log(`📚 Cached ${vocabulary.size} vocabulary words (will be reused for subsequent queries)`);\r\n } else {\r\n console.error('❌ Radix tree not found for vocabulary extraction');\r\n return { elapsed: { formatted: '0ms', raw: 0 }, hits: [], count: 0 };\r\n }\r\n } catch (error) {\r\n console.error('❌ Failed to extract vocabulary:', error);\r\n return { elapsed: { formatted: '0ms', raw: 0 }, hits: [], count: 0 };\r\n }\r\n } else {\r\n console.log(`📚 Using cached vocabulary (${vocabulary.size} words)`);\r\n }\r\n\r\n // Find candidates for all query tokens\r\n const candidatesMap = findAllCandidates(\r\n queryTokens,\r\n vocabulary,\r\n tolerance,\r\n state.config.enableSynonyms ? state.synonymMap : undefined,\r\n state.config.synonymMatchScore\r\n );\r\n\r\n // Filter by minimum score (skip when tolerance=0 since all matches are exact with score 1.0)\r\n const filteredCandidates = tolerance === 0\r\n ? candidatesMap // Skip filtering - all matches are exact\r\n : filterCandidatesByScore(candidatesMap, state.config.minScore);\r\n\r\n console.log(`🎯 Found candidates: ${Array.from(filteredCandidates.values()).reduce((sum, c) => sum + c.length, 0)} total`);\r\n\r\n // Search through all documents\r\n const documentMatches: DocumentMatch[] = [];\r\n \r\n console.log('🔍 DEBUG orama.data structure:', {\r\n dataKeys: Object.keys((orama as any).data || {}),\r\n hasDocs: !!((orama as any).data?.docs),\r\n docsType: (orama as any).data?.docs ? typeof (orama as any).data.docs : 'undefined'\r\n });\r\n \r\n // Try multiple possible document storage locations\r\n let docs: Record<string, any> = {};\r\n \r\n // Access the actual documents - they're nested in orama.data.docs.docs\r\n if ((orama as any).data?.docs?.docs) {\r\n docs = (orama as any).data.docs.docs;\r\n console.log('✅ Found docs at orama.data.docs.docs');\r\n }\r\n // Fallback: orama.data.docs (might be the correct structure in some cases)\r\n else if ((orama as any).data?.docs && typeof (orama as any).data.docs === 'object') {\r\n // Check if it has document-like properties (not sharedInternalDocumentStore, etc.)\r\n const firstKey = Object.keys((orama as any).data.docs)[0];\r\n if (firstKey && firstKey !== 'sharedInternalDocumentStore' && firstKey !== 'count') {\r\n docs = (orama as any).data.docs;\r\n console.log('✅ Found docs at orama.data.docs (direct)');\r\n }\r\n }\r\n \r\n if (Object.keys(docs).length === 0) {\r\n console.log('❌ Could not find documents - available structure:', {\r\n hasDataDocs: !!((orama as any).data?.docs),\r\n dataDocsKeys: (orama as any).data?.docs ? Object.keys((orama as any).data.docs) : 'none',\r\n hasDataDocsDocs: !!((orama as any).data?.docs?.docs),\r\n dataDocsDocsCount: (orama as any).data?.docs?.docs ? Object.keys((orama as any).data.docs.docs).length : 0\r\n });\r\n }\r\n \r\n const cacheHits = tokenCache ? tokenCache.size : 0;\r\n const docsToSearch = candidateIdSet ? candidateIdSet.size : Object.keys(docs).length;\r\n console.log(`📄 Searching through ${docsToSearch} documents${candidateIdSet ? ' (pruned by candidateIds)' : ''} (${cacheHits > 0 ? `${cacheHits} tokens cached` : 'no cache'})`);\r\n\r\n for (const [docId, doc] of Object.entries(docs)) {\r\n // PRUNING: Skip docs not in candidateIds if provided\r\n // candidateIds can contain user document IDs (doc.id) OR internal Orama keys\r\n if (candidateIdSet) {\r\n const userDocId = doc.id !== undefined ? String(doc.id) : docId;\r\n if (!candidateIdSet.has(userDocId) && !candidateIdSet.has(docId)) {\r\n continue;\r\n }\r\n }\r\n \r\n const text = doc[textProperty];\r\n \r\n if (!text || typeof text !== 'string') {\r\n continue;\r\n }\r\n\r\n // Use cached tokens if available, otherwise tokenize\r\n let docTokens: string[];\r\n if (tokenCache && tokenCache.has(docId)) {\r\n docTokens = tokenCache.get(docId) as string[];\r\n } else {\r\n docTokens = tokenize(text);\r\n }\r\n\r\n // Find phrases in this document\r\n // Note: state.config.weights is guaranteed to have all properties from default merge\r\n // Pass original queryTokens to preserve duplicates (e.g., \"de ... de\")\r\n const phrases = findPhrasesInDocument(\r\n docTokens,\r\n filteredCandidates,\r\n {\r\n weights: state.config.weights as { exact: number; fuzzy: number; order: number; proximity: number; density: number; semantic: number },\r\n maxGap: state.config.maxGap,\r\n proximitySpanMultiplier: state.config.proximitySpanMultiplier,\r\n tolerance\r\n },\r\n state.documentFrequency,\r\n state.totalDocuments,\r\n queryTokens // Original tokens with duplicates preserved\r\n );\r\n\r\n if (phrases.length > 0) {\r\n // Calculate overall document score (highest phrase score)\r\n const docScore = Math.max(...phrases.map(p => p.score));\r\n\r\n documentMatches.push({\r\n id: docId,\r\n phrases,\r\n score: docScore,\r\n document: doc\r\n });\r\n }\r\n }\r\n\r\n // Sort by score descending\r\n documentMatches.sort((a, b) => b.score - a.score);\r\n\r\n // Apply final score minimum filter if enabled\r\n let filteredMatches = documentMatches;\r\n if (state.config.enableFinalScoreMinimum && state.config.finalScoreMinimum > 0) {\r\n const threshold = state.config.finalScoreMinimum;\r\n const beforeCount = filteredMatches.length;\r\n filteredMatches = filteredMatches.filter(m => m.score >= threshold);\r\n console.log(`🎚️ Final score filter: ${beforeCount} → ${filteredMatches.length} (threshold: ${threshold})`);\r\n }\r\n\r\n // Apply limit if specified\r\n const limit = params.limit ?? filteredMatches.length;\r\n const limitedMatches = filteredMatches.slice(0, limit);\r\n\r\n // Convert to Orama results format\r\n const hits = limitedMatches.map(match => ({\r\n id: match.id,\r\n score: match.score,\r\n document: match.document,\r\n // Store phrases for highlighting\r\n _phrases: match.phrases\r\n })) as any[];\r\n\r\n const elapsed = performance.now() - startTime;\r\n\r\n console.log(`✅ Found ${hits.length} results in ${elapsed.toFixed(2)}ms (limit: ${limit})`);\r\n\r\n return {\r\n elapsed: {\r\n formatted: `${elapsed.toFixed(2)}ms`,\r\n raw: Math.floor(elapsed * 1000000) // nanoseconds\r\n },\r\n hits,\r\n count: hits.length\r\n } as any;\r\n}\r\n\r\n/**\r\n * Load synonyms from Supabase\r\n */\r\nasync function loadSynonymsFromSupabase(\r\n supabaseConfig: { url: string; serviceKey: string }\r\n): Promise<SynonymMap> {\r\n try {\r\n console.log('🔍 DEBUG: Calling Supabase RPC get_synonym_map...');\r\n \r\n // Dynamic import to avoid bundling Supabase client if not needed\r\n const { createClient } = await import('@supabase/supabase-js');\r\n \r\n const supabase = createClient(supabaseConfig.url, supabaseConfig.serviceKey);\r\n \r\n // Call the get_synonym_map function\r\n const { data, error } = await supabase.rpc('get_synonym_map');\r\n \r\n console.log('🔍 DEBUG: Supabase RPC response:', {\r\n hasError: !!error,\r\n errorMessage: error?.message,\r\n hasData: !!data,\r\n dataType: typeof data,\r\n dataKeys: data ? Object.keys(data).length : 0\r\n });\r\n \r\n if (error) {\r\n throw new Error(`Supabase error: ${error.message}`);\r\n }\r\n \r\n const synonymMap = data || {};\r\n console.log(`📚 Loaded ${Object.keys(synonymMap).length} synonym entries from Supabase`);\r\n \r\n return synonymMap;\r\n } catch (error) {\r\n console.error('❌ Failed to load synonyms from Supabase:', error);\r\n throw error;\r\n }\r\n}\r\n\r\n/**\r\n * Calculate document frequencies for TF-IDF\r\n */\r\nfunction calculateDocumentFrequencies(\r\n docs: Record<string, any>,\r\n textProperty: string\r\n): Map<string, number> {\r\n const df = new Map<string, number>();\r\n\r\n for (const doc of Object.values(docs)) {\r\n const text = doc[textProperty];\r\n \r\n if (!text || typeof text !== 'string') {\r\n continue;\r\n }\r\n\r\n // Get unique words in this document\r\n const words = new Set(tokenize(text));\r\n\r\n // Increment document frequency for each unique word\r\n for (const word of words) {\r\n df.set(word, (df.get(word) || 0) + 1);\r\n }\r\n }\r\n\r\n return df;\r\n}\r\n\r\n/**\r\n * Normalize text using the same rules as server-side\r\n * \r\n * CRITICAL: This must match the normalizeText() function in server/index.js exactly\r\n * PLUS we remove all punctuation to match Orama's French tokenizer behavior\r\n */\r\nfunction normalizeText(text: string): string {\r\n return text\r\n .toLowerCase()\r\n .normalize('NFD')\r\n .replace(/[\\u0300-\\u036f]/g, '') // Remove diacritics\r\n // Replace French elisions (l', d', etc.) with space to preserve word boundaries\r\n .replace(/\\b[ldcjmnst][\\u2018\\u2019\\u201A\\u201B\\u2032\\u2035\\u0027\\u0060\\u00B4](?=\\w)/gi, ' ')\r\n .replace(/[\\u2018\\u2019\\u201A\\u201B\\u2032\\u2035\\u0027\\u0060\\u00B4]/g, '') // Remove remaining apostrophes\r\n .replace(/[\\u201c\\u201d]/g, '\"') // Normalize curly quotes to straight quotes\r\n .replace(/[.,;:!?()[\\]{}\\-—–«»\"\"]/g, ' ') // Remove punctuation (replace with space to preserve word boundaries)\r\n .replace(/\\s+/g, ' ') // Normalize multiple spaces to single space\r\n .trim();\r\n}\r\n\r\n/**\r\n * Tokenization matching normalized text behavior\r\n * \r\n * Note: Text should already be normalized before indexing, so we normalize again\r\n * to ensure plugin tokenization matches index tokenization\r\n */\r\nfunction tokenize(text: string): string[] {\r\n // Normalize first (same as indexing), then split by whitespace\r\n return normalizeText(text)\r\n .split(/\\s+/)\r\n .filter(token => token.length > 0);\r\n}\r\n\r\n/**\r\n * Export types for external use\r\n */\r\nexport type {\r\n FuzzyPhraseConfig,\r\n WordMatch,\r\n PhraseMatch,\r\n DocumentMatch,\r\n SynonymMap,\r\n Candidate,\r\n PluginState\r\n} from './types.js';\r\n\r\n/**\r\n * Export optimized search (QPS + fuzzy phrase pruning)\r\n */\r\nexport {\r\n searchWithQPSPruning,\r\n createOptimizedSearch,\r\n normalizeText as normalizeTextOptimized,\r\n tokenize as tokenizeOptimized,\r\n type OptimizedSearchConfig\r\n} from './optimized.js';\r\n\r\n/**\r\n * Re-export plugin state accessor for optimized search integration\r\n */\r\nexport function getPluginState(orama: any): PluginState | undefined {\r\n return pluginStates.get(orama);\r\n}\r\n"]}
|
|
1
|
+
{"version":3,"sources":["../src/fuzzy.ts","../src/candidates.ts","../src/scoring.ts","../src/optimized.ts","../src/index.ts"],"names":["tokenize","normalizeText"],"mappings":"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","sourcesContent":["/**\n * Fuzzy matching utilities using bounded Levenshtein distance\n * \n * This is the same algorithm used by Orama's match-highlight plugin\n * for consistent fuzzy matching behavior.\n */\n\n/**\n * Result of bounded Levenshtein distance calculation\n */\nexport interface BoundedLevenshteinResult {\n /** Whether the distance is within bounds */\n isBounded: boolean;\n /** The actual distance (only valid if isBounded is true) */\n distance: number;\n}\n\n/**\n * Calculate bounded Levenshtein distance between two strings\n * \n * Stops early if distance exceeds the bound for better performance.\n * This is the same algorithm as Orama's internal boundedLevenshtein.\n * \n * @param a - First string\n * @param b - Second string\n * @param bound - Maximum allowed distance\n * @returns Result indicating if strings are within bound and the distance\n */\nexport function boundedLevenshtein(\n a: string,\n b: string,\n bound: number\n): BoundedLevenshteinResult {\n // Quick checks\n if (a === b) {\n return { isBounded: true, distance: 0 };\n }\n\n const aLen = a.length;\n const bLen = b.length;\n\n // If length difference exceeds bound, no need to calculate\n if (Math.abs(aLen - bLen) > bound) {\n return { isBounded: false, distance: bound + 1 };\n }\n\n // Swap to ensure a is shorter (optimization)\n if (aLen > bLen) {\n [a, b] = [b, a];\n }\n\n const m = a.length;\n const n = b.length;\n\n // Use single array instead of matrix (memory optimization)\n let prevRow = new Array(n + 1);\n let currRow = new Array(n + 1);\n\n // Initialize first row\n for (let j = 0; j <= n; j++) {\n prevRow[j] = j;\n }\n\n for (let i = 1; i <= m; i++) {\n currRow[0] = i;\n let minInRow = i;\n\n for (let j = 1; j <= n; j++) {\n const cost = a[i - 1] === b[j - 1] ? 0 : 1;\n\n currRow[j] = Math.min(\n prevRow[j] + 1, // deletion\n currRow[j - 1] + 1, // insertion\n prevRow[j - 1] + cost // substitution\n );\n\n minInRow = Math.min(minInRow, currRow[j]);\n }\n\n // Early termination: if all values in row exceed bound, we're done\n if (minInRow > bound) {\n return { isBounded: false, distance: bound + 1 };\n }\n\n // Swap rows for next iteration\n [prevRow, currRow] = [currRow, prevRow];\n }\n\n const distance = prevRow[n];\n return {\n isBounded: distance <= bound,\n distance\n };\n}\n\n/**\n * Check if a word matches a query token with fuzzy matching\n * \n * @param word - Word from document\n * @param queryToken - Token from search query\n * @param tolerance - Maximum edit distance allowed\n * @returns Match result with score\n */\nexport function fuzzyMatch(\n word: string,\n queryToken: string,\n tolerance: number\n): { matches: boolean; distance: number; score: number } {\n // Exact match\n if (word === queryToken) {\n return { matches: true, distance: 0, score: 1.0 };\n }\n\n // NOTE: Prefix matching removed entirely\n // It was causing false positives (e.g., \"de\" matching \"dedain\", \"desert\")\n // and interfering with tolerance settings. Levenshtein-only is cleaner.\n\n // Fuzzy match with bounded Levenshtein distance\n const result = boundedLevenshtein(word, queryToken, tolerance);\n \n if (result.isBounded) {\n // Score decreases with distance\n // distance 1 = 0.8, distance 2 = 0.6, etc.\n const score = 1.0 - (result.distance * 0.2);\n return {\n matches: true,\n distance: result.distance,\n score: Math.max(0.1, score) // Minimum score of 0.1\n };\n }\n\n return { matches: false, distance: tolerance + 1, score: 0 };\n}\n\n/**\n * Calculate adaptive tolerance based on query length\n * \n * Longer queries get higher tolerance for better fuzzy matching.\n * \n * @param queryTokens - Array of query tokens\n * @param baseTolerance - Base tolerance value\n * @returns Calculated tolerance (always an integer)\n */\nexport function calculateAdaptiveTolerance(\n queryTokens: string[],\n baseTolerance: number\n): number {\n const queryLength = queryTokens.length;\n \n if (queryLength <= 2) {\n return baseTolerance;\n } else if (queryLength <= 4) {\n return baseTolerance + 1;\n } else if (queryLength <= 6) {\n return baseTolerance + 2;\n } else {\n return baseTolerance + 3;\n }\n}\n","/**\r\n * Candidate expansion: Find all possible matches for query tokens\r\n * including exact matches, fuzzy matches, and synonyms\r\n */\r\n\r\nimport { fuzzyMatch } from './fuzzy.js';\r\nimport type { Candidate, SynonymMap } from './types.js';\r\n\r\n/**\r\n * Extract all unique words from the radix tree index\r\n * \r\n * @param radixNode - Root node of the radix tree\r\n * @returns Set of all unique words in the index\r\n */\r\nexport function extractVocabularyFromRadixTree(radixNode: any): Set<string> {\r\n const vocabulary = new Set<string>();\r\n let nodesVisited = 0;\r\n let wordsFound = 0;\r\n \r\n function traverse(node: any, depth: number = 0) {\r\n if (!node) {\r\n return;\r\n }\r\n \r\n nodesVisited++;\r\n \r\n // Check if this node represents a complete word\r\n // e = true means it's an end of a word\r\n if (node.e && node.w && typeof node.w === 'string' && node.w.length > 0) {\r\n vocabulary.add(node.w);\r\n wordsFound++;\r\n }\r\n \r\n // Children can be Map, Array, or Object\r\n if (node.c) {\r\n if (node.c instanceof Map) {\r\n // Map format\r\n for (const [_key, childNode] of node.c) {\r\n traverse(childNode, depth + 1);\r\n }\r\n } else if (Array.isArray(node.c)) {\r\n // Array format: [[key, childNode], ...]\r\n for (const [_key, childNode] of node.c) {\r\n traverse(childNode, depth + 1);\r\n }\r\n } else if (typeof node.c === 'object') {\r\n // Object format: {key: childNode, ...}\r\n for (const childNode of Object.values(node.c)) {\r\n traverse(childNode, depth + 1);\r\n }\r\n }\r\n }\r\n }\r\n \r\n traverse(radixNode);\r\n console.log(`📚 Extracted ${vocabulary.size} words from ${nodesVisited} nodes visited`);\r\n return vocabulary;\r\n}\r\n\r\n/**\r\n * Find all candidate matches for a single query token\r\n * \r\n * @param queryToken - Token from search query\r\n * @param vocabulary - Set of all words in the index\r\n * @param tolerance - Fuzzy matching tolerance\r\n * @param synonyms - Synonym map (optional)\r\n * @param synonymScore - Score multiplier for synonym matches\r\n * @returns Array of candidate matches\r\n */\r\nexport function findCandidatesForToken(\r\n queryToken: string,\r\n vocabulary: Set<string>,\r\n tolerance: number,\r\n synonyms?: SynonymMap,\r\n synonymScore: number = 0.8\r\n): Candidate[] {\r\n const candidates: Candidate[] = [];\r\n const seen = new Set<string>();\r\n\r\n // 1. Check for exact match\r\n if (vocabulary.has(queryToken)) {\r\n candidates.push({\r\n word: queryToken,\r\n type: 'exact',\r\n queryToken,\r\n distance: 0,\r\n score: 1.0\r\n });\r\n seen.add(queryToken);\r\n }\r\n\r\n // 2. Check for fuzzy matches\r\n for (const word of vocabulary) {\r\n if (seen.has(word)) continue;\r\n\r\n const match = fuzzyMatch(word, queryToken, tolerance);\r\n if (match.matches) {\r\n candidates.push({\r\n word,\r\n type: 'fuzzy',\r\n queryToken,\r\n distance: match.distance,\r\n score: match.score\r\n });\r\n seen.add(word);\r\n }\r\n }\r\n\r\n // 3. Check for synonym matches\r\n if (synonyms && synonyms[queryToken]) {\r\n for (const synonym of synonyms[queryToken]) {\r\n if (seen.has(synonym)) continue;\r\n if (vocabulary.has(synonym)) {\r\n candidates.push({\r\n word: synonym,\r\n type: 'synonym',\r\n queryToken,\r\n distance: 0,\r\n score: synonymScore\r\n });\r\n seen.add(synonym);\r\n }\r\n }\r\n }\r\n\r\n return candidates;\r\n}\r\n\r\n/**\r\n * Find candidates for all query tokens\r\n * \r\n * @param queryTokens - Array of tokens from search query\r\n * @param vocabulary - Set of all words in the index\r\n * @param tolerance - Fuzzy matching tolerance\r\n * @param synonyms - Synonym map (optional)\r\n * @param synonymScore - Score multiplier for synonym matches\r\n * @returns Map of query tokens to their candidate matches\r\n */\r\nexport function findAllCandidates(\r\n queryTokens: string[],\r\n vocabulary: Set<string>,\r\n tolerance: number,\r\n synonyms?: SynonymMap,\r\n synonymScore: number = 0.8\r\n): Map<string, Candidate[]> {\r\n const candidatesMap = new Map<string, Candidate[]>();\r\n\r\n for (const token of queryTokens) {\r\n const tokenCandidates = findCandidatesForToken(\r\n token,\r\n vocabulary,\r\n tolerance,\r\n synonyms,\r\n synonymScore\r\n );\r\n candidatesMap.set(token, tokenCandidates);\r\n }\r\n\r\n return candidatesMap;\r\n}\r\n\r\n/**\r\n * Get total number of candidates across all tokens\r\n * \r\n * @param candidatesMap - Map of token to candidates\r\n * @returns Total count of all candidates\r\n */\r\nexport function getTotalCandidateCount(\r\n candidatesMap: Map<string, Candidate[]>\r\n): number {\r\n let total = 0;\r\n for (const candidates of candidatesMap.values()) {\r\n total += candidates.length;\r\n }\r\n return total;\r\n}\r\n\r\n/**\r\n * Filter candidates by minimum score threshold\r\n * \r\n * @param candidatesMap - Map of token to candidates\r\n * @param minScore - Minimum score threshold\r\n * @returns Filtered candidates map\r\n */\r\nexport function filterCandidatesByScore(\r\n candidatesMap: Map<string, Candidate[]>,\r\n minScore: number\r\n): Map<string, Candidate[]> {\r\n const filtered = new Map<string, Candidate[]>();\r\n\r\n for (const [token, candidates] of candidatesMap.entries()) {\r\n const filteredCandidates = candidates.filter(c => c.score >= minScore);\r\n if (filteredCandidates.length > 0) {\r\n filtered.set(token, filteredCandidates);\r\n }\r\n }\r\n\r\n return filtered;\r\n}\r\n","/**\n * Phrase scoring algorithm with semantic weighting\n */\n\nimport type { WordMatch, PhraseMatch, Candidate, GapWord } from './types.js';\n\n/**\n * Type for the candidate lookup structure\n */\nexport type CandidateLookup = Map<string, Array<{ queryToken: string, candidate: Candidate }>>;\n\n/**\n * PHASE 1 OPTIMIZATION A: Build candidateLookup once per query instead of per document\n * \n * Creates a map from document words to their matching query candidates,\n * sorted by quality (exact matches first, then by score).\n * \n * @param candidatesMap - Map of query tokens to their candidates\n * @returns Lookup map for fast document word matching\n */\nexport function buildCandidateLookup(\n candidatesMap: Map<string, Candidate[]>\n): CandidateLookup {\n const candidateLookup: CandidateLookup = new Map();\n for (const [queryToken, candidates] of candidatesMap.entries()) {\n for (const candidate of candidates) {\n if (!candidateLookup.has(candidate.word)) {\n candidateLookup.set(candidate.word, []);\n }\n candidateLookup.get(candidate.word)!.push({ queryToken, candidate });\n }\n }\n // Sort each word's matches by quality: exact first, then by score\n for (const entries of candidateLookup.values()) {\n entries.sort((a, b) => {\n if (a.candidate.type === 'exact' && b.candidate.type !== 'exact') return -1;\n if (b.candidate.type === 'exact' && a.candidate.type !== 'exact') return 1;\n return b.candidate.score - a.candidate.score;\n });\n }\n return candidateLookup;\n}\n\n/**\n * PHASE 1 OPTIMIZATION B: Build queryTokenCounts once per query instead of per phrase\n * \n * Counts occurrences of each token in the query (handles duplicates like \"de ... de\").\n * \n * @param queryTokens - Original query tokens array\n * @returns Map of token to occurrence count\n */\nexport function buildQueryTokenCounts(queryTokens: string[]): Map<string, number> {\n const queryTokenCounts = new Map<string, number>();\n for (const token of queryTokens) {\n queryTokenCounts.set(token, (queryTokenCounts.get(token) || 0) + 1);\n }\n return queryTokenCounts;\n}\n\n/**\n * Configuration for phrase scoring\n */\nexport interface ScoringConfig {\n weights: {\n exact: number;\n fuzzy: number;\n order: number;\n proximity: number;\n density: number;\n semantic: number;\n };\n maxGap: number;\n /** \n * Multiplier for proximity window calculation.\n * proximityWindow = queryTokens.length × proximitySpanMultiplier\n */\n proximitySpanMultiplier: number;\n /**\n * Fuzzy tolerance (Levenshtein distance). When 0, only exact matches.\n */\n tolerance: number;\n}\n\n/**\n * Find all phrase matches in a document\n * \n * @param documentTokens - Tokenized document content (needed to extract gap words)\n * @param candidatesMap - Map of query tokens to their candidates\n * @param config - Scoring configuration\n * @param documentFrequency - Document frequency map for TF-IDF\n * @param totalDocuments - Total number of documents\n * @param originalQueryTokens - Original query tokens array (preserves duplicates like \"de...de\")\n * @param candidateLookup - PHASE 1 OPTIMIZATION A: Pre-built candidate lookup\n * @param queryTokenCounts - PHASE 1 OPTIMIZATION B: Pre-built query token counts\n * @returns Array of phrase matches\n */\nexport function findPhrasesInDocument(\n documentTokens: string[],\n candidatesMap: Map<string, Candidate[]>,\n config: ScoringConfig,\n documentFrequency: Map<string, number>,\n totalDocuments: number,\n originalQueryTokens: string[],\n candidateLookup: CandidateLookup,\n queryTokenCounts: Map<string, number>\n): PhraseMatch[] {\n const phrases: PhraseMatch[] = [];\n // Use original query tokens to preserve duplicates (e.g., \"de maux ... de\")\n const queryTokens = originalQueryTokens;\n\n // Find all word matches in document\n const wordMatches: WordMatch[] = [];\n\n // Iterate documentTokens in position order (0, 1, 2...)\n // Matches at same position come out quality-sorted (from candidateLookup)\n for (let i = 0; i < documentTokens.length; i++) {\n const docWord = documentTokens[i];\n const matches = candidateLookup.get(docWord);\n if (matches) {\n for (const { queryToken, candidate } of matches) {\n wordMatches.push({\n word: docWord,\n queryToken,\n position: i,\n type: candidate.type,\n distance: candidate.distance,\n score: candidate.score\n });\n }\n }\n }\n\n // Build phrases from word matches using sliding window\n for (let i = 0; i < wordMatches.length; i++) {\n const phrase = buildPhraseFromPosition(\n wordMatches,\n i,\n queryTokens,\n config,\n documentFrequency,\n totalDocuments,\n wordMatches,\n documentTokens,\n queryTokenCounts // OPTIMIZATION B: Pass pre-built queryTokenCounts\n );\n\n if (phrase && phrase.words.length > 0) {\n phrases.push(phrase);\n }\n }\n\n // Filter out low-quality single-word matches for multi-word queries\n // This prevents noise from common words like \"de\", \"la\", \"des\" appearing as separate phrases\n const minTokensRequired = queryTokens.length >= 3 ? 2 : 1;\n const filteredPhrases = phrases.filter(p => p.words.length >= minTokensRequired);\n\n // Deduplicate and sort by score\n return deduplicatePhrases(filteredPhrases);\n}\n\n/**\n * Build a phrase starting from a specific word match position\n * \n * @param wordMatches - All word matches in document\n * @param startIndex - Starting index in wordMatches array\n * @param queryTokens - Original query tokens\n * @param config - Scoring configuration\n * @param documentFrequency - Document frequency map\n * @param totalDocuments - Total document count\n * @param allWordMatches - All word matches in document (for density calculation)\n * @param documentTokens - Original document tokens (for gap word extraction)\n * @param queryTokenCounts - OPTIMIZATION B: Pre-built query token counts\n * @returns Phrase match or null\n */\nfunction buildPhraseFromPosition(\n wordMatches: WordMatch[],\n startIndex: number,\n queryTokens: string[],\n config: ScoringConfig,\n documentFrequency: Map<string, number>,\n totalDocuments: number,\n allWordMatches: WordMatch[],\n documentTokens: string[],\n queryTokenCounts: Map<string, number>\n): PhraseMatch | null {\n const startMatch = wordMatches[startIndex];\n const phraseWords: WordMatch[] = [startMatch];\n\n // Track how many times we've matched each token\n const matchedCounts = new Map<string, number>();\n matchedCounts.set(startMatch.queryToken, 1);\n\n const gapWords: GapWord[] = [];\n let totalGapUsed = 0;\n let totalMatchedTokens = 1;\n\n // Look for nearby matches to complete the phrase\n for (let i = startIndex + 1; i < wordMatches.length; i++) {\n const match = wordMatches[i];\n const lastPos = phraseWords[phraseWords.length - 1].position;\n\n // Skip if same position (can happen with multiple fuzzy matches for same word)\n if (match.position <= lastPos) {\n continue;\n }\n\n const gap = match.position - lastPos - 1;\n\n // Stop if this gap alone exceeds maximum\n if (gap > config.maxGap) {\n break;\n }\n\n // Stop if adding this gap would exceed total maxGap budget\n if (totalGapUsed + gap > config.maxGap) {\n break;\n }\n\n // Check if we still need more of this token (handles duplicates)\n const neededCount = queryTokenCounts.get(match.queryToken) || 0;\n const currentCount = matchedCounts.get(match.queryToken) || 0;\n\n if (currentCount < neededCount) {\n // Track gap words between last match and current match\n for (let pos = lastPos + 1; pos < match.position; pos++) {\n totalGapUsed++;\n gapWords.push({\n word: documentTokens[pos],\n position: pos,\n gapIndex: totalGapUsed\n });\n }\n\n phraseWords.push(match);\n matchedCounts.set(match.queryToken, currentCount + 1);\n totalMatchedTokens++;\n\n // Stop if we have all query tokens (including duplicates)\n if (totalMatchedTokens === queryTokens.length) {\n break;\n }\n }\n }\n\n // Calculate phrase score\n if (phraseWords.length > 0) {\n const coverage = phraseWords.length / queryTokens.length;\n const span = phraseWords[phraseWords.length - 1].position - phraseWords[0].position + 1;\n\n const { score, breakdown } = calculatePhraseScore(\n phraseWords,\n queryTokens,\n config,\n documentFrequency,\n totalDocuments,\n allWordMatches,\n coverage\n );\n\n return {\n words: phraseWords,\n gapWords,\n gapUsed: totalGapUsed,\n coverage,\n startPosition: phraseWords[0].position,\n endPosition: phraseWords[phraseWords.length - 1].position,\n span,\n inOrder: isInOrder(phraseWords, queryTokens),\n score,\n scoreBreakdown: breakdown\n };\n }\n\n return null;\n}\n\n/**\n * Calculate overall phrase score\n * \n * @param phraseWords - Words in the phrase\n * @param queryTokens - Original query tokens\n * @param config - Scoring configuration\n * @param documentFrequency - Document frequency map\n * @param totalDocuments - Total document count\n * @param allWordMatches - All word matches in document (for density calculation)\n * @param coverage - Pre-calculated coverage ratio (phraseWords.length / queryTokens.length)\n * @returns Phrase score (0-1) and detailed component breakdown\n */\nfunction calculatePhraseScore(\n phraseWords: WordMatch[],\n queryTokens: string[],\n config: ScoringConfig,\n documentFrequency: Map<string, number>,\n totalDocuments: number,\n allWordMatches: WordMatch[],\n coverage: number\n): { score: number; breakdown: { base: number; order: number; proximity: number; density: number; semantic: number; coverage: number } } {\n // Base score from word matches\n // Each word contributes: matchScore × typeWeight\n let baseScore = 0;\n for (const word of phraseWords) {\n const weight = word.type === 'exact' ? config.weights.exact :\n word.type === 'fuzzy' ? config.weights.fuzzy :\n config.weights.fuzzy * 0.8; // synonym gets 80% of fuzzy weight\n baseScore += word.score * weight;\n }\n baseScore /= phraseWords.length;\n\n // Order bonus: 1.0 if words appear in query order, 0.5 otherwise\n const inOrder = isInOrder(phraseWords, queryTokens);\n const orderScore = inOrder ? 1.0 : 0.5;\n\n // Proximity bonus (closer words score higher)\n // Short-circuit: skip if maxGap=0, proximity weight is 0, or single-word query (proximity meaningless)\n let proximityScore = 0;\n if (config.maxGap > 0 && config.weights.proximity > 0 && queryTokens.length > 1) {\n const span = phraseWords[phraseWords.length - 1].position - phraseWords[0].position + 1;\n const proximityWindow = queryTokens.length * config.proximitySpanMultiplier;\n proximityScore = Math.max(0, 1.0 - (span / proximityWindow));\n }\n\n // Density: Only applies to single-word queries (measures word repetition in document)\n // For multi-word phrase queries, density is 0 (coverage handles completeness separately)\n let densityScore = 0;\n\n if (queryTokens.length === 1) {\n // Single-word query: reward repetition\n const totalOccurrences = allWordMatches.length;\n // Cap at reasonable maximum to avoid runaway scores\n densityScore = Math.min(1.0, totalOccurrences / 10);\n }\n // For multi-word queries: densityScore stays 0\n // Coverage is applied as a multiplier at the end instead\n\n // Semantic score (TF-IDF based)\n const semanticScore = calculateSemanticScore(\n phraseWords,\n documentFrequency,\n totalDocuments\n );\n\n // Weighted combination\n const weights = config.weights;\n\n // Calculate weighted components\n const weightedBase = baseScore;\n const weightedOrder = orderScore * weights.order;\n const weightedProximity = proximityScore * weights.proximity;\n const weightedDensity = densityScore * weights.density;\n const weightedSemantic = semanticScore * weights.semantic;\n\n const totalScore = weightedBase + weightedOrder + weightedProximity + weightedDensity + weightedSemantic;\n\n // Calculate max possible score\n // FIX: Use actual max base weight (highest of exact/fuzzy) instead of hardcoded 1.0\n // When tolerance=0 or fuzzy weight=0, only exact matches are possible\n const canHaveFuzzyMatches = config.tolerance > 0 && weights.fuzzy > 0;\n const maxBaseWeight = canHaveFuzzyMatches ? Math.max(weights.exact, weights.fuzzy) : weights.exact;\n // Only include proximity in max if it can actually contribute (avoids penalizing scores when maxGap=0 or single-word)\n const effectiveProximityWeight = (config.maxGap > 0 && weights.proximity > 0 && queryTokens.length > 1) ? weights.proximity : 0;\n const maxPossibleScore = maxBaseWeight + weights.order + effectiveProximityWeight + weights.density + weights.semantic;\n\n // Normalize to 0-1 range\n const normalizedScore = totalScore / maxPossibleScore;\n\n // FIX: Apply coverage as a MULTIPLIER for multi-word queries\n // This ensures incomplete matches (2/3) can never outscore complete matches (3/3)\n const coverageMultiplier = queryTokens.length > 1 ? coverage : 1.0;\n const score = normalizedScore * coverageMultiplier;\n\n // Component contributions to the final normalized score (before coverage multiplier)\n const base = weightedBase / maxPossibleScore;\n const order = weightedOrder / maxPossibleScore;\n const proximity = weightedProximity / maxPossibleScore;\n const density = weightedDensity / maxPossibleScore;\n const semantic = weightedSemantic / maxPossibleScore;\n\n return {\n score,\n breakdown: {\n base,\n order,\n proximity,\n density,\n semantic,\n coverage: coverageMultiplier // Show coverage multiplier in breakdown\n }\n };\n}\n\n/**\n * Check if phrase words appear in query order\n * Handles duplicate tokens (e.g., \"de ... de\") by tracking position consumption\n * \n * @param phraseWords - Words in the phrase\n * @param queryTokens - Original query tokens\n * @returns True if in order\n */\nfunction isInOrder(phraseWords: WordMatch[], queryTokens: string[]): boolean {\n // Build array of {token, index} to handle duplicates\n // e.g., [\"de\", \"maux\", ..., \"de\"] → [{token:\"de\", idx:0}, {token:\"maux\", idx:1}, ..., {token:\"de\", idx:7}]\n const tokenPositions = queryTokens.map((token, index) => ({ token, index }));\n\n let lastMatchedIndex = -1;\n\n for (const phraseWord of phraseWords) {\n // Find the first unused position for this token that's after lastMatchedIndex\n let foundIndex = -1;\n for (const pos of tokenPositions) {\n if (pos.token === phraseWord.queryToken && pos.index > lastMatchedIndex) {\n foundIndex = pos.index;\n break;\n }\n }\n\n if (foundIndex === -1) {\n // Token not found in expected position - out of order\n return false;\n }\n\n lastMatchedIndex = foundIndex;\n }\n\n return true;\n}\n\n/**\n * Calculate semantic score using TF-IDF\n * \n * @param phraseWords - Words in the phrase\n * @param documentFrequency - Document frequency map\n * @param totalDocuments - Total document count\n * @returns Semantic score (0-1)\n */\nfunction calculateSemanticScore(\n phraseWords: WordMatch[],\n documentFrequency: Map<string, number>,\n totalDocuments: number\n): number {\n // Handle edge case: no documents\n if (totalDocuments === 0) {\n return 0;\n }\n\n let tfidfSum = 0;\n\n for (const word of phraseWords) {\n const df = documentFrequency.get(word.word) || 1;\n const idf = Math.log(totalDocuments / df);\n tfidfSum += idf;\n }\n\n // Normalize by phrase length\n const avgTfidf = tfidfSum / phraseWords.length;\n\n // Normalize to 0-1 range (assuming max IDF of ~10)\n return Math.min(1.0, avgTfidf / 10);\n}\n\n/**\n * Deduplicate overlapping phrases, keeping highest scoring ones\n * \n * @param phrases - Array of phrase matches\n * @returns Deduplicated phrases sorted by score\n */\nfunction deduplicatePhrases(phrases: PhraseMatch[]): PhraseMatch[] {\n if (phrases.length === 0) return [];\n\n // Sort by score descending\n const sorted = phrases.slice().sort((a, b) => b.score - a.score);\n const result: PhraseMatch[] = [];\n const covered = new Set<number>();\n\n for (const phrase of sorted) {\n // Check if this phrase overlaps with already selected phrases\n let overlaps = false;\n for (let pos = phrase.startPosition; pos <= phrase.endPosition; pos++) {\n if (covered.has(pos)) {\n overlaps = true;\n break;\n }\n }\n\n if (!overlaps) {\n result.push(phrase);\n // Mark positions as covered\n for (let pos = phrase.startPosition; pos <= phrase.endPosition; pos++) {\n covered.add(pos);\n }\n }\n }\n\n return result.sort((a, b) => b.score - a.score);\n}\n","/**\r\n * Optimized Search: QPS Candidate Pruning + Fuzzy Phrase Scoring\r\n * \r\n * This module provides an algorithmic pruning path where:\r\n * 1. QPS (Quantum Proximity Scoring) finds candidate documents quickly (token-level, typo-tolerant)\r\n * 2. Fuzzy Phrase scoring runs only on those candidates (expensive phrase scoring on smaller set)\r\n * \r\n * This dramatically reduces the search space for phrase scoring while maintaining\r\n * the same result quality as full fuzzy phrase search.\r\n */\r\n\r\nimport type { AnyOrama, Results, TypedDocument } from '@wcs-colab/orama';\r\nimport type { FuzzyPhraseConfig, PluginState, DocumentMatch, Candidate } from './types.js';\r\nimport { calculateAdaptiveTolerance } from './fuzzy.js';\r\nimport { findAllCandidates, filterCandidatesByScore, extractVocabularyFromRadixTree } from './candidates.js';\r\nimport { findPhrasesInDocument, buildCandidateLookup, buildQueryTokenCounts } from './scoring.js';\r\n\r\n/**\r\n * QPS Index data structure (matches plugin-qps internal format)\r\n */\r\ninterface QPSIndexData {\r\n indexes: Record<string, {\r\n type: string;\r\n node: any; // RadixNode\r\n isArray: boolean;\r\n }>;\r\n stats: Record<string, {\r\n tokenQuantums: Record<string, Record<string, number>>;\r\n tokensLength: Map<string, number>;\r\n }>;\r\n searchableProperties: string[];\r\n searchablePropertiesWithTypes: Record<string, string>;\r\n vectorIndexes: Record<string, any>;\r\n}\r\n\r\n/**\r\n * Configuration for optimized search\r\n */\r\nexport interface OptimizedSearchConfig extends FuzzyPhraseConfig {\r\n /** Maximum number of QPS candidates to consider for phrase scoring */\r\n maxQPSCandidates?: number;\r\n /** Minimum QPS score to include a candidate (0-1, relative to best score) */\r\n minQPSScore?: number;\r\n /** Whether to use exact matching for QPS (faster but no typo tolerance) */\r\n qpsExact?: boolean;\r\n /** QPS tolerance (edit distance for fuzzy matching) */\r\n qpsTolerance?: number;\r\n /** Boost factors per property for QPS scoring */\r\n qpsBoostPerProp?: Record<string, number>;\r\n}\r\n\r\n/**\r\n * Default optimized search configuration\r\n */\r\nconst DEFAULT_OPTIMIZED_CONFIG: Required<Pick<OptimizedSearchConfig,\r\n 'maxQPSCandidates' | 'minQPSScore' | 'qpsExact' | 'qpsTolerance'>> = {\r\n maxQPSCandidates: 100, // Limit phrase scoring to top 100 candidates\r\n minQPSScore: 0.1, // Include candidates with 10%+ of best score\r\n qpsExact: false, // Use fuzzy matching by default\r\n qpsTolerance: 1 // Default tolerance of 1 edit distance\r\n};\r\n\r\n/**\r\n * Normalize text using the same rules as Orama's French tokenizer\r\n * \r\n * CRITICAL: This must match normalizeFrenchText() in Orama's tokenizer exactly\r\n */\r\nexport function normalizeText(text: string): string {\r\n return text\r\n .toLowerCase()\r\n .normalize('NFD')\r\n .replace(/[\\u0300-\\u036f]/g, '') // Remove diacritics\r\n // Replace French elisions (l', d', etc.) with space to preserve word boundaries\r\n .replace(/\\b[ldcjmnst][\\u2018\\u2019\\u201A\\u201B\\u2032\\u2035\\u0027\\u0060\\u00B4](?=\\w)/gi, ' ')\r\n .replace(/[\\u2018\\u2019\\u201A\\u201B\\u2032\\u2035\\u0027\\u0060\\u00B4]/g, '') // Remove remaining apostrophes\r\n .replace(/[\\u201c\\u201d]/g, '\"') // Normalize curly quotes to straight quotes\r\n .replace(/[.,;:!?()[\\]{}\\-—–«»\"\"]/g, ' ') // Remove punctuation (replace with space to preserve word boundaries)\r\n .replace(/\\s+/g, ' ') // Normalize multiple spaces to single space\r\n .trim();\r\n}\r\n\r\n/**\r\n * Tokenize text using normalization matching Orama's French tokenizer\r\n */\r\nexport function tokenize(text: string): string[] {\r\n return normalizeText(text)\r\n .split(/\\s+/)\r\n .filter(token => token.length > 0);\r\n}\r\n\r\n/**\r\n * PHASE 2 OPTIMIZATION C: Build reduced vocabulary from candidate documents only\r\n * \r\n * When QPS returns N candidates, we only score those N docs.\r\n * Fuzzy-matching query tokens against the FULL vocabulary wastes work on words\r\n * that don't appear in any candidate doc. Build vocabulary from candidates only.\r\n * \r\n * @param candidateDocIds - Set of candidate document IDs from QPS\r\n * @param docs - Documents object from Orama\r\n * @returns Set of unique tokens from candidate documents only\r\n */\r\nfunction buildReducedVocabularyFromDocs(\r\n candidateDocIds: Set<string>,\r\n docs: Record<string, any>\r\n): Set<string> {\r\n const reducedVocab = new Set<string>();\r\n for (const docId of candidateDocIds) {\r\n const doc = docs[docId];\r\n if (!doc?.normalized_content) continue;\r\n // normalized_content is already normalized, just split into tokens\r\n const tokens = doc.normalized_content.split(/\\s+/).filter((token: string) => token.length > 0);\r\n for (const token of tokens) {\r\n reducedVocab.add(token);\r\n }\r\n }\r\n return reducedVocab;\r\n}\r\n\r\n/**\r\n * Call QPS search to get candidate document IDs with scores\r\n * \r\n * This function interfaces directly with QPS's internal search function,\r\n * returning candidate documents ranked by token proximity.\r\n * \r\n * @param term - Search term\r\n * @param qpsIndex - QPS index data\r\n * @param tokenizer - Tokenizer instance from Orama\r\n * @param properties - Properties to search\r\n * @param config - Optimized search configuration\r\n * @returns Array of [docId, score] tuples\r\n */\r\nfunction searchQPS(\r\n term: string,\r\n qpsIndex: QPSIndexData,\r\n tokenizer: any,\r\n properties: string[],\r\n config: OptimizedSearchConfig,\r\n language?: string\r\n): Array<[string, number]> {\r\n const tokens = tokenizer.tokenize(term, language);\r\n\r\n if (tokens.length === 0) {\r\n return [];\r\n }\r\n\r\n // QPS search configuration\r\n const exact = config.qpsExact ?? DEFAULT_OPTIMIZED_CONFIG.qpsExact;\r\n const tolerance = config.qpsTolerance ?? DEFAULT_OPTIMIZED_CONFIG.qpsTolerance;\r\n const boostPerProp = config.qpsBoostPerProp ?? {};\r\n\r\n // Results map: docId -> [score, tokenBitmask]\r\n const resultMap = new Map<string, [number, number]>();\r\n\r\n // Search each property\r\n for (const prop of properties) {\r\n const indexEntry = qpsIndex.indexes[prop];\r\n if (!indexEntry || indexEntry.type !== 'Radix') {\r\n continue;\r\n }\r\n\r\n const radixNode = indexEntry.node;\r\n const stats = qpsIndex.stats[prop];\r\n\r\n if (!radixNode || !stats) {\r\n continue;\r\n }\r\n\r\n const boost = boostPerProp[prop] ?? 1;\r\n\r\n // Search the radix tree for each token\r\n for (let i = 0; i < tokens.length; i++) {\r\n const token = tokens[i];\r\n\r\n // Find matches in radix tree (exact or fuzzy)\r\n const matches = radixNode.find({\r\n term: token,\r\n exact,\r\n tolerance: exact ? 0 : tolerance\r\n });\r\n\r\n // Process matches\r\n for (const [matchedWord, docIds] of Object.entries(matches)) {\r\n if (!Array.isArray(docIds)) continue;\r\n\r\n const isExactMatch = matchedWord === token;\r\n\r\n for (const docId of docIds as string[]) {\r\n const tokensLength = stats.tokensLength.get(docId) || 1;\r\n const quantum = stats.tokenQuantums[docId]?.[matchedWord];\r\n\r\n // Extract occurrence count and position bitmask from quantum\r\n const occurrences = quantum ? (quantum >> 20) : 1;\r\n const posBitmask = quantum ? (quantum & 0xFFFFF) : 0;\r\n\r\n // Calculate score contribution\r\n const scoreContrib = ((occurrences * occurrences) / tokensLength + (isExactMatch ? 1 : 0)) * boost;\r\n\r\n if (!resultMap.has(docId)) {\r\n resultMap.set(docId, [scoreContrib, 1 << i]);\r\n } else {\r\n const [prevScore, prevMask] = resultMap.get(docId)!;\r\n // Add proximity bonus for adjacent tokens\r\n const adjacencyBonus = countSetBits((prevMask >> 1) & (1 << i)) * 2;\r\n resultMap.set(docId, [prevScore + scoreContrib + adjacencyBonus, prevMask | (1 << i)]);\r\n }\r\n }\r\n }\r\n }\r\n }\r\n\r\n // Convert to sorted array\r\n const results = Array.from(resultMap.entries())\r\n .map(([docId, [score]]) => [docId, score] as [string, number])\r\n .sort((a, b) => b[1] - a[1]);\r\n\r\n return results;\r\n}\r\n\r\n/**\r\n * Count set bits in a number (popcount)\r\n */\r\nfunction countSetBits(n: number): number {\r\n let count = 0;\r\n while (n) {\r\n count += n & 1;\r\n n >>= 1;\r\n }\r\n return count;\r\n}\r\n\r\n/**\r\n * Get plugin state from WeakMap\r\n * Note: This function accesses the internal pluginStates WeakMap\r\n */\r\nfunction getPluginState(orama: AnyOrama, pluginStates: WeakMap<AnyOrama, PluginState>): PluginState | undefined {\r\n return pluginStates.get(orama);\r\n}\r\n\r\n/**\r\n * Optimized search combining QPS candidate pruning with fuzzy phrase scoring\r\n * \r\n * This is the main entry point for optimized search. It:\r\n * 1. Uses QPS to quickly find candidate documents (token-level matching)\r\n * 2. Filters to top candidates by QPS score\r\n * 3. Runs full fuzzy phrase scoring only on those candidates\r\n * \r\n * @param orama - Orama instance\r\n * @param qpsIndex - QPS index data (from pluginQPS.getComponents().index)\r\n * @param pluginState - Fuzzy phrase plugin state\r\n * @param params - Search parameters\r\n * @param config - Optimized search configuration\r\n * @param language - Language for tokenization (default: french)\r\n * @returns Search results with fuzzy phrase scoring\r\n */\r\nexport async function searchWithQPSPruning<T extends AnyOrama>(\r\n orama: T,\r\n qpsIndex: QPSIndexData,\r\n pluginState: PluginState,\r\n params: {\r\n term?: string;\r\n properties?: string[];\r\n limit?: number;\r\n tokenCache?: Map<string, string[]>;\r\n },\r\n config: OptimizedSearchConfig = {},\r\n language: string = 'french'\r\n): Promise<Results<TypedDocument<T>>> {\r\n const startTime = performance.now();\r\n\r\n const { term, properties, tokenCache } = params;\r\n\r\n if (!term || typeof term !== 'string') {\r\n return { elapsed: { formatted: '0ms', raw: 0 }, hits: [], count: 0 };\r\n }\r\n\r\n // Use specified property or default from plugin config\r\n const textProperty = (properties && properties[0]) || pluginState.config.textProperty;\r\n const searchProperties = properties || [textProperty];\r\n\r\n // Tokenize query\r\n const queryTokens = tokenize(term);\r\n\r\n if (queryTokens.length === 0) {\r\n return { elapsed: { formatted: '0ms', raw: 0 }, hits: [], count: 0 };\r\n }\r\n\r\n // Calculate tolerance (adaptive or fixed)\r\n const tolerance = pluginState.config.adaptiveTolerance\r\n ? calculateAdaptiveTolerance(queryTokens, pluginState.config.tolerance)\r\n : pluginState.config.tolerance;\r\n\r\n console.log(`🚀 Optimized search: \"${term}\" (${queryTokens.length} tokens, tolerance: ${tolerance})`);\r\n\r\n // STEP 1: Use QPS to find candidate documents\r\n const qpsStartTime = performance.now();\r\n\r\n // Get tokenizer from Orama\r\n const tokenizer = (orama as any).tokenizer;\r\n\r\n const qpsCandidates = searchQPS(\r\n term,\r\n qpsIndex,\r\n tokenizer,\r\n searchProperties,\r\n config,\r\n language\r\n );\r\n\r\n const qpsTime = performance.now() - qpsStartTime;\r\n console.log(`⚡ QPS found ${qpsCandidates.length} candidates in ${qpsTime.toFixed(2)}ms`);\r\n\r\n if (qpsCandidates.length === 0) {\r\n return { elapsed: { formatted: '0ms', raw: 0 }, hits: [], count: 0 };\r\n }\r\n\r\n // STEP 2: Filter to top candidates\r\n const maxCandidates = config.maxQPSCandidates ?? DEFAULT_OPTIMIZED_CONFIG.maxQPSCandidates;\r\n const minScoreRatio = config.minQPSScore ?? DEFAULT_OPTIMIZED_CONFIG.minQPSScore;\r\n\r\n const bestScore = qpsCandidates[0][1];\r\n const minScore = bestScore * minScoreRatio;\r\n\r\n const filteredCandidates = qpsCandidates\r\n .filter(([, score]) => score >= minScore)\r\n .slice(0, maxCandidates);\r\n\r\n console.log(`📋 Filtered to ${filteredCandidates.length} candidates (min score: ${minScore.toFixed(2)})`);\r\n\r\n // STEP 3: Get candidate document IDs as a Set for fast lookup\r\n // Convert to strings since Object.entries returns string keys\r\n const candidateDocIds = new Set(filteredCandidates.map(([docId]) => String(docId)));\r\n\r\n // STEP 4: Find fuzzy candidates for phrase scoring\r\n // Vocabulary is lazily extracted on first search (needed for fallback and logging)\r\n let vocabulary = pluginState.vocabulary;\r\n\r\n if (vocabulary.size === 0) {\r\n console.log('📚 Vocabulary not initialized - extracting from index...');\r\n try {\r\n const indexData = (orama as any).data?.index;\r\n let radixNode = null;\r\n\r\n if (indexData?.indexes?.[textProperty]?.node) {\r\n radixNode = indexData.indexes[textProperty].node;\r\n } else if (indexData?.[textProperty]?.node) {\r\n radixNode = indexData[textProperty].node;\r\n }\r\n\r\n if (radixNode) {\r\n pluginState.vocabulary = extractVocabularyFromRadixTree(radixNode);\r\n vocabulary = pluginState.vocabulary;\r\n console.log(`📚 Extracted ${vocabulary.size} vocabulary words`);\r\n } else {\r\n console.error('❌ Radix tree not found for vocabulary extraction');\r\n return { elapsed: { formatted: '0ms', raw: 0 }, hits: [], count: 0 };\r\n }\r\n } catch (error) {\r\n console.error('❌ Failed to extract vocabulary:', error);\r\n return { elapsed: { formatted: '0ms', raw: 0 }, hits: [], count: 0 };\r\n }\r\n }\r\n\r\n // PHASE 2 OPTIMIZATION C: Build reduced vocabulary from candidate documents only\r\n // Extract docs object early so we can build vocabulary from candidates only\r\n let docs: Record<string, any> = {};\r\n if ((orama as any).data?.docs?.docs) {\r\n docs = (orama as any).data.docs.docs;\r\n }\r\n\r\n const vocabStartTime = performance.now();\r\n const reducedVocabulary = buildReducedVocabularyFromDocs(candidateDocIds, docs);\r\n const vocabTime = performance.now() - vocabStartTime;\r\n console.log(`📚 Reduced vocabulary: ${reducedVocabulary.size} words (full: ${vocabulary.size}, reduction: ${(100 * (1 - reducedVocabulary.size / vocabulary.size)).toFixed(1)}%, built in ${vocabTime.toFixed(2)}ms)`);\r\n\r\n // Find candidates for all query tokens - using REDUCED vocabulary\r\n const candidatesMap = findAllCandidates(\r\n queryTokens,\r\n reducedVocabulary,\r\n tolerance,\r\n pluginState.config.enableSynonyms ? pluginState.synonymMap : undefined,\r\n pluginState.config.synonymMatchScore\r\n );\r\n\r\n // Filter by minimum score (skip when tolerance=0 since all matches are exact with score 1.0)\r\n const filteredFuzzyCandidates = tolerance === 0\r\n ? candidatesMap\r\n : filterCandidatesByScore(candidatesMap, pluginState.config.minScore);\r\n\r\n console.log(`🎯 Fuzzy candidates: ${Array.from(filteredFuzzyCandidates.values()).reduce((sum, c) => sum + c.length, 0)} total`);\r\n\r\n // STEP 5: Run phrase scoring ONLY on QPS candidate documents\r\n const phraseStartTime = performance.now();\r\n\r\n // PHASE 1 OPTIMIZATION: Build candidateLookup and queryTokenCounts once per query\r\n const candidateLookup = buildCandidateLookup(filteredFuzzyCandidates);\r\n const queryTokenCounts = buildQueryTokenCounts(queryTokens);\r\n\r\n const documentMatches: DocumentMatch[] = [];\r\n // Note: docs already extracted above for reduced vocabulary optimization\r\n\r\n let docsScored = 0;\r\n\r\n for (const [docId, doc] of Object.entries(docs)) {\r\n // PRUNING: Only score documents that are in QPS candidates\r\n if (!candidateDocIds.has(docId)) {\r\n continue;\r\n }\r\n\r\n docsScored++;\r\n\r\n const text = doc[textProperty];\r\n if (!text || typeof text !== 'string') {\r\n continue;\r\n }\r\n\r\n // Use cached tokens if available, otherwise split\r\n let docTokens: string[];\r\n if (tokenCache && tokenCache.has(docId)) {\r\n docTokens = tokenCache.get(docId)!;\r\n } else {\r\n // textProperty is 'normalized_content' which is already normalized, just split\r\n docTokens = text.split(/\\s+/).filter((token: string) => token.length > 0);\r\n }\r\n\r\n // Find phrases in this document\r\n const phrases = findPhrasesInDocument(\r\n docTokens,\r\n filteredFuzzyCandidates,\r\n {\r\n weights: pluginState.config.weights as { exact: number; fuzzy: number; order: number; proximity: number; density: number; semantic: number },\r\n maxGap: pluginState.config.maxGap,\r\n proximitySpanMultiplier: pluginState.config.proximitySpanMultiplier,\r\n tolerance\r\n },\r\n pluginState.documentFrequency,\r\n pluginState.totalDocuments,\r\n queryTokens,\r\n candidateLookup, // PHASE 1 OPTIMIZATION A: Pre-built candidate lookup\r\n queryTokenCounts // PHASE 1 OPTIMIZATION B: Pre-built query token counts\r\n );\r\n\r\n if (phrases.length > 0) {\r\n const docScore = Math.max(...phrases.map(p => p.score));\r\n documentMatches.push({\r\n id: docId,\r\n phrases,\r\n score: docScore,\r\n document: doc\r\n });\r\n }\r\n }\r\n\r\n const phraseTime = performance.now() - phraseStartTime;\r\n console.log(`📊 Phrase scored ${docsScored} documents in ${phraseTime.toFixed(2)}ms`);\r\n\r\n // Sort by score descending\r\n documentMatches.sort((a, b) => b.score - a.score);\r\n\r\n // Apply final score minimum filter if enabled\r\n let finalMatches = documentMatches;\r\n if (pluginState.config.enableFinalScoreMinimum && pluginState.config.finalScoreMinimum > 0) {\r\n const threshold = pluginState.config.finalScoreMinimum;\r\n const beforeCount = finalMatches.length;\r\n finalMatches = finalMatches.filter(m => m.score >= threshold);\r\n console.log(`🎚️ Final score filter: ${beforeCount} → ${finalMatches.length} (threshold: ${threshold})`);\r\n }\r\n\r\n // Apply limit if specified\r\n const limit = params.limit ?? finalMatches.length;\r\n const limitedMatches = finalMatches.slice(0, limit);\r\n\r\n // Convert to Orama results format\r\n const hits = limitedMatches.map(match => ({\r\n id: match.id,\r\n score: match.score,\r\n document: match.document,\r\n _phrases: match.phrases\r\n })) as any[];\r\n\r\n const elapsed = performance.now() - startTime;\r\n\r\n console.log(`✅ Optimized search: ${hits.length} results in ${elapsed.toFixed(2)}ms (QPS: ${qpsTime.toFixed(2)}ms, Phrase: ${phraseTime.toFixed(2)}ms)`);\r\n\r\n return {\r\n elapsed: {\r\n formatted: `${elapsed.toFixed(2)}ms`,\r\n raw: Math.floor(elapsed * 1000000),\r\n qpsTime,\r\n phraseTime\r\n },\r\n hits,\r\n count: hits.length\r\n } as any;\r\n}\r\n\r\n/**\r\n * Create an optimized search function bound to a specific Orama + QPS index\r\n * \r\n * This is a convenience wrapper that captures the Orama instance and QPS index,\r\n * returning a simpler search function.\r\n * \r\n * @param orama - Orama instance\r\n * @param qpsIndex - QPS index data\r\n * @param pluginState - Fuzzy phrase plugin state\r\n * @param config - Optimized search configuration\r\n * @returns Bound search function\r\n */\r\nexport function createOptimizedSearch<T extends AnyOrama>(\r\n orama: T,\r\n qpsIndex: QPSIndexData,\r\n pluginState: PluginState,\r\n config: OptimizedSearchConfig = {}\r\n) {\r\n return async (params: {\r\n term?: string;\r\n properties?: string[];\r\n limit?: number;\r\n tokenCache?: Map<string, string[]>;\r\n }, language: string = 'french'): Promise<Results<TypedDocument<T>>> => {\r\n return searchWithQPSPruning(orama, qpsIndex, pluginState, params, config, language);\r\n };\r\n}\r\n","/**\r\n * Fuzzy Phrase Plugin for Orama\r\n * \r\n * Advanced fuzzy phrase matching with semantic weighting and synonym expansion.\r\n * Completely independent from QPS - accesses Orama's radix tree directly.\r\n */\r\n\r\nimport type { AnyOrama, OramaPlugin, Results, TypedDocument } from '@wcs-colab/orama';\r\nimport type { FuzzyPhraseConfig, PluginState, SynonymMap, DocumentMatch } from './types.js';\r\nimport { calculateAdaptiveTolerance } from './fuzzy.js';\r\nimport {\r\n extractVocabularyFromRadixTree,\r\n findAllCandidates,\r\n filterCandidatesByScore\r\n} from './candidates.js';\r\nimport {\r\n findPhrasesInDocument,\r\n buildCandidateLookup,\r\n buildQueryTokenCounts,\r\n type CandidateLookup\r\n} from './scoring.js';\r\n\r\n/**\r\n * Default configuration\r\n */\r\nconst DEFAULT_CONFIG: Required<FuzzyPhraseConfig> = {\r\n textProperty: 'normalized_content', // Must match server's field name\r\n tolerance: 1,\r\n adaptiveTolerance: true,\r\n enableSynonyms: false,\r\n supabase: undefined as any,\r\n synonymMatchScore: 0.8,\r\n weights: {\r\n exact: 1.0,\r\n fuzzy: 0.8,\r\n order: 0.3,\r\n proximity: 0.2,\r\n density: 0.2,\r\n semantic: 0.15\r\n },\r\n maxGap: 5,\r\n minScore: 0.1,\r\n enableFinalScoreMinimum: false,\r\n finalScoreMinimum: 0.3,\r\n proximitySpanMultiplier: 5\r\n};\r\n\r\n/**\r\n * Plugin state storage (keyed by Orama instance)\r\n */\r\nconst pluginStates = new WeakMap<AnyOrama, PluginState>();\r\n\r\n/**\r\n * Create the Fuzzy Phrase Plugin\r\n * \r\n * @param userConfig - User configuration options\r\n * @returns Orama plugin instance\r\n */\r\nexport function pluginFuzzyPhrase(userConfig: FuzzyPhraseConfig = {}): OramaPlugin {\r\n // Merge user config with defaults\r\n const config: Required<FuzzyPhraseConfig> = {\r\n textProperty: userConfig.textProperty ?? DEFAULT_CONFIG.textProperty,\r\n tolerance: userConfig.tolerance ?? DEFAULT_CONFIG.tolerance,\r\n adaptiveTolerance: userConfig.adaptiveTolerance ?? DEFAULT_CONFIG.adaptiveTolerance,\r\n enableSynonyms: userConfig.enableSynonyms ?? DEFAULT_CONFIG.enableSynonyms,\r\n supabase: userConfig.supabase || DEFAULT_CONFIG.supabase,\r\n synonymMatchScore: userConfig.synonymMatchScore ?? DEFAULT_CONFIG.synonymMatchScore,\r\n weights: {\r\n exact: userConfig.weights?.exact ?? DEFAULT_CONFIG.weights.exact,\r\n fuzzy: userConfig.weights?.fuzzy ?? DEFAULT_CONFIG.weights.fuzzy,\r\n order: userConfig.weights?.order ?? DEFAULT_CONFIG.weights.order,\r\n proximity: userConfig.weights?.proximity ?? DEFAULT_CONFIG.weights.proximity,\r\n density: userConfig.weights?.density ?? DEFAULT_CONFIG.weights.density,\r\n semantic: userConfig.weights?.semantic ?? DEFAULT_CONFIG.weights.semantic\r\n },\r\n maxGap: userConfig.maxGap ?? DEFAULT_CONFIG.maxGap,\r\n minScore: userConfig.minScore ?? DEFAULT_CONFIG.minScore,\r\n enableFinalScoreMinimum: userConfig.enableFinalScoreMinimum ?? DEFAULT_CONFIG.enableFinalScoreMinimum,\r\n finalScoreMinimum: userConfig.finalScoreMinimum ?? DEFAULT_CONFIG.finalScoreMinimum,\r\n proximitySpanMultiplier: userConfig.proximitySpanMultiplier ?? DEFAULT_CONFIG.proximitySpanMultiplier\r\n };\r\n\r\n const plugin: OramaPlugin = {\r\n name: 'fuzzy-phrase',\r\n\r\n /**\r\n * Initialize plugin after index is created\r\n */\r\n afterCreate: async (orama: AnyOrama) => {\r\n console.log('🔮 Initializing Fuzzy Phrase Plugin...');\r\n\r\n // Initialize state with empty vocabulary (will be populated below)\r\n const state: PluginState = {\r\n synonymMap: {},\r\n config,\r\n documentFrequency: new Map(),\r\n totalDocuments: 0,\r\n vocabulary: new Set()\r\n };\r\n\r\n // Load synonyms from Supabase if enabled\r\n if (config.enableSynonyms && config.supabase) {\r\n try {\r\n console.log('📖 Loading synonyms from Supabase...');\r\n state.synonymMap = await loadSynonymsFromSupabase(config.supabase);\r\n console.log(`✅ Loaded ${Object.keys(state.synonymMap).length} words with synonyms`);\r\n } catch (error) {\r\n console.error('⚠️ Failed to load synonyms:', error);\r\n // Continue without synonyms\r\n }\r\n }\r\n\r\n // Calculate document frequencies for TF-IDF from document store\r\n const docs = (orama.data as any)?.docs?.docs;\r\n if (docs) {\r\n state.totalDocuments = Object.keys(docs).length;\r\n state.documentFrequency = calculateDocumentFrequencies(docs, config.textProperty);\r\n console.log(`📊 Calculated document frequencies for ${state.totalDocuments} documents`);\r\n }\r\n\r\n // CACHE VOCABULARY: Extract from radix tree ONCE at startup\r\n // This avoids O(V) radix traversal on every query\r\n try {\r\n const indexData = (orama as any).data?.index;\r\n let radixNode = null;\r\n\r\n if (indexData?.indexes?.[config.textProperty]?.node) {\r\n radixNode = indexData.indexes[config.textProperty].node;\r\n } else if (indexData?.[config.textProperty]?.node) {\r\n radixNode = indexData[config.textProperty].node;\r\n }\r\n\r\n if (radixNode) {\r\n state.vocabulary = extractVocabularyFromRadixTree(radixNode);\r\n console.log(`📚 Cached ${state.vocabulary.size} vocabulary words (eliminates per-query radix traversal)`);\r\n } else {\r\n console.warn('⚠️ Could not find radix tree for vocabulary caching');\r\n }\r\n } catch (error) {\r\n console.error('⚠️ Failed to cache vocabulary:', error);\r\n }\r\n\r\n // Store state\r\n pluginStates.set(orama, state);\r\n console.log('✅ Fuzzy Phrase Plugin initialized');\r\n\r\n // Signal ready - emit a custom event that can be listened to\r\n // Use setImmediate to ensure this runs after the afterCreate hook completes\r\n setImmediate(() => {\r\n if (typeof (globalThis as any).fuzzyPhrasePluginReady === 'function') {\r\n console.log('📡 Signaling plugin ready...');\r\n (globalThis as any).fuzzyPhrasePluginReady();\r\n } else {\r\n console.warn('⚠️ fuzzyPhrasePluginReady callback not found');\r\n }\r\n });\r\n }\r\n };\r\n\r\n return plugin;\r\n}\r\n\r\n/**\r\n * Search with fuzzy phrase matching\r\n * \r\n * This function should be called instead of the regular search() function\r\n * to enable fuzzy phrase matching.\r\n */\r\nexport async function searchWithFuzzyPhrase<T extends AnyOrama>(\r\n orama: T,\r\n params: {\r\n term?: string;\r\n properties?: string[];\r\n limit?: number;\r\n tokenCache?: Map<string, string[]>; // Pre-tokenized documents for fast search\r\n candidateIds?: Set<string> | string[]; // Optional: only score these doc IDs (for QPS pruning)\r\n },\r\n language?: string\r\n): Promise<Results<TypedDocument<T>>> {\r\n const startTime = performance.now();\r\n\r\n // Get plugin state\r\n const state = pluginStates.get(orama);\r\n\r\n if (!state) {\r\n console.error('❌ Plugin state not initialized');\r\n throw new Error('Fuzzy Phrase Plugin not properly initialized');\r\n }\r\n\r\n const { term, properties, tokenCache, candidateIds } = params;\r\n\r\n // Convert candidateIds to Set for fast lookup if provided\r\n const candidateIdSet: Set<string> | null = candidateIds\r\n ? (candidateIds instanceof Set ? candidateIds : new Set(candidateIds))\r\n : null;\r\n\r\n if (!term || typeof term !== 'string') {\r\n return { elapsed: { formatted: '0ms', raw: 0 }, hits: [], count: 0 };\r\n }\r\n\r\n // Use specified property or default\r\n const textProperty = (properties && properties[0]) || state.config.textProperty;\r\n\r\n // Tokenize query\r\n const queryTokens = tokenize(term);\r\n\r\n if (queryTokens.length === 0) {\r\n return { elapsed: { formatted: '0ms', raw: 0 }, hits: [], count: 0 };\r\n }\r\n\r\n // Calculate tolerance (adaptive or fixed)\r\n const tolerance = state.config.adaptiveTolerance\r\n ? calculateAdaptiveTolerance(queryTokens, state.config.tolerance)\r\n : state.config.tolerance;\r\n\r\n console.log(`🔍 Fuzzy phrase search: \"${term}\" (${queryTokens.length} tokens, tolerance: ${tolerance})`);\r\n\r\n // USE CACHED VOCABULARY - lazy initialization on first search\r\n // afterCreate runs before documents are inserted, so we extract on first search\r\n let vocabulary = state.vocabulary;\r\n\r\n if (vocabulary.size === 0) {\r\n console.log('📚 Vocabulary cache empty - extracting on first search...');\r\n try {\r\n const indexData = (orama as any).data?.index;\r\n let radixNode = null;\r\n\r\n if (indexData?.indexes?.[textProperty]?.node) {\r\n radixNode = indexData.indexes[textProperty].node;\r\n } else if (indexData?.[textProperty]?.node) {\r\n radixNode = indexData[textProperty].node;\r\n }\r\n\r\n if (radixNode) {\r\n state.vocabulary = extractVocabularyFromRadixTree(radixNode);\r\n vocabulary = state.vocabulary;\r\n console.log(`📚 Cached ${vocabulary.size} vocabulary words (will be reused for subsequent queries)`);\r\n } else {\r\n console.error('❌ Radix tree not found for vocabulary extraction');\r\n return { elapsed: { formatted: '0ms', raw: 0 }, hits: [], count: 0 };\r\n }\r\n } catch (error) {\r\n console.error('❌ Failed to extract vocabulary:', error);\r\n return { elapsed: { formatted: '0ms', raw: 0 }, hits: [], count: 0 };\r\n }\r\n } else {\r\n console.log(`📚 Using cached vocabulary (${vocabulary.size} words)`);\r\n }\r\n\r\n // Find candidates for all query tokens\r\n const candidatesMap = findAllCandidates(\r\n queryTokens,\r\n vocabulary,\r\n tolerance,\r\n state.config.enableSynonyms ? state.synonymMap : undefined,\r\n state.config.synonymMatchScore\r\n );\r\n\r\n // Filter by minimum score (skip when tolerance=0 since all matches are exact with score 1.0)\r\n const filteredCandidates = tolerance === 0\r\n ? candidatesMap // Skip filtering - all matches are exact\r\n : filterCandidatesByScore(candidatesMap, state.config.minScore);\r\n\r\n console.log(`🎯 Found candidates: ${Array.from(filteredCandidates.values()).reduce((sum, c) => sum + c.length, 0)} total`);\r\n\r\n // PHASE 1 OPTIMIZATION: Build candidateLookup and queryTokenCounts once per query\r\n const candidateLookup = buildCandidateLookup(filteredCandidates);\r\n const queryTokenCounts = buildQueryTokenCounts(queryTokens);\r\n\r\n\r\n // Search through all documents\r\n const documentMatches: DocumentMatch[] = [];\r\n\r\n console.log('🔍 DEBUG orama.data structure:', {\r\n dataKeys: Object.keys((orama as any).data || {}),\r\n hasDocs: !!((orama as any).data?.docs),\r\n docsType: (orama as any).data?.docs ? typeof (orama as any).data.docs : 'undefined'\r\n });\r\n\r\n // Try multiple possible document storage locations\r\n let docs: Record<string, any> = {};\r\n\r\n // Access the actual documents - they're nested in orama.data.docs.docs\r\n if ((orama as any).data?.docs?.docs) {\r\n docs = (orama as any).data.docs.docs;\r\n console.log('✅ Found docs at orama.data.docs.docs');\r\n }\r\n // Fallback: orama.data.docs (might be the correct structure in some cases)\r\n else if ((orama as any).data?.docs && typeof (orama as any).data.docs === 'object') {\r\n // Check if it has document-like properties (not sharedInternalDocumentStore, etc.)\r\n const firstKey = Object.keys((orama as any).data.docs)[0];\r\n if (firstKey && firstKey !== 'sharedInternalDocumentStore' && firstKey !== 'count') {\r\n docs = (orama as any).data.docs;\r\n console.log('✅ Found docs at orama.data.docs (direct)');\r\n }\r\n }\r\n\r\n if (Object.keys(docs).length === 0) {\r\n console.log('❌ Could not find documents - available structure:', {\r\n hasDataDocs: !!((orama as any).data?.docs),\r\n dataDocsKeys: (orama as any).data?.docs ? Object.keys((orama as any).data.docs) : 'none',\r\n hasDataDocsDocs: !!((orama as any).data?.docs?.docs),\r\n dataDocsDocsCount: (orama as any).data?.docs?.docs ? Object.keys((orama as any).data.docs.docs).length : 0\r\n });\r\n }\r\n\r\n const cacheHits = tokenCache ? tokenCache.size : 0;\r\n const docsToSearch = candidateIdSet ? candidateIdSet.size : Object.keys(docs).length;\r\n console.log(`📄 Searching through ${docsToSearch} documents${candidateIdSet ? ' (pruned by candidateIds)' : ''} (${cacheHits > 0 ? `${cacheHits} tokens cached` : 'no cache'})`);\r\n\r\n for (const [docId, doc] of Object.entries(docs)) {\r\n // PRUNING: Skip docs not in candidateIds if provided\r\n // candidateIds can contain user document IDs (doc.id) OR internal Orama keys\r\n if (candidateIdSet) {\r\n const userDocId = doc.id !== undefined ? String(doc.id) : docId;\r\n if (!candidateIdSet.has(userDocId) && !candidateIdSet.has(docId)) {\r\n continue;\r\n }\r\n }\r\n\r\n const text = doc[textProperty];\r\n\r\n if (!text || typeof text !== 'string') {\r\n continue;\r\n }\r\n\r\n // Use cached tokens if available, otherwise tokenize\r\n let docTokens: string[];\r\n if (tokenCache && tokenCache.has(docId)) {\r\n docTokens = tokenCache.get(docId) as string[];\r\n } else {\r\n docTokens = tokenize(text);\r\n }\r\n\r\n // Find phrases in this document\r\n // Note: state.config.weights is guaranteed to have all properties from default merge\r\n // Pass original queryTokens to preserve duplicates (e.g., \"de ... de\")\r\n const phrases = findPhrasesInDocument(\r\n docTokens,\r\n filteredCandidates,\r\n {\r\n weights: state.config.weights as { exact: number; fuzzy: number; order: number; proximity: number; density: number; semantic: number },\r\n maxGap: state.config.maxGap,\r\n proximitySpanMultiplier: state.config.proximitySpanMultiplier,\r\n tolerance\r\n },\r\n state.documentFrequency,\r\n state.totalDocuments,\r\n queryTokens,\r\n candidateLookup, // PHASE 1 OPTIMIZATION A: Pre-built candidate lookup\r\n queryTokenCounts // PHASE 1 OPTIMIZATION B: Pre-built query token counts\r\n );\r\n\r\n if (phrases.length > 0) {\r\n // Calculate overall document score (highest phrase score)\r\n const docScore = Math.max(...phrases.map(p => p.score));\r\n\r\n documentMatches.push({\r\n id: docId,\r\n phrases,\r\n score: docScore,\r\n document: doc\r\n });\r\n }\r\n }\r\n\r\n // Sort by score descending\r\n documentMatches.sort((a, b) => b.score - a.score);\r\n\r\n // Apply final score minimum filter if enabled\r\n let filteredMatches = documentMatches;\r\n if (state.config.enableFinalScoreMinimum && state.config.finalScoreMinimum > 0) {\r\n const threshold = state.config.finalScoreMinimum;\r\n const beforeCount = filteredMatches.length;\r\n filteredMatches = filteredMatches.filter(m => m.score >= threshold);\r\n console.log(`🎚️ Final score filter: ${beforeCount} → ${filteredMatches.length} (threshold: ${threshold})`);\r\n }\r\n\r\n // Apply limit if specified\r\n const limit = params.limit ?? filteredMatches.length;\r\n const limitedMatches = filteredMatches.slice(0, limit);\r\n\r\n // Convert to Orama results format\r\n const hits = limitedMatches.map(match => ({\r\n id: match.id,\r\n score: match.score,\r\n document: match.document,\r\n // Store phrases for highlighting\r\n _phrases: match.phrases\r\n })) as any[];\r\n\r\n const elapsed = performance.now() - startTime;\r\n\r\n console.log(`✅ Found ${hits.length} results in ${elapsed.toFixed(2)}ms (limit: ${limit})`);\r\n\r\n return {\r\n elapsed: {\r\n formatted: `${elapsed.toFixed(2)}ms`,\r\n raw: Math.floor(elapsed * 1000000) // nanoseconds\r\n },\r\n hits,\r\n count: hits.length\r\n } as any;\r\n}\r\n\r\n/**\r\n * Load synonyms from Supabase\r\n */\r\nasync function loadSynonymsFromSupabase(\r\n supabaseConfig: { url: string; serviceKey: string }\r\n): Promise<SynonymMap> {\r\n try {\r\n console.log('🔍 DEBUG: Calling Supabase RPC get_synonym_map...');\r\n\r\n // Dynamic import to avoid bundling Supabase client if not needed\r\n const { createClient } = await import('@supabase/supabase-js');\r\n\r\n const supabase = createClient(supabaseConfig.url, supabaseConfig.serviceKey);\r\n\r\n // Call the get_synonym_map function\r\n const { data, error } = await supabase.rpc('get_synonym_map');\r\n\r\n console.log('🔍 DEBUG: Supabase RPC response:', {\r\n hasError: !!error,\r\n errorMessage: error?.message,\r\n hasData: !!data,\r\n dataType: typeof data,\r\n dataKeys: data ? Object.keys(data).length : 0\r\n });\r\n\r\n if (error) {\r\n throw new Error(`Supabase error: ${error.message}`);\r\n }\r\n\r\n const synonymMap = data || {};\r\n console.log(`📚 Loaded ${Object.keys(synonymMap).length} synonym entries from Supabase`);\r\n\r\n return synonymMap;\r\n } catch (error) {\r\n console.error('❌ Failed to load synonyms from Supabase:', error);\r\n throw error;\r\n }\r\n}\r\n\r\n/**\r\n * Calculate document frequencies for TF-IDF\r\n */\r\nfunction calculateDocumentFrequencies(\r\n docs: Record<string, any>,\r\n textProperty: string\r\n): Map<string, number> {\r\n const df = new Map<string, number>();\r\n\r\n for (const doc of Object.values(docs)) {\r\n const text = doc[textProperty];\r\n\r\n if (!text || typeof text !== 'string') {\r\n continue;\r\n }\r\n\r\n // Get unique words in this document\r\n const words = new Set(tokenize(text));\r\n\r\n // Increment document frequency for each unique word\r\n for (const word of words) {\r\n df.set(word, (df.get(word) || 0) + 1);\r\n }\r\n }\r\n\r\n return df;\r\n}\r\n\r\n/**\r\n * Normalize text using the same rules as server-side\r\n * \r\n * CRITICAL: This must match the normalizeText() function in server/index.js exactly\r\n * PLUS we remove all punctuation to match Orama's French tokenizer behavior\r\n */\r\nfunction normalizeText(text: string): string {\r\n return text\r\n .toLowerCase()\r\n .normalize('NFD')\r\n .replace(/[\\u0300-\\u036f]/g, '') // Remove diacritics\r\n // Replace French elisions (l', d', etc.) with space to preserve word boundaries\r\n .replace(/\\b[ldcjmnst][\\u2018\\u2019\\u201A\\u201B\\u2032\\u2035\\u0027\\u0060\\u00B4](?=\\w)/gi, ' ')\r\n .replace(/[\\u2018\\u2019\\u201A\\u201B\\u2032\\u2035\\u0027\\u0060\\u00B4]/g, '') // Remove remaining apostrophes\r\n .replace(/[\\u201c\\u201d]/g, '\"') // Normalize curly quotes to straight quotes\r\n .replace(/[.,;:!?()[\\]{}\\-—–«»\"\"]/g, ' ') // Remove punctuation (replace with space to preserve word boundaries)\r\n .replace(/\\s+/g, ' ') // Normalize multiple spaces to single space\r\n .trim();\r\n}\r\n\r\n/**\r\n * Tokenization matching normalized text behavior\r\n * \r\n * Note: Text should already be normalized before indexing, so we normalize again\r\n * to ensure plugin tokenization matches index tokenization\r\n */\r\nfunction tokenize(text: string): string[] {\r\n // Normalize first (same as indexing), then split by whitespace\r\n return normalizeText(text)\r\n .split(/\\s+/)\r\n .filter(token => token.length > 0);\r\n}\r\n\r\n/**\r\n * Export types for external use\r\n */\r\nexport type {\r\n FuzzyPhraseConfig,\r\n WordMatch,\r\n PhraseMatch,\r\n DocumentMatch,\r\n SynonymMap,\r\n Candidate,\r\n PluginState\r\n} from './types.js';\r\n\r\n/**\r\n * Export optimized search (QPS + fuzzy phrase pruning)\r\n */\r\nexport {\r\n searchWithQPSPruning,\r\n createOptimizedSearch,\r\n normalizeText as normalizeTextOptimized,\r\n tokenize as tokenizeOptimized,\r\n type OptimizedSearchConfig\r\n} from './optimized.js';\r\n\r\n/**\r\n * Re-export plugin state accessor for optimized search integration\r\n */\r\nexport function getPluginState(orama: any): PluginState | undefined {\r\n return pluginStates.get(orama);\r\n}\r\n"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wcs-colab/plugin-fuzzy-phrase",
|
|
3
|
-
"version": "3.1.16-custom.newbase.
|
|
3
|
+
"version": "3.1.16-custom.newbase.32",
|
|
4
4
|
"description": "Advanced fuzzy phrase matching plugin for Orama with semantic weighting and synonym expansion",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"orama",
|
|
@@ -46,7 +46,7 @@
|
|
|
46
46
|
"typescript": "^5.0.0"
|
|
47
47
|
},
|
|
48
48
|
"dependencies": {
|
|
49
|
-
"@wcs-colab/orama": "3.1.16-custom.base.
|
|
49
|
+
"@wcs-colab/orama": "3.1.16-custom.base.5"
|
|
50
50
|
},
|
|
51
51
|
"peerDependencies": {
|
|
52
52
|
"@supabase/supabase-js": "^2.39.0"
|
|
@@ -61,4 +61,4 @@
|
|
|
61
61
|
"lint": "exit 0",
|
|
62
62
|
"test": "node --test --import tsx test/*.test.ts"
|
|
63
63
|
}
|
|
64
|
-
}
|
|
64
|
+
}
|