|Home|      <<Prev<<      <-Back--      >>Next>>

Data mining : streams et similarités


1. Combiner des "streams" de données à partir de plusieurs collections

1.1. Intro

En général dans une collection de documents, on indexe les documents selon leur contenu -- donc on note pour chaque document lesquels sont les mots qui y apparaissent (et combien de fois). Ensuite, lors du traitement d'une requête (qui peut contenir plusieurs mots) on cherche les documents les contenant (pour les sélectionner plus finement, en calculant certaines quantités, etc.). Par exemple :
Les questions qu'on se pose sont comment trouver les documents ayant au moins un mot de la requête dedans, et comment faire ceci en un temps raisonnable.

1.2 Algo naif

Un algorithme simple et naif ferait deux boucles, imbriquées : De combien de pas a besoin cet algorithme? Autrement dit, combien de fois on fait la vérification "si d contient r" ? Ce sera le nombre de mots de la requête multiplié par le nombre total de documents. Donc avec l'exemple ci-dessus, 8*3=24. Par contre, si au lieu de 8 nous avons 80000000 documents, une telle exécution risque de prendre beaucoup de temps, même avec les ordinateurs puissants d'aujourd'hui.

1.2 Algo plus efficace

1.2.1 Idée

Une idée pour améliorer cet algo serait de pouvoir éliminer les documents qui n'ont aucun mot en commun avec la requête, donc dans notre exemple, D7 et D8. Autrement dit, lorsqu'on commence à examiner les documents "candidats", qu'on ne "touche pas du tout" à D7 et D8, c'est-à-dire qu'au moment où on reçoit la requête à traiter, qu'on "sache" que "ce n'est pas la peine" d'examiner D7 et D8, et qu'on sache ceci juste en regardant la requête.

1.2.2 Indices

Vouloir aboutir à ceci avant même d'examiner les documents un par un semble impossible. Mais souvenons-nous qu'il y a un moment où chaque document "nous passe par la main" forcément : le moment d'insertion dans la collection. Donc ce qu'on peut faire c'est (à ce moment-là, ou bien au moment de ce qu'on appelle la clôture de la session d'insertion, donc quand on n'écrit plus (pour un bout de temps) dedans), préparer des listes d'indices.

1.2.3 Outil

Donc la collection est construite "intelligemment", et nous offre la possibilité d'énumérer un par un tous les documents contenant un mot donné et seulement ceux-ci, et dans l'ordre. Appelons TD(r).nextDoc() la manière d'obtenir, un par un (lors de chaque appel, et rapidement), tous les documents contenant le mot r dans l'ordre (croissant de leur docId par exemple), et seulement ces documents.

1.2.4 Cas particulier pour un seul mot dans la requête

Considérons, de plus, pour l'instant que la requête n'a qu'un seul mot r. Réécrivons notre algorithme avec cet outil. Maintenant on voit qu'on ne touche plus qu'au documents nécessaires.

1.2.5 Idée du cas général (plusieurs mots dans la requête)

Comment "élargir" le balayage pour "toucher" tous les documents qui contiennent au moins un mot de la requête ? Souvenons-nous que les appels successifs de TD(r).nextDoc() donnent les ids des documents dans l'ordre croissant. Considérons une requête avec les mots A, B, C comme plus haut, et regardons l'ensemble des résultats du premier appel pour chacun (car ils sont indépendants)
 E[0] = {TD(A).nextDoc(), TD(B).nextDoc(), TD(C).nextDoc()}
