Nuevo Critério: encontrar artículos similares

En este artículo se explica cómo encontrar artículos, u otros elementos, con un texto similar valiéndose de la indexación del motor de búsquedas y la declaración de un nuevo criterio de bucles.

Idea

SPIP almacena en las tablas index_* la información sobre la idexación de los artículos. Por el momento, estas tablas sólo se utilizan para calcular los resultados de una búsqueda.

La estructura es simple:

-  hay una tabla diccionario central, que lista todas las palabras importantes encontradas en el sitio,
-  para cada tipo de objeto hay una tabla que vincula el ID de las palabras que se encuentran en este objeto. Este vínculo se pondera con un número de puntos (que corresponde a la relevancia de la palabra en el texto). (ver «¿Cómo funciona el motor de búsqueda de SPIP?» en spip.net)

En lugar de hacer una búsqueda partiendo de una palabra como se hace habitualmente con el criterio {recherche}, se quiere encontrar los artículos similares a otro (contando el número de palabras que comparten).

Existe una forma simple para hacer esto: Se puede analizar los “documentos” (artículos, breves, etc) como vectores de N dimensiones, cada una correspondiendo a una palabra en el diccionario. Los valores almacenados en el vector de un documento representan el peso de cada dimensión (cada palabra) en este documento.

Por ejemplo:

dico doc1 doc2
libertad 0 3
wiki 10 3
spip 3 10
sueño 4 0

Una medida de la semejanza entre los dos documentos es el producto vectorial entre los dos vectores:

S = 0*3+10*3+3*10+4*0 = 60

Eso es lo que hace esta consulta sobre las tablas de indexación de SPIP:

SELECT 
            primaire.id_article,
            secondaire.id_article, 
            SUM(primaire.points*secondaire.points) AS similarite
FROM 
         spip_index_articles AS primaire, 
         spip_index_articles AS secondaire 
WHERE 
           primaire.hash = secondaire.hash AND 
           primaire.id_article = 2
GROUP BY secondaire.id_article

Critério

Vamos a implementar un nuevo criterio para los bucles [1]:
{similaire} :

function critere_similaire($idb, &$boucles, $param, $not) {
  $boucle = &$boucles[$idb];
  
  if ($param != 'similaire' OR $not)
	erreur_squelette(_T('info_erreur_squelette'), $param);

  $boucle->select[] = 'SUM(primaire.points*secondaire.points) AS similarite';

  $boucle->from[] = 'spip_index_'.$boucle->id_table.' AS primaire';
  $boucle->from[] = 'spip_index_'.$boucle->id_table.' AS secondaire';

$boucle->where[] = 'primaire.'.$boucle->primary.'=\'".'.calculer_argument_precedent($idb,
                                                    $boucle->primary,
                                                    $boucles).'."\''; 

 $boucle->where[] = 'primaire.hash = secondaire.hash';
  $boucle->where[] = $boucle->id_table.'.'.$boucle->primary.' = secondaire.'.$boucle->primary;

  $boucle->group = 'secondaire.'.$boucle->primary;  
}

-  Se añade un nuevo campo en el select de la consulta. Se trata del cálculo del producto escalar entre los dos vectores. Hay que señalar que el orden será del menos similar al más similar, por lo que generalmente será necesario utilizar este criterio con {inverse}.

-  Se añaden tres nuevas tablas sobre que hacer el empalme:

  • 2 tablas index_TYPE, para comparar el objeto actual con todos los otros indexados. Se puede observar que se utiliza la variable id_table para conocer el tipo de objeto del bucle

-  se añaden cuatro condiciones a WHERE:

  • la tabla primaria recupera el objeto en cuestión. Obtiene su ID gracias a la función calculer_argument_precedent,
  • se seleccionan todos los objeto que tengan las mismas palabras dentro (se comprueba con el hash de la palabra),
  • la última condición está en relación a la lista de artículo similar con la tabla del objeto principal (aquí se utiliza $boucle->id_table.'.'.$boucle->primary como índice),

-  se agrupa la columna secundaria sólo para no tener los objetos similares duplicados (es en ese momento que se calcula el producto escalar),

Baliza

El nuevo compilador de spip permite añadir sus propias balizas. Aquí se quiere poder recuperar la nueva columna calculada por nuestra petición: la “similiridad”.

He aquí el código, [2]:

function balise_SIMILARITE($p) {
	return rindex_pile($p, 'similarite', 'similaire');
}

Se puede utilizar esta baliza en cualquier bucle que tenga el criterio {similaire} en sus criterios o bien en los de un bucle englobante [3].

