Дезинфекция и подготовка строки текстового поиска для удобного запроса

У нас есть таблица следующей структуры в postgresql (упрощенная):

CREATE TABLE items(
    id bigint NOT NULL DEFAULT nextval('item_id_seq') PRIMARY KEY,
    name varchar(40) not null,
    name_search tsvector GENERATED ALWAYS AS (to_tsvector('simple', name)) STORED
);

Используя C# в качестве языка программирования, я хотел бы выполнить полнотекстовый поиск в поле name_search. Без полного ORM, как EntifyFramework.

Dapper используется, и текущий запрос/логика выглядит так:

    public async Task<IEnumerable<FeedItemInDb>> GetItems(string searchTerm)
    {
        string searchFilter = "";
        if (!string.IsNullOrEmpty(searchTerm))
        {
            string searchTermProcessed = $"{searchTerm}:*";
            searchTermProcessed = searchTermProcessed.Replace(" ", " & ");

            searchFilter = $"AND i.name_search @@ to_tsquery('simple', '{searchTermProcessed}')";
        }

        var results = await uow.Connection.QueryAsync<FeedItemInDb>($@"select i.* FROM items i WHERE 1=1 {searchFilter}", new
        {
            // params here
        });

        return results;
    }

Это работает нормально для очень тривиальных случаев. Например, строка поиска my test преобразуется в my & test:*

В этом подходе есть один главный недостаток - вы должны заранее знать все правила санации, необходимые для запроса! Например, следующий необработанный ввод my :*test заканчивается исключением:

Npgsql.PostgresException: 42601: syntax error in tsquery: "my & :test:"

Мне было интересно, есть ли в Dapper какой-то пакет, правила или код, которые выполняли бы всю необходимую работу по дезинфекции за меня? Аналогично тому, как мы можем параметризовать другие значения в запросе...

Обычно, используя Dapper, я ожидаю, что код будет выглядеть так:

    public async Task<IEnumerable<FeedItemInDb>> GetItems(string searchTerm)
    {

        var results = await uow.Connection.QueryAsync<FeedItemInDb>($@"select i.* FROM items i
    WHERE i.name_search @@ to_tsquery('simple', '@SearchParam:*')", new
        {
            SearchParam = searchTerm
        });

        return results;
    }

Но он не возвращает никаких результатов, к сожалению. Ни с to_tsquery('simple', '@SearchParam').

В общем, я просто хочу знать, как решить эту проблему. Как очистить строку для полнотекстового поиска с помощью Dapper. Если пользователь начнет передавать :,.%&* в запросе, я ожидаю, что мой код начнет давать сбой, если оставить его как есть. Должен ли я запретить/отфильтровать все специальные символы из пользовательского ввода? Проблема в том, что я понятия не имею, что я должен фильтровать.

Обновлено:to_tsquery('simple', @SearchParam), похоже, работает, если я отформатирую строку поиска вручную перед поиском. Так что, по сути, у меня та же проблема, что и раньше. Если строка неправильно отформатирована, то выдается исключение sql. Поэтому вы должны знать и применять заранее все правила форматирования/санации, чтобы запрос не завершился ошибкой. Так что у меня все тот же вопрос о том, как справиться с этой ситуацией.

plainto_tsquery('simple',...) подготовит для вас запрос. Он отбрасывает знаки препинания и соединяет слова амперсандами. Он не добавляет :* к какому-то случайному слову, но делать это в любом случае не имеет особого смысла, поэтому при отсутствии объяснения того, почему вы этого хотите, вероятно, хорошо, что этого не происходит.
jjanes 18.03.2022 16:16
ReactJs | Supabase | Добавление данных в базу данных
ReactJs | Supabase | Добавление данных в базу данных
Это и есть ваш редактор таблиц в supabase.👇
Понимание Python и переход к SQL
Понимание Python и переход к SQL
Перед нами лабораторная работа по BloodOath:
0
1
30
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

Хотя ответа нет, он счел «несколько выполнимым» просто заменить неподдерживаемые символы регулярным выражением, например:

    private static string CorrectInputString(string input)
    {
        string result = input?.Trim();

        if (!string.IsNullOrEmpty(result))
        {
            // remove newlines and tabs
            result = Regex.Replace(result, @"\t|\n|\r", "");

            // remove not-supported characters (supported are: numbers, regular letters, hyphens, spaces)
            result = Regex.Replace(result, "[^\p{L}0-9- ]", "");

            // remove double spaces (also trims)
            result = string.Join(" ", result.Split(' ', StringSplitOptions.RemoveEmptyEntries));
        }

        return result;
    }

потом

    public static string PrepareSearchString(string input)
    {
        string result = input?.Trim();

        if (!string.IsNullOrEmpty(result))
        {
            // prepare query #1
            result = $"{result}:*";

            // prepare query #2
            result = result.Replace(" ", "&");
        }

        return result;
    }

Я использую 2 из этих методов последовательно во вспомогательном классе.

Это, конечно, удаляет довольно много информации из исходной входной строки, чего я хотел избежать. Похоже, есть другое решение для полнотекстового поиска с использованием индексов GIST и GIN, описанное здесь (Подстановочный знак префикса Postgresql для полного текста). Это может работать лучше для данного варианта использования.

Я решил оставить решение как есть (если нет лучшего ответа).

p.s. вот unit-test, который охватывает все виды преобразований:

    [TestMethod]
    public async Task Sanitize_DisplayName()
    {
        var sourceExpected = new List<(string source, string expected)>()
        {
            ("1", "1"),
            ("test", "test"),
            ("test1", "test1"),
            ("test 1", "test 1"),
            ("test-1", "test-1"),
            ("test-test", "test-test"),
            ("test-test test", "test-test test"),
            ("1 test-TEST test", "1 test-TEST test"),
            ("тест", "тест"),
            ("  тест", "тест"),
            (@"
        тест", "тест"), // ENTER here!
            ("test*/!#%^()[]{}", "test"),
            ("te st*/", "te st"),
            ("te  st*/", "te st"),
            ("te   st*/", "te st"),
            ("\t te    st */", "te st"),
            (" te     st */", "te st"),
            ("test ? &", "test"),
            ("test ? &-", "test -"), // !
        };

        foreach (var item in sourceExpected)
        {
            var res = SqlHelper.CorrectInputString(item.source);

            Assert.AreEqual(item.expected, res);
        }
    }

Другие вопросы по теме