Me gustaría que le dierais un vistazo a esta funcionalidad (que está integrada a partir de Postgresql 8.3) , ya que pude ser muy muy interesante.
Os pongo en situación: muchos clientes piden poder buscar por todos los campos a la vez en una tabla, por lo que se les pone por defecto el campo de búsqueda en Eneboo a asterisco '*'.
El problema es que esto es funcional para tablas con pocos campos y pocos registros. Además, no busca por fechas, ni precios, ni otros números. Cuando busca por demasiados campos y registros, cada búsqueda puede demorarse 3 o 4 segundos (hablando de 30.000 registros x 20 campos)
Hemos hecho unas pruebas en el motor para permitir especificar en las MTD qué campos queríamos incluir en estas búsquedas y cuales no. Aunque la solución está bastante bien, porque permite limitar a los 5 o 6 campos que el cliente va a querer buscar normalmente, sigue siendo igual de lento cuando le pones que busque por 20 campos.
A raíz de este problema nos hemos puesto a investigar mecanismos para optimizar este tipo de consultas. El resultado es bastante bueno, aunque el montaje es un poco complejo. Se trata de implementar Full Text Search [1] para acelerar la búsqueda, ya que se parece mucho a lo que queremos hacer. Reduce el tiempo de búsqueda de 4 segundos a 0.05s (50ms)
Os pongo aquí el ejemplo que yo tengo hecho, que es con pedidos de cliente:
Primero que nada vamos a necesitar indexar los documentos de la tabla pedidoscli. En mi caso hice una tabla nueva, llamada "pedidoscli_fts". Lo que hacemos es concatenarle todos los campos como texto y convertirlos a ts_vector:
CREATE TABLE pedidoscli_fts AS
SELECT idpedido,
to_tsvector('spanish',coalesce(codigo,'')) ||
to_tsvector('spanish',coalesce(servido,'')) ||
to_tsvector('spanish',coalesce(replace(fecha::text,'-','/'),'')) ||
to_tsvector('spanish',coalesce(nombrecliente,'')) ||
to_tsvector('spanish',coalesce(cifnif,'')) ||
to_tsvector('spanish',coalesce(total::text,'')) ||
to_tsvector('spanish',coalesce(codcliente,'')) ||
to_tsvector('spanish',coalesce(observaciones,'')) ||
to_tsvector('spanish',coalesce(coddivisa,'')) ||
to_tsvector('spanish',coalesce(codserie,'')) ||
to_tsvector('spanish',coalesce(neto::text,'')) ||
to_tsvector('spanish',coalesce(totaleuros::text,'')) ||
to_tsvector('spanish',coalesce(codpago,'')) ||
to_tsvector('spanish',coalesce(codagente,'')) ||
to_tsvector('spanish',coalesce(codalmacen,'')) ||
to_tsvector('spanish',coalesce(direccion,'')) ||
to_tsvector('spanish',coalesce(codpostal,'')) ||
to_tsvector('spanish',coalesce(ciudad,'')) ||
to_tsvector('spanish',coalesce(provincia,'')) ||
to_tsvector('spanish',coalesce(apartado,'')) ||
to_tsvector('spanish',coalesce(codpais,'')) ||
to_tsvector('spanish',coalesce(codejercicio,''))
as concatenacion_tokens
FROM pedidoscli
Luego, tenemos que crear un índice en esta columna llamada "concatenacion_tokens":
CREATE INDEX pedidoscli_fts_idx
ON pedidoscli_fts
USING gin
(concatenacion_tokens);
El tipo de índice puede ser GIN o GIST. Por lo que tengo entendido GIN tiene mejor velocidad de búsqueda, pero GIST es más rápido actualizando el índice. En mi caso elegí GIN, tal y como está en la SQL.
Con esto sería suficiente para empezar a buscar, pero el problema es que solo permite buscar por palabras completas, es decir, no permite preguntar por cosas similares, o con un LIKE. Para mejorar esto yo voy a hacer uso del módulo "pg_trgm" que sirve para indexar y buscar palabras por similitud. Por tanto, instalaremos la extensión:
CREATE EXTENSION pg_trgm;
Ahora lo que necesito es un diccionario de palabras conocidas, para ello uso la tabla anterior y creo a partir de ella un resumen en una tabla nueva pedidoscli_fts_words:
CREATE pedidoscli_fts_words AS
SELECT word from ts_stat('SELECT concatenacion_tokens FROM pedidoscli_fts');
Y crearemos un índice también sobre la columna words. Este índce será de trigramas (con el módulo pg_trgm). Esto acelerará los LIKE y permite también búsquedas por similitud.
CREATE INDEX pedidoscli_fts_words_idx
ON pedidoscli_fts_words
USING gin
(word COLLATE pg_catalog."default" gin_trgm_ops);
Ahora con esto ya se pueden hacer consultas, pero como es un poco coñazo el construir la consulta a mano, me hice yo una función de postgresql que lo automatiza. Básicamente convierte la entrada a un array de palabras, localiza en el diccionario las palabras indexadas que se parecen y luego consulta pedidoscli_fts para obtener un idpedido y un ranking. El ranking es útil si quieres poner arriba los mejores resultados. De todos modos creo que no es nuestro caso.
Esta sería la función:
CREATE OR REPLACE FUNCTION search_fts_pedidoscli(IN filtros text, OUT idpedido integer, OUT rank real)
RETURNS SETOF record AS
$BODY$
DECLARE
qry tsquery;
a_filtros text[];
txtqry text;
txtnew text;
filtro text;
BEGIN
-- DIVIDIR filtro en un array separado por espacios (palabras)
a_filtros := ('{' || replace(lower(filtros), ' ', ',') || '}')::text[];
txtqry := '';
FOREACH filtro IN ARRAY a_filtros
LOOP
txtnew := '(' || string_agg(word, ' | ') || ')' FROM pedidoscli_fts_words WHERE word LIKE filtro;
IF txtnew IS NULL THEN
filtro := replace(filtro,'%','');
txtnew := '(' || string_agg(word, ' | ') || ')' FROM pedidoscli_fts_words WHERE word % filtro;
IF txtnew IS NULL THEN
RAISE WARNING 'filtro(SIMILAR %) -> 0 results -> NULL', filtro;
txtnew := '(' || filtro || ')';
ELSE
RAISE WARNING 'filtro SIMILAR % -> %', filtro, txtnew ;
END IF;
END IF;
IF txtqry != '' THEN
txtqry := txtqry || '&';
END IF;
txtqry := txtqry || txtnew;
END LOOP;
RAISE WARNING 'Query (%)', txtqry;
qry := to_tsquery(txtqry);
RETURN QUERY SELECT fts.idpedido, ts_rank_cd(concatenacion_tokens, qry) AS rank
FROM pedidoscli_fts fts
WHERE concatenacion_tokens @@ qry;
--RETURN QUERY SELECT 1 as idpedido, 0 as rank;
END;
$BODY$
LANGUAGE plpgsql VOLATILE
COST 100
ROWS 1000;
Si os fijais un poco, al final lo que hace es un simple select, donde busca con el operador @@, que es el que hace el trabajo.
Y con esto, ya podemos hacer búsquedas como esta:
SELECT codigo, nombrecliente, direccion, ciudad, fecha
FROM pedidoscli WHERE idpedido IN (SELECT idpedido FROM search_fts_pedidoscli('valdemoro 2012/10%') )
Parte de la gracia es que puedo buscar simultáneamente en varias columnas: valdemoro es ciudad y 2012 es fecha. Separando con espacio las palabras de búsqueda puedo incluir el porcentaje (%) para expandir las palabras que quiera, no solamente la última. Si no ponemos porcentaje, entonces busca palabras enteras en lugar de buscar el campo entero como hacíamos con LIKE.
Por cierto, este ejemplo lo hice con un desarrollo propio. He quitado varias columnas que creo que eran propias, pero es posible me haya dejado algo. Si hay alguna columna de más, la borrais y ya está.
Obviamente, si cambia algo en pedidoscli, tenéis que rehacer la parte de los índices que toquen. Se puede hacer a mano, con triggers, etc. No me he metido aún en este tema.
En fin, si podéis, lo probáis. A ver que os parece.
(Nota: creo que es mejor poner una columna en pedidoscli, en vez de crear una tabla nueva pedidoscli_fts)