Добро пожаловать в форум, Guest  >>   Войти | Регистрация | Поиск | Правила | В избранное | Подписаться
Все форумы / Oracle Новый топик    Ответить
 Нечеткий поиск по русским словам.  [new]
vivaldi
Guest
Я думаю, с подобной проблемой сталкивались многие, может, подскажете:

Необходимо найти всех людей, у которых ФИО такие же, как у искомого человека или сильно похожие (например, написанные с опечатками, как в паре
КлЁПик ОмелИя МИхаЙловна и
КлЕНик АмелЬя МУхаИловВна ) (или просто похожие, как Петров Василий Федорович и Петрова Василиса Федоровна).
Количество различий в каждом слове от 0 до 3, иначе получается много false-positive результатов.

Может, у кого-нибудь есть реализованный вариант fuzzy-поиска на SQL или PL/SQL, основанный на n-граммном индексе и/или функции близости Левенштайна-Дамерау?

Стандартный Oracle Text, как я понял, не позволяет решить эту проблему c русскими словами? Или я неправильно понял?
Подскажите, пожалуйста...
3 фев 05, 22:59    [1298925]     Ответить | Цитировать Сообщить модератору
 Re: Нечеткий поиск по русским словам.  [new]
Victor Repetsky
Member

Откуда: Kiev
Сообщений: 12
Есть RCO для оракла.
Кроме того для ФИО можно самому придумать что-то простое. Когда-то писал такую функцию ключа для ФИО - три фонетизированные буквы фамилии, буква имени, буква отчества. Вместе с годом рождения давало очень высокую вероятность нахождения двойников.
3 фев 05, 23:30    [1298949]     Ответить | Цитировать Сообщить модератору
 Re: Нечеткий поиск по русским словам.  [new]
vivaldi
Guest
Victor Repetsky
Есть RCO для оракла.


RCO - это за денежку, причем немалую, у меня такой нету, я еще студент. Да и потом, там слишком большой функционал, мне так много не требуется для одной задачки.

Victor Repetsky

Кроме того для ФИО можно самому придумать что-то простое. Когда-то писал такую функцию ключа для ФИО - три фонетизированные буквы фамилии, буква имени, буква отчества. Вместе с годом рождения давало очень высокую вероятность нахождения двойников.


В моем случае все несколько хуже - количество записей измеряется десятками тысяч, ОЧЕНЬ много людей одного года рождения (52% записей). Однофамильцев одногодок с одинаковыми инициалами - 848 человек.
Например:
Иванова Е А - 15 человек.
Васильев А В - 6 человек.

