diff --git a/AUTHORS b/AUTHORS
index a19e49b..038b8f8 100644
--- a/AUTHORS
+++ b/AUTHORS
@@ -85,6 +85,7 @@ answer newbie questions, and generally made Django that much better:
     Natalia Bidart <nataliabidart@gmail.com>
     Paul Bissex <http://e-scribe.com/>
     Simon Blanchard
+    Sean Bleier <sebleier@gmail.com>
     David Blewett <david@dawninglight.net>
     Matt Boersma <matt@sprout.org>
     boobsd@gmail.com
@@ -215,6 +216,7 @@ answer newbie questions, and generally made Django that much better:
     Scot Hacker <shacker@birdhouse.org>
     dAniel hAhler
     hambaloney
+    Chuck Harmston <chuck@chuckharmston.com>
     Brian Harring <ferringb@gmail.com>
     Brant Harris
     Ronny Haryanto <http://ronny.haryan.to/>
diff --git a/django/contrib/admin/media/css/widgets.css b/django/contrib/admin/media/css/widgets.css
index 26400fa..870ffeb 100644
--- a/django/contrib/admin/media/css/widgets.css
+++ b/django/contrib/admin/media/css/widgets.css
@@ -5,6 +5,10 @@
     float: left;
 }
 
+.selector-single{
+	width: 282px !important;
+}
+
 .selector select {
     width: 270px;
     height: 17.2em;
diff --git a/django/contrib/admin/media/js/fkfilter.js b/django/contrib/admin/media/js/fkfilter.js
new file mode 100644
index 0000000..42af5a0
--- /dev/null
+++ b/django/contrib/admin/media/js/fkfilter.js
@@ -0,0 +1,83 @@
+(function($) {
+    $.fn.fk_filter = function(verbose_name) {
+        return this.each(function() {
+            var excluded;;
+            var j = 0;
+            $(this).attr('size', 2); // Force a multi-row <select>
+            $(this).find('option[value=""]').remove(); // Remove the '-----'
+
+            // Create the wrappers
+            var outerwrapper = $('<div />', {
+                'class': 'selector selector-single'
+            });
+            $(this).wrap(outerwrapper);
+            var innerwrapper = $('<div />', {
+                'class': 'selector-available'
+            });
+            $(this).wrap(innerwrapper);
+
+            // Creates Header
+            var header = $('<h2 />', {
+                'text': interpolate(gettext('Available %s'), [verbose_name])
+            }).insertBefore(this);
+
+            // Creates search bar
+            var searchbar = $('<p />', {
+                'html': '<img src="' + window.__admin_media_prefix__ + 'img/admin/selector-search.gif"> <input type="text" id="' + $(this).attr('id') + '_input">',
+                'class': 'selector-filter'
+            }).insertBefore(this);
+
+            var select = $(this);
+            var original = select.clone();
+
+            var filter = $('#' + $(this).attr('id') + '_input');
+            var excluded = [];
+            var lastPatternLength = 0;
+
+            filter.bind('keyup.fkfilter', function(evt) {
+                /*
+                Procedure for filtering options::
+
+                    * Detach the select from the DOM so each change doesn't
+                      trigger the browser to re-render.
+                    * If the pattern is longer than the previous pattern, then
+                      we can skip any excluded select options and search the
+                      included options for pattern matches.
+                    * If the pattern is shorter, then we're forced to check
+                      excluded select options for matches.
+                    * ``matched`` is incremented whenever an excluded select
+                      option is matched so that we don't have to search
+                      recently appended options twice.
+                    * Note the length of the pattern in ``lastPatternLength``.
+                    * Append the select back into the DOM for rendering.
+                */
+                var i, size, option, options;
+                var pattern = filter.val();
+                var parent = select.parent();
+                var matched = 0;
+                select.detach();
+                if (lastPatternLength > pattern.length) {
+                    size = excluded.length;
+                    for (i = 0; i < size; i++) {
+                        option = excluded[i];
+                        if (option.text().indexOf(pattern) !== -1) {
+                            select.append(option);
+                            matched++;
+                        }
+                    }
+                }
+                options = select.children();
+                size = options.length - matched;
+                for(i = 0; i < size; i++) {
+                    if (options[i].text.indexOf(pattern) === -1) {
+                        option = $(options[i]).detach();
+                        excluded.push(option);
+                    }
+                }
+                lastPatternLength = pattern.length;
+                parent.append(select);
+            });
+        });
+    };
+})(django.jQuery);
+
diff --git a/django/contrib/admin/options.py b/django/contrib/admin/options.py
index fbda8b7..3b23dec 100644
--- a/django/contrib/admin/options.py
+++ b/django/contrib/admin/options.py
@@ -159,6 +159,8 @@ class BaseModelAdmin(object):
                 'class': get_ul_class(self.radio_fields[db_field.name]),
             })
             kwargs['empty_label'] = db_field.blank and _('None') or None
