Moteur de recherches.

Administrateur PHPfrance
Administrateur PHPfrance | 3131 Messages

28 août 2007, 10:27

Voilà le stemmer dont je parlais, il s'agit du stemmer "Paice/Husk". L'auteur original du script est en commentaires, je me suis contenté de faire un refactoring pour le passer en POO, et de compléter un peu la liste des règles FR (c'est là qu'est le gros du travail, qui n'est je pense jamais totalement terminé).

Note importante : j'ai écrit cette classe pour un site que j'ai repris et qui avait des problèmes assez bizarres d'encodage que je n'avais pas le temps de régler. Il y a donc quelques bidouilles de charset qui mériteraient d'être éclaircies pour une utilisation globale.

Pour l'utiliser c'est assez simple :
- Inclure "Stemmer_FR" ou "Stemmer_EN" (selon la langue à utiliser) ou s'assurer qu'ils seront autochargés
- Include "Stemmer" ou s'assurer qu'il sera autochargé
- Instancier l'objet avec $stemmer = Stemmer::factory('fr') ou Stemmer::factory('en')
- Appeler $stemmer->textStemIndex($monTexte) pour un texte
- On peut aussi appeler simplement $stemmer->stem($monMot) pour un simple mot


La class principale (note: dans la version que j'utilise réellement il y a également tout un set de fonctions destinées à ezPdo, afin de rendre automatique et persistante l'indexation de tout objet héritant de Stemmer, je vous en fais grâce) :
<?php

/**
 * Paice/Husk Stemmer
 * This code is in the public domain.
 * @author original author : Alexis Ulrich (http://alx2002.free.fr)
 * @author oop refactoring : Nicolas Chambrier ([email protected])
 */

if (!function_exists('mb_strrev')) {
	/**
	 * mb-string version of strrev
	 *
	 * @param string $string
	 * @param string $charset
	 * @return string
	 */
    function mb_strrev($string, $charset = null) {
        if ($charset === null) {
            $charset = mb_detect_encoding($string);
        }
        if ($charset == 'UTF-8') {
            $string = utf8_decode($string);
            $string = strrev($string);
            $string = utf8_encode($string);
        } else {
            $string = strrev($string);
        }
        return $string;
    }
}

if (!function_exists('mb_html_entity_decode')) {
	/**
	 * mb-string version of html_entity_decode
	 *
	 * @param string $string
	 * @param string $charset
	 * @return string
	 */
    function mb_html_entity_decode($string, $charset = null) {
        if ($charset === null) {
            $charset = mb_detect_encoding($string);
        }
        if ($charset == 'UTF-8') {
            $string = html_entity_decode($string);
            $string = utf8_encode($string);
        } else {
            $string = html_entity_decode($string);
        }
        return $string;
    }
}

/**
 * Main class
 *
 * @see Stemmer::factory()
 */
class Stemmer {

    protected $rulePattern = '/^([a-z]*)(\*){0,1}(\d)([a-z]*)([.|>])/';
    protected $rules = array('end0.');
    protected $stopWords = array();
    protected $vowels = 'aeiouy';
    protected $leastLettersIfFirstIsVowel = 0;
    protected $leastLettersIfFirstIsConsonant = 0;
    protected $language = null;
    
    protected static $instances = array();
    
    public $charset = 'UTF-8';

    /**
     * Returns the instance of the Stemmer for the given language 
     *
     * @param string $language
     * @return Stemmer
     */
    public static function factory($language = null) {
        if (!$language) {
            $language = MMVC_Lang::$code;
        }
        $key = strtolower($language);
        if (!isset(self::$instances[$key])) {
            $className = 'Stemmer_' . ucfirst($language);
            $parents = @class_parents($className);
            if ($parents && in_array('Stemmer',$parents)) {
                $instance = new $className;
                self::$instances[$key] = $instance;
            } else {
                return null;
            }
        }
        return @self::$instances[$key];
    }
    
    /**
     * Returns the language of the current Stemmer instance
     *
     * @return string
     */
    public function getLanguage() {
        return $this->language;
    }
    
    /**
     * returns the number of the first rule from the rule number $rule_number
     * that can be applied to the given reversed form
     * returns -1 if no rule can be applied, ie the stem has been found
     * 
     * @param string $reversedForm
     * @param int $ruleNumber
     * @return int
     */
    protected function getFirstRule($reversedForm, $ruleNumber = 0) {
        $nbRules = count($this->rules);
        for ($i=$ruleNumber; $i<$nbRules; ++$i) {
            $rule = $this->rules[$i];
            $rule = preg_replace($this->rulePattern, '\1', $rule);
            // var_dump($rule, $reversedForm, mb_substr($reversedForm,0,mb_strlen($rule))); echo '<br/>';
            if ($rule == mb_substr($reversedForm,0,mb_strlen($rule,$this->charset),$this->charset)) {
                return $i;
            }
        }
        return -1;
    }

    /**
     * Check the acceptability of a stem for a given language
     * 
     * @param string $reversed_stem the stem to check in reverse form
     */
    protected function checkAcceptability($reversed_stem) {
        if ( preg_match ("/[{$this->vowels}]$/", $reversed_stem) ) {
            // if the form starts with a vowel then at least X letters must remain after stemming (e.g.: "étaient" --> "ét")
            return mb_strlen($reversed_stem,$this->charset) > $this->leastLettersIfFirstIsVowel;
        } else {
            // if the form starts with a consonant then at X two letters must remain after stemming
            if (mb_strlen($reversed_stem,$this->charset) < $this->leastLettersIfFirstIsConsonant) {
                return False;
            }
            return preg_match("/[{$this->vowels}]/", $reversed_stem);
        }
    }

    /**
     * Returns the Stem indexing array for the given text. Each word's stem is stored as
     * a key of the array, and the value is the number of times this stem has been met.
     *
     * @param string $text
     * @return Array
     */
    public function textStemIndex($text) {
        preg_match_all('/\b[^\s\r\n\t\'\"]+\b/', mb_strtolower(mb_html_entity_decode(strip_tags($text), $this->charset), $this->charset), $matches);
        $words = $matches[0];
        $result = array();
        foreach ($words as $word) {
            if (strlen($word) > 1 && !in_array($word, $this->stopWords)) {
                $stem = $this->stem($word);
                if (!in_array($stem, $this->stopWords)) {
                    if (!isset($result[$stem])) {
                        $result[$stem] = 0;
                    }
                    $result[$stem] += 1;
                }
            }
        }
        return $result;
    }

    /**
     * the actual Paice/Husk stemmer
     * which returns a stem for the given form
     * 
     * @param string $form the word for which we want the stem
     */
    public function stem($form) {
        static $stems = array();
        if (!isset($stems[$form])) {
            $intact = True ;
            $stem_found = False ;
            $reversed_form = mb_strrev($form, $this->charset);
            $rule_number = 0 ;
            // that loop goes through the rules' array until it finds an ending one (ending by '.') or the last one ('end0.')
            while ( True ) {
                $rule_number = $this->getFirstRule($reversed_form, $rule_number);
                if ( $rule_number == - 1 ) {
                    // no other rule can be applied => the stem has been found
                    break;
                }
                $rule = $this->rules[$rule_number];
                preg_match($this->rulePattern, $rule, $matches);
                if (( $matches [ 2 ] != '*' ) || ( $intact )) {
                    $reversed_stem = $matches[ 4 ] . mb_substr($reversed_form , $matches[3], mb_strlen($reversed_form) - $matches[3], $this->charset);
                    if ( $this->checkAcceptability($reversed_stem) ) {
                        $reversed_form = $reversed_stem ;
                        if ( $matches[5] == '.' ) {
                            break;
                        }
                    }
                    else {
                        // go to another rule
                        $rule_number ++;
                    }
                }
                else {
                    // go to another rule
                    $rule_number ++;
                }
            }
            $stems[$form] = mb_strrev($reversed_form);
        }
        return $stems[$form];
    }

}
Le complément FR :
<?php

/**
 * @author nchambrier
 * @date 29 juin 07
 * @encoding UTF-8
 */

class Stemmer_Fr extends Stemmer {

    function __construct() {
        $this->language = 'fr';
        $this->charset = 'UTF-8';
        $this->rulePattern = '/^([a-zàâèéêëîïôûùç]*)(\*){0,1}(\d)([a-zàâèéêëîïôûùç]*)([.|>])/';
        $this->vowels = 'aàâeèéêëiîïoôuûùy';
        $this->leastLettersIfFirstIsVowel = 2;
        $this->leastLettersIfFirstIsConsonant = 2;
        $this->rules = array(
            'ug1>' ,            # { -gu > -g }
            'emsi4.',            # { -isme > - } 
            'etsi4.',            # { -iste > - }
            //'gol3.' ,           # { -log > - }
            'isme4.' ,           # { -isme > - }
            'iste4.' ,           # { -iste > - }
            'euq3>' ,           # { -que > - }
            'esre1>' ,             # { -erse > -ers }
            'esio1>' ,             # { -oise > -ois }
            'siol1.' ,             # { -lois > -loi }
            'siof0.' ,             # { -fois > -fois }
            'sioe0.' ,             # { -eois > -eois }
            'sio3>' ,              # { -ois > - }
            'st1>' ,               # { -ts > -t }
            'sf1>' ,               # { -fs > -f }
            'sle1>' ,              # { -els > -el }
            'slo1>' ,              # { -ols > -ol }
            'sé1>' ,               # { -és > -é }
            'étuae5.' ,            # { -eauté > - }
            'étuae2.' ,            # { -eauté > -eau }
            'tnia0.' ,             # { -aint > -aint }
            'tniv1.' ,             # { -vint > -vin }
            'tni3>' ,              # { -int > - }
            'suor1.' ,             # { -rous > -ou }
            'suo0.' ,              # { -ous > -ous }
            'sdrail5.' ,           # { -liards > -l }
            'sdrai4.' ,            # { -iards > -i }
            'erèi1>' ,             # { -ière > -ier }
            'sesue3x>' ,           # { -euses > -euse }
            'esuey5i.' ,           # { -yeuse > -i }
            'esue2x>' ,            # { -euse > -eux }
            'se1>' ,               # { -es > -e }
            'erèg3.' ,             # { -gère > -g }
            'eca1>' ,              # { -ace > -ac }
            'esiah0.' ,            # { -haise > - }
            'esi1>' ,              # { -ise > -is }
            'siss2.' ,             # { -ssis > -ss }
            'sir2>' ,              # { -ris > -r }
            'sit2>' ,              # { -tis > -t }
            'egané1.' ,            # { -énage > -énag }
            'egalli6>' ,           # { -illage > - }
            'egass1.' ,            # { -ssage > -sag }
            'egas0.' ,             # { -sage > - }
            'egat3.' ,             # { -tage > - }
            'ega3>' ,              # { -age > - }
            'ette4>' ,             # { -ette > - }
            'ett2>' ,              # { -tte > -t }
            'etio1.' ,             # { -oite > -oit }
            'tioç4c.' ,            # { -çoit > -c }
            'tio0.' ,              # { -oit > -oit }
            'et1>' ,               # { -te > -t }
            'eb1>' ,               # { -be > -b }
            'snia1>' ,             # { -ains > -ain }
            'eniatnau8>' ,         # { -uantaine > - }
            'eniatn4.' ,           # { -ntaine > -nt }
            'enia1>' ,             # { -aine > -ain }
            'niatnio3.' ,          # { -ointain > -oint }
            'niatg3.' ,            # { -gtain > -gt }
            'eé1>' ,               # { -ée > -é }
            'éhcat1.' ,            # { -taché > -tach }
            'éhca4.' ,             # { -aché > - }
            'étila5>' ,            # { -alité > - }
            'étici5.' ,            # { -icité > - }
            'étir1.' ,             # { -rité > -rit }
            'éti3>' ,              # { -ité > - }
            'égan1.' ,             # { -nagé > -nag }
            'éga3>' ,              # { -agé > - }
            'étehc1.' ,            # { -cheté > -chet }
            'éte3>' ,              # { -eté > - }
            'éit0.' ,              # { -tié > -tié }
            'é1>' ,                # { -é > - }
            'eire4.' ,             # { -erie > - }
            'eirue5.' ,            # { -eurie > - }
            'eio1.' ,              # { -oie > -oi }
            'eia1.' ,              # { -aie > -ai }
            'ei1>' ,               # { -ie > -i }
            'eng1.' ,              # { -gne > -gn }
            'xuaessi7.' ,          # { -isseaux > - }
            'xuae1>' ,             # { -eaux > -eau }
            'uaes0.' ,             # { -seau > -seau }
            'uae3.' ,              # { -eau > - }
            'xuave2l.' ,           # { -evaux > -eval }
            'xuav2li>' ,           # { -vaux > -vail }
            'xua3la>' ,            # { -aux > -al }
            'ela1>' ,              # { -ale > -al }
            'lart2.' ,             # { -tral > -tr }
            'lani2>' ,             # { -inal > -in }
            'laé2>' ,              # { -éal > -é }
            'siay4i.' ,            # { -yais > -i }    
            'siassia7.' ,          # { -aissais > - }    
            'siarv1*.' ,           # { -vrais > -vrai if intact }    
            'sia1>' ,              # { -ais > -ai }    
            'tneiayo6i.' ,         # { -oyaient > -oi }
            'tneiay6i.' ,          # { -yaient > -i }
            'tneiassia9.' ,        # { -aissaient > - }
            'tneiareio7.' ,        # { -oieraient > -oi }
            'tneia5>' ,            # { -aient > - }
            'tneia4>' ,            # { -aient > -a }
            'tiario4.' ,           # { -oirait > -oi }
            'tiarim3.' ,           # { -mirait > -mir }
            'tiaria3.' ,           # { -airait > -air }
            'tiaris3.' ,           # { -sirait > -sir }
            'tiari5.' ,            # { -irait > - }
            'tiarve6>' ,           # { -evrait > - }
            'tiare5>' ,            # { -erait > - }
            'iare4>' ,             # { -erai > - }
            'are3>' ,              # { -era > - }
            'tiay4i.' ,            # { -yait > -i }    
            'tia3>' ,              # { -ait > - }
            'tnay4i.' ,            # { -yant > -i }    
            'emèiu5>' ,            # { -uième > - }
            'emèi4>' ,             # { -ième > - }
            'tnaun3.' ,            # { -nuant > -nu }
            'tnauqo3.' ,           # { -oquant > -oqu }
            'tnau4>' ,             # { -uant > - }
            'tnaf0.' ,             # { -fant > -fant }
            'tnaté2>' ,            # { -étant > -ét }
            'tna3>' ,              # { -ant > - }
            'tno3>' ,              # { -ont > - }
            'zeiy4i.' ,            # { -yiez > -i }
            'zey3i.' ,             # { -yez > -i }
            'zeire5>' ,            # { -eriez > - }
            'zeird4.' ,            # { -driez > -d }
            'zeirio4.' ,           # { -oiriez > -oi }
            'ze2>' ,               # { -ez > - }
            'ssiab0.' ,            # { -baiss > - }
            'ssia4.' ,             # { -aiss > - }
            'ssi3.' ,              # { -iss > - }
            'tnemma6>' ,           # { -amment > - }
            'tnemesuey9i.' ,       # { -yeusement > -i }
            'tnemesue8>' ,         # { -eusement > - }
            'tnemevi7.' ,          # { -ivement > - }
            'tnemessia5.' ,        # { -aissement > -aiss }
            'tnemessi8.' ,         # { -issement > - }
            'tneme5>' ,            # { -ement > - }
            'tnemia4.' ,           # { -aiment > -ai }
            'tnemé5>' ,            # { -ément > - }
            'el2l>' ,              # { -le > -l }
            'lle3le>' ,            # { -ell > -el }
            'letô0.' ,             # { -ôtel > -ôtel }
            'lepp0.' ,             # { -ppel > -ppel }
            'le2>' ,               # { -el > - }
            'srei1>' ,             # { -iers > -ier }
            'reit3.' ,             # { -tier > -t }
            'reila2.' ,            # { -alier > -ali }
            'rei3>' ,              # { -ier > - }
            'ertâe5.' ,            # { -eâtre > - }
            'ertâé1.' ,            # { -éâtre > -éâtr }
            'ertâ4.' ,             # { -âtre > - }
            'drai4.' ,             # { -iard > - }
            'erdro0.' ,            # { -ordre > -ordre }
            'erute5.' ,            # { -eture > - }
            'ruta0.' ,             # { -atur > -atur }
            'eruta1.' ,            # { -ature > -atur }
            'erutiov1.' ,          # { -voiture > -voitur }
            'erub3.' ,             # { -bure > -b }
            'eruh3.' ,             # { -hure > -h }
            'erul3.' ,             # { -lure > -l }
            'er2r>' ,              # { -re > -r }
            'nn1>' ,               # { -nn > -n }
            'rèi3.' ,              # { -ièr > - }
            'srev0.' ,             # { -vers > -vers }
            'sr1>' ,               # { -rs > -r }
            'rid2>' ,              # { -dir > -d }
            're2>' ,               # { -er > - }
            'xuei4.' ,             # { -ieux > - }
            'esuei5.' ,            # { -ieuse > - }
            'lbati3.' ,            # { -itabl > -it }
            'lba3>' ,              # { -abl > - }
            'rueis0.' ,            # { -sieur > - }
            'ruehcn4.' ,           # { -ncheur > -nc }
            'ecirta6.' ,           # { -atrice > - }
            'ruetai6.' ,           # { -iateur > - }
            'rueta5.' ,            # { -ateur > - }
            'rueir0.' ,            # { -rieur > - }
            'rue3>' ,              # { -eur > - }
            'esseti6.' ,           # { -itesse > - }
            'essere6>' ,           # { -eresse > - }
            'esserd1.' ,           # { -dresse > -dress }
            'esse4>' ,             # { -esse > - }
            'essiab1.' ,           # { -baisse > -baiss }
            'essia5.' ,            # { -aisse > - }
            'essio1.' ,            # { -oisse > -oiss }
            'essi4.' ,             # { -isse > - }
            'essal4.' ,            # { -lasse > -l }
            'essa1>' ,             # { -asse > -ass }
            'ssab1.' ,             # { -bass > -bas }
            'essurp1.' ,           # { -prusse > -uss }
            'essu4.' ,             # { -usse > - }
            'essi1.' ,             # { -isse > -ss }
            'ssor1.' ,             # { -ross > -ros }
            'essor2.' ,             # { -rosse > -ros }
            'esso1>' ,             # { -osse > -oss }
            'ess2>' ,             # { -sse > -s }
            'tio3.' ,             # { -oit > - }
            'rès2re.' ,             # { -sèr > -ser }
            'rè0e.' ,             # { -èr > -ère }
            'esn1.' ,             # { -nse > -èns }
            'eu1>' ,                 # { -ue > -u }
            'sua0.' ,             # { -aus > -aus }
            'su1>' ,                 # { -us > -u }
            'utt1>' ,             # { -utt > -tt }
            'tuç3c.' ,             # { -çut > -c }
            'uç2c.' ,             # { -çu > -c }
            'ur1.' ,                 # { -ru > -r }
            'ehcn2>' ,             # { -nche > -nc }
            'ehcu1>' ,             # { -uche > -uch }
            'snorr3.' ,             # { -rrons > -rr }
            'snoru3.' ,             # { -urons > -ur }
            'snorua3.' ,             # { -aurons > -aur }
            'snorv3.' ,             # { -vrons > -vr }
            'snorio4.' ,             # { -oirons > -oi }
            'snori5.' ,             # { -irons > - }
            'snore5>' ,             # { -erons > - }
            'snortt4>' ,             # { -ttrons > -tt }
            'snortîa7.' ,             # { -aîtrons > - }
            'snort3.' ,             # { -trons > -tr }
            'snor4.' ,             # { -rons > - }
            'snossi6.' ,             # { -issons > - }
            'snoire6.' ,             # { -erions > - }
            'snoird5.' ,             # { -drions > -d }
            'snoitai7.' ,             # { -iations > - }
            'snoita6.' ,             # { -ations > - }
            'snoits1>' ,             # { -stions > -stion }
            'noits0.' ,             # { -stion > -stion }
            'snoi4>' ,             # { -ions > - }
            'noitaci7>' ,             # { -ication > - }
            'noitai6.' ,             # { -iation > - }
            'noita5.' ,             # { -ation > - }
            'noitu4.' ,             # { -ution > -u }
            'noi3>' ,             # { -ion > - }
            'snoya0.' ,             # { -ayons > -ayons }
            'snoy4i.' ,             # { -yons > -i }
            'snoça1.' ,             # { -açons > -açon }
            'snoçr1.' ,             # { -rçons > -rçon }
            'snoe4.' ,             # { -eons > - }
            'snosiar1>' ,             # { -raisons > - }
            'snola1.' ,             # { -alons > -alon }
            'sno3>' ,             # { -ons > - }
            'sno1>' ,             # { -ons > -on }
            'noll2.' ,             # { -llon > -ll }
            'tnennei4.' ,             # { -iennent > -ien }
            'ennei2>' ,             # { -ienne > -ien }
            'snei1>' ,             # { -iens > -ien }
            'sneé1>' ,             # { -éens > -éen }
            'enneé5e.' ,             # { -éenne > -e }
            'neé3e.' ,             # { -éen > -e }
            'neic0.' ,             # { -cien > -cien }
            'neiv0.' ,             # { -vien > -vien }
            'nei3.' ,             # { -ien > - }
            'sc1.' ,                 # { -cs > -c }
            'sd1.' ,                 # { -ds > -d }
            'sg1.' ,                 # { -gs > -g }
            'sni1.' ,             # { -ins > -in }
            'tiu0.' ,             # { -uit > - }
            'ti2.' ,                 # { -it > - }
            'sp1>' ,                 # { -ps > -p }
            'sna1>' ,             # { -ans > -an }
            'sue1.' ,             # { -eus > -eu }
            'enn2>' ,             # { -nne > -n }
            'nong2.' ,             # { -gnon > -gn }
            'noss2.' ,             # { -sson > -ss }
            'rioe4.' ,             # { -eoir > - }
            'riot0.' ,             # { -toir > -toir }
            'riorc1.' ,             # { -croir > -croi }
            'riovec5.' ,             # { -cevoir > -c }
            'rio3.' ,             # { -oir > - }
            'ric2.' ,             # { -cir > -l }
            'ril2.' ,             # { -lir > -l }
            'tnerim3.' ,             # { -mirent > -mir }
            'tneris3>' ,             # { -sirent > -sir }
            'tneri5.' ,             # { -irent > - }
            'tîa3.' ,             # { -aît > - }
            'riss2.' ,             # { -ssir > -ss }
            'tî2.' ,                 # { -ît > - }
            'tâ2>' ,                 # { -ât > - }
            'ario2.' ,             # { -oira > -oi }
            'arim1.' ,             # { -mira > -m }
            'ara1.' ,             # { -ara > -ar }
            'aris1.' ,             # { -sira > -sir }
            'ari3.' ,             # { -ira > - }
            'art1>' ,             # { -tra > -tr }
            'ardn2.' ,             # { -ndra > -nd }
            'arr1.' ,             # { -rra > -rr }
            'arua1.' ,             # { -aura > -aur }
            'aro1.' ,             # { -ora > -or }
            'arv1.' ,             # { -vra > -vr }
            'aru1.' ,             # { -ura > -ur }
            'ar2.' ,                 # { -ra > - }
            'rd1.' ,                 # { -dr > -d }
            'ud1.' ,                 # { -du > - }
            'ul1.' ,                 # { -lu > -l }
            'ini1.' ,             # { -ini > -in }
            'rin2.' ,             # { -nir > - }
            'tnessiab3.' ,             # { -baissent > -baiss }
            'tnessia7.' ,             # { -aissent > - }
            'tnessi6.' ,             # { -issent > - }
            'tnessni4.' ,             # { -inssent > -ins }
            'sini2.' ,             # { -inis > -in }
            'sl1.' ,                 # { -ls > -l }
            'iard3.' ,             # { -drai > -d }
            'iario3.' ,             # { -oirai > -oi }
            'ia2>' ,                 # { -ai > - }
            'io0.' ,                 # { -oi > -oi }
            'iule2.' ,             # { -elui > -el }
            'i1>' ,                 # { -i > - }
            'sid2.' ,             # { -dis > -d }
            'sic2.' ,             # { -cis > -c }
            'esoi4.' ,             # { -iose > - }
            'ed1.' ,                 # { -de > -d }
            'ai2>' ,                 # { -ia > - }
            'a1>' ,                 # { -a > - }
            'adr1.' ,             # { -rda > -rd }
            'tnerè5>' ,             # { -èrent > - }
            'evir1.' ,             # { -rive > -riv }
            'evio4>' ,             # { -oive > - }
            'evi3.' ,             # { -ive > - }
            'fita4.' ,             # { -atif > - }
            'fi2>' ,                 # { -if > - }
            'enie1.' ,             # { -eine > -ein }
            'sare4>' ,             # { -eras > - }
            'sari4>' ,             # { -iras > - }
            'sard3.' ,             # { -dras > -d }
            'sart2>' ,             # { -tras > -tr }
            'sa2.' ,                 # { -as > - }
            'tnessa6>' ,             # { -assent > - }
            'tnessu6>' ,             # { -ussent > - }
            'tnegna3.' ,             # { -angent > -ang }
            'tnegi3.' ,             # { -igent > -ig }
            'tneg0.' ,             # { -gent > -gent }
            'tneru5>' ,             # { -urent > - }
            'tnemg0.' ,             # { -gment > -gment }
            'tnerni4.' ,             # { -inrent > -in }
            'tneiv1.' ,             # { -vient > -vien }
            'tne3>' ,             # { -ent > - }
            'une1.' ,             # { -enu > -en }
            'en1>' ,                 # { -ne > -n }
            'nitn2.' ,             # { -ntin > - }
            'ecnay5i.' ,          # { -yance > -i }
            'ecnal1.' ,           # { -lance > -lanc }
            'ecna4.' ,            # { -ance > - }
            'ec1>' ,              # { -ce > -c }
            'nn1.' ,              # { -nn > -n }
            'rit2>' ,             # { -tir > - }
            'rut2>' ,             # { -tur > -t }
            'rud2.' ,             # { -dur > -d }
            'ugn1>' ,             # { -ngu > -ng }
            'eg1>' ,              # { -ge > -g }
            'tuo0.' ,             # { -out > -out }
            'tul2>' ,             # { -lut > -l }
            'tû2>' ,              # { -ût > - }
            'ev1>' ,              # { -ve > -v }
            'vè2ve>' ,            # { -èv > -ev }
            'rtt1>' ,             # { -ttr > -tt }
            'emissi6.' ,          # { -issime > - }
            'em1.' ,              # { -me > -m }
            'ehc1.' ,             # { -che > -ch }
            'céi2cè.' ,           # { -iéc > -ièc }
            'libi2l.' ,           # { -ibil > -ibl }
            'llie1.' ,            # { -eill > -eil }
            'liei4i.' ,           # { -ieil > -i }
            'xuev1.' ,            # { -veux > -veu }
            'xuey4i.' ,           # { -yeux > -i }
            'xueni5>' ,           # { -ineux > - }
            'xuell4.' ,           # { -lleux > -l }
            'xuere5.' ,           # { -ereux > - }
            'xue3>' ,             # { -eux > - }
            'rbé3rbè.' ,          # { -ébr > -èbr }
            'tur2.' ,             # { -rut > -r }
            'riré4re.' ,          # { -érir > -er }
            'rir2.' ,             # { -rir > -r }
            'câ2ca.' ,            # { -âc > -ac }
            'snu1.' ,             # { -uns > -un }
            'rtîa4.' ,            # { -aîtr > - }
            'long2.' ,            # { -gnol > -gn }
            'vec2.' ,             # { -cev > -c }
            'ç1c>' ,              # { -ç > -c }
            'ssilp3.' ,           # { -pliss > -pl }
            'silp2.' ,            # { -plis > -pl }
            'tèhc2te.' ,          # { -chèt > -chet }
            'nèm2ne.' ,           # { -mèn > -men }
            'llepp1.' ,           # { -ppell > -ppel }
            'tan2.' ,             # { -nat > -n }
            'rvè3rve.' ,          # { -èvr > -evr }
            'rvé3rve.' ,          # { -évr > -evr }
            'rè2re.' ,            # { -èr > -er }
            'ré2re.' ,            # { -ér > -er }
            'tè2te.' ,            # { -èt > -et }
            'té2te.' ,            # { -ét > -et }
            'epp1.' ,             # { -ppe > -pp }
            'eya2i.' ,            # { -aye > -ai }
            'ya1i.' ,             # { -ay > -ai }
            'yo1i.' ,             # { -oy > -oi }
            'esu1.' ,             # { -use > -us }
            'ugi1.' ,             # { -igu > -g }
            'tt1.' ,              # { -tt > -t }
            'e1.',
            
            # end rule: the stem has already been found
            'end0.'
        );
        $this->stopWords = array(
            'alors',
            'au',
            'aucuns',
            'aussi',
            'autre',
            'avant',
            'avec',
            'avoir',
            'bon',
            'car',
            'ce',
            'cela',
            'ces',
            'ceux',
            'chaque',
            'ci',
            'comme',
            'comment',
            'dans',
            'de',
            'des',
            'du',
            'dedans',
            'dehors',
            'depuis',
            'deux',
            'devrait',
            'doit',
            'donc',
            'dos',
            'droite',
            'début',
            'elle',
            'elles',
            'en',
            'encore',
            'essai',
            'est',
            'et',
            'eu',
            'fait',
            'faites',
            'fois',
            'font',
            'force',
            'haut',
            'hors',
            'ici',
            'il',
            'ils',
            'je',
            'juste',
            'la',
            'le',
            'les',
            'leur',
            'là',
            'ma',
            'maintenant',
            'mais',
            'mes',
            'mine',
            'moins',
            'mon',
            'mot',
            'même',
            'ni',
            'nommés',
            'notre',
            'nous',
            'nouveaux',
            'ou',
            'où',
            'par',
            'parce',
            'parole',
            'pas',
            'personnes',
            'peut',
            'peu',
            'pièce',
            'plupart',
            'pour',
            'pourquoi',
            'quand',
            'que',
            'quel',
            'quelle',
            'quelles',
            'quels',
            'qui',
            'sa',
            'sans',
            'se',
            'ses',
            'seulement',
            'si',
            'sien',
            'son',
            'sont',
            'sous',
            'soyez',
            'sujet',
            'sur',
            'ta',
            'tandis',
            'tellement',
            'tels',
            'tes',
            'ton',
            'tous',
            'tout',
            'trop',
            'très',
            'tu',
            'valeur',
            'voie',
            'voient',
            'vont',
            'votre',
            'vous',
            'vu',
            'ça',
            'étaient',
            'état',
            'étions',
            'été',
            'être',
        );
    }

}
Le complément EN :
<?php

/**
 * @author nchambrier
 * @date 29 juin 07
 * @encoding UTF-8
 */

class Stemmer_En extends Stemmer {

    function __construct() {
        $this->language = 'en';
        $this->charset = 'UTF-8';
        $this->rulePattern = '/^([a-z]*)(\*){0,1}(\d)([a-z]*)([.|>])/';
        $this->vowels = 'aeiouy';
        $this->leastLettersIfFirstIsVowel = 2;
        $this->leastLettersIfFirstIsConsonant = 3;
        $this->rules = array(
            'ai*2.',        # { -ia > -   if intact }
            'a*1.',            # { -a > -    if intact }
            'bb1.',            # { -bb > -b   }
            'city3s.',        # { -ytic > -ys }
            'ci2>',            # { -ic > -    }
            'cn1t>',        # { -nc > -nt  }
            'dd1.',            # { -dd > -d   }
            'dei3y>',        # { -ied > -y  }
            'deec2ss.',        # { -ceed > -cess }
            'dee1.',        # { -eed > -ee }
            'de2>',            # { -ed > -    }
            'dooh4>',        # { -hood > -  }
            'e1>',            # { -e > -     }
            'feil1v.',        # { -lief > -liev }
            'fi2>',            # { -if > -    }
            'gni3>',        # { -ing > -   }
            'gai3y.',        # { -iag > -y  }
            'ga2>',            # { -ag > -    }
            'gg1.',            # { -gg > -g   }
            'ht*2.',        # { -th > -   if intact }
            'hsiug5ct.',        # { -guish > -ct }
            'hsi3>',        # { -ish > -   }
            'i*1.',            # { -i > -    if intact }
            'i1y>',            # { -i > -y    }
            '@i1d.',        # { -i@ > -id   --  see nois4@> & vis3@> }
            'juf1s.',        # { -fuj > -fus }
            'ju1d.',        # { -uj > -ud  }
            'jo1d.',        # { -oj > -od  }
            'jeh1r.',        # { -hej > -her }
            'jrev1t.',        # { -verj > -vert }
            'jsim2t.',        # { -misj > -mit }
            'jn1d.',        # { -nj > -nd  }
            'j1s.',            # { -j > -s    }
            'lbaifi6.',        # { -ifiabl > - }
            'lbai4y.',        # { -iabl > -y }
            'lba3>',        # { -abl > -   }
            'lbi3.',        # { -ibl > -   }
            'lib2l>',        # { -bil > -bl }
            'lc1.',            # { -cl > c    }
            'lufi4y.',        # { -iful > -y }
            'luf3>',        # { -ful > -   }
            'lu2.',            # { -ul > -    }
            'lai3>',        # { -ial > -   }
            'lau3>',        # { -ual > -   }
            'la2>',            # { -al > -    }
            'll1.',            # { -ll > -l   }
            'mui3.',        # { -ium > -   }
            'mu*2.',        # { -um > -   if intact }
            'msi3>',        # { -ism > -   }
            'mm1.',            # { -mm > -m   }
            'nois4@>',        # { -sion > -@ }
            'noix4ct.',        # { -xion > -ct }
            'noi3>',        # { -ion > -   }
            'nai3>',        # { -ian > -   }
            'na2>',            # { -an > -    }
            'nee0.',        # { protect  -een }
            'ne2>',            # { -en > -    }
            'nn1.',            # { -nn > -n   }
            'pihs4>',        # { -ship > -  }
            'pp1.',            # { -pp > -p   }
            're2>',            # { -er > -    }
            'rae0.',        # { protect  -ear }
            'ra2.',            # { -ar > -    }
            'ro2>',            # { -or > -    }
            'ru2>',            # { -ur > -    }
            'rr1.',            # { -rr > -r   }
            'rt1>',            # { -tr > -t   }
            'rei3y>',        # { -ier > -y  }
            'sei3y>',        # { -ies > -y  }
            'sis2.',        # { -sis > -s  }
            'si2>',            # { -is > -    }
            'ssen4>',        # { -ness > -  }
            'ss0.',            # { protect  -ss }
            'suo3>',        # { -ous > -   }
            'su*2.',        # { -us > -   if intact }
            's*1>',            # { -s > -    if intact }
            's0.',            # { -s > -s    }
            'tacilp4y.',        # { -plicat > -ply }
            'ta2>',            # { -at > -    }
            'tnem4>',        # { -ment > -  }
            'tne3>',        # { -ent > -   }
            'tna3>',        # { -ant > -   }
            'tpir2b.',        # { -ript > -rib }
            'tpro2b.',        # { -orpt > -orb }
            'tcud1.',        # { -duct > -duc }
            'tpmus2.',        # { -sumpt > -sum }
            'tpec2iv.',        # { -cept > -ceiv }
            'tulo2v.',        # { -olut > -olv }
            'tsis0.',        # { protect  -sist }
            'tsi3>',        # { -ist > -   }
            'tt1.',            # { -tt > -t   }
            'uqi3.',        # { -iqu > -   }
            'ugo1.',        # { -ogu > -og }
            'vis3@>',        # { -siv > -@  }
            'vie0.',        # { protect  -eiv }
            'vi2>',            # { -iv > -    }
            'ylb1>',        # { -bly > -bl }
            'yli3y>',        # { -ily > -y  }
            'ylp0.',        # { protect  -ply }
            'yl2>',            # { -ly > -    }
            'ygo1.',        # { -ogy > -og }
            'yhp1.',        # { -phy > -ph }
            'ymo1.',        # { -omy > -om }
            'ypo1.',        # { -opy > -op }
            'yti3>',        # { -ity > -   }
            'yte3>',        # { -ety > -   }
            'ytl2.',        # { -lty > -l  }
            'yrtsi5.',        # { -istry > - }
            'yra3>',        # { -ary > -   }
            'yro3>',        # { -ory > -   }
            'yfi3.',        # { -ify > -   }
            'ycn2t>',        # { -ncy > -nt }
            'yca3>',        # { -acy > -   }
            'zi2>',            # { -iz > -    }
            'zy1s.',        # { -yz > -ys  }
            'end0.'            # end rule: the stem has already been found
        );
        $this->stopWords = array(
            'a',
            'about',
            'an',
            'are',
            'as',
            'at',
            'be',
            'by',
            'com',
            'de',
            'en',
            'for',
            'from',
            'how',
            'i',
            'in',
            'is',
            'it',
            'la',
            'my',
            'of',
            'on',
            'or',
            'that',
            'the',
            'this',
            'to',
            'very',
            'was',
            'what',
            'when',
            'where',
            'who',
            'will',
            'with',
            'und',
            'the',
            'www',
        );
    }

}
Je vous laisse faire mumuse avec ça ;) en sachant que c'est forcément toujours incomplet, je pense qu'il manquera toujours un stop-word ou une règle. Le but dans ces système c'est de choisir à partir de quel moment on s'arrête.

