Repository: devind-team/graphene-django-filter (language: python)

Issue:
Full text search
Django provides [the API](https://docs.djangoproject.com/en/4.0/ref/contrib/postgres/search/) for PostgreSQL full text search. It would be convenient to expose this API to the graphql filter level. The additional lookup with the name `fullsearch` can be added for implementation. For instance
```python
import graphene
from graphene_django_filter import AdvancedFilterSet
from graphene_django import DjangoObjectType

class TaskFilter(AdvancedFilterSet)
    class Meta:
        model = Task
        fields = {
            'name': ('exact', 'contains', 'fullsearch')
        }
 
class UserType(DjangoObjectType):
    class Meta:
        model = User
        interfaces = (graphene.relay.Node,)
        fields = '__all__'
        filter_fields = {
           'name': ('exact', 'contains', 'fullsearch')
        }
```
One of the full search queries could look like this.
```graphql
{
  tasks(
    filter: {
      name: {
        trigramSimilarity: {
          gt: 0.8
          value: "Important"
        }
      }
    }
  ){
    edges {
      node {
        id
        name
      }
    }
  }
}
```


Discussion / hints:

Fix patch:
diff --git a/.coveragerc b/.coveragerc
new file mode 100644
index 0000000..e540073
--- /dev/null
+++ b/.coveragerc
@@ -0,0 +1,4 @@
+[run]
+branch = True
+source =
+    graphene_django_filter
diff --git a/.flake8 b/.flake8
index c86bb03..fec084b 100644
--- a/.flake8
+++ b/.flake8
@@ -4,6 +4,8 @@ per-file-ignores =
     __init__.py:F401
     connection_field.py:A002
     test_filterset.py:N802
+    filters.py:A003
+    0001_initial.py:D100,D101,D104
 ignore = ANN002,ANN003,ANN101,ANN102,D106,D107
 
 import-order-style = pycharm
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 431e0c4..f2cc5f6 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -15,11 +15,14 @@ jobs:
     name: Lint
     runs-on: ubuntu-latest
     steps:
-      - uses: actions/checkout@v2
-      - uses: actions/setup-python@v2
+      - name: Checkout
+        uses: actions/checkout@v2
+      - name: Install Python
+        uses: actions/setup-python@v2
         with:
-          python-version: 3.6
-      - uses: Gr1N/setup-poetry@v7
+          python-version: 3.7
+      - name: Setup Poetry
+        uses: Gr1N/setup-poetry@v7
       - name: Install dependencies
         run: poetry install
       - name: Run linter
@@ -30,22 +33,48 @@ jobs:
     needs: [lint]
     runs-on: ubuntu-latest
     steps:
-      - uses: actions/checkout@v2
-      - uses: actions/setup-python@v2
+      - name: Checkout
+        uses: actions/checkout@v2
+      - name: Install Python
+        uses: actions/setup-python@v2
         with:
-          python-version: 3.6
-      - uses: Gr1N/setup-poetry@v7
+          python-version: 3.7
+      - name: Setup Poetry
+        uses: Gr1N/setup-poetry@v7
       - name: Install dependencies
-        run: poetry install --no-dev
+        run: poetry install
       - name: Run tests
         run: poetry run python manage.py test
 
+  coverage:
+    name: Coverage
+    needs: [test]
+    runs-on: ubuntu-latest
+    steps:
+      - name: Checkout
+        uses: actions/checkout@v2
+      - name: Install Python
+        uses: actions/setup-python@v2
+        with:
+          python-version: 3.7
+      - name: Setup Poetry
+        uses: Gr1N/setup-poetry@v7
+      - name: Install dependencies
+        run: poetry install
+      - name: Coverage
+        run: poetry run coverage run manage.py test
+      - name: Coveralls
+        env:
+          COVERALLS_REPO_TOKEN: ${{ secrets.COVERALLS_REPO_TOKEN }}
+        run: poetry run coveralls
+
   release:
     name: Release
-    needs: [test]
+    needs: [coverage]
     runs-on: ubuntu-latest
     steps:
-      - uses: actions/checkout@v2
+      - name: Checkout
+        uses: actions/checkout@v2
         with:
           fetch-depth: 0
       - name: Semantic Release
diff --git a/.gitignore b/.gitignore
index 485dee6..4fa8b77 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,1 +1,2 @@
 .idea
+tests/.env
diff --git a/README.md b/README.md
index fa5ff18..644b82c 100644
--- a/README.md
+++ b/README.md
@@ -1,10 +1,15 @@
 # Graphene-Django-Filter
-[![CI](https://github.com/devind-team/graphene-django-filter/workflows/CI/badge.svg)](https://github.com/devind-team/graphene-django-filter/actions) [![PyPI version](https://badge.fury.io/py/graphene-django-filter.svg)](https://badge.fury.io/py/graphene-django-filter)
+[![CI](https://github.com/devind-team/graphene-django-filter/workflows/CI/badge.svg)](https://github.com/devind-team/graphene-django-filter/actions)
+[![PyPI version](https://badge.fury.io/py/graphene-django-filter.svg)](https://badge.fury.io/py/graphene-django-filter)
 
-This package contains advanced filters for [graphene-django](https://github.com/graphql-python/graphene-django). The standard filtering feature in graphene-django relies on the [django-filter](https://github.com/carltongibson/django-filter) library and therefore provides the flat API without the ability to use logical operators such as `and`, `or` and `not`. This library makes the API nested and adds logical expressions by extension of the `DjangoFilterConnectionField` field and the `FilterSet` class.
-
-# Install
+This package contains advanced filters for [graphene-django](https://github.com/graphql-python/graphene-django).
+The standard filtering feature in graphene-django relies on the [django-filter](https://github.com/carltongibson/django-filter)
+library and therefore provides the flat API without the ability to use logical operators such as
+`and`, `or` and `not`. This library makes the API nested and adds logical expressions by extension
+of the `DjangoFilterConnectionField` field and the `FilterSet` class.
+Also, the library provides some other convenient filtering features.
 
+# Installation
 ```shell
 # pip
 pip install graphene-django-filter
@@ -13,18 +18,24 @@ poetry add graphene-django-filter
 ```
 
 # Requirements
-* Python (3.6, 3.7, 3.8, 3.9, 3.10)
+* Python (3.7, 3.8, 3.9, 3.10)
 * Graphene-Django (2.15)
+
 # Features
+
 ## Nested API with the ability to use logical operators
-To use, simply replace all `DjangoFilterConnectionField` fields with `AdvancedDjangoFilterConnectionField` fields in your queries. Also, if you create custom FilterSets, replace the inheritance from the `FilterSet` class with the inheritance from the `AdvancedFilterSet` class. For example, the following task query exposes the old flat API.
+To use, simply replace all `DjangoFilterConnectionField` fields with
+`AdvancedDjangoFilterConnectionField` fields in your queries.
+Also,if you create custom FilterSets, replace the inheritance from the `FilterSet` class
+with the inheritance from the `AdvancedFilterSet` class.
+For example, the following task query exposes the old flat API.
 ```python
 import graphene
 from django_filters import FilterSet
 from graphene_django import DjangoObjectType
 from graphene_django.filter import DjangoFilterConnectionField
 
-class TaskFilter(FilterSet)
+class TaskFilter(FilterSet):
     class Meta:
         model = Task
         fields = {
@@ -33,13 +44,13 @@ class TaskFilter(FilterSet)
             'user__first_name': ('exact', 'contains'),
             'user__last_name': ('exact', 'contains'),
         }
- 
+
 class UserType(DjangoObjectType):
     class Meta:
         model = User
         interfaces = (graphene.relay.Node,)
         fields = '__all__'
-        
+
 class TaskType(DjangoObjectType):
     user = graphene.Field(UserType)
 
@@ -48,7 +59,7 @@ class TaskType(DjangoObjectType):
         interfaces = (graphene.relay.Node,)
         fields = '__all__'
         filterset_class = TaskFilter
-        
+
 class Query(graphene.ObjectType):
     tasks = DjangoFilterConnectionField(TaskType)
 ```
@@ -70,11 +81,14 @@ The flat API in which all filters are applied using the `and` operator looks lik
   }
 }
 ```
-After replacing the field class with the `AdvancedDjangoFilterConnectionField` and the `FilterSet` class with the `AdvancedFilterSet` the API becomes nested with support for logical expressions.
+After replacing the field class with the `AdvancedDjangoFilterConnectionField`
+and the `FilterSet` class with the `AdvancedFilterSet`
+the API becomes nested with support for logical expressions.
 ```python
+import graphene
 from graphene_django_filter import AdvancedDjangoFilterConnectionField, AdvancedFilterSet
 
-class TaskFilter(AdvancedFilterSet)
+class TaskFilter(AdvancedFilterSet):
     class Meta:
         model = Task
         fields = {
@@ -87,23 +101,29 @@ class TaskFilter(AdvancedFilterSet)
 class Query(graphene.ObjectType):
     tasks = AdvancedDjangoFilterConnectionField(TaskType)
 ```
-For example, the following query returns tasks which names contain the word "important" or the user's email address contains the word "john" and the user's last name is "Dou" and the first name is not "John". Note that the operators are applied to lookups such as `contains`, `exact`, etc. at the last level of nesting.
+For example, the following query returns tasks which names contain the word "important"
+or the user's email address contains the word "john" and the user's last name is "Dou"
+and the first name is not "John".
+Note that the operators are applied to lookups
+such as `contains`, `exact`, etc. at the last level of nesting.
 ```graphql
 {
   tasks(
     filter: {
       or: [
         {name: {contains: "important"}}
-        and: [
-          {user: {email: {contains: "john"}}}
-          {user: {lastName: {exact: "Dou"}}}
-        ]
+        {
+            and: [
+              {user: {email: {contains: "john"}}}
+              {user: {lastName: {exact: "Dou"}}}
+            ]
+        }
       ]
       not: {
-        {user: {firstName: {exact: "John"}}}
+        user: {firstName: {exact: "John"}}
       }
     }
-  ){
+  ) {
     edges {
       node {
         id
@@ -113,7 +133,8 @@ For example, the following query returns tasks which names contain the word "imp
   }
 }
 ```
-The same result can be achieved with an alternative query structure because within the same object the `and` operator is always used.
+The same result can be achieved with an alternative query structure
+because within the same object the `and` operator is always used.
 ```graphql
 {
   tasks(
@@ -128,7 +149,7 @@ The same result can be achieved with an alternative query structure because with
         }
       ]
       not: {
-        {user: {firstName: {exact: "John"}}}
+        user: {firstName: {exact: "John"}}
       }
     }
   ){
@@ -151,3 +172,221 @@ input FilterInputType {
 }
 ```
 For more examples, see [tests](https://github.com/devind-team/graphene-django-filter/blob/8faa52bdfc2a66fc74a8aecb798b8358f7f7ea7c/tests/test_connection_field.py#L19).
+
+## Full text search
+Django provides the [API](https://docs.djangoproject.com/en/3.2/ref/contrib/postgres/search/)
+for PostgreSQL full text search. Graphene-Django-Filter inject this API into the GraphQL filter API.
+To use, add `full_text_search` lookup to fields for which you want to enable full text search.
+For example, the following type has full text search for
+`first_name` and `last_name` fields.
+```python
+import graphene
+from graphene_django import DjangoObjectType
+from graphene_django_filter import AdvancedDjangoFilterConnectionField
+
+class UserType(DjangoObjectType):
+    class Meta:
+        model = User
+        interfaces = (graphene.relay.Node,)
+        fields = '__all__'
+        filter_fields = {
+            'email': ('exact', 'startswith', 'contains'),
+            'first_name': ('exact', 'contains', 'full_text_search'),
+            'last_name': ('exact', 'contains', 'full_text_search'),
+        }
+
+class Query(graphene.ObjectType):
+    users = AdvancedDjangoFilterConnectionField(UserType)
+```
+Since this feature belongs to the AdvancedFilterSet,
+it can be used in a custom FilterSet.
+The following example will work exactly like the previous one.
+```python
+import graphene
+from graphene_django import DjangoObjectType
+from graphene_django_filter import AdvancedDjangoFilterConnectionField, AdvancedFilterSet
+
+class UserFilter(AdvancedFilterSet):
+    class Meta:
+        model = User
+        fields = {
+            'email': ('exact', 'startswith', 'contains'),
+            'first_name': ('exact', 'contains', 'full_text_search'),
+            'last_name': ('exact', 'contains', 'full_text_search'),
+        }
+
+class UserType(DjangoObjectType):
+    class Meta:
+        model = User
+        interfaces = (graphene.relay.Node,)
+        fields = '__all__'
+        filterset_class = UserFilter
+
+class Query(graphene.ObjectType):
+    users = AdvancedDjangoFilterConnectionField(UserType)
+```
+Full text search API includes SearchQuery, SearchRank, and Trigram filters.
+SearchQuery and SearchRank filters are at the top level.
+If some field has been enabled for full text search then it can be included in the field array.
+The following queries show an example of using the SearchQuery and SearchRank filters.
+```graphql
+{
+  users(
+    filter: {
+      searchQuery: {
+        vector: {
+          fields: ["first_name"]
+        }
+        query: {
+          or: [
+            {value: "Bob"}
+            {value: "Alice"}
+          ]
+        }
+      }
+    }
+  ){
+    edges {
+      node {
+        id
+        firstName
+        lastName  
+      }
+    }
+  }
+}
+```
+```graphql
+{
+  users(
+    filter: {
+      searchRank: {
+        vector: {fields: ["first_name", "last_name"]}
+        query: {value: "John Dou"}
+        lookups: {gte: 0.5}
+      }
+    }
+  ){
+    edges {
+      node {
+        id
+        firstName
+        lastName  
+      }
+    }
+  }
+}
+```
+Trigram filter belongs to the corresponding field.
+The following query shows an example of using the Trigram filter.
+```graphql
+{
+  users(
+    filter: {
+      firstName: {
+        trigram: {
+          value: "john"
+          lookups: {gte: 0.85}
+        }
+      }
+    }
+  ){
+    edges {
+      node {
+        id
+        firstName
+        lastName  
+      }
+    }
+  }
+}
+```
+Input types have the following structure.
+```graphql
+input SearchConfigInputType {
+  value: String!
+  isField: Boolean
+}
+enum SearchVectorWeight {
+  A
+  B
+  C
+  D
+}
+input SearchVectorInputType {
+  fields: [String!]!
+  config: SearchConfigInputType
+  weight: SearchVectorWeight
+}
+enum SearchQueryType {
+  PLAIN
+  PHRASE
+  RAW
+  WEBSEARCH
+}
+input SearchQueryInputType {
+  value: String
+  config: SearchConfigInputType
+  and: [SearchQueryInputType]
+  or: [SearchQueryInputType]
+  not: SearchQueryInputType
+}
+input SearchQueryFilterInputType {
+  vector: SearchVectorInputType!
+  query: SearchQueryInputType!
+}
+input FloatLookupsInputType {
+  exact: Float
+  gt: Float
+  gte: Float
+  lt: Float
+  lte: Float
+}
+input SearchRankWeightsInputType {
+  D: Float
+  C: Float
+  B: Float
+  A: Float
+}
+input SearchRankFilterInputType {
+  vector: SearchVectorInputType!
+  query: SearchQueryInputType!
+  lookups: FloatLookupsInputType!
+  weights: SearchRankWeightsInputType
+  coverDensity: Boolean
+  normalization: Int
+}
+enum TrigramSearchKind {
+  SIMILARITY
+  DISTANCE
+}
+input TrigramFilterInputType {
+  kind: TrigramSearchKind
+  lookups: FloatLookupsInputType!
+  value: String!
+}
+```
+For more examples, see [tests](https://github.com/devind-team/graphene-django-filter/blob/8faa52bdfc2a66fc74a8aecb798b8358f7f7ea7c/tests/test_connection_field.py#L19).
+
+## Settings
+The library can be customised using settings.
+To add settings, create a dictionary
+with name `GRAPHENE_DJANGO_FILTER` in the project’s `settings.py`.
+The default settings are as follows.
+```python
+GRAPHENE_DJANGO_FILTER = {
+    'FILTER_KEY': 'filter',
+    'AND_KEY': 'and',
+    'OR_KEY': 'or',
+    'NOT_KEY': 'not',
+}
+```
+To read the settings, import them from the `conf` module.
+```python
+from graphene_django_filter.conf import settings
+
+print(settings.FILTER_KEY)
+```
+The `settings` object also includes fixed settings, which depend on the user's environment.
+`IS_POSTGRESQL` determinate that current database is PostgreSQL
+and `HAS_TRIGRAM_EXTENSION` that `pg_trgm` extension is installed.
diff --git a/graphene_django_filter/conf.py b/graphene_django_filter/conf.py
new file mode 100644
index 0000000..d4fc976
--- /dev/null
+++ b/graphene_django_filter/conf.py
@@ -0,0 +1,73 @@
+"""Library settings."""
+
+from typing import Any, Dict, Optional, Union
+
+from django.conf import settings as django_settings
+from django.db import connection
+from django.test.signals import setting_changed
+
+
+def get_fixed_settings() -> Dict[str, bool]:
+    """Return fixed settings."""
+    is_postgresql = connection.vendor == 'postgresql'
+    has_trigram_extension = False
+    if is_postgresql:
+        with connection.cursor() as cursor:
+            cursor.execute("SELECT COUNT(*) FROM pg_available_extensions WHERE name='pg_trgm'")
+            has_trigram_extension = cursor.fetchone()[0] == 1
+    return {
+        'IS_POSTGRESQL': is_postgresql,
+        'HAS_TRIGRAM_EXTENSION': has_trigram_extension,
+    }
+
+
+FIXED_SETTINGS = get_fixed_settings()
+DEFAULT_SETTINGS = {
+    'FILTER_KEY': 'filter',
+    'AND_KEY': 'and',
+    'OR_KEY': 'or',
+    'NOT_KEY': 'not',
+}
+DJANGO_SETTINGS_KEY = 'GRAPHENE_DJANGO_FILTER'
+
+
+class Settings:
+    """Library settings.
+
+    Settings consist of fixed ones that depend on the user environment
+    and others that can be set with Django settings.py module.
+    """
+
+    def __init__(self, user_settings: Optional[dict] = None) -> None:
+        self._user_settings = user_settings
+
+    @property
+    def user_settings(self) -> dict:
+        """Return user-defined settings."""
+        if self._user_settings is None:
+            self._user_settings = getattr(django_settings, DJANGO_SETTINGS_KEY, {})
+        return self._user_settings
+
+    def __getattr__(self, name: str) -> Union[str, bool]:
+        """Return a setting value."""
+        if name not in FIXED_SETTINGS and name not in DEFAULT_SETTINGS:
+            raise AttributeError(f'Invalid Graphene setting: `{name}`')
+        if name in FIXED_SETTINGS:
+            return FIXED_SETTINGS[name]
+        elif name in self.user_settings:
+            return self.user_settings[name]
+        else:
+            return DEFAULT_SETTINGS[name]
+
+
+settings = Settings(None)
+
+
+def reload_settings(setting: str, value: Any, **kwargs) -> None:
+    """Reload settings in response to the `setting_changed` signal."""
+    global settings
+    if setting == DJANGO_SETTINGS_KEY:
+        settings = Settings(value)
+
+
+setting_changed.connect(reload_settings)
diff --git a/graphene_django_filter/connection_field.py b/graphene_django_filter/connection_field.py
index b558a49..6a5ce38 100644
--- a/graphene_django_filter/connection_field.py
+++ b/graphene_django_filter/connection_field.py
@@ -9,12 +9,12 @@
 import graphene
 from django.core.exceptions import ValidationError
 from django.db import models
-from django_filters import FilterSet
 from graphene_django import DjangoObjectType
 from graphene_django.filter import DjangoFilterConnectionField
 
-from .filterset import AdvancedFilterSet, tree_input_type_to_data
+from .filterset import AdvancedFilterSet
 from .filterset_factories import get_filterset_class
+from .input_data_factories import tree_input_type_to_data
 from .input_type_factories import get_filtering_args_from_filterset
 
 
@@ -74,14 +74,19 @@ def resolve_queryset(
         info: graphene.ResolveInfo,
         args: Dict[str, Any],
         filtering_args: Dict[str, graphene.InputField],
-        filterset_class: Type[FilterSet],
+        filterset_class: Type[AdvancedFilterSet],
     ) -> models.QuerySet:
         """Return a filtered QuerySet."""
         qs = super(DjangoFilterConnectionField, cls).resolve_queryset(
             connection, iterable, info, args,
         )
         filterset = filterset_class(
-            data=tree_input_type_to_data(args['filter']), queryset=qs, request=info.context,
+            data=tree_input_type_to_data(
+                filterset_class,
+                args['filter'],
+            ),
+            queryset=qs,
+            request=info.context,
         )
         if filterset.form.is_valid():
             return filterset.qs
diff --git a/graphene_django_filter/filters.py b/graphene_django_filter/filters.py
new file mode 100644
index 0000000..30cc053
--- /dev/null
+++ b/graphene_django_filter/filters.py
@@ -0,0 +1,109 @@
+"""Additional filters for special lookups."""
+
+from typing import Any, Callable, NamedTuple, Optional, Union
+
+from django.contrib.postgres.search import (
+    SearchQuery,
+    SearchRank,
+    SearchVector,
+    TrigramDistance,
+    TrigramSimilarity,
+)
+from django.db import models
+from django.db.models.constants import LOOKUP_SEP
+from django_filters import Filter
+from django_filters.constants import EMPTY_VALUES
+
+
+class AnnotatedFilter(Filter):
+    """Filter with a QuerySet object annotation."""
+
+    class Value(NamedTuple):
+        annotation_value: Any
+        search_value: Any
+
+    postfix = 'annotated'
+
+    def __init__(
+        self,
+        field_name: Optional[str] = None,
+        lookup_expr: Optional[str] = None,
+        *,
+        label: Optional[str] = None,
+        method: Optional[Union[str, Callable]] = None,
+        distinct: bool = False,
+        exclude: bool = False,
+        **kwargs
+    ) -> None:
+        super().__init__(
+            field_name,
+            lookup_expr,
+            label=label,
+            method=method,
+            distinct=distinct,
+            exclude=exclude,
+            **kwargs
+        )
+        self.filter_counter = 0
+
+    @property
+    def annotation_name(self) -> str:
+        """Return the name used for the annotation."""
+        return f'{self.field_name}_{self.postfix}_{self.creation_counter}_{self.filter_counter}'
+
+    def filter(self, qs: models.QuerySet, value: Value) -> models.QuerySet:
+        """Filter a QuerySet using annotation."""
+        if value in EMPTY_VALUES:
+            return qs
+        if self.distinct:
+            qs = qs.distinct()
+        annotation_name = self.annotation_name
+        self.filter_counter += 1
+        qs = qs.annotate(**{annotation_name: value.annotation_value})
+        lookup = f'{annotation_name}{LOOKUP_SEP}{self.lookup_expr}'
+        return self.get_method(qs)(**{lookup: value.search_value})
+
+
+class SearchQueryFilter(AnnotatedFilter):
+    """Full text search filter using the `SearchVector` and `SearchQuery` object."""
+
+    class Value(NamedTuple):
+        annotation_value: SearchVector
+        search_value: SearchQuery
+
+    postfix = 'search_query'
+    available_lookups = ('exact',)
+
+    def filter(self, qs: models.QuerySet, value: Value) -> models.QuerySet:
+        """Filter a QuerySet using the `SearchVector` and `SearchQuery` object."""
+        return super().filter(qs, value)
+
+
+class SearchRankFilter(AnnotatedFilter):
+    """Full text search filter using the `SearchRank` object."""
+
+    class Value(NamedTuple):
+        annotation_value: SearchRank
+        search_value: float
+
+    postfix = 'search_rank'
+    available_lookups = ('exact', 'gt', 'gte', 'lt', 'lte')
+
+    def filter(self, qs: models.QuerySet, value: Value) -> models.QuerySet:
+        """Filter a QuerySet using the `SearchRank` object."""
+        return super().filter(qs, value)
+
+
+class TrigramFilter(AnnotatedFilter):
+    """Full text search filter using similarity or distance of trigram."""
+
+    class Value(NamedTuple):
+        annotation_value: Union[TrigramSimilarity, TrigramDistance]
+        search_value: float
+
+    postfix = 'trigram'
+    available_lookups = ('exact', 'gt', 'gte', 'lt', 'lte')
+
+    def filter(self, qs: models.QuerySet, value: Value) -> models.QuerySet:
+        """Filter a QuerySet using similarity or distance of trigram."""
+        return super().filter(qs, value)
diff --git a/graphene_django_filter/filterset.py b/graphene_django_filter/filterset.py
index 2266cc2..cdef4aa 100644
--- a/graphene_django_filter/filterset.py
+++ b/graphene_django_filter/filterset.py
@@ -3,39 +3,20 @@
 Use the `AdvancedFilterSet` class from this module instead of the `FilterSet` from django-filter.
 """
 
-from typing import Any, Dict, List, Optional, Type, Union, cast
+import warnings
+from collections import OrderedDict
+from typing import Any, Callable, Dict, Iterator, List, Optional, Tuple, Type, Union, cast
 
-from django.db import models
+from django.db import connection, models
 from django.db.models.constants import LOOKUP_SEP
 from django.forms import Form
 from django.forms.utils import ErrorDict
 from django_filters import Filter
-from django_filters.conf import settings
+from django_filters.conf import settings as django_settings
 from django_filters.filterset import BaseFilterSet, FilterSetMetaclass
-from graphene.types.inputobjecttype import InputObjectTypeContainer
 from wrapt import ObjectProxy
 
-
-def tree_input_type_to_data(
-    tree_input_type: InputObjectTypeContainer,
-    prefix: str = '',
-) -> Dict[str, Any]:
-    """Convert a tree_input_type to a FilterSet data."""
-    result: Dict[str, Any] = {}
-    for key, value in tree_input_type.items():
-        if key in ('and', 'or'):
-            result[key] = [tree_input_type_to_data(subtree) for subtree in value]
-        elif key == 'not':
-            result[key] = tree_input_type_to_data(value)
-        else:
-            k = (prefix + LOOKUP_SEP + key if prefix else key).replace(
-                LOOKUP_SEP + settings.DEFAULT_LOOKUP_EXPR, '',
-            )
-            if isinstance(value, InputObjectTypeContainer):
-                result.update(tree_input_type_to_data(value, k))
-            else:
-                result[k] = value
-    return result
+from .conf import settings
 
 
 class QuerySetProxy(ObjectProxy):
@@ -50,9 +31,9 @@ class QuerySetProxy(ObjectProxy):
 
     __slots__ = 'q'
 
-    def __init__(self, wrapped: models.QuerySet) -> None:
+    def __init__(self, wrapped: models.QuerySet, q: Optional[models.Q] = None) -> None:
         super().__init__(wrapped)
-        self.q = models.Q()
+        self.q = q or models.Q()
 
     def __getattr__(self, name: str) -> Any:
         """Return QuerySet attributes for all cases except `filter` and `exclude`."""
@@ -60,7 +41,19 @@ def __getattr__(self, name: str) -> Any:
             return self.filter_
         elif name == 'exclude':
             return self.exclude_
-        return super().__getattr__(name)
+        attr = super().__getattr__(name)
+        if callable(attr):
+            def func(*args, **kwargs) -> Any:
+                result = attr(*args, **kwargs)
+                if isinstance(result, models.QuerySet):
+                    return QuerySetProxy(result, self.q)
+                return result
+            return func
+        return attr
+
+    def __iter__(self) -> Iterator[Any]:
+        """Return QuerySet and Q objects."""
+        return iter([self.__wrapped__, self.q])
 
     def filter_(self, *args, **kwargs) -> 'QuerySetProxy':
         """Replace the `filter` method of the QuerySet class."""
@@ -81,15 +74,20 @@ def exclude_(self, *args, **kwargs) -> 'QuerySetProxy':
         return self
 
 
-def get_q(queryset: models.QuerySet, filter_obj: Filter, value: Any) -> models.Q:
-    """Return a Q object for a queryset, filter and value."""
-    queryset_proxy = QuerySetProxy(queryset)
-    filter_obj.filter(queryset_proxy, value)
-    return queryset_proxy.q
+def is_full_text_search_lookup_expr(lookup_expr: str) -> bool:
+    """Determine if a lookup_expr is a full text search expression."""
+    return lookup_expr.split(LOOKUP_SEP)[-1] == 'full_text_search'
+
+
+def is_regular_lookup_expr(lookup_expr: str) -> bool:
+    """Determine whether the lookup_expr must be processed in a regular way."""
+    return not any([
+        is_full_text_search_lookup_expr(lookup_expr),
+    ])
 
 
 class AdvancedFilterSet(BaseFilterSet, metaclass=FilterSetMetaclass):
-    """Allow you to use advanced filters with `or` and `and` expressions."""
+    """Allow you to use advanced filters."""
 
     class TreeFormMixin(Form):
         """Tree-like form mixin."""
@@ -173,8 +171,8 @@ def find_filter(self, data_key: str) -> Filter:
         if LOOKUP_SEP in data_key:
             field_name, lookup_expr = data_key.rsplit(LOOKUP_SEP, 1)
         else:
-            field_name, lookup_expr = data_key, settings.DEFAULT_LOOKUP_EXPR
-        key = field_name if lookup_expr == settings.DEFAULT_LOOKUP_EXPR else data_key
+            field_name, lookup_expr = data_key, django_settings.DEFAULT_LOOKUP_EXPR
+        key = field_name if lookup_expr == django_settings.DEFAULT_LOOKUP_EXPR else data_key
         if key in self.filters:
             return self.filters[key]
         for filter_value in self.filters.values():
@@ -183,22 +181,120 @@ def find_filter(self, data_key: str) -> Filter:
 
     def filter_queryset(self, queryset: models.QuerySet) -> models.QuerySet:
         """Filter a queryset with a top level form's `cleaned_data`."""
-        return queryset.filter(self.get_q_for_form(queryset, self.form))
+        qs, q = self.get_queryset_proxy_for_form(queryset, self.form)
+        return qs.filter(q)
 
-    def get_q_for_form(
+    def get_queryset_proxy_for_form(
         self,
         queryset: models.QuerySet,
         form: Union[Form, TreeFormMixin],
-    ) -> models.Q:
-        """Return a Q object for a form's `cleaned_data` using `and`, `or` or `not` operator."""
+    ) -> QuerySetProxy:
+        """Return a `QuerySetProxy` object for a form's `cleaned_data`."""
+        qs = queryset
         q = models.Q()
         for name, value in form.cleaned_data.items():
-            q = q & get_q(queryset, self.find_filter(name), value)
+            qs, q = self.find_filter(name).filter(QuerySetProxy(qs, q), value)
         and_q = models.Q()
         for and_form in form.and_forms:
-            and_q = and_q & self.get_q_for_form(queryset, and_form)
+            qs, new_q = self.get_queryset_proxy_for_form(qs, and_form)
+            and_q = and_q & new_q
         or_q = models.Q()
         for or_form in form.or_forms:
-            or_q = or_q | self.get_q_for_form(queryset, or_form)
-        not_q = ~self.get_q_for_form(queryset, form.not_form) if form.not_form else models.Q()
-        return q & and_q & or_q & not_q
+            qs, new_q = self.get_queryset_proxy_for_form(qs, or_form)
+            or_q = or_q | new_q
+        if form.not_form:
+            qs, new_q = self.get_queryset_proxy_for_form(queryset, form.not_form)
+            not_q = ~new_q
+        else:
+            not_q = models.Q()
+        return QuerySetProxy(qs, q & and_q & or_q & not_q)
+
+    @classmethod
+    def get_filters(cls) -> OrderedDict:
+        """Get all filters for the filterset.
+
+        This is the combination of declared and generated filters.
+        """
+        filters = super().get_filters()
+        if not cls._meta.model:
+            return filters
+        return OrderedDict([
+            *filters.items(),
+            *cls.create_full_text_search_filters(filters).items(),
+        ])
+
+    @classmethod
+    def create_full_text_search_filters(
+        cls,
+        base_filters: OrderedDict,
+    ) -> OrderedDict:
+        """Create available full text search filters."""
+        new_filters = OrderedDict()
+        full_text_search_fields = cls.get_full_text_search_fields()
+        if not len(full_text_search_fields):
+            return new_filters
+        if not settings.IS_POSTGRESQL:
+            warnings.warn(
+                f'Full text search is not available because the {connection.vendor} vendor is '
+                'used instead of the postgresql vendor.',
+            )
+            return new_filters
+        from .filters import SearchQueryFilter, SearchRankFilter, TrigramFilter
+        new_filters = OrderedDict([
+            *new_filters.items(),
+            *cls.create_special_filters(base_filters, SearchQueryFilter).items(),
+            *cls.create_special_filters(base_filters, SearchRankFilter).items(),
+        ])
+        if not settings.HAS_TRIGRAM_EXTENSION:
+            warnings.warn(
+                'Trigram search is not available because the `pg_trgm` extension is not installed.',
+            )
+            return new_filters
+        for field_name in full_text_search_fields:
+            new_filters = OrderedDict([
+                *new_filters.items(),
+                *cls.create_special_filters(base_filters, TrigramFilter, field_name).items(),
+            ])
+        return new_filters
+
+    @classmethod
+    def create_special_filters(
+        cls,
+        base_filters: OrderedDict,
+        filter_class: Union[Type[Filter], Any],
+        field_name: Optional[str] = None,
+    ) -> OrderedDict:
+        """Create special filters using a filter class and a field name."""
+        new_filters = OrderedDict()
+        for lookup_expr in filter_class.available_lookups:
+            if field_name:
+                postfix_field_name = f'{field_name}{LOOKUP_SEP}{filter_class.postfix}'
+            else:
+                postfix_field_name = filter_class.postfix
+            filter_name = cls.get_filter_name(postfix_field_name, lookup_expr)
+            if filter_name not in base_filters:
+                new_filters[filter_name] = filter_class(
+                    field_name=postfix_field_name,
+                    lookup_expr=lookup_expr,
+                )
+        return new_filters
+
+    @classmethod
+    def get_fields(cls) -> OrderedDict:
+        """Resolve the `Meta.fields` argument including only regular lookups."""
+        return cls._get_fields(is_regular_lookup_expr)
+
+    @classmethod
+    def get_full_text_search_fields(cls) -> OrderedDict:
+        """Resolve the `Meta.fields` argument including only full text search lookups."""
+        return cls._get_fields(is_full_text_search_lookup_expr)
+
+    @classmethod
+    def _get_fields(cls, predicate: Callable[[str], bool]) -> OrderedDict:
+        """Resolve the `Meta.fields` argument including lookups that match the predicate."""
+        fields: List[Tuple[str, List[str]]] = []
+        for k, v in super().get_fields().items():
+            regular_field = [lookup_expr for lookup_expr in v if predicate(lookup_expr)]
+            if len(regular_field):
+                fields.append((k, regular_field))
+        return OrderedDict(fields)
diff --git a/graphene_django_filter/input_data_factories.py b/graphene_django_filter/input_data_factories.py
new file mode 100644
index 0000000..a688c63
--- /dev/null
+++ b/graphene_django_filter/input_data_factories.py
@@ -0,0 +1,230 @@
+"""Functions for converting tree data into data suitable for the FilterSet."""
+
+from typing import Any, Dict, List, Type, Union
+
+from django.contrib.postgres.search import (
+    SearchQuery,
+    SearchRank,
+    SearchVector,
+    TrigramDistance,
+    TrigramSimilarity,
+)
+from django.core.exceptions import ValidationError
+from django.db import models
+from django.db.models.constants import LOOKUP_SEP
+from django_filters.conf import settings as django_settings
+from graphene.types.inputobjecttype import InputObjectTypeContainer
+from graphene_django_filter.filters import SearchQueryFilter, SearchRankFilter, TrigramFilter
+from graphene_django_filter.input_types import (
+    SearchConfigInputType,
+    SearchQueryFilterInputType,
+    SearchQueryInputType,
+    SearchRankFilterInputType,
+    SearchRankWeightsInputType,
+    SearchVectorInputType,
+    TrigramFilterInputType,
+    TrigramSearchKind,
+)
+
+from .conf import settings
+from .filterset import AdvancedFilterSet
+
+
+def tree_input_type_to_data(
+    filterset_class: Type[AdvancedFilterSet],
+    tree_input_type: InputObjectTypeContainer,
+    prefix: str = '',
+) -> Dict[str, Any]:
+    """Convert a tree_input_type to a FilterSet data."""
+    result: Dict[str, Any] = {}
+    for key, value in tree_input_type.items():
+        if key in ('and', 'or'):
+            result[key] = [tree_input_type_to_data(filterset_class, subtree) for subtree in value]
+        elif key == 'not':
+            result[key] = tree_input_type_to_data(filterset_class, value)
+        else:
+            result.update(
+                create_data(
+                    (prefix + LOOKUP_SEP + key if prefix else key).replace(
+                        LOOKUP_SEP + django_settings.DEFAULT_LOOKUP_EXPR, '',
+                    ),
+                    value,
+                    filterset_class,
+                ),
+            )
+    return result
+
+
+def create_data(key: str, value: Any, filterset_class: Type[AdvancedFilterSet]) -> Dict[str, Any]:
+    """Create data from a key and a value."""
+    for factory_key, factory in DATA_FACTORIES.items():
+        if factory_key in key:
+            return factory(value, key, filterset_class)
+    if isinstance(value, InputObjectTypeContainer):
+        return tree_input_type_to_data(filterset_class, value, key)
+    else:
+        return {key: value}
+
+
+def create_search_query_data(
+    input_type: SearchQueryFilterInputType,
+    key: str,
+    filterset_class: Type[AdvancedFilterSet],
+) -> Dict[str, SearchQueryFilter.Value]:
+    """Create a data for the `SearchQueryFilter` class."""
+    return {
+        key: SearchQueryFilter.Value(
+            annotation_value=create_search_vector(input_type.vector, filterset_class),
+            search_value=create_search_query(input_type.query),
+        ),
+    }
+
+
+def create_search_rank_data(
+    input_type: Union[SearchRankFilterInputType, InputObjectTypeContainer],
+    key: str,
+    filterset_class: Type[AdvancedFilterSet],
+) -> Dict[str, SearchRankFilter.Value]:
+    """Create a data for the `SearchRankFilter` class."""
+    rank_data = {}
+    for lookup, value in input_type.lookups.items():
+        search_rank_data = {
+            'vector': create_search_vector(input_type.vector, filterset_class),
+            'query': create_search_query(input_type.query),
+            'cover_density': input_type.cover_density,
+        }
+        weights = input_type.get('weights', None)
+        if weights:
+            search_rank_data['weights'] = create_search_rank_weights(weights)
+        normalization = input_type.get('normalization', None)
+        if normalization:
+            search_rank_data['normalization'] = normalization
+        k = (key + LOOKUP_SEP + lookup).replace(
+            LOOKUP_SEP + django_settings.DEFAULT_LOOKUP_EXPR, '',
+        )
+        rank_data[k] = SearchRankFilter.Value(
+            annotation_value=SearchRank(**search_rank_data),
+            search_value=value,
+        )
+    return rank_data
+
+
+def create_trigram_data(
+    input_type: TrigramFilterInputType,
+    key: str,
+    *args
+) -> Dict[str, TrigramFilter.Value]:
+    """Create a data for the `TrigramFilter` class."""
+    trigram_data = {}
+    if input_type.kind == TrigramSearchKind.SIMILARITY:
+        trigram_class = TrigramSimilarity
+    else:
+        trigram_class = TrigramDistance
+    for lookup, value in input_type.lookups.items():
+        k = (key + LOOKUP_SEP + lookup).replace(
+            LOOKUP_SEP + django_settings.DEFAULT_LOOKUP_EXPR, '',
+        )
+        trigram_data[k] = TrigramFilter.Value(
+            annotation_value=trigram_class(
+                LOOKUP_SEP.join(key.split(LOOKUP_SEP)[:-1]),
+                input_type.value,
+            ),
+            search_value=value,
+        )
+    return trigram_data
+
+
+def create_search_vector(
+    input_type: Union[SearchVectorInputType, InputObjectTypeContainer],
+    filterset_class: Type[AdvancedFilterSet],
+) -> SearchVector:
+    """Create an object of the `SearchVector` class."""
+    validate_search_vector_fields(filterset_class, input_type.fields)
+    search_vector_data = {}
+    config = input_type.get('config', None)
+    if config:
+        search_vector_data['config'] = create_search_config(config)
+    weight = input_type.get('weight', None)
+    if weight:
+        search_vector_data['weight'] = weight.value
+    return SearchVector(*input_type.fields, **search_vector_data)
+
+
+def create_search_query(
+    input_type: Union[SearchQueryInputType, InputObjectTypeContainer],
+) -> SearchQuery:
+    """Create an object of the `SearchQuery` class."""
+    validate_search_query(input_type)
+    value = input_type.get('value', None)
+    if value:
+        config = input_type.get('config', None)
+        if config:
+            search_query = SearchQuery(input_type.value, config=create_search_config(config))
+        else:
+            search_query = SearchQuery(input_type.value)
+    else:
+        search_query = None
+    and_search_query = None
+    for and_input_type in input_type.get(settings.AND_KEY, []):
+        if and_search_query is None:
+            and_search_query = create_search_query(and_input_type)
+        else:
+            and_search_query = and_search_query & create_search_query(and_input_type)
+    or_search_query = None
+    for or_input_type in input_type.get(settings.OR_KEY, []):
+        if or_search_query is None:
+            or_search_query = create_search_query(or_input_type)
+        else:
+            or_search_query = or_search_query | create_search_query(or_input_type)
+    not_input_type = input_type.get(settings.NOT_KEY, None)
+    not_search_query = create_search_query(not_input_type) if not_input_type else None
+    valid_queries = (
+        q for q in (and_search_query, or_search_query, not_search_query) if q is not None
+    )
+    for valid_query in valid_queries:
+        search_query = search_query & valid_query if search_query else valid_query
+    return search_query
+
+
+def create_search_config(input_type: SearchConfigInputType) -> Union[str, models.F]:
+    """Create a `SearchVector` or `SearchQuery` object config."""
+    return models.F(input_type.value) if input_type.is_field else input_type.value
+
+
+def create_search_rank_weights(input_type: SearchRankWeightsInputType) -> List[float]:
+    """Create a search rank weights list."""
+    return [input_type.D, input_type.C, input_type.B, input_type.A]
+
+
+def validate_search_vector_fields(
+    filterset_class: Type[AdvancedFilterSet],
+    fields: List[str],
+) -> None:
+    """Validate that fields is included in full text search fields."""
+    full_text_search_fields = filterset_class.get_full_text_search_fields()
+    for field in fields:
+        if field not in full_text_search_fields:
+            raise ValidationError(f'The `{field}` field is not included in full text search fields')
+
+
+def validate_search_query(
+    input_type: Union[SearchQueryInputType, InputObjectTypeContainer],
+) -> None:
+    """Validate that search query contains at least one required field."""
+    if all([
+        'value' not in input_type,
+        settings.AND_KEY not in input_type,
+        settings.OR_KEY not in input_type,
+        settings.NOT_KEY not in input_type,
+    ]):
+        raise ValidationError(
+            'The search query must contains at least one required field '
+            f'such as `value`, `{settings.AND_KEY}`, `{settings.OR_KEY}`, `{settings.NOT_KEY}`.',
+        )
+
+
+DATA_FACTORIES = {
+    SearchQueryFilter.postfix: create_search_query_data,
+    SearchRankFilter.postfix: create_search_rank_data,
+    TrigramFilter.postfix: create_trigram_data,
+}
diff --git a/graphene_django_filter/input_type_factories.py b/graphene_django_filter/input_type_factories.py
index c90d672..6998eeb 100644
--- a/graphene_django_filter/input_type_factories.py
+++ b/graphene_django_filter/input_type_factories.py
@@ -7,12 +7,19 @@
 from django.db import models
 from django.db.models.constants import LOOKUP_SEP
 from django_filters import Filter
-from django_filters.conf import settings
+from django_filters.conf import settings as django_settings
 from graphene_django.filter.utils import get_model_field
 from graphene_django.forms.converter import convert_form_field
 from stringcase import pascalcase
 
+from .conf import settings
+from .filters import SearchQueryFilter, SearchRankFilter, TrigramFilter
 from .filterset import AdvancedFilterSet
+from .input_types import (
+    SearchQueryFilterInputType,
+    SearchRankFilterInputType,
+    TrigramFilterInputType,
+)
 
 
 def get_filtering_args_from_filterset(
@@ -24,7 +31,7 @@ def get_filtering_args_from_filterset(
     These arguments will be available to filter against in the GraphQL.
     """
     return {
-        'filter': graphene.Argument(
+        settings.FILTER_KEY: graphene.Argument(
             create_filter_input_type(
                 filterset_to_trees(filterset_class),
                 filterset_class,
@@ -47,59 +54,87 @@ def create_filter_input_type(
             (graphene.InputObjectType,),
             {
                 **{
-                    root.name: graphene.InputField(
-                        create_filter_input_subtype(root, filterset_class, type_name),
-                        description=f'`{pascalcase(root.name)}` field',
+                    root.name: create_filter_input_subfield(
+                        root,
+                        filterset_class,
+                        type_name,
+                        f'`{pascalcase(root.name)}` field',
                     )
                     for root in roots
                 },
-                'and': graphene.InputField(
+                settings.AND_KEY: graphene.InputField(
                     graphene.List(lambda: input_type),
                     description='`And` field',
                 ),
-                'or': graphene.InputField(
+                settings.OR_KEY: graphene.InputField(
                     graphene.List(lambda: input_type),
                     description='`Or` field',
                 ),
-                'not': graphene.InputField(lambda: input_type, description='`Not` field'),
+                settings.NOT_KEY: graphene.InputField(
+                    lambda: input_type,
+                    description='`Not` field',
+                ),
             },
         ),
     )
     return input_type
 
 
-def create_filter_input_subtype(
+def create_filter_input_subfield(
     root: Node,
     filterset_class: Type[AdvancedFilterSet],
     prefix: str,
-) -> Type[graphene.InputObjectType]:
-    """Create a filter input subtype from a filter set subtree."""
+    description: str,
+) -> graphene.InputField:
+    """Create a filter input subfield from a filter set subtree."""
     fields: Dict[str, graphene.InputField] = {}
-    for child in root.children:
-        if child.height == 0:
-            filter_name = f'{LOOKUP_SEP}'.join(
-                node.name for node in child.path if node.name != settings.DEFAULT_LOOKUP_EXPR
-            )
-            fields[child.name] = get_field(
-                filterset_class,
-                filter_name,
-                filterset_class.base_filters[filter_name],
-            )
-        else:
-            fields[child.name] = graphene.InputField(
-                create_filter_input_subtype(
+    if root.name in SPECIAL_FILTER_INPUT_TYPES_FACTORIES:
+        return SPECIAL_FILTER_INPUT_TYPES_FACTORIES[root.name]()
+    else:
+        for child in root.children:
+            if child.height == 0:
+                filter_name = f'{LOOKUP_SEP}'.join(
+                    node.name for node in child.path
+                    if node.name != django_settings.DEFAULT_LOOKUP_EXPR
+                )
+                fields[child.name] = get_field(
+                    filterset_class,
+                    filter_name,
+                    filterset_class.base_filters[filter_name],
+                )
+            else:
+                fields[child.name] = create_filter_input_subfield(
                     child,
                     filterset_class,
                     prefix + pascalcase(root.name),
-                ),
-                description=f'`{pascalcase(child.name)}` subfield',
-            )
-    return create_input_object_type(
-        f'{prefix}{pascalcase(root.name)}FilterInputType',
-        fields,
+                    f'`{pascalcase(child.name)}` subfield',
+                )
+    return graphene.InputField(
+        create_input_object_type(
+            f'{prefix}{pascalcase(root.name)}FilterInputType',
+            fields,
+        ),
+        description=description,
     )
 
 
+SPECIAL_FILTER_INPUT_TYPES_FACTORIES = {
+    SearchQueryFilter.postfix: lambda: graphene.InputField(
+        SearchQueryFilterInputType,
+        description='Field for the full text search using '
+                    'the `SearchVector` and `SearchQuery` object',
+    ),
+    SearchRankFilter.postfix: lambda: graphene.InputField(
+        SearchRankFilterInputType,
+        description='Field for the full text search using the `SearchRank` object',
+    ),
+    TrigramFilter.postfix: lambda: graphene.InputField(
+        TrigramFilterInputType,
+        description='Field for the full text search using similarity or distance of trigram',
+    ),
+}
+
+
 def create_input_object_type(name: str, fields: Dict[str, Any]) -> Type[graphene.InputObjectType]:
     """Create an inheritor for the `InputObjectType` class."""
     return cast(
diff --git a/graphene_django_filter/input_types.py b/graphene_django_filter/input_types.py
new file mode 100644
index 0000000..85656dc
--- /dev/null
+++ b/graphene_django_filter/input_types.py
@@ -0,0 +1,153 @@
+"""InputObjectType classes for special lookups."""
+
+from typing import Type, cast
+
+import graphene
+
+from .conf import settings
+
+
+class SearchConfigInputType(graphene.InputObjectType):
+    """Input type for the `SearchVector` or `SearchQuery` object config."""
+
+    value = graphene.String(
+        required=True,
+        description='`SearchVector` or `SearchQuery` object config value',
+    )
+    is_field = graphene.Boolean(
+        default_value=False,
+        description='Whether to wrap the value with the F object',
+    )
+
+
+class SearchVectorWeight(graphene.Enum):
+    """Weight of the `SearchVector` object."""
+
+    A = 'A'
+    B = 'B'
+    C = 'C'
+    D = 'D'
+
+
+class SearchVectorInputType(graphene.InputObjectType):
+    """Input type for creating the `SearchVector` object."""
+
+    fields = graphene.InputField(
+        graphene.List(graphene.NonNull(graphene.String)),
+        required=True,
+        description='Field names of vector',
+    )
+    config = graphene.InputField(SearchConfigInputType, description='Vector config'),
+    weight = graphene.InputField(SearchVectorWeight, description='Vector weight')
+
+
+class SearchQueryType(graphene.Enum):
+    """Search type of the `SearchQuery` object."""
+
+    PLAIN = 'plain'
+    PHRASE = 'phrase'
+    RAW = 'raw'
+    WEBSEARCH = 'websearch'
+
+
+def create_search_query_input_type() -> Type[graphene.InputObjectType]:
+    """Return input type for creating the `SearchQuery` object."""
+    search_query_input_type = cast(
+        Type[graphene.InputObjectType],
+        type(
+            'SearchQueryInputType',
+            (graphene.InputObjectType,),
+            {
+                '__doc__': 'Input type for creating the `SearchQuery` object.',
+                'value': graphene.String(description='Query value'),
+                'config': graphene.InputField(SearchConfigInputType, description='Query config'),
+                settings.AND_KEY: graphene.InputField(
+                    graphene.List(graphene.NonNull(lambda: search_query_input_type)),
+                    description='`And` field',
+                ),
+                settings.OR_KEY: graphene.InputField(
+                    graphene.List(graphene.NonNull(lambda: search_query_input_type)),
+                    description='`Or` field',
+                ),
+                settings.NOT_KEY: graphene.InputField(
+                    graphene.List(graphene.NonNull(lambda: search_query_input_type)),
+                    description='`Not` field',
+                ),
+            },
+        ),
+    )
+    return search_query_input_type
+
+
+SearchQueryInputType = create_search_query_input_type()
+
+
+class SearchQueryFilterInputType(graphene.InputObjectType):
+    """Input type for the full text search using the `SearchVector` and `SearchQuery` object."""
+
+    vector = graphene.InputField(SearchVectorInputType, required=True, description='Search vector')
+    query = graphene.InputField(SearchQueryInputType, required=True, description='Search query')
+
+
+class FloatLookupsInputType(graphene.InputObjectType):
+    """Input type for float lookups."""
+
+    exact = graphene.Float(description='Is exact')
+    gt = graphene.Float(description='Is greater than')
+    gte = graphene.Float(description='Is greater than or equal to')
+    lt = graphene.Float(description='Is less than')
+    lte = graphene.Float(description='Is less than or equal to')
+
+
+class SearchRankWeightsInputType(graphene.InputObjectType):
+    """`SearchRank` object weights.
+
+    Default values are set according to the documentation.
+    https://docs.djangoproject.com/en/3.2/ref/contrib/postgres/search/#weighting-queries
+    """
+
+    D = graphene.Float(default_value=0.1, description='D letter')
+    C = graphene.Float(default_value=0.2, description='C letter')
+    B = graphene.Float(default_value=0.4, description='B letter')
+    A = graphene.Float(default_value=1.0, description='A letter')
+
+
+class SearchRankFilterInputType(graphene.InputObjectType):
+    """Input type for the full text search using the `SearchRank` object."""
+
+    vector = graphene.InputField(SearchVectorInputType, required=True, description='Search vector')
+    query = graphene.InputField(SearchQueryInputType, required=True, description='Search query')
+    lookups = graphene.InputField(
+        FloatLookupsInputType,
+        required=True,
+        description='Available lookups',
+    )
+    weights = graphene.InputField(SearchRankWeightsInputType, description='Search rank weights')
+    cover_density = graphene.Boolean(
+        default_value=False,
+        description='Whether to include coverage density ranking',
+    )
+    normalization = graphene.Int(description='Search rank normalization')
+
+
+class TrigramSearchKind(graphene.Enum):
+    """Type of the search using trigrams."""
+
+    SIMILARITY = 'similarity'
+    DISTANCE = 'distance'
+
+
+class TrigramFilterInputType(graphene.InputObjectType):
+    """Input type for the full text search using similarity or distance of trigram."""
+
+    kind = graphene.InputField(
+        TrigramSearchKind,
+        default_value=TrigramSearchKind.SIMILARITY,
+        description='Type of the search using trigrams',
+    )
+    lookups = graphene.InputField(
+        FloatLookupsInputType,
+        required=True,
+        description='Available lookups',
+    )
+    value = graphene.String(required=True, description='Search value')
diff --git a/poetry.lock b/poetry.lock
index ef81882..a593ab7 100644
--- a/poetry.lock
+++ b/poetry.lock
@@ -57,6 +57,68 @@ docs = ["furo", "sphinx", "zope.interface", "sphinx-notfound-page"]
 tests = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "zope.interface", "cloudpickle"]
 tests_no_zope = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "cloudpickle"]
 
+[[package]]
+name = "certifi"
+version = "2021.10.8"
+description = "Python package for providing Mozilla's CA Bundle."
+category = "dev"
+optional = false
+python-versions = "*"
+
+[[package]]
+name = "cfgv"
+version = "3.3.1"
+description = "Validate configuration and produce human readable error messages."
+category = "dev"
+optional = false
+python-versions = ">=3.6.1"
+
+[[package]]
+name = "charset-normalizer"
+version = "2.0.12"
+description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet."
+category = "dev"
+optional = false
+python-versions = ">=3.5.0"
+
+[package.extras]
+unicode_backport = ["unicodedata2"]
+
+[[package]]
+name = "coverage"
+version = "6.3.2"
+description = "Code coverage measurement for Python"
+category = "dev"
+optional = false
+python-versions = ">=3.7"
+
+[package.extras]
+toml = ["tomli"]
+
+[[package]]
+name = "coveralls"
+version = "3.3.1"
+description = "Show coverage stats online via coveralls.io"
+category = "dev"
+optional = false
+python-versions = ">= 3.5"
+
+[package.dependencies]
+coverage = ">=4.1,<6.0.0 || >6.1,<6.1.1 || >6.1.1,<7.0"
+docopt = ">=0.6.1"
+requests = ">=1.0.0"
+
+[package.extras]
+yaml = ["PyYAML (>=3.10)"]
+
+[[package]]
+name = "distlib"
+version = "0.3.4"
+description = "Distribution utilities"
+category = "dev"
+optional = false
+python-versions = "*"
+
 [[package]]
 name = "django"
 version = "3.2"
@@ -98,6 +160,14 @@ django = ">=1.11"
 Faker = ">=0.7.7"
 toposort = ">=1.5"
 
+[[package]]
+name = "docopt"
+version = "0.6.2"
+description = "Pythonic argument parser, that will make you smile"
+category = "dev"
+optional = false
+python-versions = "*"
+
 [[package]]
 name = "eradicate"
 version = "2.0.0"
@@ -118,6 +188,18 @@ python-versions = ">=3.6"
 python-dateutil = ">=2.4"
 typing-extensions = {version = ">=3.10.0.2", markers = "python_version < \"3.8\""}
 
+[[package]]
+name = "filelock"
+version = "3.4.1"
+description = "A platform independent file lock."
+category = "dev"
+optional = false
+python-versions = ">=3.6"
+
+[package.extras]
+docs = ["furo (>=2021.8.17b43)", "sphinx (>=4.1)", "sphinx-autodoc-typehints (>=1.12)"]
+testing = ["covdefaults (>=1.2.0)", "coverage (>=4)", "pytest (>=4)", "pytest-cov", "pytest-timeout (>=1.4.2)"]
+
 [[package]]
 name = "flake8"
 version = "4.0.1"
@@ -328,6 +410,25 @@ graphql-core = ">=2.2,<3"
 promise = ">=2.2,<3"
 six = ">=1.12"
 
+[[package]]
+name = "identify"
+version = "2.4.4"
+description = "File identification library for Python"
+category = "dev"
+optional = false
+python-versions = ">=3.6.1"
+
+[package.extras]
+license = ["ukkonen"]
+
+[[package]]
+name = "idna"
+version = "3.3"
+description = "Internationalized Domain Names in Applications (IDNA)"
+category = "dev"
+optional = false
+python-versions = ">=3.5"
+
 [[package]]
 name = "importlib-metadata"
 version = "4.2.0"
@@ -352,6 +453,14 @@ category = "dev"
 optional = false
 python-versions = "*"
 
+[[package]]
+name = "nodeenv"
+version = "1.6.0"
+description = "Node.js virtual environment builder"
+category = "dev"
+optional = false
+python-versions = "*"
+
 [[package]]
 name = "pep8-naming"
 version = "0.12.1"
@@ -364,6 +473,35 @@ python-versions = "*"
 flake8 = ">=3.9.1"
 flake8-polyfill = ">=1.0.2,<2"
 
+[[package]]
+name = "platformdirs"
+version = "2.4.0"
+description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"."
+category = "dev"
+optional = false
+python-versions = ">=3.6"
+
+[package.extras]
+docs = ["Sphinx (>=4)", "furo (>=2021.7.5b38)", "proselint (>=0.10.2)", "sphinx-autodoc-typehints (>=1.12)"]
+test = ["appdirs (==1.4.4)", "pytest (>=6)", "pytest-cov (>=2.7)", "pytest-mock (>=3.6)"]
+
+[[package]]
+name = "pre-commit"
+version = "2.17.0"
+description = "A framework for managing and maintaining multi-language pre-commit hooks."
+category = "dev"
+optional = false
+python-versions = ">=3.6.1"
+
+[package.dependencies]
+cfgv = ">=2.0.0"
+identify = ">=1.0.0"
+importlib-metadata = {version = "*", markers = "python_version < \"3.8\""}
+nodeenv = ">=0.11.1"
+pyyaml = ">=5.1"
+toml = "*"
+virtualenv = ">=20.0.8"
+
 [[package]]
 name = "promise"
 version = "2.3"
@@ -427,6 +565,17 @@ python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7"
 [package.dependencies]
 six = ">=1.5"
 
+[[package]]
+name = "python-dotenv"
+version = "0.19.2"
+description = "Read key-value pairs from a .env file and set them as environment variables"
+category = "dev"
+optional = false
+python-versions = ">=3.5"
+
+[package.extras]
+cli = ["click (>=5.0)"]
+
 [[package]]
 name = "pytz"
 version = "2021.3"
@@ -435,6 +584,32 @@ category = "main"
 optional = false
 python-versions = "*"
 
+[[package]]
+name = "pyyaml"
+version = "6.0"
+description = "YAML parser and emitter for Python"
+category = "dev"
+optional = false
+python-versions = ">=3.6"
+
+[[package]]
+name = "requests"
+version = "2.27.1"
+description = "Python HTTP for Humans."
+category = "dev"
+optional = false
+python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*"
+
+[package.dependencies]
+certifi = ">=2017.4.17"
+charset-normalizer = {version = ">=2.0.0,<2.1.0", markers = "python_version >= \"3\""}
+idna = {version = ">=2.5,<4", markers = "python_version >= \"3\""}
+urllib3 = ">=1.21.1,<1.27"
+
+[package.extras]
+socks = ["PySocks (>=1.5.6,!=1.5.7)", "win-inet-pton"]
+use_chardet_on_py3 = ["chardet (>=3.0.2,<5)"]
+
 [[package]]
 name = "rx"
 version = "1.6.1"
@@ -498,6 +673,14 @@ category = "main"
 optional = false
 python-versions = "*"
 
+[[package]]
+name = "toml"
+version = "0.10.2"
+description = "Python Library for Tom's Obvious, Minimal Language"
+category = "dev"
+optional = false
+python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*"
+
 [[package]]
 name = "toposort"
 version = "1.7"
@@ -522,6 +705,37 @@ category = "main"
 optional = false
 python-versions = "*"
 
+[[package]]
+name = "urllib3"
+version = "1.22"
+description = "HTTP library with thread-safe connection pooling, file post, and more."
+category = "dev"
+optional = false
+python-versions = "*"
+
+[package.extras]
+secure = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "certifi", "ipaddress"]
+socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"]
+
+[[package]]
+name = "virtualenv"
+version = "20.13.2"
+description = "Virtual Python Environment builder"
+category = "dev"
+optional = false
+python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7"
+
+[package.dependencies]
+distlib = ">=0.3.1,<1"
+filelock = ">=3.2,<4"
+importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""}
+platformdirs = ">=2,<3"
+six = ">=1.9.0,<2"
+
+[package.extras]
+docs = ["proselint (>=0.10.2)", "sphinx (>=3)", "sphinx-argparse (>=0.2.5)", "sphinx-rtd-theme (>=0.4.3)", "towncrier (>=21.3)"]
+testing = ["coverage (>=4)", "coverage-enable-subprocess (>=1)", "flaky (>=3)", "pytest (>=4)", "pytest-env (>=0.6.2)", "pytest-freezegun (>=0.4.1)", "pytest-mock (>=2)", "pytest-randomly (>=1)", "pytest-timeout (>=1)", "packaging (>=20.0)"]
+
 [[package]]
 name = "wrapt"
 version = "1.13.3"
@@ -544,8 +758,8 @@ testing = ["pytest (>=4.6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytes
 
 [metadata]
 lock-version = "1.1"
-python-versions = ">=3.6.2"
-content-hash = "550854a73f3ecece6e013013e614555efe93b7b412e6a8c0c3e53f980cec8227"
+python-versions = ">=3.7"
+content-hash = "f7117ad2d968c75378bcd20239d5e7690b394a155201ade9ee2f3ff34bc0739a"
 
 [metadata.files]
 aniso8601 = [
@@ -568,6 +782,69 @@ attrs = [
     {file = "attrs-21.4.0-py2.py3-none-any.whl", hash = "sha256:2d27e3784d7a565d36ab851fe94887c5eccd6a463168875832a1be79c82828b4"},
     {file = "attrs-21.4.0.tar.gz", hash = "sha256:626ba8234211db98e869df76230a137c4c40a12d72445c45d5f5b716f076e2fd"},
 ]
+certifi = [
+    {file = "certifi-2021.10.8-py2.py3-none-any.whl", hash = "sha256:d62a0163eb4c2344ac042ab2bdf75399a71a2d8c7d47eac2e2ee91b9d6339569"},
+    {file = "certifi-2021.10.8.tar.gz", hash = "sha256:78884e7c1d4b00ce3cea67b44566851c4343c120abd683433ce934a68ea58872"},
+]
+cfgv = [
+    {file = "cfgv-3.3.1-py2.py3-none-any.whl", hash = "sha256:c6a0883f3917a037485059700b9e75da2464e6c27051014ad85ba6aaa5884426"},
+    {file = "cfgv-3.3.1.tar.gz", hash = "sha256:f5a830efb9ce7a445376bb66ec94c638a9787422f96264c98edc6bdeed8ab736"},
+]
+charset-normalizer = [
+    {file = "charset-normalizer-2.0.12.tar.gz", hash = "sha256:2857e29ff0d34db842cd7ca3230549d1a697f96ee6d3fb071cfa6c7393832597"},
+    {file = "charset_normalizer-2.0.12-py3-none-any.whl", hash = "sha256:6881edbebdb17b39b4eaaa821b438bf6eddffb4468cf344f09f89def34a8b1df"},
+]
+coverage = [
+    {file = "coverage-6.3.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:9b27d894748475fa858f9597c0ee1d4829f44683f3813633aaf94b19cb5453cf"},
+    {file = "coverage-6.3.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:37d1141ad6b2466a7b53a22e08fe76994c2d35a5b6b469590424a9953155afac"},
+    {file = "coverage-6.3.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f9987b0354b06d4df0f4d3e0ec1ae76d7ce7cbca9a2f98c25041eb79eec766f1"},
+    {file = "coverage-6.3.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:26e2deacd414fc2f97dd9f7676ee3eaecd299ca751412d89f40bc01557a6b1b4"},
+    {file = "coverage-6.3.2-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4dd8bafa458b5c7d061540f1ee9f18025a68e2d8471b3e858a9dad47c8d41903"},
+    {file = "coverage-6.3.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:46191097ebc381fbf89bdce207a6c107ac4ec0890d8d20f3360345ff5976155c"},
+    {file = "coverage-6.3.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:6f89d05e028d274ce4fa1a86887b071ae1755082ef94a6740238cd7a8178804f"},
+    {file = "coverage-6.3.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:58303469e9a272b4abdb9e302a780072c0633cdcc0165db7eec0f9e32f901e05"},
+    {file = "coverage-6.3.2-cp310-cp310-win32.whl", hash = "sha256:2fea046bfb455510e05be95e879f0e768d45c10c11509e20e06d8fcaa31d9e39"},
+    {file = "coverage-6.3.2-cp310-cp310-win_amd64.whl", hash = "sha256:a2a8b8bcc399edb4347a5ca8b9b87e7524c0967b335fbb08a83c8421489ddee1"},
+    {file = "coverage-6.3.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:f1555ea6d6da108e1999b2463ea1003fe03f29213e459145e70edbaf3e004aaa"},
+    {file = "coverage-6.3.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e5f4e1edcf57ce94e5475fe09e5afa3e3145081318e5fd1a43a6b4539a97e518"},
+    {file = "coverage-6.3.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7a15dc0a14008f1da3d1ebd44bdda3e357dbabdf5a0b5034d38fcde0b5c234b7"},
+    {file = "coverage-6.3.2-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:21b7745788866028adeb1e0eca3bf1101109e2dc58456cb49d2d9b99a8c516e6"},
+    {file = "coverage-6.3.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:8ce257cac556cb03be4a248d92ed36904a59a4a5ff55a994e92214cde15c5bad"},
+    {file = "coverage-6.3.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:b0be84e5a6209858a1d3e8d1806c46214e867ce1b0fd32e4ea03f4bd8b2e3359"},
+    {file = "coverage-6.3.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:acf53bc2cf7282ab9b8ba346746afe703474004d9e566ad164c91a7a59f188a4"},
+    {file = "coverage-6.3.2-cp37-cp37m-win32.whl", hash = "sha256:8bdde1177f2311ee552f47ae6e5aa7750c0e3291ca6b75f71f7ffe1f1dab3dca"},
+    {file = "coverage-6.3.2-cp37-cp37m-win_amd64.whl", hash = "sha256:b31651d018b23ec463e95cf10070d0b2c548aa950a03d0b559eaa11c7e5a6fa3"},
+    {file = "coverage-6.3.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:07e6db90cd9686c767dcc593dff16c8c09f9814f5e9c51034066cad3373b914d"},
+    {file = "coverage-6.3.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:2c6dbb42f3ad25760010c45191e9757e7dce981cbfb90e42feef301d71540059"},
+    {file = "coverage-6.3.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c76aeef1b95aff3905fb2ae2d96e319caca5b76fa41d3470b19d4e4a3a313512"},
+    {file = "coverage-6.3.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8cf5cfcb1521dc3255d845d9dca3ff204b3229401994ef8d1984b32746bb45ca"},
+    {file = "coverage-6.3.2-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8fbbdc8d55990eac1b0919ca69eb5a988a802b854488c34b8f37f3e2025fa90d"},
+    {file = "coverage-6.3.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:ec6bc7fe73a938933d4178c9b23c4e0568e43e220aef9472c4f6044bfc6dd0f0"},
+    {file = "coverage-6.3.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:9baff2a45ae1f17c8078452e9e5962e518eab705e50a0aa8083733ea7d45f3a6"},
+    {file = "coverage-6.3.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:fd9e830e9d8d89b20ab1e5af09b32d33e1a08ef4c4e14411e559556fd788e6b2"},
+    {file = "coverage-6.3.2-cp38-cp38-win32.whl", hash = "sha256:f7331dbf301b7289013175087636bbaf5b2405e57259dd2c42fdcc9fcc47325e"},
+    {file = "coverage-6.3.2-cp38-cp38-win_amd64.whl", hash = "sha256:68353fe7cdf91f109fc7d474461b46e7f1f14e533e911a2a2cbb8b0fc8613cf1"},
+    {file = "coverage-6.3.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b78e5afb39941572209f71866aa0b206c12f0109835aa0d601e41552f9b3e620"},
+    {file = "coverage-6.3.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:4e21876082ed887baed0146fe222f861b5815455ada3b33b890f4105d806128d"},
+    {file = "coverage-6.3.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:34626a7eee2a3da12af0507780bb51eb52dca0e1751fd1471d0810539cefb536"},
+    {file = "coverage-6.3.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1ebf730d2381158ecf3dfd4453fbca0613e16eaa547b4170e2450c9707665ce7"},
+    {file = "coverage-6.3.2-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd6fe30bd519694b356cbfcaca9bd5c1737cddd20778c6a581ae20dc8c04def2"},
+    {file = "coverage-6.3.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:96f8a1cb43ca1422f36492bebe63312d396491a9165ed3b9231e778d43a7fca4"},
+    {file = "coverage-6.3.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:dd035edafefee4d573140a76fdc785dc38829fe5a455c4bb12bac8c20cfc3d69"},
+    {file = "coverage-6.3.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5ca5aeb4344b30d0bec47481536b8ba1181d50dbe783b0e4ad03c95dc1296684"},
+    {file = "coverage-6.3.2-cp39-cp39-win32.whl", hash = "sha256:f5fa5803f47e095d7ad8443d28b01d48c0359484fec1b9d8606d0e3282084bc4"},
+    {file = "coverage-6.3.2-cp39-cp39-win_amd64.whl", hash = "sha256:9548f10d8be799551eb3a9c74bbf2b4934ddb330e08a73320123c07f95cc2d92"},
+    {file = "coverage-6.3.2-pp36.pp37.pp38-none-any.whl", hash = "sha256:18d520c6860515a771708937d2f78f63cc47ab3b80cb78e86573b0a760161faf"},
+    {file = "coverage-6.3.2.tar.gz", hash = "sha256:03e2a7826086b91ef345ff18742ee9fc47a6839ccd517061ef8fa1976e652ce9"},
+]
+coveralls = [
+    {file = "coveralls-3.3.1-py2.py3-none-any.whl", hash = "sha256:f42015f31d386b351d4226389b387ae173207058832fbf5c8ec4b40e27b16026"},
+    {file = "coveralls-3.3.1.tar.gz", hash = "sha256:b32a8bb5d2df585207c119d6c01567b81fba690c9c10a753bfe27a335bfc43ea"},
+]
+distlib = [
+    {file = "distlib-0.3.4-py2.py3-none-any.whl", hash = "sha256:6564fe0a8f51e734df6333d08b8b94d4ea8ee6b99b5ed50613f731fd4089f34b"},
+    {file = "distlib-0.3.4.zip", hash = "sha256:e4b58818180336dc9c529bfb9a0b58728ffc09ad92027a3f30b7cd91e3458579"},
+]
 django = [
     {file = "Django-3.2-py3-none-any.whl", hash = "sha256:0604e84c4fb698a5e53e5857b5aea945b2f19a18f25f10b8748dbdf935788927"},
     {file = "Django-3.2.tar.gz", hash = "sha256:21f0f9643722675976004eb683c55d33c05486f94506672df3d6a141546f389d"},
@@ -579,6 +856,9 @@ django-filter = [
 django-seed = [
     {file = "django-seed-0.3.1.tar.gz", hash = "sha256:93e65d2c10449c464b83e9031be02763ec10e786aa064d649c4dd5ad06fa0eea"},
 ]
+docopt = [
+    {file = "docopt-0.6.2.tar.gz", hash = "sha256:49b3a825280bd66b3aa83585ef59c4a8c82f2c8a522dbe754a8bc8d08c85c491"},
+]
 eradicate = [
     {file = "eradicate-2.0.0.tar.gz", hash = "sha256:27434596f2c5314cc9b31410c93d8f7e8885747399773cd088d3adea647a60c8"},
 ]
@@ -586,6 +866,10 @@ faker = [
     {file = "Faker-12.0.0-py3-none-any.whl", hash = "sha256:56397929f87baf1d40b794dbcc4edbb9a393ad1d02d865c67f871f5921b9ae4a"},
     {file = "Faker-12.0.0.tar.gz", hash = "sha256:4894ce3309c0099e8e6eb1db6e376372e7bcab822334bbdbcd2b1fa4df3d2231"},
 ]
+filelock = [
+    {file = "filelock-3.4.1-py3-none-any.whl", hash = "sha256:a4bc51381e01502a30e9f06dd4fa19a1712eab852b6fb0f84fd7cce0793d8ca3"},
+    {file = "filelock-3.4.1.tar.gz", hash = "sha256:0f12f552b42b5bf60dba233710bf71337d35494fc8bdd4fd6d9f6d082ad45e06"},
+]
 flake8 = [
     {file = "flake8-4.0.1-py2.py3-none-any.whl", hash = "sha256:479b1304f72536a55948cb40a32dce8bb0ffe3501e26eaf292c7e60eb5e0428d"},
     {file = "flake8-4.0.1.tar.gz", hash = "sha256:806e034dda44114815e23c16ef92f95c91e4c71100ff52813adf7132a6ad870d"},
@@ -644,6 +928,14 @@ graphql-relay = [
     {file = "graphql-relay-2.0.1.tar.gz", hash = "sha256:870b6b5304123a38a0b215a79eace021acce5a466bf40cd39fa18cb8528afabb"},
     {file = "graphql_relay-2.0.1-py3-none-any.whl", hash = "sha256:ac514cb86db9a43014d7e73511d521137ac12cf0101b2eaa5f0a3da2e10d913d"},
 ]
+identify = [
+    {file = "identify-2.4.4-py2.py3-none-any.whl", hash = "sha256:aa68609c7454dbcaae60a01ff6b8df1de9b39fe6e50b1f6107ec81dcda624aa6"},
+    {file = "identify-2.4.4.tar.gz", hash = "sha256:6b4b5031f69c48bf93a646b90de9b381c6b5f560df4cbe0ed3cf7650ae741e4d"},
+]
+idna = [
+    {file = "idna-3.3-py3-none-any.whl", hash = "sha256:84d9dd047ffa80596e0f246e2eab0b391788b0503584e8945f2368256d2735ff"},
+    {file = "idna-3.3.tar.gz", hash = "sha256:9d643ff0a55b762d5cdb124b8eaa99c66322e2157b69160bc32796e824360e6d"},
+]
 importlib-metadata = [
     {file = "importlib_metadata-4.2.0-py3-none-any.whl", hash = "sha256:057e92c15bc8d9e8109738a48db0ccb31b4d9d5cfbee5a8670879a30be66304b"},
     {file = "importlib_metadata-4.2.0.tar.gz", hash = "sha256:b7e52a1f8dec14a75ea73e0891f3060099ca1d8e6a462a4dff11c3e119ea1b31"},
@@ -652,10 +944,22 @@ mccabe = [
     {file = "mccabe-0.6.1-py2.py3-none-any.whl", hash = "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42"},
     {file = "mccabe-0.6.1.tar.gz", hash = "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f"},
 ]
+nodeenv = [
+    {file = "nodeenv-1.6.0-py2.py3-none-any.whl", hash = "sha256:621e6b7076565ddcacd2db0294c0381e01fd28945ab36bcf00f41c5daf63bef7"},
+    {file = "nodeenv-1.6.0.tar.gz", hash = "sha256:3ef13ff90291ba2a4a7a4ff9a979b63ffdd00a464dbe04acf0ea6471517a4c2b"},
+]
 pep8-naming = [
     {file = "pep8-naming-0.12.1.tar.gz", hash = "sha256:bb2455947757d162aa4cad55dba4ce029005cd1692f2899a21d51d8630ca7841"},
     {file = "pep8_naming-0.12.1-py2.py3-none-any.whl", hash = "sha256:4a8daeaeb33cfcde779309fc0c9c0a68a3bbe2ad8a8308b763c5068f86eb9f37"},
 ]
+platformdirs = [
+    {file = "platformdirs-2.4.0-py3-none-any.whl", hash = "sha256:8868bbe3c3c80d42f20156f22e7131d2fb321f5bc86a2a345375c6481a67021d"},
+    {file = "platformdirs-2.4.0.tar.gz", hash = "sha256:367a5e80b3d04d2428ffa76d33f124cf11e8fff2acdaa9b43d545f5c7d661ef2"},
+]
+pre-commit = [
+    {file = "pre_commit-2.17.0-py2.py3-none-any.whl", hash = "sha256:725fa7459782d7bec5ead072810e47351de01709be838c2ce1726b9591dad616"},
+    {file = "pre_commit-2.17.0.tar.gz", hash = "sha256:c1a8040ff15ad3d648c70cc3e55b93e4d2d5b687320955505587fd79bbaed06a"},
+]
 promise = [
     {file = "promise-2.3.tar.gz", hash = "sha256:dfd18337c523ba4b6a58801c164c1904a9d4d1b1747c7d5dbf45b693a49d93d0"},
 ]
@@ -733,10 +1037,53 @@ python-dateutil = [
     {file = "python-dateutil-2.8.2.tar.gz", hash = "sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86"},
     {file = "python_dateutil-2.8.2-py2.py3-none-any.whl", hash = "sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9"},
 ]
+python-dotenv = [
+    {file = "python-dotenv-0.19.2.tar.gz", hash = "sha256:a5de49a31e953b45ff2d2fd434bbc2670e8db5273606c1e737cc6b93eff3655f"},
+    {file = "python_dotenv-0.19.2-py2.py3-none-any.whl", hash = "sha256:32b2bdc1873fd3a3c346da1c6db83d0053c3c62f28f1f38516070c4c8971b1d3"},
+]
 pytz = [
     {file = "pytz-2021.3-py2.py3-none-any.whl", hash = "sha256:3672058bc3453457b622aab7a1c3bfd5ab0bdae451512f6cf25f64ed37f5b87c"},
     {file = "pytz-2021.3.tar.gz", hash = "sha256:acad2d8b20a1af07d4e4c9d2e9285c5ed9104354062f275f3fcd88dcef4f1326"},
 ]
+pyyaml = [
+    {file = "PyYAML-6.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d4db7c7aef085872ef65a8fd7d6d09a14ae91f691dec3e87ee5ee0539d516f53"},
+    {file = "PyYAML-6.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9df7ed3b3d2e0ecfe09e14741b857df43adb5a3ddadc919a2d94fbdf78fea53c"},
+    {file = "PyYAML-6.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:77f396e6ef4c73fdc33a9157446466f1cff553d979bd00ecb64385760c6babdc"},
+    {file = "PyYAML-6.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a80a78046a72361de73f8f395f1f1e49f956c6be882eed58505a15f3e430962b"},
+    {file = "PyYAML-6.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:f84fbc98b019fef2ee9a1cb3ce93e3187a6df0b2538a651bfb890254ba9f90b5"},
+    {file = "PyYAML-6.0-cp310-cp310-win32.whl", hash = "sha256:2cd5df3de48857ed0544b34e2d40e9fac445930039f3cfe4bcc592a1f836d513"},
+    {file = "PyYAML-6.0-cp310-cp310-win_amd64.whl", hash = "sha256:daf496c58a8c52083df09b80c860005194014c3698698d1a57cbcfa182142a3a"},
+    {file = "PyYAML-6.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:897b80890765f037df3403d22bab41627ca8811ae55e9a722fd0392850ec4d86"},
+    {file = "PyYAML-6.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:50602afada6d6cbfad699b0c7bb50d5ccffa7e46a3d738092afddc1f9758427f"},
+    {file = "PyYAML-6.0-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:48c346915c114f5fdb3ead70312bd042a953a8ce5c7106d5bfb1a5254e47da92"},
+    {file = "PyYAML-6.0-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:98c4d36e99714e55cfbaaee6dd5badbc9a1ec339ebfc3b1f52e293aee6bb71a4"},
+    {file = "PyYAML-6.0-cp36-cp36m-win32.whl", hash = "sha256:0283c35a6a9fbf047493e3a0ce8d79ef5030852c51e9d911a27badfde0605293"},
+    {file = "PyYAML-6.0-cp36-cp36m-win_amd64.whl", hash = "sha256:07751360502caac1c067a8132d150cf3d61339af5691fe9e87803040dbc5db57"},
+    {file = "PyYAML-6.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:819b3830a1543db06c4d4b865e70ded25be52a2e0631ccd2f6a47a2822f2fd7c"},
+    {file = "PyYAML-6.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:473f9edb243cb1935ab5a084eb238d842fb8f404ed2193a915d1784b5a6b5fc0"},
+    {file = "PyYAML-6.0-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0ce82d761c532fe4ec3f87fc45688bdd3a4c1dc5e0b4a19814b9009a29baefd4"},
+    {file = "PyYAML-6.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:231710d57adfd809ef5d34183b8ed1eeae3f76459c18fb4a0b373ad56bedcdd9"},
+    {file = "PyYAML-6.0-cp37-cp37m-win32.whl", hash = "sha256:c5687b8d43cf58545ade1fe3e055f70eac7a5a1a0bf42824308d868289a95737"},
+    {file = "PyYAML-6.0-cp37-cp37m-win_amd64.whl", hash = "sha256:d15a181d1ecd0d4270dc32edb46f7cb7733c7c508857278d3d378d14d606db2d"},
+    {file = "PyYAML-6.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0b4624f379dab24d3725ffde76559cff63d9ec94e1736b556dacdfebe5ab6d4b"},
+    {file = "PyYAML-6.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:213c60cd50106436cc818accf5baa1aba61c0189ff610f64f4a3e8c6726218ba"},
+    {file = "PyYAML-6.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9fa600030013c4de8165339db93d182b9431076eb98eb40ee068700c9c813e34"},
+    {file = "PyYAML-6.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:277a0ef2981ca40581a47093e9e2d13b3f1fbbeffae064c1d21bfceba2030287"},
+    {file = "PyYAML-6.0-cp38-cp38-win32.whl", hash = "sha256:d4eccecf9adf6fbcc6861a38015c2a64f38b9d94838ac1810a9023a0609e1b78"},
+    {file = "PyYAML-6.0-cp38-cp38-win_amd64.whl", hash = "sha256:1e4747bc279b4f613a09eb64bba2ba602d8a6664c6ce6396a4d0cd413a50ce07"},
+    {file = "PyYAML-6.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:055d937d65826939cb044fc8c9b08889e8c743fdc6a32b33e2390f66013e449b"},
+    {file = "PyYAML-6.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e61ceaab6f49fb8bdfaa0f92c4b57bcfbea54c09277b1b4f7ac376bfb7a7c174"},
+    {file = "PyYAML-6.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d67d839ede4ed1b28a4e8909735fc992a923cdb84e618544973d7dfc71540803"},
+    {file = "PyYAML-6.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cba8c411ef271aa037d7357a2bc8f9ee8b58b9965831d9e51baf703280dc73d3"},
+    {file = "PyYAML-6.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:40527857252b61eacd1d9af500c3337ba8deb8fc298940291486c465c8b46ec0"},
+    {file = "PyYAML-6.0-cp39-cp39-win32.whl", hash = "sha256:b5b9eccad747aabaaffbc6064800670f0c297e52c12754eb1d976c57e4f74dcb"},
+    {file = "PyYAML-6.0-cp39-cp39-win_amd64.whl", hash = "sha256:b3d267842bf12586ba6c734f89d1f5b871df0273157918b0ccefa29deb05c21c"},
+    {file = "PyYAML-6.0.tar.gz", hash = "sha256:68fb519c14306fec9720a2a5b45bc9f0c8d1b9c72adf45c37baedfcd949c35a2"},
+]
+requests = [
+    {file = "requests-2.27.1-py2.py3-none-any.whl", hash = "sha256:f22fa1e554c9ddfd16e6e41ac79759e17be9e492b3587efa038054674760e72d"},
+    {file = "requests-2.27.1.tar.gz", hash = "sha256:68d7c56fd5a8999887728ef304a6d12edc7be74f1cfa47714fc8b414525c9a61"},
+]
 rx = [
     {file = "Rx-1.6.1-py2.py3-none-any.whl", hash = "sha256:7357592bc7e881a95e0c2013b73326f704953301ab551fbc8133a6fadab84105"},
     {file = "Rx-1.6.1.tar.gz", hash = "sha256:13a1d8d9e252625c173dc795471e614eadfe1cf40ffc684e08b8fff0d9748c23"},
@@ -764,6 +1111,10 @@ text-unidecode = [
     {file = "text-unidecode-1.3.tar.gz", hash = "sha256:bad6603bb14d279193107714b288be206cac565dfa49aa5b105294dd5c4aab93"},
     {file = "text_unidecode-1.3-py2.py3-none-any.whl", hash = "sha256:1311f10e8b895935241623731c2ba64f4c455287888b18189350b67134a822e8"},
 ]
+toml = [
+    {file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"},
+    {file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"},
+]
 toposort = [
     {file = "toposort-1.7-py2.py3-none-any.whl", hash = "sha256:8ed8e109e96ae30bf66da2d2155e4eb9989d9c5c743c837e37d9774a4eddd804"},
     {file = "toposort-1.7.tar.gz", hash = "sha256:ddc2182c42912a440511bd7ff5d3e6a1cabc3accbc674a3258c8c41cbfbb2125"},
@@ -794,6 +1145,14 @@ typing-extensions = [
     {file = "typing_extensions-3.10.0.2-py3-none-any.whl", hash = "sha256:f1d25edafde516b146ecd0613dabcc61409817af4766fbbcfb8d1ad4ec441a34"},
     {file = "typing_extensions-3.10.0.2.tar.gz", hash = "sha256:49f75d16ff11f1cd258e1b988ccff82a3ca5570217d7ad8c5f48205dd99a677e"},
 ]
+urllib3 = [
+    {file = "urllib3-1.22-py2.py3-none-any.whl", hash = "sha256:06330f386d6e4b195fbfc736b297f58c5a892e4440e54d294d7004e3a9bbea1b"},
+    {file = "urllib3-1.22.tar.gz", hash = "sha256:cc44da8e1145637334317feebd728bd869a35285b93cbb4cca2577da7e62db4f"},
+]
+virtualenv = [
+    {file = "virtualenv-20.13.2-py2.py3-none-any.whl", hash = "sha256:e7b34c9474e6476ee208c43a4d9ac1510b041c68347eabfe9a9ea0c86aa0a46b"},
+    {file = "virtualenv-20.13.2.tar.gz", hash = "sha256:01f5f80744d24a3743ce61858123488e91cb2dd1d3bdf92adaf1bba39ffdedf0"},
+]
 wrapt = [
     {file = "wrapt-1.13.3-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:e05e60ff3b2b0342153be4d1b597bbcfd8330890056b9619f4ad6b8d5c96a81a"},
     {file = "wrapt-1.13.3-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:85148f4225287b6a0665eef08a178c15097366d46b210574a658c1ff5b377489"},
diff --git a/pyproject.toml b/pyproject.toml
index 5d7f2f0..fbad115 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -16,7 +16,7 @@ classifiers = [
 ]
 
 [tool.poetry.dependencies]
-python = ">=3.6.2"
+python = ">=3.7"
 Django = "^3"
 graphene = "2.1.9"
 graphene-django = "^2.15.0"
@@ -39,6 +39,9 @@ flake8-simplify = "^0.14.5"
 pep8-naming = "^0.12.1"
 flake8-use-fstring = "^1.3"
 flake8-annotations = "^2.7.0"
+python-dotenv = "^0.19.2"
+pre-commit = "^2.17.0"
+coveralls = "^3.3.1"
 
 [build-system]
 requires = ["poetry-core>=1.0.0"]


Test patch:
diff --git a/tests/.env.example b/tests/.env.example
new file mode 100644
index 0000000..377e348
--- /dev/null
+++ b/tests/.env.example
@@ -0,0 +1,4 @@
+DB_HOST=localhost
+DB_NAME=graphene_django_filter
+DB_USER=postgres
+DB_PASSWORD=1234
diff --git a/tests/data_generation.py b/tests/data_generation.py
index 3d48425..9eb9102 100644
--- a/tests/data_generation.py
+++ b/tests/data_generation.py
@@ -3,6 +3,7 @@
 from datetime import datetime
 from itertools import count
 
+from django.db import connection
 from django.utils.timezone import make_aware
 from django_seed import Seed
 from django_seed.seeder import Seeder
@@ -12,6 +13,7 @@
 
 def generate_data() -> None:
     """Generate data for testing."""
+    reset_sequences()
     seeder: Seeder = Seed.seeder()
     generate_users(seeder)
     generate_tasks(seeder)
@@ -20,20 +22,37 @@ def generate_data() -> None:
     set_task_groups_tasks()
 
 
+def reset_sequences() -> None:
+    """Reset database sequences."""
+    with connection.cursor() as cursor:
+        for model in (User, Task, TaskGroup):
+            cursor.execute(f'ALTER SEQUENCE {model._meta.db_table}_id_seq RESTART WITH 1;')
+
+
 def generate_users(seeder: Seeder) -> None:
     """Generate user data for testing."""
     number_generator = iter(count(1))
     seeder.add_entity(
         User, 5, {
             'birthday': datetime.strptime('01/01/2000', '%m/%d/%Y'),
+            'first_name': 'Bob',
+            'last_name': 'Smith',
+            'is_active': False,
+        },
+    )
+    seeder.add_entity(
+        User, 10, {
+            'email': lambda ie: f'alice{next(number_generator)}@domain.com',
+            'first_name': 'Alice',
+            'last_name': 'Stone',
             'is_active': False,
         },
     )
-    seeder.add_entity(User, 10, {'is_active': False})
     seeder.add_entity(
         User, 15, {
-            'email': lambda ie: f'kate{next(number_generator)}@domain.com',
-            'first_name': 'Kate',
+            'email': lambda ie: f'alice{next(number_generator)}@domain.com',
+            'first_name': 'Alice',
+            'last_name': 'Stone',
             'is_active': True,
         },
     )
@@ -69,7 +88,7 @@ def generate_tasks(seeder: Seeder) -> None:
     seeder.add_entity(
         Task, 15, {
             'user_id': 2,
-            'description': 'This task in very important',
+            'description': 'This task is very important',
             'created_at': make_aware(datetime.strptime('01/01/2020', '%m/%d/%Y')),
             'completed_at': make_aware(datetime.strptime('02/01/2020', '%m/%d/%Y')),
         },
diff --git a/tests/filtersets.py b/tests/filtersets.py
index a4fec5f..53bf630 100644
--- a/tests/filtersets.py
+++ b/tests/filtersets.py
@@ -12,8 +12,8 @@ class Meta:
         model = User
         fields = {
             'email': ('exact', 'startswith', 'contains'),
-            'first_name': ('exact', 'contains'),
-            'last_name': ('exact', 'contains'),
+            'first_name': ('exact', 'contains', 'full_text_search'),
+            'last_name': ('exact', 'contains', 'full_text_search'),
             'is_active': ('exact',),
             'birthday': ('exact',),
         }
@@ -25,7 +25,7 @@ class TaskFilter(AdvancedFilterSet):
     class Meta:
         model = Task
         fields = {
-            'name': ('exact', 'contains'),
+            'name': ('exact', 'contains', 'full_text_search'),
             'created_at': ('gt',),
             'completed_at': ('lt',),
             'description': ('exact', 'contains'),
@@ -41,7 +41,7 @@ class TaskGroupFilter(AdvancedFilterSet):
     class Meta:
         model = TaskGroup
         fields = {
-            'name': ('exact', 'contains'),
+            'name': ('exact', 'contains', 'full_text_search'),
             'priority': ('exact', 'gte', 'lte'),
             'tasks': ('exact',),
         }
diff --git a/tests/migrations/0001_initial.py b/tests/migrations/0001_initial.py
new file mode 100644
index 0000000..915139d
--- /dev/null
+++ b/tests/migrations/0001_initial.py
@@ -0,0 +1,76 @@
+# Generated by Django 3.2 on 2022-02-15 19:31
+
+import django.db.models.deletion
+from django.contrib.postgres.operations import TrigramExtension
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    initial = True
+
+    dependencies = [
+    ]
+
+    operations = [
+        TrigramExtension(),
+        migrations.CreateModel(
+            name='Task',
+            fields=[
+                (
+                    'id',
+                    models.AutoField(
+                        auto_created=True,
+                        primary_key=True,
+                        serialize=False,
+                        verbose_name='ID',
+                    ),
+                ),
+                ('name', models.CharField(max_length=256)),
+                ('created_at', models.DateTimeField(auto_now_add=True)),
+                ('completed_at', models.DateTimeField(null=True)),
+                ('description', models.TextField()),
+            ],
+        ),
+        migrations.CreateModel(
+            name='User',
+            fields=[
+                (
+                    'id',
+                    models.AutoField(
+                        auto_created=True,
+                        primary_key=True,
+                        serialize=False,
+                        verbose_name='ID',
+                    ),
+                ),
+                ('email', models.EmailField(max_length=254, unique=True)),
+                ('first_name', models.CharField(max_length=128)),
+                ('last_name', models.CharField(max_length=128)),
+                ('is_active', models.BooleanField(default=True)),
+                ('birthday', models.DateField(null=True)),
+            ],
+        ),
+        migrations.CreateModel(
+            name='TaskGroup',
+            fields=[
+                (
+                    'id',
+                    models.AutoField(
+                        auto_created=True,
+                        primary_key=True,
+                        serialize=False,
+                        verbose_name='ID',
+                    ),
+                ),
+                ('name', models.CharField(max_length=256)),
+                ('priority', models.PositiveSmallIntegerField(default=0)),
+                ('tasks', models.ManyToManyField(to='tests.Task')),
+            ],
+        ),
+        migrations.AddField(
+            model_name='task',
+            name='user',
+            field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='tests.user'),
+        ),
+    ]
diff --git a/tests/migrations/__init__.py b/tests/migrations/__init__.py
new file mode 100644
index 0000000..0fbcd93
--- /dev/null
+++ b/tests/migrations/__init__.py
@@ -0,0 +1,1 @@
+"""Migrations for graphene-django-filter tests."""
diff --git a/tests/object_types.py b/tests/object_types.py
index de73dd5..a7164c8 100644
--- a/tests/object_types.py
+++ b/tests/object_types.py
@@ -16,8 +16,8 @@ class Meta:
         fields = '__all__'
         filter_fields = {
             'email': ('exact', 'startswith', 'contains'),
-            'first_name': ('exact', 'contains'),
-            'last_name': ('exact', 'contains'),
+            'first_name': ('exact', 'contains', 'full_text_search'),
+            'last_name': ('exact', 'contains', 'full_text_search'),
             'is_active': ('exact',),
             'birthday': ('exact',),
         }
@@ -43,7 +43,7 @@ class Meta:
         interfaces = (graphene.relay.Node,)
         fields = '__all__'
         filter_fields = {
-            'name': ('exact', 'contains'),
+            'name': ('exact', 'contains', 'full_text_search'),
             'created_at': ('gt',),
             'completed_at': ('lt',),
             'description': ('exact', 'contains'),
@@ -75,7 +75,7 @@ class Meta:
         interfaces = (graphene.relay.Node,)
         fields = '__all__'
         filter_fields = {
-            'name': ('exact', 'contains'),
+            'name': ('exact', 'contains', 'full_text_search'),
             'priority': ('exact', 'gte', 'lte'),
             'tasks': ('exact',),
         }
diff --git a/tests/settings.py b/tests/settings.py
index 2cbd9e9..234f919 100644
--- a/tests/settings.py
+++ b/tests/settings.py
@@ -1,9 +1,18 @@
 """Django settings for graphene-django-filter project."""
 
+import os
+
+from dotenv import load_dotenv
+
+load_dotenv(dotenv_path='.env')
+
 DATABASES = {
     'default': {
-        'ENGINE': 'django.db.backends.sqlite3',
-        'NAME': ':memory:',
+        'ENGINE': 'django.db.backends.postgresql',
+        'HOST': os.getenv('DB_HOST', 'localhost'),
+        'NAME': os.getenv('DB_NAME', 'graphene_django_filter'),
+        'USER': os.getenv('DB_USER', 'postgres'),
+        'PASSWORD': os.getenv('DB_PASSWORD', '1234'),
     },
 }
 
@@ -14,6 +23,7 @@
     'django.contrib.auth',
     'django_filters',
     'django_seed',
+    'graphene_django_filter',
     'tests',
 )
 
diff --git a/tests/test_conf.py b/tests/test_conf.py
new file mode 100644
index 0000000..e260063
--- /dev/null
+++ b/tests/test_conf.py
@@ -0,0 +1,24 @@
+"""Library settings tests."""
+
+from django.test import TestCase, override_settings
+from graphene_django_filter import conf
+
+
+class SettingsTests(TestCase):
+    """Library settings tests."""
+
+    def test_initial(self) -> None:
+        """Test initial settings."""
+        self.assertTrue(conf.settings.IS_POSTGRESQL)
+        self.assertTrue(conf.settings.HAS_TRIGRAM_EXTENSION)
+        self.assertEqual('filter', conf.settings.FILTER_KEY)
+        self.assertEqual('and', conf.settings.AND_KEY)
+        self.assertEqual('or', conf.settings.OR_KEY)
+        self.assertEqual('not', conf.settings.NOT_KEY)
+
+    def test_overridden(self) -> None:
+        """Test overridden settings."""
+        self.assertEqual('filter', conf.settings.FILTER_KEY)
+        with override_settings(GRAPHENE_DJANGO_FILTER={'FILTER_KEY': 'where'}):
+            self.assertEqual('where', conf.settings.FILTER_KEY)
+        self.assertEqual('filter', conf.settings.FILTER_KEY)
diff --git a/tests/test_connection_field.py b/tests/test_connection_field.py
index 6de3f02..9eeb2e3 100644
--- a/tests/test_connection_field.py
+++ b/tests/test_connection_field.py
@@ -1,115 +1,16 @@
 """`connection_field` module tests."""
 
-from datetime import datetime
-from typing import List
-
 from django.test import TestCase
-from django.utils.timezone import make_aware
 from django_filters import FilterSet
 from graphene_django_filter import AdvancedDjangoFilterConnectionField
-from graphql.execution import ExecutionResult
-from graphql_relay import from_global_id
 
-from .data_generation import generate_data
 from .filtersets import TaskFilter
 from .object_types import TaskFilterSetClassType
-from .schema import schema
 
 
-class AdvancedDjangoFilterConnectionFieldTest(TestCase):
+class AdvancedDjangoFilterConnectionFieldTests(TestCase):
     """`AdvancedDjangoFilterConnectionField` class tests."""
 
-    users_query = """
-    {
-        %s(
-            filter: {
-                isActive: {exact: true}
-                or: [
-                    {email: {contains: "kate"}}
-                    {firstName: {exact: "Jane"}}
-                ]
-            }
-        ) {
-            edges {
-                node {
-                    id
-                }
-            }
-        }
-    }
-    """
-    users_fields_query = users_query % 'usersFields'
-    users_filterset_query = users_query % 'usersFilterset'
-    tasks_query = f"""
-    {{
-        %s(
-            filter: {{
-                completedAt: {{
-                    lt: "{make_aware(datetime.strptime('02/02/2021', '%m/%d/%Y')).isoformat()}"
-                }}
-                createdAt: {{
-                    gt: "{make_aware(datetime.strptime('12/31/2019', '%m/%d/%Y')).isoformat()}"
-                }}
-                or: [
-                    {{name: {{contains: "Important"}}}}
-                    {{description: {{contains: "important"}}}}
-                ]
-            }}
-        ) {{
-            edges {{
-                node {{
-                    id
-                }}
-            }}
-        }}
-    }}
-    """
-    tasks_fields_query = tasks_query % 'tasksFields'
-    tasks_filterset_query = tasks_query % 'tasksFilterset'
-    task_groups_query = """
-    {
-        %s(
-            filter: {
-                or: [
-                    {name: {exact: "Task group №1"}}
-                    {
-                        and: [
-                            {priority: {gte: 5}}
-                            {priority: {lte: 10}}
-                        ]
-                    }
-                ]
-                and: [
-                    {not: {priority: {exact: 7}}}
-                    {not: {priority: {exact: 9}}}
-                ]
-            }
-        ) {
-            edges {
-                node {
-                    id
-                }
-            }
-        }
-    }
-    """
-    task_groups_fields_query = task_groups_query % 'taskGroupsFields'
-    task_groups_filterset_query = task_groups_query % 'taskGroupsFilterset'
-
-    @classmethod
-    def setUpClass(cls) -> None:
-        """Set up `AdvancedDjangoFilterConnectionField` class tests."""
-        super().setUpClass()
-        generate_data()
-
-    @staticmethod
-    def get_ids(execution_result: ExecutionResult, key: str) -> List[int]:
-        """Return identifiers from an execution result using a key."""
-        return [
-            int(from_global_id(edge['node']['id'])[1]) for edge
-            in execution_result.data[key]['edges']
-        ]
-
     def test_init(self) -> None:
         """Test the `__init__` method."""
         AdvancedDjangoFilterConnectionField(TaskFilterSetClassType)
@@ -133,27 +34,3 @@ def test_filtering_args(self) -> None:
             'TaskFilterSetClassFilterInputType',
             filtering_args['filter'].type.__name__,
         )
-
-    def test_users_execution(self) -> None:
-        """Test the schema execution by querying users."""
-        expected = list(range(16, 51))
-        execution_result = schema.execute(self.users_fields_query)
-        self.assertEqual(expected, self.get_ids(execution_result, 'usersFields'))
-        execution_result = schema.execute(self.users_filterset_query)
-        self.assertEqual(expected, self.get_ids(execution_result, 'usersFilterset'))
-
-    def test_tasks_execution(self) -> None:
-        """Test the schema execution by querying tasks."""
-        expected = list(range(16, 76))
-        execution_result = schema.execute(self.tasks_fields_query)
-        self.assertEqual(expected, self.get_ids(execution_result, 'tasksFields'))
-        execution_result = schema.execute(self.tasks_filterset_query)
-        self.assertEqual(expected, self.get_ids(execution_result, 'tasksFilterset'))
-
-    def test_task_groups_execution(self) -> None:
-        """Test the schema execution by querying task groups."""
-        expected = [1, 5, 6, 8, 10]
-        execution_result = schema.execute(self.task_groups_fields_query)
-        self.assertEqual(expected, self.get_ids(execution_result, 'taskGroupsFields'))
-        execution_result = schema.execute(self.task_groups_filterset_query)
-        self.assertEqual(expected, self.get_ids(execution_result, 'taskGroupsFilterset'))
diff --git a/tests/test_filters.py b/tests/test_filters.py
new file mode 100644
index 0000000..55f6d15
--- /dev/null
+++ b/tests/test_filters.py
@@ -0,0 +1,92 @@
+"""Tests for additional filters for special lookups."""
+
+from unittest.mock import patch
+
+from django.contrib.postgres.search import SearchQuery, SearchRank, SearchVector, TrigramSimilarity
+from django.db import models
+from django.db.models import functions
+from django.test import TestCase
+from django_filters import Filter
+from graphene_django_filter.filters import (
+    AnnotatedFilter,
+    SearchQueryFilter,
+    SearchRankFilter,
+    TrigramFilter,
+)
+
+
+from .data_generation import generate_data
+from .models import User
+
+
+class FiltersTests(TestCase):
+    """Tests for additional filters for special lookups."""
+
+    @classmethod
+    def setUpClass(cls) -> None:
+        """Set up tests for additional filters for special lookups."""
+        super().setUpClass()
+        generate_data()
+
+    @patch.object(Filter, 'creation_counter', new=0)
+    def test_annotate_name(self) -> None:
+        """Test the `annotation_name` property of the `AnnotatedFilter` class."""
+        annotated_filter = AnnotatedFilter(field_name='id', lookup_expr='exact')
+        self.assertEqual('id_annotated_0_0', annotated_filter.annotation_name)
+
+    def test_annotated_filter(self) -> None:
+        """Test the `filter` method of the `AnnotatedFilter` class."""
+        annotated_filter = AnnotatedFilter(field_name='id', lookup_expr='exact')
+        self.assertEqual(0, annotated_filter.filter_counter)
+        users = annotated_filter.filter(
+            User.objects.all(),
+            AnnotatedFilter.Value(
+                annotation_value=functions.Concat(
+                    models.Value('#'),
+                    functions.Cast(models.F('id'), output_field=models.CharField()),
+                ),
+                search_value='#5',
+            ),
+        ).all()
+        self.assertEqual(1, annotated_filter.filter_counter)
+        self.assertEqual([5], list(map(lambda user: user.id, users)))
+
+    def test_search_query_filter(self) -> None:
+        """Test the `SearchQueryFilter` class."""
+        search_query_filter = SearchQueryFilter(field_name='first_name', lookup_expr='exact')
+        users = search_query_filter.filter(
+            User.objects.all(),
+            SearchQueryFilter.Value(
+                annotation_value=SearchVector('first_name'),
+                search_value=SearchQuery('Jane'),
+            ),
+        ).all()
+        self.assertTrue(all('Jane' in user.first_name for user in users))
+
+    @patch.object(Filter, 'creation_counter', new=0)
+    def test_search_rank_filter(self) -> None:
+        """Test the `SearchQueryFilter` class."""
+        search_rank_filter = SearchRankFilter(field_name='first_name', lookup_expr='lte')
+        users = search_rank_filter.filter(
+            User.objects.all(),
+            SearchRankFilter.Value(
+                annotation_value=SearchRank(
+                    vector=SearchVector('first_name'),
+                    query=SearchQuery('Jane'),
+                ),
+                search_value=1,
+            ),
+        ).all()
+        self.assertTrue(all(hasattr(user, 'first_name_search_rank_0_0') for user in users))
+
+    def test_trigram_filter(self) -> None:
+        """Test the `TrigramFilter` class."""
+        trigram_filter = TrigramFilter(field_name='field_name', lookup_expr='exact')
+        users = trigram_filter.filter(
+            User.objects.all(),
+            TrigramFilter.Value(
+                annotation_value=TrigramSimilarity('first_name', 'Jane'),
+                search_value=1,
+            ),
+        ).all()
+        self.assertTrue(all('Jane' in user.first_name for user in users))
diff --git a/tests/test_filterset.py b/tests/test_filterset.py
index 023e46f..2b62f55 100644
--- a/tests/test_filterset.py
+++ b/tests/test_filterset.py
@@ -1,19 +1,21 @@
 """`filterset` module tests."""
 
+from collections import OrderedDict
 from contextlib import ExitStack
-from datetime import datetime, timedelta
-from unittest.mock import patch
+from datetime import datetime
+from typing import List
+from unittest.mock import MagicMock, patch
 
-import django_filters
-import graphene
 from django.db import models
 from django.test import TestCase
 from django.utils.timezone import make_aware
+from django_filters import CharFilter
+from graphene_django_filter.filters import SearchQueryFilter, SearchRankFilter, TrigramFilter
 from graphene_django_filter.filterset import (
     AdvancedFilterSet,
     QuerySetProxy,
-    get_q,
-    tree_input_type_to_data,
+    is_full_text_search_lookup_expr,
+    is_regular_lookup_expr,
 )
 
 from .data_generation import generate_data
@@ -21,107 +23,17 @@
 from .models import Task, User
 
 
-class UtilsTest(TestCase):
+class UtilsTests(TestCase):
     """Tests for utility functions and classes of the `filterset` module."""
 
-    class TaskNameFilterInputType(graphene.InputObjectType):
-        exact = graphene.String()
-
-    class TaskDescriptionFilterInputType(graphene.InputObjectType):
-        exact = graphene.String()
-
-    class TaskUserEmailFilterInputType(graphene.InputObjectType):
-        exact = graphene.String()
-        iexact = graphene.String()
-        contains = graphene.String()
-        icontains = graphene.String()
-
-    class TaskUserLastNameFilterInputType(graphene.InputObjectType):
-        exact = graphene.String()
-
-    class TaskUserFilterInputType(graphene.InputObjectType):
-        exact = graphene.String()
-        email = graphene.InputField(
-            lambda: UtilsTest.TaskUserEmailFilterInputType,
-        )
-        last_name = graphene.InputField(
-            lambda: UtilsTest.TaskUserLastNameFilterInputType,
-        )
-
-    class TaskCreatedAtInputType(graphene.InputObjectType):
-        gt = graphene.DateTime()
-
-    class TaskCompletedAtInputType(graphene.InputObjectType):
-        lg = graphene.DateTime()
-
-    TaskFilterInputType = type(
-        'TaskFilterInputType',
-        (graphene.InputObjectType,),
-        {
-            'name': graphene.InputField(lambda: UtilsTest.TaskNameFilterInputType),
-            'description': graphene.InputField(lambda: UtilsTest.TaskDescriptionFilterInputType),
-            'user': graphene.InputField(lambda: UtilsTest.TaskUserFilterInputType),
-            'created_at': graphene.InputField(lambda: UtilsTest.TaskCreatedAtInputType),
-            'completed_at': graphene.InputField(lambda: UtilsTest.TaskCompletedAtInputType),
-            'and': graphene.InputField(graphene.List(lambda: UtilsTest.TaskFilterInputType)),
-            'or': graphene.InputField(graphene.List(lambda: UtilsTest.TaskFilterInputType)),
-            'not': graphene.InputField(lambda: UtilsTest.TaskFilterInputType),
-        },
-    )
-
-    gt_datetime = datetime.today() - timedelta(days=1)
-    lt_datetime = datetime.today()
-    tree_input_type = TaskFilterInputType._meta.container({
-        'name': TaskNameFilterInputType._meta.container({'exact': 'Important task'}),
-        'description': TaskDescriptionFilterInputType._meta.container(
-            {'exact': 'This task in very important'},
-        ),
-        'user': TaskUserFilterInputType._meta.container(
-            {'email': TaskUserEmailFilterInputType._meta.container({'contains': 'dev'})},
-        ),
-        'and': [
-            TaskFilterInputType._meta.container({
-                'completed_at': TaskCompletedAtInputType._meta.container({'lt': lt_datetime}),
-            }),
-        ],
-        'or': [
-            TaskFilterInputType._meta.container({
-                'created_at': TaskCreatedAtInputType._meta.container({'gt': gt_datetime}),
-            }),
-        ],
-        'not': TaskFilterInputType._meta.container({
-            'user': TaskUserFilterInputType._meta.container(
-                {'first_name': TaskUserEmailFilterInputType._meta.container({'exact': 'John'})},
-            ),
-        }),
-    })
-
-    def test_tree_input_type_to_data(self) -> None:
-        """Test the `tree_input_type_to_data` function."""
-        data = tree_input_type_to_data(self.tree_input_type)
-        self.assertEqual(
-            {
-                'name': 'Important task',
-                'description': 'This task in very important',
-                'user__email__contains': 'dev',
-                'and': [{
-                    'completed_at__lt': self.lt_datetime,
-                }],
-                'or': [{
-                    'created_at__gt': self.gt_datetime,
-                }],
-                'not': {
-                    'user__first_name': 'John',
-                },
-            },
-            data,
-        )
-
     def test_queryset_proxy(self) -> None:
         """Test the `QuerySetProxy` class."""
         queryset = User.objects.all()
         queryset_proxy = QuerySetProxy(queryset)
-        self.assertEqual(queryset.get, queryset_proxy.get)
+        self.assertIsInstance(
+            queryset_proxy.annotate(number=models.F('id') + models.Value('#')),
+            QuerySetProxy,
+        )
         self.assertNotEqual(queryset.filter, queryset_proxy.filter)
         self.assertNotEqual(queryset.exclude, queryset_proxy.exclude)
         queryset_proxy.filter(email__contains='kate').exclude(
@@ -133,20 +45,24 @@ def test_queryset_proxy(self) -> None:
             ),
             queryset_proxy.q,
         )
+        self.assertEqual([queryset_proxy.__wrapped__, queryset_proxy.q], list(queryset_proxy))
 
-    def test_get_q(self) -> None:
-        """Test the `test_get_q` method."""
-        queryset = User.objects.all()
-        filter_obj = django_filters.Filter(field_name='first_name', lookup_expr='exact')
-        q = get_q(queryset, filter_obj, 'John')
-        self.assertEqual(models.Q(first_name__exact='John'), q)
+    def test_is_full_text_search_lookup(self) -> None:
+        """Test the `is_full_text_search_lookup` function."""
+        self.assertFalse(is_full_text_search_lookup_expr('name__exact'))
+        self.assertTrue(is_full_text_search_lookup_expr('name__full_text_search'))
+
+    def test_is_regular_lookup(self) -> None:
+        """Test the `is_regular_lookup` function."""
+        self.assertTrue(is_regular_lookup_expr('name__exact'))
+        self.assertFalse(is_regular_lookup_expr('name__full_text_search'))
 
 
-class AdvancedFilterSetTest(TestCase):
+class AdvancedFilterSetTests(TestCase):
     """`AdvancedFilterSetTest` class tests."""
 
     class FindFilterFilterSet(AdvancedFilterSet):
-        in_last_name = django_filters.CharFilter(field_name='last_name', lookup_expr='contains')
+        in_last_name = CharFilter(field_name='last_name', lookup_expr='contains')
 
         class Meta:
             model = User
@@ -172,12 +88,92 @@ class Meta:
         },
     }
 
+    class FullTextSearchFilterSet(AdvancedFilterSet):
+        class Meta:
+            model = Task
+            fields = {
+                'user__email': ('exact', 'contains'),
+                'user__first_name': ('exact', 'contains', 'full_text_search'),
+                'user__last_name': ('full_text_search',),
+            }
+
+    expected_regular_filters = [
+        ('user__email', CharFilter(field_name='user__email', lookup_expr='exact')),
+        ('user__email__contains', CharFilter(field_name='user__email', lookup_expr='contains')),
+        ('user__first_name', CharFilter(field_name='user__first_name', lookup_expr='exact')),
+        (
+            'user__first_name__contains',
+            CharFilter(field_name='user__first_name', lookup_expr='contains'),
+        ),
+    ]
+    expected_search_query_filters = [
+        ('search_query', SearchQueryFilter(field_name='search_query', lookup_expr='exact')),
+    ]
+    expected_search_rank_filters = [
+        ('search_rank', SearchRankFilter(field_name='search_rank', lookup_expr='exact')),
+        ('search_rank__gt', SearchRankFilter(field_name='search_rank', lookup_expr='gt')),
+        ('search_rank__gte', SearchRankFilter(field_name='search_rank', lookup_expr='gte')),
+        ('search_rank__lt', SearchRankFilter(field_name='search_rank', lookup_expr='lt')),
+        ('search_rank__lte', SearchRankFilter(field_name='search_rank', lookup_expr='lte')),
+    ]
+    expected_trigram_filters = [
+        (
+            'user__first_name__trigram',
+            TrigramFilter(field_name='user__first_name__trigram', lookup_expr='exact'),
+        ),
+        (
+            'user__first_name__trigram__gt',
+            TrigramFilter(field_name='user__first_name__trigram', lookup_expr='gt'),
+        ),
+        (
+            'user__first_name__trigram__gte',
+            TrigramFilter(field_name='user__first_name__trigram', lookup_expr='gte'),
+        ),
+        (
+            'user__first_name__trigram__lt',
+            TrigramFilter(field_name='user__first_name__trigram', lookup_expr='lt'),
+        ),
+        (
+            'user__first_name__trigram__lte',
+            TrigramFilter(field_name='user__first_name__trigram', lookup_expr='lte'),
+        ),
+        (
+            'user__last_name__trigram',
+            TrigramFilter(field_name='user__last_name__trigram', lookup_expr='exact'),
+        ),
+        (
+            'user__last_name__trigram__gt',
+            TrigramFilter(field_name='user__last_name__trigram', lookup_expr='gt'),
+        ),
+        (
+            'user__last_name__trigram__gte',
+            TrigramFilter(field_name='user__last_name__trigram', lookup_expr='gte'),
+        ),
+        (
+            'user__last_name__trigram__lt',
+            TrigramFilter(field_name='user__last_name__trigram', lookup_expr='lt'),
+        ),
+        (
+            'user__last_name__trigram__lte',
+            TrigramFilter(field_name='user__last_name__trigram', lookup_expr='lte'),
+        ),
+    ]
+
     @classmethod
     def setUpClass(cls) -> None:
-        """Set up `AdvancedFilterSetTest` class tests."""
+        """Set up `AdvancedFilterSetTest` class."""
         super().setUpClass()
         generate_data()
 
+    def assertFiltersEqual(self, first: List[tuple], second: List[tuple]) -> None:
+        """Fail if the two filter lists unequal."""
+        self.assertEqual(len(first), len(second))
+        for expected, actual in zip(first, second):
+            self.assertEqual(expected[0], actual[0])
+            self.assertEqual(type(expected[1]), type(actual[1]))
+            self.assertEqual(expected[1].field_name, actual[1].field_name)
+            self.assertEqual(expected[1].lookup_expr, actual[1].lookup_expr)
+
     def test_get_form_class(self) -> None:
         """Test getting a tree form class with the `get_form_class` method."""
         form_class = TaskFilter().get_form_class()
@@ -245,7 +241,7 @@ def test_form(self) -> None:
 
     def test_find_filter(self) -> None:
         """Test the `find_filter` method."""
-        filterset = AdvancedFilterSetTest.FindFilterFilterSet()
+        filterset = AdvancedFilterSetTests.FindFilterFilterSet()
         email_filter = filterset.find_filter('email')
         self.assertEqual(email_filter.field_name, 'email')
         self.assertEqual(email_filter.lookup_expr, 'exact')
@@ -272,3 +268,113 @@ def test_filter_queryset(self) -> None:
             ) & ~models.Q(user=2),
         ).all()
         self.assertEqual(list(expected_tasks), list(tasks))
+
+    def test_get_fields(self) -> None:
+        """Test `get_fields` and `get_full_text_search_fields` methods."""
+        self.assertEqual(
+            OrderedDict([
+                ('user__email', ['exact', 'contains']),
+                ('user__first_name', ['exact', 'contains']),
+            ]),
+            self.FullTextSearchFilterSet.get_fields(),
+        )
+        self.assertEqual(
+            OrderedDict([
+                ('user__first_name', ['full_text_search']),
+                ('user__last_name', ['full_text_search']),
+            ]),
+            self.FullTextSearchFilterSet.get_full_text_search_fields(),
+        )
+
+    def test_create_special_filters_without_field_name(self) -> None:
+        """Test the `create_special_filters` method without the `field_name` parameter."""
+        base_filters = OrderedDict([('search_rank__gt', MagicMock())])
+        filters = AdvancedFilterSet.create_special_filters(base_filters, SearchRankFilter)
+        expected_filters = [
+            ('search_rank', SearchRankFilter(field_name='search_rank', lookup_expr='exact')),
+            ('search_rank__gte', SearchRankFilter(field_name='search_rank', lookup_expr='gte')),
+            ('search_rank__lt', SearchRankFilter(field_name='search_rank', lookup_expr='lt')),
+            ('search_rank__lte', SearchRankFilter(field_name='search_rank', lookup_expr='lte')),
+        ]
+        self.assertFiltersEqual(expected_filters, filters.items())
+
+    def test_create_special_filters_with_field_name(self) -> None:
+        """Test the `create_special_filters` method with the `field_name` parameter."""
+        base_filters = OrderedDict([('name__trigram__gt', MagicMock())])
+        filters = AdvancedFilterSet.create_special_filters(base_filters, TrigramFilter, 'name')
+        expected_filters = [
+            ('name__trigram', TrigramFilter(field_name='name__trigram', lookup_expr='exact')),
+            ('name__trigram__gte', TrigramFilter(field_name='name__trigram', lookup_expr='gte')),
+            ('name__trigram__lt', TrigramFilter(field_name='name__trigram', lookup_expr='lt')),
+            ('name__trigram__lte', TrigramFilter(field_name='name__trigram', lookup_expr='lte')),
+        ]
+        self.assertFiltersEqual(expected_filters, filters.items())
+
+    @patch.object(
+        AdvancedFilterSet,
+        'get_full_text_search_fields',
+        new=MagicMock(return_value=OrderedDict()),
+    )
+    def test_create_full_text_search_filters_without_fields(self) -> None:
+        """Test the `create_full_text_search_filters` method without full text search fields."""
+        base_filters = OrderedDict()
+        filters = self.FullTextSearchFilterSet.create_full_text_search_filters(base_filters)
+        expected_filters = []
+        self.assertFiltersEqual(expected_filters, filters.items())
+
+    @patch(
+        'graphene_django_filter.conf.FIXED_SETTINGS', new={
+            'IS_POSTGRESQL': False,
+            'HAS_TRIGRAM_EXTENSION': False,
+        },
+    )
+    def test_create_full_text_search_filters_without_postgresql(self) -> None:
+        """Test the `create_full_text_search_filters` method if the database is not PostgreSQL."""
+        base_filters = OrderedDict()
+        with self.assertWarns(UserWarning):
+            filters = self.FullTextSearchFilterSet.create_full_text_search_filters(base_filters)
+        expected_filters = []
+        self.assertFiltersEqual(expected_filters, filters.items())
+
+    @patch(
+        'graphene_django_filter.conf.FIXED_SETTINGS', new={
+            'IS_POSTGRESQL': True,
+            'HAS_TRIGRAM_EXTENSION': False,
+        },
+    )
+    def test_create_full_text_search_filters_without_trigrams(self) -> None:
+        """Test the `create_full_text_search_filters` method.
+
+        The database has not `pg_trgm` extension.
+        """
+        base_filters = OrderedDict()
+        with self.assertWarns(UserWarning):
+            filters = self.FullTextSearchFilterSet.create_full_text_search_filters(base_filters)
+        expected_filters = [
+            *self.expected_search_query_filters,
+            *self.expected_search_rank_filters,
+        ]
+        self.assertFiltersEqual(expected_filters, filters.items())
+
+    def test_create_full_text_search_filter(self) -> None:
+        """Test the `create_full_text_search_filters` method with all filters."""
+        base_filters = OrderedDict(
+            [('search_query', SearchQueryFilter(field_name='', lookup_expr='exact'))],
+        )
+        filters = self.FullTextSearchFilterSet.create_full_text_search_filters(base_filters)
+        expected_filters = [
+            *self.expected_search_rank_filters,
+            *self.expected_trigram_filters,
+        ]
+        self.assertFiltersEqual(expected_filters, filters.items())
+
+    def test_get_filters(self) -> None:
+        """Test the `get_filters` method."""
+        filters = self.FullTextSearchFilterSet.get_filters()
+        expected_filters = [
+            *self.expected_regular_filters,
+            *self.expected_search_query_filters,
+            *self.expected_search_rank_filters,
+            *self.expected_trigram_filters,
+        ]
+        self.assertFiltersEqual(expected_filters, filters.items())
diff --git a/tests/test_input_data_factories.py b/tests/test_input_data_factories.py
new file mode 100644
index 0000000..94219b6
--- /dev/null
+++ b/tests/test_input_data_factories.py
@@ -0,0 +1,478 @@
+"""Input data factories tests."""
+
+from collections import OrderedDict
+from contextlib import contextmanager
+from datetime import datetime, timedelta
+from typing import Any, Generator, Tuple, Type, cast
+from unittest.mock import MagicMock, patch
+
+import graphene
+from django.contrib.postgres.search import (
+    SearchQuery,
+    SearchRank,
+    SearchVector,
+    TrigramDistance,
+    TrigramSimilarity,
+)
+from django.core.exceptions import ValidationError
+from django.db import models
+from django.test import TestCase
+from graphene.types.inputobjecttype import InputObjectTypeContainer
+from graphene_django_filter.filters import (
+    SearchQueryFilter,
+    SearchRankFilter,
+    TrigramFilter,
+)
+from graphene_django_filter.filterset import AdvancedFilterSet
+from graphene_django_filter.input_data_factories import (
+    create_data,
+    create_search_config,
+    create_search_query,
+    create_search_query_data,
+    create_search_rank_data,
+    create_search_rank_weights,
+    create_search_vector,
+    create_trigram_data,
+    tree_input_type_to_data,
+    validate_search_query,
+    validate_search_vector_fields,
+)
+from graphene_django_filter.input_types import (
+    FloatLookupsInputType,
+    SearchConfigInputType,
+    SearchQueryFilterInputType,
+    SearchQueryInputType,
+    SearchRankFilterInputType,
+    SearchRankWeightsInputType,
+    SearchVectorInputType,
+    SearchVectorWeight,
+    TrigramFilterInputType,
+    TrigramSearchKind,
+)
+
+
+class InputDataFactoriesTests(TestCase):
+    """Input data factories tests."""
+
+    filterset_class_mock = cast(
+        Type[AdvancedFilterSet],
+        MagicMock(
+            get_full_text_search_fields=MagicMock(
+                return_value=OrderedDict([
+                    ('field1', MagicMock()),
+                    ('field2', MagicMock()),
+                ]),
+            ),
+        ),
+    )
+
+    rank_weights_input_type = SearchRankWeightsInputType._meta.container({
+        'A': 0.9,
+        'B': SearchRankWeightsInputType.B.kwargs['default_value'],
+        'C': SearchRankWeightsInputType.C.kwargs['default_value'],
+        'D': SearchRankWeightsInputType.D.kwargs['default_value'],
+    })
+
+    invalid_search_query_input_type = SearchQueryInputType._meta.container({})
+    config_search_query_input_type = SearchConfigInputType._meta.container({
+        'config': SearchConfigInputType._meta.container({
+            'value': 'russian',
+            'is_field': False,
+        }),
+        'value': 'value',
+    })
+    expressions_search_query_input_type = SearchQueryInputType._meta.container({
+        'value': 'value1',
+        'and': [
+            SearchQueryInputType._meta.container({'value': 'and_value1'}),
+            SearchQueryInputType._meta.container({'value': 'and_value2'}),
+        ],
+        'or': [
+            SearchQueryInputType._meta.container({'value': 'or_value1'}),
+            SearchQueryInputType._meta.container({'value': 'or_value2'}),
+        ],
+        'not': SearchQueryInputType._meta.container({'value': 'not_value'}),
+    })
+
+    search_rank_input_type = SearchRankFilterInputType._meta.container({
+        'vector': MagicMock(),
+        'query': MagicMock(),
+        'lookups': FloatLookupsInputType._meta.container({'gt': 0.8, 'lt': 0.9}),
+        'weights': rank_weights_input_type,
+        'cover_density': True,
+        'normalization': 2,
+    })
+
+    @contextmanager
+    def patch_vector_and_query_factories(
+        self,
+    ) -> Generator[Tuple[MagicMock, MagicMock, MagicMock, MagicMock], Any, None]:
+        """Patch `create_search_vector` and `create_search_query` functions."""
+        with patch(
+            'graphene_django_filter.input_data_factories.create_search_vector',
+        ) as create_search_vector_mock, patch(
+            'graphene_django_filter.input_data_factories.create_search_query',
+        ) as create_search_query_mock:
+            search_vector_mock = MagicMock()
+            create_search_vector_mock.return_value = search_vector_mock
+            search_query_mock = MagicMock()
+            create_search_query_mock.return_value = search_query_mock
+            yield (
+                create_search_vector_mock,
+                search_vector_mock,
+                create_search_query_mock,
+                search_query_mock,
+            )
+
+    class TaskNameFilterInputType(graphene.InputObjectType):
+        exact = graphene.String()
+        trigram = graphene.InputField(TrigramFilterInputType)
+
+    class TaskDescriptionFilterInputType(graphene.InputObjectType):
+        exact = graphene.String()
+
+    class TaskUserEmailFilterInputType(graphene.InputObjectType):
+        exact = graphene.String()
+        iexact = graphene.String()
+        contains = graphene.String()
+        icontains = graphene.String()
+
+    class TaskUserLastNameFilterInputType(graphene.InputObjectType):
+        exact = graphene.String()
+
+    class TaskUserFilterInputType(graphene.InputObjectType):
+        exact = graphene.String()
+        email = graphene.InputField(
+            lambda: InputDataFactoriesTests.TaskUserEmailFilterInputType,
+        )
+        last_name = graphene.InputField(
+            lambda: InputDataFactoriesTests.TaskUserLastNameFilterInputType,
+        )
+
+    class TaskCreatedAtInputType(graphene.InputObjectType):
+        gt = graphene.DateTime()
+
+    class TaskCompletedAtInputType(graphene.InputObjectType):
+        lg = graphene.DateTime()
+
+    TaskFilterInputType = type(
+        'TaskFilterInputType',
+        (graphene.InputObjectType,),
+        {
+            'name': graphene.InputField(
+                lambda: InputDataFactoriesTests.TaskNameFilterInputType,
+            ),
+            'description': graphene.InputField(
+                lambda: InputDataFactoriesTests.TaskDescriptionFilterInputType,
+            ),
+            'user': graphene.InputField(
+                lambda: InputDataFactoriesTests.TaskUserFilterInputType,
+            ),
+            'created_at': graphene.InputField(
+                lambda: InputDataFactoriesTests.TaskCreatedAtInputType,
+            ),
+            'completed_at': graphene.InputField(
+                lambda: InputDataFactoriesTests.TaskCompletedAtInputType,
+            ),
+            'and': graphene.InputField(
+                graphene.List(lambda: InputDataFactoriesTests.TaskFilterInputType),
+            ),
+            'or': graphene.InputField(
+                graphene.List(lambda: InputDataFactoriesTests.TaskFilterInputType),
+            ),
+            'not': graphene.InputField(
+                lambda: InputDataFactoriesTests.TaskFilterInputType,
+            ),
+            'search_query': graphene.InputField(
+                SearchQueryFilterInputType,
+            ),
+            'search_rank': graphene.InputField(
+                SearchRankFilterInputType,
+            ),
+        },
+    )
+
+    task_filterset_class_mock = cast(
+        Type[AdvancedFilterSet],
+        MagicMock(
+            get_full_text_search_fields=MagicMock(
+                return_value=OrderedDict([
+                    ('name', MagicMock()),
+                ]),
+            ),
+        ),
+    )
+    gt_datetime = datetime.today() - timedelta(days=1)
+    lt_datetime = datetime.today()
+    tree_input_type = TaskFilterInputType._meta.container({
+        'name': TaskNameFilterInputType._meta.container({
+            'exact': 'Important task',
+            'trigram': TrigramFilterInputType._meta.container({
+                'kind': TrigramSearchKind.SIMILARITY,
+                'lookups': FloatLookupsInputType._meta.container({'gt': 0.8}),
+                'value': 'Buy some milk',
+            }),
+        }),
+        'description': TaskDescriptionFilterInputType._meta.container(
+            {'exact': 'This task is very important'},
+        ),
+        'user': TaskUserFilterInputType._meta.container(
+            {'email': TaskUserEmailFilterInputType._meta.container({'contains': 'dev'})},
+        ),
+        'and': [
+            TaskFilterInputType._meta.container({
+                'completed_at': TaskCompletedAtInputType._meta.container({'lt': lt_datetime}),
+            }),
+        ],
+        'or': [
+            TaskFilterInputType._meta.container({
+                'created_at': TaskCreatedAtInputType._meta.container({'gt': gt_datetime}),
+            }),
+        ],
+        'not': TaskFilterInputType._meta.container({
+            'user': TaskUserFilterInputType._meta.container(
+                {'first_name': TaskUserEmailFilterInputType._meta.container({'exact': 'John'})},
+            ),
+        }),
+        'search_query': SearchQueryFilterInputType._meta.container({
+            'vector': SearchVectorInputType._meta.container({'fields': ['name']}),
+            'query': SearchQueryInputType._meta.container({'value': 'Fix the bug'}),
+        }),
+        'search_rank': SearchRankFilterInputType._meta.container({
+            'vector': SearchVectorInputType._meta.container({'fields': ['name']}),
+            'query': SearchQueryInputType._meta.container({'value': 'Fix the bug'}),
+            'lookups': FloatLookupsInputType._meta.container({'gt': 0.8}),
+            'cover_density': False,
+        }),
+    })
+
+    def test_validate_search_query(self) -> None:
+        """Test the `validate_search_query` function."""
+        for key in ('value', 'and', 'or', 'not'):
+            search_query_input_type = SearchQueryInputType._meta.container({
+                key: 'value',
+            })
+            validate_search_query(search_query_input_type)
+        with self.assertRaisesMessage(
+            ValidationError,
+            'The search query must contains at least one required field '
+            'such as `value`, `and`, `or`, `not`.',
+        ):
+            validate_search_query(SearchQueryInputType._meta.container({}))
+
+    def test_validate_search_vector_fields(self) -> None:
+        """Test the `validate_search_vector_fields` function."""
+        validate_search_vector_fields(self.filterset_class_mock, ['field1', 'field2'])
+        with self.assertRaisesMessage(
+            ValidationError,
+            'The `field3` field is not included in full text search fields',
+        ):
+            validate_search_vector_fields(self.filterset_class_mock, ['field1', 'field2', 'field3'])
+
+    def test_create_search_rank_weights(self) -> None:
+        """Test the `create_search_rank_weights` function."""
+        self.assertEqual(
+            [0.1, 0.2, 0.4, 0.9],
+            create_search_rank_weights(self.rank_weights_input_type),
+        )
+
+    def test_create_search_config(self) -> None:
+        """Test the `create_search_config` function."""
+        string_input_type = SearchConfigInputType._meta.container({
+            'value': 'russian',
+            'is_field': False,
+        })
+        string_config = create_search_config(string_input_type)
+        self.assertEqual(string_input_type.value, string_config)
+        field_input_type = SearchConfigInputType._meta.container({
+            'value': 'russian',
+            'is_field': True,
+        })
+        field_config = create_search_config(field_input_type)
+        self.assertEqual(models.F('russian'), field_config)
+
+    def test_create_search_query(self) -> None:
+        """Test the `create_search_query` function."""
+        with self.assertRaises(ValidationError):
+            create_search_query(self.invalid_search_query_input_type)
+        config_search_query = create_search_query(self.config_search_query_input_type)
+        self.assertEqual(SearchQuery('value', config='russian'), config_search_query)
+        expressions_search_query = create_search_query(self.expressions_search_query_input_type)
+        self.assertEqual(
+            SearchQuery('value1') & (
+                SearchQuery('and_value1') & SearchQuery('and_value2')
+            ) & (
+                SearchQuery('or_value1') | SearchQuery('or_value2')
+            ) & ~SearchQuery('not_value'),
+            expressions_search_query,
+        )
+
+    def test_create_search_vector(self) -> None:
+        """Test the `create_search_vector` function."""
+        invalid_input_type = SearchVectorInputType._meta.container({
+            'fields': ['field1', 'field2', 'field3'],
+        })
+        with self.assertRaises(ValidationError):
+            create_search_vector(invalid_input_type, self.filterset_class_mock)
+        config_input_type = SearchVectorInputType._meta.container({
+            'fields': ['field1', 'field2'],
+            'config': SearchConfigInputType._meta.container({
+                'value': 'russian',
+            }),
+        })
+        config_search_vector = create_search_vector(config_input_type, self.filterset_class_mock)
+        self.assertEqual(SearchVector('field1', 'field2', config='russian'), config_search_vector)
+        weight_input_type = SearchVectorInputType._meta.container({
+            'fields': ['field1', 'field2'],
+            'weight': SearchVectorWeight.A,
+        })
+        weight_search_vector = create_search_vector(weight_input_type, self.filterset_class_mock)
+        self.assertEqual(SearchVector('field1', 'field2', weight='A'), weight_search_vector)
+
+    def test_create_trigram_data(self) -> None:
+        """Test the `create_trigram_data` function."""
+        for trigram_class in (TrigramSimilarity, TrigramDistance):
+            with self.subTest(trigram_class=trigram_class):
+                similarity_input_type = TrigramFilterInputType._meta.container({
+                    'kind': TrigramSearchKind.SIMILARITY
+                    if trigram_class == TrigramSimilarity else TrigramSearchKind.DISTANCE,
+                    'lookups': FloatLookupsInputType._meta.container({'gt': 0.8, 'lt': 0.9}),
+                    'value': 'value',
+                })
+                trigram_data = create_trigram_data(similarity_input_type, 'field__trigram')
+                expected_trigram_data = {
+                    'field__trigram__gt': TrigramFilter.Value(
+                        annotation_value=trigram_class('field', 'value'),
+                        search_value=0.8,
+                    ),
+                    'field__trigram__lt': TrigramFilter.Value(
+                        annotation_value=trigram_class('field', 'value'),
+                        search_value=0.9,
+                    ),
+                }
+                self.assertEqual(expected_trigram_data, trigram_data)
+
+    @patch.object(
+        SearchRank,
+        '__eq__',
+        new=lambda self, other: str(self) + self.function == str(other) + other.function,
+    )
+    def test_create_search_rank_data(self) -> None:
+        """Test the `create_search_rank_data` function."""
+        with self.patch_vector_and_query_factories() as mocks:
+            create_sv_mock, sv_mock, create_sq_mock, sq_mock = mocks
+            search_rank_data = create_search_rank_data(
+                self.search_rank_input_type,
+                'field',
+                self.filterset_class_mock,
+            )
+            expected_search_rank = SearchRank(
+                vector=sv_mock,
+                query=sq_mock,
+                weights=[0.1, 0.2, 0.4, 0.9],
+                cover_density=True,
+                normalization=2,
+            )
+            self.assertEqual(
+                {
+                    'field__gt': SearchRankFilter.Value(
+                        annotation_value=expected_search_rank,
+                        search_value=0.8,
+                    ),
+                    'field__lt': SearchRankFilter.Value(
+                        annotation_value=expected_search_rank,
+                        search_value=0.9,
+                    ),
+                }, search_rank_data,
+            )
+            create_sv_mock.assert_called_with(
+                self.search_rank_input_type.vector,
+                self.filterset_class_mock,
+            )
+            create_sq_mock.assert_called_with(self.search_rank_input_type.query)
+
+    def test_create_search_query_data(self) -> None:
+        """Test the `create_search_query_data` function."""
+        with self.patch_vector_and_query_factories() as mocks:
+            create_sv_mock, sv_mock, create_sq_mock, sq_mock = mocks
+            vector = MagicMock()
+            query = MagicMock()
+            search_query_data = create_search_query_data(
+                SearchQueryFilterInputType._meta.container({
+                    'vector': vector,
+                    'query': query,
+                }),
+                'field',
+                self.filterset_class_mock,
+            )
+            self.assertEqual(
+                {
+                    'field': SearchQueryFilter.Value(
+                        annotation_value=sv_mock,
+                        search_value=sq_mock,
+                    ),
+                }, search_query_data,
+            )
+            create_sv_mock.assert_called_once_with(vector, self.filterset_class_mock)
+            create_sq_mock.assert_called_once_with(query)
+
+    def test_create_data(self) -> None:
+        """Test the `create_data` function."""
+        with patch(
+            'graphene_django_filter.input_data_factories.DATA_FACTORIES',
+            new={
+                'search_query': MagicMock(return_value=MagicMock()),
+                'search_rank': MagicMock(return_value=MagicMock()),
+                'trigram': MagicMock(return_value=MagicMock()),
+            },
+        ):
+            from graphene_django_filter.input_data_factories import DATA_FACTORIES
+            for factory_key, factory in DATA_FACTORIES.items():
+                value = MagicMock()
+                self.assertEqual(
+                    factory.return_value,
+                    create_data(factory_key, value, self.filterset_class_mock),
+                )
+                factory.assert_called_once_with(value, factory_key, self.filterset_class_mock)
+        with patch('graphene_django_filter.input_data_factories.tree_input_type_to_data') as mock:
+            key = 'field'
+            value = MagicMock(spec=InputObjectTypeContainer)
+            mock.return_value = {key: value}
+            self.assertEqual(mock.return_value, create_data(key, value, self.filterset_class_mock))
+        key = 'field'
+        value = MagicMock()
+        self.assertEqual({key: value}, create_data(key, value, self.filterset_class_mock))
+
+    def test_tree_input_type_to_data(self) -> None:
+        """Test the `tree_input_type_to_data` function."""
+        data = tree_input_type_to_data(self.task_filterset_class_mock, self.tree_input_type)
+        expected_data = {
+            'name': 'Important task',
+            'name__trigram__gt': TrigramFilter.Value(
+                annotation_value=TrigramSimilarity('name', 'Buy some milk'),
+                search_value=0.8,
+            ),
+            'description': 'This task is very important',
+            'user__email__contains': 'dev',
+            'and': [{
+                'completed_at__lt': self.lt_datetime,
+            }],
+            'or': [{
+                'created_at__gt': self.gt_datetime,
+            }],
+            'not': {
+                'user__first_name': 'John',
+            },
+            'search_query': SearchQueryFilter.Value(
+                annotation_value=SearchVector('name'),
+                search_value=SearchQuery('Fix the bug'),
+            ),
+            'search_rank__gt': SearchRankFilter.Value(
+                annotation_value=SearchRank(
+                    vector=SearchVector('name'),
+                    query=SearchQuery('Fix the bug'),
+                ),
+                search_value=0.8,
+            ),
+        }
+        self.assertEqual(expected_data, data)
diff --git a/tests/test_input_type_factories.py b/tests/test_input_type_factories.py
index 3033f16..d97bfdf 100644
--- a/tests/test_input_type_factories.py
+++ b/tests/test_input_type_factories.py
@@ -5,7 +5,7 @@
 from anytree.exporter import DictExporter
 from django.test import TestCase
 from graphene_django_filter.input_type_factories import (
-    create_filter_input_subtype,
+    create_filter_input_subfield,
     create_filter_input_type,
     create_input_object_type,
     filterset_to_trees,
@@ -13,13 +13,18 @@
     sequence_to_tree,
     try_add_sequence,
 )
+from graphene_django_filter.input_types import (
+    SearchQueryFilterInputType,
+    SearchRankFilterInputType,
+    TrigramFilterInputType,
+)
 from stringcase import pascalcase
 
 from .filtersets import TaskFilter
 from .object_types import TaskFilterSetClassType
 
 
-class InputTypeBuildersTest(TestCase):
+class InputTypeFactoriesTests(TestCase):
     """Input type factories tests."""
 
     abstract_tree_root = Node(
@@ -36,7 +41,21 @@ class InputTypeBuildersTest(TestCase):
         ),
     )
     task_filter_trees_roots = [
-        Node(name='name', children=[Node(name='exact'), Node(name='contains')]),
+        Node(
+            name='name', children=[
+                Node(name='exact'),
+                Node(name='contains'),
+                Node(
+                    name='trigram', children=[
+                        Node(name='exact'),
+                        Node(name='gt'),
+                        Node(name='gte'),
+                        Node(name='lt'),
+                        Node(name='lte'),
+                    ],
+                ),
+            ],
+        ),
         Node(name='created_at', children=[Node(name='gt')]),
         Node(name='completed_at', children=[Node(name='lt')]),
         Node(name='description', children=[Node(name='exact'), Node(name='contains')]),
@@ -60,6 +79,16 @@ class InputTypeBuildersTest(TestCase):
                 ),
             ],
         ),
+        Node(name='search_query', children=[Node(name='exact')]),
+        Node(
+            name='search_rank', children=[
+                Node(name='exact'),
+                Node(name='gt'),
+                Node(name='gte'),
+                Node(name='lt'),
+                Node(name='lte'),
+            ],
+        ),
     ]
 
     def test_sequence_to_tree(self) -> None:
@@ -122,13 +151,16 @@ def test_create_input_object_type(self) -> None:
         self.assertTrue(issubclass(input_object_type, graphene.InputObjectType))
         self.assertTrue(hasattr(input_object_type, 'field'))
 
-    def test_create_filter_input_subtype(self) -> None:
-        """Test the `create_filter_input_subtype` function."""
-        input_object_type = create_filter_input_subtype(
+    def test_create_filter_input_subfield_without_special(self) -> None:
+        """Test the `create_filter_input_subfield` function without any special filters."""
+        input_field = create_filter_input_subfield(
             self.task_filter_trees_roots[4],
             TaskFilter,
             'Task',
+            'User field',
         )
+        self.assertEqual('User field', input_field.description)
+        input_object_type = input_field.type
         self.assertEqual('TaskUserFilterInputType', input_object_type.__name__)
         self.assertEqual('`Exact` lookup', getattr(input_object_type, 'exact').description)
         self.assertEqual('`LastName` subfield', getattr(input_object_type, 'last_name').description)
@@ -138,6 +170,37 @@ def test_create_filter_input_subtype(self) -> None:
         for attr in ('iexact', 'contains', 'icontains'):
             self.assertEqual(f'`{pascalcase(attr)}` lookup', getattr(email_type, attr).description)
 
+    def test_create_filter_input_subfield_with_search_query(self) -> None:
+        """Test the `create_filter_input_subfield` function with the search query filter."""
+        search_query_type = create_filter_input_subfield(
+            self.task_filter_trees_roots[5],
+            TaskFilter,
+            'Task',
+            'SearchQuery',
+        ).type
+        self.assertEqual(search_query_type, SearchQueryFilterInputType)
+
+    def test_create_filter_input_subtype_with_search_rank(self) -> None:
+        """Test the `create_filter_input_subtype` function with the search rank filter."""
+        search_rank_type = create_filter_input_subfield(
+            self.task_filter_trees_roots[6],
+            TaskFilter,
+            'Task',
+            'SearchRank',
+        ).type
+        self.assertEqual(search_rank_type, SearchRankFilterInputType)
+
+    def test_create_filter_input_subtype_with_trigram(self) -> None:
+        """Test the `create_filter_input_subtype` function with the trigram filter."""
+        input_object_type = create_filter_input_subfield(
+            self.task_filter_trees_roots[0],
+            TaskFilter,
+            'Task',
+            'Name field',
+        ).type
+        trigram_type = getattr(input_object_type, 'trigram').type
+        self.assertEqual(trigram_type, TrigramFilterInputType)
+
     def test_create_filter_input_type(self) -> None:
         """Test the `create_filter_input_type` function."""
         input_object_type = create_filter_input_type(
diff --git a/tests/test_queries_execution.py b/tests/test_queries_execution.py
new file mode 100644
index 0000000..dfa370d
--- /dev/null
+++ b/tests/test_queries_execution.py
@@ -0,0 +1,248 @@
+"""Queries execution tests."""
+
+from datetime import datetime
+from typing import List
+
+from django.test import TestCase
+from django.utils.timezone import make_aware
+from graphql.execution import ExecutionResult
+from graphql_relay import from_global_id
+
+from .data_generation import generate_data
+from .schema import schema
+
+
+def get_ids(execution_result: ExecutionResult, key: str) -> List[int]:
+    """Return identifiers from an execution result using a key."""
+    return sorted(
+        int(from_global_id(edge['node']['id'])[1]) for edge
+        in execution_result.data[key]['edges']
+    )
+
+
+class LogicalExpressionsTests(TestCase):
+    """Tests for executing queries with logical expressions."""
+
+    users_query = """
+        {
+            %s(
+                filter: {
+                    isActive: {exact: true}
+                    or: [
+                        {email: {contains: "alice"}}
+                        {firstName: {exact: "Jane"}}
+                    ]
+                }
+            ) {
+                edges {
+                    node {
+                        id
+                    }
+                }
+            }
+        }
+    """
+    users_fields_query = users_query % 'usersFields'
+    users_filterset_query = users_query % 'usersFilterset'
+    tasks_query = f"""
+        {{
+            %s(
+                filter: {{
+                    completedAt: {{
+                        lt: "{make_aware(datetime.strptime('02/02/2021', '%m/%d/%Y')).isoformat()}"
+                    }}
+                    createdAt: {{
+                        gt: "{make_aware(datetime.strptime('12/31/2019', '%m/%d/%Y')).isoformat()}"
+                    }}
+                    or: [
+                        {{name: {{contains: "Important"}}}}
+                        {{description: {{contains: "important"}}}}
+                    ]
+                }}
+            ) {{
+                edges {{
+                    node {{
+                        id
+                    }}
+                }}
+            }}
+        }}
+    """
+    tasks_fields_query = tasks_query % 'tasksFields'
+    tasks_filterset_query = tasks_query % 'tasksFilterset'
+    task_groups_query = """
+        {
+            %s(
+                filter: {
+                    or: [
+                        {name: {exact: "Task group №1"}}
+                        {
+                            and: [
+                                {priority: {gte: 5}}
+                                {priority: {lte: 10}}
+                            ]
+                        }
+                    ]
+                    and: [
+                        {not: {priority: {exact: 7}}}
+                        {not: {priority: {exact: 9}}}
+                    ]
+                }
+            ) {
+                edges {
+                    node {
+                        id
+                    }
+                }
+            }
+        }
+    """
+    task_groups_fields_query = task_groups_query % 'taskGroupsFields'
+    task_groups_filterset_query = task_groups_query % 'taskGroupsFilterset'
+
+    @classmethod
+    def setUpClass(cls) -> None:
+        """Set up `LogicalExpressionsTests` class."""
+        super().setUpClass()
+        generate_data()
+
+    def test_users_execution(self) -> None:
+        """Test the schema execution by querying users."""
+        expected = list(range(16, 51))
+        execution_result = schema.execute(self.users_fields_query)
+        self.assertEqual(expected, get_ids(execution_result, 'usersFields'))
+        execution_result = schema.execute(self.users_filterset_query)
+        self.assertEqual(expected, get_ids(execution_result, 'usersFilterset'))
+
+    def test_tasks_execution(self) -> None:
+        """Test the schema execution by querying tasks."""
+        expected = list(range(16, 76))
+        execution_result = schema.execute(self.tasks_fields_query)
+        self.assertEqual(expected, get_ids(execution_result, 'tasksFields'))
+        execution_result = schema.execute(self.tasks_filterset_query)
+        self.assertEqual(expected, get_ids(execution_result, 'tasksFilterset'))
+
+    def test_task_groups_execution(self) -> None:
+        """Test the schema execution by querying task groups."""
+        expected = [1, 5, 6, 8, 10]
+        execution_result = schema.execute(self.task_groups_fields_query)
+        self.assertEqual(expected, get_ids(execution_result, 'taskGroupsFields'))
+        execution_result = schema.execute(self.task_groups_filterset_query)
+        self.assertEqual(expected, get_ids(execution_result, 'taskGroupsFilterset'))
+
+
+class FullTextSearchTests(TestCase):
+    """Tests for executing queries with full text search."""
+
+    search_query_query = """
+        {
+            %s(
+                filter: {
+                    searchQuery: {
+                        vector: {
+                            fields: ["first_name"]
+                        }
+                        query: {
+                            or: [
+                                {value: "Bob"}
+                                {value: "Alice"}
+                            ]
+                        }
+                    }
+                }
+            ) {
+                edges {
+                    node {
+                        id
+                    }
+                }
+            }
+        }
+    """
+    search_query_fields_query = search_query_query % 'usersFields'
+    search_query_filterset_query = search_query_query % 'usersFilterset'
+    search_rank_query = """
+        {
+            %s(
+                filter: {
+                    searchRank: {
+                        vector: {fields: ["name"]}
+                        query: {value: "Important task №"}
+                        lookups: {gte: 0.08}
+                    }
+                }
+            ) {
+                edges {
+                    node {
+                        id
+                    }
+                }
+            }
+        }
+    """
+    search_rank_fields_query = search_rank_query % 'tasksFields'
+    search_rank_filterset_query = search_rank_query % 'tasksFilterset'
+    trigram_query = """
+        {
+            %s(
+                filter: {
+                    or: [
+                        {
+                            firstName: {
+                                trigram: {
+                                    value: "john"
+                                    lookups: {gte: 0.85}
+                                }
+                            }
+                        }
+                        {
+                            lastName: {
+                                trigram: {
+                                    value: "dou"
+                                    lookups: {gte: 0.85}
+                                }
+                            }
+                        }
+                    ]
+                }
+            ) {
+                edges {
+                    node {
+                        id
+                    }
+                }
+            }
+        }
+    """
+    trigram_fields_query = trigram_query % 'usersFields'
+    trigram_filterset_query = trigram_query % 'usersFilterset'
+
+    @classmethod
+    def setUpClass(cls) -> None:
+        """Set up `FullTextSearchTests` class."""
+        super().setUpClass()
+        generate_data()
+
+    def test_search_query_execution(self) -> None:
+        """Test the schema execution by a search query."""
+        expected = list(range(1, 31))
+        execution_result = schema.execute(self.search_query_fields_query)
+        self.assertEqual(expected, get_ids(execution_result, 'usersFields'))
+        execution_result = schema.execute(self.search_query_filterset_query)
+        self.assertEqual(expected, get_ids(execution_result, 'usersFilterset'))
+
+    def test_search_rank_execution(self) -> None:
+        """Test the schema execution by a search rank."""
+        expected = list(range(31, 76))
+        execution_result = schema.execute(self.search_rank_fields_query)
+        self.assertEqual(expected, get_ids(execution_result, 'tasksFields'))
+        execution_result = schema.execute(self.search_rank_filterset_query)
+        self.assertEqual(expected, get_ids(execution_result, 'tasksFilterset'))
+
+    def test_trigram_execution(self) -> None:
+        """Test the schema execution by a trigram."""
+        expected = list(range(31, 76))
+        execution_result = schema.execute(self.trigram_fields_query)
+        self.assertEqual(expected, get_ids(execution_result, 'usersFields'))
+        execution_result = schema.execute(self.trigram_filterset_query)
+        self.assertEqual(expected, get_ids(execution_result, 'usersFilterset'))