+        elif db_field.name in self.filter_vertical:
+            kwargs['widget'] = widgets.FilteredSelectSingle(db_field.verbose_name)
 
         return db_field.formfield(**kwargs)
 
@@ -1301,7 +1303,7 @@ class InlineModelAdmin(BaseModelAdmin):
             js.append('js/urlify.js')
             js.append('js/prepopulate.min.js')
         if self.filter_vertical or self.filter_horizontal:
-            js.extend(['js/SelectBox.js' , 'js/SelectFilter2.js'])
+            js.extend(['js/SelectBox.js' , 'js/SelectFilter2.js', 'js/fkfilter.js'])
         return forms.Media(js=['%s%s' % (settings.ADMIN_MEDIA_PREFIX, url) for url in js])
     media = property(_media)
 
diff --git a/django/contrib/admin/validation.py b/django/contrib/admin/validation.py
index 159afa4..d318bd3 100644
--- a/django/contrib/admin/validation.py
+++ b/django/contrib/admin/validation.py
@@ -296,9 +296,9 @@ def validate_base(cls, model):
         check_isseq(cls, 'filter_vertical', cls.filter_vertical)
         for idx, field in enumerate(cls.filter_vertical):
             f = get_field(cls, model, opts, 'filter_vertical', field)
-            if not isinstance(f, models.ManyToManyField):
+            if not isinstance(f, (models.ManyToManyField, models.ForeignKey)):
                 raise ImproperlyConfigured("'%s.filter_vertical[%d]' must be "
-                    "a ManyToManyField." % (cls.__name__, idx))
+                    "a ManyToManyField or ForeignKey." % (cls.__name__, idx))
 
     # filter_horizontal
     if hasattr(cls, 'filter_horizontal'):
diff --git a/django/contrib/admin/widgets.py b/django/contrib/admin/widgets.py
index f210d4e..f035586 100644
--- a/django/contrib/admin/widgets.py
+++ b/django/contrib/admin/widgets.py
@@ -14,6 +14,32 @@ from django.utils.encoding import force_unicode
 from django.conf import settings
 from django.core.urlresolvers import reverse, NoReverseMatch
 
