diff --git a/backend/src/baserow/contrib/database/config.py b/backend/src/baserow/contrib/database/config.py
index 2e507b746..37bb6cc8e 100644
--- a/backend/src/baserow/contrib/database/config.py
+++ b/backend/src/baserow/contrib/database/config.py
@@ -71,11 +71,13 @@ class DatabaseConfig(AppConfig):
             EqualViewFilterType, NotEqualViewFilterType, EmptyViewFilterType,
             NotEmptyViewFilterType, DateEqualViewFilterType, DateNotEqualViewFilterType,
             HigherThanViewFilterType, LowerThanViewFilterType, ContainsViewFilterType,
-            ContainsNotViewFilterType, BooleanViewFilterType,
-            SingleSelectEqualViewFilterType, SingleSelectNotEqualViewFilterType
+            FilenameContainsViewFilterType, ContainsNotViewFilterType,
+            BooleanViewFilterType, SingleSelectEqualViewFilterType,
+            SingleSelectNotEqualViewFilterType
         )
         view_filter_type_registry.register(EqualViewFilterType())
         view_filter_type_registry.register(NotEqualViewFilterType())
+        view_filter_type_registry.register(FilenameContainsViewFilterType())
         view_filter_type_registry.register(ContainsViewFilterType())
         view_filter_type_registry.register(ContainsNotViewFilterType())
         view_filter_type_registry.register(HigherThanViewFilterType())
diff --git a/backend/src/baserow/contrib/database/table/models.py b/backend/src/baserow/contrib/database/table/models.py
index 262034a58..538891a47 100644
--- a/backend/src/baserow/contrib/database/table/models.py
+++ b/backend/src/baserow/contrib/database/table/models.py
@@ -201,6 +201,13 @@ class TableModelQuerySet(models.QuerySet):
                     model_field
                 )
 
+                view_filter_annotation = view_filter_type.get_annotation(
+                    field_name,
+                    value
+                )
+                if view_filter_annotation:
+                    self = self.annotate(**view_filter_annotation)
+
                 # Depending on filter type we are going to combine the Q either as
                 # AND or as OR.
                 if filter_type == FILTER_TYPE_AND:
diff --git a/backend/src/baserow/contrib/database/views/handler.py b/backend/src/baserow/contrib/database/views/handler.py
index 5fbd1070f..1267916b3 100644
--- a/backend/src/baserow/contrib/database/views/handler.py
+++ b/backend/src/baserow/contrib/database/views/handler.py
@@ -254,6 +254,13 @@ class ViewHandler:
                 model_field
             )
 
+            view_filter_annotation = view_filter_type.get_annotation(
+                field_name,
+                view_filter.value
+            )
+            if view_filter_annotation:
+                queryset = queryset.annotate(**view_filter_annotation)
+
             # Depending on filter type we are going to combine the Q either as AND or
             # as OR.
             if view.filter_type == FILTER_TYPE_AND:
diff --git a/backend/src/baserow/contrib/database/views/registries.py b/backend/src/baserow/contrib/database/views/registries.py
index f0c688e37..d1c29f697 100644
--- a/backend/src/baserow/contrib/database/views/registries.py
+++ b/backend/src/baserow/contrib/database/views/registries.py
@@ -120,6 +120,25 @@ class ViewFilterType(Instance):
 
         raise NotImplementedError('Each must have his own get_filter method.')
 