ViPHP
ViPHP | 4674 Messages

28 août 2007, 17:28

C'est pas mal du tout le stemmer. Je connaissais pas. La limite du système, c'est d'écrire toutes les règles.

Maintenant, laissez moi vous présenter les deux scripts que j'ai fais ici.

Le premier est une introduction au modèle vecteur-binaire, qui sert à créer une machine de recherches pour des mots de longueurs inférieures à des mots machines. On s'en sert pour faire des recherches avec joker (je n'ai pas encore fait le script). Par exemple, si on dit que le caractère joker est §, alors abc§ef, on recherche toutes les occurences avec abc[une lettre]ef. C'est pratique comme technique. Donc, le premier script montre comment fonctionne le modèle vecteur-binaire, qui sert à faire une recherche par motifs approchés à l'aide d'un joker.

Le second algo sert à faire une recherche par motifs approchés avec k différences près. Je l'ai pas encore documenté car j'ai eu du mal à l'adapter en PHP, et c'est surtout le résultat d'une looongue réflexion sur la monotie des diagonales dans les recherches par différences. Enfin bref, je vous passe les détails, c'est long et compliqué. On va juste considérer le résultat, à savoir que ça marche ;-).

Modèle Vecteur-Binaire :
<?php

header('content-type: text/plain');

