Аналогичные строки UTF-8 для поля автозаполнения

Фон

Пользователи могут вводить имя, и система должна соответствовать тексту, даже если пользовательский ввод или поле базы данных содержат символы с акцентом (UTF-8). Это использует модуль pg_trgm.

Проблема

Код напоминает следующее:

  SELECT
    t.label
  FROM
    the_table t
  WHERE
    label % 'fil'
  ORDER BY
    similarity( t.label, 'fil' ) DESC

Когда пользователь набирает fil, запрос соответствует filbert, но не filé powder. (Из-за акцентированного характера?)

Неудачное решение №1

Я попытался реализовать функцию unaccent и переписать запрос как:

  SELECT
    t.label
  FROM
    the_table t
  WHERE
    unaccent( label ) % unaccent( 'fil' )
  ORDER BY
    similarity( unaccent( t.label ), unaccent( 'fil' ) ) DESC

Это возвращает только filbert.

Неудачное решение №2

Как было предложено:

CREATE EXTENSION pg_trgm;
CREATE EXTENSION unaccent;

CREATE OR REPLACE FUNCTION unaccent_text(text)
  RETURNS text AS
$BODY$
  SELECT unaccent($1); 
$BODY$
  LANGUAGE sql IMMUTABLE
  COST 1;

Все остальные индексы в таблице были удалены. Тогда:

CREATE INDEX label_unaccent_idx 
ON the_table( lower( unaccent_text( label ) ) );

Это возвращает только один результат:

  SELECT
    t.label
  FROM
    the_table t
  WHERE
    label % 'fil'
  ORDER BY
    similarity( t.label, 'fil' ) DESC

Вопрос

Каков наилучший способ переписать запрос, чтобы гарантировать, что оба результата будут возвращены?

Спасибо!

Похожие

http://wiki.postgresql.org/wiki/What%27s_new_in_PostgreSQL_9.0#Unaccent_filtering_dictionary

http://postgresql.1045698.n5.nabble.com/index-refuses-to-build-td5108810.html

Ответ 1

Вы не используете операторский класс, предоставляемый модулем pg_trgm. Я бы создал такой индекс:

CREATE INDEX label_Lower_unaccent_trgm_idx
ON test_trgm USING gist (lower(unaccent_text(label)) gist_trgm_ops);

Первоначально у меня был индекс GIN, но позже я узнал, что GiST, вероятно, даже лучше подходит для такого типа запросов, потому что он может возвращать значения, отсортированные по подобию. Подробнее:

Ваш запрос должен соответствовать выражению индекса, чтобы иметь возможность использовать его.

SELECT label
FROM   the_table
WHERE  lower(unaccent_text(label)) % 'fil'
ORDER  BY similarity(label, 'fil') DESC -- it ok to use original string here

Однако "filbert" и "filé powder" на самом деле не очень похожи на "fil" в соответствии с оператором%. Я подозреваю, что вы действительно этого хотите:

SELECT label
FROM   the_table
WHERE  lower(unaccent_text(label)) ~~ '%fil%'
ORDER  BY similarity(label, 'fil') DESC -- it ok to use original string here

Здесь будут найдены все строки, содержащие строку поиска, и сначала отсортируйте лучшие совпадения в соответствии с оператором %.

И сочная часть: выражение может использовать индекс GIN или GiST, поскольку PostgreSQL 9.1! Я цитирую руководство на pu_trgm moule:

Начиная с PostgreSQL 9.1, эти типы индексов также поддерживают индекс ищет LIKE и ILIKE, например


Если вы действительно использовали оператор %:

Вы пробовали понизить порог для оператора подобия % с помощью set_limit():

SELECT set_limit(0.1);

или даже ниже? Значение по умолчанию - 0,3. Просто, чтобы узнать, является ли его порог, который фильтрует дополнительные совпадения.

Ответ 2

Решение для PostgreSQL 9.1:

-- Install the requisite extensions.
CREATE EXTENSION pg_trgm;
CREATE EXTENSION unaccent;

-- Function fixes STABLE vs. IMMUTABLE problem of the unaccent function.
CREATE OR REPLACE FUNCTION unaccent_text(text)
  RETURNS text AS
$BODY$
  -- unaccent is STABLE, but indexes must use IMMUTABLE functions.
  SELECT unaccent($1); 
$BODY$
  LANGUAGE sql IMMUTABLE
  COST 1;

-- Create an unaccented index.
CREATE INDEX the_table_label_unaccent_idx
ON the_table USING gin (lower(unaccent_text(label)) gin_trgm_ops);

-- Define the matching threshold.
SELECT set_limit(0.175);

-- Test the query (matching against the index expression).
SELECT
  label
FROM
  the_table
WHERE
  lower(unaccent_text(label)) % 'fil'
ORDER BY
  similarity(label, 'fil') DESC 

Возвращает "filbert", "fish fillet" и "filé powder".

Без вызова SELECT set_limit(0.175); вы можете использовать оператор double tilde (~~):

-- Test the query (matching against the index expression).
SELECT
  label
FROM
  the_table
WHERE
  lower(unaccent_text(label)) ~~ 'fil'
ORDER BY
  similarity(label, 'fil') DESC 

Также возвращает "filbert", "fish fillet" и "filé powder".