+    def get_annotation(self, field_name, value):
+        """
+        Optional method allowing this ViewFilterType to annotate the queryset prior to
+        the application of any Q filters returned by ViewFilterType.get_filter.
+
+        Should return a dictionary which can be unpacked into an annotate call or None
+        if you do not wish any annotation to be applied by your filter.
+
+        :param field_name: The name of the field that needs to be filtered.
+        :type field_name: str
+        :param value: The value that the field must be compared to.
+        :type value: str
+        :return: The dict object that will be unpacked into an annotate call or None if
+            no annotation needs to be done.
+        :rtype: None or dict
+        """
+
+        return None
+
 
 class ViewFilterTypeRegistry(Registry):
     """
diff --git a/backend/src/baserow/contrib/database/views/view_filters.py b/backend/src/baserow/contrib/database/views/view_filters.py
index 82beb3ea3..8ba550c31 100644
--- a/backend/src/baserow/contrib/database/views/view_filters.py
+++ b/backend/src/baserow/contrib/database/views/view_filters.py
@@ -6,6 +6,7 @@ from dateutil import parser
 from dateutil.parser import ParserError
 
 from django.db.models import Q, IntegerField, BooleanField
+from django.db.models.expressions import RawSQL
 from django.db.models.fields.related import ManyToManyField, ForeignKey
 from django.contrib.postgres.fields import JSONField
 
@@ -62,6 +63,58 @@ class NotEqualViewFilterType(NotViewFilterTypeMixin, EqualViewFilterType):
     type = 'not_equal'
 
 
+class FilenameContainsViewFilterType(ViewFilterType):
+    """
+    The filename contains filter checks if the filename's visible name contains the
+    provided filter value. It is only compatible with fields.JSONField which contain
+    a list of File JSON Objects.
+    """
+
+    type = 'filename_contains'
+    compatible_field_types = [
+        FileFieldType.type
+    ]
+
+    def get_annotation(self, field_name, value):
+        value = value.strip()
+
+        # If an empty value has been provided we do not want to filter at all.
+        if value == '':
+            return None
+
+        # It is not possible to use Django's ORM to query for if one item in a JSONB
+        # list has has a key which contains a specified value.
+        #
+        # The closest thing the Django ORM provides is:
+        #   queryset.filter(your_json_field__contains=[{"key":"value"}])
+        # However this is an exact match, so in the above example [{"key":"value_etc"}]
+        # would not match the filter.
+        #
+        # Instead we have to resort to RawSQL to use various built in PostgreSQL JSON
+        # Array manipulation functions to be able to 'iterate' over a JSONB list
+        # performing `like` on individual keys in said list.
+        num_files_with_name_like_value = f"""
+            EXISTS(
+                SELECT attached_files ->> 'visible_name'
+                FROM JSONB_ARRAY_ELEMENTS("{field_name}") as attached_files
+                WHERE UPPER(attached_files ->> 'visible_name') LIKE UPPER(%s)
+            )
+        """
+        query = RawSQL(num_files_with_name_like_value, params=[f"%{value}%"],
+                       output_field=BooleanField())
+        return {f"{field_name}_matches_visible_names": query}
+
+    def get_filter(self, field_name, value, model_field):
+        value = value.strip()
+
+        # If an empty value has been provided we do not want to filter at all.
+        if value == '':
+            return Q()
+
+        # Check if the model_field has a file which matches the provided filter value.
+        return Q(**{f'{field_name}_matches_visible_names': True})
+
+
 class ContainsViewFilterType(ViewFilterType):
     """
     The contains filter checks if the field value contains the provided filter value.
diff --git a/backend/tests/baserow/contrib/database/view/test_view_filters.py b/backend/tests/baserow/contrib/database/view/test_view_filters.py
index 03f304832..d48e955a9 100644
--- a/backend/tests/baserow/contrib/database/view/test_view_filters.py
+++ b/backend/tests/baserow/contrib/database/view/test_view_filters.py
@@ -1329,3 +1329,67 @@ def test_not_empty_filter_type(data_fixture):
     filter.field = single_select_field
     filter.save()
     assert handler.apply_filters(grid_view, model.objects.all()).get().id == row_2.id
+
+
+@pytest.mark.django_db
+def test_filename_contains_filter_type(data_fixture):
+    user = data_fixture.create_user()
+    table = data_fixture.create_database_table(user=user)
+    grid_view = data_fixture.create_grid_view(table=table)
+    file_field = data_fixture.create_file_field(table=table)
+
+    handler = ViewHandler()
+    model = table.get_model()
+
+    row = model.objects.create(**{
+        f'field_{file_field.id}': [{'visible_name': 'test_file.png'}],
+    })
+    row_with_multiple_files = model.objects.create(**{
+        f'field_{file_field.id}': [
+            {'visible_name': 'test.doc'},
+            {'visible_name': 'test.txt'}
+        ],
+    })
+    row_with_no_files = model.objects.create(**{
+        f'field_{file_field.id}': [],
+    })
+
+    filter = data_fixture.create_view_filter(
+        view=grid_view,
+        field=file_field,
+        type='filename_contains',
+        value='test_file.png'
+    )
+    ids = [r.id for r in handler.apply_filters(grid_view, model.objects.all()).all()]
+    assert len(ids) == 1
+    assert row.id in ids
+
+    filter.value = '.jpg'
+    filter.save()
+    ids = [r.id for r in handler.apply_filters(grid_view, model.objects.all()).all()]
+    assert len(ids) == 0
+
+    filter.value = '.png'
+    filter.save()
+    ids = [r.id for r in handler.apply_filters(grid_view, model.objects.all()).all()]
+    assert len(ids) == 1
+    assert row.id in ids
+
+    filter.value = 'test.'
+    filter.save()
+    ids = [r.id for r in handler.apply_filters(grid_view, model.objects.all()).all()]
+    assert len(ids) == 1
+    assert row_with_multiple_files.id in ids
+
+    filter.value = ''
+    filter.save()
+    ids = [r.id for r in handler.apply_filters(grid_view, model.objects.all()).all()]
+    assert len(ids) == 3
+    assert row.id in ids
+    assert row_with_multiple_files.id in ids
+    assert row_with_no_files.id in ids
+
+    results = model.objects.all().filter_by_fields_object(filter_object={
+        f'filter__field_{file_field.id}__filename_contains': ['.png'],
+    }, filter_type='AND')
+    assert len(results) == 1
diff --git a/changelog.md b/changelog.md
index d2b687f88..be433d26e 100644
--- a/changelog.md
+++ b/changelog.md
@@ -24,6 +24,7 @@
 * Made it possible for the admin to disable new signups.
 * Reduced the amount of queries when using the link row field.
 * Respect the date format when converting to a date field.