/** Modèle Vecteur-Binaire **/


/**
 * On écrit un petit automate où :
 *   - X est le motif que l'on recherche, dans l'occurence y.
 *   - X est un ensemble de mots non vide, y est une suite de lettre de
 *     l'alphabet A.
 * 
 * init est le vecteur qui enregistre la position des mots de X qui commencent.
 * final est le vecteur qui enregistre la position des mots de X qui se
 * terminent.
 * Par exemple, si X = {aab}    , alors init = 110  , et final = 001.
 *              si X = {aab, cb}, alors init = 10010, et final = 00101 ;
 * 
 * mask est une table de vecteurs pour chaque lettre utilisée par y dans
 * l'alphabet A.
 * Par exemple, si y = abbcaabaacb, alors A = {a, b, c}.
 * mask va alors marquer la position de chaque lettre des mots de X.
 * Par exemple, X = {aab}, alors :
 *     mask[a] = 110
 *     mask[b ] = 001
 *     mask[c] = 000
 *
 */
function littleMachine ( $X, $y ) {

    $m     = strlen($X);
    $init  = 0 << $m;
    $final = 0 << $m;
    $y     = str_split($y);

    // check alphabet.
    foreach($y as $i => $a)
        $mask[$a] = 0 << $m;
    ksort($mask);

    $p = -1;
    $X = explode(' ', $X);

    // check each word of X.
    foreach($X as $e => $x) {

        $n     = strlen($x)-1;
        $init |= 1 << ($n-($p+1));
        $x     = str_split($x);

        // check each letter of x.
        foreach($x as $i => $a) {
            $p++;
            $mask[$a] |= 1 << ($n-$p);
        }

        $final |= 1 << ($n-$p);
    }

    return array(
        0 => $init,
        1 => $final,
        2 => $mask
    );
}