+
+class FilteredSelectSingle(forms.Select):
+    """
+    A Select with a JavaScript filter interface.
+    """
+    class Media:
+        js = (settings.ADMIN_MEDIA_PREFIX + "js/fkfilter.js",)
+
+    def __init__(self, verbose_name, attrs=None, choices=()):
+        self.verbose_name = verbose_name
+        super(FilteredSelectSingle, self).__init__(attrs, choices)
+
+    def render(self, name, value, attrs={}, choices=()):
+        attrs['class'] = 'selectfilter'
+        output = [super(FilteredSelectSingle, self).render(
+            name, value, attrs, choices)]
+        output.append((
+            '<script type="text/javascript">'
+            'django.jQuery(document).ready(function(){'
+            'django.jQuery("#id_%s").fk_filter("%s")'
+            '});'
+            '</script>'
+        ) % (name, self.verbose_name.replace('"', '\\"'),));
+        return mark_safe(u''.join(output))
+
+
 class FilteredSelectMultiple(forms.SelectMultiple):
     """
     A SelectMultiple with a JavaScript filter interface.
diff --git a/docs/ref/contrib/admin/index.txt b/docs/ref/contrib/admin/index.txt
index c28dc8b..20a3599 100644
--- a/docs/ref/contrib/admin/index.txt
+++ b/docs/ref/contrib/admin/index.txt
@@ -276,7 +276,10 @@ subclass::
 
     Same as :attr:`~ModelAdmin.filter_horizontal`, but uses a vertical display
     of the filter interface with the box of unselected options appearing above
-    the box of selected options.
+    the box of selected options.  You can also add a
+    :class:`~django.db.models.ForeignKey` to this list and a text box will
+    allow you to narrow your search for a particular object through a
+    potentially large list of options.
 
 .. attribute:: ModelAdmin.form
 
diff --git a/tests/regressiontests/admin_widgets/tests.py b/tests/regressiontests/admin_widgets/tests.py
index 7ad74a3..2351eb0 100644
--- a/tests/regressiontests/admin_widgets/tests.py
+++ b/tests/regressiontests/admin_widgets/tests.py
@@ -8,7 +8,7 @@ from django.contrib import admin
 from django.contrib.admin import widgets
 from django.contrib.admin.widgets import (FilteredSelectMultiple,
     AdminSplitDateTime, AdminFileWidget, ForeignKeyRawIdWidget, AdminRadioSelect,
-    RelatedFieldWidgetWrapper, ManyToManyRawIdWidget)
+    RelatedFieldWidgetWrapper, ManyToManyRawIdWidget, FilteredSelectSingle)
 from django.core.files.storage import default_storage
 from django.core.files.uploadedfile import SimpleUploadedFile
 from django.db.models import DateField
@@ -181,6 +181,15 @@ class AdminForeignKeyRawIdWidget(DjangoTestCase):
                 'Select a valid choice. That choice is not one of the available choices.')
 
 
+class FilteredSelectSingleWidgetTest(TestCase):
+    def test_render(self):
+        w = FilteredSelectSingle('test')
+        self.assertEqual(
+            conditional_escape(w.render('test', 'test')),
+            '<select name="test" class="selectfilter">\n</select><script type="text/javascript">django.jQuery(document).ready(function(){django.jQuery("#id_test").fk_filter("test")});</script>'
+        )
+
+
 class FilteredSelectMultipleWidgetTest(TestCase):
     def test_render(self):
         w = FilteredSelectMultiple('test', False)
diff --git a/tests/regressiontests/modeladmin/tests.py b/tests/regressiontests/modeladmin/tests.py
index a20e579..a011114 100644
--- a/tests/regressiontests/modeladmin/tests.py
+++ b/tests/regressiontests/modeladmin/tests.py
@@ -6,7 +6,8 @@ from django.contrib.admin.options import ModelAdmin, TabularInline, \
     HORIZONTAL, VERTICAL
 from django.contrib.admin.sites import AdminSite
 from django.contrib.admin.validation import validate
-from django.contrib.admin.widgets import AdminDateWidget, AdminRadioSelect
+from django.contrib.admin.widgets import AdminDateWidget, AdminRadioSelect, \
+    FilteredSelectSingle
 from django.core.exceptions import ImproperlyConfigured
 from django.forms.models import BaseModelFormSet
 from django.forms.widgets import Select
@@ -301,6 +302,38 @@ class ModelAdminTests(TestCase):
             list(ma.get_formsets(request))[0]().forms[0].fields.keys(),
             ['extra', 'transport', 'id', 'DELETE', 'main_band'])
 
+    def test_filter_vertical_foreignkey(self):
+        class ConcertModelAdmin(ModelAdmin):
+            filter_vertical = ('main_band',)
+
+        cma = ConcertModelAdmin(Concert, self.site)
+        cmafa = cma.get_form(request)
+
+        self.assertEqual(type(cmafa.base_fields['main_band'].widget.widget),
+            FilteredSelectSingle)
+        self.assertEqual(type(cmafa.base_fields['opening_band'].widget.widget),
+            Select)
+
+        self.assertEqual(
+            list(cmafa.base_fields['main_band'].widget.choices),
+            [(u'', u'---------'), (self.band.id, u'The Doors')])
+
+        self.assertEqual(
+            type(cmafa.base_fields['opening_band'].widget.widget), Select)
+        self.assertEqual(
+            list(cmafa.base_fields['opening_band'].widget.choices),
+            [(u'', u'---------'), (self.band.id, u'The Doors')])
+
+        self.assertEqual(type(cmafa.base_fields['day'].widget), Select)
+        self.assertEqual(list(cmafa.base_fields['day'].widget.choices),
+            [('', '---------'), (1, 'Fri'), (2, 'Sat')])
+
+        self.assertEqual(type(cmafa.base_fields['transport'].widget),
+            Select)
+        self.assertEqual(
+            list(cmafa.base_fields['transport'].widget.choices),
+            [('', '---------'), (1, 'Plane'), (2, 'Train'), (3, 'Bus')])
+
 
 class ValidationTests(unittest.TestCase):
     def test_validation_only_runs_in_debug(self):