+* Added a field type filename contains filter.
 
 ## Released (2021-02-04)
 
diff --git a/web-frontend/modules/database/fieldTypes.js b/web-frontend/modules/database/fieldTypes.js
index 8024450c8..3766d2156 100644
--- a/web-frontend/modules/database/fieldTypes.js
+++ b/web-frontend/modules/database/fieldTypes.js
@@ -327,6 +327,10 @@ export class LongTextFieldType extends FieldType {
     return RowEditFieldLongText
   }
 
+  getEmptyValue(field) {
+    return ''
+  }
+
   getSort(name, order) {
     return (a, b) => {
       const stringA = a[name] === null ? '' : '' + a[name]
diff --git a/web-frontend/modules/database/plugin.js b/web-frontend/modules/database/plugin.js
index 3eea08ba8..f6153ec08 100644
--- a/web-frontend/modules/database/plugin.js
+++ b/web-frontend/modules/database/plugin.js
@@ -18,6 +18,7 @@ import {
   DateEqualViewFilterType,
   DateNotEqualViewFilterType,
   ContainsViewFilterType,
+  FilenameContainsViewFilterType,
   ContainsNotViewFilterType,
   HigherThanViewFilterType,
   LowerThanViewFilterType,
@@ -53,6 +54,7 @@ export default ({ store, app }) => {
   app.$registry.register('viewFilter', new DateEqualViewFilterType())
   app.$registry.register('viewFilter', new DateNotEqualViewFilterType())
   app.$registry.register('viewFilter', new ContainsViewFilterType())
+  app.$registry.register('viewFilter', new FilenameContainsViewFilterType())
   app.$registry.register('viewFilter', new ContainsNotViewFilterType())
   app.$registry.register('viewFilter', new HigherThanViewFilterType())
   app.$registry.register('viewFilter', new LowerThanViewFilterType())
diff --git a/web-frontend/modules/database/viewFilters.js b/web-frontend/modules/database/viewFilters.js
index 37e5c8a06..8eaf7dc11 100644
--- a/web-frontend/modules/database/viewFilters.js
+++ b/web-frontend/modules/database/viewFilters.js
@@ -140,6 +140,45 @@ export class NotEqualViewFilterType extends ViewFilterType {
   }
 }
 
+export class FilenameContainsViewFilterType extends ViewFilterType {
+  static getType() {
+    return 'filename_contains'
+  }
+
+  getName() {
+    return 'filename contains'
+  }
+
+  getInputComponent() {
+    return ViewFilterTypeText
+  }
+
+  getCompatibleFieldTypes() {
+    return ['file']
+  }
+
+  matches(rowValue, filterValue) {
+    filterValue = filterValue.toString().toLowerCase().trim()
+
+    if (filterValue === '') {
+      return true
+    }
+
+    for (let i = 0; i < rowValue.length; i++) {
+      const visibleName = rowValue[i].visible_name
+        .toString()
+        .toLowerCase()
+        .trim()
+
+      if (visibleName.includes(filterValue)) {
+        return true
+      }
+    }
+
+    return false
+  }
+}
+
 export class ContainsViewFilterType extends ViewFilterType {
   static getType() {
     return 'contains'