/**
 * On localise les mots courts.
 * 
 * y \in A, A = {a, b, c}, y = abbcaabaacb,
 * X \subseteq A*, le motif, tel que X = {aab},
 * m = |X| (la cardinalité de X).
 * 
 * Soit init    : 100
 *      final   : 001
 *      mask[a] : 110
 *          [b ] : 001
 *          [c] : 000
 *      r       : (init | (r >> 1)) & mask[a]
 *      offset  : r & final != 0
 * 
 * On a alors :
 * 
 *     +-----+---------+----------+----------+
 *     |  j  |  y[j]   |  bits r  |  offset  |
 *     +-----+---------+----------+----------+
 *     |  0  |   a     |  1 0 0   |  0 0 0   |
 *     |  1  |   b     |  0 0 0   |  0 0 0   |
 *     |  2  |   b     |  0 0 0   |  0 0 0   |
 *     |  3  |   c     |  0 0 0   |  0 0 0   |
 *     |  4  | / a \   |  1 0 0   |  0 0 0   | i = j-m-1 = 4
 *     |  5  | | a |=X |  1 1 0   |  0 0 0   |
 *     |  6  | \ b /   |  0 0 1   |> 0 0 1 < | j = 6, donc on déduit i.
 *     |  7  |   a     |  1 0 0   |  0 0 0   |
 *     |  8  |   a     |  1 1 0   |  0 0 0   |
 *     |  9  |   c     |  0 0 0   |  0 0 0   |
 *     | 10  |   b     |  0 0 0   |  0 0 0   |
 *     +-----+---------+----------+----------+
 * 
 * Il faut que les mots de X soient inférieurs à la longueur des mots machines.
 */
