**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.
const String & string, TokenExtractorPtr token_extractor, BloomFilter & bloom_filter)
{
    const char * data = string.data();
    size_t size = string.size();
    size_t cur = 0;
    size_t token_start = 0;
    size_t token_len = 0;
    while (cur < size && token_extractor->nextInField(data, size, &cur, &token_start, &token_len))
        bloom_filter.add(data + token_start, token_len);
}
static void columnToBloomFilter(
    const char * data, size_t size, TokenExtractorPtr token_extractor, BloomFilter & bloom_filter)
{
    size_t cur = 0;
    size_t token_start = 0;
    size_t token_len = 0;
    while (cur < size && token_extractor->nextInColumn(data, size, &cur, &token_start, &token_len))
        bloom_filter.add(data + token_start, token_len);
}
static void likeStringToBloomFilter(
    const String & data, TokenExtractorPtr token_extractor, BloomFilter & bloom_filter)
{
    size_t cur = 0;
    String token;
    while (cur < data.size() && token_extractor->nextLike(data, &cur, token))
        bloom_filter.add(token.c_str(), token.size());
}
bool MergeTreeConditionFullText::createFunctionEqualsCondition(