Se puede a continuación recuperar el valor de esta columna en la pila proporcionada por el compilador. Todo eso es hecho automáticamente por la función rindex_pile.

Bucle de ejemplo

Un bucle muy simple para utilizar este criterio sobre los artículos:

<html>
  <body>
  <BOUCLE_article(ARTICLES) {id_article}>
  blabla blabla<br/>
  #TITRE:
  <B_sim>
    Los articulos similares a este son:
    <ul>
    <BOUCLE_sim(ARTICLES) {similaire} {par similarite} {inverse} {0,5}>
    <li><a href="#URL_ARTICLE">#TITRE</a></li>
    </BOUCLE_sim>
    </ul>
  </B_sim>
  </BOUCLE_article>
  </body>
</html>

Se puede también indicarle a SPIP que clasifique por un campo calculado dinámicamente en una consulta [4]. Así, se clasifica por semejanza y uego se invierte, para tener los artículos clasificados en un orden decreciente de semejanza (el más parecido, aparecerá en primer lugar).

Este criterio va a devolver todos los artículos que tengan al menos unas palabras en común con el artículo en curso, lo que puede dar muchos resultados. Se limita entonces el bucle a 5.

Optimización

La consulta SQL hecha por este criterio hace tres empalmes [5] sobre tablas muy grandes . Por ejemplo para un bucle de artículos:

-  la tabla articles contiene todos los artículos,
-  la tabla index_articles contiene una fila por cada palabra indexada, por artículo. Eso quiere decir que si el artículo X tiene 10 palabras indexadas, entonces ocupará 10 filas en esta tabla. Imagínese el tamaño de esta tabla si hay muchos artículos largos.

Por ello es necesario evitar absolutamente hacer todos estos empalmes. Desgraciadamente, el empalme de index_loquesea sobre si mismo es obligatorio. Por el contrario, el empalme la tabla loquesea no es obligatoria.

Efectivamente, es obligatorio si se quiere aplicar este criterio directamente sobre la tabla artículos. Es más intuitivo y permite recuperar directamente la información sobre el artículo.

Aquí, se va a exponer un método que evita este empalme sobre la tabla artículo. Este nuevo criterio se aplicará directamente sobre una tabla index_loquesea y permitirá recuperar cada ID de los artículos similares.

Es necesario decir en primer lugar a SPIP que se puede ’buclear’ las tablas índice describiéndole las columnas disponibles:

include_ecrire("inc_auxbase.php");
global $tables_principales;

$tables_principales['spip_index_articles'] =
       array('field' => &$spip_index_articles, 'key' => &$spip_index_articles_key);

$tables_principales['spip_index_breves'] =
       array('field' => &$spip_index_breves, 'key' => &$spip_index_breves_key);

$tables_principales['spip_index_forums'] =
       array('field' => &$spip_index_forums, 'key' => &$spip_index_forums_key);

$tables_principales['spip_index_auteurs'] =
       array('field' => &$spip_index_auteurs, 'key' => &$spip_index_auteurs_key);

$tables_principales['spip_index_mots'] =
       array('field' => &$spip_index_mots, 'key' => &$spip_index_articles_key);

$tables_principales['spip_index_signatures'] =
       array('field' => &$spip_index_signatures, 'key' => &$spip_index_signatures_key);

$tables_principales['spip_index_syndic'] =
       array('field' => &$spip_index_syndic, 'key' => &$spip_index_syndic_key);

es necesario también declarar las funciones boucle_... para inicializar las consultas sobre estas tablas

function boucle_INDEX_ARTICLES($id_boucle, &$boucles) {
	$boucle = &$boucles[$id_boucle];
	$id_table = $boucle->id_table;
	$boucle->from[] =  "spip_index_articles AS $id_table";
	return calculer_boucle($id_boucle, $boucles); 
}

function boucle_INDEX_BREVES($id_boucle, &$boucles) {
	$boucle = &$boucles[$id_boucle];
	$id_table = $boucle->id_table;
	$boucle->from[] =  "spip_index_BREVES AS $id_table";
	return calculer_boucle($id_boucle, $boucles); 
}

function boucle_INDEX_FORUMS($id_boucle, &$boucles) {
	$boucle = &$boucles[$id_boucle];
	$id_table = $boucle->id_table;
	$boucle->from[] =  "spip_index_forums AS $id_table";
	return calculer_boucle($id_boucle, $boucles); 
}

function boucle_INDEX_AUTEURS($id_boucle, &$boucles) {
	$boucle = &$boucles[$id_boucle];
	$id_table = $boucle->id_table;
	$boucle->from[] =  "spip_index_auteurs AS $id_table";
	return calculer_boucle($id_boucle, $boucles); 
}

