From 1bd4e4a7ea00c9bd5a4297bbd093fb31478918be Mon Sep 17 00:00:00 2001
From: Petr Stribny <petr@stribny.name>
Date: Tue, 16 Jul 2024 15:04:20 +0000
Subject: [PATCH] Allow filtering text-based lookups

---
 backend/src/baserow/contrib/database/apps.py  |  22 +
 .../contrib/database/fields/field_types.py    |  95 ++-
 .../contrib/database/fields/filter_support.py | 150 ++++
 .../django_expressions.py                     |  74 +-
 .../database/formula/types/formula_types.py   |  62 +-
 .../database/views/array_view_filters.py      | 172 ++++
 .../database/view/test_view_array_filters.py  | 754 ++++++++++++++++++
 ...ters_for_formula_arrays_based_on_text.json |   7 +
 web-frontend/locales/en.json                  |   9 +
 .../modules/database/arrayViewFilters.js      | 210 +++++
 .../database/fieldFilterCompatibility.js      |  82 ++
 web-frontend/modules/database/fieldTypes.js   |  74 +-
 .../modules/database/formula/formulaTypes.js  |  40 +
 web-frontend/modules/database/plugin.js       |  41 +
 .../modules/database/utils/fieldFilters.js    |  88 ++
 web-frontend/modules/database/viewFilters.js  |   1 -
 .../database/arrayViewFiltersMatch.spec.js    | 388 +++++++++
 17 files changed, 2253 insertions(+), 16 deletions(-)
 create mode 100644 backend/src/baserow/contrib/database/fields/filter_support.py
 create mode 100644 backend/src/baserow/contrib/database/views/array_view_filters.py
 create mode 100644 backend/tests/baserow/contrib/database/view/test_view_array_filters.py
 create mode 100644 changelog/entries/unreleased/feature/2727_add_array_filters_for_formula_arrays_based_on_text.json
 create mode 100644 web-frontend/modules/database/arrayViewFilters.js
 create mode 100644 web-frontend/modules/database/fieldFilterCompatibility.js
 create mode 100644 web-frontend/test/unit/database/arrayViewFiltersMatch.spec.js

diff --git a/backend/src/baserow/contrib/database/apps.py b/backend/src/baserow/contrib/database/apps.py
index 612a0afb0..b003ca161 100755
--- a/backend/src/baserow/contrib/database/apps.py
+++ b/backend/src/baserow/contrib/database/apps.py
@@ -385,6 +385,28 @@ class DatabaseConfig(AppConfig):
         view_filter_type_registry.register(UserIsViewFilterType())
         view_filter_type_registry.register(UserIsNotViewFilterType())
 