Cet ensemble peut contenir un ou plusieurs documents, au hasard de leur contenu et de leur ordonnancement. Peu importe. Considérons le document avec le plus petit id dans E[0] -- notons-le Dmin[0].
Si on décide de traiter tout d'abord Dmin[0], alors on peut "le mettre de côté" et ne plus y revenir. Autrement dit, non seulement Dmin[0] est le minimum de E[0], mais il est également le minimum de tous les documents ayant au moins un mot de la requête et qui ne sont pas encore traités. Donc une fois Dmin[0] traité, on sait que tout autre document candidat légitime a un id plus grand. Souvenons-nous qu'à chaque appel, TD().nextDoc() nous donne des id de plus en plus grands (pas forcément consécutifs -- mais ceci n'a pas d'importance). Donc, une fois Dmin[0] traité, on appelle le (ou les) TD().nextDoc() seulement pour ceux qui étaient égaux à Dmin[0]. Comme cela on est sûr qu'on ne rate aucun document candidat légitime, et on ne passe pas plus de temps que nécessaire.

1.2.6 Algorithme du cas général (plusieurs mots dans la requête)

On donne ici l'algo général, qui utilise plusieurs "fonctions", qu'on détaille également La fonction qEnd(E) utilise une constante spéciale "fin-de-stream" qui est rendue par un appel TD().nextDoc() quand il a fini tous les documents qu'il aurait pu rendre. Si tous les streams sont épuisés, alors qEnd(E) rend vrai, autrement non.

1.3 Lucene

Lucene contient du code qui crée les collections indexées de documents, ainsi que du code qui nous permet de rechercher dedans, comme on l'a déjà vu. Ce code a une certaine structure bien définie -- les différentes étapes du travail sont réparties entre plusieurs classes spécifiques.
L'algorithme de balayage qui a été discuté plus haut est exécuté principalement par des classes dérivées de la classe Scorer. Une fois une instance construite, le "point d'entrée" en est la méthode d'instance Scorer.score(Collector c), et c'est donc cette méthode qui exécute les étapes principales de notre algorithme.
Le Collector est une classe "seau", qui recueille (et trie, si on prend la bonne classe dérivée de Collector) les documents.
Le Scorer est créé (et mis au travail) par l'IndexSearcher, qui est le "maître" -- créé depuis le point d'entrée principal du programme. Le "maître" se sert également d'IndexReader, qui donne l'accès effectif à la collection de documents, et qui nous met à disposition entre autres les accesseurs de stream utilisés par TD().nextDoc().

1.3.1 Fonctionnalités à réaliser