function boucle_INDEX_MOTS($id_boucle, &$boucles) {
	$boucle = &$boucles[$id_boucle];
	$id_table = $boucle->id_table;
	$boucle->from[] =  "spip_index_mots AS $id_table";
	return calculer_boucle($id_boucle, $boucles); 
}

function boucle_INDEX_SIGNATURES($id_boucle, &$boucles) {
	$boucle = &$boucles[$id_boucle];
	$id_table = $boucle->id_table;
	$boucle->from[] =  "spip_index_signatures AS $id_table";
	return calculer_boucle($id_boucle, $boucles); 
}

function boucle_INDEX_SYNDIC($id_boucle, &$boucles) {
	$boucle = &$boucles[$id_boucle];
	$id_table = $boucle->id_table;
	$boucle->from[] =  "spip_index_syndic AS $id_table";
	return calculer_boucle($id_boucle, $boucles); 
}

Se declara así cual es la tabla principal sobre la que se harán las consultas para cada una de estos bucles.

Ahora se puede declarar el criterio:

function critere_similaire($idb, &$boucles, $param, $not) {
  $boucle = &$boucles[$idb];
 $table = $boucle->id_table;
 
 if (!ereg("index_(.*[^s])s?$",$table,$m) OR $not)
     erreur_squelette(_T('info_erreur_squelette'), $param);

 $id = 'id_' . $m[1];

 $boucle->select[] = 'SUM(' . $table . '.points*secondaire.points) AS similarite';
 $boucle->select[] = 'secondaire.i'.$id.' AS id_similaire';

 $boucle->from[]  = 'spip_'.$table .' AS secondaire';

 $boucle->where[] = $table .'.hash = secondaire.hash'; 
 $boucle->group = 'secondaire.'.$id;

}

El principio del criterio es el mismo que antes, sólo que SPIP va a hacer el SELECT inicial directamente desde una tabla index_loquesea. Por eso se tiene que añadir el empalme sobre esta tabla y los criterios en consecuencia

Desgraciadamente, no se puede directamente utilizar #ID_ARTICLE dentro de un bucle sobre index_loquesea. Debemos entonces añadir una baliza para recuperarse el ID que nos interesa:

function balise_SIMILARITE($p) {
	return rindex_pile($p, 'similarite', 'similaire');
}

function balise_ID_SIMILAIRE($p) {
	return rindex_pile($p, 'id_similaire', 'similaire');
}

El boucle a realizar es un poco más complejo que antes. Por ejemplo para un los artículos:

-  se debe anidar un bucle articles dentro de un bucle index_articles,
-  se debe indicar a este bucle que seleccione el artículo que tiene el mismo id que #ID_SIMILAIRE: {id_article=#ID_SIMILAIRE}
-  se puede excluir el artículo en curso con el criterio {exclus}.

<BOUCLE_article(ARTICLES) {id_article}>
#TITRE:<br>
<B_amis>
Aqui la lista de articulos similares:
<ul>
<BOUCLE_amis(index_articles){similaire}{id_article}{par similarite}{inverse} {0,7}>
<BOUCLE_selection(ARTICLES){id_article=#ID_SIMILAIRE}{exclus}>
<li><a href="#URL_ARTICLE">#TITRE:#SIMILARITE</a></li>
</BOUCLE_selection>
</BOUCLE_amis>
</ul>
</B_amis>
ningun articulo
<//B_amis>
</BOUCLE_article>

Footnotes

[1el código propuesto se debe colocar en mes_fonctions.php3.

[2se trata del mismo código {par POINTS} que ofrece un bucle recherche

[3o sea, el bucle en cuestion no incluye el critério, sí lo tiene uno de orden superior

[4un campo que no está declarado como columna en la base de datos

[5Se relacionan las tablas unas a otras haciendo todas las combinaciones posibles.

Tenga en cuenta que este criterio puede utilizarse sobre todos los tipos de bucles con los que se puede utilizar el criterio {recherche}.

updated on 27 November 2005

Discussion

Aucune discussion

Comment on this article

Who are you?
  • [Log in]

To show your avatar with your message, register it first on gravatar.com (free et painless) and don’t forget to indicate your Email addresse here.

Enter your comment here

This form accepts SPIP shortcuts {{bold}} {italic} -*list [text->url] <quote> <code> and HTML code <q> <del> <ins>. To create paragraphs, just leave empty lines.

Add a document

Follow the comments: RSS 2.0 | Atom