+        from .views.array_view_filters import (
+            HasEmptyValueViewFilterType,
+            HasNotEmptyValueViewFilterType,
+            HasNotValueContainsViewFilterType,
+            HasNotValueContainsWordViewFilterType,
+            HasNotValueEqualViewFilterType,
+            HasValueContainsViewFilterType,
+            HasValueContainsWordViewFilterType,
+            HasValueEqualViewFilterType,
+            HasValueLengthIsLowerThanViewFilterType,
+        )
+
+        view_filter_type_registry.register(HasValueEqualViewFilterType())
+        view_filter_type_registry.register(HasNotValueEqualViewFilterType())
+        view_filter_type_registry.register(HasValueContainsViewFilterType())
+        view_filter_type_registry.register(HasNotValueContainsViewFilterType())
+        view_filter_type_registry.register(HasValueContainsWordViewFilterType())
+        view_filter_type_registry.register(HasNotValueContainsWordViewFilterType())
+        view_filter_type_registry.register(HasValueLengthIsLowerThanViewFilterType())
+        view_filter_type_registry.register(HasEmptyValueViewFilterType())
+        view_filter_type_registry.register(HasNotEmptyValueViewFilterType())
+
         from .views.view_aggregations import (
             AverageViewAggregationType,
             DecileViewAggregationType,
diff --git a/backend/src/baserow/contrib/database/fields/field_types.py b/backend/src/baserow/contrib/database/fields/field_types.py
index bf68b1bf4..35e2ccdf9 100755
--- a/backend/src/baserow/contrib/database/fields/field_types.py
+++ b/backend/src/baserow/contrib/database/fields/field_types.py
@@ -77,6 +77,14 @@ from baserow.contrib.database.api.views.errors import (
 )
 from baserow.contrib.database.db.functions import RandomUUID
 from baserow.contrib.database.export_serialized import DatabaseExportSerializedStructure
+from baserow.contrib.database.fields.filter_support import (
+    FilterNotSupportedException,
+    HasValueContainsFilterSupport,
+    HasValueContainsWordFilterSupport,
+    HasValueEmptyFilterSupport,
+    HasValueFilterSupport,
+    HasValueLengthIsLowerThanFilterSupport,
+)
 from baserow.contrib.database.formula import (
     BASEROW_FORMULA_TYPE_ALLOWED_FIELDS,
     BaserowExpression,
@@ -149,6 +157,7 @@ from .expressions import extract_jsonb_array_values_to_single_string
 from .field_cache import FieldCache
 from .field_filters import (
     AnnotatedQ,
+    OptionallyAnnotatedQ,
     contains_filter,
     contains_word_filter,
     filename_contains_filter,
@@ -4308,7 +4317,14 @@ class PhoneNumberFieldType(CollationSortMixin, CharFieldMatchingRegexFieldType):
         return collate_expression(Value(value))
 
 
-class FormulaFieldType(ReadOnlyFieldType):
+class FormulaFieldType(
+    HasValueEmptyFilterSupport,
+    HasValueFilterSupport,
+    HasValueContainsFilterSupport,
+    HasValueContainsWordFilterSupport,
+    HasValueLengthIsLowerThanFilterSupport,
+    ReadOnlyFieldType,
+):
     type = "formula"
     model_class = FormulaField
 
@@ -4467,6 +4483,83 @@ class FormulaFieldType(ReadOnlyFieldType):
             rich_value=rich_value,
         )
 
+    def get_in_array_empty_query(self, field_name, model_field, field: FormulaField):
+        (
+            field_instance,
+            field_type,
+        ) = self._get_field_instance_and_type_from_formula_field(field)
+
+        if not isinstance(field_type, HasValueEmptyFilterSupport):
+            raise FilterNotSupportedException()
+
+        return field_type.get_in_array_empty_query(
+            field_name, model_field, field_instance
+        )
+
+    def get_in_array_is_query(
+        self,
+        field_name: str,
+        value: str,
+        model_field: models.Field,
+        field: FormulaField,
+    ) -> Q | OptionallyAnnotatedQ:
+        (
+            field_instance,
+            field_type,
+        ) = self._get_field_instance_and_type_from_formula_field(field)
+
+        if not isinstance(field_type, HasValueFilterSupport):
+            raise FilterNotSupportedException()
+
+        return field_type.get_in_array_is_query(
+            field_name, value, model_field, field_instance
+        )
+
+    def get_in_array_contains_query(
+        self, field_name, value, model_field, field: FormulaField
+    ):
+        (
+            field_instance,
+            field_type,
+        ) = self._get_field_instance_and_type_from_formula_field(field)
+
+        if not isinstance(field_type, HasValueContainsFilterSupport):
+            raise FilterNotSupportedException()
+
+        return field_type.get_in_array_contains_query(
+            field_name, value, model_field, field_instance
+        )
+
+    def get_in_array_contains_word_query(
+        self, field_name, value, model_field, field: FormulaField
+    ):
+        (
+            field_instance,
+            field_type,
+        ) = self._get_field_instance_and_type_from_formula_field(field)
+
+        if not isinstance(field_type, HasValueContainsWordFilterSupport):
+            raise FilterNotSupportedException()
+
+        return field_type.get_in_array_contains_word_query(
+            field_name, value, model_field, field_instance
+        )
+
+    def get_in_array_length_is_lower_than_query(
+        self, field_name, value, model_field, field: FormulaField
+    ):
+        (
+            field_instance,
+            field_type,
+        ) = self._get_field_instance_and_type_from_formula_field(field)
+
+        if not isinstance(field_type, HasValueLengthIsLowerThanFilterSupport):
+            raise FilterNotSupportedException()
+
+        return field_type.get_in_array_length_is_lower_than_query(
+            field_name, value, model_field, field_instance
+        )
+
     def contains_query(self, field_name, value, model_field, field: FormulaField):
         (
             field_instance,
diff --git a/backend/src/baserow/contrib/database/fields/filter_support.py b/backend/src/baserow/contrib/database/fields/filter_support.py
new file mode 100644
index 000000000..e8f3b0f99
--- /dev/null
+++ b/backend/src/baserow/contrib/database/fields/filter_support.py
@@ -0,0 +1,150 @@
+import re
+import typing
+
+from django.contrib.postgres.fields import JSONField
+from django.db import models
+from django.db.models import BooleanField, F, Q, Value
+
+from baserow.contrib.database.fields.field_filters import (
+    AnnotatedQ,
+    OptionallyAnnotatedQ,
+)
+from baserow.contrib.database.formula.expression_generator.django_expressions import (
+    JSONArrayContainsValueExpr,
+    JSONArrayContainsValueLengthLowerThanExpr,
+    JSONArrayContainsValueSimilarToExpr,
+)
+
+if typing.TYPE_CHECKING:
+    from baserow.contrib.database.fields.models import Field
+
+
+class FilterNotSupportedException(Exception):
+    pass
+
+
+class HasValueEmptyFilterSupport:
+    def get_in_array_empty_query(
+        self, field_name: str, model_field: models.Field, field: "Field"
+    ) -> OptionallyAnnotatedQ:
+        """
+        Specifies a Q expression to filter empty values contained in an array.
+
+        :param field_name: The name of the field.
+        :param model_field: The field's actual django field model instance.
+        :param field: The related field's instance.
+        :return: A Q or AnnotatedQ filter given value.
+        """
+
+        return Q(**{f"{field_name}__contains": Value([{"value": ""}], JSONField())})
+
+
+class HasValueFilterSupport:
+    def get_in_array_is_query(
+        self, field_name: str, value: str, model_field: models.Field, field: "Field"
+    ) -> OptionallyAnnotatedQ:
+        """
+        Specifies a Q expression to filter exact values contained in an array.
+
+        :param field_name: The name of the field.
+        :param value: The value to check if it is contained in array.
+        :param model_field: The field's actual django field model instance.
+        :param field: The related field's instance.
+        :return: A Q or AnnotatedQ filter given value.
+        """
+
+        if not value:
+            return Q()
+
+        return Q(**{f"{field_name}__contains": Value([{"value": value}], JSONField())})
+
+
+class HasValueContainsFilterSupport:
+    def get_in_array_contains_query(
+        self, field_name: str, value: str, model_field: models.Field, field: "Field"
+    ) -> OptionallyAnnotatedQ:
+        """
+        Specifies a Q expression to filter values in an array that contain a
+        specific value.
+
+        :param field_name: The name of the field.
+        :param value: The value to check if it is contained in array.
+        :param model_field: The field's actual django field model instance.
+        :param field: The related field's instance.
+        :return: A Q or AnnotatedQ filter given value.
+        """
+
+        if not value:
+            return Q()
+        annotation_query = JSONArrayContainsValueExpr(
+            F(field_name), Value(f"%{value}%"), output_field=BooleanField()
+        )
+        hashed_value = hash(value)
+        return AnnotatedQ(
+            annotation={
+                f"{field_name}_has_value_contains_{hashed_value}": annotation_query
+            },
+            q={f"{field_name}_has_value_contains_{hashed_value}": True},
+        )
+
+
+class HasValueContainsWordFilterSupport:
+    def get_in_array_contains_word_query(
+        self, field_name: str, value: str, model_field: models.Field, field: "Field"
+    ) -> OptionallyAnnotatedQ:
+        """
+        Specifies a Q expression to filter values in an array that contain a
+        specific word.
+
+        :param field_name: The name of the field.
+        :param value: The value to check if it is contained in array.
+        :param model_field: The field's actual django field model instance.
+        :param field: The related field's instance.
+        :return: A Q or AnnotatedQ filter given value.
+        """
+
+        value = value.strip()
+        if not value:
+            return Q()
+        value = re.escape(value.upper())
+        annotation_query = JSONArrayContainsValueSimilarToExpr(
+            F(field_name), Value(f"%\\m{value}\\M%"), output_field=BooleanField()
+        )
+        hashed_value = hash(value)
+        return AnnotatedQ(
+            annotation={
+                f"{field_name}_has_value_contains_word_{hashed_value}": annotation_query
+            },
+            q={f"{field_name}_has_value_contains_word_{hashed_value}": True},
+        )
+
+
+class HasValueLengthIsLowerThanFilterSupport:
+    def get_in_array_length_is_lower_than_query(
+        self, field_name: str, value: str, model_field: models.Field, field: "Field"
+    ) -> OptionallyAnnotatedQ:
+        """
+        Specifies a Q expression to filter values in an array that has lower
+        than length.
+
+        :param field_name: The name of the field.
+        :param value: The value representing the length to use for the check.
+        :param model_field: The field's actual django field model instance.
+        :param field: The related field's instance.
+        :return: A Q or AnnotatedQ filter given value.
+        """
+
+        value = value.strip()
+        if not value:
+            return Q()
+        converted_value = int(value)
+        annotation_query = JSONArrayContainsValueLengthLowerThanExpr(
+            F(field_name), Value(converted_value), output_field=BooleanField()
+        )
+        hashed_value = hash(value)
+        return AnnotatedQ(
+            annotation={
+                f"{field_name}_has_value_length_is_lower_than_{hashed_value}": annotation_query
+            },
+            q={f"{field_name}_has_value_length_is_lower_than_{hashed_value}": True},
+        )
diff --git a/backend/src/baserow/contrib/database/formula/expression_generator/django_expressions.py b/backend/src/baserow/contrib/database/formula/expression_generator/django_expressions.py
index c3de6cc91..7f8db40c6 100644
--- a/backend/src/baserow/contrib/database/formula/expression_generator/django_expressions.py
+++ b/backend/src/baserow/contrib/database/formula/expression_generator/django_expressions.py
@@ -116,18 +116,12 @@ class JSONArray(Func):
         )
 
 
-class FileNameContainsExpr(Expression):
-    # fmt: off
-    template = (
-        f"""
-        EXISTS(
-            SELECT attached_files ->> 'visible_name'
-            FROM JSONB_ARRAY_ELEMENTS(%(field_name)s) as attached_files
-            WHERE UPPER(attached_files ->> 'visible_name') LIKE UPPER(%(value)s)
-        )
-        """  # nosec B608
-    )
-    # fmt: on
+class BaserowFilterExpression(Expression):
+    """
+    Baserow expression that works with field_name and value
+    to provide expressions for filters. To use, subclass and
+    define the template.
+    """
 
     def __init__(self, field_name: F, value: Value, output_field: Field):
         super().__init__(output_field=output_field)
@@ -159,3 +153,59 @@ class FileNameContainsExpr(Expression):
             "value": sql_value,
         }
         return template % data, params_value
+
+
+class FileNameContainsExpr(BaserowFilterExpression):
+    # fmt: off
+    template = (
+        f"""
+        EXISTS(
+            SELECT attached_files ->> 'visible_name'
+            FROM JSONB_ARRAY_ELEMENTS(%(field_name)s) as attached_files
+            WHERE UPPER(attached_files ->> 'visible_name') LIKE UPPER(%(value)s)
+        )
+        """  # nosec B608
+    )
+    # fmt: on
+
+
+class JSONArrayContainsValueExpr(BaserowFilterExpression):
+    # fmt: off
+    template = (
+        f"""
+        EXISTS(
+            SELECT filtered_field ->> 'value'
+            FROM JSONB_ARRAY_ELEMENTS(%(field_name)s) as filtered_field
+            WHERE UPPER(filtered_field ->> 'value') LIKE UPPER(%(value)s)
+        )
+        """  # nosec B608
+    )
+    # fmt: on
+
+
+class JSONArrayContainsValueSimilarToExpr(BaserowFilterExpression):
+    # fmt: off
+    template = (
+        f"""
+        EXISTS(
+            SELECT filtered_field ->> 'value'
+            FROM JSONB_ARRAY_ELEMENTS(%(field_name)s) as filtered_field
+            WHERE UPPER(filtered_field ->> 'value') SIMILAR TO %(value)s
+        )
+        """  # nosec B608 %(value)s
+    )
+    # fmt: on
+
+
+class JSONArrayContainsValueLengthLowerThanExpr(BaserowFilterExpression):
+    # fmt: off
+    template = (
+        f"""
+        EXISTS(
+            SELECT filtered_field ->> 'value'
+            FROM JSONB_ARRAY_ELEMENTS(%(field_name)s) as filtered_field
+            WHERE LENGTH(filtered_field ->> 'value') < %(value)s
+        )
+        """  # nosec B608 %(value)s
+    )
+    # fmt: on
diff --git a/backend/src/baserow/contrib/database/formula/types/formula_types.py b/backend/src/baserow/contrib/database/formula/types/formula_types.py
index 620e52f27..74f42da92 100644
--- a/backend/src/baserow/contrib/database/formula/types/formula_types.py
+++ b/backend/src/baserow/contrib/database/formula/types/formula_types.py
@@ -20,6 +20,14 @@ from baserow.contrib.database.fields.expressions import (
     json_extract_path,
 )
 from baserow.contrib.database.fields.field_sortings import OptionallyAnnotatedOrderBy
+from baserow.contrib.database.fields.filter_support import (
+    FilterNotSupportedException,
+    HasValueContainsFilterSupport,
+    HasValueContainsWordFilterSupport,
+    HasValueEmptyFilterSupport,
+    HasValueFilterSupport,
+    HasValueLengthIsLowerThanFilterSupport,
+)
 from baserow.contrib.database.fields.mixins import get_date_time_format
 from baserow.contrib.database.fields.utils.duration import (
     D_H_M_S,
@@ -95,6 +103,11 @@ class BaserowFormulaBaseTextType(BaserowFormulaTypeHasEmptyBaserowExpression):
 
 
 class BaserowFormulaTextType(
+    HasValueEmptyFilterSupport,
+    HasValueFilterSupport,
+    HasValueContainsFilterSupport,
+    HasValueContainsWordFilterSupport,
+    HasValueLengthIsLowerThanFilterSupport,
     BaserowFormulaBaseTextType,
     BaserowFormulaTypeHasEmptyBaserowExpression,
     BaserowFormulaValidType,
@@ -961,7 +974,14 @@ class BaserowFormulaSingleFileType(BaserowJSONBObjectBaseType):
         )
 
 
-class BaserowFormulaArrayType(BaserowFormulaValidType):
+class BaserowFormulaArrayType(
+    HasValueEmptyFilterSupport,
+    HasValueFilterSupport,
+    HasValueContainsFilterSupport,
+    HasValueContainsWordFilterSupport,
+    HasValueLengthIsLowerThanFilterSupport,
+    BaserowFormulaValidType,
+):
     type = "array"
     user_overridable_formatting_option_fields = [
         "array_formula_type",
@@ -1123,6 +1143,46 @@ class BaserowFormulaArrayType(BaserowFormulaValidType):
     def contains_query(self, field_name, value, model_field, field):
         return Q()
 
+    def get_in_array_is_query(self, field_name, value, model_field, field):
+        if not isinstance(self.sub_type, HasValueFilterSupport):
+            raise FilterNotSupportedException()
+
+        return self.sub_type.get_in_array_is_query(
+            field_name, value, model_field, field
+        )
+
+    def get_in_array_empty_query(self, field_name, model_field, field):
+        if not isinstance(self.sub_type, HasValueEmptyFilterSupport):
+            raise FilterNotSupportedException()
+
+        return self.sub_type.get_in_array_empty_query(field_name, model_field, field)
+
+    def get_in_array_contains_query(self, field_name, value, model_field, field):
+        if not isinstance(self.sub_type, HasValueContainsFilterSupport):
+            raise FilterNotSupportedException()
+
+        return self.sub_type.get_in_array_contains_query(
+            field_name, value, model_field, field
+        )
+
+    def get_in_array_contains_word_query(self, field_name, value, model_field, field):
+        if not isinstance(self.sub_type, HasValueContainsWordFilterSupport):
+            raise FilterNotSupportedException()
+
+        return self.sub_type.get_in_array_contains_word_query(
+            field_name, value, model_field, field
+        )
+
+    def get_in_array_length_is_lower_than_query(
+        self, field_name, value, model_field, field
+    ):
+        if not isinstance(self.sub_type, HasValueLengthIsLowerThanFilterSupport):
+            raise FilterNotSupportedException()
+
+        return self.sub_type.get_in_array_length_is_lower_than_query(
+            field_name, value, model_field, field
+        )
+
     def get_alter_column_prepare_old_value(self, connection, from_field, to_field):
         return "p_in = '';"
 
diff --git a/backend/src/baserow/contrib/database/views/array_view_filters.py b/backend/src/baserow/contrib/database/views/array_view_filters.py
new file mode 100644
index 000000000..6f6dbcb1b
--- /dev/null
+++ b/backend/src/baserow/contrib/database/views/array_view_filters.py
@@ -0,0 +1,172 @@
+from baserow.contrib.database.fields.field_filters import OptionallyAnnotatedQ
+from baserow.contrib.database.fields.field_types import FormulaFieldType
+from baserow.contrib.database.fields.filter_support import (
+    FilterNotSupportedException,
+    HasValueContainsFilterSupport,
+    HasValueContainsWordFilterSupport,
+    HasValueEmptyFilterSupport,
+    HasValueFilterSupport,
+    HasValueLengthIsLowerThanFilterSupport,
+)
+from baserow.contrib.database.fields.registries import field_type_registry
+from baserow.contrib.database.formula import BaserowFormulaTextType
+
+from .registries import ViewFilterType
+from .view_filters import NotViewFilterTypeMixin
+
+
+class HasEmptyValueViewFilterType(ViewFilterType):
+    """
+    The filter can be used to check for empty condition for
+    items in an array.
+    """
+
+    type = "has_empty_value"
+    compatible_field_types = [
+        FormulaFieldType.compatible_with_formula_types(
+            FormulaFieldType.array_of(BaserowFormulaTextType.type),
+        ),
+    ]
+
+    def get_filter(self, field_name, value, model_field, field) -> OptionallyAnnotatedQ:
+        try:
+            field_type = field_type_registry.get_by_model(field)
+
+            if not isinstance(field_type, HasValueEmptyFilterSupport):
+                raise FilterNotSupportedException()
+
+            return field_type.get_in_array_empty_query(field_name, model_field, field)
+        except Exception:
+            return self.default_filter_on_exception()
+
+
+class HasNotEmptyValueViewFilterType(
+    NotViewFilterTypeMixin, HasEmptyValueViewFilterType
+):
+    type = "has_not_empty_value"
+
+
+class HasValueEqualViewFilterType(ViewFilterType):
+    """
+    The filter can be used to check for "is" condition for
+    items in an array.
+    """
+
+    type = "has_value_equal"
+    compatible_field_types = [
+        FormulaFieldType.compatible_with_formula_types(
+            FormulaFieldType.array_of(BaserowFormulaTextType.type),
+        ),
+    ]
+
+    def get_filter(self, field_name, value, model_field, field) -> OptionallyAnnotatedQ:
+        try:
+            field_type = field_type_registry.get_by_model(field)
+
+            if not isinstance(field_type, HasValueFilterSupport):
+                raise FilterNotSupportedException()
+
+            return field_type.get_in_array_is_query(
+                field_name, value, model_field, field
+            )
+        except Exception:
+            return self.default_filter_on_exception()
+
+
+class HasNotValueEqualViewFilterType(
+    NotViewFilterTypeMixin, HasValueEqualViewFilterType
+):
+    type = "has_not_value_equal"
+
+
+class HasValueContainsViewFilterType(ViewFilterType):
+    """
+    The filter can be used to check for "contains" condition for
+    items in an array.
+    """
+
+    type = "has_value_contains"
+    compatible_field_types = [
+        FormulaFieldType.compatible_with_formula_types(
+            FormulaFieldType.array_of(BaserowFormulaTextType.type),
+        ),
+    ]
+
+    def get_filter(self, field_name, value, model_field, field) -> OptionallyAnnotatedQ:
+        try:
+            field_type = field_type_registry.get_by_model(field)
+
+            if not isinstance(field_type, HasValueContainsFilterSupport):
+                raise FilterNotSupportedException()
+
+            return field_type.get_in_array_contains_query(
+                field_name, value, model_field, field
+            )
+        except Exception:
+            return self.default_filter_on_exception()
+
+
+class HasNotValueContainsViewFilterType(
+    NotViewFilterTypeMixin, HasValueContainsViewFilterType
+):
+    type = "has_not_value_contains"
+
+
+class HasValueContainsWordViewFilterType(ViewFilterType):
+    """
+    The filter can be used to check for "contains word" condition
+    for items in an array.
+    """
+
+    type = "has_value_contains_word"
+    compatible_field_types = [
+        FormulaFieldType.compatible_with_formula_types(
+            FormulaFieldType.array_of(BaserowFormulaTextType.type),
+        ),
+    ]
+
+    def get_filter(self, field_name, value, model_field, field) -> OptionallyAnnotatedQ:
+        try:
+            field_type = field_type_registry.get_by_model(field)
+
+            if not isinstance(field_type, HasValueContainsWordFilterSupport):
+                raise FilterNotSupportedException()
+
+            return field_type.get_in_array_contains_word_query(
+                field_name, value, model_field, field
+            )
+        except Exception:
+            return self.default_filter_on_exception()
+
+
+class HasNotValueContainsWordViewFilterType(
+    NotViewFilterTypeMixin, HasValueContainsWordViewFilterType
+):
+    type = "has_not_value_contains_word"
+
+
+class HasValueLengthIsLowerThanViewFilterType(ViewFilterType):
+    """
+    The filter can be used to check for "length is lower than" condition
+    for items in an array.
+    """
+
+    type = "has_value_length_is_lower_than"
+    compatible_field_types = [
+        FormulaFieldType.compatible_with_formula_types(
+            FormulaFieldType.array_of(BaserowFormulaTextType.type),
+        ),
+    ]
+
+    def get_filter(self, field_name, value, model_field, field) -> OptionallyAnnotatedQ:
+        try:
+            field_type = field_type_registry.get_by_model(field)
+
+            if not isinstance(field_type, HasValueLengthIsLowerThanFilterSupport):
+                raise FilterNotSupportedException()
+
+            return field_type.get_in_array_length_is_lower_than_query(
+                field_name, value, model_field, field
+            )
+        except Exception:
+            return self.default_filter_on_exception()
diff --git a/backend/tests/baserow/contrib/database/view/test_view_array_filters.py b/backend/tests/baserow/contrib/database/view/test_view_array_filters.py
new file mode 100644
index 000000000..d809023ff
--- /dev/null
+++ b/backend/tests/baserow/contrib/database/view/test_view_array_filters.py
@@ -0,0 +1,754 @@
+from dataclasses import dataclass
+
+from django.contrib.auth.models import AbstractUser
+
+import pytest
+
+from baserow.contrib.database.fields.models import Field, LinkRowField, LookupField
+from baserow.contrib.database.rows.handler import RowHandler
+from baserow.contrib.database.table.models import GeneratedTableModel, Table
+from baserow.contrib.database.views.handler import ViewHandler
+from baserow.contrib.database.views.models import GridView
+
+
+@dataclass
+class ArrayFiltersSetup:
+    user: AbstractUser
+    table: Table
+    model: GeneratedTableModel
+    other_table_model: GeneratedTableModel
+    grid_view: GridView
+    link_row_field: LinkRowField
+    lookup_field: LookupField
+    target_field: Field
+    row_handler: RowHandler
+    view_handler: ViewHandler
+
+
+def text_field_factory(data_fixture, table, user):
+    return data_fixture.create_text_field(name="target", user=user, table=table)
+
+
+def long_text_field_factory(data_fixture, table, user):
+    return data_fixture.create_long_text_field(name="target", user=user, table=table)
+
+
+def setup(data_fixture, target_field_factory):
+    user = data_fixture.create_user()
+    database = data_fixture.create_database_application(user=user)
+    table = data_fixture.create_database_table(user=user, database=database)
+    other_table = data_fixture.create_database_table(user=user, database=database)
+    target_field = target_field_factory(data_fixture, other_table, user)
+    link_row_field = data_fixture.create_link_row_field(
+        name="link", table=table, link_row_table=other_table
+    )
+    lookup_field = data_fixture.create_lookup_field(
+        table=table,
+        through_field=link_row_field,
+        target_field=target_field,
+        through_field_name=link_row_field.name,
+        target_field_name=target_field.name,
+        setup_dependencies=False,
+    )
+    grid_view = data_fixture.create_grid_view(table=table)
+    view_handler = ViewHandler()
+    row_handler = RowHandler()
+    model = table.get_model()
+    other_table_model = other_table.get_model()
+    return ArrayFiltersSetup(
+        user=user,
+        table=table,
+        other_table_model=other_table_model,
+        target_field=target_field,
+        row_handler=row_handler,
+        grid_view=grid_view,
+        link_row_field=link_row_field,
+        lookup_field=lookup_field,
+        view_handler=view_handler,
+        model=model,
+    )
+
+
+@pytest.mark.parametrize(
+    "target_field_factory", [text_field_factory, long_text_field_factory]
+)
+@pytest.mark.django_db
+def test_has_empty_value_filter_text_field_types(data_fixture, target_field_factory):
+    test_setup = setup(data_fixture, target_field_factory)
+
+    other_row_A = test_setup.other_table_model.objects.create(
+        **{f"field_{test_setup.target_field.id}": "A"}
+    )
+    other_row_B = test_setup.other_table_model.objects.create(
+        **{f"field_{test_setup.target_field.id}": "B"}
+    )
+    other_row_empty = test_setup.other_table_model.objects.create(
+        **{f"field_{test_setup.target_field.id}": ""}
+    )
+    row_1 = test_setup.row_handler.create_row(
+        user=test_setup.user,
+        table=test_setup.table,
+        values={
+            f"field_{test_setup.link_row_field.id}": [
+                other_row_A.id,
+                other_row_empty.id,
+            ]
+        },
+    )
+    row_2 = test_setup.row_handler.create_row(
+        user=test_setup.user,
+        table=test_setup.table,
+        values={f"field_{test_setup.link_row_field.id}": []},
+    )
+    row_3 = test_setup.row_handler.create_row(
+        user=test_setup.user,
+        table=test_setup.table,
+        values={f"field_{test_setup.link_row_field.id}": [other_row_B.id]},
+    )
+
+    view_filter = data_fixture.create_view_filter(
+        view=test_setup.grid_view,
+        field=test_setup.lookup_field,
+        type="has_empty_value",
+        value="",
+    )
+    ids = [
+        r.id
+        for r in test_setup.view_handler.apply_filters(
+            test_setup.grid_view, test_setup.model.objects.all()
+        ).all()
+    ]
+    assert len(ids) == 1
+    assert row_1.id in ids
+
+
+@pytest.mark.parametrize(
+    "target_field_factory", [text_field_factory, long_text_field_factory]
+)
+@pytest.mark.django_db
+def test_has_not_empty_value_filter_text_field_types(
+    data_fixture, target_field_factory
+):
+    test_setup = setup(data_fixture, target_field_factory)
+
+    other_row_A = test_setup.other_table_model.objects.create(
+        **{f"field_{test_setup.target_field.id}": "A"}
+    )
+    other_row_B = test_setup.other_table_model.objects.create(
+        **{f"field_{test_setup.target_field.id}": "B"}
+    )
+    other_row_empty = test_setup.other_table_model.objects.create(
+        **{f"field_{test_setup.target_field.id}": ""}
+    )
+    row_1 = test_setup.row_handler.create_row(
+        user=test_setup.user,
+        table=test_setup.table,
+        values={
+            f"field_{test_setup.link_row_field.id}": [
+                other_row_A.id,
+                other_row_empty.id,
+            ]
+        },
+    )
+    row_2 = test_setup.row_handler.create_row(
+        user=test_setup.user,
+        table=test_setup.table,
+        values={f"field_{test_setup.link_row_field.id}": []},
+    )
+    row_3 = test_setup.row_handler.create_row(
+        user=test_setup.user,
+        table=test_setup.table,
+        values={f"field_{test_setup.link_row_field.id}": [other_row_B.id]},
+    )
+
+    view_filter = data_fixture.create_view_filter(
+        view=test_setup.grid_view,
+        field=test_setup.lookup_field,
+        type="has_not_empty_value",
+        value="",
+    )
+    ids = [
+        r.id
+        for r in test_setup.view_handler.apply_filters(
+            test_setup.grid_view, test_setup.model.objects.all()
+        ).all()
+    ]
+    assert len(ids) == 2
+    assert row_2.id in ids
+    assert row_3.id in ids
+
+
+@pytest.mark.parametrize(
+    "target_field_factory", [text_field_factory, long_text_field_factory]
+)
+@pytest.mark.django_db
+def test_has_value_equal_filter_text_field_types(data_fixture, target_field_factory):
+    test_setup = setup(data_fixture, target_field_factory)
+
+    other_row_A = test_setup.other_table_model.objects.create(
+        **{f"field_{test_setup.target_field.id}": "A"}
+    )
+    other_row_B = test_setup.other_table_model.objects.create(
+        **{f"field_{test_setup.target_field.id}": "B"}
+    )
+    other_row_C = test_setup.other_table_model.objects.create(
+        **{f"field_{test_setup.target_field.id}": "C"}
+    )
+    other_row_a = test_setup.other_table_model.objects.create(
+        **{f"field_{test_setup.target_field.id}": "a"}
+    )
+    row_1 = test_setup.row_handler.create_row(
+        user=test_setup.user,
+        table=test_setup.table,
+        values={
+            f"field_{test_setup.link_row_field.id}": [other_row_A.id, other_row_B.id]
+        },
+    )
+    row_2 = test_setup.row_handler.create_row(
+        user=test_setup.user,
+        table=test_setup.table,
+        values={f"field_{test_setup.link_row_field.id}": [other_row_a.id]},
+    )
+    row_3 = test_setup.row_handler.create_row(
+        user=test_setup.user,
+        table=test_setup.table,
+        values={
+            f"field_{test_setup.link_row_field.id}": [other_row_B.id, other_row_a.id]
+        },
+    )
+
+    view_filter = data_fixture.create_view_filter(
+        view=test_setup.grid_view,
+        field=test_setup.lookup_field,
+        type="has_value_equal",
+        value="A",
+    )
+    ids = [
+        r.id
+        for r in test_setup.view_handler.apply_filters(
+            test_setup.grid_view, test_setup.model.objects.all()
+        ).all()
+    ]
+    assert len(ids) == 1
+    assert row_1.id in ids
+
+    view_filter.value = "a"
+    view_filter.save()
+    ids = [
+        r.id
+        for r in test_setup.view_handler.apply_filters(
+            test_setup.grid_view, test_setup.model.objects.all()
+        ).all()
+    ]
+    assert len(ids) == 2
+    assert row_2.id in ids
+    assert row_3.id in ids
+
+    view_filter.value = "C"
+    view_filter.save()
+    ids = [
+        r.id
+        for r in test_setup.view_handler.apply_filters(
+            test_setup.grid_view, test_setup.model.objects.all()
+        ).all()
+    ]
+    assert len(ids) == 0
+
+    view_filter.value = ""
+    view_filter.save()
+    ids = [
+        r.id
+        for r in test_setup.view_handler.apply_filters(
+            test_setup.grid_view, test_setup.model.objects.all()
+        ).all()
+    ]
+    assert len(ids) == 3
+
+
+@pytest.mark.parametrize(
+    "target_field_factory", [text_field_factory, long_text_field_factory]
+)
+@pytest.mark.django_db
+def test_has_not_value_equal_filter_text_field_types(
+    data_fixture, target_field_factory
+):
+    test_setup = setup(data_fixture, target_field_factory)
+
+    other_row_A = test_setup.other_table_model.objects.create(
+        **{f"field_{test_setup.target_field.id}": "A"}
+    )
+    other_row_B = test_setup.other_table_model.objects.create(
+        **{f"field_{test_setup.target_field.id}": "B"}
+    )
+    other_row_C = test_setup.other_table_model.objects.create(
+        **{f"field_{test_setup.target_field.id}": "C"}
+    )
+    row_1 = test_setup.row_handler.create_row(
+        user=test_setup.user,
+        table=test_setup.table,
+        values={
+            f"field_{test_setup.link_row_field.id}": [other_row_A.id, other_row_B.id]
+        },
+    )
+    row_2 = test_setup.row_handler.create_row(
+        user=test_setup.user,
+        table=test_setup.table,
+        values={f"field_{test_setup.link_row_field.id}": []},
+    )
+    row_3 = test_setup.row_handler.create_row(
+        user=test_setup.user,
+        table=test_setup.table,
+        values={f"field_{test_setup.link_row_field.id}": [other_row_B.id]},
+    )
+
+    view_filter = data_fixture.create_view_filter(
+        view=test_setup.grid_view,
+        field=test_setup.lookup_field,
+        type="has_not_value_equal",
+        value="A",
+    )
+    ids = [
+        r.id
+        for r in test_setup.view_handler.apply_filters(
+            test_setup.grid_view, test_setup.model.objects.all()
+        ).all()
+    ]
+    assert len(ids) == 2
+    assert row_2.id in ids
+    assert row_3.id in ids
+
+    view_filter.value = "a"
+    view_filter.save()
+    ids = [
+        r.id
+        for r in test_setup.view_handler.apply_filters(
+            test_setup.grid_view, test_setup.model.objects.all()
+        ).all()
+    ]
+    assert len(ids) == 3
+
+    view_filter.value = "C"
+    view_filter.save()
+    ids = [
+        r.id
+        for r in test_setup.view_handler.apply_filters(
+            test_setup.grid_view, test_setup.model.objects.all()
+        ).all()
+    ]
+    assert len(ids) == 3
+
+    view_filter.value = ""
+    view_filter.save()
+    ids = [
+        r.id
+        for r in test_setup.view_handler.apply_filters(
+            test_setup.grid_view, test_setup.model.objects.all()
+        ).all()
+    ]
+    assert len(ids) == 3
+
+
+@pytest.mark.parametrize(
+    "target_field_factory", [text_field_factory, long_text_field_factory]
+)
+@pytest.mark.django_db
+def test_has_value_contains_filter_text_field_types(data_fixture, target_field_factory):
+    test_setup = setup(data_fixture, target_field_factory)
+
+    other_row_John_Smith = test_setup.other_table_model.objects.create(
+        **{f"field_{test_setup.target_field.id}": "John Smith"}
+    )
+    other_row_Anna_Smith = test_setup.other_table_model.objects.create(
+        **{f"field_{test_setup.target_field.id}": "Anna Smith"}
+    )
+    other_row_John_Wick = test_setup.other_table_model.objects.create(
+        **{f"field_{test_setup.target_field.id}": "John Wick"}
+    )
+    row_1 = test_setup.row_handler.create_row(
+        user=test_setup.user,
+        table=test_setup.table,
+        values={f"field_{test_setup.link_row_field.id}": [other_row_John_Smith.id]},
+    )
+    row_2 = test_setup.row_handler.create_row(
+        user=test_setup.user,
+        table=test_setup.table,
+        values={f"field_{test_setup.link_row_field.id}": []},
+    )
+    row_3 = test_setup.row_handler.create_row(
+        user=test_setup.user,
+        table=test_setup.table,
+        values={f"field_{test_setup.link_row_field.id}": [other_row_Anna_Smith.id]},
+    )
+    row_4 = test_setup.row_handler.create_row(
+        user=test_setup.user,
+        table=test_setup.table,
+        values={f"field_{test_setup.link_row_field.id}": [other_row_John_Wick.id]},
+    )
+
+    view_filter = data_fixture.create_view_filter(
+        view=test_setup.grid_view,
+        field=test_setup.lookup_field,
+        type="has_value_contains",
+        value="smith",
+    )
+    ids = [
+        r.id
+        for r in test_setup.view_handler.apply_filters(
+            test_setup.grid_view, test_setup.model.objects.all()
+        ).all()
+    ]
+    assert len(ids) == 2
+    assert row_1.id in ids
+    assert row_3.id in ids
+
+    view_filter.value = "john"
+    view_filter.save()
+    ids = [
+        r.id
+        for r in test_setup.view_handler.apply_filters(
+            test_setup.grid_view, test_setup.model.objects.all()
+        ).all()
+    ]
+    assert len(ids) == 2
+    assert row_1.id in ids
+    assert row_4.id in ids
+
+    view_filter.value = ""
+    view_filter.save()
+    ids = [
+        r.id
+        for r in test_setup.view_handler.apply_filters(
+            test_setup.grid_view, test_setup.model.objects.all()
+        ).all()
+    ]
+    assert len(ids) == 4
+
+
+@pytest.mark.parametrize(
+    "target_field_factory", [text_field_factory, long_text_field_factory]
+)
+@pytest.mark.django_db
+def test_has_not_value_contains_filter_text_field_types(
+    data_fixture, target_field_factory
+):
+    test_setup = setup(data_fixture, target_field_factory)
+
+    other_row_John_Smith = test_setup.other_table_model.objects.create(
+        **{f"field_{test_setup.target_field.id}": "John Smith"}
+    )
+    other_row_Anna_Smith = test_setup.other_table_model.objects.create(
+        **{f"field_{test_setup.target_field.id}": "Anna Smith"}
+    )
+    other_row_John_Wick = test_setup.other_table_model.objects.create(
+        **{f"field_{test_setup.target_field.id}": "John Wick"}
+    )
+    row_1 = test_setup.row_handler.create_row(
+        user=test_setup.user,
+        table=test_setup.table,
+        values={f"field_{test_setup.link_row_field.id}": [other_row_John_Smith.id]},
+    )
+    row_2 = test_setup.row_handler.create_row(
+        user=test_setup.user,
+        table=test_setup.table,
+        values={f"field_{test_setup.link_row_field.id}": []},
+    )
+    row_3 = test_setup.row_handler.create_row(
+        user=test_setup.user,
+        table=test_setup.table,
+        values={f"field_{test_setup.link_row_field.id}": [other_row_Anna_Smith.id]},
+    )
+    row_4 = test_setup.row_handler.create_row(
+        user=test_setup.user,
+        table=test_setup.table,
+        values={f"field_{test_setup.link_row_field.id}": [other_row_John_Wick.id]},
+    )
+
+    view_filter = data_fixture.create_view_filter(
+        view=test_setup.grid_view,
+        field=test_setup.lookup_field,
+        type="has_not_value_contains",
+        value="smith",
+    )
+    ids = [
+        r.id
+        for r in test_setup.view_handler.apply_filters(
+            test_setup.grid_view, test_setup.model.objects.all()
+        ).all()
+    ]
+    assert len(ids) == 2
+    assert row_2.id in ids
+    assert row_4.id in ids
+
+    view_filter.value = "john"
+    view_filter.save()
+    ids = [
+        r.id
+        for r in test_setup.view_handler.apply_filters(
+            test_setup.grid_view, test_setup.model.objects.all()
+        ).all()
+    ]
+    assert len(ids) == 2
+    assert row_2.id in ids
+    assert row_3.id in ids
+
+    view_filter.value = ""
+    view_filter.save()
+    ids = [
+        r.id
+        for r in test_setup.view_handler.apply_filters(
+            test_setup.grid_view, test_setup.model.objects.all()
+        ).all()
+    ]
+    assert len(ids) == 4
+
+
+@pytest.mark.parametrize(
+    "target_field_factory", [text_field_factory, long_text_field_factory]
+)
+@pytest.mark.django_db
+def test_has_value_contains_word_filter_text_field_types(
+    data_fixture, target_field_factory
+):
+    test_setup = setup(data_fixture, target_field_factory)
+
+    other_row_1 = test_setup.other_table_model.objects.create(
+        **{f"field_{test_setup.target_field.id}": "This is a sentence."}
+    )
+    other_row_2 = test_setup.other_table_model.objects.create(
+        **{f"field_{test_setup.target_field.id}": "Another Sentence."}
+    )
+    other_row_3 = test_setup.other_table_model.objects.create(
+        **{f"field_{test_setup.target_field.id}": ""}
+    )
+    row_1 = test_setup.row_handler.create_row(
+        user=test_setup.user,
+        table=test_setup.table,
+        values={
+            f"field_{test_setup.link_row_field.id}": [other_row_1.id, other_row_3.id]
+        },
+    )
+    row_2 = test_setup.row_handler.create_row(
+        user=test_setup.user,
+        table=test_setup.table,
+        values={f"field_{test_setup.link_row_field.id}": [other_row_3.id]},
+    )
+    row_3 = test_setup.row_handler.create_row(
+        user=test_setup.user,
+        table=test_setup.table,
+        values={f"field_{test_setup.link_row_field.id}": [other_row_2.id]},
+    )
+    row_4 = test_setup.row_handler.create_row(
+        user=test_setup.user,
+        table=test_setup.table,
+        values={f"field_{test_setup.link_row_field.id}": []},
+    )
+
+    view_filter = data_fixture.create_view_filter(
+        view=test_setup.grid_view,
+        field=test_setup.lookup_field,
+        type="has_value_contains_word",
+        value="sentence",
+    )
+    ids = [
+        r.id
+        for r in test_setup.view_handler.apply_filters(
+            test_setup.grid_view, test_setup.model.objects.all()
+        ).all()
+    ]
+    assert len(ids) == 2
+    assert row_1.id in ids
+    assert row_3.id in ids
+
+    view_filter.value = "Sentence"
+    view_filter.save()
+    ids = [
+        r.id
+        for r in test_setup.view_handler.apply_filters(
+            test_setup.grid_view, test_setup.model.objects.all()
+        ).all()
+    ]
+    assert len(ids) == 2
+    assert row_1.id in ids
+    assert row_3.id in ids
+
+    view_filter.value = ""
+    view_filter.save()
+    ids = [
+        r.id
+        for r in test_setup.view_handler.apply_filters(
+            test_setup.grid_view, test_setup.model.objects.all()
+        ).all()
+    ]
+    assert len(ids) == 4
+
+
+@pytest.mark.parametrize(
+    "target_field_factory", [text_field_factory, long_text_field_factory]
+)
+@pytest.mark.django_db
+def test_has_not_value_contains_word_filter_text_field_types(
+    data_fixture, target_field_factory
+):
+    test_setup = setup(data_fixture, target_field_factory)
+
+    other_row_1 = test_setup.other_table_model.objects.create(
+        **{f"field_{test_setup.target_field.id}": "This is a sentence."}
+    )
+    other_row_2 = test_setup.other_table_model.objects.create(
+        **{f"field_{test_setup.target_field.id}": "Another Sentence."}
+    )
+    other_row_3 = test_setup.other_table_model.objects.create(
+        **{f"field_{test_setup.target_field.id}": ""}
+    )
+    row_1 = test_setup.row_handler.create_row(
+        user=test_setup.user,
+        table=test_setup.table,
+        values={
+            f"field_{test_setup.link_row_field.id}": [other_row_1.id, other_row_3.id]
+        },
+    )
+    row_2 = test_setup.row_handler.create_row(
+        user=test_setup.user,
+        table=test_setup.table,
+        values={f"field_{test_setup.link_row_field.id}": [other_row_3.id]},
+    )
+    row_3 = test_setup.row_handler.create_row(
+        user=test_setup.user,
+        table=test_setup.table,
+        values={f"field_{test_setup.link_row_field.id}": [other_row_2.id]},
+    )
+    row_4 = test_setup.row_handler.create_row(
+        user=test_setup.user,
+        table=test_setup.table,
+        values={f"field_{test_setup.link_row_field.id}": []},
+    )
+
+    view_filter = data_fixture.create_view_filter(
+        view=test_setup.grid_view,
+        field=test_setup.lookup_field,
+        type="has_not_value_contains_word",
+        value="sentence",
+    )
+    ids = [
+        r.id
+        for r in test_setup.view_handler.apply_filters(
+            test_setup.grid_view, test_setup.model.objects.all()
+        ).all()
+    ]
+    assert len(ids) == 2
+    assert row_2.id in ids
+    assert row_4.id in ids
+
+    view_filter.value = "Sentence"
+    view_filter.save()
+    ids = [
+        r.id
+        for r in test_setup.view_handler.apply_filters(
+            test_setup.grid_view, test_setup.model.objects.all()
+        ).all()
+    ]
+    assert len(ids) == 2
+    assert row_2.id in ids
+    assert row_4.id in ids
+
+    view_filter.value = ""
+    view_filter.save()
+    ids = [
+        r.id
+        for r in test_setup.view_handler.apply_filters(
+            test_setup.grid_view, test_setup.model.objects.all()
+        ).all()
+    ]
+    assert len(ids) == 4
+
+
+@pytest.mark.parametrize(
+    "target_field_factory", [text_field_factory, long_text_field_factory]
+)
+@pytest.mark.django_db
+def test_has_value_length_is_lower_than_text_field_types(
+    data_fixture, target_field_factory
+):
+    test_setup = setup(data_fixture, target_field_factory)
+
+    other_row_10a = test_setup.other_table_model.objects.create(
+        **{f"field_{test_setup.target_field.id}": "aaaaaaaaaa"}
+    )
+    other_row_5a = test_setup.other_table_model.objects.create(
+        **{f"field_{test_setup.target_field.id}": "aaaaa"}
+    )
+    other_row_0a = test_setup.other_table_model.objects.create(
+        **{f"field_{test_setup.target_field.id}": ""}
+    )
+    row_1 = test_setup.row_handler.create_row(
+        user=test_setup.user,
+        table=test_setup.table,
+        values={f"field_{test_setup.link_row_field.id}": [other_row_10a.id]},
+    )
+    row_2 = test_setup.row_handler.create_row(
+        user=test_setup.user,
+        table=test_setup.table,
+        values={
+            f"field_{test_setup.link_row_field.id}": [other_row_0a.id, other_row_10a.id]
+        },
+    )
+    row_3 = test_setup.row_handler.create_row(
+        user=test_setup.user,
+        table=test_setup.table,
+        values={f"field_{test_setup.link_row_field.id}": [other_row_5a.id]},
+    )
+    row_4 = test_setup.row_handler.create_row(
+        user=test_setup.user,
+        table=test_setup.table,
+        values={f"field_{test_setup.link_row_field.id}": []},
+    )
+
+    view_filter = data_fixture.create_view_filter(
+        view=test_setup.grid_view,
+        field=test_setup.lookup_field,
+        type="has_value_length_is_lower_than",
+        value="10",
+    )
+    ids = [
+        r.id
+        for r in test_setup.view_handler.apply_filters(
+            test_setup.grid_view, test_setup.model.objects.all()
+        ).all()
+    ]
+    assert len(ids) == 2
+    assert row_2.id in ids
+    assert row_3.id in ids
+
+    view_filter.value = "5"
+    view_filter.save()
+    ids = [
+        r.id
+        for r in test_setup.view_handler.apply_filters(
+            test_setup.grid_view, test_setup.model.objects.all()
+        ).all()
+    ]
+    assert len(ids) == 1
+    assert row_2.id in ids
+
+    view_filter.value = "11"
+    view_filter.save()
+    ids = [
+        r.id
+        for r in test_setup.view_handler.apply_filters(
+            test_setup.grid_view, test_setup.model.objects.all()
+        ).all()
+    ]
+    assert len(ids) == 3
+    assert row_1.id in ids
+    assert row_2.id in ids
+    assert row_3.id in ids
+
+    view_filter.value = ""
+    view_filter.save()
+    ids = [
+        r.id
+        for r in test_setup.view_handler.apply_filters(
+            test_setup.grid_view, test_setup.model.objects.all()
+        ).all()
+    ]
+    assert len(ids) == 4
diff --git a/changelog/entries/unreleased/feature/2727_add_array_filters_for_formula_arrays_based_on_text.json b/changelog/entries/unreleased/feature/2727_add_array_filters_for_formula_arrays_based_on_text.json
new file mode 100644
index 000000000..6291857e3
--- /dev/null
+++ b/changelog/entries/unreleased/feature/2727_add_array_filters_for_formula_arrays_based_on_text.json
@@ -0,0 +1,7 @@
+{
+    "type": "feature",
+    "message": "Add array filters for formula arrays based on text",
+    "issue_number": 2727,
+    "bullet_points": [],
+    "created_at": "2024-07-10"
+}
\ No newline at end of file
diff --git a/web-frontend/locales/en.json b/web-frontend/locales/en.json
index a2c18f318..3b16499d0 100644
--- a/web-frontend/locales/en.json
+++ b/web-frontend/locales/en.json
@@ -173,6 +173,15 @@
     "password": "A write-only field that holds a hashed password. The value will be `null` if not set, or `true` if it has been set. It accepts a string to set it."
   },
   "viewFilter": {
+    "hasEmptyValue": "has empty value",
+    "hasNotEmptyValue": "doesn't have empty value",
+    "hasValueEqual": "has value equal",
+    "hasNotValueEqual": "doesn't have value equal",
+    "hasValueContains": "has value contains",
+    "hasNotValueContains": "doesn't have value contains",
+    "hasValueContainsWord": "has value contains word",
+    "hasNotValueContainsWord": "doesn't have value contains word",
+    "hasValueLengthIsLowerThan": "has value length is lower than",
     "contains": "contains",
     "containsNot": "doesn't contain",
     "containsWord": "contains word",
diff --git a/web-frontend/modules/database/arrayViewFilters.js b/web-frontend/modules/database/arrayViewFilters.js
new file mode 100644
index 000000000..1e8ed18cc
--- /dev/null
+++ b/web-frontend/modules/database/arrayViewFilters.js
@@ -0,0 +1,210 @@
+import ViewFilterTypeText from '@baserow/modules/database/components/view/ViewFilterTypeText'
+import ViewFilterTypeNumber from '@baserow/modules/database/components/view/ViewFilterTypeNumber'
+import { FormulaFieldType } from '@baserow/modules/database/fieldTypes'
+import { ViewFilterType } from '@baserow/modules/database/viewFilters'
+
+export class HasEmptyValueViewFilterType extends ViewFilterType {
+  static getType() {
+    return 'has_empty_value'
+  }
+
+  getName() {
+    const { i18n } = this.app
+    return i18n.t('viewFilter.hasEmptyValue')
+  }
+
+  getCompatibleFieldTypes() {
+    return [FormulaFieldType.compatibleWithFormulaTypes('array(text)')]
+  }
+
+  matches(cellValue, filterValue, field, fieldType) {
+    return fieldType.getHasEmptyValueFilterFunction(field)(cellValue)
+  }
+}
+
+export class HasNotEmptyValueViewFilterType extends ViewFilterType {
+  static getType() {
+    return 'has_not_empty_value'
+  }
+
+  getName() {
+    const { i18n } = this.app
+    return i18n.t('viewFilter.hasNotEmptyValue')
+  }
+
+  getCompatibleFieldTypes() {
+    return [FormulaFieldType.compatibleWithFormulaTypes('array(text)')]
+  }
+
+  matches(cellValue, filterValue, field, fieldType) {
+    return !fieldType.getHasEmptyValueFilterFunction(field)(cellValue)
+  }
+}
+
+export class HasValueEqualViewFilterType extends ViewFilterType {
+  static getType() {
+    return 'has_value_equal'
+  }
+
+  getName() {
+    const { i18n } = this.app
+    return i18n.t('viewFilter.hasValueEqual')
+  }
+
+  getInputComponent(field) {
+    return ViewFilterTypeText
+  }
+
+  getCompatibleFieldTypes() {
+    return [FormulaFieldType.compatibleWithFormulaTypes('array(text)')]
+  }
+
+  matches(cellValue, filterValue, field, fieldType) {
+    return fieldType.hasValueEqualFilter(cellValue, filterValue, field)
+  }
+}
+
+export class HasNotValueEqualViewFilterType extends ViewFilterType {
+  static getType() {
+    return 'has_not_value_equal'
+  }
+
+  getName() {
+    const { i18n } = this.app
+    return i18n.t('viewFilter.hasNotValueEqual')
+  }
+
+  getInputComponent(field) {
+    return ViewFilterTypeText
+  }
+
+  getCompatibleFieldTypes() {
+    return [FormulaFieldType.compatibleWithFormulaTypes('array(text)')]
+  }
+
+  matches(cellValue, filterValue, field, fieldType) {
+    return fieldType.hasNotValueEqualFilter(cellValue, filterValue, field)
+  }
+}
+
+export class HasValueContainsViewFilterType extends ViewFilterType {
+  static getType() {
+    return 'has_value_contains'
+  }
+
+  getName() {
+    const { i18n } = this.app
+    return i18n.t('viewFilter.hasValueContains')
+  }
+
+  getInputComponent(field) {
+    return ViewFilterTypeText
+  }
+
+  getCompatibleFieldTypes() {
+    return [FormulaFieldType.compatibleWithFormulaTypes('array(text)')]
+  }
+
+  matches(cellValue, filterValue, field, fieldType) {
+    return fieldType.hasValueContainsFilter(cellValue, filterValue, field)
+  }
+}
+
+export class HasNotValueContainsViewFilterType extends ViewFilterType {
+  static getType() {
+    return 'has_not_value_contains'
+  }
+
+  getName() {
+    const { i18n } = this.app
+    return i18n.t('viewFilter.hasNotValueContains')
+  }
+
+  getInputComponent(field) {
+    return ViewFilterTypeText
+  }
+
+  getCompatibleFieldTypes() {
+    return [FormulaFieldType.compatibleWithFormulaTypes('array(text)')]
+  }
+
+  matches(cellValue, filterValue, field, fieldType) {
+    return fieldType.hasNotValueContainsFilter(cellValue, filterValue, field)
+  }
+}
+
+export class HasValueContainsWordViewFilterType extends ViewFilterType {
+  static getType() {
+    return 'has_value_contains_word'
+  }
+
+  getName() {
+    const { i18n } = this.app
+    return i18n.t('viewFilter.hasValueContainsWord')
+  }
+
+  getInputComponent(field) {
+    return ViewFilterTypeText
+  }
+
+  getCompatibleFieldTypes() {
+    return [FormulaFieldType.compatibleWithFormulaTypes('array(text)')]
+  }
+
+  matches(cellValue, filterValue, field, fieldType) {
+    return fieldType.hasValueContainsWordFilter(cellValue, filterValue, field)
+  }
+}
+
+export class HasNotValueContainsWordViewFilterType extends ViewFilterType {
+  static getType() {
+    return 'has_not_value_contains_word'
+  }
+
+  getName() {
+    const { i18n } = this.app
+    return i18n.t('viewFilter.hasNotValueContainsWord')
+  }
+
+  getInputComponent(field) {
+    return ViewFilterTypeText
+  }
+
+  getCompatibleFieldTypes() {
+    return [FormulaFieldType.compatibleWithFormulaTypes('array(text)')]
+  }
+
+  matches(cellValue, filterValue, field, fieldType) {
+    return fieldType.hasNotValueContainsWordFilter(
+      cellValue,
+      filterValue,
+      field
+    )
+  }
+}
+
+export class HasValueLengthIsLowerThanViewFilterType extends ViewFilterType {
+  static getType() {
+    return 'has_value_length_is_lower_than'
+  }
+
+  getName() {
+    const { i18n } = this.app
+    return i18n.t('viewFilter.hasValueLengthIsLowerThan')
+  }
+
+  getInputComponent(field) {
+    return ViewFilterTypeNumber
+  }
+
+  getCompatibleFieldTypes() {
+    return [FormulaFieldType.compatibleWithFormulaTypes('array(text)')]
+  }
+
+  matches(cellValue, filterValue, field, fieldType) {
+    return fieldType.getHasValueLengthIsLowerThanFilterFunction(field)(
+      cellValue,
+      filterValue
+    )
+  }
+}
diff --git a/web-frontend/modules/database/fieldFilterCompatibility.js b/web-frontend/modules/database/fieldFilterCompatibility.js
new file mode 100644
index 000000000..d73abb984
--- /dev/null
+++ b/web-frontend/modules/database/fieldFilterCompatibility.js
@@ -0,0 +1,82 @@
+import {
+  genericHasValueEqualFilter,
+  genericHasValueContainsFilter,
+  genericHasValueContainsWordFilter,
+  genericHasEmptyValueFilter,
+  genericHasValueLengthLowerThanFilter,
+} from '@baserow/modules/database/utils/fieldFilters'
+
+export function fieldSupportsFilter(fieldType, filterMixin) {
+  for (const [key, value] of Object.entries(filterMixin)) {
+    /* eslint no-prototype-builtins: "off" */
+    if (!fieldType.prototype.hasOwnProperty(key)) {
+      fieldType.prototype[key] = value
+    }
+  }
+}
+
+export const hasEmptyValueFilterMixin = {
+  getHasEmptyValueFilterFunction(field) {
+    return genericHasEmptyValueFilter
+  },
+}
+
+export const hasValueEqualFilterMixin = {
+  getHasValueEqualFilterFunction(field) {
+    return genericHasValueEqualFilter
+  },
+  hasValueEqualFilter(cellValue, filterValue, field) {
+    return (
+      filterValue === '' ||
+      this.getHasValueEqualFilterFunction(field)(cellValue, filterValue)
+    )
+  },
+  hasNotValueEqualFilter(cellValue, filterValue, field) {
+    return (
+      filterValue === '' ||
+      !this.getHasValueEqualFilterFunction(field)(cellValue, filterValue)
+    )
+  },
+}
+
+export const hasValueContainsFilterMixin = {
+  getHasValueContainsFilterFunction(field) {
+    return genericHasValueContainsFilter
+  },
+  hasValueContainsFilter(cellValue, filterValue, field) {
+    return (
+      filterValue === '' ||
+      this.getHasValueContainsFilterFunction(field)(cellValue, filterValue)
+    )
+  },
+  hasNotValueContainsFilter(cellValue, filterValue, field) {
+    return (
+      filterValue === '' ||
+      !this.getHasValueContainsFilterFunction(field)(cellValue, filterValue)
+    )
+  },
+}
+
+export const hasValueContainsWordFilterMixin = {
+  getHasValueContainsWordFilterFunction(field) {
+    return genericHasValueContainsWordFilter
+  },
+  hasValueContainsWordFilter(cellValue, filterValue, field) {
+    return (
+      filterValue === '' ||
+      this.getHasValueContainsWordFilterFunction(field)(cellValue, filterValue)
+    )
+  },
+  hasNotValueContainsWordFilter(cellValue, filterValue, field) {
+    return (
+      filterValue === '' ||
+      !this.getHasValueContainsWordFilterFunction(field)(cellValue, filterValue)
+    )
+  },
+}
+
+export const hasValueLengthIsLowerThanFilterMixin = {
+  getHasValueLengthIsLowerThanFilterFunction(field) {
+    return genericHasValueLengthLowerThanFilter
+  },
+}
diff --git a/web-frontend/modules/database/fieldTypes.js b/web-frontend/modules/database/fieldTypes.js
index a72e7500d..f547f51e3 100644
--- a/web-frontend/modules/database/fieldTypes.js
+++ b/web-frontend/modules/database/fieldTypes.js
@@ -15,7 +15,14 @@ import {
   isValidEmail,
   isValidURL,
 } from '@baserow/modules/core/utils/string'
-
+import {
+  fieldSupportsFilter,
+  hasEmptyValueFilterMixin,
+  hasValueContainsFilterMixin,
+  hasValueEqualFilterMixin,
+  hasValueContainsWordFilterMixin,
+  hasValueLengthIsLowerThanFilterMixin,
+} from '@baserow/modules/database/fieldFilterCompatibility'
 import moment from '@baserow/modules/core/moment'
 import guessFormat from 'moment-guess'
 import { Registerable } from '@baserow/modules/core/registry'
@@ -135,6 +142,12 @@ import FormViewFieldMultipleLinkRow from '@baserow/modules/database/components/v
 import FormViewFieldMultipleSelectCheckboxes from '@baserow/modules/database/components/view/form/FormViewFieldMultipleSelectCheckboxes'
 import FormViewFieldSingleSelectRadios from '@baserow/modules/database/components/view/form/FormViewFieldSingleSelectRadios'
 
+import {
+  BaserowFormulaArrayType,
+  BaserowFormulaCharType,
+  BaserowFormulaTextType,
+} from '@baserow/modules/database/formula/formulaTypes'
+
 import { trueValues } from '@baserow/modules/core/utils/constants'
 import {
   getDateMomentFormat,
@@ -3658,6 +3671,31 @@ export class FormulaFieldType extends FieldType {
     const subType = this.app.$registry.get('formula_type', field.formula_type)
     return subType.canRepresentFiles(field)
   }
+
+  getHasEmptyValueFilterFunction(field) {
+    const subType = this.app.$registry.get('formula_type', field.formula_type)
+    return subType.getHasEmptyValueFilterFunction(field)
+  }
+
+  getHasValueEqualFilterFunction(field) {
+    const subType = this.app.$registry.get('formula_type', field.formula_type)
+    return subType.getHasValueEqualFilterFunction(field)
+  }
+
+  getHasValueContainsFilterFunction(field) {
+    const subType = this.app.$registry.get('formula_type', field.formula_type)
+    return subType.getHasValueContainsFilterFunction(field)
+  }
+
+  getHasValueContainsWordFilterFunction(field) {
+    const subType = this.app.$registry.get('formula_type', field.formula_type)
+    return subType.getHasValueContainsWordFilterFunction(field)
+  }
+
+  getHasValueLengthIsLowerThanFilterFunction(field) {
+    const subType = this.app.$registry.get('formula_type', field.formula_type)
+    return subType.getHasValueLengthIsLowerThanFilterFunction(field)
+  }
 }
 
 export class CountFieldType extends FormulaFieldType {
@@ -4202,3 +4240,37 @@ export class PasswordFieldType extends FieldType {
     return RowHistoryFieldPassword
   }
 }
+
+fieldSupportsFilter(FormulaFieldType, hasEmptyValueFilterMixin)
+fieldSupportsFilter(BaserowFormulaArrayType, hasEmptyValueFilterMixin)
+fieldSupportsFilter(BaserowFormulaTextType, hasEmptyValueFilterMixin)
+fieldSupportsFilter(BaserowFormulaCharType, hasEmptyValueFilterMixin)
+
+fieldSupportsFilter(FormulaFieldType, hasValueEqualFilterMixin)
+fieldSupportsFilter(BaserowFormulaArrayType, hasValueEqualFilterMixin)
+fieldSupportsFilter(BaserowFormulaTextType, hasValueEqualFilterMixin)
+fieldSupportsFilter(BaserowFormulaCharType, hasValueEqualFilterMixin)
+
+fieldSupportsFilter(FormulaFieldType, hasValueContainsFilterMixin)
+fieldSupportsFilter(BaserowFormulaArrayType, hasValueContainsFilterMixin)
+fieldSupportsFilter(BaserowFormulaTextType, hasValueContainsFilterMixin)
+fieldSupportsFilter(BaserowFormulaCharType, hasValueContainsFilterMixin)
+
+fieldSupportsFilter(FormulaFieldType, hasValueContainsWordFilterMixin)
+fieldSupportsFilter(BaserowFormulaArrayType, hasValueContainsWordFilterMixin)
+fieldSupportsFilter(BaserowFormulaTextType, hasValueContainsWordFilterMixin)
+fieldSupportsFilter(BaserowFormulaCharType, hasValueContainsWordFilterMixin)
+
+fieldSupportsFilter(FormulaFieldType, hasValueLengthIsLowerThanFilterMixin)
+fieldSupportsFilter(
+  BaserowFormulaArrayType,
+  hasValueLengthIsLowerThanFilterMixin
+)
+fieldSupportsFilter(
+  BaserowFormulaTextType,
+  hasValueLengthIsLowerThanFilterMixin
+)
+fieldSupportsFilter(
+  BaserowFormulaCharType,
+  hasValueLengthIsLowerThanFilterMixin
+)
diff --git a/web-frontend/modules/database/formula/formulaTypes.js b/web-frontend/modules/database/formula/formulaTypes.js
index b6099dc7a..235fe2ba8 100644
--- a/web-frontend/modules/database/formula/formulaTypes.js
+++ b/web-frontend/modules/database/formula/formulaTypes.js
@@ -666,6 +666,46 @@ export class BaserowFormulaArrayType extends BaserowFormulaTypeDefinition {
   canGroupByInView() {
     return false
   }
+
+  getHasEmptyValueFilterFunction(field) {
+    const subType = this.app.$registry.get(
+      'formula_type',
+      field.array_formula_type
+    )
+    return subType.getHasEmptyValueFilterFunction(field)
+  }
+
+  getHasValueEqualFilterFunction(field) {
+    const subType = this.app.$registry.get(
+      'formula_type',
+      field.array_formula_type
+    )
+    return subType.getHasValueEqualFilterFunction(field)
+  }
+
+  getHasValueContainsFilterFunction(field) {
+    const subType = this.app.$registry.get(
+      'formula_type',
+      field.array_formula_type
+    )
+    return subType.getHasValueContainsFilterFunction(field)
+  }
+
+  getHasValueContainsWordFilterFunction(field) {
+    const subType = this.app.$registry.get(
+      'formula_type',
+      field.array_formula_type
+    )
+    return subType.getHasValueContainsWordFilterFunction(field)
+  }
+
+  getHasValueLengthIsLowerThanFilterFunction(field) {
+    const subType = this.app.$registry.get(
+      'formula_type',
+      field.array_formula_type
+    )
+    return subType.getHasValueLengthIsLowerThanFilterFunction(field)
+  }
 }
 
 export class BaserowFormulaFileType extends BaserowFormulaTypeDefinition {
diff --git a/web-frontend/modules/database/plugin.js b/web-frontend/modules/database/plugin.js
index cca396549..c46cee5f6 100644
--- a/web-frontend/modules/database/plugin.js
+++ b/web-frontend/modules/database/plugin.js
@@ -95,6 +95,17 @@ import {
   DateAfterOrEqualViewFilterType,
   DateEqualsDayOfMonthViewFilterType,
 } from '@baserow/modules/database/viewFilters'
+import {
+  HasValueEqualViewFilterType,
+  HasEmptyValueViewFilterType,
+  HasNotEmptyValueViewFilterType,
+  HasNotValueEqualViewFilterType,
+  HasValueContainsViewFilterType,
+  HasNotValueContainsViewFilterType,
+  HasValueContainsWordViewFilterType,
+  HasNotValueContainsWordViewFilterType,
+  HasValueLengthIsLowerThanViewFilterType,
+} from '@baserow/modules/database/arrayViewFilters'
 import {
   CSVImporterType,
   PasteImporterType,
@@ -427,6 +438,36 @@ export default (context) => {
     new DateAfterDaysAgoViewFilterType(context)
   )
   // END
+  app.$registry.register('viewFilter', new HasEmptyValueViewFilterType(context))
+  app.$registry.register(
+    'viewFilter',
+    new HasNotEmptyValueViewFilterType(context)
+  )
+  app.$registry.register('viewFilter', new HasValueEqualViewFilterType(context))
+  app.$registry.register(
+    'viewFilter',
+    new HasNotValueEqualViewFilterType(context)
+  )
+  app.$registry.register(
+    'viewFilter',
+    new HasValueContainsViewFilterType(context)
+  )
+  app.$registry.register(
+    'viewFilter',
+    new HasNotValueContainsViewFilterType(context)
+  )
+  app.$registry.register(
+    'viewFilter',
+    new HasValueContainsWordViewFilterType(context)
+  )
+  app.$registry.register(
+    'viewFilter',
+    new HasNotValueContainsWordViewFilterType(context)
+  )
+  app.$registry.register(
+    'viewFilter',
+    new HasValueLengthIsLowerThanViewFilterType(context)
+  )
   app.$registry.register('viewFilter', new ContainsViewFilterType(context))
   app.$registry.register('viewFilter', new ContainsNotViewFilterType(context))
   app.$registry.register('viewFilter', new ContainsWordViewFilterType(context))
diff --git a/web-frontend/modules/database/utils/fieldFilters.js b/web-frontend/modules/database/utils/fieldFilters.js
index 29aac60a1..6d7c36e1c 100644
--- a/web-frontend/modules/database/utils/fieldFilters.js
+++ b/web-frontend/modules/database/utils/fieldFilters.js
@@ -49,3 +49,91 @@ export function genericContainsWordFilter(
   filterValue = filterValue.replace(/[-[\]{}()*+?.,\\^$|#]/g, '\\$&')
   return humanReadableRowValue.match(new RegExp(`\\b${filterValue}\\b`))
 }
+
+export function genericHasEmptyValueFilter(cellValue, filterValue) {
+  if (!Array.isArray(cellValue)) {
+    return false
+  }
+
+  for (let i = 0; i < cellValue.length; i++) {
+    const value = cellValue[i].value
+
+    if (value === '') {
+      return true
+    }
+  }
+
+  return false
+}
+
+export function genericHasValueEqualFilter(cellValue, filterValue) {
+  if (!Array.isArray(cellValue)) {
+    return false
+  }
+
+  for (let i = 0; i < cellValue.length; i++) {
+    const value = cellValue[i].value
+    if (value === filterValue) {
+      return true
+    }
+  }
+
+  return false
+}
+
+export function genericHasValueContainsFilter(cellValue, filterValue) {
+  if (!Array.isArray(cellValue)) {
+    return false
+  }
+
+  filterValue = filterValue.toString().toLowerCase().trim()
+
+  for (let i = 0; i < cellValue.length; i++) {
+    const value = cellValue[i].value.toString().toLowerCase().trim()
+
+    if (value.includes(filterValue)) {
+      return true
+    }
+  }
+
+  return false
+}
+
+export function genericHasValueContainsWordFilter(cellValue, filterValue) {
+  if (!Array.isArray(cellValue)) {
+    return false
+  }
+
+  filterValue = filterValue.toString().toLowerCase().trim()
+  filterValue = filterValue.replace(/[-[\]{}()*+?.,\\^$|#]/g, '\\$&')
+
+  for (let i = 0; i < cellValue.length; i++) {
+    if (cellValue[i].value == null) {
+      continue
+    }
+    const value = cellValue[i].value.toString().toLowerCase().trim()
+    if (value.match(new RegExp(`\\b${filterValue}\\b`))) {
+      return true
+    }
+  }
+
+  return false
+}
+
+export function genericHasValueLengthLowerThanFilter(cellValue, filterValue) {
+  if (!Array.isArray(cellValue)) {
+    return false
+  }
+
+  for (let i = 0; i < cellValue.length; i++) {
+    if (cellValue[i].value == null) {
+      continue
+    }
+    const valueLength = cellValue[i].value.toString().length
+    if (valueLength < filterValue) {
+      return true
+    }
+  }
+
+  return false
+}
diff --git a/web-frontend/modules/database/viewFilters.js b/web-frontend/modules/database/viewFilters.js
index d638c32a3..ed9a8481e 100644
--- a/web-frontend/modules/database/viewFilters.js
+++ b/web-frontend/modules/database/viewFilters.js
@@ -197,7 +197,6 @@ export class EqualViewFilterType extends ViewFilterType {
       'uuid',
       'autonumber',
       'duration',
-      FormulaFieldType.compatibleWithFormulaTypes('text', 'char', 'number'),
     ]
   }
 
diff --git a/web-frontend/test/unit/database/arrayViewFiltersMatch.spec.js b/web-frontend/test/unit/database/arrayViewFiltersMatch.spec.js
new file mode 100644
index 000000000..129c67799
--- /dev/null
+++ b/web-frontend/test/unit/database/arrayViewFiltersMatch.spec.js
@@ -0,0 +1,388 @@
+import { TestApp } from '@baserow/test/helpers/testApp'
+import {
+  HasValueEqualViewFilterType,
+  HasNotValueEqualViewFilterType,
+  HasValueContainsViewFilterType,
+  HasNotValueContainsViewFilterType,
+  HasValueContainsWordViewFilterType,
+  HasNotValueContainsWordViewFilterType,
+  HasEmptyValueViewFilterType,
+  HasNotEmptyValueViewFilterType,
+  HasValueLengthIsLowerThanViewFilterType,
+} from '@baserow/modules/database/arrayViewFilters'
+import { FormulaFieldType } from '@baserow/modules/database/fieldTypes'
+
+describe('Text-based array view filters', () => {
+  let testApp = null
+
+  beforeAll(() => {
+    testApp = new TestApp()
+  })
+
+  afterEach(() => {
+    testApp.afterEach()
+  })
+
+  const hasTextValueEqualCases = [
+    {
+      cellValue: [],
+      filterValue: 'A',
+      expected: false,
+    },
+    {
+      cellValue: [{ value: 'B' }, { value: 'A' }],
+      filterValue: 'A',
+      expected: true,
+    },
+    {
+      cellValue: [{ value: 'a' }],
+      filterValue: 'A',
+      expected: false,
+    },
+    {
+      cellValue: [{ value: 'Aa' }],
+      filterValue: 'A',
+      expected: false,
+    },
+  ]
+
+  const hasValueEqualSupportedFields = [
+    {
+      TestFieldType: FormulaFieldType,
+      formula_type: 'array',
+      array_formula_type: 'text',
+    },
+  ]
+
+  describe.each(hasValueEqualSupportedFields)(
+    'HasValueEqualViewFilterType %j',
+    (field) => {
+      test.each(hasTextValueEqualCases)(
+        'filter matches values %j',
+        (testValues) => {
+          const fieldType = new field.TestFieldType({
+            app: testApp._app,
+          })
+          const result = new HasValueEqualViewFilterType({
+            app: testApp._app,
+          }).matches(
+            testValues.cellValue,
+            testValues.filterValue,
+            field,
+            fieldType
+          )
+          expect(result).toBe(testValues.expected)
+        }
+      )
+    }
+  )
+
+  describe.each(hasValueEqualSupportedFields)(
+    'HasNotValueEqualViewFilterType %j',
+    (field) => {
+      test.each(hasTextValueEqualCases)(
+        'filter not matches values %j',
+        (testValues) => {
+          const fieldType = new field.TestFieldType({
+            app: testApp._app,
+          })
+          const result = new HasNotValueEqualViewFilterType({
+            app: testApp._app,
+          }).matches(
+            testValues.cellValue,
+            testValues.filterValue,
+            field,
+            fieldType
+          )
+          expect(result).toBe(!testValues.expected)
+        }
+      )
+    }
+  )
+
+  const hasValueContainsCases = [
+    {
+      cellValue: [],
+      filterValue: 'A',
+      expected: false,
+    },
+    {
+      cellValue: [{ value: 'B' }, { value: 'Aa' }],
+      filterValue: 'A',
+      expected: true,
+    },
+    {
+      cellValue: [{ value: 't a t' }],
+      filterValue: 'A',
+      expected: true,
+    },
+    {
+      cellValue: [{ value: 'C' }],
+      filterValue: 'A',
+      expected: false,
+    },
+  ]
+
+  const hasValueContainsSupportedFields = [
+    {
+      TestFieldType: FormulaFieldType,
+      formula_type: 'array',
+      array_formula_type: 'text',
+    },
+  ]
+
+  describe.each(hasValueContainsSupportedFields)(
+    'HasValueContainsViewFilterType %j',
+    (field) => {
+      test.each(hasValueContainsCases)(
+        'filter matches values %j',
+        (testValues) => {
+          const fieldType = new field.TestFieldType({
+            app: testApp._app,
+          })
+          const result = new HasValueContainsViewFilterType({
+            app: testApp._app,
+          }).matches(
+            testValues.cellValue,
+            testValues.filterValue,
+            field,
+            fieldType
+          )
+          expect(result).toBe(testValues.expected)
+        }
+      )
+    }
+  )
+
+  describe.each(hasValueContainsSupportedFields)(
+    'HasNotValueContainsViewFilterType %j',
+    (field) => {
+      test.each(hasValueContainsCases)(
+        'filter not matches values %j',
+        (testValues) => {
+          const fieldType = new field.TestFieldType({
+            app: testApp._app,
+          })
+          const result = new HasNotValueContainsViewFilterType({
+            app: testApp._app,
+          }).matches(
+            testValues.cellValue,
+            testValues.filterValue,
+            field,
+            fieldType
+          )
+          expect(result).toBe(!testValues.expected)
+        }
+      )
+    }
+  )
+
+  const hasValueContainsWordCases = [
+    {
+      cellValue: [],
+      filterValue: 'Word',
+      expected: false,
+    },
+    {
+      cellValue: [{ value: '...Word...' }, { value: 'Some sentence' }],
+      filterValue: 'Word',
+      expected: true,
+    },
+    {
+      cellValue: [{ value: 'Word' }],
+      filterValue: 'ord',
+      expected: false,
+    },
+    {
+      cellValue: [{ value: 'Some word in a sentence.' }],
+      filterValue: 'Word',
+      expected: true,
+    },
+    {
+      cellValue: [{ value: 'Some Word in a sentence.' }],
+      filterValue: 'word',
+      expected: true,
+    },
+  ]
+
+  const hasValueContainsWordSupportedFields = [
+    {
+      TestFieldType: FormulaFieldType,
+      formula_type: 'array',
+      array_formula_type: 'text',
+    },
+  ]
+
+  describe.each(hasValueContainsWordSupportedFields)(
+    'HasValueContainsWordViewFilterType %j',
+    (field) => {
+      test.each(hasValueContainsWordCases)(
+        'filter matches values %j',
+        (testValues) => {
+          const fieldType = new field.TestFieldType({
+            app: testApp._app,
+          })
+          const result = new HasValueContainsWordViewFilterType({
+            app: testApp._app,
+          }).matches(
+            testValues.cellValue,
+            testValues.filterValue,
+            field,
+            fieldType
+          )
+          expect(result).toBe(testValues.expected)
+        }
+      )
+    }
+  )
+
+  describe.each(hasValueContainsWordSupportedFields)(
+    'HasNotValueContainsWordViewFilterType %j',
+    (field) => {
+      test.each(hasValueContainsWordCases)(
+        'filter not matches values %j',
+        (testValues) => {
+          const fieldType = new field.TestFieldType({
+            app: testApp._app,
+          })
+          const result = new HasNotValueContainsWordViewFilterType({
+            app: testApp._app,
+          }).matches(
+            testValues.cellValue,
+            testValues.filterValue,
+            field,
+            fieldType
+          )
+          expect(result).toBe(!testValues.expected)
+        }
+      )
+    }
+  )
+
+  const hasEmptyValueCases = [
+    {
+      cellValue: [],
+      expected: false,
+    },
+    {
+      cellValue: [{ value: 'B' }, { value: '' }],
+      expected: true,
+    },
+    {
+      cellValue: [{ value: '' }],
+      expected: true,
+    },
+    {
+      cellValue: [{ value: 'C' }],
+      expected: false,
+    },
+  ]
+
+  const hasEmptyValueSupportedFields = [
+    {
+      TestFieldType: FormulaFieldType,
+      formula_type: 'array',
+      array_formula_type: 'text',
+    },
+  ]
+
+  describe.each(hasEmptyValueSupportedFields)(
+    'HasEmptyValueViewFilterType %j',
+    (field) => {
+      test.each(hasEmptyValueCases)(
+        'filter matches values %j',
+        (testValues) => {
+          const fieldType = new field.TestFieldType({
+            app: testApp._app,
+          })
+          const result = new HasEmptyValueViewFilterType({
+            app: testApp._app,
+          }).matches(
+            testValues.cellValue,
+            testValues.filterValue,
+            field,
+            fieldType
+          )
+          expect(result).toBe(testValues.expected)
+        }
+      )
+    }
+  )
+
+  describe.each(hasEmptyValueSupportedFields)(
+    'HasNotEmptyValueViewFilterType %j',
+    (field) => {
+      test.each(hasEmptyValueCases)(
+        'filter not matches values %j',
+        (testValues) => {
+          const fieldType = new field.TestFieldType({
+            app: testApp._app,
+          })
+          const result = new HasNotEmptyValueViewFilterType({
+            app: testApp._app,
+          }).matches(
+            testValues.cellValue,
+            testValues.filterValue,
+            field,
+            fieldType
+          )
+          expect(result).toBe(!testValues.expected)
+        }
+      )
+    }
+  )
+
+  const hasLengthLowerThanValueCases = [
+    {
+      cellValue: [],
+      filterValue: '1',
+      expected: false,
+    },
+    {
+      cellValue: [{ value: 'aaaaa' }, { value: 'aaaaaaaaaa' }],
+      filterValue: '6',
+      expected: true,
+    },
+    {
+      cellValue: [{ value: 'aaaaa' }],
+      filterValue: '5',
+      expected: false,
+    },
+    {
+      cellValue: [{ value: '' }],
+      filterValue: '1',
+      expected: true,
+    },
+  ]
+
+  const hasLengthLowerThanSupportedFields = [
+    {
+      TestFieldType: FormulaFieldType,
+      formula_type: 'array',
+      array_formula_type: 'text',
+    },
+  ]
+
+  describe.each(hasLengthLowerThanSupportedFields)(
+    'HasValueLengthIsLowerThanViewFilterType %j',
+    (field) => {
+      test.each(hasLengthLowerThanValueCases)(
+        'filter matches values %j',
+        (testValues) => {
+          const fieldType = new field.TestFieldType({
+            app: testApp._app,
+          })
+          const result = new HasValueLengthIsLowerThanViewFilterType({
+            app: testApp._app,
+          }).matches(
+            testValues.cellValue,
+            testValues.filterValue,
+            field,
+            fieldType
+          )
+          expect(result).toBe(testValues.expected)
+        }
+      )
+    }
+  )
+})