По именам некоторые из них различаются, но не все, есть и опечатки (ложные различия). К тому же как быть с опечатками в инициалах, как в примере Амелья и Омелия (подразумевалось одно и то же имя, написанное кривыми руками) :(

И это примеры только по правильно занесенным фамилиям. А как быть с фамилиями, написанными с опечатками?

В общем, изобретать велосипед тут только себе во вред. Алгоритмы нечеткого поиска существуют и с успехом применяются, просто я не знаю где их искать... Я уже килобайты статей по этой теме перелопатил, но все-равно слабо соображаю, как их можно реализовать в БД...
Может, не дорос еще...

Леонид Бойцов, автор нескольких статей по этой теме, подсказал мне, что для БД наиболее подходящим с точки зрения быстродействия и релевантности является метод n-граммного индекса с использованием функции близости Левенштейна-Дамерау. (если кто интересуется нечетким поиском и не только в том контексте - загляните на его сайт, целиком посвященный этой теме http://itman.narod.ru/. Много ссылок на англоязычные статьи. Есть и на русском.)

В общем, суть этих алгоритмов я понял, а реализовать на SQL или PL/SQL никак не получается. Неужели этот нечеткий поиск - такая редкая проблема и никто с ней не сталкивался?!
4 фев 05, 00:46    [1298996]     Ответить | Цитировать Сообщить модератору
 Re: Нечеткий поиск по русским словам.  [new]
vbegun
Guest

Леонид Бойцов проделал большую работу. Отдельное ему спасибо за собранный
материал. Я пользовался англоговорящими источниками, но сайт, ИМХО, кладезь!
СПАСИБО!

Это то что есть у меня. Из кода ld сделует убрать секции IFDEF, так просто удобно
код компилировать, недождался я 10gR2 :) Если найдёшь ошибку с благодарностью приму
исправления.
-- Hamming Distance
CREATE OR REPLACE FUNCTION hd (
  as_src_i                         IN VARCHAR2
, as_trg_i                         IN VARCHAR2
)
RETURN NUMBER
DETERMINISTIC
AS
/* PL/SQL implementation (c) 2002 Vladimir Begun */
  ln_src_len                       PLS_INTEGER := NVL(LENGTH(as_src_i), 0);
  ln_trg_len                       PLS_INTEGER := NVL(LENGTH(as_trg_i), 0);
  ln_distance                      PLS_INTEGER := 0;
BEGIN
  IF (ln_src_len <> ln_trg_len)
  THEN
    RETURN NULL;
  END IF;

  IF (ln_src_len = 0)
  THEN
    RETURN ln_src_len;
  END IF;

  FOR i IN 1..ln_src_len
  LOOP
    IF (SUBSTR(as_src_i, i, 1) <> SUBSTR(as_trg_i, i, 1))
    THEN
      ln_distance := ln_distance + 1;
    END IF;
  END LOOP;
  RETURN ln_distance;
END hd;
/

UNDEFINE DEBUG=1
-- Levenshtein distance, PL/SQL implementation
-- http://www.merriampark.com/ld.htm
-- Levenshtein Distance, in Three Flavors
CREATE OR REPLACE FUNCTION ld (
  as_src_i                         IN VARCHAR2
, as_trg_i                         IN VARCHAR2
)
RETURN NUMBER
DETERMINISTIC
AS
/* PL/SQL implementation (c) 2002 Vladimir Begun */
  ln_src_len                       PLS_INTEGER := NVL(LENGTH(as_src_i), 0);
  ln_trg_len                       PLS_INTEGER := NVL(LENGTH(as_trg_i), 0);
  ln_hlen                          PLS_INTEGER;
  ln_cost                          PLS_INTEGER;
  TYPE t_numtbl IS TABLE OF PLS_INTEGER INDEX BY BINARY_INTEGER;
  la_ldmatrix                         t_numtbl;
@ifdef DEBUG
  PROCEDURE show_la_ldmatrix
  IS
  BEGIN
    dbms_output.put_line(RPAD('=', 80, '='));
    FOR v IN 0 .. ln_trg_len
    LOOP
      FOR h IN 0 .. ln_src_len
      LOOP
        BEGIN
          dbms_output.put(TO_CHAR(la_ldmatrix(v * ln_hlen + h), '09'));
        EXCEPTION
          WHEN NO_DATA_FOUND
          THEN dbms_output.put_line('  ');
        END;
      END LOOP;
      dbms_output.put_line('');
    END LOOP;
  END show_la_ldmatrix;
@endif
BEGIN
  IF (ln_src_len = 0)
  THEN
    RETURN ln_trg_len;
  ELSIF (ln_trg_len = 0)
  THEN
    RETURN ln_src_len;
  END IF;

  ln_hlen := ln_src_len + 1;
  FOR h IN 0 .. ln_src_len
  LOOP
    la_ldmatrix(h) := h;
  END LOOP;

  FOR v IN 0 .. ln_trg_len
  LOOP
    la_ldmatrix(v * ln_hlen) := v;
  END LOOP;

  FOR h IN 1 .. ln_src_len
  LOOP
    FOR v IN 1 .. ln_trg_len
    LOOP
      IF (SUBSTR(as_src_i, h, 1) = SUBSTR(as_trg_i, v, 1))
      THEN
        ln_cost := 0;
      ELSE
        ln_cost := 1;
      END IF;
      la_ldmatrix(v * ln_hlen + h) :=
         LEAST(
           la_ldmatrix((v - 1) * ln_hlen + h    ) + 1
         , la_ldmatrix( v      * ln_hlen + h - 1) + 1
         , la_ldmatrix((v - 1) * ln_hlen + h - 1) + ln_cost
         )
      ;
    END LOOP;
@ifdef DEBUG
    show_la_ldmatrix;
@endif
  END LOOP;
  RETURN la_ldmatrix(ln_trg_len * ln_hlen + ln_src_len);
END ld;
/
--
Vladimir Begun
The statements and opinions expressed here are my own and
do not necessarily represent those of Oracle Corporation.
4 фев 05, 01:52    [1299022]     Ответить | Цитировать Сообщить модератору
 Re: Нечеткий поиск по русским словам.  [new]
vbegun
Guest
Да, стоит отметить, что скорость работы этой функции можно увеличить.
Зависит от того как часто она будет вызываться.
--
Vladimir Begun
The statements and opinions expressed here are my own and
do not necessarily represent those of Oracle Corporation.
4 фев 05, 02:08    [1299026]     Ответить | Цитировать Сообщить модератору
 Re: Нечеткий поиск по русским словам.  [new]
vivaldi
Guest
vbegun, спасибо Вам за помощь.
На первый взгляд все работает замечательно.
Более скурпулезно тестировать буду завтра, точнее уже сегодня утром. Сейчас уже сил нет - пойду спать.
Вообще говоря, вызывать ее надо будет очень часто. Для ускорения работы поиска мне рекомендовали построить n-граммный индекс в отдельной таблице на все элементы словаря (фамилии, имена, отчества). Самый идеальный индекс для случая поиска именно по ФИО - N-грамы (подстроки элементов словаря) длины 2 или 3. Лучше 2, т.к. часто ФИО достаточно короткие, а количество n-грам должно быть как минимум на 1 больше допустимого числа ошибок. Но его тоже перестраивать достаточно долго, если объемы данных будут быстро возрастать.

то есть есть таблица имен
create table names (
id number primary key,
name1 varchar2(64),
name2 varchar2(64),
name3 varchar3(64)
);
таблица индексов
create table ngrams (
ngram varchar2(2 или 3),
ngram_pos int,
word_len int,
word_id int
word_signature int, -- это сигнатура слова, опционально
};
для ускорения поиска по таблице индексов:
create index ... on ngrams (ngram, ngram_pos, word_len)
4 фев 05, 03:09    [1299046]     Ответить | Цитировать Сообщить модератору
 Re: Нечеткий поиск по русским словам.  [new]
Victor Repetsky
Member

Откуда: Kiev
Сообщений: 12
Господа, а учитывает ли этот алгоритм то что при написании фамилий и имен ошибка а->о более вероятна чем а->б? По описанию мне показалось что нет. Думаю что привидение к какой-то фонетической нормальной форме могло бы еще улучшить результаты работы.
4 фев 05, 12:55    [1300031]     Ответить | Цитировать Сообщить модератору
 Re: Нечеткий поиск по русским словам.  [new]
Red Sand
Member

Откуда: Масква
Сообщений: 30
Для приведения слов к NF и работы с NF словарем есть серьезные продукты типа RCO, а у этих функций, ИМХО, несколько другие задачи.
4 фев 05, 15:12    [1300849]     Ответить | Цитировать Сообщить модератору
 Re: Нечеткий поиск по русским словам.  [new]
vivaldi
Guest
to vbegun
результат: запрос к таблице из 35000 записей с вычислением Levenshtein distance по трем столбцам выполнялся 41 секунду. Длина искомых строк (ФИО)= 9, 8, 9; средняя длина строк в таблице = 8, 6, 10. Это достаточно быстро для детерминированного нечеткого on-line поиска.
Говорят, лучших результатов можно добиться, построив индексированную таблицу n-грамм. Но у меня получается, что вставка значений в эту таблицу (обновление индекса при добавлении новых ФИО) будет занимать очень много времени (кол-во записей в таблице n-грамм= сумма по всем словам (длина слова / размер n-граммы), у меня получается 35000 * 3 * 5 = 525 000 записей).
Поскольку таблица с ФИО будет расти и дальше, не за горами тот день, когда размер таблицы индексов перевалит за миллион записей. Возможно, вставка новых значений в нее будет нетривиальной задачей из-за перестройки индекса при insert каждой строки.
В общем, если получится, что выгода все-таки есть, то результат опубликую тут. Если нет - аргументированно опишу все это здесь.


to Victor Repetsky

Вообще говоря, приведение к фонетически нормальной форме по вероятности появления ошибок дает положительный результат, НО ... с другими алгоритмами нечеткого поиска. В случае использования функции близости Левенштейна - никакого изменения не будет.
Приведенные здесь функции близости Хэмминга и Левенштейна считают число операций редактирования для получения одной строки из другой. И в случае а->о, и в случае а->б, результат будет = 1 (одна ошибка, или одна операция редактирования).
Единственное фонетическое корректирование, из всех что помню, которое может дать мизерный положительный эффект - предварительная замена ошибочных парных сочетаний символов, означающих один звук соответствующим символом (например 'тс'->'ц'). И эта мера поможет только в том случае, если такие ошибки встречаются ОЧЕНЬ часто. (Иначе только замедлит поиск, но не поцедуру подсчета функции близости).
4 фев 05, 16:27    [1301172]     Ответить | Цитировать Сообщить модератору
 Re: Нечеткий поиск по русским словам.  [new]
vivaldi
Guest
Red Sand
Для приведения слов к NF и работы с NF словарем есть серьезные продукты типа RCO, а у этих функций, ИМХО, несколько другие задачи.


RCO, судя по описанию, работает как раз именно с этими функциями близости плюс (и это важно!) готовыми индексированными словарями. Также там используются инвертированные индексы и кординаные структуры. И это не менее важно. Именно поэтому там достигнуты более высокие скорости поиска.

Согласен, продукт серьезный. Но мне, как я уже говорил, такого большого функционала не нужно. Многим и приведенных здесь функций близости будет более чем достаточно для того, чтобы найти в БД слова, похожие на искомое.
4 фев 05, 16:49    [1301273]     Ответить | Цитировать Сообщить модератору
 Re: Нечеткий поиск по русским словам.  [new]
Victor Repetsky
Member

Откуда: Kiev
Сообщений: 12
vivaldi
Вообще говоря, приведение к фонетически нормальной форме по вероятности появления ошибок дает положительный результат, НО ... с другими алгоритмами нечеткого поиска. В случае использования функции близости Левенштейна - никакого изменения не будет.
Приведенные здесь функции близости Хэмминга и Левенштейна считают число операций редактирования для получения одной строки из другой. И в случае а->о, и в случае а->б, результат будет = 1 (одна ошибка, или одна операция редактирования).

А если без нормальной формы считать по этому алгоритму для а->о не = 1, а, например = 0,5 , для а->б = 1 ? На досуге попробую проверить на своей базе.
4 фев 05, 16:50    [1301277]     Ответить | Цитировать Сообщить модератору
 Re: Нечеткий поиск по русским словам.  [new]
Red Sand
Member

Откуда: Масква
Сообщений: 30
vivaldi
...Согласен, продукт серьезный. Но мне, как я уже говорил, такого большого функционала не нужно. Многим и приведенных здесь функций близости будет более чем достаточно для того, чтобы найти в БД слова, похожие на искомое.


я это и имел в виду :)
4 фев 05, 17:45    [1301494]     Ответить | Цитировать Сообщить модератору
 Re: Нечеткий поиск по русским словам.  [new]
