У нас есть таблица следующей структуры в 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. Поэтому вы должны знать и применять заранее все правила форматирования/санации, чтобы запрос не завершился ошибкой. Так что у меня все тот же вопрос о том, как справиться с этой ситуацией.


Хотя ответа нет, он счел «несколько выполнимым» просто заменить неподдерживаемые символы регулярным выражением, например:
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);
}
}
plainto_tsquery('simple',...)подготовит для вас запрос. Он отбрасывает знаки препинания и соединяет слова амперсандами. Он не добавляет:*к какому-то случайному слову, но делать это в любом случае не имеет особого смысла, поэтому при отсутствии объяснения того, почему вы этого хотите, вероятно, хорошо, что этого не происходит.