diff --git a/app/CMakeLists.txt b/app/CMakeLists.txt
index 03d3435fc..a3131e959 100644
--- a/app/CMakeLists.txt
+++ b/app/CMakeLists.txt
@@ -58,6 +58,7 @@ set(MM_SRCS
     fieldsmodel.cpp
     filter/uniquevaluesfiltermodel.cpp
     filter/valuemapfiltermodel.cpp
+    filter/searchproxymodel.cpp
     filtercontroller.cpp
     guidelinecontroller.cpp
     hapticsmodel.cpp
@@ -152,6 +153,7 @@ set(MM_HDRS
     fieldsmodel.h
     filter/uniquevaluesfiltermodel.h
     filter/valuemapfiltermodel.h
+    filter/searchproxymodel.h
     filtercontroller.h
     guidelinecontroller.h
     hapticsmodel.h
diff --git a/app/filter/searchproxymodel.cpp b/app/filter/searchproxymodel.cpp
new file mode 100644
index 000000000..7f16671a2
--- /dev/null
+++ b/app/filter/searchproxymodel.cpp
@@ -0,0 +1,30 @@
+/***************************************************************************
+ *                                                                         *
+ *   This program is free software; you can redistribute it and/or modify  *
+ *   it under the terms of the GNU General Public License as published by  *
+ *   the Free Software Foundation; either version 2 of the License, or     *
+ *   (at your option) any later version.                                   *
+ *                                                                         *
+ ***************************************************************************/
+
+#include "searchproxymodel.h"
+
+SearchProxyModel::SearchProxyModel( QObject *parent )
+  : QSortFilterProxyModel{parent}
+{
+  setFilterCaseSensitivity( Qt::CaseInsensitive );
+}
+
+QString SearchProxyModel::searchString() const
+{
+  return mSearchString;
+}
+
+void SearchProxyModel::setSearchString( const QString &search )
+{
+  if ( mSearchString == search )
+    return;
+  mSearchString = search;
+  setFilterFixedString( search );
+  emit searchStringChanged();
+}
diff --git a/app/filter/searchproxymodel.h b/app/filter/searchproxymodel.h
new file mode 100644
index 000000000..d276829cd
--- /dev/null
+++ b/app/filter/searchproxymodel.h
@@ -0,0 +1,36 @@
+/***************************************************************************
+ *                                                                         *
+ *   This program is free software; you can redistribute it and/or modify  *
+ *   it under the terms of the GNU General Public License as published by  *
+ *   the Free Software Foundation; either version 2 of the License, or     *
+ *   (at your option) any later version.                                   *
+ *                                                                         *
+ ***************************************************************************/
+
+#ifndef SEARCHPROXYMODEL_H
+#define SEARCHPROXYMODEL_H
+
+#include <QSortFilterProxyModel>
+#include <QtQml/qqmlregistration.h>
+
+class SearchProxyModel : public QSortFilterProxyModel
+{
+    Q_OBJECT
+    QML_ELEMENT
+
+    Q_PROPERTY( QString searchString READ searchString WRITE setSearchString NOTIFY searchStringChanged )
+
+  public:
+    explicit SearchProxyModel( QObject *parent = nullptr );
+
+    QString searchString() const;
+    void setSearchString( const QString &newSearchString );
+
+  signals:
+    void searchStringChanged();
+
+  private:
+    QString mSearchString;
+};
+
+#endif // SEARCHPROXYMODEL_H
diff --git a/app/filter/valuemapfiltermodel.cpp b/app/filter/valuemapfiltermodel.cpp
index 5489bacdd..9d7c1137b 100644
--- a/app/filter/valuemapfiltermodel.cpp
+++ b/app/filter/valuemapfiltermodel.cpp
@@ -17,23 +17,22 @@ ValueMapFilterModel::ValueMapFilterModel( QObject *parent )
 int ValueMapFilterModel::rowCount( const QModelIndex &parent ) const
 {
   Q_UNUSED( parent )
-  return mFilteredItems.size();
+  return mItems.size();
 }
 
 QVariant ValueMapFilterModel::data( const QModelIndex &index, int role ) const
 {
-  if ( !index.isValid() || index.row() >= mFilteredItems.size() )
+  if ( !index.isValid() || index.row() >= mItems.size() )
     return {};
 
-  const Item &item = mFilteredItems.at( index.row() );
+  const Item &item = mItems.at( index.row() );
 
   switch ( role )
   {
     case Qt::DisplayRole:
-    case TextRole:
-      return item.text;
-    case ValueRole:
-      return item.value;
+      return item.description;
+    case KeyRole:
+      return item.key;
     default:
       return {};
   }
@@ -41,11 +40,10 @@ QVariant ValueMapFilterModel::data( const QModelIndex &index, int role ) const
 
 QHash<int, QByteArray> ValueMapFilterModel::roleNames() const
 {
-  return
-  {
-    { TextRole, "text" },
-    { ValueRole, "value" },
-  };
+  QHash<int, QByteArray> roles = QAbstractListModel::roleNames();
+  roles[KeyRole] = QStringLiteral( "Key" ).toLatin1();
+
+  return roles;
 }
 
 QVariantMap ValueMapFilterModel::config() const
@@ -64,24 +62,10 @@ void ValueMapFilterModel::setConfig( const QVariantMap &config )
   populate();
 }
 
-QString ValueMapFilterModel::searchText() const
-{
-  return mSearchText;
-}
-
-void ValueMapFilterModel::setSearchText( const QString &searchText )
-{
-  if ( mSearchText == searchText )
-    return;
-
-  mSearchText = searchText;
-  emit searchTextChanged();
-
-  applyFilter();
-}
-
 void ValueMapFilterModel::populate()
 {
+  beginResetModel();
+
   mItems.clear();
 
   const QVariantList mapList = mConfig.value( QStringLiteral( "map" ) ).toList();
@@ -90,30 +74,16 @@ void ValueMapFilterModel::populate()
   for ( const QVariant &entry : mapList )
   {
     const QVariantMap entryMap = entry.toMap();
+
     if ( entryMap.isEmpty() )
       continue;
 
     // Each entry is a single-key map: {"Display Text": "stored_value"}
     Item item;
-    item.text = entryMap.constBegin().key();
-    item.value = entryMap.constBegin().value().toString();
-    mItems.append( item );
-  }
-
-  applyFilter();
-}
+    item.description = entryMap.constBegin().key();
+    item.key = entryMap.constBegin().value().toString();
 
-void ValueMapFilterModel::applyFilter()
-{
-  beginResetModel();
-  mFilteredItems.clear();
-
-  for ( const Item &item : std::as_const( mItems ) )
-  {
-    if ( mSearchText.isEmpty() || item.text.contains( mSearchText, Qt::CaseInsensitive ) )
-    {
-      mFilteredItems.append( item );
-    }
+    mItems.append( item );
   }
 
   endResetModel();
diff --git a/app/filter/valuemapfiltermodel.h b/app/filter/valuemapfiltermodel.h
index 317b5185f..2313cfe90 100644
--- a/app/filter/valuemapfiltermodel.h
+++ b/app/filter/valuemapfiltermodel.h
@@ -23,13 +23,11 @@ class ValueMapFilterModel : public QAbstractListModel
     QML_ELEMENT
 
     Q_PROPERTY( QVariantMap config READ config WRITE setConfig NOTIFY configChanged )
-    Q_PROPERTY( QString searchText READ searchText WRITE setSearchText NOTIFY searchTextChanged )
 
   public:
     enum Roles
     {
-      TextRole = Qt::UserRole + 1,
-      ValueRole
+      KeyRole = Qt::UserRole + 1, // DisplayRole is used for description
     };
     Q_ENUM( Roles )
 
@@ -43,27 +41,21 @@ class ValueMapFilterModel : public QAbstractListModel
     QVariantMap config() const;
     void setConfig( const QVariantMap &config );
 
-    QString searchText() const;
-    void setSearchText( const QString &searchText );
-
   signals:
     void configChanged();
-    void searchTextChanged();
 
   private:
     void populate();
-    void applyFilter();
 
     struct Item
     {
-      QString text;
-      QString value;
+      QString description;
+      QString key;
     };
 
     QVariantMap mConfig;
-    QString mSearchText;
+
     QList<Item> mItems;
-    QList<Item> mFilteredItems;
 };
 
 #endif // VALUEMAPFILTERMODEL_H
diff --git a/app/qml/CMakeLists.txt b/app/qml/CMakeLists.txt
index 62e2c8693..c985034e4 100644
--- a/app/qml/CMakeLists.txt
+++ b/app/qml/CMakeLists.txt
@@ -65,6 +65,7 @@ set(MM_QML
     filters/components/MMFilterBanner.qml
     filters/components/MMFilterBoolInput.qml
     filters/components/MMFilterDateRange.qml
+    filters/components/MMFilterDropdownValueMapInput.qml
     filters/components/MMFilterDropdownValueRelationInput.qml
     filters/components/MMFilterDropdownUniqueValuesInput.qml
     filters/components/MMFilterRangeInput.qml
diff --git a/app/qml/filters/MMFiltersPanel.qml b/app/qml/filters/MMFiltersPanel.qml
index 0c07a1709..c2a713eb6 100644
--- a/app/qml/filters/MMFiltersPanel.qml
+++ b/app/qml/filters/MMFiltersPanel.qml
@@ -142,7 +142,6 @@ MMComponents.MMDrawer {
                 base['isMultiSelect'] = isMulti
 
                 const dropdownConfig = __activeProject.filterController.getDropdownConfiguration( modelData.filterId )
-                console.log( "--> dropdown config:", JSON.stringify( dropdownConfig ) )
 
                 if ( !Object.keys( dropdownConfig ).length )
                 {
@@ -156,6 +155,16 @@ MMComponents.MMDrawer {
                   base["fieldName"] = dropdownConfig["field_name"]
                   setSource( "components/MMFilterDropdownUniqueValuesInput.qml", base )
                 }
+                else if ( dropdownConfig["type"] === "value_relation" )
+                {
+                  base["widgetConfig"] = dropdownConfig["config"]
+                  setSource( "components/MMFilterDropdownValueRelationInput.qml", base )
+                }
+                else if ( dropdownConfig["type"] === "value_map" )
+                {
+                  base["widgetConfig"] = dropdownConfig["config"]
+                  setSource( "components/MMFilterDropdownValueMapInput.qml", base )
+                }
               }
             }
 
diff --git a/app/qml/filters/components/MMFilterDropdownUniqueValuesInput.qml b/app/qml/filters/components/MMFilterDropdownUniqueValuesInput.qml
index 59e6ef01b..bef77c98c 100644
--- a/app/qml/filters/components/MMFilterDropdownUniqueValuesInput.qml
+++ b/app/qml/filters/components/MMFilterDropdownUniqueValuesInput.qml
@@ -10,7 +10,7 @@
 import QtQuick
 
 import MMInput as MM
-import "../../components"
+import "../../components" as MMComponents
 
 Column {
   id: root
@@ -28,7 +28,7 @@ Column {
 
   spacing: __style.margin8
 
-  MMText {
+  MMComponents.MMText {
     width: parent.width
 
     text: root.filterName
@@ -72,13 +72,17 @@ Column {
     // TODO: add indication that model is loading features
     // TODO: add animation when drawer height is changed
 
-    sourceComponent: MMListMultiselectDrawer {
+    sourceComponent: MMComponents.MMListMultiselectDrawer {
       drawerHeader.title: root.filterName
 
-      withSearch: true
+      withSearch: uniqueValuesModel.rowCount() > 5
       multiSelect: root.isMultiSelect
 
-      list.model: uniqueValuesModel
+      list.model: MM.SearchProxyModel {
+        id: searchProxyModel
+
+        sourceModel: uniqueValuesModel
+      }
 
       textRole: "display"
       secondaryTextRole: ""
@@ -93,7 +97,7 @@ Column {
         close()
       }
 
-      onSearchTextChanged: console.log("--> Search me") // TODO: search
+      onSearchTextChanged: ( searchText ) => searchProxyModel.searchString = searchText
 
       onClosed: dropdownDrawerLoader.active = false
 
diff --git a/app/qml/filters/components/MMFilterDropdownValueMapInput.qml b/app/qml/filters/components/MMFilterDropdownValueMapInput.qml
new file mode 100644
index 000000000..e187b03e0
--- /dev/null
+++ b/app/qml/filters/components/MMFilterDropdownValueMapInput.qml
@@ -0,0 +1,121 @@
+/***************************************************************************
+ *                                                                         *
+ *   This program is free software; you can redistribute it and/or modify  *
+ *   it under the terms of the GNU General Public License as published by  *
+ *   the Free Software Foundation; either version 2 of the License, or     *
+ *   (at your option) any later version.                                   *
+ *                                                                         *
+ ***************************************************************************/
+
+import QtQuick
+
+import MMInput as MM
+import "../../components" as MMComponents
+
+Column {
+  id: root
+
+  required property string filterName
+  required property var widgetConfig
+
+  required property var currentValue
+
+  property bool isMultiSelect: false
+
+  width: parent.width
+
+  spacing: __style.margin8
+
+  MMComponents.MMText {
+    width: parent.width
+
+    text: root.filterName
+
+    visible: text
+    font: __style.p6
+    color: __style.nightColor
+  }
+
+  MMFilterTextInput {
+    id: dropdownInput
+
+    width: parent.width
+
+    type: MMFilterTextInput.InputType.Dropdown
+
+    checked: text !== ""
+
+    placeholderText: qsTr( "Select..." )
+    text: root.currentValue && root.currentValue.length ? qsTr( "%1 selected" ).arg( root.currentValue.length ) : ""
+
+    onTextClicked: openDrawer()
+    onRightContentClicked: {
+      if ( root.currentValue && root.currentValue.length )
+      {
+        root.currentValue = []
+        root.currentValueChanged()
+      }
+      else
+      {
+        openDrawer()
+      }
+    }
+  }
+
+  Loader {
+    id: dropdownDrawerLoader
+
+    active: false
+
+    // TODO: add indication that model is loading features
+    // TODO: add animation when drawer height is changed
+
+    sourceComponent: MMComponents.MMListMultiselectDrawer {
+      drawerHeader.title: root.filterName
+
+      withSearch: valueMapModel.rowCount() > 5
+      multiSelect: root.isMultiSelect
+
+      list.model: MM.SearchProxyModel {
+        id: searchProxyModel
+
+        sourceModel: MM.ValueMapFilterModel {
+          id: valueMapModel
+
+          config: root.widgetConfig
+        }
+      }
+
+      textRole: "display"
+      secondaryTextRole: ""
+      valueRole: "Key"
+
+      onSelectionFinished: function( selectedItems ) {
+        root.currentValue = selectedItems
+
+        // TODO: this is just a hack, we need to add a dedicated signal instead, Qt does not always get that array length has changed
+        root.currentValueChanged()
+
+        close()
+      }
+
+      onSearchTextChanged: ( searchText ) => searchProxyModel.searchString = searchText
+
+      onClosed: dropdownDrawerLoader.active = false
+
+      Component.onCompleted: {
+        if ( root.currentValue )
+        {
+          // preselect choices if any are set
+          selected = root.currentValue
+        }
+        open()
+      }
+    }
+  }
+
+  function openDrawer() {
+    dropdownDrawerLoader.active = true
+    dropdownDrawerLoader.focus = true
+  }
+}
diff --git a/app/qml/filters/components/MMFilterDropdownValueRelationInput.qml b/app/qml/filters/components/MMFilterDropdownValueRelationInput.qml
index 34a99fdd5..baebdf811 100644
--- a/app/qml/filters/components/MMFilterDropdownValueRelationInput.qml
+++ b/app/qml/filters/components/MMFilterDropdownValueRelationInput.qml
@@ -10,7 +10,7 @@
 import QtQuick
 
 import mm 1.0 as MM
-import "../../components"
+import "../../components" as MMComponents
 
 Column {
   id: root
@@ -27,7 +27,7 @@ Column {
 
   spacing: __style.margin8
 
-  MMText {
+  MMComponents.MMText {
     width: parent.width
 
     text: root.filterName
@@ -71,10 +71,10 @@ Column {
     // TODO: add indication that model is loading features
     // TODO: add animation when drawer height is changed
 
-    sourceComponent: MMListMultiselectDrawer {
+    sourceComponent: MMComponents.MMListMultiselectDrawer {
       drawerHeader.title: root.filterName
 
-      withSearch: true
+      withSearch: vrDropdownModel.count > 5
       multiSelect: root.isMultiSelect
 
       list.model: MM.ValueRelationFeaturesModel {
@@ -84,33 +84,17 @@ Column {
       }
 
       textRole: "FeatureTitle"
-      valueRole: "FeatureId"
+      valueRole: "Key"
 
       onSelectionFinished: function( selectedItems ) {
 
-        if ( root.isMultiSelect )
-        {
-          let isNull = selectedItems.length === 0
+        //
+        // Large fids could be converted to scientific notation on their way to cpp,
+        // so we convert them to string first in JS.
+        //
+        selectedItems = selectedItems.map( x => x.toString() )
 
-          if ( !isNull )
-          {
-            // We need to convert feature id to string prior to sending it to C++ in order to
-            // avoid conversion to scientific notation.
-            selectedItems = selectedItems.map( function(x) { return x.toString() } )
-          }
-
-          root.currentValue = vrDropdownModel.convertToQgisType( selectedItems ).replace( "{","" ).replace( "}","" ).split( ',' ).filter( x => x )
-        }
-        else
-        {
-          // We need to convert feature id to string prior to sending it to C++ in order to
-          // avoid conversion to scientific notation.
-          selectedItems = selectedItems.toString()
-
-          root.currentValue = [vrDropdownModel.convertToKey( selectedItems )]
-        }
-
-        console.log("--> selected items:", selectedItems)
+        root.currentValue = selectedItems
 
         // TODO: this is just a hack, we need to add a dedicated signal instead, Qt does not always get that array length has changed
         root.currentValueChanged()
@@ -118,15 +102,15 @@ Column {
         close()
       }
 
+      onSearchTextChanged: ( searchText ) => vrDropdownModel.searchExpression = searchText
+
       onClosed: dropdownDrawerLoader.active = false
 
       Component.onCompleted: {
         if ( root.currentValue )
         {
           // preselect choices if any are set
-
-          // TODO: this is expecting FIDs, but we fill in Key-column values, what to do?
-          // selected = root.currentValue
+          selected = root.currentValue
         }
 
         open()
diff --git a/app/valuerelationfeaturesmodel.cpp b/app/valuerelationfeaturesmodel.cpp
index 1d4f8066b..94af425bd 100644
--- a/app/valuerelationfeaturesmodel.cpp
+++ b/app/valuerelationfeaturesmodel.cpp
@@ -48,6 +48,33 @@ void ValueRelationFeaturesModel::setupFeatureRequest( QgsFeatureRequest &request
   }
 }
 
+QHash<int, QByteArray> ValueRelationFeaturesModel::roleNames() const
+{
+  QHash<int, QByteArray> roles = LayerFeaturesModel::roleNames();
+  roles[KeyRole] = QStringLiteral( "Key" ).toLatin1();
+
+  return roles;
+}
+
+QVariant ValueRelationFeaturesModel::data( const QModelIndex &index, int role ) const
+{
+  int row = index.row();
+  if ( row < 0 || row >= mFeatures.count() )
+    return QVariant();
+
+  if ( !index.isValid() )
+    return QVariant();
+
+  const FeatureLayerPair pair = mFeatures.at( index.row() );
+
+  if ( role == KeyRole )
+  {
+    return pair.feature().attribute( mKeyField );
+  }
+
+  return LayerFeaturesModel::data( index, role );
+}
+
 void ValueRelationFeaturesModel::setup()
 {
   if ( mConfig.isEmpty() )
diff --git a/app/valuerelationfeaturesmodel.h b/app/valuerelationfeaturesmodel.h
index 50e75a3fc..c4d4f8fb9 100644
--- a/app/valuerelationfeaturesmodel.h
+++ b/app/valuerelationfeaturesmodel.h
@@ -29,9 +29,19 @@ class ValueRelationFeaturesModel : public LayerFeaturesModel
 
   public:
 
+    enum ValueRelationFeaturesModelRoles
+    {
+      KeyRole = LayerFeaturesModel::LastRole + 1, // the key-column value
+      LastRole = KeyRole
+    };
+    Q_ENUM( ValueRelationFeaturesModelRoles );
+
     explicit ValueRelationFeaturesModel( QObject *parent = nullptr );
     ~ValueRelationFeaturesModel() override;
 
+    QVariant data( const QModelIndex &index, int role = Qt::DisplayRole ) const override;
+    QHash<int, QByteArray> roleNames() const override;
+
     void setup() override;
     void reset() override;
     void setupFeatureRequest( QgsFeatureRequest &request ) override;