function matchShortWords ( $X, $y ) {

    list($init, $final, $mask) = littleMachine($X, $y);
    $m                         = strlen($X);
    $r                         = 0 << $m;
    $y                         = str_split($y);
    $j                         = 0;
    $offset                    = array();

    foreach($y as $i => $a) {

        $r = ($init | ($r >> 1)) & $mask[$a];

        if($r & $final != 0) {
            $i = $j-($m-1);
            $offset[] = array('i' => $i, 'j' => $j);
        }

        $j++;
    }

    return $offset;
}

$X      = 'aab';
$y      = 'abbcaabaacb';
$search = matchShortWords($X, $y);

$n      = count($search);
echo 'Recherche de "' . $X . '" dans "' . $y . '" : ' . "\n" .
     $n . ' occurence(s) trouvée(s).';

Affichera :

Code : Tout sélectionner

Recherche de "aab" dans "abbcaabaacb" : 1 occurence(s) trouvée(s).
Recherche par motifs approchés avec k différences :
<?php

header('content-type: text/plain');

/*  Recherche par motifs approchés, avec différences  */
/* sur le principe de la monotonie sur les diagonales */


function PatternMatchingWithDiff ( $x, $y, $k ) {

    $m      = strlen($x);
    $n      = strlen($y);
    $offset = array();

    for($d = -1; $d <= ($n-$m+$k+1); $d++)
        $L[-1][$d] = -2;

    for($q = 0; $q <= ($k-1); $q++) {

        $L[$q][-$q-1] = $q-1;
        $L[$q][-$q-2] = $q-1;
    }

    for($q = 0; $q <= $k; $q++) {

        for($d = -$q; $d <= ($n-$m+$k-$q); $d++) {

            $l         = max($L[$q-1][$d-1],
                             $L[$q-1][$d]  +1,
                             $L[$q-1][$d+1]+1);
            $l         = min($l, $m-1);
            $a         = substr($x, $l+1   , -($l)   +($m-1));
            $b         = substr($y, $d+$l+1, -($d+$l)+($n-1));
            $L[$q][$d] = $l+lcp($a, $b);

            if(   $L[$q][$d]    == $m-1
               || $d+$L[$q][$d] == $n-1) {
                $j            = $m+$d;
                $i            = $j-$m;
                $offset[$q][] = array('i' => $i, 'j' => $j);
            }
        }
    }

    return $offset;
}

