|
| ||||||||||||
| ||||||||||||
|
2007 г
Введение в полнотекстовый поиск в PostgreSQLОлег Бартунов, Федор СигаевПриложениеПоиск с очепятками
Часто полнотекстовый поиск используется совместно с модулем
=# select show_trgm('supyrnova');
show_trgm
-------------------------------------------------
{" s"," su",nov,ova,pyr,rno,sup,upy,"va ",yrn}
С помощью функции
=# select * into apod_words from stat('select fts from apod') order by ndoc desc,
nentry desc,word;
=# \d apod_words
Table "public.apod_words"
Column | Type | Modifiers
--------+---------+-----------
word | text |
ndoc | integer |
nentry | integer |
=# create index trgm_idx on apod_words using gist(word gist_trgm_ops);
Теперь мы можем быстро искать слова-кандидаты используя функцию
similarity, которая подсчитывает похожесть слова используя
количество общих триграмм.
=# select word, similarity(word, 'supyrnova') AS sml
from apod_words where word % 'supyrnova' order by sml desc, word;
word | sml
---------------+----------
supernova | 0.538462
pre-supernova | 0.411765
(2 rows)
Из соображений производительности, слова, у которых похожесть не превышает
некоторый порог, отбрасываются. Посмотреть значение порога и изменить его
можно с помощью функций
show_limit() и set_limit(real). По умолчанию
используется значение 0.3.
Советы по повышению производительностиЕсли ваша коллекция документов очень большая и непрерывно пополняется, то может возникнуть ситуация, когда скорость вставки в базу и поиск станут не удовлетворять вас. PostgreSQL предоставляет много возможностей по оптимизации, но мы кратко коснемся сегментирования и распределения данных. Сегментирование данных
Сегментирование данных можно организовать с помощью наследования
(
В нашем примере мы создаем таблицу
CREATE TABLE apod_class (
id integer,
title text,
body text,
sdate date,
keywords text,
fts tsvector
);
CREATE TABLE apod_new ( CHECK ( sdate >2001-08-08 ) ) INHERITS (apod_class);
CREATE INDEX gist_idx ON apod_new USING gist(fts);
CREATE TABLE apod_archive ( CHECK ( sdate ≤2001-08-08 ) ) INHERITS (apod_class);
CREATE INDEX gist_idx ON apod_new USING gin(fts);
PostgreSQL позволяет искать как по всей коллекции, указав таблицу
apod_class, так и по отдельным частям.
В зависимости от задачи, сегментировать данные можно и по большему количеству
таблиц, например, распределять документы по годам, месяцам.
Оптимизатор PostgreSQL автоматически выбирает только те таблицы, которые
удовлетворяют условию CHECK, что очень благоприятно
сказывается на производительности запросов. Например, для запроса
apod=# select title,rank_cd(fts, q) from apod_class, to_tsquery('stars') q
where fts @@ q order by rank_cd desc limit 5;
будут просматриваться две таблицы, а для запроса
apod=# select title,rank_cd(fts, q) from apod_class, to_tsquery('stars') q
where fts @@ q and sdate > 2001-08-08 order by rank_cd desc limit 5;
будет использоваться только таблица apod_new. Отметим, что
для этого необходимо включить CONSTRAINT EXCLUSION
SET constraint_exclusion TO on; Распределение данных
Если сегментирование данных по таблицам недостаточно, то можно распределять
данные по серверам. В этом случае, с помощью модуля
select dblink_connect('pgweb','dbname=pgweb hostaddr='XXX.XXX.XXX.XXX');
select * from dblink('pgweb',
'select tid, title, rank_cd(fts_index, q) as rank from pgweb,
to_tsquery(''table'') q
where q @@ fts_index and tid >= 6000 order by rank desc limit 10' )
as t1 (tid integer, title text, rank real)
union all
select tid, title, rank_cd(fts_index, q) as rank from pgweb,
to_tsquery('table') q
where q @@ fts_index and tid < 6000 and tid > 0 order by rank desc limit 10
) as foo
order by rank desc limit 10;
Отметим, что ранжирующая функция требует только локальной информации, что
облегчает реализацию.
Словарь для целых чисел
В качестве примера нового словаря для полнотекстового поиска мы рассмотрим
словарь для целых чисел
Для создания словаря необходимо написать две функции, имена которых потом
будут использованы в команде Словарь просто обрезает длинные целые числа.
=# select lexize('intdict', 11234567890);
lexize
----------
{112345}
Теперь будем трактовать длинные целые числа как стоп-слово.
=# ALTER FULLTEXT DICTIONARY intdict SET OPTION 'MAXLEN=6, REJECTLONG=TRUE';
=# select lexize('intdict', 11234567890);
lexize
--------
{}
Файлы make && make install psql DBNAME < dict_intdict.sql
Файл
#include "postgres.h"
#include "utils/builtins.h"
#include "fmgr.h"
#ifdef PG_MODULE_MAGIC
PG_MODULE_MAGIC;
#endif
#include "utils/ts_locale.h"
#include "utils/ts_public.h"
#include "utils/ts_utils.h"
typedef struct {
int maxlen;
bool rejectlong;
} DictInt;
PG_FUNCTION_INFO_V1(dinit_intdict);
Datum dinit_intdict(PG_FUNCTION_ARGS);
Datum
dinit_intdict(PG_FUNCTION_ARGS) {
DictInt *d = (DictInt*)malloc( sizeof(DictInt) );
Map *cfg, *pcfg;
text *in;
if ( !d )
elog(ERROR, "No memory");
memset(d,0,sizeof(DictInt));
/* Your INIT code */
/* defaults */
d->maxlen = 6;
d->rejectlong = false;
if ( PG_ARGISNULL(0) || PG_GETARG_POINTER(0) == NULL ) { /* no options */
PG_RETURN_POINTER(d);
}
in = PG_GETARG_TEXT_P(0);
parse_keyvalpairs(in,&cfg);
PG_FREE_IF_COPY(in, 0);
pcfg=cfg;
while (pcfg->key) {
if ( strcasecmp("MAXLEN", pcfg->key) == 0 ) {
d->maxlen=atoi(pcfg->value);
} else if ( strcasecmp("REJECTLONG", pcfg->key) == 0 ) {
if ( strcasecmp("true", pcfg->value) == 0 ) {
d->rejectlong=true;
} else if ( strcasecmp("false", pcfg->value) == 0 ) {
d->rejectlong=false;
} else {
elog(ERROR,"Unknown value: %s => %s", pcfg->key,
pcfg->value);
}
} else {
elog(ERROR,"Unknown option: %s => %s", pcfg->key, pcfg->
value);
}
pfree(pcfg->key);
pfree(pcfg->value);
pcfg++;
}
pfree(cfg);
PG_RETURN_POINTER(d);
}
PG_FUNCTION_INFO_V1(dlexize_intdict);
Datum dlexize_intdict(PG_FUNCTION_ARGS);
Datum
dlexize_intdict(PG_FUNCTION_ARGS) {
DictInt *d = (DictInt*)PG_GETARG_POINTER(0);
char *in = (char*)PG_GETARG_POINTER(1);
char *txt = pnstrdup(in, PG_GETARG_INT32(2));
TSLexeme *res=palloc(sizeof(TSLexeme)*2);
/* Your INIT dictionary code */
res[1].lexeme = NULL;
if ( PG_GETARG_INT32(2) > d->maxlen ) {
if ( d->rejectlong ) { /* stop, return void array */
pfree(txt);
res[0].lexeme = NULL;
} else { /* cut integer */
txt[d->maxlen] = '\0';
res[0].lexeme = txt;
}
} else {
res[0].lexeme = txt;
}
PG_RETURN_POINTER(res);
}
Файл subdir = contrib/dict_intdict top_builddir = ../.. include $(top_builddir)/src/Makefile.global MODULE_big = dict_intdict OBJS = dict_tmpl.o DATA_built = dict_intdict.sql DOCS = include $(top_srcdir)/contrib/contrib-global.mk
Файл
SET search_path = public;
BEGIN;
CREATE OR REPLACE FUNCTION dinit_intdict(internal)
returns internal
as 'MODULE_PATHNAME'
language 'C';
CREATE OR REPLACE FUNCTION dlexize_intdict(internal,internal,internal,internal)
returns internal
as 'MODULE_PATHNAME'
language 'C'
with (isstrict);
CREATE FULLTEXT DICTIONARY intdict
LEXIZE 'dlexize_intdict' INIT 'dinit_intdict'
OPTION 'MAXLEN=6,REJECTLONG=false'
;
END;
Очень простой парсер
Предположим, что мы хотим создать свой парсер, который выделяет только
один тип токена - слово (3,word,Word) и подключить его к полнотекстовому
поиску. Для этого нам нужен еще один тип токена - это разделитель
(12, blank,Space symbols).
Идентификаторы типов (3,12) выбраны таким образом, чтобы можно было
использовать стандартную функцию
Поместите файлы make make install psql regression < test_parser.sql
Мы создали тестовую FTS конфигурацию Для написания своего парсера необходимо разработать как-минимум 4 функции, см. SQL команду CREATE FULLTEXT PARSER.
=# SELECT * FROM parse('testparser','That''s my first own parser');
tokid | token
-------+--------
3 | That's
12 |
3 | my
12 |
3 | first
12 |
3 | own
12 |
3 | parser
=# SELECT to_tsvector('testcfg','That''s my first own parser');
to_tsvector
-------------------------------------------------
'my':2 'own':4 'first':3 'parser':5 'that''s':1
=# SELECT headline('testcfg','Supernovae stars are the brightest phenomena in galaxies', to_tsquery('testcfg', 'star'));
headline
-----------------------------------------------------------------
Supernovae stars are the brightest phenomena in galaxies
Файл
#ifdef PG_MODULE_MAGIC
PG_MODULE_MAGIC;
#endif
/*
* types
*/
/* self-defined type */
typedef struct {
char * buffer; /* text to parse */
int len; /* length of the text in buffer */
int pos; /* position of the parser */
} ParserState;
/* copy-paste from wparser.h of tsearch2 */
typedef struct {
int lexid;
char *alias;
char *descr;
} LexDescr;
/*
* prototypes
*/
PG_FUNCTION_INFO_V1(testprs_start);
Datum testprs_start(PG_FUNCTION_ARGS);
PG_FUNCTION_INFO_V1(testprs_getlexeme);
Datum testprs_getlexeme(PG_FUNCTION_ARGS);
PG_FUNCTION_INFO_V1(testprs_end);
Datum testprs_end(PG_FUNCTION_ARGS);
PG_FUNCTION_INFO_V1(testprs_lextype);
Datum testprs_lextype(PG_FUNCTION_ARGS);
/*
* functions
*/
Datum testprs_start(PG_FUNCTION_ARGS)
{
ParserState *pst = (ParserState *) palloc(sizeof(ParserState));
pst->buffer = (char *) PG_GETARG_POINTER(0);
pst->len = PG_GETARG_INT32(1);
pst->pos = 0;
PG_RETURN_POINTER(pst);
}
Datum testprs_getlexeme(PG_FUNCTION_ARGS)
{
ParserState *pst = (ParserState *) PG_GETARG_POINTER(0);
char **t = (char **) PG_GETARG_POINTER(1);
int *tlen = (int *) PG_GETARG_POINTER(2);
int type;
*tlen = pst->pos;
*t = pst->buffer + pst->pos;
if ((pst->buffer)[pst->pos] == ' ') {
/* blank type */
type = 12;
/* go to the next non-white-space character */
while (((pst->buffer)[pst->pos] == ' ') && (pst->pos < pst->len)) {
(pst->pos)++;
}
} else {
/* word type */
type = 3;
/* go to the next white-space character */
while (((pst->buffer)[pst->pos] != ' ') && (pst->pos < pst->len)) {
(pst->pos)++;
}
}
*tlen = pst->pos - *tlen;
/* we are finished if (*tlen == 0) */
if (*tlen == 0) type=0;
PG_RETURN_INT32(type);
}
Datum testprs_end(PG_FUNCTION_ARGS)
{
ParserState *pst = (ParserState *) PG_GETARG_POINTER(0);
pfree(pst);
PG_RETURN_VOID();
}
Datum testprs_lextype(PG_FUNCTION_ARGS)
{
/*
Remarks:
- we have to return the blanks for headline reason
- we use the same lexids like Teodor in the default
word parser; in this way we can reuse the headline
function of the default word parser.
*/
LexDescr *descr = (LexDescr *) palloc(sizeof(LexDescr) * (2+1));
/* there are only two types in this parser */
descr[0].lexid = 3;
descr[0].alias = pstrdup("word");
descr[0].descr = pstrdup("Word");
descr[1].lexid = 12;
descr[1].alias = pstrdup("blank");
descr[1].descr = pstrdup("Space symbols");
descr[2].lexid = 0;
PG_RETURN_POINTER(descr);
}
Файл override CPPFLAGS := -I. $(CPPFLAGS) MODULE_big = test_parser OBJS = test_parser.o DATA_built = test_parser.sql DATA = DOCS = README.test_parser REGRESS = test_parser ifdef USE_PGXS PGXS := $(shell pg_config --pgxs) include $(PGXS) else subdir = contrib/test_parser top_builddir = ../.. include $(top_builddir)/src/Makefile.global include $(top_srcdir)/contrib/contrib-global.mk endif
Файл
SET search_path = public;
BEGIN;
CREATE FUNCTION testprs_start(internal,int4)
RETURNS internal
AS 'MODULE_PATHNAME'
LANGUAGE 'C' with (isstrict);
CREATE FUNCTION testprs_getlexeme(internal,internal,internal)
RETURNS internal
AS 'MODULE_PATHNAME'
LANGUAGE 'C' with (isstrict);
CREATE FUNCTION testprs_end(internal)
RETURNS void
AS 'MODULE_PATHNAME'
LANGUAGE 'C' with (isstrict);
CREATE FUNCTION testprs_lextype(internal)
RETURNS internal
AS 'MODULE_PATHNAME'
LANGUAGE 'C' with (isstrict);
CREATE FULLTEXT PARSER testparser
START 'testprs_start'
GETTOKEN 'testprs_getlexeme'
END 'testprs_end'
LEXTYPES 'testprs_lextype'
;
CREATE FULLTEXT CONFIGURATION testcfg PARSER 'testparser' LOCALE NULL;
CREATE FULLTEXT MAPPING ON testcfg FOR word WITH simple;
END;
|
|
CITForum © 1997–2025