<?php

// +-------------------------------------------------+
// © 2002-2004 PMB Services / www.sigb.net pmb@sigb.net et contributeurs (voir www.sigb.net)
// +-------------------------------------------------+
// $Id: Algo.php,v 1.4.2.2 2026/04/30 08:05:02 qvarin Exp $

namespace Pmb\AI\Library\Reranking\Bm25;

if (stristr($_SERVER['REQUEST_URI'], basename(__FILE__))) {
    die("no access");
}

use InvalidArgumentException;
use Pmb\AI\Library\Entity\{Text, Document};

class Algo
{
    /**
     * Calculer la frequence inverse du document pour le token donne dans les textes donnes
     *
     * @param string $token Terme a analyser
     * @param Text[]|Document[] $texts Liste de textes avec des termes
     * @return float Frequence inverse du document pour le terme
     *
     * @see https://fr.wikipedia.org/wiki/TF-IDF#Calcul_de_IDF
     */
    public static function computeInverseDocumentFrequency(string $token, array $texts): float
    {
        $nbDocument = count($texts);
        $nbDocumentWithAnOccurrenceOfToken = 0;
        for ($i = 0; $i < $nbDocument; $i++) {
            if ($texts[$i]->containsToken($token)) {
                $nbDocumentWithAnOccurrenceOfToken++;
            }
        }

        // Ici, le + 0.5 est une methode de lissage bayesien pour eviter IDF = log(0) ou IDF negatifs
        // Ce qui par consequence evite la division par zero
        $num1 = $nbDocument - $nbDocumentWithAnOccurrenceOfToken + 0.5;
        $num2 = $nbDocumentWithAnOccurrenceOfToken + 0.5;

        return log(($num1 / $num2) + 1);
    }

    /**
     * Calcule le score BM25 pour les tokens et les textes fournis
     *
     * @param string[] $tokens Liste de termes a analyser
     * @param Text[]|Document[] $texts Liste de textes avec des termes
     * @param float $k1 Parametre k1 du BM25 (defaut: 1.2 - doit etre en [1.2, 2.0])
     * @param float $b Parametre b du BM25 (defaut: 0.75 - doit etre en [0, 1.0])
     * @return array Tableau indexe avec les scores BM25 pour chaque texte
     *
     * @throws InvalidArgumentException les parametres k1 ou b sont invalides
     * @throws InvalidArgumentException les parametres texts ou tokens sont vides
     *
     * @see https://fr.wikipedia.org/wiki/Okapi_BM25
     */
    public static function computeBM25(array $tokens, array $texts, float $k1 = 1.2, float $b = 0.75): array
    {
        if ($k1 < 1.2 || $k1 > 2.0) {
            throw new InvalidArgumentException('The parameter k1 must be between 1.2 and 2.0');
        }
        if ($b < 0 || $b > 1.0) {
            throw new InvalidArgumentException('The parameter b must be between 0 and 1.0');
        }
        if (empty($texts)) {
            throw new InvalidArgumentException('The parameter texts must not be empty');
        }
        if (empty($tokens)) {
            throw new InvalidArgumentException('The parameter tokens must not be empty');
        }

        // Calcul de la longueur moyenne des documents
        $avgCountToken = array_map(fn (Text|Document $text) => $text->countTokens(), $texts);
        $avgCountToken = array_sum($avgCountToken) / count($texts);

        // Calcul de la frequence inverse du document pour chaque terme
        $inverseDocumentFrequency = [];
        foreach ($tokens as $token) {
            // On peut avoir plusieurs fois le meme terme dans une question/phrase
            // Donc on le stock en format associatif terme => score IDF
            $inverseDocumentFrequency[$token] ??= self::computeInverseDocumentFrequency($token, $texts);
        }

        // Calcul du score BM25 pour chaque texte
        $result = [];
        foreach ($texts as $text) {
            $score = 0;
            foreach ($tokens as $token) {
                if (!$text->containsToken($token)) {
                    continue;
                }

                $termFrequency = $text->occurrencesOfToken($token);
                $num1 = $termFrequency * ($k1 + 1);
                $num2 = $termFrequency + ($k1 * (1 - $b + ($b * ($text->countTokens() / $avgCountToken))));
                $score += $inverseDocumentFrequency[$token] * ($num1 / $num2);
            }
            $result[] = [$text, $score];
        }

        // Tri par score decroissant
        usort($result, fn ($a, $b) => $b[1] <=> $a[1]);

        return $result;
    }
}
