**Use case**

I want to estimate better parameters for fulltext (`ngrambf_v1`) index.

**Describe the solution you'd like**

Implement it as SQL function:

`ngrams(string, N)`

that returns array of FixedString.

**Describe alternatives you've considered**

It can be done with less efficient and more cumbersome way:

```
WITH 3 AS n, extractAll(text, '.') AS chars 
SELECT 
  arrayMap(x -> arrayStringConcat(x), 
    arrayFilter(x -> length(x) = n, 
      arrayMap(x, i -> arraySlice(chars, i, n), 
        chars, arrayEnumerate(chars)))) AS ngrams
```

**Additional context**

`ngramsUTF8` can also be implemented to extract ngrams of Unicode code points.
{
public:
    void SetUp() override
    {
        const auto & param = GetParam();
        const auto & source = param.source;
        data = std::make_unique<PaddedPODArray<char>>(source.data(), source.data() + source.size());
        
        const char extra_padding[] = "this is the end \xd1\x8d\xd1\x82\xd0\xbe\xd0\xba\xd0\xbe \xd0\xbd\xd0\xb5\xd1\x86";
        data->insert(data->end(), std::begin(extra_padding), std::end(extra_padding));
        data->resize(data->size() - sizeof(extra_padding));
    }
    std::unique_ptr<PaddedPODArray<char>> data;
};
TEST_P(SplitTokenExtractorTest, next)
{
    const auto & param = GetParam();
    SplitTokenExtractor token_extractor;
    size_t i = 0;
    size_t pos = 0;
    size_t token_start = 0;
    size_t token_len = 0;
    for (const auto & expected_token : param.tokens)
    {
        SCOPED_TRACE(++i);
        ASSERT_TRUE(token_extractor.nextInColumn(data->data(), data->size(), &pos, &token_start, &token_len));
        EXPECT_EQ(expected_token, std::string_view(data->data() + token_start, token_len))
                << " token_start:" << token_start << " token_len: " << token_len;
    }
    ASSERT_FALSE(token_extractor.nextInColumn(data->data(), data->size(), &pos, &token_start, &token_len))