为小巧的查询清理和准备文本搜索字符串

Sanitizing and preparation of text search string for dapper query

我们在 postgresql 中有一个 table 以下结构(简化):

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 字段进行全文搜索。没有像 EntifyFramework.

这样的完整 ORM 使用

Dapper,当前的 query/logic 如下所示:

    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;
    }

但不幸的是,它没有 return 任何结果。也不与 to_tsquery('simple', '@SearchParam').

总的来说我只是想知道如何解决这个问题。如何使用 Dapper 清理全文搜索的字符串。如果用户开始在查询中传递 :,.%&*,如果保持原样,我希望我的代码开始失败。我是否应该 prohibit/filter 从用户输入中删除所有特殊字符?问题是我不知道我应该过滤掉什么..

编辑: to_tsquery('simple', @SearchParam) 似乎确实有效,如果我在搜索之前手动格式化搜索字符串。所以基本上我遇到了和以前一样的问题。如果字符串格式不正确,则会抛出 sql 异常。因此您必须提前了解并应用所有 formatting/sanitization 规则,这样查询才不会失败。所以我对如何处理这种情况仍然有同样的疑问。

虽然没有答案,但它发现用像这样的正则表达式替换 non-supported 个字符“有点可行”:

    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 个方法 class。

这当然会从初始输入字符串中删除相当多的信息,这正是我想避免做的事情。似乎还有另一种 full-text 搜索解决方案,使用 GISTGIN 索引在这里解释 (Postgresql prefix wildcard for full text)。对于给定的 use-case.

它可能会更好

我决定保留解决方案原样(如果没有更好的答案)。

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);
        }
    }