function lcp ( $x, $y ) {

    $max = min(strlen($x), strlen($y));
    $i   = 0;

    while($i < $max && $x{$i} == $y{$i})
        $i++;

    return $i;
}

$x      = 'GATAA';
$y      = 'CAGATAAGAGAA';
$k      = 1;
$search = PatternMatchingWithDiff($x, $y, $k);

$n      = count($search);
echo 'Recherche de "' . $x . '" dans "' . $y . '" avec au plus ' . $k . ' différence(s) : ' . "\n";
foreach($search as $q => $n) {
    echo '  - pour ' . $q . ' différence(s), ' . count($n) . ' occurence(s) trouvée(s) :' . "\n";

    foreach($n as $i => $pos)
        echo '    - ' . substr($y, $pos['i'], -$pos['i']+$pos['j']) . ' ;' . "\n";
}
Affichera :

Code : Tout sélectionner

Recherche de "GATAA" dans "CAGATAAGAGAA" avec au plus 1 différence(s) : - pour 0 différence(s), 1 occurence(s) trouvée(s) : - GATAA ; - pour 1 différence(s), 4 occurence(s) trouvée(s) : - AGATA ; - GATAA ; - ATAAG ; - GAGAA ;
Vous pouvez tester, ça marche (normalement :P).


J'attends vos réactions sur les algos ci-dessus. Normalement, je peux encore optimiser une chouillat le modèle vecteur-binaire, mais c'est pas grand chose (comme virer le ksort() par exemple).

Je vais regarder de plus prêt le stemmer, ça à l'air d'être un bon compromis quand même. Merci Naholyr :).

Je pensais aussi à autre chose. Quand quelqu'un fait une recherche, et que la recherche est correcte, alors on enregistre sa requête dans une autre table. De cette façon, on obtient une liste des recherches qui ont le mieux porté leurs fruits. Ce serait encore une autre idée, nan ?
« Un handicap est le résultat d'une rencontre entre une déficience ou différence et une incapacité de la société à répondre à celle-ci. »

Hoa : http://hoa-project.net (sur @hoaproject).