vbegun
Guest
vivaldi
to vbegun
результат: запрос к таблице из 35000 записей с вычислением Levenshtein distance по трем столбцам выполнялся 41 секунду. Длина искомых строк (ФИО)= 9, 8, 9; средняя длина строк в таблице = 8, 6, 10. Это достаточно быстро для детерминированного нечеткого on-line поиска.
Говорят, лучших результатов можно добиться, построив индексированную таблицу n-грамм. Но у меня получается, что вставка значений в эту таблицу (обновление индекса при добавлении новых ФИО) будет занимать очень много времени (кол-во записей в таблице n-грамм= сумма по всем словам (длина слова / размер n-граммы), у меня получается 35000 * 3 * 5 = 525 000 записей).
Поскольку таблица с ФИО будет расти и дальше, не за горами тот день, когда размер таблицы индексов перевалит за миллион записей. Возможно, вставка новых значений в нее будет нетривиальной задачей из-за перестройки индекса при insert каждой строки.
В общем, если получится, что выгода все-таки есть, то результат опубликую тут. Если нет - аргументированно опишу все это здесь.


Когда "здесь" и "тут" наполн[и|я]тся информацией с удовольствием прочитаю. :)

А пока отмечу, что я не говорил ни о каком индексе (хотя очевидно, что
использование комплексной индексации и изменение логики работы,
а, скорее всего написание абсолютно других функций для работы как с
иднексом так и для поиска, коренным образом изменит всё дело), я сказал
лишь о том что, цитирую:

"скорость работы этой функции можно увеличить". Когда я говорю
"этой" это означает -- вышеприведённый код, а не изменение алгоритма.

