Buscador en PHP

Bueeeno, aquesta nit toca post tècnic, que ja he aconseguit _quasi_ acabar una de les parts que trobo més interessant del blog: el cercador. Amb el pas del temps la quantitat d'informació enregistrada a la web ha augmentat considerablement, i sense una bona categorització i organització dels continguts, certs missatges corren el perill de caure en l'oblit. En aquest sentit, tenir un arxiu de missatges agrupats per categories o data pot ser molt útil, però perd efectivitat quan parlem de categoríes amb més de 100 missatges. Trobar un post concret es pot convertir en tota una aventura. És aqui on es posa a prova la utilitat d'un cercador eficient.

La majoria de cercadors que he pogut veure a altres llocs web es limiten a fer cerques per paraules a la base de dades, permetent fer la cerca al títol, al contingut de la web, a paraules clau, etc... i utilitzant operadors del tipus "LIKE" per trobar coincidències amb els termes de recerca. Aquest sistema presenta força limitacions, com haver de repetir la cerca per separat per cada paraula, especificar per separat tots els camps de la base de dades on s'ha de mirar, impossibilitat de donar més importància a unes paraules que d'altres, filtrar paraules de poca rellevància... És aqui on vaig descobrir la utilitat d'un tipus d'index que utilitza MySQL, anomenat "FULL TEXT". La troballa de l'any, certament. El "FULL TEXT" ens permet agrupar diversos camps en un sol índex, per poder realitzar-hi cerques conjuntes mitjançant una sola cadena de caràcters (una paraula o frase), i afegir-hi modificadors a l'estil Google. És a dir, podem utilitzar modificadors com les cometes, per agrupar paraules, els simbols < i > per especificar una major o menos rellevància de certa paraula, els simbols - o + per mostrar només resultats que contingin o no una paraula determinada... I tot això de manera automàtica, amb una consulta senzilla.

Un exemple ens ho farà més fàcil d'entendre: Imagineu que vull buscar un manual de photoshop a la web. La consulta que realitzaria a la base de dades, on prèviament he creat un índex FULL TEXT per a certs camps que m'interessen seria

SELECT * FROM posts WHERE MATCH(titulo,descripcion,contenido,contenido_ext,tipo) AGAINST ('manual de photoshop')

Llestos, així de senzill. Les cerques de FULL TEXT ens permeten, a més, conèixer la rellevància (el camp score) de les coincidències per a cada registre trobat. Així podem tambè ordenar els resultats segons s'acostin més o menys al que buscàvem. Això ho fariem de la següent manera.

SELECT *,MATCH(titulo,descripcion,contenido,contenido_ext,tipo) AGAINST ('manual de photoshop') as score FROM posts WHERE MATCH(titulo,descripcion,contenido,contenido_ext,tipo) AGAINST ('manual de photoshop') ORDER BY score desc;

Només amb això ja disposariem d'una eina força potent per trobar informació a una base de dades (els índex FULL TEXT permeten cerques molt més complexes que aquestes, això us ho deixo investigar a les pròpies especificacions de la página de MySQL). Ens trobem, però, que tan sols un llistat amb el títol dels missatges en què hem trobat continguts interessants pot no ser útil, si la cerca no és suficientment acurada, i tots sabem per experiència que afinar amb les paraules correctes no és tasca senzilla. És aqui on em vaig plantejar de implantar un sistema d'extracció de texte rellevant per la pàgina de resultats del cercador. De nou el sistema de Google que ens mostra la part de la pàgina on apareixen les paraules que estem buscant, em va semblar la millor opció. D'un sol cop d'ull pots veure si aquell missatge on apareixen els termes que busques parla realment del que volíes, ja que no necessàriament ha de ser sempre així.

El que faig es separar cada una de les paraules, eliminar les paraules massa curtes, i veure a quina posició dins del contingut del missatge es troben la resta. Després ordeno aquestes posicions i busco aquella part del texte on aquest termes es troben més aprop entre ells, per mostrar-la a mode d'extracte. Aquest seria l'exemple en PHP, que és el llenguatge en què s'ha programat obokaman.com

$busca_ace = array ("á","é","í","ó","ú","Á","É","Í","Ó","Ú","à","è","ì","ò","ù","À","È","Ì","Ò","Ù","+","<",">");
$reemplaza_ace = array ("a","e","i","o","u","A","E","I","O","U","a","e","i","o","u","A","E","I","O","U"," "," "," ");

$buscar=trim(strtolower($texto));
$buscar=str_replace($busca_ace, $reemplaza_ace, $buscar);

$texto_tmp=explode(" ",$buscar);
foreach ($texto_tmp as $valor) {

if (strlen($valor)>3) {
$texto_tmp2[]=$valor;
}else{
$texto_tmp3[]=$valor;
}

}
if (is_array($texto_tmp2)) {
$texto_incluido=implode(",",$texto_tmp2);
}
if (is_array($texto_tmp3)) {
$texto_omitido=implode(",",$texto_tmp3);
}

Ara tenim les paraules que ens interessen (les més llargues de 3 caràcters) a un array anomenat $texto_tmp2. Només caldrà recòrrer aquest array a cada missatge per cercar les coincidències, mirar la seva posició, i destacar cada paraula al texte resultant.

$text1=html_entity_decode($mensaje);
$text1=str_replace($busca_ace, $reemplaza_ace, $text1);
$text1=preg_replace('/[\n\r\t]/',' ',$text1);
if (is_array($texto_tmp2)) {//4
$posiciones= array();
foreach ($texto_tmp2 as $valor) {
$string=strtolower($text1);
$searched=strtolower($valor);
$offsets= array();
$next=0;

while(1){
$entry = strpos(
$string,
$searched,
$next+$offsets[sizeof($offsets)-1] /*juggling*/
);
if(gettype($entry)==boolean){break;};
$next=strlen($searched);
$offsets[]=$entry;
}

$posiciones=array_merge($posiciones,$offsets);
}
sort($posiciones);
for ($i=0;$i
$n=1;
while ($posiciones[($i+$n)]&&($posiciones[($i+$n)]-$posiciones[$i])<500) {
if ($n>$max) {
$max=$n;
$posicion_def=$posiciones[$i];
}
$n++;
}
}
if ($posicion_def) {
$posicion1=$posicion_def;
}else{
$posicion1=$posiciones[0];
}
if ($posicion1>60) {
$posicion1=$posicion1-60;
}else{
$posicion1=0;
}
unset($posicion_def);
unset($posiciones);
unset($offsets);
unset($max);
}
$text1 = substr(trim($text1),$posicion1,500);
if ($text1!="") {
$text1 = '[...]'.$text1.'[...]';
}
foreach ($texto_tmp2 as $valor) {
$text1 = eregi_replace("($valor)","\\1",$text1);
}

$mensaje és la variable que conté tot el text de cada post. Primer busquem les posicions de les coincidències. Després busquem la posició més alta en què les paraules es trobin més properes entre elles (al menys de 500 caràcters entre cadascuna), i finalment, utilitzem aquesta posició per fer un extracte de 500 caràcters d'aquesta porció del text, destacant cada paraula amb un . Això, a mode d'esboç, és el que fa el cercador d'obokaman per cercar i mostrar els resultats. És el millor de fer les coses a mà: que fan exactament el què tu vols. Estaré encantat de sentir les teves suggerències, crítiques, consells per millorar aquest sistema. Encara no l'has provat?

Albert García Gibert

Cofounder and former CTO of Uvinum. Founder of Obolog and Splitweet. Father of Júlia & Abril. I'd like to travel more and improve my guitar skills.

El Prat de Llobregat, Barcelona https://twitter.com/obokaman