Skip to content

Commit 177ef6c

Browse files
committed
feat(utils): ajout d'une fonction searchScore pour calcul de similarité entre 1 recherche et 1 texte, utilisable dans evaluateExpression (pour plugin readcsv notamment)
1 parent e92791d commit 177ef6c

5 files changed

Lines changed: 173 additions & 5 deletions

File tree

app/js/markdown/custom/variablesDynamic/evaluateExpression.mjs

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { config } from "../../../config.mjs";
22
import { tryConvertStringToNumber } from "../../../utils/strings.mjs";
3-
import { mainTopic } from "../../../utils/nlp.mjs";
3+
import { mainTopic, normalizeText, searchScore } from "../../../utils/nlp.mjs";
44

55
// Opérations autorisées pour le calcul des expressions complexes
66
const sanitizeCodeAllowedOperations = [
@@ -50,6 +50,9 @@ const sanitizeCodeAllowedOperations = [
5050
"tryConvertStringToNumber",
5151
"dynamicVariables",
5252
"mainTopic",
53+
"normalizeText",
54+
"searchScore",
55+
"boostWords",
5356
];
5457

5558
// Regex pour identifier les parties autorisées dans le code
@@ -154,6 +157,8 @@ export function evaluateExpression(expression, dynamicVariables) {
154157
"dynamicVariables",
155158
"tryConvertStringToNumber",
156159
"mainTopic",
160+
"normalizeText",
161+
"searchScore",
157162
"return " + finalExpression,
158163
);
159164

@@ -165,5 +170,11 @@ export function evaluateExpression(expression, dynamicVariables) {
165170
functionCache.set(finalExpression, fn);
166171
}
167172

168-
return fn(dynamicVariables, tryConvertStringToNumber, mainTopic);
173+
return fn(
174+
dynamicVariables,
175+
tryConvertStringToNumber,
176+
mainTopic,
177+
normalizeText,
178+
searchScore,
179+
);
169180
}

app/js/utils/nlp.mjs

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -469,6 +469,84 @@ const stopWords = new Set([
469469
"peux",
470470
]);
471471