Hints (бесплатно, за плату есть RCO, как мы знаем):

  • из функции сделать пакет
  • все переменные сделать глобальными переменными в теле пакета
  • позаботиться о том чтобы всё "подчищалось" (разумно, если нужно) между
    вызовами
  • проверить что не происходит лишнего приведения типов (это out of scope
    этой дискуссии).

    Реализация этих (хотя бы 3-х) пунктов позволит экономнее исполользовать
    память (операции работы с памятью) между вызовами функции в SELECT
    выражении.

  • чуть поменять логику и ввести ограничение на величину отличия, чтобы
    не считать лишнего.
  • по-возможности использовать native compilation

    А так, я не верю что 41 секунда -- это быстро. "Быстро" -- это абстрактная
    величина. Быстро по сравнению с чем? Быстро потому что у тебя план доступа
    [не]использует FTS? Просто действительно интересно, как ты можешь сравнивать
    эту поделку с тем, что у тебя ещё вчера было? :)
    --
    Vladimir Begun
    The statements and opinions expressed here are my own and
    do not necessarily represent those of Oracle Corporation.

    P.S.: Кстати, студент, неужели Oracle дешевле RCO? :)
  • 5 фев 05, 00:01    [1302001]     Ответить | Цитировать Сообщить модератору
     Re: Нечеткий поиск по русским словам.  [new]
    vivaldi
    Guest
    vbegun


    P.S.: Кстати, студент, неужели Oracle дешевле RCO? :)



    Ничуть. Но его можно бесплатно скачать (http://www.oracle.com/technology/software/index.html) и поучиться.

    Сделал я поиск с помощью n-грамного индекса. Пока все очень сырое, но все же привожу алгоритм тут, как и обещал. Сразу скажу, что сам не в восторге от того, что получилось. Этот алгоритм поиска работает быстрее, чем поиск с вычислением расстояния Левенштайна в таких же условиях, но дает больше false-positive результатов. Плюс увеличиваются накладные расходы. Есть существенное ограничение реализации - слова для поиска с отличиями более чем в 2 слогах должны быть короче 9 символов. Иначе текст запроса для команды EXECUTE IMMEDIATE превысит 4000 символов. Любой желающий может проверить и сравнить работу алгоритмов сам. Приму любые советы.

    --Таблица индекса
    
    CREATE TABLE fsearch_index
        (ngram                         VARCHAR2(2) NOT NULL,
        ngram_pos                      NUMBER(2,0) NOT NULL,
        word_len                       NUMBER(2,0) NOT NULL,
        word_id                        NUMBER(38,0) NOT NULL,
        word_description               NUMBER(2,0))
    /
    
    --Таблица имен
    CREATE TABLE fsearch_names
        (ID                             NOT NULL  NUMBER(38,0)
        NAME                            NOT NULL  VARCHAR2(30)
        SUR_NAME                        NOT NULL  VARCHAR2(30)
        PATRON                                    VARCHAR2(30)
        BIRTHDAY                        NOT NULL  DATE)
    /
    
    
    CREATE OR REPLACE TYPE NUMSET_TYPE IS TABLE OF NUMBER;
    
    CREATE OR REPLACE PACKAGE fuzzy_search
      IS
        
        function find (
            p_string        in varchar2,
            p_description   in number,
            p_diff_count    in number
        )
        RETURN NUMSET_TYPE;
      
        procedure fill_fsearch_index;
    
    END; -- Package spec
    /
    
    
    
    CREATE OR REPLACE PACKAGE BODY fuzzy_search
      IS
    
    --Получить следующую возможную комбинацию слогов в слове
        function get_next_variant (
            p_n     in number,
            p_m     in number,
            p_x     in out NUMSET_TYPE
            )
        return number
        as
            v_i     number;
            v_res   number := 1;
        begin
            v_i := p_m;
            while v_i > 0 loop
                EXIT WHEN p_x(v_i) < p_n - (p_m - v_i);
                v_i := v_i - 1;
            end loop;
            if v_i > 0 then
                p_x(v_i) := p_x(v_i) + 1;
                v_i := v_i + 1;
                while v_i <= p_m loop
                    p_x(v_i) := p_x(v_i - 1) + 1;
                    v_i := v_i + 1;
                end loop;
            else
                v_res := 0;
            end if;
            return v_res;
        end get_next_variant;
    
    --разбить искомое слово на слоги и сформировать условия для поиска
        function format_query (
            p_string        in varchar2,
            p_str_len       in number,
            p_ngram_count   in number,
            p_diff_count    in number
        )
        return varchar2
        AS
            TYPE charset_t IS TABLE OF varchar2(30);
            v_ngrams1   charset_t := charset_t();
            v_ngrams2   charset_t := charset_t();
            v_ngr_index NUMSET_TYPE := NUMSET_TYPE();
            v_i         number;
            v_ret1      varchar2(4000) := null;
            v_ret2      varchar2(4000) := null;
            v_variants  number;
        BEGIN
            v_ngrams1.EXTEND;
            v_ngrams1(1) := 't1.ngram = ' || '''' || substr(p_string, 1, 2) || '''';
            v_ngrams2.EXTEND;
            v_ngrams2(1) := 't1.ngram = ' || '''' || substr(p_string, 1, 1) || '''';
            v_ngr_index.EXTEND;
            v_ngr_index(1) := 1;
            for v_i in 2 .. p_ngram_count
            loop
                v_ngr_index.EXTEND;
                v_ngr_index(v_i) := v_i;
                if v_i < p_ngram_count then
                    v_ngrams1.EXTEND;
                    v_ngrams1(v_i) := 't' || v_i || '.ngram = ' || '''' || substr(p_string, v_i * 2 - 1, 2) || '''';
                elsif mod(p_str_len, 2) = 1 then
                    v_ngrams1.EXTEND;
                    v_ngrams1(v_i) := 't' || v_i || '.ngram = ' || '''' || substr(p_string, v_i * 2 - 1, 2) || '''';
                else
                    v_ngrams1.EXTEND;
                    v_ngrams1(v_i) := null;
                end if;
                v_ngrams2.EXTEND;
                v_ngrams2(v_i) := 't' || v_i || '.ngram = ' || '''' || substr(p_string, (v_i - 1) * 2 , 2) || '''';
            end loop;
            v_variants := p_ngram_count - p_diff_count;
            loop
                v_ret1 := v_ret1 || '
    ( ';
                v_ret2 := v_ret2 || '
    ( ';
                for v_i in 1 .. v_variants
                loop
                    v_ret1 := v_ret1 || v_ngrams1(v_ngr_index(v_i));
                    v_ret2 := v_ret2 || v_ngrams2(v_ngr_index(v_i));
                    if v_i < v_variants then
                        if v_ngrams1(v_ngr_index(v_i + 1)) is not null then
                            v_ret1 := v_ret1 || ' AND ';
                        end if;
                        v_ret2 := v_ret2 || ' AND ';
                    end if;
                end loop;
                v_ret1 := v_ret1 || ' )';
                v_ret2 := v_ret2 || ' )';
                v_i := get_next_variant(p_ngram_count, v_variants, v_ngr_index);
                EXIT WHEN v_i = 0;
                v_ret1 := v_ret1 || '
    OR ';
                v_ret2 := v_ret2 || '
    OR ';
            end loop;
            if mod(p_str_len, 2) = 0 then
                v_ret2 := '
    (t' || p_ngram_count || '.ngram_pos = '|| p_ngram_count || ' AND (
    ' || v_ret2 || '
    ))';
            end if;
            return v_ret1 || '
    OR ' || v_ret2;
        END format_query;
    
    --составить запрос для поиска, выполнить его и вернуть результат
        function find (
            p_string        in varchar2,
            p_description   in number,
            p_diff_count    in number
        )
        RETURN NUMSET_TYPE
        AS
            v_str_len       number := NVL(length(p_string), 0);
            v_ngram_count   number := trunc(v_str_len / 2) + 1;
            v_select        varchar2(4000) := 'SELECT DISTINCT t1.word_id
    FROM ';
            v_where1         varchar2(4000) := '
    WHERE ';
            v_where2         varchar2(4000);
            v_where3         varchar2(4000);
            v_i             number;
            v_return        NUMSET_TYPE := NUMSET_TYPE();
        BEGIN
            if v_str_len = 0 then
                select distinct word_id bulk collect into v_return from fsearch_index;
                return v_return;
            end if;
            if p_diff_count = 0 then
                case p_description
                    when 1 then
                        select id bulk collect into v_return
                        from fsearch_names where name = p_string;
                    when 2 then
                        select id bulk collect into v_return
                        from fsearch_names where sur_name = p_string;
                    when 3 then
                        select id bulk collect into v_return
                        from fsearch_names where patron = p_string;
                    else return null;
                end case;
                return v_return;
            end if;
            if v_str_len <= p_diff_count * 2 then
                select distinct word_id
                bulk collect into v_return
                from fsearch_index
                where word_len <= v_str_len
                and word_description = p_description;
                return v_return;
            end if;
            v_where1 := v_where1 || '
    t1.word_len <= ' || v_ngram_count * 2;
            for v_i in 1 .. v_ngram_count
            loop
                   v_select := v_select || '
    fsearch_index t' || v_i;
                   if v_i < v_ngram_count then
                            v_select := v_select || ',';
                            v_where3 := v_where3 || '
    AND t' || v_i || '.word_id = t' || to_char(v_i + 1) || '.word_id';
                            v_where2 := v_where2 || '
    AND t' || v_i || '.ngram_pos = ' || v_i;
                   elsif mod(v_str_len, 2) = 1 then
                            v_where2 := v_where2 || '
    AND t' || v_i || '.ngram_pos = ' || v_i;
                   end if;
                   v_where1 := v_where1 || '
    AND t' || v_i || '.word_description = ' || p_description;
            end loop;
            v_where2 := v_where2 || '
    AND (' || format_query(p_string, v_str_len, v_ngram_count, p_diff_count) || '
    )';
            v_select := v_select || v_where1 || v_where2 || v_where3;
            execute IMMEDIATE v_select BULK COLLECT INTO v_return;
            RETURN v_return;
        END find;
    
        procedure fill_fsearch_index
        as
            v_max_len_name number;
            v_max_len_sur_name number;
            v_max_len_patron number;
            v_i number;
        begin
            delete from fsearch_index;
    
            select max(length(name))
            into v_max_len_name
            from fsearch_names;
            
            select max(length(sur_name))
            into v_max_len_sur_name
            from fsearch_names;
    
            select max(length(patron))
            into v_max_len_patron
            from fsearch_names;
    
            for v_i in 1 ..v_max_len_name
            loop
                if mod(v_i, 2) = 1 then
                    --(v_i+1)/2 -> íîìåð ñëîãà
                    insert into fsearch_index(
                         ngram,
                         ngram_pos,
                         word_len,
                         word_id,
                         word_description)
                    select
                        substr(name, v_i, 2) ngram,
                        (v_i + 1)/2 ngram_pos,
                        length(name) word_len,
                        id word_id,
                        1 word_description
                    from fsearch_names
                    where length(name) >= v_i;
                end if;
            end loop;
    
            for v_i in 1 ..v_max_len_sur_name
            loop
                if mod(v_i, 2) = 1 then
                    --(v_i+1)/2 -> íîìåð ñëîãà
                    insert into fsearch_index(
                         ngram,
                         ngram_pos,
                         word_len,
                         word_id,
                         word_description)
                    select
                        substr(sur_name, v_i, 2) ngram,
                        (v_i + 1)/2 ngram_pos,
                        length(sur_name) word_len,
                        id word_id,
                        2 word_description
                    from fsearch_names
                    where length(sur_name) >= v_i;
                end if;
            end loop;
    
            for v_i in 1 ..v_max_len_patron
            loop
                if mod(v_i, 2) = 1 then
                    --(v_i+1)/2 -> íîìåð ñëîãà
                    insert into fsearch_index(
                         ngram,
                         ngram_pos,
                         word_len,
                         word_id,
                         word_description)
                    select
                        substr(patron, v_i, 2) ngram,
                        (v_i + 1)/2 ngram_pos,
                        length(patron) word_len,
                        id word_id,
                        3 word_description
                    from fsearch_names
                    where length(patron) >= v_i;
                end if;
            end loop;
            
            commit;
        end fill_fsearch_index;
    
    END;
    /
    

    Это пример запроса с нечетким поиском.

    select * from (
    SELECT *
    FROM TABLE(fuzzy_search.find('ИВАН',1,1)) ids
    intersect 
    SELECT *
    FROM TABLE(fuzzy_search.find('ИВАНОВ',2,1)) ids
    intersect 
    SELECT *
    FROM TABLE(fuzzy_search.find('ИВАНОВИЧ',3,1)) ids
    ) ids
      JOIN fsearch_names ON (fsearch_names.id=ids.column_value)
    /
    
    

    можно повесить этот триггер на табличку fsearch_names, или периодически запускать fuzzy_search.fill_fsearch_index

    CREATE OR REPLACE TRIGGER fsearch_names_briud
     BEFORE INSERT OR UPDATE OR DELETE
     ON fsearch_names
    REFERENCING NEW AS NEW OLD AS OLD
     FOR EACH ROW
    BEGIN 
        if updating or deleting then
            delete from fsearch_index
            where word_id = :old.id;
        end if;
    
        if inserting or updating then
            for v_i in 1 .. length(:new.name)
            loop
                if mod(v_i, 2) = 1 then
                    insert into fsearch_index(
                         ngram,
                         ngram_pos,
                         word_len,
                         word_id,
                         word_description)
                    values(
                        substr(:new.name, v_i, 2),
                        (v_i + 1)/2,
                        length(:new.name),
                        :new.id,
                        1);
                end if;
            end loop;
    
            for v_i in 1 .. length(:new.sur_name)
            loop
                if mod(v_i, 2) = 1 then
                    insert into fsearch_index(
                         ngram,
                         ngram_pos,
                         word_len,
                         word_id,
                         word_description)
                    values(
                        substr(:new.sur_name, v_i, 2),
                        (v_i + 1)/2,
                        length(:new.sur_name),
                        :new.id,
                        2);
                end if;
            end loop;
    
            for v_i in 1 .. length(:new.patron)
            loop
                if mod(v_i, 2) = 1 then
                    insert into fsearch_index(
                         ngram,
                         ngram_pos,
                         word_len,
                         word_id,
                         word_description)
                    values(
                        substr(:new.patron, v_i, 2),
                        (v_i + 1)/2,
                        length(:new.patron),
                        :new.id,
                        3);
                end if;
            end loop;
        end if;
    END;
    /
    
    14 фев 05, 18:51    [1320526]     Ответить | Цитировать Сообщить модератору
     Re: Нечеткий поиск по русским словам.  [new]
    vbegun
    Guest
    vivaldi
    Любой желающий может проверить и сравнить работу алгоритмов сам. Приму любые советы.

    Детально в твой код не вглядывался:
  • Советы от Google -- это про "быстро" и 41 секунду
  • Cлишком много false-positives -- это не будет работать. И дело тут не в коде, а в алгоритме.
  • Длина в 4000 символов -- это не проблема (в общем случае см. dbms_sql)
  • Получаемые sql выражения могут быть очень дорогими в плане использования,
    поскольку они всегда разные (parsing и проч.) -- это немаштабируемое решение.
  • Справочник слогов однозначно бы упростил всё это дело
  • Поинт хранения пробелов непонятен, их вроде вообще никто не хранит
    --
    Vladimir Begun
    The statements and opinions expressed here are my own and
    do not necessarily represent those of Oracle Corporation.
  • 15 фев 05, 01:39    [1320897]     Ответить | Цитировать Сообщить модератору
    Между сообщениями интервал более 1 года.
     Re: Нечеткий поиск по русским словам.  [new]
    bug_scorobey
    Member

    Откуда: Москва
    Сообщений: 127
    Вот смотрю я на все ваши изыскания и оторопь берет

    раскинте мозгами . все алгоритмы реализуются в 3 строки sql кода
    процедура дедцбликации и того меньше

    не ленитесь
    За то что это так и работает отвечу
    4 июн 08, 15:12    [5759063]     Ответить | Цитировать Сообщить модератору
     Re: Нечеткий поиск по русским словам.  [new]
    valerytin
    Member [заблокирован]

    Откуда: Moscow
    Сообщений: 146
    bug_scorobey
    Вот смотрю я на все ваши изыскания и оторопь берет


    Берет, окаянная оторопь: prev post - аж 15 фев 05, 01:39 :-)))
    4 июн 08, 15:38    [5759250]     Ответить | Цитировать Сообщить модератору
     Re: Нечеткий поиск по русским словам.  [new]
    andreymx
    Member

    Откуда: Запорожье
    Сообщений: 54384
    bug_scorobey
    Вот смотрю я на все ваши изыскания и оторопь берет

    раскинте мозгами . все алгоритмы реализуются в 3 строки sql кода
    процедура дедцбликации и того меньше

    не ленитесь
    За то что это так и работает отвечу
    Я бы с интересом посмотрел на эту реализацию (спустя 3 года).
    4 июн 08, 16:08    [5759524]     Ответить | Цитировать Сообщить модератору
    Между сообщениями интервал более 1 года.
     Re: Нечеткий поиск по русским словам.  [new]
    saho
    Guest
    и еще +2.5 года :)
    28 ноя 10, 15:29    [9852258]     Ответить | Цитировать Сообщить модератору
     Re: Нечеткий поиск по русским словам.  [new]
    Светлый_Дайвер
    Member

    Откуда: Киев-братъ городов Руськихъ
    Сообщений: 903
    Эх, здорово что есть utl_match.jaro_winkler, только плохо что
    SELECT utl_match.jaro_winkler('Имя Отчество Фамилия','Фамилия Имя Отчество') FROM dual; 
    
    возвращает результат 0,79377990430622
    8 авг 11, 11:06    [11084294]     Ответить | Цитировать Сообщить модератору
     Re: Нечеткий поиск по русским словам.  [new]
    potss
    Member

    Откуда:
    Сообщений: 5
    Не могли бы вы помочь и мне с переводом процедуры на TSQL MSSQL sps!!!

    О расстояниях Левенштейна и Дамерау-Левенштейна можно почитать в википедии. Второе расстояние отличается тем, что учитываются не только замена, вставка и удаление символа, но и перестановка местами двух соседних символов. Ниже я написал код хранимой процедуры СУБД PostgreSQL на языке PlPgSQL. Она принимает две строки и максимальное расстояние между ними. Если расстояние больше заданного, функция возвращает FALSE, иначе TRUE.

    -- s1, s2 - сравниваемые строки, dist - максимальное расстояние между ними
    CREATE OR REPLACE FUNCTION damerau_levenshtein(s1 character varying, 
       s2 character varying, dist integer)
       RETURNS boolean AS
    $BODY$
            DECLARE
            s1_len INTEGER := CHAR_LENGTH(s1);
            s2_len INTEGER := CHAR_LENGTH(s2);
            i INTEGER := 0;
            j INTEGER := 0;
            c INTEGER := 0;
            prev INTEGER[]; -- первая строка
            curr INTEGER[]; -- вторая строка
            next INTEGER[]; -- третья строка
            s1_char TEXT[];
            s2_char TEXT[];
            temp INTEGER;
            st CHARACTER VARYING;
         BEGIN   
            IF s1 = s2 THEN RETURN TRUE;
            ELSIF abs(s1_len - s2_len) > dist THEN RETURN FALSE;
            ELSE
               -- для оптимизации объема используемой памяти О(m*n) -> O(min(m,n))
               IF s1_len < s2_len THEN 
                  temp := s2_len; 
                  s2_len := s1_len; 
                  s1_len := temp; 
                  st := s2;
                  s2 := s1;
                  s1 := st;
               END IF;
     
               -- увеличиваем высоту и ширину матрицы на 2
               s1_len := s1_len + 2;
               s2_len := s2_len + 2;
     
               curr[1] := s1_len + s2_len;
     
               FOR i IN 2..s2_len LOOP 
                  curr[i] := i-2;
                  s2_char[i] := substr(lower(s1), i-1, 1);
               END LOOP;
     
               FOR i IN 1..s1_len-1 LOOP 
                  s1_char[i+1] := substr(lower(s2), i, 1);
               END LOOP;  
     
               FOR i IN 3..s1_len LOOP
                  next[1] := s1_len + s2_len;
                  next[2] := i - 2;
                  FOR j IN 3..s2_len LOOP
                     -- находим цену замены символа
                     IF s1_char[i-1] = s2_char[j-1] THEN c := 0; ELSE c := 1; END IF;
                     -- считаем D(i,j) = next[j]
                     IF curr[j] <= next[j-1] THEN 
                        next[j] := curr[j] + 1; 
                     ELSE next[j] := next[j-1] + 1; END IF;
                     IF curr[j-1] + c < next[j] THEN 
                        next[j] := curr[j-1] + c; END IF;
                     IF s1_char[i-1] = s2_char[j-2] AND s1_char[i-2] = s2_char[j-1] 
                        AND next[j] > prev[j-2] + 1 THEN
                        next[j] := prev[j-2] + 1;
                     END IF;   
                     -- если значение диагонального или последующих краевых элементов
                     -- больше заданного расстояния, выходим с false
                     IF (i = j OR (i > s2_len AND j = s2_len)) AND next[i] > dist THEN
                        RETURN FALSE; END IF;
                  END LOOP;
     
                  for j IN 1..s2_len LOOP 
                  -- сдвигаем строки
                      prev[j] := curr[j];
                      curr[j] := next[j];
                  END LOOP;
               END LOOP;
     
                -- расстояние Дамерау-Левенштейна после работы функции оказывается 
                -- в ячейке next[s2_len]
                IF next[s2_len] <= dist THEN RETURN TRUE; ELSE RETURN FALSE; END IF;
            END IF;      
         END;
         $BODY$
      LANGUAGE plpgsql IMMUTABLE
    Использование процедуры:
    SELECT * FROM dictionary WHERE damerau_levenshtein(word, 'колбаса', 1)
    Вернет все слова из словаря, получаемые из слова колбаса одиночной заменой, пропуском, вставкой или перестановкой букв.
    25 авг 11, 10:50    [11175970]     Ответить | Цитировать Сообщить модератору
     Re: Нечеткий поиск по русским словам.  [new]
    env
    Member

    Откуда: Россия, Москва
    Сообщений: 6729
    potss,

    Да уж... Перевести процедуру с PlPgSQL на TSQL разумеется лучше всего попросить спецов по Oracle PL/SQL
    25 авг 11, 11:21    [11176265]     Ответить | Цитировать Сообщить модератору
     Re: Нечеткий поиск по русским словам.  [new]
    Serzzzh(C)
    Guest
    Понадобилась демка метода Q-грамм, может кому пригодится
    WITH word_to_check AS (SELECT UPPER('Масква' /*Слово для поиска*/) FROM dual),
         SearchIn_col AS (SELECT DISTINCT upper(city) AS Search_In  FROM address /*Select: Поле таблицы для поиска*/),
         b_sql AS 
          (SELECT Search_In, SUM(ocur) ocur FROM
             (SELECT o.*, c.*, CASE WHEN INSTR(Search_In, b)=0 THEN 0 ELSE 1 END  AS ocur
                        FROM (    
                              SELECT ROWNUM rn, SUBSTR((SELECT * FROM word_to_check), ROWNUM, 2 /*Колво Q-грамм в алгоритме*/) b
                                FROM all_objects o
                               WHERE ROWNUM < TRUNC(LENGTH((SELECT * FROM word_to_check)))
                             ) o,
                        SearchIn_col  c)
             GROUP BY Search_In
             ORDER BY 2 DESC) 
             
    SELECT (SELECT *
              FROM word_to_check)  AS search_for
          ,Search_In
          ,round(ocur / (LENGTH((SELECT *
                                  FROM word_to_check)) - 1) * 100
                ,2) AS "Rate, %"
      FROM b_sql
     WHERE ROWNUM < 4;
    


    Для лучшего результата конечно же надо удалять мусор (скобки, кавычки, знаки препинания) перед поиском.
    30 май 12, 18:25    [12639677]     Ответить | Цитировать Сообщить модератору
     Re: Нечеткий поиск по русским словам.  [new]
    proturborandza
    Member

    Откуда:
    Сообщений: 7
    Может кому надо (вдруг, как у меня, нет возможности регистрировать библиотеки на сервере), переписал в виде функции на T-SQL:
    USE [MyDatabase]
    GO
    /****** Object:  UserDefinedFunction [dbo].[ufn_JaroWinkler]    Script Date: 06/29/2012 12:54:27 ******/
    SET ANSI_NULLS ON
    GO
    SET QUOTED_IDENTIFIER ON
    GO
    
    -- =============================================
    -- Author:		proturborandza
    -- Create date: 2012-06-28
    -- Description:	Jaro Winkler distance counter function. 
    -- Will be used for fuzzy logic comparison of two strings, like equipment serial numbers.
    -- =============================================
    CREATE FUNCTION [dbo].[ufn_JaroWinkler] 
    (
    	-- Add the parameters for the function here
    	@s1 nvarchar(100),
    	@s2 nvarchar(100)
    )
    RETURNS numeric(11,10)
    AS
    BEGIN
    	DECLARE @Result numeric(11,10)
    
    	declare @prmKeyword as varchar(100), @prmCompareTo as varchar(100) 
    	declare @iProximity As int -- set the number of adjacent characters to compare to
    	declare @i As int
    	declare @x As int
    	declare @iFrom As int
    	declare @iTo As int
    	declare @iMatchCharacters As int
    	declare @iTransposeCount As int
    	declare @iJaro As numeric(20, 10)
    	declare @JaroWinkler As numeric(20, 10)
    
    if len(@s1) <> 0 and len(@s2) <> 0 
    	
    begin
    	if len(@s1) > len(@s2)
    	begin
    		set @prmKeyword = @s1
    		set @prmCompareTo = @s2
    	end
    	else 
    	begin  
    		set @prmKeyword = @s2 
    		set @prmCompareTo = @s1
    	end
    end
    
    set @iProximity = 0
    set @i = 0
    set @x = 0
    set @iFrom = 0
    set @iTo = 0
    set @iMatchCharacters = 0
    set @iTransposeCount = 0
    set @iJaro = 0
    set @JaroWinkler = 0
    set @prmCompareTo = UPPER(lTrim(rtrim(@prmCompareTo)))
    set @prmKeyword = UPPER(lTrim(rtrim(@prmKeyword)))
    If @prmCompareTo <> @prmKeyword 
    begin -- check if the two words are the same
    	If charindex(@prmCompareTo, @prmKeyword) <= 0 
    	begin
    		set @iProximity = (Len(@prmKeyword)/ 2) - 1-- compute for the proximity of character checking allows matching characters to be up to X number of characters away.
    		set @i = 1 
    		while @i <= Len(@prmKeyword)
    		begin -- this is the index of the character to be compared to
    			set @iTo = (@i + @iProximity) - 1
    			-- get the left most side character based on the @iProximity
    			If @i <= @iProximity 
    				set @iFrom = 1
    			Else
    				set @iFrom = @i - @iProximity + 1
    
    			-- start the letter by letter comparison
    			set @x = @iFrom
    			while @x <= @iTo
    			begin
    				If Substring(@prmKeyword, @i, 1) = Substring(@prmCompareTo, @x, 1) 
    				begin
    					If @i = @x 
    					begin
    						set @iMatchCharacters = @iMatchCharacters + 1
    						break
    					End
    					set @iMatchCharacters = @iMatchCharacters + 1
    					set @iTransposeCount = @iTransposeCount + 1
    					break
    				End
    			set @x = @x +1
    			Continue
    			end
    		set @i = @i +1
    		Continue
    		end
    		set @iTransposeCount = cast(@iTransposeCount / 2 as int)
    		If @iMatchCharacters > 0 
    		begin
    			set @x = 0
    			set @i = 1
    			while @i <= 4
    			begin
    				If Substring(@prmKeyword, @i, 1) = Substring(@prmCompareTo, @i, 1) 
    					set @x = @x + 1
    				Else
    					break
    			set @i = @i +1
    			Continue
    			end
    			set @iJaro = ((cast(@iMatchCharacters as numeric(20,10)) / cast(Len(@prmKeyword) as numeric(20,10))) 
    						+ (cast(@iMatchCharacters as numeric(20,10)) / cast(Len(@prmCompareTo) as numeric(20,10))) 
    						+((cast(@iMatchCharacters as numeric(20,10)) - cast(@iTransposeCount as numeric(20,10))) 
    						/  cast(@iMatchCharacters as numeric(20,10)))) / 3
    			If @x > 0 
    				set @JaroWinkler = @iJaro + 0.1 * @x * (1 - @iJaro)
    			Else
    				set @JaroWinkler = @iJaro
    		end
    		Else
    			set @JaroWinkler = 0
    	end
    	Else -- return 1 result if the keyword is within the search string
    		set @JaroWinkler = 1		
    end
    Else -- return a 1 result if the string are the same
    	set @JaroWinkler = 1
    	set @result = @JaroWinkler
    	RETURN @Result
    END
    
    29 июн 12, 12:56    [12793914]     Ответить | Цитировать Сообщить модератору
     Re: Нечеткий поиск по русским словам.  [new]
    Я PL\SQL
    Guest
    -- sCity - имя введенного города, sDicCity - имя города из словаря, iPermutation - количество изменений(растоянние Левенштейна)
    CREATE OR REPLACE function TWA.Damerau_Levenshtein(sCity1 varchar2, sDicCity1 varchar2, iPermutation integer)
      return varchar2 as -- подходит или нет
      iLenCity     INTEGER := LENGTH(sCity1);
      iLensDicCity INTEGER := LENGTH(sDicCity1);
            i INTEGER := 0;
            j INTEGER := 0;
            c INTEGER := 0;
            TYPE ArrInt IS TABLE OF INTEGER  INDEX BY BINARY_INTEGER;
    
            TYPE ArrChr IS TABLE OF CHAR  INDEX BY BINARY_INTEGER;
            cCity    ArrChr;
            cDicCity ArrChr;
            iTemp INTEGER;
            st varchar2(128);
             sDicCity varchar2(128) :=sDicCity1;
              sCity varchar2(128):=sCity1;
    BEGIN 
      IF sCity = sDicCity THEN RETURN 'TRUE';
      ELSIF abs(iLenCity - iLensDicCity) > iPermutation THEN RETURN 'FALSE';
      ELSE
        IF iLenCity < iLensDicCity THEN 
          iTemp := iLensDicCity; 
          iLensDicCity := iLenCity; 
          iLenCity := iTemp; 
          st := sDicCity;
          sDicCity := sCity;
          sCity := st;
        END IF;
        
         -- увеличиваем высоту и ширину матрицы на 2
        iLenCity := iLenCity + 2;
        iLensDicCity := iLensDicCity + 2;
        iCurrLine(1) := iLenCity + iLensDicCity;
        
        FOR i IN 2..iLensDicCity LOOP 
          iCurrLine(i) := i-2;
          cDicCity(i) := substr(lower(sCity), i-1, 1);
        END LOOP;
    
        FOR i IN 1..iLenCity-1 LOOP 
          cCity(i+1) := substr(lower(sDicCity), i, 1);
        END LOOP; 
        
        FOR i IN 3..iLenCity LOOP
          iNextLine(1) := iLenCity + iLensDicCity;
          iNextLine(2) := i - 2;
            FOR j IN 3..iLensDicCity LOOP
            -- находим цену замены символа
              IF cCity(i-1) =  cDicCity(j-1) 
                  THEN  c := 0; 
                  ELSE   c := 1; 
              END IF;
            -- считаем D(i,j) = next[j]
              IF iCurrLine(j) <= iNextLine(j-1) 
              THEN  
                iNextLine(j) := iCurrLine(j) + 1; 
              ELSE iNextLine(j) := iNextLine(j-1) + 1; 
              END IF;
              
              IF iCurrLine(j-1) + c < iNextLine(j) 
              THEN  
                iNextLine(j) := iCurrLine(j-1) + c; 
              END IF;
              
              IF  cCity(i-1) = cDicCity(j-2) AND 
                  cCity(i-2) = cDicCity(j-1) AND 
                  iNextLine(j) > iPrevLine(j-2) + 1 
              THEN  
                iNextLine(j) := iPrevLine(j-2) + 1;
              END IF;   
              -- если значение диагонального или последующих краевых элементов
              -- больше заданного расстояния, выходим с false
              IF (i = j OR (i > iLensDicCity AND j = iLensDicCity)) AND iNextLine(i) > iPermutation THEN
                RETURN 'FALSE'; 
              END IF;
            END LOOP;
       
            FOR j IN 1..iLensDicCity LOOP 
            -- сдвигаем строки
              iPrevLine(j) := iCurrLine(j);
              iCurrLine(j) := iNextLine(j);
            END LOOP;    
        END LOOP;
          -- расстояние Дамерау-Левенштейна после работы функции оказывается 
          -- в ячейке next[iLensDicCity]
          IF iNextLine(iLensDicCity) <= iPermutation THEN 
            RETURN 'TRUE'; 
          ELSE RETURN 'FALSE'; 
        END IF;
      END IF;  
    END Damerau_Levenshtein;
    /
    
    11 дек 12, 10:46    [13610962]     Ответить | Цитировать Сообщить модератору
    Все форумы / Oracle Ответить