Le travail est à faire dans le fichier similarity/SimScorer.java que vous devez compléter avec du code -- les méthodes SimScorer.TermData.nextDoc() et SimScorer.nextDocs().
La classe TermData correspond au TD() de la notation algorithmique -- elle gère un stream pour un mot de la requête donné.
    public class TermData {
	public        Term          queryTerm   = null;
	public        TermDocs      termDocs    = null;
	public        int           currentDoc  = -1;
	
	private final int[]         docBuff     = new int[32];
	private final int[]         freqBuff    = new int[32];
	private       int           pointer     = 0;
	private       int           pointerMax  = 0;
	public TermData() {}
        private int nextDoc() throws IOException { 
         // a faire
        }

La classe SimScorer en construit un tableau
   private TermData[] termData;
qu'elle initialise dans le constructeur. Celui-ci est appelé par la méthode search de l'IndexSearcher.
À partir du programme principal (on peut le voir dans SimpleLuceneSearcher.java), les étapes principales pour faire une recherche dans la collection de documents indexée sont :

1.3.2 Classes et outils disponibles et nécessaires pour notre travail

La classe Lucene TermDocs est celle qui permet d'énumérer les documents, comme on le fait en notation algorithmique TD(). On en obtient une instance avec la méthode d'instance IndexReader.termDocs(Term t). La classe Term sert à gérer les termes.
On utilise la classe TermDocs en appelant sa méthode d'instance TermDocs . read(int[] docBuff, int[] freqBuff) qui remplit les tampons. Ensuite, on lit depuis ces tampons, et lorsqu'ils sont "consommés", on rappelle TermDocs . read(int[] docBuff, int[] freqBuff) un coup, pour les re-remplir. Cette méthode rend combien elle a réussi à lire.
La constante NO_MORE_DOCS (prédéfinie) servira à signaler la fin d'un stream.
C'est dans la méthode TermData . nextDoc() que tout ceci est appelé. Elle gère, au moyen des données-membres pointer et pointerMax, le remplissage et la consommation du tampon de docIDs, ainsi que l'avancement de currentDoc de la manière suivante :
	private int nextDoc() throws IOException {
	    // on incremente le pointeur
	    // si le pointeur a atteint ou dépassé le pointeur maximal
	    //////// re-remplir les tampons avec termDocs.read()
	    //////// et réassigner le pointeur maximal à ce qu'on vient de lire
            //////// et remettre pointeur à zéro 
	    //////// si on n'a en fait rien lu, c'est la fin, donc
	    //////////////  on met currentDoc à NO_MORE_DOCS, et on le rend
            //////// finsi
            // finsi
	    // on met currentDoc à ce qui est pointé par pointer dans le tampon docBuff
	    // on rend currentDoc
	}
Dans la classe SimScore, c'est la méthode nextDocs() qui appelle soigneusement TermData.nextDoc(), ainsi:
    public void nextDocs(TermData[] t) throws IOException  {
	// trouver le docId minimal parmi les currentDoc des elements de t
	// pour tous les entiers de 0 a termCount (excluant termCount lui-meme)
	/////// si le currentDoc de l'element courant de t est minimal
	///////////////// lui appeler nextDoc() dessus
    }
Travail à faire
  1. Télécharger l'archive archTp4.tar.gz, et ouvrez-là dans un répertoire dédié à ce TP, et lancez make une fois
  2. Écrivez le code complet de la méthode TermData.nextDoc() dans similarity/SimScorer.java
  3. Écrivez le code complet de la méthode Scorer.nextDocs() dans similarity/SimScorer.java
  4. Testez avec une commande comme
  5. java -jar similarity.jar search testOk3/luceneIndex 'armstrong marathon york' 5 _
    
  6. Expliquez en détail la sortie écran, en lisant dans le code, en regardant dans data avec grep, etc.

2. Généralités sur les mesures de similarité

2.1 Utilité de la similarité

Calculer la similarité entre deux documents, ou bien entre un document et une requête, est une manière de voir combien proches les documents (respectivement le document et la requête) le sont. Autrement dit, plus la distance entre les deux est petite, plus la similarité est grande -- donc plus ils sont similaires.
Si nous sommes en train de passer en revue un nombre de documents pour en sélectionner ceux qui répondent à une question posé, alors on peut prendre comme méthode générale

2.2 Définitions préliminaires

Considérons une collection de documents (indexée). Soit V l'ensemble ordonné de tous les mots apparaissant dans nos documents, ainsi que dans les requêtes possibles. (Ces mots sont "purifiés", c'est-à-dire qu'on élimine les articles, prépositions, etc., on ne conjugue pas les verbes, on ne met pas les pluriels, etc, et dans la littérature on les appelle également termes (et en anglais, "terms"). Nous représentons alors chaque document (et requête) par un vecteur de dimension |V|, contenant, pour chaque coordonnée, le nombre de fois où le terme correspondant apparaît dans le document. (Habituellement, donc, les requêtes auront plutôt seulement des 1 et des 0).

Exemple

Soit les documents suivants et la requête R
        minister statemement economy
Leurs termes sont
comment,decide,economy,evening,index,macroeconomical,make,minister,morning,prime,public,statement,TV,week.
et les vecteurs de ces documents et de la requête seront
 T  = (0,1,0,1,0,0,1,1,0,1,1,1,1,0)
 U  = (2,0,1,1,1,1,0,2,1,1,0,0,0,1)
 R  = (0,0,1,0,0,0,0,1,0,0,0,1,0,0)

2.3 Définitions des similarités

Nous donnons les définitions suivantes pour deux vecteurs T et U, chacun ayant la dimension n

2.3.1 Similarité du cosinus

Pour calculer la similarité du cosinus on doit calculer le produit scalaire des deux vecteurs et le diviser par le produit des normes des deux vecteurs.
Concrètement :

2.3.2 Similarité basée sur la distance euclidienne

La distance euclidienne entre deux vecteurs T et U de dimension n est
 DE(T,U) = sqrt((T1 - U1)2 + (T2 - U2)2 + ... + (Tn - Un)2)  
Pour en faire une similarité, on peut considérer par exemple
 1/DE(T,U)
en "faisant attention" au cas ou
 DE(T,U) = 0
Par "faire attention" on entend s'assurer que ce cas est proprement pris en compte par exemple dans un programme qui calcule ces valeurs. On ne peut pas travailler avec des nombres infinis, donc on peut utiliser par exemple le plus grand float représentable, ou codifier ce cas autrement, selon le but dans lequel on le traite (trier les résultats, etc.).

2.3.3 Coefficient de Jaccard

Le coefficient de Jaccard est défini comme étant le quotient du cardinal de l'intersection par celui de l'union, et on peut (dans un premier temps) négliger les fréquences (on les prendra en compte dans le coeffcient de Jaccard généralisé).
Reprenant l'exemple donné ci-avant (avec les deux documents et la requête), le cardinal de l'intersection de la requête avec le premier document est 2, parce que le premier document contient bien les termes 'minister' et 'statement' (et non pas 'economy'). Le cardinal de l'union de l'ensemble de termes de la requêete avec celui du premier document est 9 (8 du document plus 'economy' de la requêete). Donc le coefficient est 2/9.

2.3.4 Coefficient de Jaccard généralisé

Pour ce calcul, on prend en compte les fréquences également, et alors on a comme définition
  simJaccG(T,U) = (T . U) / (|T|2 + |U|2  -  (T . U))

2.4 Exercices

  1. Calculez les quatre similarités pour chacune des paires
  2.   (T,U)  (T,R)  (U,R)  (U,T)  (R,T)  (R,U)
    
  3. Que pouvez vous observer, par exemple au sujet de (T,U) et (U,T) ? Ceci semble-t-il être vrai pour d'autres paires du même type, comme par exemple (R,T) et (T,R) ?
  4. Pouvez-vous justifier formellement ce que vous venez d'observer au point précédent ?

3. Modification de la similiarité par défaut de Lucene

Nous allons nous servir de la base de code Lucene et des bouts de code existants, à savoir la partie recherche dans un index.

3.1 But de ce travail

Le but de ce travail est de vous faire implémenter les quatre formules de calcul de similarité, tout en intégrant ce code avec la base existante, afin de permettre le traitement de nos requêtes à l'aide justement d'une de ces quatre mesures de similarités -- choisie sur la ligne de commande. On travaille maintenant dans la méthode SimScorer.score() -- celle qui ne prend aucun argument, et la donnée-membre SimScorer.scoreDoc nous dit quel est le docId du document pour lequel on doit calculer le score. (Souvenez-vous de toute la partie algorithmique que nous avons traitée au début de ce TP -- le but en ayant été d'arriver à énumérer efficacement tous les documents "intéressants" -- pour lesquelles on doit calculer le score. Souvenez-vous également de ce qui est dit plus haut dans ce TP, à savoir comment se déroulent les choses, qui appelle qui, etc. -- le tout est dans la section 1.3.1, et dans le code).

3.2 Détails de la base de code existante

Le but du jeu étant de calculer de quantités à partir des termes de la requête et du document courant et de leur fréquences, nous devons savoir comment obtenir ces renseignements (à l'aide de l'IndexReader (donné au constructeur de SimScorer), d'autres classes Lucene auxiliaires, ainsi que lesquelles des données-membre de SimScorer vont nous aider.

3.2.1 Termes d'un document

À partir de n'importe quel id d'un document, à l'aide de l'instance d'IndexReader, on peut obtenir tous les termes présents dans le document, ainsi que leur fréquences, avec un appel à la méthode d'instance IndexReader . getTermFreqVectors():
     TermFreqVector[] docTermFreqVect = docReader . getTermFreqVectors(scoreDoc);
car scoreDoc est la donnée-membre de SimScorer qui contient le docId du document courant, qui est donc celui en train d'être "noté".
Donc, nous avons ainsi accès aux termes d'un document, pour tous les champs (Field) initialement indexés de la sorte dans la collection. Souvenons-nous que dans notre cas, nous avions choisi d'indexer seulement le champ content. Donc, pour notre requête on a besoin d'un seul TermFreqVector -- celui correspondant à ce champ. On doit par contre trouver son indice dans le tableau de TermFreqVector rendu par la méthode getTermFreqVectors(). Ainsi, on cherche un par un celui dont le nom de champ (obtenable avec la méthode getField()) est 'content':
	TermFreqVector[] docTermFreqVect = docReader . getTermFreqVectors(scoreDoc);
	final int howManyTermFreqVects = docTermFreqVect . length;
	int contentFieldNdx = -1;
	for(int kField = 0; kField < howManyTermFreqVects; kField++) {
	    if(docTermFreqVect[kField] . getField() . equal("content")) {
		contentFieldNdx = kField;
	    }
	}
	if(contentFieldNdx < 0) {
	    throw new IOException("Cannot find the 'content' field among the ones with term frequencies");
 	}
Donc nous par la suite utiliser l'élément docTermFreqVect[contentFieldNdx], qui est un TermFreqVector, lequel est une autre classe Lucene, qui nous donne le String[] des termes, ainsi que le int[] de leur fréquences:
	final int      docTermCount    = docTermFreqVect[contentFieldNdx] . size();
	final String[] docTerms        = docTermFreqVect[contentFieldNdx] . getTerms();   
        final int[]    docFreqs        = docTermFreqVect[contentFieldNdx] . getTermFrequencies();
(On utilise le premier élément car c'est celui-ci, ici, dans notre cadre, Enfin un dernier mot d'algorithme et structures de données: en regardant les définitions des mesures de similarités, on s'aperçoit qu'il faut pouvoir pour tout terme trouver sa fréquence, et que docTerms ensemble avec docFreqs nous offrent cet accès plutôt en les énumérant un par un et non pas directement. Donc nous devons nous prendre une table de hachage docTermMap dans laquelle, pour chaque terme du document, on met l'indice dans ces tableaux. Ainsi, on remplit d'abord docTermMap utilisant une boucle. Ensuite, chaque fois qu'on a besoin, pour un terme String toto quelconque de connaître rapidement sa fréquence dans le document courant scoreDoc, on pourra utiliser
  docFreqs[(Integer)docTermMap . get(toto)]
De même, juste pour savoir si le document contient bien le terme String toto, on peut faire
   if(docTermMap . containsKey(toto)) { // oui, le document le contient bien

3.2.2 Termes de la requête

Les termes de la requête sont déjà disponibles car ils ont été extraits dans le constructeur de SimScorer (dont le code source vous est entièrement fourni). On y a accès depuis le tableau SimScorer . termData, ainsi
    termData[kTerm] . queryTerm . text()
(pour obtenir l'élément de rang kTerm). On peut bien entendu également les mettre dans une table de hachage, similairement à ce qui est expliqué plus haut pour les termes du document, ainsi :
	    Map<String,Integer> queryTermMap = new HashMap<String,Integer>();
	    for(int kTerm = 0; kTerm < termCount; kTerm++) {
		queryTermMap . put(termData[kTerm] . queryTerm . text(),kTerm);
	    }
et ensuite, si on a besoin de savoir donc si un certain terme String thisTerm se trouve parmi les termes de la requête, on fait
  if(queryTermMap . containsKey(thisTerm)) { // oui, la requête le contient bien
Pour ce qui est de leur fréquences, on les a toutes déjà dans la donnée-membre
  final   float[]    queryVect;
de SimScorer (c'est fait dans le constructeur -- elles sont simplement mises à 1 ici, pour simplifier). Lors des calculs des mesures de similarité on va donc utiliser queryVect pour obtenir les coordonnées non-nulles du vecteur de la requête.
Donc revenant à queryTermMap, pour obtenir la valeur du bon élément de queryVect pour un terme donné String thisTerm, on fait
    final int queryNdx = (Integer)queryTermMap . get(thisTerm);
Enfin, la norme du vecteur de termes de la requête est déjà calculée (toujours dans le constructeur), et est donc disponible dans la donnée-membre queryNorm.

3.2.3 Corps de la méthode SimScorer.score()

Vous avez ici la vue d'ensemble du corps entier de la méthode SimScorer.score().
    public float score() throws IOException  {

	System.out.print("#" + scoreDoc + " ");
	assert scoreDoc != -1;
	float scoreValue = 0.0f;
        /// 1. Préparatifs initiaux  

	if(similarityType . equals("EuclideanSimilarity")) {
             // 2. Calcul similarité basée sur la distance euclidienne
	}
	else if(similarityType . equals("CosineSimilarity")) {
             // 3. Calcul similarité du cosinus
	}
	else if(similarityType . equals("JaccardSimilarity")) {
             // 4. Calcul coefficient de Jaccard
	}
	else if(similarityType . equals("GenJaccardSimilarity")) {
             // 5. Calcul coefficient de Jaccard généralisé
	}
	else {
	    throw new IOException("Unknown similarity type " + similarityType); 
	}
	System.out.println(similarityType + " score=" + scoreValue);
	return scoreValue;
    }

3.3 Détails algorithmiques pour les préparatifs initiaux et pour le calcul de chaque mesure de similarité

Maintenant nous allons regarder comment nous prendre pour calculer chaque mesure de similarité selon leurs formules données plus haut, et utilisant les outils exposés ci-avant.

3.3.1 Préparatifs initiaux

Avant de passer aux étapes précises, signalons que le gros du code Java à écrire pour les préparatifs initiaux vous a été déjà fourni en "pièces détachées" dans la section 3.2, donc il faudra principalement aller les chercher là-dedans et les mettre ensemble.
 on récupère le TermFreqVector[] de scoreDoc depuis le reader
 on récupère la longueur du tableau de TermFreqVectors qu'on vient de récupèrer
 on se prend le contentFieldNdx à -1
 boucle pour chaque valeur entière de 0 au nombre égal à la longueur du tableau de TermFreqVectors
    si getField() . equals("content") appliquée à l'élé,ent courant du tableau de TermFreqVectors
        alors on a trouvée l'indice recherché, donc on le met dans contentFieldNdx
 finboucle
 si contentFieldNdx n'a pas de bonne valeur, on lève une exception
 on se prend une Map<String,Integer> docTermMap qu'on instancie avec new HashMap<String,Integer>();
 on se prend dans docTermCount le nombre d'éléments du TermFreqVector choisi (c'est-à-dire docTermFreqVect[contentFieldNdx])
 on se prend le String[] docTerms depuis, avec getTerms();
 boucle pour chaque valeur entière de 0 à docTermCount // pour remplir la docTermMap
    on met dans docTermMap l'élément courant de docTerms ainsi que son indice
 finboucle 

3.3.2 Calcul similarité basée sur la distance euclidienne

  on récupère dans int[] docFreqs les fréquences du document courant

	    float sumOfWeightDiffSq          = 0;
	    int   qAllQueryTermsInDoc        = 1;      
      
  on déclare, instancie et remplit la table de hachage queryTermMap comme expliqué au 3.2.2
  boucle pour chaque valeur entière de 0 à docTermCount 
     soit thisTermW la valeur de l'élément courant de  docFreqs
     si la queryTermMap contient le terme courant du document (on les a tous dans docTerms -- voir 3.3.1) alors
         on récupère son indice queryNdx depuis la queryTermMap (voir 3.2.2)
         on rajoute a sumOfWeightDiffSq la quantité (thisTermW - queryVect[queryNdx]) * (thisTermW - queryVect[queryNdx])
         si thisTermW n'est pas égale à queryVect[queryNdx], alors 
             qAllQueryTermsInDoc est mise à z´ro
         finsi
     sinon
          on rajoute a sumOfWeightDiffSq la quantité thisTermW * thisTermW
          qAllQueryTermsInDoc est mise à z´ro
     finsi
  finboucle
  si docTermCount et termCount sont égaux et que qAllQueryTermsInDoc vaut 1 alors
      scoreValue = Float . MAX_VALUE;
  sinon
      scoreValue prend la valeur de 1 divisée par sumOfWeightDiffSq
  finsi

3.3.3 Calcul similarité du cosinus

  on récupère dans int[] docFreqs les fréquences du document courant
  on calcule docNorm (la norme du vecteur de frequences du document: boucle pour la somme des carrés, et sqrt() après)
  on se prend float       dotProd = 0; pour calculer le produit scalaire ("the dot product" en anglais)
  boucle pour chaque entier de 0 à termCount (c'est-à-dire pour les terme de la requête)
     on se prend final String queryTerm le terme courant (voir 3.2.2)
     si le document contient également queryTerm parmi ses termes (utilisez docTermMap, voir la fin de 3.2.1)
        récupérez l'indice docTermCoord de queryTerm dans docFreqs (à partir de la docTermMap, voir la fin de 3.2.1)
        dotProd prend la valeur dotProd plus le produit queryVect[kTerm] * docFreqs[docTermCoord]
     finsi
  finboucle // (on a donc ainsi accumule dans dotProd les produits des bonnes coordonnees pour le produit scalaire)
  scoreValue prend la valeur selon la formule (qutient de dotProd par le produit des deux normes, qui sont queryNorm et docNorm)

3.3.4 Calcul coefficient de Jaccard

  on initialise intersectSize (qui est float) et queryTermNotInDocCount a zero
  boucle pour chaque entier de 0 à termCount 
    si la docTermMap contient le terme courant de la requete (voir 3.2.2 pour l'obtenir)
       intersectSize++
    sinon
       queryTermNotInDocCount++
    finsi
  finboucle
  on se prend alors float unionSize comme étant docTermFreqVect[contentFieldNdx] . size() + queryTermNotInDocCount
  la scoreValue est le quotient de intersectSize et de unionSize

3.3.5 Calcul coefficient de Jaccard généralisé

Vous avez tous les éléments nécessaires pour son calcul depuis la similarité du cosinus : le produit scalaire ainsi que les normes. Faites attention à utiliser les normes au carré.
Travail à faire
  1. Implémentez les préparatifs initiaux et le calcul des quatres mesures de similarité dans la méthode SimScorer.score() (celle qui ne prend aucun argument), selon les indications, dans le fichier similarity/SimScorer.java (même fichier que là où vous avez travaillé au point précédent).
  2. Compilez et testez
  3. java -jar similarity.jar search testOk3/luceneIndex 'armstrong marathon york' 5 EuclideanSimilarity
    
    java -jar similarity.jar search testOk3/luceneIndex 'armstrong marathon york' 5 CosineSimilarity
    
    java -jar similarity.jar search testOk3/luceneIndex 'armstrong marathon york' 5 JaccardSimilarity
    
    java -jar similarity.jar search testOk3/luceneIndex 'armstrong marathon york' 5 GenJaccardSimilarity