472+
function cosineSimilarityTexts(strA, strB, options = {}) {
473+
// Calcul de similarité entre deux chaînes de caractères normalisées
474+
const strAnormalized = normalizeText(strA);
475+
const strBnormalized = normalizeText(strB);
476+
477+
// Crée les vecteurs pour les deux chaînes de caractères
478+
const vectorA = createVector(strAnormalized, options);
479+
const vectorB = createVector(strBnormalized, options);
480+
481+
let result = cosineSimilarity(vectorA, vectorB);
482+
483+
const boostWords = options.boostWords ? options.boostWords : [];
484+
// On booste le score si des mots spécifiques sont présents dans les deux chaînes de caractères
485+
// et on booste encore plus le score si plusieurs mots spécifiques sont présents dans les deux chaînes de caractères
486+
let countBoostedWords = 0;
487+
for (const word of boostWords) {
488+
const wordNormalized = normalizeText(word);
489+
if (
490+
strAnormalized.includes(wordNormalized) &&
491+
strBnormalized.includes(wordNormalized)
492+
) {
493+
countBoostedWords++;
494+
result = result + 1; // On booste de 1 par mot trouvé dans les deux chaînes
495+
if (countBoostedWords > 1) {
496+
result = result + 1 * (countBoostedWords - 1); // On booste encore plus si plusieurs mots sont trouvés
497+
}
498+
}
499+
}
500+
501+
// Calcule la similarité cosinus entre les deux vecteurs
502+
return result;
503+
}
504+
505+
function getImportantWords(str) {
506+
if (!str || typeof str !== "string") return [];
507+
// On supprime les stopwords et les caractères spéciaux
508+
const words = str
509+
.replace(/[.,!?';:]/g, " ")
510+
.split(/\s+/)
511+
.filter((word) => word && !stopWords.has(word.toLowerCase()));
512+
return words;
513+
}
514+
515+
export function searchScore(baseText, searchText, options = {}) {
516+
// Fonction de recherche fondée sur la similarité cosinus et qui prend en compte les mots importants à trouver
517+
518+
// Récupère les mots importants dans le texte de recherche
519+
const importantWords = getImportantWords(searchText);
520+
const importantWordCount = importantWords.length;
521+
// Ajoute les mots importants à l'option de boostWords pour le calcul de similarité
522+
if (!options.boostWords) {
523+
options.boostWords = [];
524+
}
525+
options.boostWords = options.boostWords.concat(importantWords);
526+
527+
// Calcule la similarité cosinus entre les deux textes
528+
let cosineSim = cosineSimilarityTexts(baseText, searchText, options);
529+
530+
// On réduit le score s'il y avait plusieurs mots spécifiques à trouver et qu'on ne les a pas tous trouvés
531+
if (importantWordCount > 1 && options.strictMode) {
532+
let foundImportantWordCount = 0;
533+
for (const word of importantWords) {
534+
const wordNormalized = normalizeText(word);
535+
if (normalizeText(baseText).includes(wordNormalized)) {
536+
foundImportantWordCount++;
537+
}
538+
}
539+
if (foundImportantWordCount < importantWordCount) {
540+
const missingWords = importantWordCount - foundImportantWordCount;
541+
const penaltyPerMissingWord = 2.75;
542+
const totalPenalty = missingWords * penaltyPerMissingWord;
543+
cosineSim = Math.max(0, cosineSim - totalPenalty);
544+
}
545+
}
546+
547+
return cosineSim;
548+
}
549+
472550
export function mainTopic(str, specificExpressionsToRemove = []) {
473551
// Extrait le sujet principal d'une chaîne de caractères qui représente une courte phrase
474552
if (!str || typeof str !== "string") return "";

app/script.min.js

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

app/script.min.js.map

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

tests/unit/utils/nlp.spec.mjs

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import {
1010
createVector,
1111
cosineSimilarityTextVector,
1212
mainTopic,
13+
searchScore,
1314
} from "../../../app/js/utils/nlp.mjs";
1415

1516
describe("longestCommonSubstringWeightedLength", function () {
@@ -623,3 +624,81 @@ describe("mainTopic", function () {
623624
expect(result).toBe("impacts changement climatique biodiversité marine");
624625
});
625626
});
627+
628+
describe("searchScore", function () {
629+
it("returns a high score when all important words are present", function () {
630+
const text = "Le chat est sur le tapis rouge.";
631+
const scoreTextWithOneImportantWord = searchScore(text, "chat");
632+
expect(scoreTextWithOneImportantWord).toBeGreaterThanOrEqual(1);
633+
const scoreTextWithTwoImportantWords = searchScore(text, "chat tapis");
634+
expect(scoreTextWithTwoImportantWords).toBeGreaterThanOrEqual(1);
635+
const scoreTextWithThreeImportantWords = searchScore(
636+
text,
637+
"chat tapis rouge",
638+
);
639+
expect(scoreTextWithThreeImportantWords).toBeGreaterThanOrEqual(1);
640+
});
641+
it("returns a higher score when all important words are present and the number of important words increases", function () {
642+
const text = "Le chat est sur le tapis rouge.";
643+
const scoreTextWithOneImportantWord = searchScore(text, "chat");
644+
const scoreTextWithTwoImportantWords = searchScore(text, "chat tapis");
645+
const scoreTextWithThreeImportantWords = searchScore(
646+
text,
647+
"chat tapis rouge",
648+
);
649+
expect(scoreTextWithTwoImportantWords).toBeGreaterThan(
650+
scoreTextWithOneImportantWord,
651+
);
652+
expect(scoreTextWithThreeImportantWords).toBeGreaterThan(
653+
scoreTextWithTwoImportantWords,
654+
);
655+
});
656+
657+
it("returns a low score for completely different texts", function () {
658+
const text1 = "Le chat est sur le tapis.";
659+
const text2 = "La voiture roule vite.";
660+
const score = searchScore(text1, text2);
661+
expect(score).toBeCloseTo(0, 1);
662+
});
663+
664+
it("returns a lower score in strict mode when important words are missing", function () {
665+
const baseText =
666+
"Le changement climatique affecte la biodiversité et les écosystèmes.";
667+
const searchWithImportantWords = [
668+
"changement climatique biodiversité ?",
669+
{ strictMode: true },
670+
];
671+
const searchWithoutImportantWords = [
672+
"changement climatique pollution",
673+
{ strictMode: true },
674+
];
675+
676+
const scoreWithImportantWords = searchScore(
677+
baseText,
678+
searchWithImportantWords[0],
679+
searchWithImportantWords[1],
680+
);
681+
const scoreWithoutImportantWords = searchScore(
682+
baseText,
683+
searchWithoutImportantWords[0],
684+
searchWithoutImportantWords[1],
685+
);
686+
687+
expect(scoreWithImportantWords).toBeGreaterThan(scoreWithoutImportantWords);
688+
expect(scoreWithoutImportantWords).toBeLessThan(1);
689+
expect(scoreWithoutImportantWords).toBeGreaterThan(2 / 3 - 0.1);
690+
expect(scoreWithoutImportantWords).toBeLessThan(2 / 3 + 0.1);
691+
});
692+
693+
it("penalizes missing important words in the search text", function () {
694+
const baseText =
695+
"Les énergies renouvelables sont essentielles pour lutter contre le changement climatique.";
696+
const searchTextMissingImportantWords = "Parle-moi des énergies fossiles.";
697+
698+
const score = searchScore(baseText, searchTextMissingImportantWords);
699+
700+
// Since "énergies" and "changement climatique" are important words in the base text,
701+
// their absence in the search text should lead to a lower score.
702+
expect(score).toBeLessThan(20); // Arbitrary threshold for low score
703+
});
704+
});

0 commit comments

Comments
 (0)