From f1e29ac9eabe494659a09f2ed1500c993c805f61 Mon Sep 17 00:00:00 2001
From: Lauri Himanen <lauri.himanen@gmail.com>
Date: Tue, 14 Sep 2021 15:34:13 +0300
Subject: [PATCH] Removed dead code related to the old search interface,
 removed the 'New'-prefix from the search components.

---
 gui/src/components/DatasetPage.js             |    6 +-
 gui/src/components/UserdataPage.js            |    6 +-
 gui/src/components/dft/DFTVisualizations.js   |  189 ---
 gui/src/components/domainData.js              |   34 -
 gui/src/components/ems/EMSVisualizations.js   |   42 -
 gui/src/components/search/FilterChip.js       |    4 +
 gui/src/components/search/FilterContext.js    | 1143 --------------
 gui/src/components/search/FilterSummary.js    |    6 +-
 gui/src/components/search/NewSearch.js        |  145 --
 gui/src/components/search/NewSearchBar.js     |  431 ------
 .../components/search/QuantityHistogram.js    |  145 --
 gui/src/components/search/Search.js           |  733 ++-------
 gui/src/components/search/SearchBar.js        |  674 +++++----
 gui/src/components/search/SearchContext.js    | 1335 +++++++++++++----
 gui/src/components/search/SearchPage.js       |   79 -
 .../components/search/SearchPageEntries.js    |    6 +-
 .../components/search/SearchPageMaterials.js  |    6 +-
 gui/src/components/search/UploadsHistogram.js |  300 ----
 .../components/search/input/InputCheckbox.js  |    2 +-
 .../search/input/InputCheckboxes.js           |    2 +-
 .../components/search/input/InputDateRange.js |    2 +-
 .../search/input/InputPeriodicTable.js        |    4 +-
 gui/src/components/search/input/InputRadio.js |    2 +-
 .../components/search/input/InputSelect.js    |    2 +-
 .../components/search/input/InputSlider.js    |    2 +-
 gui/src/components/search/input/InputText.js  |    2 +-
 .../components/search/input/PeriodicTable.js  |  230 ---
 .../components/search/menus/FilterMainMenu.js |    2 +-
 gui/src/components/search/menus/FilterMenu.js |    2 +-
 .../search/menus/FilterSubMenuElements.js     |    2 +-
 .../components/search/results/GroupList.js    |  243 ---
 .../search/results/MaterialsList.js           |  141 --
 .../search/results/SearchResults.js           |    3 +-
 .../search/results/UploadersList.js           |   50 -
 ...eriodicTableData.json => elementData.json} |    0
 35 files changed, 1584 insertions(+), 4391 deletions(-)
 delete mode 100644 gui/src/components/dft/DFTVisualizations.js
 delete mode 100644 gui/src/components/ems/EMSVisualizations.js
 delete mode 100644 gui/src/components/search/FilterContext.js
 delete mode 100644 gui/src/components/search/NewSearch.js
 delete mode 100644 gui/src/components/search/NewSearchBar.js
 delete mode 100644 gui/src/components/search/QuantityHistogram.js
 delete mode 100644 gui/src/components/search/SearchPage.js
 delete mode 100644 gui/src/components/search/UploadsHistogram.js
 delete mode 100644 gui/src/components/search/input/PeriodicTable.js
 delete mode 100644 gui/src/components/search/results/GroupList.js
 delete mode 100644 gui/src/components/search/results/MaterialsList.js
 delete mode 100644 gui/src/components/search/results/UploadersList.js
 rename gui/src/{components/search/input/PeriodicTableData.json => elementData.json} (100%)

diff --git a/gui/src/components/DatasetPage.js b/gui/src/components/DatasetPage.js
index 6d30b2c041..143a63a1e4 100644
--- a/gui/src/components/DatasetPage.js
+++ b/gui/src/components/DatasetPage.js
@@ -20,8 +20,8 @@ import PropTypes from 'prop-types'
 import { Typography, makeStyles } from '@material-ui/core'
 import { errorContext } from './errors'
 import { useApi } from './apiV1'
-import NewSearch from './search/NewSearch'
-import { SearchContext } from './search/FilterContext'
+import Search from './search/Search'
+import { SearchContext } from './search/SearchContext'
 import { DOI } from './search/results/DatasetList'
 
 export const help = `
@@ -60,7 +60,7 @@ const UserdataPage = React.memo(({match}) => {
     resource="entries"
     filtersLocked={datasetFilter}
   >
-    <NewSearch header={
+    <Search header={
       <div className={styles.header}>
         <Typography variant="h4">
           {dataset.name || (dataset.isEmpty && 'Empty or non existing dataset') || 'loading ...'}
diff --git a/gui/src/components/UserdataPage.js b/gui/src/components/UserdataPage.js
index f8570f6e7c..46df92171d 100644
--- a/gui/src/components/UserdataPage.js
+++ b/gui/src/components/UserdataPage.js
@@ -17,8 +17,8 @@
  */
 import React from 'react'
 import { withLoginRequired } from './apiV1'
-import { SearchContext } from './search/FilterContext'
-import NewSearch from './search/NewSearch'
+import { SearchContext } from './search/SearchContext'
+import Search from './search/Search'
 
 export const help = `
 This page allows you to **inspect** and **manage** you own data. It is similar to the
@@ -78,7 +78,7 @@ const UserdataPage = React.memo(() => {
     resource="entries"
     filtersLocked={filtersLocked}
   >
-    <NewSearch/>
+    <Search/>
   </SearchContext>
 })
 
diff --git a/gui/src/components/dft/DFTVisualizations.js b/gui/src/components/dft/DFTVisualizations.js
deleted file mode 100644
index dfeb5e7a07..0000000000
--- a/gui/src/components/dft/DFTVisualizations.js
+++ /dev/null
@@ -1,189 +0,0 @@
-/*
- * Copyright The NOMAD Authors.
- *
- * This file is part of NOMAD. See https://nomad-lab.eu for further info.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-import React, { useContext, useEffect } from 'react'
-import PropTypes from 'prop-types'
-import { Grid } from '@material-ui/core'
-import { makeStyles } from '@material-ui/core/styles'
-import QuantityHistogram from '../search/QuantityHistogram'
-import { searchContext } from '../search/SearchContext'
-import { resolveRef } from '../archive/metainfo'
-import { nomadTheme } from '../../config'
-import Markdown from '../Markdown'
-
-export function DFTMethodVisualizations(props) {
-  const {info} = props
-  const {response: {statistics, metric}, setStatistics} = useContext(searchContext)
-  useEffect(() => {
-    setStatistics(['dft.code_name', 'dft.basis_set', 'dft.xc_functional'])
-    // eslint-disable-next-line
-  }, [])
-
-  if (statistics.code_name && info) {
-    // filter based on known codes, since elastic search might return 0 aggregations on
-    // obsolete code names
-    const filteredCodeNames = {}
-    const defaultValue = {
-      code_runs: 0
-    }
-    defaultValue[metric] = 0
-    info.codes.forEach(key => {
-      filteredCodeNames[key] = statistics.code_name[key] || defaultValue
-    })
-    statistics.code_name = filteredCodeNames
-  }
-
-  return (
-    <Grid container spacing={2}>
-      <Grid item xs={8}>
-        <QuantityHistogram quantity="dft.code_name" title="Code" initialScale={0.25} columns={2} />
-      </Grid>
-      <Grid item xs={4}>
-        <QuantityHistogram quantity="dft.basis_set" title="Basis set" initialScale={0.25} />
-        <QuantityHistogram quantity="dft.xc_functional" title="XC functionals" initialScale={0.5} />
-      </Grid>
-    </Grid>
-  )
-}
-
-DFTMethodVisualizations.propTypes = {
-  info: PropTypes.object
-}
-
-export function DFTSystemVisualizations(props) {
-  const {info} = props
-  const {response: {statistics, metric}, setStatistics} = useContext(searchContext)
-  useEffect(() => {
-    setStatistics(['dft.labels_springer_compound_class', 'dft.system', 'dft.crystal_system', 'dft.compound_type'])
-    // eslint-disable-next-line
-  }, [])
-
-  if (statistics.code_name && info) {
-    // filter based on known codes, since elastic search might return 0 aggregations on
-    // obsolete code names
-    const filteredCodeNames = {}
-    const defaultValue = {
-      code_runs: 0
-    }
-    defaultValue[metric] = 0
-    info.codes.forEach(key => {
-      filteredCodeNames[key] = statistics.code_name[key] || defaultValue
-    })
-    statistics.code_name = filteredCodeNames
-  }
-
-  return (
-    <Grid container spacing={2}>
-      <Grid item xs={4}>
-        <QuantityHistogram quantity="dft.system" title="System type" initialScale={0.25} />
-        <QuantityHistogram quantity="dft.crystal_system" title="Crystal system" />
-      </Grid>
-      <Grid item xs={4}>
-        <QuantityHistogram quantity="dft.compound_type" title="Compound type" initialScale={0.25} />
-      </Grid>
-      <Grid item xs={4}>
-        <QuantityHistogram quantity="dft.labels_springer_compound_class" title="Compound classification" />
-      </Grid>
-    </Grid>
-  )
-}
-
-DFTSystemVisualizations.propTypes = {
-  info: PropTypes.object
-}
-
-const useMetainfoTooltipStyles = makeStyles(theme => ({
-  root: {
-    display: 'flex',
-    flexDirection: 'column',
-    padding: 2
-  },
-  tooltipMarkdown: {
-    fontSize: nomadTheme.overrides.MuiTooltip.tooltip.fontSize,
-    color: 'white',
-    '& a': {
-      color: theme.palette.primary.light
-    }
-  }
-}))
-
-function MetaInfoTooltip({def, path}) {
-  const classes = useMetainfoTooltipStyles()
-  let description = def.description
-  if (!description && def.sub_section) {
-    description = resolveRef(def.sub_section)?.description
-  }
-  return <div className={classes.root} >
-    <Markdown
-      classes={{root: classes.tooltipMarkdown}}
-    >{`${description?.slice(0, description.indexOf('.') || undefined)}. Click [here](/metainfo/${path}) for full the definition.`}</Markdown>
-  </div>
-}
-
-MetaInfoTooltip.propTypes = {
-  def: PropTypes.object,
-  path: PropTypes.string
-}
-
-const workflowTypeLabels = {
-  'geometry_optimization': 'geometry optimization',
-  'phonon': 'phonons',
-  'elastic': 'elastic constants',
-  'molecular_dynamics': 'molecular dynamics'
-}
-
-export function DFTPropertyVisualizations(props) {
-  const {info} = props
-  const {response: {statistics, metric}, setStatistics} = useContext(searchContext)
-  useEffect(() => {
-    setStatistics([
-      'dft.searchable_quantities',
-      'dft.labels_springer_classification',
-      'dft.workflow.type'
-    ])
-    // eslint-disable-next-line
-  }, [])
-
-  if (statistics.code_name && info) {
-    // filter based on known codes, since elastic search might return 0 aggregations on
-    // obsolete code names
-    const filteredCodeNames = {}
-    const defaultValue = {
-      code_runs: 0
-    }
-    defaultValue[metric] = 0
-    info.codes.forEach(key => {
-      filteredCodeNames[key] = statistics.code_name[key] || defaultValue
-    })
-    statistics.code_name = filteredCodeNames
-  }
-
-  return (
-    <Grid container spacing={2}>
-      <Grid item xs={12}>
-        <QuantityHistogram quantity="dft.labels_springer_classification" title="Functional classification" initialScale={1} multiple/>
-      </Grid>
-      <Grid item xs={12}>
-        <QuantityHistogram quantity="dft.workflow.type" title="Workflows" valueLabels={workflowTypeLabels} initialScale={0.25} />
-      </Grid>
-    </Grid>
-  )
-}
-
-DFTPropertyVisualizations.propTypes = {
-  info: PropTypes.object
-}
diff --git a/gui/src/components/domainData.js b/gui/src/components/domainData.js
index 7de1385b33..012e7783a3 100644
--- a/gui/src/components/domainData.js
+++ b/gui/src/components/domainData.js
@@ -16,10 +16,6 @@
  * limitations under the License.
  */
 import React from 'react'
-import {
-  DFTSystemVisualizations, DFTPropertyVisualizations, DFTMethodVisualizations
-} from './dft/DFTVisualizations'
-import EMSVisualizations from './ems/EMSVisualizations'
 import { Link, Typography } from '@material-ui/core'
 import { amber } from '@material-ui/core/colors'
 
@@ -42,27 +38,6 @@ export const domainData = ({
         ? data.dft.code_name.charAt(0).toUpperCase() + data.dft.code_name.slice(1) + ' run'
         : 'Code run',
     searchPlaceholder: 'enter atoms, codes, functionals, or other quantity values',
-    /**
-     * A set of components and metadata that is used to present tabs of search visualizations
-     * in addition to the globally available elements and users view.
-     */
-    searchVisualizations: {
-      'system': {
-        component: DFTSystemVisualizations,
-        label: 'System',
-        description: 'Shows histograms on system metadata'
-      },
-      'method': {
-        component: DFTMethodVisualizations,
-        label: 'Method',
-        description: 'Shows histograms on method metadata'
-      },
-      'properties': {
-        component: DFTPropertyVisualizations,
-        label: 'Properties',
-        description: 'Shows histograms on the availability of key properties'
-      }
-    },
     searchMetrics: {
       code_runs: {
         label: 'Entries',
@@ -178,13 +153,6 @@ export const domainData = ({
     entryLabelPlural: 'entries',
     entryTitle: () => 'Experiment',
     searchPlaceholder: 'enter atoms, experimental methods, or other quantity values',
-    searchVisualizations: {
-      'metadata': {
-        component: EMSVisualizations,
-        label: 'Metadata',
-        description: 'Shows histograms on system metadata'
-      }
-    },
     /**
      * Metrics are used to show values for aggregations. Each metric has a key (used
      * for API calls), a label (used in the select form), and result string (to show
@@ -250,8 +218,6 @@ export const domainData = ({
     entryLabelPlural: 'calculations',
     entryTitle: () => 'Quantum-computer calculation',
     searchPlaceholder: 'enter atoms',
-    searchVisualizations: {
-    },
     /**
      * Metrics are used to show values for aggregations. Each metric has a key (used
      * for API calls), a label (used in the select form), and result string (to show
diff --git a/gui/src/components/ems/EMSVisualizations.js b/gui/src/components/ems/EMSVisualizations.js
deleted file mode 100644
index 25606dc12b..0000000000
--- a/gui/src/components/ems/EMSVisualizations.js
+++ /dev/null
@@ -1,42 +0,0 @@
-/*
- * Copyright The NOMAD Authors.
- *
- * This file is part of NOMAD. See https://nomad-lab.eu for further info.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-import React, { useContext, useEffect } from 'react'
-import { Grid } from '@material-ui/core'
-import QuantityHistogram from '../search/QuantityHistogram'
-import { searchContext } from '../search/SearchContext'
-
-export default function EMSVisualizations(props) {
-  const {setStatistics} = useContext(searchContext)
-  useEffect(() => {
-    setStatistics(['ems.method', 'ems.probing_method', 'ems.sample_microstructure', 'ems.sample_constituents'])
-    // eslint-disable-next-line
-  }, [])
-
-  return (
-    <Grid container spacing={2}>
-      <Grid item xs={6}>
-        <QuantityHistogram quantity="ems.method" title="Method" />
-        <QuantityHistogram quantity="ems.probing_method" title="Probing" />
-      </Grid>
-      <Grid item xs={6}>
-        <QuantityHistogram quantity="ems.sample_microstructure" title="Sample structure" />
-        <QuantityHistogram quantity="ems.sample_constituents" title="Sample constituents" />
-      </Grid>
-    </Grid>
-  )
-}
diff --git a/gui/src/components/search/FilterChip.js b/gui/src/components/search/FilterChip.js
index 2588c771ab..c8fa1ab4e1 100644
--- a/gui/src/components/search/FilterChip.js
+++ b/gui/src/components/search/FilterChip.js
@@ -22,6 +22,10 @@ import LockIcon from '@material-ui/icons/Lock'
 import { Chip } from '@material-ui/core'
 import PropTypes from 'prop-types'
 
+/**
+ * Thin wrapper for MUI Chip that is used for displaying (and possibly removing)
+ * filter values.
+ */
 const useStyles = makeStyles(theme => ({
   root: {
     padding: theme.spacing(0.5)
diff --git a/gui/src/components/search/FilterContext.js b/gui/src/components/search/FilterContext.js
deleted file mode 100644
index af4c4fe119..0000000000
--- a/gui/src/components/search/FilterContext.js
+++ /dev/null
@@ -1,1143 +0,0 @@
-/*
- * Copyright The NOMAD Authors.
- *
- * This file is part of NOMAD. See https://nomad-lab.eu for further info.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-import React, { useCallback, useEffect, useState, useRef, useMemo, useContext } from 'react'
-import {
-  atom,
-  atomFamily,
-  selector,
-  useSetRecoilState,
-  useRecoilValue,
-  useRecoilState,
-  useRecoilCallback
-} from 'recoil'
-import {
-  debounce,
-  isEmpty,
-  isArray,
-  isPlainObject,
-  isNil,
-  isString
-} from 'lodash'
-import qs from 'qs'
-import PropTypes from 'prop-types'
-import { useHistory } from 'react-router-dom'
-import { useApi } from '../apiV1'
-import { setToArray, formatMeta, parseMeta } from '../../utils'
-import searchQuantities from '../../searchQuantities'
-import { Quantity } from '../../units'
-
-export const filters = new Set() // Contains the full names of all the available filters
-export const filterGroups = [] // Mapping from filter full name -> group
-export const filterAbbreviations = [] // Mapping of filter full name -> abbreviation
-export const filterFullnames = [] // Mapping of filter abbreviation -> full name
-export const filterData = {} // Stores data for each registered filter
-export const labelMaterial = 'Material'
-export const labelElements = 'Elements / Formula'
-export const labelSymmetry = 'Symmetry'
-export const labelMethod = 'Method'
-export const labelSimulation = 'Simulation'
-export const labelDFT = 'DFT'
-export const labelGW = 'GW'
-export const labelProperties = 'Properties'
-export const labelElectronic = 'Electronic'
-export const labelVibrational = 'Vibrational'
-export const labelAuthor = 'Author / Origin'
-export const labelAccess = 'Access'
-export const labelDataset = 'Dataset'
-export const labelIDs = 'IDs'
-
-/**
- * This function is used to register a new filter within the FilterContext.
- * Filters are entities that can be searched throuh the filter panel and the
- * search bar, and can be encoded in the URL. Notice that a filter in this
- * context does not have to correspond to a quantity in the metainfo.
- *
- * Only registered filters may be searched for. The registration must happen
- * before any components use the filters. This is because:
- *  - The initial aggregation results must be fetched before any components
- *  using the filter values are rendered.
- *  - Several components need to know the list of available filters (e.g. the
- *  search bar and  the search panel). If filters are only registered during
- *  component initialization, it may already be too late to update other
- *  components.
- *
- * @param {string} name Name of the filter.
- * @param {string} group The group into which the filter belongs to. Groups
- * are used to e.g. in showing FilterSummaries about a group of filters.
- * @param {string|object} agg Custom setter/getter for the aggregation value. As a
- * shortcut you can provide an ES aggregation type as a string,
- * @param {object} value Custom setter/getter for the filter value.
- * @param {bool} multiple Whether this filter supports several values:
- * controls whether setting the value appends or overwrites.
- */
-function registerFilter(name, group, agg, value, multiple = true) {
-  filters.add(name)
-  if (group) {
-    filterGroups[group]
-      ? filterGroups[group].add(name)
-      : filterGroups[group] = new Set([name])
-  }
-
-  // Register mappings from full name to abbreviation and vice versa
-  const abbreviation = name.split('.').pop()
-  const oldName = filterAbbreviations[abbreviation]
-  if (!oldName) {
-    filterAbbreviations[name] = abbreviation
-    filterFullnames[abbreviation] = name
-  } else {
-    delete filterFullnames[abbreviation]
-    filterAbbreviations[name] = name
-    filterAbbreviations[oldName] = oldName
-  }
-
-  const data = filterData[name] || {}
-  if (agg) {
-    let aggSet, aggGet
-    if (isString(agg)) {
-      aggSet = {[name]: agg}
-      aggGet = (aggs) => (aggs[name][agg].data)
-    } else {
-      aggSet = agg.set
-      aggGet = agg.get
-    }
-    data.aggSet = aggSet
-    data.aggGet = aggGet
-  }
-  if (value) {
-    data.valueSet = value.set
-  }
-  data.multiple = multiple
-  filterData[name] = data
-}
-
-// Filters that directly correspond to a metainfo value
-registerFilter('results.material.structural_type', labelMaterial, 'terms')
-registerFilter('results.material.functional_type', labelMaterial, 'terms')
-registerFilter('results.material.compound_type', labelMaterial, 'terms')
-registerFilter('results.material.material_name', labelMaterial)
-registerFilter('results.material.chemical_formula_hill', labelElements)
-registerFilter('results.material.chemical_formula_anonymous', labelElements)
-registerFilter('results.material.n_elements', labelElements, 'min_max', undefined, false)
-registerFilter('results.material.symmetry.bravais_lattice', labelSymmetry, 'terms')
-registerFilter('results.material.symmetry.crystal_system', labelSymmetry, 'terms')
-registerFilter('results.material.symmetry.structure_name', labelSymmetry, 'terms')
-registerFilter('results.material.symmetry.strukturbericht_designation', labelSymmetry, 'terms')
-registerFilter('results.material.symmetry.space_group_symbol', labelSymmetry)
-registerFilter('results.material.symmetry.point_group', labelSymmetry)
-registerFilter('results.material.symmetry.hall_symbol', labelSymmetry)
-registerFilter('results.material.symmetry.prototype_aflow_id', labelSymmetry)
-registerFilter('results.method.method_name', labelMethod, 'terms')
-registerFilter('results.method.simulation.program_name', labelMethod, 'terms')
-registerFilter('results.method.simulation.program_version', labelMethod)
-registerFilter('results.method.simulation.dft.basis_set_type', labelDFT, 'terms')
-registerFilter('results.method.simulation.dft.core_electron_treatment', labelDFT, 'terms')
-registerFilter('results.method.simulation.dft.xc_functional_type', labelDFT, 'terms')
-registerFilter('results.method.simulation.dft.relativity_method', labelDFT, 'terms')
-registerFilter('results.method.simulation.gw.gw_type', labelGW, 'terms')
-registerFilter('results.properties.electronic.band_structure_electronic.channel_info.band_gap_type', labelElectronic, 'terms')
-registerFilter('results.properties.electronic.band_structure_electronic.channel_info.band_gap', labelElectronic, 'min_max', undefined, false)
-registerFilter('external_db', labelAuthor, 'terms')
-registerFilter('authors.name', labelAuthor)
-registerFilter('upload_time', labelAuthor, 'min_max', undefined, false)
-registerFilter('datasets.name', labelDataset)
-registerFilter('datasets.doi', labelDataset)
-registerFilter('entry_id', labelIDs)
-registerFilter('upload_id', labelIDs)
-registerFilter('results.material.material_id', labelIDs)
-registerFilter('datasets.dataset_id', labelIDs)
-
-// In exclusive element query the elements names are sorted and concatenated
-// into a single string.
-registerFilter(
-  'results.material.elements',
-  labelElements,
-  'terms',
-  {
-    set: (newQuery, oldQuery, value) => {
-      if (oldQuery.exclusive) {
-        if (value.size !== 0) {
-          newQuery['results.material.elements_exclusive'] = setToArray(value).sort().join(' ')
-        }
-      } else {
-        newQuery['results.material.elements'] = value
-      }
-    }
-  }
-)
-// Electronic properties: subset of results.properties.available_properties
-registerFilter(
-  'electronic_properties',
-  labelElectronic,
-  {
-    set: {'results.properties.available_properties': 'terms'},
-    get: (aggs) => (aggs['results.properties.available_properties'].terms.data)
-  },
-  {
-    set: (newQuery, oldQuery, value) => {
-      const data = newQuery['results.properties.available_properties'] || new Set()
-      value.forEach((item) => { data.add(item) })
-      newQuery['results.properties.available_properties'] = data
-    },
-    get: (data) => (data.results.properties.available_properties)
-  }
-)
-// Vibrational properties: subset of results.properties.available_properties
-registerFilter(
-  'vibrational_properties',
-  labelVibrational,
-  {
-    set: {'results.properties.available_properties': 'terms'},
-    get: (aggs) => (aggs['results.properties.available_properties'].terms.data)
-  },
-  {
-    set: (newQuery, oldQuery, value) => {
-      const data = newQuery['results.properties.available_properties'] || new Set()
-      value.forEach((item) => { data.add(item) })
-      newQuery['results.properties.available_properties'] = data
-    },
-    get: (data) => (data.results.properties.available_properties)
-  }
-)
-// Visibility: controls the 'owner'-parameter in the API query, not part of the
-// query itself.
-registerFilter(
-  'visibility',
-  labelAccess,
-  undefined,
-  {set: () => {}},
-  false
-)
-// Restricted: controls whether materials search is done in a restricted mode.
-registerFilter(
-  'restricted',
-  undefined,
-  undefined,
-  {set: () => {}},
-  false
-)
-// Exclusive: controls the way elements search is done.
-registerFilter(
-  'exclusive',
-  undefined,
-  undefined,
-  {set: () => {}},
-  false
-)
-
-// Material and entry queries target slightly different fields. Here we prebuild
-// the mapping.
-const materialNames = {} // Mapping of field name from entry -> material
-const entryNames = {} // Mapping of field name from material -> entry
-for (const name of Object.keys(searchQuantities)) {
-  const prefix = 'results.material.'
-  let materialName
-  if (name.startsWith(prefix)) {
-    materialName = name.substring(prefix.length)
-  } else {
-    materialName = `entries.${name}`
-  }
-  materialNames[name] = materialName
-  entryNames[materialName] = name
-}
-
-export const searchContext = React.createContext()
-export const SearchContext = React.memo(({
-  resource,
-  filtersLocked,
-  children
-}) => {
-  const setQuery = useSetRecoilState(queryState)
-  const setLocked = useSetRecoilState(lockedState)
-  const {api} = useApi()
-  const setInitialAggs = useSetRecoilState(initialAggsState)
-
-  // Reset the query/locks when entering the search context for the first time
-  const reset = useRecoilCallback(({reset}) => () => {
-    for (let filter of filters) {
-      reset(queryFamily(filter))
-      reset(lockedFamily(filter))
-    }
-  }, [])
-
-  useEffect(() => {
-    reset()
-  }, [reset])
-
-  // Read the initial query from the URL
-  const query = useMemo(() => {
-    const location = window.location.href
-    const split = location.split('?')
-    let qs, query
-    if (split.length === 1) {
-      query = {}
-    } else {
-      qs = split.pop()
-      query = qsToQuery(qs)
-    }
-    return query
-  }, [])
-
-  // Save the initial query and locked filters. Cannot be done inside useMemo
-  // due to bad setState.
-  useEffect(() => {
-    setQuery(query)
-    // Transform the locked values into a GUI-suitable format and store them
-    if (filtersLocked) {
-      const filtersLockedGUI = {}
-      for (const [key, value] of Object.entries(filtersLocked)) {
-        filtersLockedGUI[key] = toGUIFilter(key, value)
-      }
-      setLocked(filtersLockedGUI)
-    }
-  }, [setLocked, setQuery, query, filtersLocked])
-
-  // Fetch initial aggregation data.
-  useEffect(() => {
-    const aggRequest = {}
-    const aggNames = [...filters].filter(name => filterData[name].aggGet)
-    for (const filter of aggNames) {
-      toAPIAgg(aggRequest, filter, resource)
-    }
-
-    const search = {
-      owner: 'visible',
-      query: {},
-      aggregations: aggRequest,
-      pagination: {page_size: 0}
-    }
-
-    api.query(resource, search, false)
-      .then(data => {
-        data = toGUIAgg(data.aggregations, aggNames, resource)
-        setInitialAggs(data)
-      })
-  }, [api, setInitialAggs, resource])
-
-  const values = useMemo(() => ({
-    resource: resource
-  }), [resource])
-
-  return <searchContext.Provider value={values}>
-    {children}
-  </searchContext.Provider>
-})
-SearchContext.propTypes = {
-  resource: PropTypes.string,
-  filtersLocked: PropTypes.object,
-  children: PropTypes.node
-}
-
-export function useSearchContext() {
-  return useContext(searchContext)
-}
-
-/**
- * Each search filter is here mapped into a separate Recoil.js Atom. This
- * allows components to hook into individual search parameters (both for setting
- * and reading their value). This performs much better than having one large
- * Atom for the entire query, as this would cause all of the hooked components
- * to render even if they are not affected by some other search filter.
- * Re-renders became problematic with large and complex components (e.g. the
- * periodic table), for which the re-rendering takes significant time. Another
- * approach would have been to try and Memoize each sufficiently complex
- * component, but this quickly becomes a hard manual task.
- */
-export const queryFamily = atomFamily({
-  key: 'queryFamily',
-  default: undefined
-})
-export const lockedFamily = atomFamily({
-  key: 'lockedFamily',
-  default: false
-})
-
-// Menu open state
-export const menuOpen = atom({
-  key: 'isMenuOpen',
-  default: false
-})
-export function useMenuOpenState() {
-  return useRecoilState(menuOpen)
-}
-export function useSetMenuOpen() {
-  return useSetRecoilState(menuOpen)
-}
-
-// Current menu path
-export const menuPath = atom({
-  key: 'menuPath',
-  default: 'Filters'
-})
-export function useMenuPathState() {
-  return useRecoilState(menuPath)
-}
-export function useMenuPath() {
-  return useRecoilValue(menuPath)
-}
-export function useSetMenuPath() {
-  return useSetRecoilState(menuPath)
-}
-
-// Whether the search is initialized.
-export const initializedState = atom({
-  key: 'initialized',
-  default: false
-})
-
-/**
- * Returns a function that can be called to reset all current filters.
- *
- * @returns Function for resetting all filters.
- */
-export function useResetFilters() {
-  const locked = useRecoilValue(lockedState)
-  const reset = useRecoilCallback(({reset}) => () => {
-    for (let filter of filters) {
-      if (!locked[filter]) {
-        reset(queryFamily(filter))
-      }
-    }
-  }, [locked])
-  return reset
-}
-
-/**
- * This hook will expose a function for reading if the given filter is locked.
- *
- * @param {string} name Name of the filter.
- * @returns Whether the filter is locked or not.
- */
-export function useFilterLocked(name) {
-  return useRecoilValue(lockedFamily(name))
-}
-
-/**
- * This hook will expose a function for reading the locked status of all
- * filters.
- *
- * @returns An object containing a mapping from filter name to a boolean
- * indicating whether it is locked or not.
- */
-export function useFiltersLocked() {
-  return useRecoilValue(lockedState)
-}
-
-/**
- * This hook will expose a function for reading if the given set of filters are
- * locked.
- *
- * @param {string} names Names of the filters.
- * @returns Array containing the filter values in a map and a setter function.
- */
-let indexLocked = 0
-export function useFiltersLockedState(names) {
-  // We dynamically create a Recoil.js selector that is subscribed to the
-  // filters specified in the input. This way only the specified filters will
-  // cause a render. Recoil.js requires that each selector/atom has an unique
-  // id. Because this hook can be called dynamically, we simply generate the ID
-  // sequentially.
-  const filterState = useMemo(() => {
-    const id = `locked_selector${indexLocked}`
-    indexLocked += 1
-    return selector({
-      key: id,
-      get: ({get}) => {
-        const query = {}
-        for (let key of names) {
-          const filter = get(lockedFamily(key))
-          query[key] = filter
-        }
-        return query
-      }
-    })
-  // eslint-disable-next-line react-hooks/exhaustive-deps
-  }, [])
-
-  return useRecoilValue(filterState)
-}
-
-// Used to set the locked state of several filters at once
-const lockedState = selector({
-  key: 'lockedState',
-  get: ({get}) => {
-    const locks = {}
-    for (let key of filters) {
-      const filter = get(lockedFamily(key))
-      locks[key] = filter
-    }
-    return locks
-  },
-  set: ({ get, set, reset }, data) => {
-    if (data) {
-      for (const [key, value] of Object.entries(data)) {
-        set(queryFamily(key), value)
-        set(lockedFamily(key), true)
-      }
-    }
-  }
-})
-
-/**
- * This hook will expose a function for reading filter values. Use this hook if
- * you intend to only view the filter values and are not interested in setting
- * the filter.
- *
- * @param {string} name Name of the filter.
- * @returns currently set filter value.
- */
-export function useFilterValue(name) {
-  return useRecoilValue(queryFamily(name))
-}
-
-/**
- * This hook will expose a function for setting a filter value. Use this hook if
- * you intend to only set the filter value and are not interested in the query
- * results.
- *
- * @param {string} name Name of the quantity to set.
- * @returns function for setting the value for the given quantity
- */
-export function useSetFilter(name) {
-  return useSetRecoilState(queryFamily(name))
-}
-
-/**
- * This hook will expose a function for getting and setting filter values. Use
- * this hook if you intend to both read and write the filter value.
- *
- * @param {string} name Name of the filter.
- * @returns Array containing the filter value and setter function for it.
- */
-export function useFilterState(name) {
-  return useRecoilState(queryFamily(name))
-}
-
-/**
- * This hook will expose a function for setting the values of all filters.
- *
- * @returns An object containing a mapping from filter name to a boolean
- * indicating whether it is locked or not.
- */
-export function useSetFilters() {
-  return useSetRecoilState(filtersState)
-}
-
-// Used to get/set the locked state of all filters at once
-const filtersState = selector({
-  key: 'filtersState',
-  get: ({get}) => {
-    const query = {}
-    for (let key of filters) {
-      const filter = get(queryFamily(key))
-      query[key] = filter
-    }
-    return query
-  },
-  set: ({set}, [key, value]) => {
-    set(queryFamily(key), value)
-  }
-})
-
-/**
- * This hook will expose a function for getting and setting filter values for
- * the specified list of filters. Use this hook if you intend to both read and
- * write the filter values.
- *
- * @param {string} names Names of the filters.
- * @returns Array containing the filter values in a map and a setter function.
- */
-let indexFilters = 0
-export function useFiltersState(names) {
-  // We dynamically create a Recoil.js selector that is subscribed to the
-  // filters specified in the input. This way only the specified filters will
-  // cause a render. Recoil.js requires that each selector/atom has an unique
-  // id. Because this hook can be called dynamically, we simply generate the ID
-  // sequentially.
-  const filterState = useMemo(() => {
-    const id = `dynamic_selector${indexFilters}`
-    indexFilters += 1
-    return selector({
-      key: id,
-      get: ({get}) => {
-        const query = {}
-        for (let key of names) {
-          const filter = get(queryFamily(key))
-          query[key] = filter
-        }
-        return query
-      },
-      set: ({set}, [key, value]) => {
-        set(queryFamily(key), value)
-      }
-    })
-  // eslint-disable-next-line react-hooks/exhaustive-deps
-  }, [])
-
-  return useRecoilState(filterState)
-}
-
-/**
- * This Recoil.js selector aggregates all the currently set filters into a
- * single query object used by the API.
- */
-const queryState = selector({
-  key: 'query',
-  get: ({get}) => {
-    if (!get(initializedState)) {
-      return undefined
-    }
-    let query = {}
-    for (let key of filters) {
-      const filter = get(queryFamily(key))
-      if (filter !== undefined) {
-        query[key] = filter
-      }
-    }
-    return query
-  },
-  set: ({ get, set, reset }, data) => {
-    for (let filter of filters) {
-      reset(queryFamily(filter))
-    }
-    if (data) {
-      for (const [key, value] of Object.entries(data)) {
-        set(queryFamily(key), value)
-      }
-      set(initializedState, true)
-    } else {
-      set(initializedState, false)
-    }
-  }
-})
-
-export function useQuery() {
-  return useRecoilValue(queryState)
-}
-
-/**
- * Hook for writing a query object to the query string.
- *
- * @returns {object} Object containing the search object.
- */
-export function useUpdateQueryString() {
-  const history = useHistory()
-
-  const updateQueryString = useCallback((query, locked) => {
-    const queryString = queryToQs(query, locked)
-    history.replace(history.location.pathname + '?' + queryString)
-  }, [history])
-
-  return updateQueryString
-}
-
-/**
- * Converts a query string into a valid query object.
- *
- * @param {string} queryString URL querystring, encoded or not.
- * @returns Returns an object containing the filters. Values are converted into
- * datatypes that are directly compatible with the filter components.
- */
-function qsToQuery(queryString) {
-  const query = qs.parse(queryString, {comma: true})
-  const newQuery = {}
-  for (let [key, value] of Object.entries(query)) {
-    const split = key.split(':')
-    key = split[0]
-    let newKey = filterFullnames[key] || key
-    const valueGUI = toGUIFilter(newKey, value)
-    if (split.length !== 1) {
-      const op = split[1]
-      const oldValue = newQuery[newKey]
-      if (!oldValue) {
-        newQuery[newKey] = {[op]: valueGUI}
-      } else {
-        newQuery[newKey][op] = valueGUI
-      }
-    } else {
-      newQuery[newKey] = valueGUI
-    }
-  }
-  return newQuery
-}
-
-/**
- * Converts a query into a valid query string.
- * @param {object} query A query object representing the currently active
- * filters.
- * @returns URL querystring, not encoded if possible to improve readability.
- */
-function queryToQs(query, locked) {
-  const newQuery = {}
-  for (const [key, value] of Object.entries(query)) {
-    if (locked[key]) {
-      continue
-    }
-    const {formatter} = formatMeta(key, false)
-    let newValue
-    const newKey = filterAbbreviations[key]
-    if (isPlainObject(value)) {
-      if (!isNil(value.gte)) {
-        newQuery[`${newKey}:gte`] = formatter(value.gte)
-      }
-      if (!isNil(value.lte)) {
-        newQuery[`${newKey}:lte`] = formatter(value.lte)
-      }
-    } else {
-      if (isArray(value)) {
-        newValue = value.map(formatter)
-      } else if (value instanceof Set) {
-        newValue = [...value].map(formatter)
-      } else {
-        newValue = formatter(value)
-      }
-      newQuery[newKey] = newValue
-    }
-  }
-  return qs.stringify(newQuery, {indices: false, encode: false})
-}
-
-export const initialAggsState = atom({
-  key: 'initialAggs',
-  default: undefined
-})
-
-/**
- * Hook for returning an initial aggregation value for a filter.
- *
- * @returns {array} Array containing the aggregation data.
- */
-export function useInitialAgg(name) {
-  const aggs = useRecoilValue(initialAggsState)
-  return aggs?.[name]
-}
-
-/**
- * Hook for retrieving the most up-to-date aggregation results for a specific
- * filter, taking into account the current search context.
- *
- * @param {string} name The filter name
- * @param {bool} restrict If true, the ES query targeting this particular filter
- * will be removed. This makes it possible to return all possible values for
- * dropdowns etc.
- * @param {bool} update Whether the hook needs to react to changes in the
- * current query context. E.g. if the component showing the data is not visible,
- * this can be set to false.
- *
- * @returns {array} The data-array returned by the API.
- */
-export function useAgg(name, restrict = false, update = true, delay = 500) {
-  const {api} = useApi()
-  const { resource } = useSearchContext()
-  const [results, setResults] = useState(undefined)
-  const initialAggs = useRecoilValue(initialAggsState)
-  const query = useQuery()
-  const firstLoad = useRef(true)
-
-  // Pretty much all of the required pre-processing etc. should be done in this
-  // function, as it is the final one that gets called after the debounce
-  // interval.
-  const apiCall = useCallback((query) => {
-    // If the restrict option is enabled, the filters targeting the specified
-    // quantity will be removed. This way all possible options pre-selection can
-    // be returned.
-    let queryCleaned = {...query}
-    if (restrict && query && name in query) {
-      delete queryCleaned[name]
-    }
-    queryCleaned = toAPIQuery(queryCleaned, resource, query.restricted)
-    const aggRequest = {}
-    toAPIAgg(aggRequest, name, resource)
-    const search = {
-      owner: query.visibility || 'visible',
-      query: queryCleaned,
-      aggregations: aggRequest,
-      pagination: {page_size: 0},
-      required: { include: [] }
-    }
-
-    api.query(resource, search, false)
-      .then(data => {
-        data = toGUIAgg(data.aggregations, [name], resource)
-        firstLoad.current = false
-        setResults(data[name])
-      })
-  }, [api, name, restrict, resource])
-
-  // This is a debounced version of apiCall.
-  const debounced = useCallback(debounce(apiCall, delay), [])
-
-  // The API call is made immediately on first render. On subsequent renders it
-  // will be debounced.
-  useEffect(() => {
-    if (!update || query === undefined) {
-      return
-    }
-    if (firstLoad.current) {
-      // Fetch the initial aggregation values if no query
-      // is specified.
-      if (isEmpty(query)) {
-        setResults(initialAggs[name])
-      // Make an immediate request for the aggregation values if query has been
-      // specified.
-      } else {
-        apiCall(query)
-      }
-    } else {
-      debounced(query)
-    }
-  }, [apiCall, name, debounced, query, update, initialAggs])
-
-  return results
-}
-
-/**
- * Hook for returning a set of results based on the currently set query together
- * with a function for retrieving a new set of results.
- *
- * @param {int} pageSize The number of results to return with one scroll.
- * @param {string} orderBy The field used for sorting.
- * @param {string} order Ascending or descending order.
- * @param {number} delay The debounce delay in milliseconds.
- *
- * @returns {object} Object containing the search results and a function for
- * scrolling to next set of results.
- */
-export function useScrollResults(pageSize, orderBy, order, delay = 500) {
-  const {api} = useApi()
-  const {resource} = useSearchContext()
-  const firstRender = useRef(true)
-  const [results, setResults] = useState()
-  const pageNumber = useRef(1)
-  const query = useQuery(true)
-  const locked = useRecoilValue(lockedState)
-  const updateQueryString = useUpdateQueryString()
-  const pageAfterValue = useRef()
-  const searchRef = useRef()
-  const loading = useRef(false)
-  const total = useRef(0)
-
-  // The results are fetched as a side effect in order to not block the
-  // rendering. This causes two renders: first one without the data, the second
-  // one with the data.
-  const apiCall = useCallback((query, locked, pageSize, orderBy, order) => {
-    pageAfterValue.current = undefined
-    const restricted = query.restricted
-    const cleanedQuery = toAPIQuery(query, resource, restricted)
-    const search = {
-      owner: query.visibility || 'visible',
-      query: cleanedQuery,
-      pagination: {
-        page_size: pageSize,
-        order_by: orderBy,
-        order: order,
-        page_after_value: pageAfterValue.current
-      }
-    }
-    searchRef.current = search
-
-    loading.current = true
-    api.query(resource, search)
-      .then(data => {
-        pageAfterValue.current = data.pagination.next_page_after_value
-        total.current = data.pagination.total
-        setResults(data)
-        loading.current = false
-      })
-
-    // We only update the query string after the API call is finished. Updating
-    // the query string causes quite an intensive render (not sure why), so it
-    // is better to debounce this value as well to keep the user interaction
-    // smoother.
-    updateQueryString(query, locked)
-  }, [resource, api, updateQueryString])
-
-  // This is a debounced version of apiCall.
-  const debounced = useCallback(debounce(apiCall, delay), [])
-
-  // Used to load the next bath of results
-  const next = useCallback(() => {
-    if (loading.current) {
-      return
-    }
-    pageNumber.current += 1
-    searchRef.current.pagination.page_after_value = pageAfterValue.current
-    loading.current = true
-    api.query(resource, searchRef.current)
-      .then(data => {
-        pageAfterValue.current = data.pagination.next_page_after_value
-        total.current = data.pagination.total
-        setResults(old => {
-          data.data = old.data.concat(data.data)
-          return data
-        })
-        loading.current = false
-      })
-  }, [api, resource])
-
-  // Whenever the query changes, we make a new query that resets pagination and
-  // shows the first batch of results.
-  useEffect(() => {
-    // If the initial query is not yet ready, do nothing
-    if (query === undefined) {
-      return
-    }
-    if (firstRender.current) {
-      apiCall(query, locked, pageSize, orderBy, order)
-      firstRender.current = false
-    } else {
-      debounced(query, locked, pageSize, orderBy, order)
-    }
-  }, [apiCall, debounced, query, locked, pageSize, order, orderBy])
-
-  // Whenever the ordering changes, we perform a single API call that fetches
-  // results in the new order. The amount of fetched results is based on the
-  // already loaded amount.
-  // TODO
-  return {
-    results: results,
-    next: next,
-    page: pageNumber.current,
-    total: total.current
-  }
-}
-
-/**
- * Converts the contents a query into a format that is suitable for the API.
- *
- * Should only be called when making the final API call, as during the
- * construction of the query it is much more convenient to store filters within
- * e.g. Sets.
- *
- * @param {number} query The query object
- * @param {bool} exclusive The chemical element search mode.
- *
- * @returns {object} A copy of the object with certain items cleaned into a
- * format that is supported by the API.
- */
-export function toAPIQuery(query, resource, restricted) {
-  // Perform custom transformations
-  let queryCustomized = {}
-  for (let [k, v] of Object.entries(query)) {
-    const setter = filterData[k]?.valueSet
-    if (setter) {
-      setter(queryCustomized, query, v)
-    } else {
-      queryCustomized[k] = v
-    }
-  }
-
-  let queryNormalized = {}
-  for (const [k, v] of Object.entries(queryCustomized)) {
-    // Transform sets into lists and Quantities into SI values and modify keys
-    // according to target resource (entries/materials).
-    let newValue
-    if (isPlainObject(v)) {
-      newValue = {}
-      if (!isNil(v.lte)) {
-        newValue.lte = toAPIQueryValue(v.lte)
-      }
-      if (!isNil(v.gte)) {
-        newValue.gte = toAPIQueryValue(v.gte)
-      }
-    } else {
-      newValue = toAPIQueryValue(v)
-    }
-
-    // The postfixes are added here. By default query items with array values
-    // get the 'any'-postfix.
-    let postfix
-    if (isArray(newValue)) {
-      const fieldPostfixMap = {
-        'results.properties.available_properties': 'all',
-        'results.material.elements': 'all'
-      }
-      postfix = fieldPostfixMap[k] || 'any'
-    }
-
-    // For material query the keys are remapped.
-    let newKey = resource === 'materials' ? materialNames[k] : k
-    newKey = postfix ? `${newKey}:${postfix}` : newKey
-    queryNormalized[newKey] = newValue
-  }
-
-  if (resource === 'materials') {
-    // In restricted search we simply move all method/properties filters
-    // inside a single entries-subsection.
-    if (restricted) {
-      const entrySearch = {}
-      for (const [k, v] of Object.entries(queryNormalized)) {
-        if (k.startsWith('entries.')) {
-          const name = k.split('entries.').pop()
-          entrySearch[name] = v
-          delete queryNormalized[k]
-        }
-      }
-      if (!isEmpty(entrySearch)) {
-        queryNormalized.entries = entrySearch
-      }
-    // In unrestricted search we have to split each filter and each filter value
-    // into it's own separate entries query. These queries are then joined with
-    // 'and'.
-    } else {
-      const entrySearch = []
-      for (const [k, v] of Object.entries(queryNormalized)) {
-        if (k.startsWith('entries.')) {
-          const newKey = k.split(':')[0]
-          if (isArray(v)) {
-            for (const item of v) {
-              entrySearch.push({[newKey]: item})
-            }
-          } else {
-            entrySearch.push({[newKey]: v})
-          }
-          delete queryNormalized[k]
-        }
-      }
-      if (entrySearch.length > 0) {
-        queryNormalized.and = entrySearch
-      }
-    }
-  }
-
-  return queryNormalized
-}
-
-/**
- * Cleans a filter value into a form that is supported by the API. This includes:
- * - Sets are transformed into Arrays
- * - Quantities are converted to SI values.
- *
- * @returns {any} The filter value in a format that is suitable for the API.
- */
-function toAPIQueryValue(value) {
-  let newValue
-  if (value instanceof Set) {
-    newValue = setToArray(value)
-    if (newValue.length === 0) {
-      newValue = undefined
-    } else {
-      newValue = newValue.map((item) => item instanceof Quantity ? item.toSI() : item)
-    }
-  } else if (value instanceof Quantity) {
-    newValue = value.toSI()
-  } else if (isArray(value)) {
-    if (value.length === 0) {
-      newValue = undefined
-    } else {
-      newValue = value.map((item) => item instanceof Quantity ? item.toSI() : item)
-    }
-  } else {
-    newValue = value
-  }
-  return newValue
-}
-
-/**
- * Cleans a filter value into a form that is supported by the GUI. This includes:
- * - Arrays are are transformed into Sets
- * - If multiple values are supported, scalar values are stored inside sets.
- * - Numerical values with units are transformed into Quantities.
- *
- * @returns {any} The filter value in a format that is suitable for the GUI.
- */
-export function toGUIFilter(name, value, units = undefined) {
-  let multiple = filterData[name].multiple
-  let newValue
-  const {parser} = parseMeta(name)
-  if (isArray(value)) {
-    newValue = new Set(value.map((v) => parser(v, units)))
-  } else if (isPlainObject(value)) {
-    newValue = {}
-    if (!isNil(value.gte)) {
-      newValue.gte = parser(value.gte, units)
-    }
-    if (!isNil(value.lte)) {
-      newValue.lte = parser(value.lte, units)
-    }
-  } else {
-    newValue = parser(value, units)
-    if (multiple) {
-      newValue = new Set([newValue])
-    }
-  }
-  return newValue
-}
-
-/**
- * Used to transform a GUI aggregation query into a form that is usable by the
- * API.
- *
- * @param {object} aggs The aggregation data in which the modifications are
- * made.
- * @param {string} filter The filter name
- * @param {string} resource The resource we are looking at: entries or materials.
- */
-function toAPIAgg(aggs, filter, resource) {
-  const aggSet = filterData[filter].aggSet
-  if (aggSet) {
-    for (const [key, type] of Object.entries(aggSet)) {
-      const name = resource === 'materials' ? materialNames[key.split(':')[0]] : key
-      const agg = aggs[name] || {}
-      agg[type] = {
-        quantity: name,
-        size: 500
-      }
-      aggs[name] = agg
-    }
-  }
-}
-
-/**
- * Used to transform an API aggregation query into a form that is usable by the
- * GUI.
- *
- * @param {object} aggs The aggregation data as returned by the API.
- * @param {array} filters The filters to take into account.
- * @param {string} resource The resource we are looking at: entries or materials.
- *
- * @returns {object} Aggregation data that is usable by the GUI.
- */
-function toGUIAgg(aggs, filters, resource) {
-  if (isEmpty(aggs)) {
-    return aggs
-  }
-  // Modify keys according to target resource (entries/materials).
-  let aggsNormalized
-  if (resource === 'materials') {
-    aggsNormalized = {}
-    for (const key of Object.keys(aggs)) {
-      const name = resource === 'materials' ? entryNames[key] : key
-      aggs[key].quantity = name
-      aggsNormalized[name] = aggs[key]
-    }
-  } else {
-    aggsNormalized = aggs
-  }
-
-  // Perform custom transformations
-  const aggsCustomized = {}
-  for (const name of filters) {
-    const aggGet = filterData[name].aggGet
-    if (aggGet) {
-      let agg
-      agg = aggGet(aggsNormalized)
-      aggsCustomized[name] = agg
-    }
-  }
-  return aggsCustomized
-}
diff --git a/gui/src/components/search/FilterSummary.js b/gui/src/components/search/FilterSummary.js
index 5152cbd269..b77b5d6f5f 100644
--- a/gui/src/components/search/FilterSummary.js
+++ b/gui/src/components/search/FilterSummary.js
@@ -22,13 +22,13 @@ import PropTypes from 'prop-types'
 import clsx from 'clsx'
 import { isNil, isPlainObject } from 'lodash'
 import FilterChip from './FilterChip'
-import { useFiltersState, useFiltersLockedState } from './FilterContext'
+import { useFiltersState, useFiltersLockedState } from './SearchContext'
 import { formatMeta } from '../../utils'
 import { useUnits } from '../../units'
 
 /**
- * Displays an interactable summary for a given subset of filters
- * (=searchQuantities).
+ * Displays a summary for the given subset of filters. Each filter value is
+ * displayed as a chip.
  */
 const useStyles = makeStyles(theme => {
   const padding = theme.spacing(2)
diff --git a/gui/src/components/search/NewSearch.js b/gui/src/components/search/NewSearch.js
deleted file mode 100644
index f80bd7a0ad..0000000000
--- a/gui/src/components/search/NewSearch.js
+++ /dev/null
@@ -1,145 +0,0 @@
-/*
- * Copyright The NOMAD Authors.
- *
- * This file is part of NOMAD. See https://nomad-lab.eu for further info.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-import React, { useState } from 'react'
-import clsx from 'clsx'
-import PropTypes from 'prop-types'
-import { makeStyles } from '@material-ui/core/styles'
-import FilterMainMenu from './menus/FilterMainMenu'
-import NewSearchBar from './NewSearchBar'
-import SearchResults from './results/SearchResults'
-import {
-  useMenuOpenState
-} from './FilterContext'
-
-const useStyles = makeStyles(theme => {
-  return {
-    root: {
-      display: 'flex',
-      height: '100%',
-      width: '100%',
-      overflow: 'hidden'
-    },
-    leftColumn: {
-      flexShrink: 0,
-      flexGrow: 0,
-      height: '100%',
-      zIndex: 2
-    },
-    leftColumnCollapsed: {
-      maxWidth: '4rem'
-    },
-    center: {
-      flex: `1 1 100%`,
-      display: 'flex',
-      flexDirection: 'column',
-      zIndex: 1,
-      paddingBottom: theme.spacing(2.5),
-      paddingLeft: theme.spacing(3),
-      paddingRight: theme.spacing(3),
-      paddingTop: theme.spacing(0.25)
-    },
-    container: {
-    },
-    resultList: {
-      flexGrow: 1,
-      minHeight: 0 // This makes sure that the flex item is not bigger than the parent.
-    },
-    spacer: {
-      flexGrow: 1
-    },
-    header: {
-      marginTop: theme.spacing(2)
-    },
-    searchBar: {
-      marginTop: theme.spacing(2),
-      display: 'flex',
-      flexGrow: 0,
-      zIndex: 1,
-      marginBottom: theme.spacing(2.0)
-    },
-    spacerBar: {
-      flex: `0 0 ${theme.spacing(3)}px`
-    },
-    nonInteractive: {
-      pointerEvents: 'none',
-      position: 'absolute',
-      left: 0,
-      right: 0,
-      bottom: 0,
-      top: 0,
-      height: '100%',
-      width: '100%'
-    },
-    shadow: {
-      backgroundColor: 'black',
-      transition: 'opacity 200ms',
-      willChange: 'opacity',
-      zIndex: 1
-    },
-    hidden: {
-      display: 'none'
-    },
-    shadowHidden: {
-      opacity: 0
-    },
-    shadowVisible: {
-      opacity: 0.1
-    },
-    placeholderVisible: {
-      display: 'block'
-    }
-  }
-})
-
-const NewSearch = React.memo(({
-  collapsed,
-  header
-}) => {
-  const styles = useStyles()
-  const [isMenuOpen, setIsMenuOpen] = useMenuOpenState(false)
-  const [isCollapsed, setIsCollapsed] = useState(collapsed)
-
-  return <div className={styles.root}>
-    <div className={clsx(styles.leftColumn, isCollapsed && styles.leftColumnCollapsed)}>
-      <FilterMainMenu
-        open={isMenuOpen}
-        onOpenChange={setIsMenuOpen}
-        collapsed={isCollapsed}
-        onCollapsedChange={setIsCollapsed}
-      />
-    </div>
-    <div className={styles.center} onClick={() => setIsMenuOpen(false)}>
-      <div className={styles.header}>
-        {header}
-      </div>
-      <NewSearchBar
-        className={styles.searchBar}
-      />
-      <SearchResults
-        className={styles.resultList}
-      />
-      <div className={clsx(styles.nonInteractive, styles.shadow, styles.shadowHidden, isMenuOpen && styles.shadowVisible)}></div>
-    </div>
-  </div>
-})
-NewSearch.propTypes = {
-  collapsed: PropTypes.bool,
-  header: PropTypes.node
-}
-
-export default NewSearch
diff --git a/gui/src/components/search/NewSearchBar.js b/gui/src/components/search/NewSearchBar.js
deleted file mode 100644
index 93dbd613ef..0000000000
--- a/gui/src/components/search/NewSearchBar.js
+++ /dev/null
@@ -1,431 +0,0 @@
-/*
- * Copyright The NOMAD Authors.
- *
- * This file is part of NOMAD. See https://nomad-lab.eu for further info.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-import React, { useCallback, useState, useMemo } from 'react'
-import PropTypes from 'prop-types'
-import clsx from 'clsx'
-import { debounce, isNil } from 'lodash'
-import Autocomplete from '@material-ui/lab/Autocomplete'
-import { makeStyles } from '@material-ui/core/styles'
-import SearchIcon from '@material-ui/icons/Search'
-import CloseIcon from '@material-ui/icons/Close'
-import {
-  TextField,
-  CircularProgress,
-  Paper,
-  Divider,
-  Tooltip,
-  Typography
-} from '@material-ui/core'
-import IconButton from '@material-ui/core/IconButton'
-import { useApi } from '../apiV1'
-import { useUnits } from '../../units'
-import { isMetaNumber, isMetaTimestamp } from '../../utils'
-import {
-  useSetFilters,
-  useFiltersLocked,
-  filterFullnames,
-  filterAbbreviations,
-  toGUIFilter,
-  filterData,
-  filters
-} from './FilterContext'
-import searchQuantities from '../../searchQuantities'
-
-const opMap = {
-  '<=': 'lte',
-  '>=': 'gte',
-  '>': 'gt',
-  '<': 'lt'
-}
-const opMapReverse = {
-  '<=': 'gte',
-  '>=': 'lte',
-  '>': 'lt',
-  '<': 'gt'
-}
-
-// Decides which options are shown
-const filterOptions = (options, {inputValue}) => {
-  const trimmed = inputValue.trim().toLowerCase()
-  return options.filter(option => {
-    // ES results do not need to be filtered at all
-    const category = option.category
-    if (category !== 'quantity name') {
-      return true
-    }
-    // Underscore can be replaced by a whitespace
-    const optionClean = option.value.trim().toLowerCase()
-    const matchUnderscore = optionClean.includes(trimmed)
-    const matchNoUnderscore = optionClean.replaceAll('_', ' ').includes(trimmed)
-    return matchUnderscore || matchNoUnderscore
-  })
-}
-
-// Customized paper component for the autocompletion options
-const CustomPaper = (props) => {
-  return <Paper elevation={3} {...props} />
-}
-
-const useStyles = makeStyles(theme => ({
-  root: {
-    display: 'flex',
-    alignItems: 'center',
-    position: 'relative'
-  },
-  notchedOutline: {
-    borderColor: 'rgba(0, 0, 0, 0.0)'
-  },
-  iconButton: {
-    padding: 10
-  },
-  divider: {
-    height: '2rem'
-  },
-  endAdornment: {
-    position: 'static'
-  },
-  examples: {
-    position: 'absolute',
-    left: 0,
-    right: 0,
-    top: 'calc(100% + 4px)',
-    padding: theme.spacing(2),
-    fontStyle: 'italic'
-  }
-}))
-
-/**
- * This component shows a searchbar with autocomplete functionality. It does its
- * on API calls to provide autocomplete suggestion options.
- */
-const NewSearchBar = React.memo(({
-  className
-}) => {
-  const styles = useStyles()
-  const units = useUnits()
-  const [suggestions, setSuggestions] = useState([])
-  const [loading, setLoading] = useState(false)
-  const [inputValue, setInputValue] = useState('')
-  const [highlighted, setHighlighted] = useState({value: ''})
-  const [open, setOpen] = useState(false)
-  const [error, setError] = useState(false)
-  const [showExamples, setShowExamples] = useState(false)
-  const {api} = useApi()
-  const filtersLocked = useFiltersLocked()
-  const setFilter = useSetFilters()
-  const quantitySet = filters
-  const quantitySuggestions = useMemo(() => {
-    const suggestions = []
-    for (let q of filters) {
-      suggestions.push({
-        value: filterAbbreviations[q] || q,
-        category: 'quantity name'
-      })
-    }
-    return suggestions
-  }, [])
-
-  // Triggered when a value is submitted by pressing enter or clicking the
-  // search icon.
-  const handleSubmit = useCallback(() => {
-    if (inputValue.trim().length === 0) {
-      return
-    }
-    const reString = '[^\\s=<>](?:[^=<>]*[^\\s=<>])?'
-    const op = '(?:<|>)=?'
-    let valid = false
-    let quantityFullname
-    let queryValue
-
-    // Equality query
-    const equals = inputValue.match(new RegExp(`^\\s*(${reString})\\s*=\\s*(${reString})\\s*$`))
-    if (equals) {
-      const quantityName = equals[1]
-      quantityFullname = filterFullnames[quantityName] || quantityName
-      if (!quantitySet.has(quantityFullname)) {
-        setError(`Unknown quantity name`)
-        return
-      }
-      try {
-        queryValue = toGUIFilter(quantityFullname, equals[2], units)
-      } catch (error) {
-        setError(`Invalid value for this metainfo. Please check your syntax.`)
-        return
-      }
-      valid = true
-    }
-
-    // Simple LTE/GTE query
-    if (!valid) {
-      const ltegte = inputValue.match(new RegExp(`^\\s*(${reString})\\s*(${op})\\s*(${reString})\\s*$`))
-      if (ltegte) {
-        const a = ltegte[1]
-        const op = ltegte[2]
-        const b = ltegte[3]
-        const aFullname = filterFullnames[a]
-        const bFullname = filterFullnames[b]
-        const isAQuantity = quantitySet.has(aFullname)
-        const isBQuantity = quantitySet.has(bFullname)
-        if (!isAQuantity && !isBQuantity) {
-          setError(`Unknown quantity name`)
-          return
-        }
-        quantityFullname = isAQuantity ? aFullname : bFullname
-        if (!isMetaNumber(quantityFullname) && !isMetaTimestamp(quantityFullname)) {
-          setError(`Cannot perform range query for a non-numeric quantity.`)
-          return
-        }
-        let quantityValue
-        try {
-          quantityValue = toGUIFilter(quantityFullname, isAQuantity ? b : a, units)
-        } catch (error) {
-          setError(`Invalid value for this metainfo. Please check your syntax.`)
-          return
-        }
-        queryValue = {}
-        queryValue[opMap[op]] = quantityValue
-        valid = true
-      }
-    }
-
-    // Sandwiched LTE/GTE query
-    if (!valid) {
-      const ltegteSandwich = inputValue.match(new RegExp(`^\\s*(${reString})\\s*(${op})\\s*(${reString})\\s*(${op})\\s*(${reString})\\s*$`))
-      if (ltegteSandwich) {
-        const a = ltegteSandwich[1]
-        const op1 = ltegteSandwich[2]
-        const b = ltegteSandwich[3]
-        const op2 = ltegteSandwich[4]
-        const c = ltegteSandwich[5]
-        quantityFullname = filterFullnames[b]
-        if (!isMetaNumber(quantityFullname) && !isMetaTimestamp(quantityFullname)) {
-          setError(`Cannot perform range query for a non-numeric quantity.`)
-          return
-        }
-        const isBQuantity = quantitySet.has(quantityFullname)
-        if (!isBQuantity) {
-          setError(`Unknown quantity name`)
-          return
-        }
-
-        queryValue = {}
-        try {
-          queryValue[opMapReverse[op1]] = toGUIFilter(quantityFullname, a, units)
-          queryValue[opMap[op2]] = toGUIFilter(quantityFullname, c, units)
-        } catch (error) {
-          setError(`Invalid value for this metainfo. Please check your syntax.`)
-          return
-        }
-        valid = true
-      }
-    }
-
-    // Check if filter is locked
-    if (filtersLocked[quantityFullname]) {
-      setError(`Cannot change the filter as it is locked in the current search context.`)
-      return
-    }
-
-    if (valid) {
-      // Submit to search context on successful validation.
-      setFilter([quantityFullname, old => {
-        const multiple = filterData[quantityFullname].multiple
-        return (isNil(old) || !multiple) ? queryValue : new Set([...old, ...queryValue])
-      }])
-      setInputValue('')
-      setOpen(false)
-    } else {
-      setError(`Invalid query`)
-    }
-  // eslint-disable-next-line react-hooks/exhaustive-deps
-  }, [inputValue, quantitySet])
-
-  // Handle clear button
-  const handleClose = useCallback(() => {
-    setInputValue('')
-    setSuggestions([])
-    setOpen(false)
-    setShowExamples(true)
-  }, [])
-
-  const handleHighlight = useCallback((event, value, reason) => {
-    setHighlighted(value)
-  }, [])
-
-  // When enter is pressed, select currently highlighted value and close menu,
-  // or if menu is not open submit the value.
-  const handleEnter = useCallback((event) => {
-    if (event.key === 'Enter') {
-      if (open && highlighted?.value) {
-        setInputValue(highlighted.value)
-        setOpen(false)
-      } else {
-        handleSubmit()
-      }
-      event.stopPropagation()
-      event.preventDefault()
-    }
-  }, [open, highlighted, handleSubmit])
-
-  const suggestionCall = useCallback((quantityList, value) => {
-    setLoading(true)
-    // If some input is given, and the quantity supports suggestions, we use
-    // input suggester to suggest values
-    const filteredList = quantityList.filter(q => searchQuantities[q]?.suggestion)
-    api.suggestions(filteredList, value)
-      .then(data => {
-        let res = []
-        for (let q of filteredList) {
-          const name = filterAbbreviations[q] || q
-          const esSuggestions = data[q]
-          if (esSuggestions) {
-            res = res.concat(esSuggestions.map(suggestion => ({
-              value: `${name}=${suggestion.value}`,
-              category: name
-            })))
-          }
-        }
-        setSuggestions(res)
-      })
-      .finally(() => setLoading(false))
-  }, [api])
-  const suggestionDebounced = useCallback(debounce(suggestionCall, 150), [])
-
-  // Handle typing events. After a debounce time has expired, a list of
-  // suggestion will be retrieved if they are available for this metainfo and
-  // the input is deemed meaningful.
-  const handleInputChange = useCallback((event, value, reason) => {
-    setError(error => error ? undefined : null)
-    setInputValue(value)
-    value = value?.trim()
-    setShowExamples(!value)
-    if (!value) {
-      setSuggestions([])
-      setOpen(false)
-      setShowExamples(true)
-      return
-    } else {
-      setOpen(true)
-      setShowExamples(false)
-    }
-    if (reason !== 'input') {
-      setSuggestions([])
-      setOpen(false)
-    }
-    // If the input is prefixed with a proper quantity name and an equals-sign,
-    // we extract the quantity name and the typed input
-    const split = value.split('=', 2)
-    let quantityList = [...filters]
-    if (split.length === 2) {
-      const quantityName = split[0].trim()
-      const quantityFullname = filterFullnames[quantityName]
-      if (quantitySet.has(quantityName)) {
-        quantityList = [quantityName]
-        value = split[1].trim()
-      } else if (quantitySet.has(quantityFullname)) {
-        quantityList = [quantityFullname]
-        value = split[1].trim()
-      }
-    }
-
-    setLoading(true)
-    // If some input is given, and the quantity supports suggestions, we use
-    // input suggester to suggest values
-    if (value.length > 0) {
-      suggestionDebounced(quantityList, value)
-    // If no input is given, we suggest Enum values, or for non-enum quantities
-    // use terms aggregation.
-    } else {
-    }
-  }, [quantitySet, suggestionDebounced])
-
-  // This determines the order: notice that items should be sorted by group
-  // first in order for the grouping to work correctly.
-  const options = useMemo(() => {
-    return suggestions.concat(quantitySuggestions)
-  }, [quantitySuggestions, suggestions])
-
-  return <Paper className={clsx(className, styles.root)}>
-    <Autocomplete
-      className={styles.input}
-      freeSolo
-      clearOnBlur={false}
-      inputValue={inputValue}
-      value={null}
-      open={open}
-      onFocus={() => setShowExamples(true)}
-      onBlur={() => setShowExamples(false)}
-      onOpen={() => { if (inputValue.trim() !== '') { setOpen(true) } }}
-      onClose={() => setOpen(false)}
-      fullWidth
-      disableClearable
-      PaperComponent={CustomPaper}
-      classes={{endAdornment: styles.endAdornment}}
-      groupBy={(option) => option.category}
-      filterOptions={filterOptions}
-      options={options}
-      onInputChange={handleInputChange}
-      onHighlightChange={handleHighlight}
-      getOptionLabel={option => option.value}
-      getOptionSelected={(option, value) => false}
-      renderInput={(params) => (
-        <TextField
-          {...params}
-          className={styles.textField}
-          variant="outlined"
-          placeholder=""
-          label={error || undefined}
-          error={!!error}
-          onKeyDown={handleEnter}
-          InputLabelProps={{ shrink: true }}
-          InputProps={{
-            ...params.InputProps,
-            classes: {
-              notchedOutline: styles.notchedOutline
-            },
-            endAdornment: (<>
-              {loading ? <CircularProgress color="inherit" size={20} /> : null}
-              {(inputValue?.length || null) && <>
-                <Tooltip title="Clear">
-                  <IconButton onClick={handleClose} className={styles.iconButton} aria-label="clear">
-                    <CloseIcon />
-                  </IconButton>
-                </Tooltip>
-                <Divider className={styles.divider} orientation="vertical"/>
-              </>}
-              <Tooltip title="Add filter">
-                <IconButton onClick={handleSubmit} className={styles.iconButton} aria-label="search">
-                  <SearchIcon />
-                </IconButton>
-              </Tooltip>
-            </>)
-          }}
-        />
-      )}
-    />
-    {showExamples && <CustomPaper className={styles.examples}>
-      <Typography>{'Start typing a query or a keyword to get relevant suggestions.'}</Typography>
-    </CustomPaper>}
-  </Paper>
-})
-
-NewSearchBar.propTypes = {
-  className: PropTypes.string
-}
-
-export default NewSearchBar
diff --git a/gui/src/components/search/QuantityHistogram.js b/gui/src/components/search/QuantityHistogram.js
deleted file mode 100644
index e47b99af57..0000000000
--- a/gui/src/components/search/QuantityHistogram.js
+++ /dev/null
@@ -1,145 +0,0 @@
-/*
- * Copyright The NOMAD Authors.
- *
- * This file is part of NOMAD. See https://nomad-lab.eu for further info.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-import React, { useContext, useMemo, useCallback } from 'react'
-import PropTypes from 'prop-types'
-import { searchContext } from './SearchContext.js'
-import searchQuantities from '../../searchQuantities'
-import Histogram from '../Histogram.js'
-
-const unprocessedLabel = 'not processed'
-const unavailableLabel = 'unavailable'
-
-export default function QuantityHistogram({
-  quantity, valueLabels = {}, title, values, numberOfValues, multiple, tooltips = {},
-  ...props
-}) {
-  title = title || quantity
-  values = values || (searchQuantities[quantity] && searchQuantities[quantity].statistic_values)
-  numberOfValues = numberOfValues || (values && values.length) || (searchQuantities[quantity] && searchQuantities[quantity].statistic_size)
-  const {response: {statistics, metric}, query, setQuery} = useContext(searchContext)
-  const statisticsData = statistics[quantity]
-
-  const handleItemClicked = useCallback(item => {
-    if (multiple) {
-      // Add or remove item from query
-      let newQuery = query[quantity]
-      if (newQuery === undefined) {
-        newQuery = [item.key]
-      } else {
-        if (!Array.isArray(newQuery)) {
-          newQuery = [newQuery]
-        }
-        newQuery = new Set(newQuery)
-        if (newQuery.has(item.key)) {
-          newQuery.delete(item.key)
-        } else {
-          newQuery.add([item.key])
-        }
-        newQuery = Array.from(newQuery.values())
-      }
-      setQuery({[quantity]: newQuery})
-    } else {
-      setQuery({[quantity]: (query[quantity] === item.key) ? null : item.key})
-    }
-  }, [query, setQuery, multiple, quantity])
-
-  const data = useMemo(() => {
-    let data
-    if (!statistics[quantity]) {
-      data = []
-    } else if (values) {
-      data = values.map(value => ({
-        key: value,
-        name: valueLabels[value] || value,
-        value: statisticsData[value] ? statisticsData[value][metric] : 0,
-        tooltip: tooltips[value]
-      }))
-    } else {
-      data = Object.keys(statisticsData)
-        .map(value => ({
-          key: value,
-          name: valueLabels[value] || value,
-          value: statisticsData[value][metric]
-        }))
-      // keep the data sorting, but put unavailable and not processed to the end
-      const unavailableIndex = data.findIndex(d => d.name === unavailableLabel)
-      const unprocessedIndex = data.findIndex(d => d.name === unprocessedLabel)
-      if (unavailableIndex !== -1) {
-        data.push(data.splice(unavailableIndex, 1)[0])
-      }
-      if (unprocessedIndex !== -1) {
-        data.push(data.splice(unprocessedIndex, 1)[0])
-      }
-    }
-    return data
-  }, [metric, quantity, statistics, statisticsData, valueLabels, values, tooltips])
-
-  return <Histogram
-    card data={data}
-    numberOfValues={numberOfValues}
-    title={title}
-    onClick={handleItemClicked}
-    selected={query[quantity]}
-    multiple={multiple}
-    tooltips={!!tooltips}
-    {...props}
-  />
-}
-QuantityHistogram.propTypes = {
-  /**
-   * The name of the search quantity that is displayed in the histogram. This has to
-   * match the provided statistics data.
-   */
-  quantity: PropTypes.string.isRequired,
-  /**
-   * An optional title for the chart. If no title is given, the quantity is used.
-   */
-  title: PropTypes.string,
-  /**
-   * The data. Usually the statistics data send by NOMAD's API.
-   */
-  data: PropTypes.object,
-  /**
-   * Optional list of possible values. This is used to sort the data and fill the data
-   * with 0-values to keep a persistent appearance, even if no data for that value exists.
-   * Otherwise, the values are not sorted.
-   */
-  values: PropTypes.arrayOf(PropTypes.string),
-  /**
-   * The maximum number of values. This is used to fix the histograms size. Otherwise,
-   * the size is determined by the required space to render the existing values.
-   */
-  numberOfValues: PropTypes.number,
-  /**
-   * An optional mapping between values and labels that should be used to render the
-   * values.
-   */
-  valueLabels: PropTypes.object,
-  /**
-   * An optional mapping between values and their tooltip content.
-   */
-  tooltips: PropTypes.object,
-  /**
-   * Whether multiple values can be appended to the same query key.
-   */
-  multiple: PropTypes.bool
-}
-
-QuantityHistogram.defaultProps = {
-  multiple: false
-}
diff --git a/gui/src/components/search/Search.js b/gui/src/components/search/Search.js
index e0627733a5..bf3859f1a4 100644
--- a/gui/src/components/search/Search.js
+++ b/gui/src/components/search/Search.js
@@ -15,629 +15,136 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import React, { useState, useContext, useEffect } from 'react'
+import React, { useState } from 'react'
+import clsx from 'clsx'
 import PropTypes from 'prop-types'
 import { makeStyles } from '@material-ui/core/styles'
-import { Card, Button, Tooltip, Tabs, Tab, Paper, FormControl,
-  FormGroup, Checkbox, FormControlLabel, CardContent, IconButton, Select, MenuItem, Box } from '@material-ui/core'
-import { useQueryParam, useQueryParams, StringParam, NumberParam } from 'use-query-params'
+import FilterMainMenu from './menus/FilterMainMenu'
 import SearchBar from './SearchBar'
-import { DisableOnLoading } from '../api'
-import { domainData } from '../domainData'
-import PeriodicTable from './input/PeriodicTable'
-import ReloadIcon from '@material-ui/icons/Cached'
-import GroupList from './results/GroupList'
-import ApiDialogButton from '../ApiDialogButton'
-import UploadsHistogram from './UploadsHistogram'
-import QuantityHistogram from './QuantityHistogram'
-import SearchContext, { searchContext, useUrlQuery } from './SearchContext'
-import {objectFilter} from '../../utils'
-import MaterialsList from './results/MaterialsList'
-import UploadList from './results/UploadsList'
-import DatasetList from './results/DatasetList'
-import EntryList from './EntryList'
-
-const resultTabs = {
-  'entries': {
-    label: 'Entries',
-    groups: {},
-    component: SearchEntryList
-  },
-  'materials': {
-    label: 'Materials',
-    groups: {'encyclopedia.material.materials_grouped': true},
-    component: SearchMaterialsList
-  },
-  'groups': {
-    label: 'Grouped entries',
-    groups: {'dft.groups_grouped': true},
-    component: SearchGroupList
-  },
-  'uploads': {
-    label: 'Uploads',
-    groups: {'uploads_grouped': true},
-    component: SearchUploadList
-  },
-  'datasets': {
-    label: 'Datasets',
-    groups: {'datasets_grouped': true},
-    component: SearchDatasetList
-  }
-}
-
-const useSearchStyles = makeStyles(theme => ({
-  root: {
-    padding: theme.spacing(3)
-  }
-}))
+import SearchResults from './results/SearchResults'
+import {
+  useMenuOpenState
+} from './SearchContext'
 
 /**
- * This component shows the full search interface including result lists.
+ * The primary search interface that is reused throughout the application in
+ * different contexts. Displays a menu of filters, a search bar, a list of
+ * results and optionally a customizable header above the search bar.
  */
-export default function Search(props) {
-  const {
-    initialVisualizationTab,
-    initialOwner,
-    ownerTypes,
-    initialDomain,
-    initialMetric,
-    initialResultTab,
-    availableResultTabs,
-    query,
-    initialQuery,
-    resultListProps,
-    initialRequest,
-    showDisclaimer,
-    ...rest} = props
-  const classes = useSearchStyles()
-  return <DisableOnLoading>
-    <SearchContext query={query} initialQuery={initialQuery}>
-      <div className={classes.root} {...rest}>
-        <SearchEntry
-          initialTab={initialVisualizationTab}
-          initialOwner={initialOwner}
-          ownerTypes={ownerTypes}
-          initialDomain={initialDomain}
-          initialMetric={initialMetric}
-          initialRequest={initialRequest}
-          showDisclaimer={showDisclaimer}
-        />
-        <SearchResults
-          initialTab={initialResultTab}
-          availableTabs={availableResultTabs}
-          resultListProps={resultListProps}
-        />
-      </div>
-    </SearchContext>
-  </DisableOnLoading>
-}
-Search.propTypes = {
-  initialResultTab: PropTypes.string,
-  initialVisualizationTab: PropTypes.string,
-  availableResultTabs: PropTypes.arrayOf(PropTypes.string),
-  initialOwner: PropTypes.string,
-  ownerTypes: PropTypes.arrayOf(PropTypes.string),
-  initialDomain: PropTypes.string,
-  initialMetric: PropTypes.string,
-  initialRequest: PropTypes.object,
-  resultListProps: PropTypes.object,
-  /**
-   * Additional search parameters that will be added to all searches that are send to
-   * the API. The idea is that this can be used to lock some aspects of the search for
-   * special contexts, like the dataset page for example.
-   */
-  query: PropTypes.object,
-  /**
-   * Similar to query, but these parameters can be changes by the user interacting with
-   * the component.
-   */
-  initialQuery: PropTypes.object,
-  showDisclaimer: PropTypes.bool
-}
-
-const useSearchEntryStyles = makeStyles(theme => ({
-  search: {
-    marginTop: theme.spacing(2),
-    marginBottom: theme.spacing(2),
-    maxWidth: 1024,
-    margin: 'auto',
-    width: '100%'
-  },
-
-  domainButton: {
-    margin: theme.spacing(1)
-  },
-  metricButton: {
-    margin: theme.spacing(1),
-    marginRight: 0
-  },
-  searchBar: {
-    marginTop: theme.spacing(1),
-    marginBottom: theme.spacing(1)
-  },
-  selectButton: {
-    margin: theme.spacing(1)
-  },
-  visualizations: {
-    display: 'block',
-    maxWidth: 900,
-    margin: 'auto',
-    marginTop: theme.spacing(2),
-    marginBottom: theme.spacing(2)
-  }
-}))
-function SearchEntry({initialTab, initialOwner, ownerTypes, initialDomain, initialMetric, showDisclaimer}) {
-  const classes = useSearchEntryStyles()
-  const [openVisualizationParam, setOpenVisualizationParam] = useQueryParam('visualization', StringParam)
-  const {domain} = useContext(searchContext)
-
-  const visualizations = {}
-  visualizations.elements = {
-    component: ElementsVisualization,
-    label: 'Elements',
-    description: 'Shows data as a heatmap over the periodic table'
-  }
-  Object.assign(visualizations, domain.searchVisualizations)
-  visualizations.users = {
-    component: UsersVisualization,
-    label: 'Uploads',
-    description: 'Show statistics about when and by whom data was uploaded'
-  }
-
-  const openVisualizationKey = openVisualizationParam || initialTab
-  const openVisualizationTab = visualizations[openVisualizationKey]
-
-  const VisualizationComponent = openVisualizationTab ? openVisualizationTab.component : React.Fragment
-
-  const handleVisualizationChange = value => {
-    if (value === openVisualizationKey) {
-      setOpenVisualizationParam('none')
-    } else {
-      setOpenVisualizationParam(value)
+const useStyles = makeStyles(theme => {
+  return {
+    root: {
+      display: 'flex',
+      height: '100%',
+      width: '100%',
+      overflow: 'hidden'
+    },
+    leftColumn: {
+      flexShrink: 0,
+      flexGrow: 0,
+      height: '100%',
+      zIndex: 2
+    },
+    leftColumnCollapsed: {
+      maxWidth: '4rem'
+    },
+    center: {
+      flex: `1 1 100%`,
+      display: 'flex',
+      flexDirection: 'column',
+      zIndex: 1,
+      paddingBottom: theme.spacing(2.5),
+      paddingLeft: theme.spacing(3),
+      paddingRight: theme.spacing(3),
+      paddingTop: theme.spacing(0.25)
+    },
+    container: {
+    },
+    resultList: {
+      flexGrow: 1,
+      minHeight: 0 // This makes sure that the flex item is not bigger than the parent.
+    },
+    spacer: {
+      flexGrow: 1
+    },
+    header: {
+      marginTop: theme.spacing(2)
+    },
+    searchBar: {
+      marginTop: theme.spacing(2),
+      display: 'flex',
+      flexGrow: 0,
+      zIndex: 1,
+      marginBottom: theme.spacing(2.0)
+    },
+    spacerBar: {
+      flex: `0 0 ${theme.spacing(3)}px`
+    },
+    nonInteractive: {
+      pointerEvents: 'none',
+      position: 'absolute',
+      left: 0,
+      right: 0,
+      bottom: 0,
+      top: 0,
+      height: '100%',
+      width: '100%'
+    },
+    shadow: {
+      backgroundColor: 'black',
+      transition: 'opacity 200ms',
+      willChange: 'opacity',
+      zIndex: 1
+    },
+    hidden: {
+      display: 'none'
+    },
+    shadowHidden: {
+      opacity: 0
+    },
+    shadowVisible: {
+      opacity: 0.1
+    },
+    placeholderVisible: {
+      display: 'block'
     }
   }
-
-  return <div>
-    <div className={classes.search}>
-      {domain.disclaimer && showDisclaimer && <Box marginBottom={2} fontStyle="italic">
-        {domain.disclaimer}
-      </Box>}
-      <FormGroup row style={{alignItems: 'center'}}>
-        <Box marginRight={2}>
-          <DomainSelect classes={{root: classes.domainButton}} initialDomain={initialDomain} />
-        </Box>
-        <div style={{flexGrow: 1}} />
-        <OwnerSelect ownerTypes={ownerTypes} initialOwner={initialOwner}/>
-        <div style={{flexGrow: 1}} />
-        <VisualizationSelect
-          classes={{button: classes.selectButton}}
-          value={openVisualizationKey}
-          onChange={handleVisualizationChange}
-          visualizations={visualizations}
-        />
-        <Box marginLeft={2}>
-          <MetricSelect classes={{root: classes.metricButton}} initialMetric={initialMetric}/>
-        </Box>
-      </FormGroup>
-
-      {/* <SearchBar classes={{autosuggestRoot: classes.searchBar}} /> */}
-      <div className={classes.searchBar}>
-        <SearchBar />
-      </div>
-    </div>
-
-    <div className={classes.visualizations}>
-      <VisualizationComponent/>
+})
+
+const Search = React.memo(({
+  collapsed,
+  header
+}) => {
+  const styles = useStyles()
+  const [isMenuOpen, setIsMenuOpen] = useMenuOpenState(false)
+  const [isCollapsed, setIsCollapsed] = useState(collapsed)
+
+  return <div className={styles.root}>
+    <div className={clsx(styles.leftColumn, isCollapsed && styles.leftColumnCollapsed)}>
+      <FilterMainMenu
+        open={isMenuOpen}
+        onOpenChange={setIsMenuOpen}
+        collapsed={isCollapsed}
+        onCollapsedChange={setIsCollapsed}
+      />
     </div>
-  </div>
-}
-SearchEntry.propTypes = {
-  initialTab: PropTypes.string,
-  initialOwner: PropTypes.string,
-  initialDomain: PropTypes.string,
-  initialMetric: PropTypes.string,
-  ownerTypes: PropTypes.arrayOf(PropTypes.string),
-  showDisclaimer: PropTypes.bool
-}
-
-const originLabels = {
-  'Stefano Curtarolo': 'AFLOW',
-  'Chris Wolverton': 'OQMD',
-  'Patrick Huck': 'Materials Project',
-  'Markus Scheidgen': 'NOMAD Laboratory'
-}
-
-function UsersVisualization() {
-  const {setStatistics} = useContext(searchContext)
-  useEffect(() => {
-    setStatistics(['origin'])
-    // eslint-disable-next-line
-  }, [])
-  return <div>
-    <UploadsHistogram tooltips initialScale={0.5} />
-    <QuantityHistogram quantity="origin" title="Uploader/origin" valueLabels={originLabels}/>
-  </div>
-}
-
-function ElementsVisualization(props) {
-  const [exclusive, setExclusive] = useState(false)
-  const {response: {statistics, metric}, query, setQuery, setStatistics} = useContext(searchContext)
-  useEffect(() => {
-    setStatistics(['atoms'])
-    // eslint-disable-next-line
-  }, [])
-
-  const handleExclusiveChanged = () => {
-    if (!exclusive) {
-      setQuery({only_atoms: query.atoms, atoms: []})
-    } else {
-      setQuery({atoms: query.only_atoms, only_atoms: []})
-    }
-    setExclusive(!exclusive)
-  }
-  const handleAtomsChanged = atoms => {
-    if (exclusive) {
-      setExclusive(false)
-    }
-    setQuery({atoms: atoms, only_atoms: []})
-  }
-
-  return <Card>
-    <CardContent>
-      <PeriodicTable
-        aggregations={statistics.atoms}
-        metric={metric}
-        exclusive={exclusive}
-        values={[...(query.atoms || []), ...(query.only_atoms || [])]}
-        onChanged={handleAtomsChanged}
-        onExclusiveChanged={handleExclusiveChanged}
+    <div className={styles.center} onClick={() => setIsMenuOpen(false)}>
+      <div className={styles.header}>
+        {header}
+      </div>
+      <SearchBar
+        className={styles.searchBar}
       />
-    </CardContent>
-  </Card>
-}
-
-const useMetricSelectStyles = makeStyles(theme => ({
-  root: {
-    minWidth: 100
-  }
-}))
-function MetricSelect({initialMetric}) {
-  const {domain, setMetric} = useContext(searchContext)
-  const [metricParam, setMetricParam] = useQueryParam('metric', StringParam)
-  const metric = metricParam || initialMetric || domain.defaultSearchMetric
-
-  useEffect(() => setMetric(metric), [metric, setMetric])
-
-  const metricsDefinitions = domain.searchMetrics
-  const classes = useMetricSelectStyles()
-  const [tooltipOpen, setTooltipOpen] = useState(false)
-  const handleTooltip = bool => setTooltipOpen(bool)
-  return <FormControl className={classes.root}>
-    <Tooltip title="Select the metric used to represent data" open={tooltipOpen}>
-      <Select
-        MenuProps={{
-          getContentAnchorEl: null,
-          anchorOrigin: {
-            vertical: 'bottom',
-            horizontal: 'left'
-          }
-        }}
-        renderValue={key => {
-          const metric = metricsDefinitions[key]
-          return metric.shortLabel || metric.label
-        }}
-        value={metric}
-        onChange={event => setMetricParam(event.target.value)}
-        onMouseEnter={() => handleTooltip(true)}
-        onMouseLeave={() => handleTooltip(false)}
-        onOpen={() => handleTooltip(false)}
-      >
-        {Object.keys(metricsDefinitions).map(metricKey => {
-          const {label, tooltip} = metricsDefinitions[metricKey]
-          return (
-            <MenuItem value={metricKey} key={metricKey}>
-              <Tooltip title={tooltip || ''}>
-                <div>{label}</div>
-              </Tooltip>
-            </MenuItem>
-          )
-        })}
-      </Select>
-    </Tooltip>
-  </FormControl>
-}
-MetricSelect.propTypes = {
-  initialMetric: PropTypes.string
-}
-
-function VisualizationSelect({classes, value, onChange, visualizations}) {
-  return <React.Fragment>
-    {Object.keys(visualizations).map(key => {
-      const visualization = visualizations[key]
-      return <Tooltip key={key} title={visualization.description}>
-        <Button
-          variant="outlined"
-          size="small" className={classes.button}
-          color={value === key ? 'primary' : 'default'}
-          onClick={() => onChange(key)}
-        >
-          {visualization.label}
-        </Button>
-      </Tooltip>
-    })}
-  </React.Fragment>
-}
-VisualizationSelect.propTypes = {
-  classes: PropTypes.object.isRequired,
-  value: PropTypes.string,
-  visualizations: PropTypes.object.isRequired,
-  onChange: PropTypes.func.isRequired
-}
-
-const useDomainSelectStyles = makeStyles(theme => ({
-  root: {
-    minWidth: 60
-  }
-}))
-function DomainSelect({initialDomain}) {
-  const {setDomain} = useContext(searchContext)
-  const [domainParam, setDomainParam] = useQueryParam('domain', StringParam)
-  const domain = domainParam || initialDomain || domainData.dft.key
-
-  useEffect(() => setDomain(domain), [domain, setDomain])
-
-  const classes = useDomainSelectStyles()
-  const [tooltipOpen, setTooltipOpen] = useState(false)
-  const handleTooltip = bool => setTooltipOpen(bool)
-  return <FormControl className={classes.root}>
-    <Tooltip
-      title="Select the data domain to search. Different domains contain different type of data."
-      open={tooltipOpen}
-    >
-      <Select
-        MenuProps={{
-          getContentAnchorEl: null,
-          anchorOrigin: {
-            vertical: 'bottom',
-            horizontal: 'left'
-          }
-        }}
-        renderValue={key => domainData[key].name}
-        value={domain}
-        onChange={event => setDomainParam(event.target.value)}
-        onMouseEnter={() => handleTooltip(true)}
-        onMouseLeave={() => handleTooltip(false)}
-        onOpen={() => handleTooltip(false)}
-      >
-        {Object.keys(domainData).map(domainKey => {
-          const domain = domainData[domainKey]
-          return (
-            <MenuItem value={domain.key} key={domain.key}>
-              <Tooltip title={domain.about}>
-                <div>{domain.label}</div>
-              </Tooltip>
-            </MenuItem>
-          )
-        })}
-      </Select>
-    </Tooltip>
-  </FormControl>
-}
-DomainSelect.propTypes = {
-  initialDomain: PropTypes.string
-}
-
-const ownerLabel = {
-  all: 'All',
-  visible: 'Include private',
-  public: 'Only public',
-  user: 'Only yours',
-  shared: 'Shared',
-  staging: 'Unpublished'
-}
-
-const ownerTooltips = {
-  all: 'This will show all entries in the database.',
-  visible: 'Do also show entries that are only visible to you.',
-  public: 'Do not show entries with embargo.',
-  user: 'Do only show entries visible to you.',
-  shared: 'Also include data that is shared with you',
-  staging: 'Will only show entries that you uploaded, but not yet published.'
-}
-
-function OwnerSelect(props) {
-  const {ownerTypes, initialOwner} = props
-  const {setOwner} = useContext(searchContext)
-
-  const ownerTypesToRender = ownerTypes.length === 1 ? [] : ownerTypes.slice(1)
-
-  const [{owner}, setQueryParam] = useUrlQuery()
-  const ownerValue = owner || initialOwner || 'all'
-
-  useEffect(() => {
-    setOwner(ownerValue)
-  }, [ownerValue, setOwner])
-
-  const handleChange = (event) => {
-    if (owner !== event.target.value) {
-      setQueryParam({owner: event.target.value})
-    } else {
-      setQueryParam({owner: initialOwner})
-    }
-  }
-
-  if (ownerTypes.length === 1) {
-    return <React.Fragment/>
-  }
-
-  return <FormControl>
-    <FormGroup row>
-      {ownerTypesToRender.map(ownerToRender => (
-        <Tooltip key={ownerToRender} title={ownerTooltips[ownerToRender]}>
-          <FormControlLabel
-            control={<Checkbox
-              checked={ownerValue === ownerToRender}
-              onChange={handleChange} value={ownerToRender}
-            />}
-            label={ownerLabel[ownerToRender]}
-          />
-        </Tooltip>
-      ))}
-    </FormGroup>
-  </FormControl>
-}
-OwnerSelect.propTypes = {
-  ownerTypes: PropTypes.arrayOf(PropTypes.string).isRequired,
-  initialOwner: PropTypes.string
-}
-
-const useSearchResultStyles = makeStyles(theme => ({
-  root: {
-    marginTop: theme.spacing(4)
-  }
-}))
-function SearchResults({availableTabs = ['entries'], initialTab = 'entries', resultListProps = {}}) {
-  const classes = useSearchResultStyles()
-  const {domain, setGroups} = useContext(searchContext)
-  let [openTab, setOpenTab] = useQueryParam('results', StringParam)
-  openTab = openTab || initialTab
-  const ResultList = resultTabs[openTab].component
-  const handleTabChange = tab => {
-    setOpenTab(tab)
-    setGroups(resultTabs[tab].groups)
-  }
-
-  useEffect(() => {
-    if (openTab !== 'entries') {
-      handleTabChange(openTab)
-    }
-    // eslint-disable-next-line
-  }, [])
-
-  return <div className={classes.root}>
-    <Paper>
-      <Tabs
-        value={openTab}
-        indicatorColor="primary"
-        textColor="primary"
-        onChange={(event, value) => handleTabChange(value)}
-      >
-        {availableTabs.filter(tab => domain.searchTabs.includes(tab)).map(key => {
-          const tab = resultTabs[key]
-          return <Tab key={key} label={tab.label} value={key} />
-        })}
-      </Tabs>
-
-      <ResultList domain={domain} {...resultListProps} />
-    </Paper>
+      <SearchResults
+        className={styles.resultList}
+      />
+      <div className={clsx(styles.nonInteractive, styles.shadow, styles.shadowHidden, isMenuOpen && styles.shadowVisible)}></div>
+    </div>
   </div>
-}
-SearchResults.propTypes = {
-  'availableTabs': PropTypes.arrayOf(PropTypes.string),
-  'initialTab': PropTypes.string,
-  'resultListProps': PropTypes.object
-}
-
-function ReRunSearchButton() {
-  const {update} = useContext(searchContext)
-  return <Tooltip title="Re-execute the search">
-    <IconButton onClick={update}>
-      <ReloadIcon />
-    </IconButton>
-  </Tooltip>
-}
-
-const usePagination = () => {
-  const {setRequestParameters} = useContext(searchContext)
-  let [requestQueryParameters, setRequestQueryParameters] = useQueryParams({
-    order: NumberParam, order_by: StringParam, per_page: NumberParam, page: NumberParam
-  })
-  requestQueryParameters = objectFilter(requestQueryParameters, key => requestQueryParameters[key])
-  requestQueryParameters.page = requestQueryParameters.page || 1
-  useEffect(
-    () => setRequestParameters(requestQueryParameters),
-    [requestQueryParameters, setRequestParameters]
-  )
-  return setRequestQueryParameters
-}
-
-const useScroll = (apiGroupName, afterParameterName) => {
-  afterParameterName = afterParameterName || `${apiGroupName}_after`
-  const apiAfterParameterName = `${apiGroupName}_grouped_after`
-
-  const {response, setRequestParameters} = useContext(searchContext)
-  const [queryAfterParameter, setQueryAfterParameter] = useQueryParam(afterParameterName, StringParam)
-  useEffect(
-    () => {
-      const requestParameters = {}
-      requestParameters[apiAfterParameterName] = queryAfterParameter || null
-      setRequestParameters(requestParameters)
-    }, [queryAfterParameter, setRequestParameters, apiAfterParameterName]
-  )
-
-  const responseGroup = response[`${apiGroupName}_grouped`]
-  const after = responseGroup && responseGroup.after
-  const result = {
-    total: response.statistics.total.all[apiGroupName],
-    onChange: requestParameters => setQueryAfterParameter(requestParameters[apiAfterParameterName])
-  }
-  result[afterParameterName] = after
-  return result
-}
-
-function SearchEntryList(props) {
-  const {response, requestParameters, apiQuery, update} = useContext(searchContext)
-  const setRequestParameters = usePagination()
-  return <EntryList
-    query={apiQuery}
-    editable={apiQuery.owner === 'staging' || apiQuery.owner === 'user'}
-    data={response}
-    onChange={setRequestParameters}
-    onEdit={update}
-    actions={
-      <React.Fragment>
-        <ReRunSearchButton/>
-        <ApiDialogButton data={response} />
-      </React.Fragment>
-    }
-    {...requestParameters}
-    {...props}
-  />
-}
-
-function SearchDatasetList(props) {
-  const {response, update} = useContext(searchContext)
-  return <DatasetList
-    data={response}
-    onEdit={update}
-    actions={<ReRunSearchButton/>}
-    {...response} {...props} {...useScroll('datasets')}
-  />
-}
-
-function SearchGroupList(props) {
-  const {response} = useContext(searchContext)
-  return <GroupList
-    data={response}
-    actions={<ReRunSearchButton/>}
-    {...response} {...props} {...useScroll('dft.groups', 'groups_after')}
-  />
-}
-
-function SearchUploadList(props) {
-  const {response, update} = useContext(searchContext)
-  return <UploadList data={response}
-    onEdit={update}
-    actions={<ReRunSearchButton/>}
-    {...response} {...props} {...useScroll('uploads')}
-  />
+})
+Search.propTypes = {
+  collapsed: PropTypes.bool,
+  header: PropTypes.node
 }
 
-function SearchMaterialsList(props) {
-  const {response} = useContext(searchContext)
-  return <MaterialsList
-    data={response}
-    actions={<ReRunSearchButton/>}
-    {...response} {...props} {...useScroll('encyclopedia.material.materials', 'materials_after')}
-  />
-}
+export default Search
diff --git a/gui/src/components/search/SearchBar.js b/gui/src/components/search/SearchBar.js
index 6aa164c1bc..72f6a221db 100644
--- a/gui/src/components/search/SearchBar.js
+++ b/gui/src/components/search/SearchBar.js
@@ -15,379 +15,417 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import React, { useRef, useState, useContext, useCallback, useMemo } from 'react'
-import {searchContext} from './SearchContext'
+import React, { useCallback, useState, useMemo } from 'react'
+import PropTypes from 'prop-types'
+import clsx from 'clsx'
+import { debounce, isNil } from 'lodash'
 import Autocomplete from '@material-ui/lab/Autocomplete'
-import TextField from '@material-ui/core/TextField'
-import { CircularProgress, InputAdornment, Button, Tooltip } from '@material-ui/core'
+import { makeStyles } from '@material-ui/core/styles'
+import SearchIcon from '@material-ui/icons/Search'
+import CloseIcon from '@material-ui/icons/Close'
+import {
+  TextField,
+  CircularProgress,
+  Paper,
+  Divider,
+  Tooltip,
+  Typography
+} from '@material-ui/core'
+import IconButton from '@material-ui/core/IconButton'
+import { useApi } from '../apiV1'
+import { useUnits } from '../../units'
+import { isMetaNumber, isMetaTimestamp } from '../../utils'
+import {
+  useSetFilters,
+  useFiltersLocked,
+  filterFullnames,
+  filterAbbreviations,
+  toGUIFilter,
+  filterData,
+  filters
+} from './SearchContext'
 import searchQuantities from '../../searchQuantities'
-import { apiContext } from '../api'
-import { defsByName as metainfoDefs } from '../archive/metainfo'
-import { domainData } from '../domainData'
 
-const metainfoOptions = []
+const opMap = {
+  '<=': 'lte',
+  '>=': 'gte',
+  '>': 'gt',
+  '<': 'lt'
+}
+const opMapReverse = {
+  '<=': 'gte',
+  '>=': 'lte',
+  '>': 'lt',
+  '<': 'gt'
+}
 
-const quantitiesWithAlternativeOptions = {
-  calc_id: () => [],
-  upload_id: () => [],
-  calc_hash: () => [],
-  'dft.quantities': () => {
-    if (metainfoOptions.length === 0) {
-      metainfoOptions.push(...Object.keys(metainfoDefs)
-        .filter(name => !name.startsWith('x_'))
-        .map(name => ({
-          domain: 'dft',
-          quantity: 'dft.quantities',
-          value: name
-        })))
+// Decides which options are shown
+const filterOptions = (options, {inputValue}) => {
+  const trimmed = inputValue.trim().toLowerCase()
+  return options.filter(option => {
+    // ES results do not need to be filtered at all
+    const category = option.category
+    if (category !== 'quantity name') {
+      return true
     }
-    return metainfoOptions
-  }
+    // Underscore can be replaced by a whitespace
+    const optionClean = option.value.trim().toLowerCase()
+    const matchUnderscore = optionClean.includes(trimmed)
+    const matchNoUnderscore = optionClean.replaceAll('_', ' ').includes(trimmed)
+    return matchUnderscore || matchNoUnderscore
+  })
 }
 
-// We need to treat dft. and encyclopedia. special. Usually all dft domain pieces
-// are prefixed dft., but the encycloepdia is top-level and also a dft. specific
-// quantity. These to functions remove and add the dft./encyclopedia. prefixes accordingly.
-function getDomainOfQuantity(quantity) {
-  if (!quantity.includes('.')) {
-    return null
-  }
-  const firstSegment = quantity.split('.')[0]
-  if (firstSegment === 'encyclopedia') {
-    return 'dft'
-  }
-  return firstSegment
+// Customized paper component for the autocompletion options
+const CustomPaper = (props) => {
+  return <Paper elevation={3} {...props} />
 }
 
-function addDomainToQuantity(shortenedQuantityName, domainKey) {
-  if (!searchQuantities[shortenedQuantityName]) {
-    shortenedQuantityName = domainKey + '.' + shortenedQuantityName
-    if (!searchQuantities[shortenedQuantityName]) {
-      shortenedQuantityName = 'encyclopedia.' + shortenedQuantityName.slice(4)
-    }
+const useStyles = makeStyles(theme => ({
+  root: {
+    display: 'flex',
+    alignItems: 'center',
+    position: 'relative'
+  },
+  notchedOutline: {
+    borderColor: 'rgba(0, 0, 0, 0.0)'
+  },
+  iconButton: {
+    padding: 10
+  },
+  divider: {
+    height: '2rem'
+  },
+  endAdornment: {
+    position: 'static'
+  },
+  examples: {
+    position: 'absolute',
+    left: 0,
+    right: 0,
+    top: 'calc(100% + 4px)',
+    padding: theme.spacing(2),
+    fontStyle: 'italic'
   }
-  return shortenedQuantityName
-}
+}))
 
 /**
- * This searchbar component shows a searchbar with autocomplete functionality. The
- * searchbar also includes a status line about the current results. It uses the
- * search context to manipulate the current query and display results. It does its on
- * API calls to provide autocomplete suggestion options.
+ * This component shows a searchbar with autocomplete functionality. It does its
+ * on API calls to provide autocomplete suggestion options.
  */
-export default function SearchBar() {
-  const currentLoadOptionsConfigRef = useRef({
-    timer: null,
-    latestOption: null,
-    requestedOption: null
-  })
-  const {response: {statistics, pagination, error}, domain, query, apiQuery, setQuery} = useContext(searchContext)
-  const defaultOptions = useMemo(() => {
-    return Object.keys(searchQuantities)
-      .map(quantity => ({
-        quantity: quantity,
-        domain: getDomainOfQuantity(quantity)
-      }))
-      .filter(option => !option.domain || option.domain === domain.key)
-  }, [domain.key])
-
-  const [open, setOpen] = useState(false)
-  const [options, setOptions] = useState(defaultOptions)
+const SearchBar = React.memo(({
+  className
+}) => {
+  const styles = useStyles()
+  const units = useUnits()
+  const [suggestions, setSuggestions] = useState([])
   const [loading, setLoading] = useState(false)
   const [inputValue, setInputValue] = useState('')
-  const [searchType, setSearchType] = useState('nomad')
-
-  const {api} = useContext(apiContext)
-
-  const autocompleteValue = Object.keys(query).map(quantity => ({
-    quantity: quantity,
-    domain: quantity.includes('.') ? quantity.split('.')[0] : null,
-    value: query[quantity]
-  }))
-
-  const handleSearchTypeClicked = useCallback(() => {
-    if (searchType === 'nomad') {
-      setSearchType('optimade')
-    } else {
-      setSearchType('nomad')
-    }
-  }, [searchType, setSearchType])
-
-  const handleOptimadeEntered = useCallback(query => {
-    setQuery({'dft.optimade': query})
-  }, [setQuery])
-
-  let helperText = ''
-  if (error) {
-    helperText = '' + (error.apiMessage || error)
-  } else if (pagination && statistics) {
-    if (pagination.total === 0) {
-      helperText = <span>There are no more entries matching your criteria.</span>
-    } else {
-      helperText = <span>
-        There {pagination.total === 1 ? 'is' : 'are'} {
-          Object.keys(domain.searchMetrics).filter(key => statistics.total.all[key]).map(key => {
-            return <span key={key}>
-              {domain.searchMetrics[key].renderResultString(statistics.total.all[key])}
-            </span>
-          })
-        }{Object.keys(query).length ? ' left' : ''}.
-      </span>
+  const [highlighted, setHighlighted] = useState({value: ''})
+  const [open, setOpen] = useState(false)
+  const [error, setError] = useState(false)
+  const [showExamples, setShowExamples] = useState(false)
+  const {api} = useApi()
+  const filtersLocked = useFiltersLocked()
+  const setFilter = useSetFilters()
+  const quantitySet = filters
+  const quantitySuggestions = useMemo(() => {
+    const suggestions = []
+    for (let q of filters) {
+      suggestions.push({
+        value: filterAbbreviations[q] || q,
+        category: 'quantity name'
+      })
     }
-  }
-
-  const loadOptions = useCallback(option => {
-    const config = currentLoadOptionsConfigRef.current
-    config.latestOption = option
+    return suggestions
+  }, [])
 
-    if (config.timer !== null) {
-      clearTimeout(config.timer)
-    }
-    if (loading) {
+  // Triggered when a value is submitted by pressing enter or clicking the
+  // search icon.
+  const handleSubmit = useCallback(() => {
+    if (inputValue.trim().length === 0) {
       return
     }
-    config.timer = setTimeout(() => {
-      config.requestedOption = option
-
-      const alternativeOptions = quantitiesWithAlternativeOptions[option.quantity]
-      if (alternativeOptions) {
-        setOptions(alternativeOptions())
+    const reString = '[^\\s=<>](?:[^=<>]*[^\\s=<>])?'
+    const op = '(?:<|>)=?'
+    let valid = false
+    let quantityFullname
+    let queryValue
+
+    // Equality query
+    const equals = inputValue.match(new RegExp(`^\\s*(${reString})\\s*=\\s*(${reString})\\s*$`))
+    if (equals) {
+      const quantityName = equals[1]
+      quantityFullname = filterFullnames[quantityName] || quantityName
+      if (!quantitySet.has(quantityFullname)) {
+        setError(`Unknown quantity name`)
         return
       }
-
-      const size = searchQuantities[option.quantity].statistic_size
-      setLoading(true)
-      api.suggestions_search(option.quantity, apiQuery, size ? null : option.value, size || 20, true)
-        .then(response => {
-          setLoading(false)
-          if (!config.latestOption || config.requestedOption.quantity !== config.latestOption.quantity) {
-            // don't do anything if quantity has changed in the meantime
-            return
-          }
-          const options = response.suggestions.map(value => ({
-            quantity: option.quantity,
-            domain: option.domain,
-            value: value
-          }))
-          setOptions(options)
-          setOpen(true)
-        })
-        .catch(() => {
-          setLoading(false)
-        })
-    }, 200)
-  }, [api, currentLoadOptionsConfigRef, apiQuery, loading, setLoading])
-
-  const getOptionLabel = useCallback(option => {
-    if (option.quantity === 'from_time' || option.quantity === 'until_time') {
-      if (option.value) {
-        return `${option.quantity.replace('_time', '')}=${option.value.substring(0, 10)}`
+      try {
+        queryValue = toGUIFilter(quantityFullname, equals[2], units)
+      } catch (error) {
+        setError(`Invalid value for this metainfo. Please check your syntax.`)
+        return
       }
+      valid = true
     }
 
-    let label = option.quantity + '='
-    if (option.value) {
-      if (Array.isArray(option.value)) {
-        label += option.value.join(',')
-      } else {
-        label += option.value
+    // Simple LTE/GTE query
+    if (!valid) {
+      const ltegte = inputValue.match(new RegExp(`^\\s*(${reString})\\s*(${op})\\s*(${reString})\\s*$`))
+      if (ltegte) {
+        const a = ltegte[1]
+        const op = ltegte[2]
+        const b = ltegte[3]
+        const aFullname = filterFullnames[a]
+        const bFullname = filterFullnames[b]
+        const isAQuantity = quantitySet.has(aFullname)
+        const isBQuantity = quantitySet.has(bFullname)
+        if (!isAQuantity && !isBQuantity) {
+          setError(`Unknown quantity name`)
+          return
+        }
+        quantityFullname = isAQuantity ? aFullname : bFullname
+        if (!isMetaNumber(quantityFullname) && !isMetaTimestamp(quantityFullname)) {
+          setError(`Cannot perform range query for a non-numeric quantity.`)
+          return
+        }
+        let quantityValue
+        try {
+          quantityValue = toGUIFilter(quantityFullname, isAQuantity ? b : a, units)
+        } catch (error) {
+          setError(`Invalid value for this metainfo. Please check your syntax.`)
+          return
+        }
+        queryValue = {}
+        queryValue[opMap[op]] = quantityValue
+        valid = true
       }
     }
-    return label.substring(label.indexOf('.') + 1)
-  }, [])
-
-  const parseOption = useCallback(input => {
-    const [inputQuantity, inputValue] = input.split('=')
-
-    const quantity = addDomainToQuantity(inputQuantity, domain.key)
-    let value = inputValue
-    if (value && searchQuantities[quantity] && searchQuantities[quantity].many) {
-      value = value.split(',').map(item => item.trim())
-    }
-    return {
-      inputQuantity: inputQuantity,
-      inputValue: inputValue,
-      domain: inputQuantity.includes('.') ? inputQuantity.split('.')[0] : null,
-      quantity: searchQuantities[quantity] ? quantity : null,
-      value: value
-    }
-  }, [domain.key])
 
-  const filterOptions = useCallback((options, params) => {
-    const inputOption = parseOption(params.inputValue)
-    const filteredOptions = options.filter(option => {
-      if (!inputOption.quantity) {
-        return option.quantity.includes(
-          inputOption.inputQuantity) && (option.domain === domain.key || !option.domain)
-      }
-      if (option.quantity !== inputOption.quantity) {
-        return false
-      }
-
-      if (!inputOption.value) {
-        return true
-      }
+    // Sandwiched LTE/GTE query
+    if (!valid) {
+      const ltegteSandwich = inputValue.match(new RegExp(`^\\s*(${reString})\\s*(${op})\\s*(${reString})\\s*(${op})\\s*(${reString})\\s*$`))
+      if (ltegteSandwich) {
+        const a = ltegteSandwich[1]
+        const op1 = ltegteSandwich[2]
+        const b = ltegteSandwich[3]
+        const op2 = ltegteSandwich[4]
+        const c = ltegteSandwich[5]
+        quantityFullname = filterFullnames[b]
+        if (!isMetaNumber(quantityFullname) && !isMetaTimestamp(quantityFullname)) {
+          setError(`Cannot perform range query for a non-numeric quantity.`)
+          return
+        }
+        const isBQuantity = quantitySet.has(quantityFullname)
+        if (!isBQuantity) {
+          setError(`Unknown quantity name`)
+          return
+        }
 
-      const matches = option.value &&
-        inputOption.inputValue &&
-        option.value.toLowerCase().includes(inputOption.inputValue.toLowerCase())
-      if (matches) {
-        if (option.value === inputOption.inputValue) {
-          inputOption.exists |= true
+        queryValue = {}
+        try {
+          queryValue[opMapReverse[op1]] = toGUIFilter(quantityFullname, a, units)
+          queryValue[opMap[op2]] = toGUIFilter(quantityFullname, c, units)
+        } catch (error) {
+          setError(`Invalid value for this metainfo. Please check your syntax.`)
+          return
         }
-        return true
+        valid = true
       }
-
-      return false
-    })
-
-    // Add the value as option, even if it does not exist to allow search for missing,
-    // faulty, or not yet loaded options
-    if (inputOption.quantity && !inputOption.exists) {
-      filteredOptions.push(inputOption)
     }
 
-    return filteredOptions
-  }, [domain.key, parseOption])
+    // Check if filter is locked
+    if (filtersLocked[quantityFullname]) {
+      setError(`Cannot change the filter as it is locked in the current search context.`)
+      return
+    }
 
-  const handleInputChange = useCallback((event, value, reason) => {
-    if (reason === 'input') {
-      setInputValue(value)
-      const inputOption = parseOption(value)
-      if (inputOption.quantity) {
-        loadOptions(inputOption)
-      } else {
-        setOptions(defaultOptions)
-      }
+    if (valid) {
+      // Submit to search context on successful validation.
+      setFilter([quantityFullname, old => {
+        const multiple = filterData[quantityFullname].multiple
+        return (isNil(old) || !multiple) ? queryValue : new Set([...old, ...queryValue])
+      }])
+      setInputValue('')
+      setOpen(false)
+    } else {
+      setError(`Invalid query`)
     }
-  }, [loadOptions, defaultOptions, parseOption])
+  // eslint-disable-next-line react-hooks/exhaustive-deps
+  }, [inputValue, quantitySet])
+
+  // Handle clear button
+  const handleClose = useCallback(() => {
+    setInputValue('')
+    setSuggestions([])
+    setOpen(false)
+    setShowExamples(true)
+  }, [])
 
-  const handleChange = (event, entries) => {
-    currentLoadOptionsConfigRef.current.latestOption = null
+  const handleHighlight = useCallback((event, value, reason) => {
+    setHighlighted(value)
+  }, [])
 
-    entries = entries.map(entry => {
-      if (typeof entry === 'string') {
-        return parseOption(entry)
+  // When enter is pressed, select currently highlighted value and close menu,
+  // or if menu is not open submit the value.
+  const handleEnter = useCallback((event) => {
+    if (event.key === 'Enter') {
+      if (open && highlighted?.value) {
+        setInputValue(highlighted.value)
+        setOpen(false)
       } else {
-        return entry
+        handleSubmit()
       }
-    })
-
-    const newQuery = entries.reduce((query, entry) => {
-      if (entry) {
-        if (query[entry.quantity]) {
-          if (searchQuantities[entry.quantity].many) {
-            if (Array.isArray(query[entry.quantity])) {
-              query[entry.quantity].push(entry.value)
-            } else {
-              query[entry.quantity] = [query[entry.quantity], entry.value]
-            }
-          } else {
-            query[entry.quantity] = entry.value
+      event.stopPropagation()
+      event.preventDefault()
+    }
+  }, [open, highlighted, handleSubmit])
+
+  const suggestionCall = useCallback((quantityList, value) => {
+    setLoading(true)
+    // If some input is given, and the quantity supports suggestions, we use
+    // input suggester to suggest values
+    const filteredList = quantityList.filter(q => searchQuantities[q]?.suggestion)
+    api.suggestions(filteredList, value)
+      .then(data => {
+        let res = []
+        for (let q of filteredList) {
+          const name = filterAbbreviations[q] || q
+          const esSuggestions = data[q]
+          if (esSuggestions) {
+            res = res.concat(esSuggestions.map(suggestion => ({
+              value: `${name}=${suggestion.value}`,
+              category: name
+            })))
           }
-        } else {
-          query[entry.quantity] = entry.value
         }
-      }
-      return query
-    }, {})
-    setQuery(newQuery, true)
-
-    if (entries.length !== 0) {
-      const entry = entries[entries.length - 1]
-      if (entry.value) {
-        setInputValue('')
-      } else {
-        setInputValue(getOptionLabel(entry))
-        loadOptions(entry)
+        setSuggestions(res)
+      })
+      .finally(() => setLoading(false))
+  }, [api])
+  const suggestionDebounced = useCallback(debounce(suggestionCall, 150), [])
+
+  // Handle typing events. After a debounce time has expired, a list of
+  // suggestion will be retrieved if they are available for this metainfo and
+  // the input is deemed meaningful.
+  const handleInputChange = useCallback((event, value, reason) => {
+    setError(error => error ? undefined : null)
+    setInputValue(value)
+    value = value?.trim()
+    setShowExamples(!value)
+    if (!value) {
+      setSuggestions([])
+      setOpen(false)
+      setShowExamples(true)
+      return
+    } else {
+      setOpen(true)
+      setShowExamples(false)
+    }
+    if (reason !== 'input') {
+      setSuggestions([])
+      setOpen(false)
+    }
+    // If the input is prefixed with a proper quantity name and an equals-sign,
+    // we extract the quantity name and the typed input
+    const split = value.split('=', 2)
+    let quantityList = [...filters]
+    if (split.length === 2) {
+      const quantityName = split[0].trim()
+      const quantityFullname = filterFullnames[quantityName]
+      if (quantitySet.has(quantityName)) {
+        quantityList = [quantityName]
+        value = split[1].trim()
+      } else if (quantitySet.has(quantityFullname)) {
+        quantityList = [quantityFullname]
+        value = split[1].trim()
       }
     }
-  }
 
-  React.useEffect(() => {
-    if (!open) {
-      setOptions(defaultOptions)
+    setLoading(true)
+    // If some input is given, and the quantity supports suggestions, we use
+    // input suggester to suggest values
+    if (value.length > 0) {
+      suggestionDebounced(quantityList, value)
+    // If no input is given, we suggest Enum values, or for non-enum quantities
+    // use terms aggregation.
+    } else {
     }
-  }, [open, defaultOptions])
+  }, [quantitySet, suggestionDebounced])
 
-  const commonTextFieldProps = params => ({
-    error: !!error,
-    helperText: helperText,
-    variant: 'outlined',
-    fullWidth: true,
-    ...params
-  })
-
-  const commonInputProps = (params) => ({
-    ...params,
-    startAdornment: (
-      <React.Fragment>
-        {domain === domainData.dft &&
-        <InputAdornment position="start">
-          <Tooltip title="Switch between NOMAD's quantity=value search and the Optimade filter language.">
-            <Button onClick={handleSearchTypeClicked}size="small">{searchType}</Button>
-          </Tooltip>
-        </InputAdornment>}
-        {params.startAdornment}
-      </React.Fragment>
-    )
-  })
+  // This determines the order: notice that items should be sorted by group
+  // first in order for the grouping to work correctly.
+  const options = useMemo(() => {
+    return suggestions.concat(quantitySuggestions)
+  }, [quantitySuggestions, suggestions])
 
-  if (searchType === 'nomad') {
-    return <Autocomplete
-      multiple
+  return <Paper className={clsx(className, styles.root)}>
+    <Autocomplete
+      className={styles.input}
       freeSolo
+      clearOnBlur={false}
       inputValue={inputValue}
-      value={autocompleteValue}
-      limitTags={4}
-      id='search-bar'
+      value={null}
       open={open}
-      onOpen={() => {
-        setOpen(true)
-      }}
-      onClose={() => {
-        setOpen(false)
-      }}
-      onChange={handleChange}
-      onInputChange={handleInputChange}
-      getOptionSelected={(option, inputOption) => {
-        return inputOption.quantity === option.quantity && inputOption.value === option.value
-      }}
-      getOptionLabel={getOptionLabel}
-      options={options}
-      loading={loading}
+      onFocus={() => setShowExamples(true)}
+      onBlur={() => setShowExamples(false)}
+      onOpen={() => { if (inputValue.trim() !== '') { setOpen(true) } }}
+      onClose={() => setOpen(false)}
+      fullWidth
+      disableClearable
+      PaperComponent={CustomPaper}
+      classes={{endAdornment: styles.endAdornment}}
+      groupBy={(option) => option.category}
       filterOptions={filterOptions}
-      // handleHomeEndKeys
+      options={options}
+      onInputChange={handleInputChange}
+      onHighlightChange={handleHighlight}
+      getOptionLabel={option => option.value}
+      getOptionSelected={(option, value) => false}
       renderInput={(params) => (
         <TextField
-          {...commonTextFieldProps(params)}
-          label={searchType === 'nomad' ? 'Search with quantity=value' : 'Search with Optimade filter language'}
+          {...params}
+          className={styles.textField}
+          variant="outlined"
+          placeholder=""
+          label={error || undefined}
+          error={!!error}
+          onKeyDown={handleEnter}
+          InputLabelProps={{ shrink: true }}
           InputProps={{
-            ...commonInputProps(params.InputProps),
-            endAdornment: (
-              <React.Fragment>
-                {loading ? <CircularProgress color='inherit' size={20} /> : null}
-                {params.InputProps.endAdornment}
-              </React.Fragment>
-            )
+            ...params.InputProps,
+            classes: {
+              notchedOutline: styles.notchedOutline
+            },
+            endAdornment: (<>
+              {loading ? <CircularProgress color="inherit" size={20} /> : null}
+              {(inputValue?.length || null) && <>
+                <Tooltip title="Clear">
+                  <IconButton onClick={handleClose} className={styles.iconButton} aria-label="clear">
+                    <CloseIcon />
+                  </IconButton>
+                </Tooltip>
+                <Divider className={styles.divider} orientation="vertical"/>
+              </>}
+              <Tooltip title="Add filter">
+                <IconButton onClick={handleSubmit} className={styles.iconButton} aria-label="search">
+                  <SearchIcon />
+                </IconButton>
+              </Tooltip>
+            </>)
           }}
         />
       )}
     />
-  } else {
-    return <TextField
-      {...commonTextFieldProps({})}
-      label={searchType === 'nomad' ? 'Search with quantity=value' : 'Search with Optimade filter language'}
-      InputProps={{
-        ...commonInputProps({})
-      }}
-      defaultValue={query['dft.optimade'] || ''}
-      onKeyPress={(ev) => {
-        if (ev.key === 'Enter') {
-          handleOptimadeEntered(ev.target.value)
-          ev.preventDefault()
-        }
-      }}
-    />
-  }
+    {showExamples && <CustomPaper className={styles.examples}>
+      <Typography>{'Start typing a query or a keyword to get relevant suggestions.'}</Typography>
+    </CustomPaper>}
+  </Paper>
+})
+
+SearchBar.propTypes = {
+  className: PropTypes.string
 }
+
+export default SearchBar
diff --git a/gui/src/components/search/SearchContext.js b/gui/src/components/search/SearchContext.js
index 67b1d90ebd..af4c4fe119 100644
--- a/gui/src/components/search/SearchContext.js
+++ b/gui/src/components/search/SearchContext.js
@@ -15,312 +15,1129 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import React, { useState, useContext, useEffect, useRef, useCallback, useMemo } from 'react'
-import PropTypes from 'prop-types'
-import hash from 'object-hash'
-import { errorContext } from '../errors'
-import { onlyUnique, objectFilter } from '../../utils'
-import { domainData } from '../domainData'
-import { apiContext } from '../api'
-import { useLocation, useHistory } from 'react-router-dom'
+import React, { useCallback, useEffect, useState, useRef, useMemo, useContext } from 'react'
+import {
+  atom,
+  atomFamily,
+  selector,
+  useSetRecoilState,
+  useRecoilValue,
+  useRecoilState,
+  useRecoilCallback
+} from 'recoil'
+import {
+  debounce,
+  isEmpty,
+  isArray,
+  isPlainObject,
+  isNil,
+  isString
+} from 'lodash'
 import qs from 'qs'
+import PropTypes from 'prop-types'
+import { useHistory } from 'react-router-dom'
+import { useApi } from '../apiV1'
+import { setToArray, formatMeta, parseMeta } from '../../utils'
 import searchQuantities from '../../searchQuantities'
+import { Quantity } from '../../units'
+
+export const filters = new Set() // Contains the full names of all the available filters
+export const filterGroups = [] // Mapping from filter full name -> group
+export const filterAbbreviations = [] // Mapping of filter full name -> abbreviation
+export const filterFullnames = [] // Mapping of filter abbreviation -> full name
+export const filterData = {} // Stores data for each registered filter
+export const labelMaterial = 'Material'
+export const labelElements = 'Elements / Formula'
+export const labelSymmetry = 'Symmetry'
+export const labelMethod = 'Method'
+export const labelSimulation = 'Simulation'
+export const labelDFT = 'DFT'
+export const labelGW = 'GW'
+export const labelProperties = 'Properties'
+export const labelElectronic = 'Electronic'
+export const labelVibrational = 'Vibrational'
+export const labelAuthor = 'Author / Origin'
+export const labelAccess = 'Access'
+export const labelDataset = 'Dataset'
+export const labelIDs = 'IDs'
+
+/**
+ * This function is used to register a new filter within the FilterContext.
+ * Filters are entities that can be searched throuh the filter panel and the
+ * search bar, and can be encoded in the URL. Notice that a filter in this
+ * context does not have to correspond to a quantity in the metainfo.
+ *
+ * Only registered filters may be searched for. The registration must happen
+ * before any components use the filters. This is because:
+ *  - The initial aggregation results must be fetched before any components
+ *  using the filter values are rendered.
+ *  - Several components need to know the list of available filters (e.g. the
+ *  search bar and  the search panel). If filters are only registered during
+ *  component initialization, it may already be too late to update other
+ *  components.
+ *
+ * @param {string} name Name of the filter.
+ * @param {string} group The group into which the filter belongs to. Groups
+ * are used to e.g. in showing FilterSummaries about a group of filters.
+ * @param {string|object} agg Custom setter/getter for the aggregation value. As a
+ * shortcut you can provide an ES aggregation type as a string,
+ * @param {object} value Custom setter/getter for the filter value.
+ * @param {bool} multiple Whether this filter supports several values:
+ * controls whether setting the value appends or overwrites.
+ */
+function registerFilter(name, group, agg, value, multiple = true) {
+  filters.add(name)
+  if (group) {
+    filterGroups[group]
+      ? filterGroups[group].add(name)
+      : filterGroups[group] = new Set([name])
+  }
+
+  // Register mappings from full name to abbreviation and vice versa
+  const abbreviation = name.split('.').pop()
+  const oldName = filterAbbreviations[abbreviation]
+  if (!oldName) {
+    filterAbbreviations[name] = abbreviation
+    filterFullnames[abbreviation] = name
+  } else {
+    delete filterFullnames[abbreviation]
+    filterAbbreviations[name] = name
+    filterAbbreviations[oldName] = oldName
+  }
+
+  const data = filterData[name] || {}
+  if (agg) {
+    let aggSet, aggGet
+    if (isString(agg)) {
+      aggSet = {[name]: agg}
+      aggGet = (aggs) => (aggs[name][agg].data)
+    } else {
+      aggSet = agg.set
+      aggGet = agg.get
+    }
+    data.aggSet = aggSet
+    data.aggGet = aggGet
+  }
+  if (value) {
+    data.valueSet = value.set
+  }
+  data.multiple = multiple
+  filterData[name] = data
+}
 
-const padDateNumber = number => String('00' + number).slice(-2)
+// Filters that directly correspond to a metainfo value
+registerFilter('results.material.structural_type', labelMaterial, 'terms')
+registerFilter('results.material.functional_type', labelMaterial, 'terms')
+registerFilter('results.material.compound_type', labelMaterial, 'terms')
+registerFilter('results.material.material_name', labelMaterial)
+registerFilter('results.material.chemical_formula_hill', labelElements)
+registerFilter('results.material.chemical_formula_anonymous', labelElements)
+registerFilter('results.material.n_elements', labelElements, 'min_max', undefined, false)
+registerFilter('results.material.symmetry.bravais_lattice', labelSymmetry, 'terms')
+registerFilter('results.material.symmetry.crystal_system', labelSymmetry, 'terms')
+registerFilter('results.material.symmetry.structure_name', labelSymmetry, 'terms')
+registerFilter('results.material.symmetry.strukturbericht_designation', labelSymmetry, 'terms')
+registerFilter('results.material.symmetry.space_group_symbol', labelSymmetry)
+registerFilter('results.material.symmetry.point_group', labelSymmetry)
+registerFilter('results.material.symmetry.hall_symbol', labelSymmetry)
+registerFilter('results.material.symmetry.prototype_aflow_id', labelSymmetry)
+registerFilter('results.method.method_name', labelMethod, 'terms')
+registerFilter('results.method.simulation.program_name', labelMethod, 'terms')
+registerFilter('results.method.simulation.program_version', labelMethod)
+registerFilter('results.method.simulation.dft.basis_set_type', labelDFT, 'terms')
+registerFilter('results.method.simulation.dft.core_electron_treatment', labelDFT, 'terms')
+registerFilter('results.method.simulation.dft.xc_functional_type', labelDFT, 'terms')
+registerFilter('results.method.simulation.dft.relativity_method', labelDFT, 'terms')
+registerFilter('results.method.simulation.gw.gw_type', labelGW, 'terms')
+registerFilter('results.properties.electronic.band_structure_electronic.channel_info.band_gap_type', labelElectronic, 'terms')
+registerFilter('results.properties.electronic.band_structure_electronic.channel_info.band_gap', labelElectronic, 'min_max', undefined, false)
+registerFilter('external_db', labelAuthor, 'terms')
+registerFilter('authors.name', labelAuthor)
+registerFilter('upload_time', labelAuthor, 'min_max', undefined, false)
+registerFilter('datasets.name', labelDataset)
+registerFilter('datasets.doi', labelDataset)
+registerFilter('entry_id', labelIDs)
+registerFilter('upload_id', labelIDs)
+registerFilter('results.material.material_id', labelIDs)
+registerFilter('datasets.dataset_id', labelIDs)
 
-export const Dates = {
-  dateHistogramStartDate: '2014-12-15',
-  APIDate: date => date.toISOString(),
-  JSDate: date => new Date(date),
-  FormDate: date => {
-    date = new Date(date)
-    return `${date.getFullYear()}-${padDateNumber(date.getMonth())}-${padDateNumber(date.getDate())}`
+// In exclusive element query the elements names are sorted and concatenated
+// into a single string.
+registerFilter(
+  'results.material.elements',
+  labelElements,
+  'terms',
+  {
+    set: (newQuery, oldQuery, value) => {
+      if (oldQuery.exclusive) {
+        if (value.size !== 0) {
+          newQuery['results.material.elements_exclusive'] = setToArray(value).sort().join(' ')
+        }
+      } else {
+        newQuery['results.material.elements'] = value
+      }
+    }
+  }
+)
+// Electronic properties: subset of results.properties.available_properties
+registerFilter(
+  'electronic_properties',
+  labelElectronic,
+  {
+    set: {'results.properties.available_properties': 'terms'},
+    get: (aggs) => (aggs['results.properties.available_properties'].terms.data)
   },
-  addSeconds: (date, interval) => new Date(date.getTime() + interval * 1000),
-  deltaSeconds: (from, end) => Math.round((new Date(end).getTime() - new Date(from).getTime()) / 1000),
-  intervalSeconds: (from, end, buckets) => Math.round((new Date(end).getTime() - new Date(from).getTime()) / (1000 * buckets)),
-  buckets: 50
+  {
+    set: (newQuery, oldQuery, value) => {
+      const data = newQuery['results.properties.available_properties'] || new Set()
+      value.forEach((item) => { data.add(item) })
+      newQuery['results.properties.available_properties'] = data
+    },
+    get: (data) => (data.results.properties.available_properties)
+  }
+)
+// Vibrational properties: subset of results.properties.available_properties
+registerFilter(
+  'vibrational_properties',
+  labelVibrational,
+  {
+    set: {'results.properties.available_properties': 'terms'},
+    get: (aggs) => (aggs['results.properties.available_properties'].terms.data)
+  },
+  {
+    set: (newQuery, oldQuery, value) => {
+      const data = newQuery['results.properties.available_properties'] || new Set()
+      value.forEach((item) => { data.add(item) })
+      newQuery['results.properties.available_properties'] = data
+    },
+    get: (data) => (data.results.properties.available_properties)
+  }
+)
+// Visibility: controls the 'owner'-parameter in the API query, not part of the
+// query itself.
+registerFilter(
+  'visibility',
+  labelAccess,
+  undefined,
+  {set: () => {}},
+  false
+)
+// Restricted: controls whether materials search is done in a restricted mode.
+registerFilter(
+  'restricted',
+  undefined,
+  undefined,
+  {set: () => {}},
+  false
+)
+// Exclusive: controls the way elements search is done.
+registerFilter(
+  'exclusive',
+  undefined,
+  undefined,
+  {set: () => {}},
+  false
+)
+
+// Material and entry queries target slightly different fields. Here we prebuild
+// the mapping.
+const materialNames = {} // Mapping of field name from entry -> material
+const entryNames = {} // Mapping of field name from material -> entry
+for (const name of Object.keys(searchQuantities)) {
+  const prefix = 'results.material.'
+  let materialName
+  if (name.startsWith(prefix)) {
+    materialName = name.substring(prefix.length)
+  } else {
+    materialName = `entries.${name}`
+  }
+  materialNames[name] = materialName
+  entryNames[materialName] = name
 }
 
-searchQuantities['from_time'] = {name: 'from_time'}
-searchQuantities['until_time'] = {name: 'until_time'}
-searchQuantities['dft.optimade'] = {name: 'dft.optimade'}
+export const searchContext = React.createContext()
+export const SearchContext = React.memo(({
+  resource,
+  filtersLocked,
+  children
+}) => {
+  const setQuery = useSetRecoilState(queryState)
+  const setLocked = useSetRecoilState(lockedState)
+  const {api} = useApi()
+  const setInitialAggs = useSetRecoilState(initialAggsState)
+
+  // Reset the query/locks when entering the search context for the first time
+  const reset = useRecoilCallback(({reset}) => () => {
+    for (let filter of filters) {
+      reset(queryFamily(filter))
+      reset(lockedFamily(filter))
+    }
+  }, [])
+
+  useEffect(() => {
+    reset()
+  }, [reset])
+
+  // Read the initial query from the URL
+  const query = useMemo(() => {
+    const location = window.location.href
+    const split = location.split('?')
+    let qs, query
+    if (split.length === 1) {
+      query = {}
+    } else {
+      qs = split.pop()
+      query = qsToQuery(qs)
+    }
+    return query
+  }, [])
+
+  // Save the initial query and locked filters. Cannot be done inside useMemo
+  // due to bad setState.
+  useEffect(() => {
+    setQuery(query)
+    // Transform the locked values into a GUI-suitable format and store them
+    if (filtersLocked) {
+      const filtersLockedGUI = {}
+      for (const [key, value] of Object.entries(filtersLocked)) {
+        filtersLockedGUI[key] = toGUIFilter(key, value)
+      }
+      setLocked(filtersLockedGUI)
+    }
+  }, [setLocked, setQuery, query, filtersLocked])
+
+  // Fetch initial aggregation data.
+  useEffect(() => {
+    const aggRequest = {}
+    const aggNames = [...filters].filter(name => filterData[name].aggGet)
+    for (const filter of aggNames) {
+      toAPIAgg(aggRequest, filter, resource)
+    }
+
+    const search = {
+      owner: 'visible',
+      query: {},
+      aggregations: aggRequest,
+      pagination: {page_size: 0}
+    }
+
+    api.query(resource, search, false)
+      .then(data => {
+        data = toGUIAgg(data.aggregations, aggNames, resource)
+        setInitialAggs(data)
+      })
+  }, [api, setInitialAggs, resource])
+
+  const values = useMemo(() => ({
+    resource: resource
+  }), [resource])
+
+  return <searchContext.Provider value={values}>
+    {children}
+  </searchContext.Provider>
+})
+SearchContext.propTypes = {
+  resource: PropTypes.string,
+  filtersLocked: PropTypes.object,
+  children: PropTypes.node
+}
+
+export function useSearchContext() {
+  return useContext(searchContext)
+}
 
-const filterPaginationParams = query => objectFilter(query, key => key !== 'page' && !key.endsWith('_after'))
 /**
- * A custom hook to read, update, and set the query URL part.
+ * Each search filter is here mapped into a separate Recoil.js Atom. This
+ * allows components to hook into individual search parameters (both for setting
+ * and reading their value). This performs much better than having one large
+ * Atom for the entire query, as this would cause all of the hooked components
+ * to render even if they are not affected by some other search filter.
+ * Re-renders became problematic with large and complex components (e.g. the
+ * periodic table), for which the re-rendering takes significant time. Another
+ * approach would have been to try and Memoize each sufficiently complex
+ * component, but this quickly becomes a hard manual task.
  */
-export const useUrlQuery = () => {
-  const location = useLocation()
-  const history = useHistory()
-  const urlQuery = location.search ? {
-    ...qs.parse(location.search.substring(1))
-  } : {}
+export const queryFamily = atomFamily({
+  key: 'queryFamily',
+  default: undefined
+})
+export const lockedFamily = atomFamily({
+  key: 'lockedFamily',
+  default: false
+})
 
-  const setUrlQuery = urlQuery => {
-    history.push(location.pathname + '?' + qs.stringify(urlQuery, {indices: false}))
-  }
+// Menu open state
+export const menuOpen = atom({
+  key: 'isMenuOpen',
+  default: false
+})
+export function useMenuOpenState() {
+  return useRecoilState(menuOpen)
+}
+export function useSetMenuOpen() {
+  return useSetRecoilState(menuOpen)
+}
 
-  const updateUrlQuery = changes => {
-    const oldQuery = (changes.owner || changes.domain) ? filterPaginationParams(urlQuery) : urlQuery
-    setUrlQuery({...oldQuery, ...changes})
+// Current menu path
+export const menuPath = atom({
+  key: 'menuPath',
+  default: 'Filters'
+})
+export function useMenuPathState() {
+  return useRecoilState(menuPath)
+}
+export function useMenuPath() {
+  return useRecoilValue(menuPath)
+}
+export function useSetMenuPath() {
+  return useSetRecoilState(menuPath)
+}
+
+// Whether the search is initialized.
+export const initializedState = atom({
+  key: 'initialized',
+  default: false
+})
+
+/**
+ * Returns a function that can be called to reset all current filters.
+ *
+ * @returns Function for resetting all filters.
+ */
+export function useResetFilters() {
+  const locked = useRecoilValue(lockedState)
+  const reset = useRecoilCallback(({reset}) => () => {
+    for (let filter of filters) {
+      if (!locked[filter]) {
+        reset(queryFamily(filter))
+      }
+    }
+  }, [locked])
+  return reset
+}
+
+/**
+ * This hook will expose a function for reading if the given filter is locked.
+ *
+ * @param {string} name Name of the filter.
+ * @returns Whether the filter is locked or not.
+ */
+export function useFilterLocked(name) {
+  return useRecoilValue(lockedFamily(name))
+}
+
+/**
+ * This hook will expose a function for reading the locked status of all
+ * filters.
+ *
+ * @returns An object containing a mapping from filter name to a boolean
+ * indicating whether it is locked or not.
+ */
+export function useFiltersLocked() {
+  return useRecoilValue(lockedState)
+}
+
+/**
+ * This hook will expose a function for reading if the given set of filters are
+ * locked.
+ *
+ * @param {string} names Names of the filters.
+ * @returns Array containing the filter values in a map and a setter function.
+ */
+let indexLocked = 0
+export function useFiltersLockedState(names) {
+  // We dynamically create a Recoil.js selector that is subscribed to the
+  // filters specified in the input. This way only the specified filters will
+  // cause a render. Recoil.js requires that each selector/atom has an unique
+  // id. Because this hook can be called dynamically, we simply generate the ID
+  // sequentially.
+  const filterState = useMemo(() => {
+    const id = `locked_selector${indexLocked}`
+    indexLocked += 1
+    return selector({
+      key: id,
+      get: ({get}) => {
+        const query = {}
+        for (let key of names) {
+          const filter = get(lockedFamily(key))
+          query[key] = filter
+        }
+        return query
+      }
+    })
+  // eslint-disable-next-line react-hooks/exhaustive-deps
+  }, [])
+
+  return useRecoilValue(filterState)
+}
+
+// Used to set the locked state of several filters at once
+const lockedState = selector({
+  key: 'lockedState',
+  get: ({get}) => {
+    const locks = {}
+    for (let key of filters) {
+      const filter = get(lockedFamily(key))
+      locks[key] = filter
+    }
+    return locks
+  },
+  set: ({ get, set, reset }, data) => {
+    if (data) {
+      for (const [key, value] of Object.entries(data)) {
+        set(queryFamily(key), value)
+        set(lockedFamily(key), true)
+      }
+    }
   }
+})
+
+/**
+ * This hook will expose a function for reading filter values. Use this hook if
+ * you intend to only view the filter values and are not interested in setting
+ * the filter.
+ *
+ * @param {string} name Name of the filter.
+ * @returns currently set filter value.
+ */
+export function useFilterValue(name) {
+  return useRecoilValue(queryFamily(name))
+}
+
+/**
+ * This hook will expose a function for setting a filter value. Use this hook if
+ * you intend to only set the filter value and are not interested in the query
+ * results.
+ *
+ * @param {string} name Name of the quantity to set.
+ * @returns function for setting the value for the given quantity
+ */
+export function useSetFilter(name) {
+  return useSetRecoilState(queryFamily(name))
+}
 
-  return [urlQuery, updateUrlQuery, setUrlQuery]
+/**
+ * This hook will expose a function for getting and setting filter values. Use
+ * this hook if you intend to both read and write the filter value.
+ *
+ * @param {string} name Name of the filter.
+ * @returns Array containing the filter value and setter function for it.
+ */
+export function useFilterState(name) {
+  return useRecoilState(queryFamily(name))
 }
+
 /**
- * A custom hook that reads and writes search parameters from the current URL.
+ * This hook will expose a function for setting the values of all filters.
+ *
+ * @returns An object containing a mapping from filter name to a boolean
+ * indicating whether it is locked or not.
  */
-const useSearchUrlQuery = () => {
-  // eslint-disable-next-line no-unused-vars
-  const [urlQuery, unused, setUrlQuery] = useUrlQuery()
-  const searchQuery = objectFilter(urlQuery, key => searchQuantities[key] && key !== 'domain')
-  const rest = objectFilter(urlQuery, key => !searchQuantities[key] || key === 'domain')
-  if (searchQuery.atoms && !Array.isArray(searchQuery.atoms)) {
-    searchQuery.atoms = [searchQuery.atoms]
+export function useSetFilters() {
+  return useSetRecoilState(filtersState)
+}
+
+// Used to get/set the locked state of all filters at once
+const filtersState = selector({
+  key: 'filtersState',
+  get: ({get}) => {
+    const query = {}
+    for (let key of filters) {
+      const filter = get(queryFamily(key))
+      query[key] = filter
+    }
+    return query
+  },
+  set: ({set}, [key, value]) => {
+    set(queryFamily(key), value)
   }
-  if (searchQuery.only_atoms && !Array.isArray(searchQuery.only_atoms)) {
-    searchQuery.only_atoms = [searchQuery.only_atoms]
+})
+
+/**
+ * This hook will expose a function for getting and setting filter values for
+ * the specified list of filters. Use this hook if you intend to both read and
+ * write the filter values.
+ *
+ * @param {string} names Names of the filters.
+ * @returns Array containing the filter values in a map and a setter function.
+ */
+let indexFilters = 0
+export function useFiltersState(names) {
+  // We dynamically create a Recoil.js selector that is subscribed to the
+  // filters specified in the input. This way only the specified filters will
+  // cause a render. Recoil.js requires that each selector/atom has an unique
+  // id. Because this hook can be called dynamically, we simply generate the ID
+  // sequentially.
+  const filterState = useMemo(() => {
+    const id = `dynamic_selector${indexFilters}`
+    indexFilters += 1
+    return selector({
+      key: id,
+      get: ({get}) => {
+        const query = {}
+        for (let key of names) {
+          const filter = get(queryFamily(key))
+          query[key] = filter
+        }
+        return query
+      },
+      set: ({set}, [key, value]) => {
+        set(queryFamily(key), value)
+      }
+    })
+  // eslint-disable-next-line react-hooks/exhaustive-deps
+  }, [])
+
+  return useRecoilState(filterState)
+}
+
+/**
+ * This Recoil.js selector aggregates all the currently set filters into a
+ * single query object used by the API.
+ */
+const queryState = selector({
+  key: 'query',
+  get: ({get}) => {
+    if (!get(initializedState)) {
+      return undefined
+    }
+    let query = {}
+    for (let key of filters) {
+      const filter = get(queryFamily(key))
+      if (filter !== undefined) {
+        query[key] = filter
+      }
+    }
+    return query
+  },
+  set: ({ get, set, reset }, data) => {
+    for (let filter of filters) {
+      reset(queryFamily(filter))
+    }
+    if (data) {
+      for (const [key, value] of Object.entries(data)) {
+        set(queryFamily(key), value)
+      }
+      set(initializedState, true)
+    } else {
+      set(initializedState, false)
+    }
   }
-  const setSearchUrlQuery = query => setUrlQuery({
-    ...filterPaginationParams(rest),
-    ...objectFilter(query, key => query[key])
-  })
-  return [searchQuery, setSearchUrlQuery]
+})
+
+export function useQuery() {
+  return useRecoilValue(queryState)
 }
 
 /**
- * The React context object. Can be accessed from functional components with useContext.
+ * Hook for writing a query object to the query string.
+ *
+ * @returns {object} Object containing the search object.
  */
-export const searchContext = React.createContext()
+export function useUpdateQueryString() {
+  const history = useHistory()
+
+  const updateQueryString = useCallback((query, locked) => {
+    const queryString = queryToQs(query, locked)
+    history.replace(history.location.pathname + '?' + queryString)
+  }, [history])
+
+  return updateQueryString
+}
 
 /**
- * Component that provides a searchContext. Can be used with useContext. The context
- * objects provides access to the current search request and response as well as
- * callbacks to manipulate the current search request.
+ * Converts a query string into a valid query object.
  *
- * The search request is made from two objects: the request and the query. The former
- * contains all parameters that do not effect the search results themselves. This includes
- * pagination, statistics, order. The query object contains all parameters that
- * constitute the actual search. This includes the domain and owner parameters.
+ * @param {string} queryString URL querystring, encoded or not.
+ * @returns Returns an object containing the filters. Values are converted into
+ * datatypes that are directly compatible with the filter components.
  */
-export default function SearchContext({initialRequest, initialQuery, query, children}) {
-  const defaultStatistics = [] // ['atoms', 'authors'] TODO
-  const emptyResponse = useMemo(() => ({
-    statistics: {
-      total: {
-        all: {}
+function qsToQuery(queryString) {
+  const query = qs.parse(queryString, {comma: true})
+  const newQuery = {}
+  for (let [key, value] of Object.entries(query)) {
+    const split = key.split(':')
+    key = split[0]
+    let newKey = filterFullnames[key] || key
+    const valueGUI = toGUIFilter(newKey, value)
+    if (split.length !== 1) {
+      const op = split[1]
+      const oldValue = newQuery[newKey]
+      if (!oldValue) {
+        newQuery[newKey] = {[op]: valueGUI}
+      } else {
+        newQuery[newKey][op] = valueGUI
       }
-    },
-    pagination: {
-      total: undefined,
-      per_page: 10,
-      page: 1,
-      order: -1,
-      order_by: 'upload_time'
-    },
-    metric: domainData.dft.defaultSearchMetric
-  }), [])
-
-  const {api} = useContext(apiContext)
-  const {raiseError} = useContext(errorContext)
-
-  // React calls the children effects for the parent effect. But the parent effect is
-  // run with the state of the last render, which is the state before the children effects.
-  // If we would maintain the request in regular React state, we might execute unnecessary
-  // outdated requests.
-  // Therefore, we use two ref objects and one state object to manage the current state.
-  // The goal is to reduce the amounts of re-renders, only send requests to the api with
-  // the latest set parameters, and only send requests if necessary.
-  // The first ref keeps all information that will form the next
-  // search request that is send to the API. It therefore keeps the state of the current
-  // request provided by the various children of this context. It also helps us to
-  // lower the amount of state changes.
-  // The second ref keeps a hash over the last request that was send to the API.
-  // This is used to verify if a new request is actually necessary.
-  const requestRef = useRef({
-    metric: domainData.dft.defaultSearchMetric,
-    statistics: [],
-    groups: {},
-    domainKey: domainData.dft.key,
-    owner: 'all',
-    pagination: {
-      page: 1,
-      per_page: 10,
-      order: -1,
-      order_by: 'upload_time'
-    },
-    statisticsToRefresh: [],
-    query: {},
-    update: 0
-  })
-  const lastRequestHashRef = useRef(0)
-
-  // We use proper React state to maintain the last response from the API.
-  const [response, setResponse] = useState(emptyResponse)
-
-  // We use a custom hook to read/write search parameters from the current URL.
-  const [urlQuery, setUrlQuery] = useSearchUrlQuery()
-  // We set the current query. This will be used by an effect to potentially call the
-  // API after rendering.
-  requestRef.current.query = urlQuery
-
-  // This is a callback that executes the current request in requestRef without any
-  // checks for necessity. It will update the response state, once the request has
-  // been answered by the api.
-  const runRequest = useCallback(() => {
-    let dateHistogramInterval = null
-    const {metric, domainKey, owner, dateHistogram} = requestRef.current
-    const domain = domainData[domainKey]
-    const apiRequest = {
-      ...initialRequest,
-      ...requestRef.current.pagination,
-      statistics: requestRef.current.statistics,
-      ...requestRef.current.groups,
-      metrics: (metric === domain.defaultSearchMetric) ? [] : [metric],
-      domain: domain.key
-    }
-    const apiQuery = {
-      ...apiRequest,
-      owner: owner,
-      ...initialQuery,
-      ...requestRef.current.query,
-      ...query
-    }
-    if (dateHistogram) {
-      dateHistogramInterval = Dates.intervalSeconds(
-        apiQuery.from_time || Dates.dateHistogramStartDate,
-        apiQuery.until_time || new Date(), Dates.buckets)
-      apiQuery['date_histogram'] = true
-      apiQuery['interval'] = `${dateHistogramInterval}s`
-    }
-    api.search(apiQuery)
-      .then(newResponse => {
-        setResponse({
-          ...emptyResponse,
-          ...newResponse,
-          metric: metric,
-          dateHistogramInterval: dateHistogramInterval,
-          from_time: apiQuery.from_time,
-          until_time: apiQuery.until_time
-        })
-      }).catch(error => {
-        setResponse({...emptyResponse, metric: metric, error: error})
-        if (error.status !== 400) {
-          raiseError(error)
-        }
+    } else {
+      newQuery[newKey] = valueGUI
+    }
+  }
+  return newQuery
+}
+
+/**
+ * Converts a query into a valid query string.
+ * @param {object} query A query object representing the currently active
+ * filters.
+ * @returns URL querystring, not encoded if possible to improve readability.
+ */
+function queryToQs(query, locked) {
+  const newQuery = {}
+  for (const [key, value] of Object.entries(query)) {
+    if (locked[key]) {
+      continue
+    }
+    const {formatter} = formatMeta(key, false)
+    let newValue
+    const newKey = filterAbbreviations[key]
+    if (isPlainObject(value)) {
+      if (!isNil(value.gte)) {
+        newQuery[`${newKey}:gte`] = formatter(value.gte)
+      }
+      if (!isNil(value.lte)) {
+        newQuery[`${newKey}:lte`] = formatter(value.lte)
+      }
+    } else {
+      if (isArray(value)) {
+        newValue = value.map(formatter)
+      } else if (value instanceof Set) {
+        newValue = [...value].map(formatter)
+      } else {
+        newValue = formatter(value)
+      }
+      newQuery[newKey] = newValue
+    }
+  }
+  return qs.stringify(newQuery, {indices: false, encode: false})
+}
+
+export const initialAggsState = atom({
+  key: 'initialAggs',
+  default: undefined
+})
+
+/**
+ * Hook for returning an initial aggregation value for a filter.
+ *
+ * @returns {array} Array containing the aggregation data.
+ */
+export function useInitialAgg(name) {
+  const aggs = useRecoilValue(initialAggsState)
+  return aggs?.[name]
+}
+
+/**
+ * Hook for retrieving the most up-to-date aggregation results for a specific
+ * filter, taking into account the current search context.
+ *
+ * @param {string} name The filter name
+ * @param {bool} restrict If true, the ES query targeting this particular filter
+ * will be removed. This makes it possible to return all possible values for
+ * dropdowns etc.
+ * @param {bool} update Whether the hook needs to react to changes in the
+ * current query context. E.g. if the component showing the data is not visible,
+ * this can be set to false.
+ *
+ * @returns {array} The data-array returned by the API.
+ */
+export function useAgg(name, restrict = false, update = true, delay = 500) {
+  const {api} = useApi()
+  const { resource } = useSearchContext()
+  const [results, setResults] = useState(undefined)
+  const initialAggs = useRecoilValue(initialAggsState)
+  const query = useQuery()
+  const firstLoad = useRef(true)
+
+  // Pretty much all of the required pre-processing etc. should be done in this
+  // function, as it is the final one that gets called after the debounce
+  // interval.
+  const apiCall = useCallback((query) => {
+    // If the restrict option is enabled, the filters targeting the specified
+    // quantity will be removed. This way all possible options pre-selection can
+    // be returned.
+    let queryCleaned = {...query}
+    if (restrict && query && name in query) {
+      delete queryCleaned[name]
+    }
+    queryCleaned = toAPIQuery(queryCleaned, resource, query.restricted)
+    const aggRequest = {}
+    toAPIAgg(aggRequest, name, resource)
+    const search = {
+      owner: query.visibility || 'visible',
+      query: queryCleaned,
+      aggregations: aggRequest,
+      pagination: {page_size: 0},
+      required: { include: [] }
+    }
+
+    api.query(resource, search, false)
+      .then(data => {
+        data = toGUIAgg(data.aggregations, [name], resource)
+        firstLoad.current = false
+        setResults(data[name])
       })
-  }, [requestRef, setResponse, api, raiseError, emptyResponse, initialQuery, initialRequest, query])
-
-  // The following are various callbacks that can be used by children to update the
-  // request and implicitly trigger a search request to the API. The implicit triggering
-  // is realised that all changes to the request are accompanied by updates to the URL
-  // which is used to hold the whole request state. Each push to the history will rerender
-  // everything and therefore trigger effects.
-  const setRequestParameters = useCallback(
-    changes => {
-      requestRef.current.pagination = {
-        ...requestRef.current.pagination,
-        ...changes
+  }, [api, name, restrict, resource])
+
+  // This is a debounced version of apiCall.
+  const debounced = useCallback(debounce(apiCall, delay), [])
+
+  // The API call is made immediately on first render. On subsequent renders it
+  // will be debounced.
+  useEffect(() => {
+    if (!update || query === undefined) {
+      return
+    }
+    if (firstLoad.current) {
+      // Fetch the initial aggregation values if no query
+      // is specified.
+      if (isEmpty(query)) {
+        setResults(initialAggs[name])
+      // Make an immediate request for the aggregation values if query has been
+      // specified.
+      } else {
+        apiCall(query)
+      }
+    } else {
+      debounced(query)
+    }
+  }, [apiCall, name, debounced, query, update, initialAggs])
+
+  return results
+}
+
+/**
+ * Hook for returning a set of results based on the currently set query together
+ * with a function for retrieving a new set of results.
+ *
+ * @param {int} pageSize The number of results to return with one scroll.
+ * @param {string} orderBy The field used for sorting.
+ * @param {string} order Ascending or descending order.
+ * @param {number} delay The debounce delay in milliseconds.
+ *
+ * @returns {object} Object containing the search results and a function for
+ * scrolling to next set of results.
+ */
+export function useScrollResults(pageSize, orderBy, order, delay = 500) {
+  const {api} = useApi()
+  const {resource} = useSearchContext()
+  const firstRender = useRef(true)
+  const [results, setResults] = useState()
+  const pageNumber = useRef(1)
+  const query = useQuery(true)
+  const locked = useRecoilValue(lockedState)
+  const updateQueryString = useUpdateQueryString()
+  const pageAfterValue = useRef()
+  const searchRef = useRef()
+  const loading = useRef(false)
+  const total = useRef(0)
+
+  // The results are fetched as a side effect in order to not block the
+  // rendering. This causes two renders: first one without the data, the second
+  // one with the data.
+  const apiCall = useCallback((query, locked, pageSize, orderBy, order) => {
+    pageAfterValue.current = undefined
+    const restricted = query.restricted
+    const cleanedQuery = toAPIQuery(query, resource, restricted)
+    const search = {
+      owner: query.visibility || 'visible',
+      query: cleanedQuery,
+      pagination: {
+        page_size: pageSize,
+        order_by: orderBy,
+        order: order,
+        page_after_value: pageAfterValue.current
       }
-    }, [requestRef])
+    }
+    searchRef.current = search
+
+    loading.current = true
+    api.query(resource, search)
+      .then(data => {
+        pageAfterValue.current = data.pagination.next_page_after_value
+        total.current = data.pagination.total
+        setResults(data)
+        loading.current = false
+      })
 
-  const setDomain = useCallback(domainKey => {
-    requestRef.current.domainKey = domainKey || domainData.dft.key
-  }, [requestRef])
+    // We only update the query string after the API call is finished. Updating
+    // the query string causes quite an intensive render (not sure why), so it
+    // is better to debounce this value as well to keep the user interaction
+    // smoother.
+    updateQueryString(query, locked)
+  }, [resource, api, updateQueryString])
 
-  const setOwner = useCallback(owner => {
-    requestRef.current.owner = owner
-  }, [requestRef])
+  // This is a debounced version of apiCall.
+  const debounced = useCallback(debounce(apiCall, delay), [])
 
-  const setMetric = useCallback(metric => {
-    requestRef.current.metric = metric || domainData.dft.defaultSearchMetric
-  }, [requestRef])
+  // Used to load the next bath of results
+  const next = useCallback(() => {
+    if (loading.current) {
+      return
+    }
+    pageNumber.current += 1
+    searchRef.current.pagination.page_after_value = pageAfterValue.current
+    loading.current = true
+    api.query(resource, searchRef.current)
+      .then(data => {
+        pageAfterValue.current = data.pagination.next_page_after_value
+        total.current = data.pagination.total
+        setResults(old => {
+          data.data = old.data.concat(data.data)
+          return data
+        })
+        loading.current = false
+      })
+  }, [api, resource])
 
-  const setStatistics = useCallback(statistics => {
-    requestRef.current.statistics = [...statistics, ...defaultStatistics].filter(onlyUnique)
-    // eslint-disable-next-line
-  }, [requestRef])
+  // Whenever the query changes, we make a new query that resets pagination and
+  // shows the first batch of results.
+  useEffect(() => {
+    // If the initial query is not yet ready, do nothing
+    if (query === undefined) {
+      return
+    }
+    if (firstRender.current) {
+      apiCall(query, locked, pageSize, orderBy, order)
+      firstRender.current = false
+    } else {
+      debounced(query, locked, pageSize, orderBy, order)
+    }
+  }, [apiCall, debounced, query, locked, pageSize, order, orderBy])
 
-  const setGroups = useCallback(groups => {
-    requestRef.current.groups = {...groups}
-  }, [requestRef])
+  // Whenever the ordering changes, we perform a single API call that fetches
+  // results in the new order. The amount of fetched results is based on the
+  // already loaded amount.
+  // TODO
+  return {
+    results: results,
+    next: next,
+    page: pageNumber.current,
+    total: total.current
+  }
+}
 
-  const setDateHistogram = useCallback(dateHistogram => {
-    requestRef.current.dateHistogram = dateHistogram
-  }, [requestRef])
+/**
+ * Converts the contents a query into a format that is suitable for the API.
+ *
+ * Should only be called when making the final API call, as during the
+ * construction of the query it is much more convenient to store filters within
+ * e.g. Sets.
+ *
+ * @param {number} query The query object
+ * @param {bool} exclusive The chemical element search mode.
+ *
+ * @returns {object} A copy of the object with certain items cleaned into a
+ * format that is supported by the API.
+ */
+export function toAPIQuery(query, resource, restricted) {
+  // Perform custom transformations
+  let queryCustomized = {}
+  for (let [k, v] of Object.entries(query)) {
+    const setter = filterData[k]?.valueSet
+    if (setter) {
+      setter(queryCustomized, query, v)
+    } else {
+      queryCustomized[k] = v
+    }
+  }
 
-  const handleQueryChange = (changes, replace) => {
-    if (changes.atoms && changes.atoms.length === 0) {
-      changes.atoms = undefined
+  let queryNormalized = {}
+  for (const [k, v] of Object.entries(queryCustomized)) {
+    // Transform sets into lists and Quantities into SI values and modify keys
+    // according to target resource (entries/materials).
+    let newValue
+    if (isPlainObject(v)) {
+      newValue = {}
+      if (!isNil(v.lte)) {
+        newValue.lte = toAPIQueryValue(v.lte)
+      }
+      if (!isNil(v.gte)) {
+        newValue.gte = toAPIQueryValue(v.gte)
+      }
+    } else {
+      newValue = toAPIQueryValue(v)
     }
-    if (changes.only_atoms && changes.only_atoms.length === 0) {
-      changes.only_atoms = undefined
+
+    // The postfixes are added here. By default query items with array values
+    // get the 'any'-postfix.
+    let postfix
+    if (isArray(newValue)) {
+      const fieldPostfixMap = {
+        'results.properties.available_properties': 'all',
+        'results.material.elements': 'all'
+      }
+      postfix = fieldPostfixMap[k] || 'any'
     }
 
-    if (replace) {
-      setUrlQuery(changes)
+    // For material query the keys are remapped.
+    let newKey = resource === 'materials' ? materialNames[k] : k
+    newKey = postfix ? `${newKey}:${postfix}` : newKey
+    queryNormalized[newKey] = newValue
+  }
+
+  if (resource === 'materials') {
+    // In restricted search we simply move all method/properties filters
+    // inside a single entries-subsection.
+    if (restricted) {
+      const entrySearch = {}
+      for (const [k, v] of Object.entries(queryNormalized)) {
+        if (k.startsWith('entries.')) {
+          const name = k.split('entries.').pop()
+          entrySearch[name] = v
+          delete queryNormalized[k]
+        }
+      }
+      if (!isEmpty(entrySearch)) {
+        queryNormalized.entries = entrySearch
+      }
+    // In unrestricted search we have to split each filter and each filter value
+    // into it's own separate entries query. These queries are then joined with
+    // 'and'.
     } else {
-      setUrlQuery({...urlQuery, ...changes})
+      const entrySearch = []
+      for (const [k, v] of Object.entries(queryNormalized)) {
+        if (k.startsWith('entries.')) {
+          const newKey = k.split(':')[0]
+          if (isArray(v)) {
+            for (const item of v) {
+              entrySearch.push({[newKey]: item})
+            }
+          } else {
+            entrySearch.push({[newKey]: v})
+          }
+          delete queryNormalized[k]
+        }
+      }
+      if (entrySearch.length > 0) {
+        queryNormalized.and = entrySearch
+      }
     }
   }
 
-  // We check and run (if necessary) the search request after each render
-  useEffect(() => {
-    if (lastRequestHashRef.current !== hash(requestRef.current)) {
-      runRequest()
-      lastRequestHashRef.current = hash(requestRef.current)
+  return queryNormalized
+}
+
+/**
+ * Cleans a filter value into a form that is supported by the API. This includes:
+ * - Sets are transformed into Arrays
+ * - Quantities are converted to SI values.
+ *
+ * @returns {any} The filter value in a format that is suitable for the API.
+ */
+function toAPIQueryValue(value) {
+  let newValue
+  if (value instanceof Set) {
+    newValue = setToArray(value)
+    if (newValue.length === 0) {
+      newValue = undefined
+    } else {
+      newValue = newValue.map((item) => item instanceof Quantity ? item.toSI() : item)
+    }
+  } else if (value instanceof Quantity) {
+    newValue = value.toSI()
+  } else if (isArray(value)) {
+    if (value.length === 0) {
+      newValue = undefined
+    } else {
+      newValue = value.map((item) => item instanceof Quantity ? item.toSI() : item)
     }
-  })
+  } else {
+    newValue = value
+  }
+  return newValue
+}
 
-  const value = {
-    response: response,
-    query: {
-      ...requestRef.current.query
-    },
-    apiQuery: {
-      domain: requestRef.current.domainKey,
-      owner: requestRef.current.owner,
-      ...requestRef.current.query,
-      ...query
-    },
-    domain: domainData[requestRef.current.domainKey],
-    metric: requestRef.current.metric,
-    requestParameters: requestRef.current.pagination,
-    setRequestParameters: setRequestParameters,
-    setQuery: handleQueryChange,
-    setMetric: setMetric,
-    setGroups: setGroups,
-    setDomain: setDomain,
-    setOwner: setOwner,
-    setStatistics: setStatistics,
-    setDateHistogram: setDateHistogram,
-    update: runRequest
+/**
+ * Cleans a filter value into a form that is supported by the GUI. This includes:
+ * - Arrays are are transformed into Sets
+ * - If multiple values are supported, scalar values are stored inside sets.
+ * - Numerical values with units are transformed into Quantities.
+ *
+ * @returns {any} The filter value in a format that is suitable for the GUI.
+ */
+export function toGUIFilter(name, value, units = undefined) {
+  let multiple = filterData[name].multiple
+  let newValue
+  const {parser} = parseMeta(name)
+  if (isArray(value)) {
+    newValue = new Set(value.map((v) => parser(v, units)))
+  } else if (isPlainObject(value)) {
+    newValue = {}
+    if (!isNil(value.gte)) {
+      newValue.gte = parser(value.gte, units)
+    }
+    if (!isNil(value.lte)) {
+      newValue.lte = parser(value.lte, units)
+    }
+  } else {
+    newValue = parser(value, units)
+    if (multiple) {
+      newValue = new Set([newValue])
+    }
   }
+  return newValue
+}
 
-  return <searchContext.Provider value={value} >{children}</searchContext.Provider>
+/**
+ * Used to transform a GUI aggregation query into a form that is usable by the
+ * API.
+ *
+ * @param {object} aggs The aggregation data in which the modifications are
+ * made.
+ * @param {string} filter The filter name
+ * @param {string} resource The resource we are looking at: entries or materials.
+ */
+function toAPIAgg(aggs, filter, resource) {
+  const aggSet = filterData[filter].aggSet
+  if (aggSet) {
+    for (const [key, type] of Object.entries(aggSet)) {
+      const name = resource === 'materials' ? materialNames[key.split(':')[0]] : key
+      const agg = aggs[name] || {}
+      agg[type] = {
+        quantity: name,
+        size: 500
+      }
+      aggs[name] = agg
+    }
+  }
 }
-SearchContext.propTypes = {
-  /**
-   * An object with initial query parameters. These will be added to the search context
-   * and be used in all search requests.
-  */
-  query: PropTypes.object,
-  /**
-   * An object with initial query parameters. These will be added to the search context
-   * and the first search request. Afterwards search parameters might be removed or
-   * overwritten by the search.
-   */
-  initialQuery: PropTypes.object,
-  /**
-   * An object with initial request parameters. These will be added to the search context
-   * and the first search request. Afterwards request parameters might be removed or
-   * overwritten by children components.
-   */
-  initialRequest: PropTypes.object,
-  /**
-   * The children prop. All components in the children can make use of this search
-   * context via useContext.
-   */
-  children: PropTypes.any
+
+/**
+ * Used to transform an API aggregation query into a form that is usable by the
+ * GUI.
+ *
+ * @param {object} aggs The aggregation data as returned by the API.
+ * @param {array} filters The filters to take into account.
+ * @param {string} resource The resource we are looking at: entries or materials.
+ *
+ * @returns {object} Aggregation data that is usable by the GUI.
+ */
+function toGUIAgg(aggs, filters, resource) {
+  if (isEmpty(aggs)) {
+    return aggs
+  }
+  // Modify keys according to target resource (entries/materials).
+  let aggsNormalized
+  if (resource === 'materials') {
+    aggsNormalized = {}
+    for (const key of Object.keys(aggs)) {
+      const name = resource === 'materials' ? entryNames[key] : key
+      aggs[key].quantity = name
+      aggsNormalized[name] = aggs[key]
+    }
+  } else {
+    aggsNormalized = aggs
+  }
+
+  // Perform custom transformations
+  const aggsCustomized = {}
+  for (const name of filters) {
+    const aggGet = filterData[name].aggGet
+    if (aggGet) {
+      let agg
+      agg = aggGet(aggsNormalized)
+      aggsCustomized[name] = agg
+    }
+  }
+  return aggsCustomized
 }
diff --git a/gui/src/components/search/SearchPage.js b/gui/src/components/search/SearchPage.js
deleted file mode 100644
index 8c037f2993..0000000000
--- a/gui/src/components/search/SearchPage.js
+++ /dev/null
@@ -1,79 +0,0 @@
-/*
- * Copyright The NOMAD Authors.
- *
- * This file is part of NOMAD. See https://nomad-lab.eu for further info.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-import React, { useContext } from 'react'
-import { apiContext } from '../api'
-import Search from './Search'
-import { domainData } from '../domainData'
-import { encyclopediaEnabled } from '../../config'
-
-const help = `
-This page allows you to **search** in NOMAD's data. The upper part of this page
-gives you various options to enter and configure your search. The lower part
-shows all data that fulfills your search criteria.
-
-NOMAD's *domain-aware* search allows you to screen data by filtering based on
-desired properties. This is different from basic *text-search* that traditional
-search engines offer.
-
-The search bar allows you to specify various quantity values that you want to
-see in your results. This includes *authors*, *comments*, *atom labels*, *code name*,
-*system type*, *crystal system*, *basis set types*, and *XC functionals*.
-
-Alternatively, you can click *elements* or *metadata* to get a visual representation of
-NOMAD's data as a periodic table or metadata charts. You can click the various
-visualization elements to filter for respective quantities.
-
-The visual representations show metrics for all data that fit your criteria.
-You can display *entries* (i.e. code runs), *unique entries*, and *datasets*.
-Other more specific metrics might be available.
-
-Some quantities have no autocompletion for their values. You can still search for them,
-if you know exactly what you are looking for. To search for a particular entry by its id
-for example, type \`calc_id=<the_id>\` and press entry (or select the respective item from the menu).
-The usable *hidden* quantities are: ${Object.keys(domainData.dft.additionalSearchKeys).map(key => `\`${key}\``).join(', ')}.
-
-The results tabs gives you a quick overview of all entries and datasets that fit your search.
-You can click entries to see more details, download data, see the archive, etc. The *entries*
-tab displays individual entries (i.e. code runs), the *grouped entries* tab will group
-entries with similar metadata (it will group entries for the same material from the
-  same user). The *dataset* tab, shows entry curated by user created datasets. You can
-  click on datasets for a search page that will only display entries from the respective
-  dataset.
-
-The table columns can be configured. The *entries* tab also supports sorting. Selected
-entries (or all entries) can be downloaded. The download will contain all user provided
-raw calculation input and output files.
-
-You can click entries to see more details about them. The details button will navigate
-you to an entry's page. This entry page will show more metadata, raw files, the
-entry's archive, and processing logs.
-`
-export {help}
-
-export default function SearchPage() {
-  const {user} = useContext(apiContext)
-  const withoutLogin = ['all', 'public']
-
-  return <Search
-    initialVisualizationTab="elements"
-    availableResultTabs={['entries', ...(encyclopediaEnabled ? ['materials'] : []), 'groups', 'datasets']}
-    initialOwner="public"
-    ownerTypes={['public', 'visible'].filter(key => user || withoutLogin.indexOf(key) !== -1)}
-    showDisclaimer
-  />
-}
diff --git a/gui/src/components/search/SearchPageEntries.js b/gui/src/components/search/SearchPageEntries.js
index 58644f1f2b..f681036ab1 100644
--- a/gui/src/components/search/SearchPageEntries.js
+++ b/gui/src/components/search/SearchPageEntries.js
@@ -16,8 +16,8 @@
  * limitations under the License.
  */
 import React from 'react'
-import NewSearch from './NewSearch'
-import { SearchContext } from './FilterContext'
+import Search from './Search'
+import { SearchContext } from './SearchContext'
 
 export const help = `
 This page allows you to **search** in NOMAD's data. NOMAD's *domain-aware*
@@ -53,7 +53,7 @@ will show more metadata, raw files, the entry's archive, and processing logs.
 
 const SearchPageEntries = React.memo(() => {
   return <SearchContext resource="entries">
-    <NewSearch/>
+    <Search/>
   </SearchContext>
 })
 
diff --git a/gui/src/components/search/SearchPageMaterials.js b/gui/src/components/search/SearchPageMaterials.js
index 2fe3df17a8..df5c46c7e3 100644
--- a/gui/src/components/search/SearchPageMaterials.js
+++ b/gui/src/components/search/SearchPageMaterials.js
@@ -16,8 +16,8 @@
  * limitations under the License.
  */
 import React from 'react'
-import NewSearch from './NewSearch'
-import { SearchContext } from './FilterContext'
+import Search from './Search'
+import { SearchContext } from './SearchContext'
 
 export const help = `
 This page allows you to **search** in NOMAD's data. NOMAD's *domain-aware*
@@ -53,7 +53,7 @@ will show more metadata, raw files, the entry's archive, and processing logs.
 
 const SearchPageMaterials = React.memo(() => {
   return <SearchContext resource="materials">
-    <NewSearch/>
+    <Search/>
   </SearchContext>
 })
 
diff --git a/gui/src/components/search/UploadsHistogram.js b/gui/src/components/search/UploadsHistogram.js
deleted file mode 100644
index a732636562..0000000000
--- a/gui/src/components/search/UploadsHistogram.js
+++ /dev/null
@@ -1,300 +0,0 @@
-/*
- * Copyright The NOMAD Authors.
- *
- * This file is part of NOMAD. See https://nomad-lab.eu for further info.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-import React, { useContext, useState, useEffect, useRef, useLayoutEffect, useCallback } from 'react'
-import PropTypes from 'prop-types'
-import { Select, MenuItem, Card, CardHeader, CardContent, makeStyles } from '@material-ui/core'
-import Grid from '@material-ui/core/Grid'
-import TextField from '@material-ui/core/TextField'
-import * as d3 from 'd3'
-import { scaleTime, scalePow } from 'd3-scale'
-import { nomadSecondaryColor, nomadTheme } from '../../config.js'
-import { searchContext, Dates } from './SearchContext'
-
-const useStyles = makeStyles(theme => ({
-  root: {
-    marginTop: theme.spacing(2)
-  },
-  header: {
-    paddingBottom: 0
-  },
-  content: {
-    paddingTop: 0,
-    position: 'relative',
-    height: 250
-  },
-  tooltip: {
-    textAlign: 'center',
-    position: 'absolute',
-    pointerEvents: 'none',
-    opacity: 0
-  },
-  tooltipContent: {
-    // copy of the material ui popper style
-    display: 'inline-block',
-    color: '#fff',
-    padding: '4px 8px',
-    fontSize: nomadTheme.overrides.MuiTooltip.tooltip.fontSize,
-    fontWeight: nomadTheme.overrides.MuiTooltip.tooltip.fontWeight,
-    fontFamily: '"Roboto", "Helvetica", "Arial", sans-serif',
-    lineHeight: '1.4em',
-    borderRadius: '4px',
-    backgroundColor: '#616161'
-  }
-}))
-
-export default function UploadsHistogram({title = 'Uploads over time', initialScale = 1, tooltips}) {
-  const classes = useStyles()
-  const containerRef = useRef()
-  const fromTimeFieldRef = useRef()
-  const untilTimeFieldRef = useRef()
-  const [scale, setScale] = useState(initialScale)
-  const {response, query, setQuery, domain, setDateHistogram} = useContext(searchContext)
-
-  useEffect(() => {
-    setDateHistogram(true)
-    return () => {
-      setDateHistogram(false)
-    }
-  }, [setDateHistogram])
-
-  useLayoutEffect(() => {
-    fromTimeFieldRef.current.value = Dates.FormDate(query.from_time || Dates.dateHistogramStartDate)
-    untilTimeFieldRef.current.value = Dates.FormDate(query.until_time || new Date())
-  })
-
-  useEffect(() => {
-    const {statistics, metric} = response
-
-    let data = []
-    if (!statistics.date_histogram) {
-      return
-    } else {
-      data = Object.keys(statistics.date_histogram).map(key => ({
-        time: Dates.JSDate(parseInt(key)),
-        value: statistics.date_histogram[key][metric]
-      })).filter(d => d.value)
-    }
-
-    const fromTime = Dates.JSDate(response.from_time || Dates.dateHistogramStartDate)
-    const untilTime = Dates.JSDate(response.until_time || new Date())
-    const interval = response.dateHistogramInterval
-    const clickable = (interval * Dates.buckets) > 3600
-
-    const handleItemClicked = item => {
-      if (!clickable) {
-        return
-      }
-      const fromTime = item.time
-      const untilTime = Dates.addSeconds(fromTime, interval)
-      setQuery({
-        from_time: Dates.APIDate(fromTime),
-        until_time: Dates.APIDate(untilTime)
-      })
-    }
-
-    const width = containerRef.current.offsetWidth
-    const height = 250
-    const marginRight = 32
-    const marginTop = 16
-    const marginBottom = 17 // 16 misses a pixel in safari
-
-    const y = scalePow().range([height - marginBottom, marginTop]).exponent(scale)
-    const max = d3.max(data, d => d.value) || 0
-    y.domain([0, max])
-
-    const x = scaleTime()
-      .domain([Dates.addSeconds(fromTime, -interval), Dates.addSeconds(untilTime, interval)])
-      .rangeRound([marginRight, width])
-
-    const container = d3.select(containerRef.current)
-    const tooltip = container.select('.' + classes.tooltip)
-      .style('opacity', 0)
-    const tooltipContent = container.select('.' + classes.tooltipContent)
-    const svg = container.select('svg')
-      .attr('width', width)
-      .attr('height', height)
-
-    const xAxis = d3.axisBottom(x)
-    svg.select('.xaxis').remove()
-    svg.append('g')
-      .attr('transform', `translate(0,${height - marginBottom})`)
-      .attr('class', 'xaxis')
-      .call(xAxis)
-
-    svg.select('.xlabel').remove()
-    svg.append('text')
-      .attr('class', 'xlabel')
-      .attr('x', width)
-      .attr('y', height - 4)
-      .attr('dy', '.35em')
-      .attr('font-size', '12px')
-      .style('text-anchor', 'end')
-
-    const yAxis = d3.axisLeft(y).ticks(Math.min(max, 5), '.0s')
-    svg.select('.yaxis').remove()
-    svg.append('g')
-      .attr('transform', `translate(${marginRight}, 0)`)
-      .attr('class', 'yaxis')
-      .call(yAxis)
-
-    const {label, shortLabel} = domain.searchMetrics[metric]
-
-    let withData = svg
-      .selectAll('.bar').remove().exit()
-      .data(data)
-
-    let item = withData.enter()
-      .append('g')
-
-    item
-      .append('rect')
-      .attr('x', d => x(d.time) + 1)
-      .attr('y', y(max))
-      .attr('width', d => x(Dates.addSeconds(d.time, interval)) - x(d.time) - 2)
-      .attr('class', 'background')
-      .style('opacity', 0)
-      .attr('height', y(0) - y(max))
-
-    item
-      .append('rect')
-      .attr('class', 'bar')
-      .attr('x', d => x(d.time) + 1)
-      .attr('y', d => y(d.value))
-      .attr('width', d => x(Dates.addSeconds(d.time, interval)) - x(d.time) - 2)
-      .attr('height', d => y(0) - y(d.value))
-      .style('fill', nomadSecondaryColor.light)
-
-    if (clickable) {
-      item
-        .style('cursor', 'pointer')
-        .on('click', handleItemClicked)
-    }
-
-    item
-      .on('mouseover', function(d) {
-        d3.select(this).select('.background')
-          .style('opacity', 0.08)
-        if (tooltips) {
-          tooltip.transition()
-            .duration(200)
-            .style('opacity', 1)
-          tooltip
-            .style('left', x(d.time) + 'px')
-            .style('bottom', '24px')
-          tooltipContent.html(
-            `${d.time.toLocaleDateString()}-${Dates.addSeconds(d.time, interval).toLocaleDateString()} with ${d.value.toLocaleString()} ${shortLabel || label}`)
-        }
-      })
-      .on('mouseout', function(d) {
-        d3.select(this).select('.background')
-          .style('opacity', 0)
-        if (tooltips) {
-          tooltip.transition()
-            .duration(200)
-            .style('opacity', 0)
-        }
-      })
-  })
-
-  const handleDatePickerChange = useCallback((event, key) => {
-    try {
-      const date = new Date(event.target.value).getTime()
-      if (date < Dates.JSDate(Dates.dateHistogramStartDate).getTime()) {
-        return
-      }
-      if (date > new Date().getTime()) {
-        return
-      }
-      const value = Dates.APIDate(new Date(event.target.value))
-      setQuery({[key]: value})
-    } catch (error) {
-    }
-  }, [setQuery])
-
-  return <Card classes={{root: classes.root}}>
-    <CardHeader
-      classes={{root: classes.header}}
-      title={title}
-      titleTypographyProps={{variant: 'body1'}}
-      action={(
-        <Grid container alignItems='flex-end' style={{flexWrap: 'nowrap'}} spacing={2}>
-          <Grid item>
-            <TextField
-              inputRef={fromTimeFieldRef}
-              label="from time"
-              type="date"
-              defaultValue={Dates.FormDate(query.from_time || Dates.dateHistogramStartDate)}
-              onChange={event => handleDatePickerChange(event, 'from_time')}
-              InputLabelProps={{
-                shrink: true
-              }}
-            />
-          </Grid>
-          <Grid item>
-            <TextField
-              inputRef={untilTimeFieldRef}
-              label="until time"
-              type="date"
-              defaultValue={Dates.FormDate(query.until_time || new Date())}
-              onChange={event => handleDatePickerChange(event, 'until_time')}
-              InputLabelProps={{
-                shrink: true
-              }}
-            />
-          </Grid>
-          <Grid item>
-            <Select
-              value={scale}
-              onChange={(event) => setScale(event.target.value)}
-              displayEmpty
-              name="scale power"
-            >
-              <MenuItem value={1}>linear</MenuItem>
-              <MenuItem value={0.5}>1/2</MenuItem>
-              <MenuItem value={0.25}>1/4</MenuItem>
-              <MenuItem value={0.125}>1/8</MenuItem>
-            </Select>
-          </Grid>
-        </Grid>
-      )}
-    />
-    <CardContent classes={{root: classes.content}}>
-      <div ref={containerRef}>
-        <div className={classes.tooltip}>
-          <div className={classes.tooltipContent}></div>
-        </div>
-        <svg />
-      </div>
-    </CardContent>
-  </Card>
-}
-UploadsHistogram.propTypes = {
-  /**
-   * An optional title for the chart. If no title is given, the quantity is used.
-   */
-  title: PropTypes.string,
-  /**
-   * An optional scale power that is used as the initial scale before the user
-   * changes it. Default is 1 (linear scale).
-   */
-  initialScale: PropTypes.number,
-  /**
-   * Set to true to enable tooltips for each value.
-   */
-  tooltips: PropTypes.bool
-}
diff --git a/gui/src/components/search/input/InputCheckbox.js b/gui/src/components/search/input/InputCheckbox.js
index 17eb287b5c..08d38ecd32 100644
--- a/gui/src/components/search/input/InputCheckbox.js
+++ b/gui/src/components/search/input/InputCheckbox.js
@@ -26,7 +26,7 @@ import {
 import PropTypes from 'prop-types'
 import clsx from 'clsx'
 import searchQuantities from '../../../searchQuantities'
-import { useFilterState, useFilterLocked } from '../FilterContext'
+import { useFilterState, useFilterLocked } from '../SearchContext'
 
 const useStyles = makeStyles(theme => ({
   root: {
diff --git a/gui/src/components/search/input/InputCheckboxes.js b/gui/src/components/search/input/InputCheckboxes.js
index 7d66044ff5..1bde83bd5c 100644
--- a/gui/src/components/search/input/InputCheckboxes.js
+++ b/gui/src/components/search/input/InputCheckboxes.js
@@ -32,7 +32,7 @@ import {
   useAgg,
   useInitialAgg,
   useFilterLocked
-} from '../FilterContext'
+} from '../SearchContext'
 import { isArray } from 'lodash'
 
 const useStyles = makeStyles(theme => ({
diff --git a/gui/src/components/search/input/InputDateRange.js b/gui/src/components/search/input/InputDateRange.js
index ecd09a5f3f..b8854b62b4 100644
--- a/gui/src/components/search/input/InputDateRange.js
+++ b/gui/src/components/search/input/InputDateRange.js
@@ -27,7 +27,7 @@ import { isNil } from 'lodash'
 import searchQuantities from '../../../searchQuantities'
 import InputLabel from './InputLabel'
 import InputTooltip from './InputTooltip'
-import { useAgg, useFilterState, useFilterLocked } from '../FilterContext'
+import { useAgg, useFilterState, useFilterLocked } from '../SearchContext'
 import { getTime } from 'date-fns'
 import { dateFormat } from '../../../config'
 
diff --git a/gui/src/components/search/input/InputPeriodicTable.js b/gui/src/components/search/input/InputPeriodicTable.js
index 42fb63e028..dbab24a31f 100644
--- a/gui/src/components/search/input/InputPeriodicTable.js
+++ b/gui/src/components/search/input/InputPeriodicTable.js
@@ -17,7 +17,7 @@
  */
 import React, { useCallback } from 'react'
 import PropTypes from 'prop-types'
-import periodicTableData from './PeriodicTableData'
+import elementData from '../../../elementData'
 import {
   Typography,
   Button,
@@ -30,7 +30,7 @@ const elements = []
 for (var i = 0; i < 10; i++) {
   elements[i] = Array.apply(null, Array(18))
 }
-periodicTableData.elements.forEach(element => {
+elementData.elements.forEach(element => {
   elements[element.ypos - 1][element.xpos - 1] = element
   element.category = element.category.replace(' ', '')
 })
diff --git a/gui/src/components/search/input/InputRadio.js b/gui/src/components/search/input/InputRadio.js
index 62f31450f4..cc80c2354f 100644
--- a/gui/src/components/search/input/InputRadio.js
+++ b/gui/src/components/search/input/InputRadio.js
@@ -28,7 +28,7 @@ import PropTypes from 'prop-types'
 import clsx from 'clsx'
 import InputLabel from './InputLabel'
 import searchQuantities from '../../../searchQuantities'
-import { useFilterState, useFilterLocked } from '../FilterContext'
+import { useFilterState, useFilterLocked } from '../SearchContext'
 
 const useStyles = makeStyles(theme => ({
   root: {
diff --git a/gui/src/components/search/input/InputSelect.js b/gui/src/components/search/input/InputSelect.js
index 7d4904cac0..bff291adc6 100644
--- a/gui/src/components/search/input/InputSelect.js
+++ b/gui/src/components/search/input/InputSelect.js
@@ -31,7 +31,7 @@ import FilterChip from '../FilterChip'
 import searchQuantities from '../../../searchQuantities'
 import InputLabel from './InputLabel'
 import InputTooltip from './InputTooltip'
-import { useFilterState, useFilterLocked, useAgg } from '../FilterContext'
+import { useFilterState, useFilterLocked, useAgg } from '../SearchContext'
 
 // This forces the menu to have a fixed anchor instead of jumping around
 const MenuProps = {
diff --git a/gui/src/components/search/input/InputSlider.js b/gui/src/components/search/input/InputSlider.js
index ca9ad855ad..ac9532082e 100644
--- a/gui/src/components/search/input/InputSlider.js
+++ b/gui/src/components/search/input/InputSlider.js
@@ -31,7 +31,7 @@ import InputTooltip from './InputTooltip'
 import { Quantity, Unit, toUnitSystem, toSI } from '../../../units'
 import { formatNumber } from '../../../utils'
 import searchQuantities from '../../../searchQuantities'
-import { useFilterState, useFilterLocked, useAgg } from '../FilterContext'
+import { useFilterState, useFilterLocked, useAgg } from '../SearchContext'
 
 function format(value) {
   return formatNumber(value, 'float', 6, true)
diff --git a/gui/src/components/search/input/InputText.js b/gui/src/components/search/input/InputText.js
index 4e13b3047c..5c2b5a6b60 100644
--- a/gui/src/components/search/input/InputText.js
+++ b/gui/src/components/search/input/InputText.js
@@ -33,7 +33,7 @@ import { useApi } from '../../apiV1'
 import searchQuantities from '../../../searchQuantities'
 import InputLabel from './InputLabel'
 import InputTooltip from './InputTooltip'
-import { useSetFilter, useFilterLocked } from '../FilterContext'
+import { useSetFilter, useFilterLocked } from '../SearchContext'
 
 const useStyles = makeStyles(theme => ({
   root: {
diff --git a/gui/src/components/search/input/PeriodicTable.js b/gui/src/components/search/input/PeriodicTable.js
deleted file mode 100644
index 127b2d82a7..0000000000
--- a/gui/src/components/search/input/PeriodicTable.js
+++ /dev/null
@@ -1,230 +0,0 @@
-/*
- * Copyright The NOMAD Authors.
- *
- * This file is part of NOMAD. See https://nomad-lab.eu for further info.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-import React from 'react'
-import PropTypes from 'prop-types'
-import periodicTableData from './PeriodicTableData'
-import { withStyles, Typography, Button, Tooltip, FormControlLabel, Checkbox } from '@material-ui/core'
-import chroma from 'chroma-js'
-import { nomadSecondaryColor } from '../../../config.js'
-
-const elements = []
-for (var i = 0; i < 10; i++) {
-  elements[i] = Array.apply(null, Array(18))
-}
-periodicTableData.elements.forEach(element => {
-  elements[element.ypos - 1][element.xpos - 1] = element
-  element.category = element.category.replace(' ', '')
-})
-
-class ElementUnstyled extends React.Component {
-  static propTypes = {
-    classes: PropTypes.object.isRequired,
-    element: PropTypes.object.isRequired,
-    onClick: PropTypes.func,
-    selected: PropTypes.bool,
-    count: PropTypes.number.isRequired,
-    heatmapScale: PropTypes.func.isRequired
-  }
-
-  static styles = theme => ({
-    root: {
-      position: 'relative'
-    },
-    button: {
-      border: '1px solid',
-      paddingTop: theme.spacing(1),
-      paddingBottom: theme.spacing(1),
-      paddingLeft: 0,
-      paddingRight: 0,
-      width: '100%',
-      textAlign: 'center',
-      fontSize: '1rem',
-      fontWeight: 700,
-      textTransform: 'none',
-      minWidth: 0,
-      minHeight: 0,
-      borderRadius: 0,
-      boxShadow: 'none'
-    },
-    containedPrimary: {
-      backgroundColor: theme.palette.primary.dark,
-      color: 'white'
-    },
-    number: {
-      position: 'absolute',
-      top: 2,
-      left: 2,
-      margin: 0,
-      padding: 0,
-      fontSize: 8,
-      pointerEvents: 'none'
-    },
-    count: {
-      position: 'absolute',
-      bottom: 2,
-      right: 2,
-      margin: 0,
-      padding: 0,
-      fontSize: 8,
-      pointerEvents: 'none'
-    }
-  })
-
-  render() {
-    const {classes, element, selected, count, heatmapScale} = this.props
-    const buttonClasses = {
-      root: classes.button,
-      containedPrimary: classes.containedPrimary
-    }
-    const disabled = count <= 0
-
-    const style = (count > 0) ? {
-      backgroundColor: !selected ? heatmapScale(count).hex() : undefined,
-      borderColor: '#555'
-    } : undefined
-
-    return (
-      <div className={classes.root}>
-        <Tooltip title={element.name}>
-          <div>
-            <Button
-              disabled={disabled}
-              classes={buttonClasses}
-              style={style}
-              onClick={this.props.onClick} variant="contained"
-              color={selected ? 'primary' : 'default'}
-            >
-              {element.symbol}
-            </Button>
-          </div>
-        </Tooltip>
-        <Typography
-          classes={{root: classes.number}} variant="caption"
-          style={selected ? {color: 'white'} : disabled ? {color: '#BDBDBD'} : {}}>
-          {element.number}
-        </Typography>
-        {count >= 0
-          ? <Typography
-            classes={{root: classes.count}} variant="caption"
-            style={selected ? {color: 'white'} : disabled ? {color: '#BDBDBD'} : {}}>
-            {count.toLocaleString()}
-          </Typography> : ''
-        }
-      </div>
-    )
-  }
-}
-
-const Element = withStyles(ElementUnstyled.styles)(ElementUnstyled)
-
-class PeriodicTable extends React.Component {
-  static propTypes = {
-    classes: PropTypes.object.isRequired,
-    aggregations: PropTypes.object,
-    metric: PropTypes.string.isRequired,
-    values: PropTypes.array.isRequired,
-    onChanged: PropTypes.func.isRequired,
-    exclusive: PropTypes.bool,
-    onExclusiveChanged: PropTypes.func.isRequired
-  }
-
-  static styles = theme => ({
-    root: {
-      position: 'relative'
-    },
-    table: {
-      margin: 'auto',
-      width: '100%',
-      minWidth: 500,
-      maxWidth: 900,
-      tableLayout: 'fixed',
-      borderSpacing: theme.spacing(0.5)
-    },
-    formContainer: {
-      position: 'absolute',
-      top: theme.spacing(0),
-      left: '10%',
-      textAlign: 'center'
-    }
-  })
-
-  onElementClicked(element) {
-    const index = this.props.values.indexOf(element)
-    const isClicked = index >= 0
-    let selected
-    if (isClicked) {
-      selected = [...this.props.values]
-      selected.splice(index, 1)
-    } else {
-      selected = [element, ...this.props.values]
-    }
-
-    this.props.onChanged(selected)
-  }
-
-  unSelectedAggregations() {
-    const { aggregations, metric, values } = this.props
-    return Object.keys(aggregations)
-      .filter(key => values.indexOf(key) === -1)
-      .map(key => aggregations[key][metric])
-  }
-
-  render() {
-    const {classes, aggregations, metric, values, exclusive, onExclusiveChanged} = this.props
-    const max = aggregations ? Math.max(...this.unSelectedAggregations()) || 1 : 1
-    const heatmapScale = chroma.scale([nomadSecondaryColor.veryLight, nomadSecondaryColor.main]).domain([1, max], 10, 'log')
-    return (
-      <div className={classes.root}>
-        <table className={classes.table}>
-          <tbody>
-            {elements.map((row, i) => (
-              <tr key={i}>
-                {row.map((element, j) => (
-                  <td key={j}>
-                    {element
-                      ? <Element
-                        element={element}
-                        count={aggregations ? (aggregations[element.symbol] || {})[metric] || 0 : 0}
-                        heatmapScale={heatmapScale}
-                        relativeCount={aggregations ? ((aggregations[element.symbol] || {})[metric] || 0) / max : 0}
-                        onClick={() => this.onElementClicked(element.symbol)}
-                        selected={values.indexOf(element.symbol) >= 0}
-                      /> : ''}
-                  </td>
-                ))}
-              </tr>
-            ))}
-          </tbody>
-        </table>
-        <div className={classes.formContainer}>
-          <Tooltip title={
-            'Search for entries with compositions that only (exclusively) contain the ' +
-            'selected atoms. The default is to return all entries that have at least ' +
-            '(inclusively) the selected atoms.'}>
-            <FormControlLabel
-              control={<Checkbox checked={exclusive} onChange={onExclusiveChanged} />}
-              label={'only composition that exclusively contain these atoms'}
-            />
-          </Tooltip>
-        </div>
-      </div>
-    )
-  }
-}
-
-export default withStyles(PeriodicTable.styles)(PeriodicTable)
diff --git a/gui/src/components/search/menus/FilterMainMenu.js b/gui/src/components/search/menus/FilterMainMenu.js
index c1b8559fab..302bb070e4 100644
--- a/gui/src/components/search/menus/FilterMainMenu.js
+++ b/gui/src/components/search/menus/FilterMainMenu.js
@@ -53,7 +53,7 @@ import {
   labelIDs,
   labelAccess,
   useSearchContext
-} from '../FilterContext'
+} from '../SearchContext'
 import InputCheckbox from '../input/InputCheckbox'
 
 /**
diff --git a/gui/src/components/search/menus/FilterMenu.js b/gui/src/components/search/menus/FilterMenu.js
index fc4add3492..5b0bf5d048 100644
--- a/gui/src/components/search/menus/FilterMenu.js
+++ b/gui/src/components/search/menus/FilterMenu.js
@@ -35,7 +35,7 @@ import ClearIcon from '@material-ui/icons/Clear'
 import Scrollable from '../../visualization/Scrollable'
 import FilterSummary from '../FilterSummary'
 import { Actions, Action } from '../../Actions'
-import { filterGroups, useResetFilters } from '../FilterContext'
+import { filterGroups, useResetFilters } from '../SearchContext'
 
 // The menu animations use a transition on the 'transform' property. Notice that
 // animating 'transform' instead of e.g. the 'left' property is much more
diff --git a/gui/src/components/search/menus/FilterSubMenuElements.js b/gui/src/components/search/menus/FilterSubMenuElements.js
index a2fcebe85a..31e5705284 100644
--- a/gui/src/components/search/menus/FilterSubMenuElements.js
+++ b/gui/src/components/search/menus/FilterSubMenuElements.js
@@ -26,7 +26,7 @@ import InputSlider from '../input/InputSlider'
 import {
   useFilterState,
   useAgg
-} from '../FilterContext'
+} from '../SearchContext'
 import { useUnits } from '../../../units'
 
 const useStyles = makeStyles(theme => ({
diff --git a/gui/src/components/search/results/GroupList.js b/gui/src/components/search/results/GroupList.js
deleted file mode 100644
index 4f0b0c9091..0000000000
--- a/gui/src/components/search/results/GroupList.js
+++ /dev/null
@@ -1,243 +0,0 @@
-/*
- * Copyright The NOMAD Authors.
- *
- * This file is part of NOMAD. See https://nomad-lab.eu for further info.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-import React from 'react'
-import PropTypes from 'prop-types'
-import { withStyles, TableCell, Toolbar, IconButton, Table, TableHead, TableRow, TableBody, Tooltip } from '@material-ui/core'
-import { compose } from 'recompose'
-import { withRouter } from 'react-router'
-import NextIcon from '@material-ui/icons/ChevronRight'
-import StartIcon from '@material-ui/icons/SkipPrevious'
-import DataTable from '../../DataTable'
-import { withApi } from '../../api'
-import { EntryListUnstyled } from '../EntryList'
-import MoreIcon from '@material-ui/icons/MoreHoriz'
-import DownloadButton from '../../DownloadButton'
-import { searchContext } from '../SearchContext'
-
-class GroupUnstyled extends React.Component {
-  static contextType = searchContext
-
-  static propTypes = {
-    classes: PropTypes.object.isRequired,
-    groupHash: PropTypes.string.isRequired,
-    api: PropTypes.object.isRequired,
-    raiseError: PropTypes.func.isRequired,
-    history: PropTypes.object.isRequired
-  }
-
-  static styles = theme => ({
-    root: {
-      padding: theme.spacing(3)
-    }
-  })
-
-  state = {
-    entries: []
-  }
-
-  update() {
-    const {groupHash, api, raiseError} = this.props
-    const {query} = this.context
-    api.search({...query, 'dft.group_hash': groupHash, per_page: 100})
-      .then(data => {
-        this.setState({entries: data.results})
-      })
-      .catch(raiseError)
-  }
-
-  componentDidMount() {
-    this.update()
-  }
-
-  componentDidUpdate(prevProps) {
-    if (prevProps.groupHash !== this.props.groupHash || prevProps.api !== this.props.api) {
-      this.update()
-    }
-  }
-
-  render() {
-    const {history} = this.props
-    const {entries} = this.state
-    return (
-      <Table>
-        <TableHead>
-          <TableRow>
-            <TableCell>Mainfile</TableCell>
-            <TableCell>Upload time</TableCell>
-            <TableCell></TableCell>
-          </TableRow>
-        </TableHead>
-        <TableBody>
-          {entries.map(entry => (
-            <TableRow key={entry.calc_id}>
-              <TableCell>{entry.mainfile}</TableCell>
-              <TableCell>{new Date(entry.upload_time).toLocaleString()}</TableCell>
-              <TableCell align="right">
-                <DownloadButton query={{calc_id: entry.calc_id}} tooltip="Download files of this entry" />
-                <Tooltip title="Show raw files and archive">
-                  <IconButton onClick={() => history.push(`/entry/id/${entry.upload_id}/${entry.calc_id}`)}>
-                    <MoreIcon />
-                  </IconButton>
-                </Tooltip>
-              </TableCell>
-            </TableRow>
-          ))}
-        </TableBody>
-      </Table>
-    )
-  }
-}
-
-const Group = compose(withRouter, withApi(false), withStyles(GroupUnstyled.styles))(GroupUnstyled)
-
-class GroupListUnstyled extends React.Component {
-  static propTypes = {
-    classes: PropTypes.object.isRequired,
-    data: PropTypes.object,
-    total: PropTypes.number,
-    onChange: PropTypes.func.isRequired,
-    history: PropTypes.any.isRequired,
-    groups_after: PropTypes.string,
-    actions: PropTypes.element,
-    domain: PropTypes.object.isRequired,
-    selectedColumns: PropTypes.arrayOf(PropTypes.string)
-  }
-
-  static styles = theme => ({
-    root: {
-      overflow: 'auto',
-      paddingLeft: theme.spacing(2),
-      paddingRight: theme.spacing(2)
-    },
-    scrollCell: {
-      padding: 0
-    },
-    scrollBar: {
-      minHeight: 56,
-      padding: 0
-    },
-    scrollSpacer: {
-      flexGrow: 1
-    },
-    clickableRow: {
-      cursor: 'pointer'
-    },
-    details: {
-      padding: 0
-    }
-  })
-
-  addColumns(columns) {
-    Object.keys(columns).forEach(key => {
-      const column = columns[key]
-      this.columns[key] = {
-        ...column,
-        supportsSort: false
-      }
-    })
-  }
-
-  constructor(props) {
-    super(props)
-    this.renderEntryActions = this.renderEntryActions.bind(this)
-
-    this.columns = {}
-  }
-
-  componentDidMount() {
-    this.addColumns(this.props.domain.searchResultColumns)
-    this.addColumns(EntryListUnstyled.defaultColumns)
-    this.addColumns({
-      entries: {
-        label: 'Entries',
-        render: group => group.total.toLocaleString(),
-        description: 'Number of entries in this group'
-      }
-    })
-  }
-
-  renderEntryActions(entry, selected) {
-    return <DownloadButton
-      dark={selected}
-      query={{'dft.group_hash': entry.dft.group_hash}} tooltip="Download all entries of this group"
-    />
-  }
-
-  renderEntryDetails(entry) {
-    return <Group groupHash={entry.dft.group_hash} />
-  }
-
-  render() {
-    const { classes, data, total, groups_after, onChange, actions, domain } = this.props
-    const groups = data['dft.groups_grouped'] || {values: []}
-    const results = Object.keys(groups.values).map(group_hash => {
-      const example = groups.values[group_hash].examples[0]
-      return {
-        ...example,
-        total: groups.values[group_hash].total,
-        example: example
-      }
-    })
-    const per_page = 10
-    const after = groups.after
-
-    const defaultSelectedColumns = this.props.selectedColumns || [
-      ...domain.defaultSearchResultColumns,
-      'datasets', 'authors', 'entries']
-
-    let paginationText
-    if (groups_after) {
-      paginationText = `next ${results.length.toLocaleString()} of ${(total || 0).toLocaleString()}`
-    } else {
-      paginationText = `1-${results.length.toLocaleString()} of ${(total || 0).toLocaleString()}`
-    }
-
-    const pagination = <TableCell colSpan={1000} classes={{root: classes.scrollCell}}>
-      <Toolbar className={classes.scrollBar}>
-        <span className={classes.scrollSpacer}>&nbsp;</span>
-        <span>{paginationText}</span>
-        <IconButton disabled={!groups_after} onClick={() => onChange({'dft.groups_grouped_after': null})}>
-          <StartIcon />
-        </IconButton>
-        <IconButton disabled={results.length < per_page} onClick={() => onChange({'dft.groups_grouped_after': after})}>
-          <NextIcon />
-        </IconButton>
-      </Toolbar>
-    </TableCell>
-
-    return <DataTable
-      classes={{details: classes.details}}
-      entityLabels={['group of similar entries', 'groups of similar entries']}
-      id={row => row.dft.group_hash}
-      total={total}
-      columns={this.columns}
-      selectedColumns={defaultSelectedColumns}
-      selectedColumnsKey="groups"
-      entryDetails={this.renderEntryDetails.bind(this)}
-      entryActions={this.renderEntryActions}
-      data={results}
-      rows={per_page}
-      actions={actions}
-      pagination={pagination}
-    />
-  }
-}
-
-const GroupList = compose(withRouter, withApi(false), withStyles(GroupListUnstyled.styles))(GroupListUnstyled)
-
-export default GroupList
diff --git a/gui/src/components/search/results/MaterialsList.js b/gui/src/components/search/results/MaterialsList.js
deleted file mode 100644
index 85056e7eb6..0000000000
--- a/gui/src/components/search/results/MaterialsList.js
+++ /dev/null
@@ -1,141 +0,0 @@
-/*
- * Copyright The NOMAD Authors.
- *
- * This file is part of NOMAD. See https://nomad-lab.eu for further info.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-import React from 'react'
-import PropTypes from 'prop-types'
-import { makeStyles, TableCell, Toolbar, IconButton, Tooltip } from '@material-ui/core'
-import NextIcon from '@material-ui/icons/ChevronRight'
-import StartIcon from '@material-ui/icons/SkipPrevious'
-import DataTable from '../../DataTable'
-import DetailsIcon from '@material-ui/icons/MoreHoriz'
-import { appBase } from '../../../config'
-
-const useStyles = makeStyles(theme => ({
-  root: {
-    overflow: 'auto',
-    paddingLeft: theme.spacing(2),
-    paddingRight: theme.spacing(2)
-  },
-  scrollCell: {
-    padding: 0
-  },
-  scrollBar: {
-    minHeight: 56,
-    padding: 0
-  },
-  scrollSpacer: {
-    flexGrow: 1
-  },
-  clickableRow: {
-    cursor: 'pointer'
-  }
-}))
-
-const columns = {
-  'encyclopedia.material.formula': {
-    label: 'Formula'
-  },
-  'encyclopedia.material.material_name': {
-    label: 'Name'
-  },
-  'encyclopedia.material.material_type': {
-    label: 'Type'
-  },
-  'encyclopedia.material.bulk': {
-    label: 'Spacegroup',
-    render: entry => {
-      const bulk = entry.encyclopedia.material.bulk
-      return (bulk && bulk.space_group_international_short_symbol) || '-'
-    }
-  },
-  calculations: {
-    label: 'No calculations',
-    description: 'The number of entries with data for this material',
-    render: entry => entry.total
-  }
-}
-
-export default function MaterialsList(props) {
-  const { data, total, materials_after, per_page, onChange, actions } = props
-  const classes = useStyles()
-  const materials = data['encyclopedia.material.materials_grouped'] || {values: []}
-  const results = Object.keys(materials.values).map(id => {
-    return {
-      id: id,
-      total: materials.values[id].total,
-      ...materials.values[id].examples[0]
-    }
-  })
-  const after = materials.after
-  const perPage = per_page || 10
-
-  let paginationText
-  if (materials_after) {
-    paginationText = `next ${results.length.toLocaleString()} of ${(total || 0).toLocaleString()}`
-  } else {
-    paginationText = `1-${results.length.toLocaleString()} of ${(total || 0).toLocaleString()}`
-  }
-
-  /*   const handleViewMaterial = useCallback((event, materialId) => {
-    event.stopPropagation()
-    history.push(`/material/${materialId}/overview`)
-  }, [history]) */
-
-  const pagination = <TableCell colSpan={1000} classes={{root: classes.scrollCell}}>
-    <Toolbar className={classes.scrollBar}>
-      <span className={classes.scrollSpacer}>&nbsp;</span>
-      <span>{paginationText}</span>
-      <IconButton disabled={!materials_after} onClick={() => onChange({materials_grouped_after: null})}>
-        <StartIcon />
-      </IconButton>
-      <IconButton disabled={results.length < perPage} onClick={() => onChange({materials_grouped_after: after})}>
-        <NextIcon />
-      </IconButton>
-    </Toolbar>
-  </TableCell>
-
-  const entryActions = entry => <Tooltip title="Open this material in the Encyclopedia.">
-    <IconButton href={`${appBase}/encyclopedia/#/material/${entry.encyclopedia.material.material_id}`}>
-      <DetailsIcon />
-    </IconButton>
-    {/*     <IconButton onClick={event => handleViewMaterial(event, entry.encyclopedia.material.material_id)}>
-      <DetailsIcon />
-    </IconButton> */}
-  </Tooltip>
-
-  return <DataTable
-    entityLabels={['material', 'materials']}
-    id={row => row.id}
-    total={total}
-    columns={columns}
-    selectedColumns={['encyclopedia.material.formula', 'encyclopedia.material.material_name', 'encyclopedia.material.material_type', 'encyclopedia.material.bulk.spacegroup', 'calculations']}
-    selectedColumnsKey="materials"
-    data={results}
-    rows={perPage}
-    actions={actions}
-    pagination={pagination}
-    entryActions={entryActions}
-  />
-}
-MaterialsList.propTypes = ({
-  data: PropTypes.object,
-  total: PropTypes.number,
-  onChange: PropTypes.func.isRequired,
-  materials_after: PropTypes.string,
-  per_page: PropTypes.number,
-  actions: PropTypes.element
-})
diff --git a/gui/src/components/search/results/SearchResults.js b/gui/src/components/search/results/SearchResults.js
index 20f8520399..6169da1d76 100644
--- a/gui/src/components/search/results/SearchResults.js
+++ b/gui/src/components/search/results/SearchResults.js
@@ -24,7 +24,7 @@ import {
 } from '@material-ui/core'
 import SearchResultsMaterials from './SearchResultsMaterials'
 import SearchResultsEntries from './SearchResultsEntries'
-import { useScrollResults, useSearchContext } from '../FilterContext'
+import { useScrollResults, useSearchContext } from '../SearchContext'
 
 /**
  * Displays the list of search results
@@ -65,7 +65,6 @@ const SearchResults = React.memo(({
   // re-render the results list only when the actual results have changed, and
   // not just when the search query changes. Has a significant effect on
   // performance.
-  // const component = resource === 'materials' ? MaterialResults : NewEntryList
   const result = useMemo(() => {
     const Component = resource === 'materials' ? SearchResultsMaterials : SearchResultsEntries
     return <Paper className={clsx(className, styles.root)}>
diff --git a/gui/src/components/search/results/UploadersList.js b/gui/src/components/search/results/UploadersList.js
deleted file mode 100644
index 4c3db9126e..0000000000
--- a/gui/src/components/search/results/UploadersList.js
+++ /dev/null
@@ -1,50 +0,0 @@
-/*
- * Copyright The NOMAD Authors.
- *
- * This file is part of NOMAD. See https://nomad-lab.eu for further info.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-import React from 'react'
-import PropTypes from 'prop-types'
-import Grid from '@material-ui/core/Grid'
-import { Quantity } from '../QuantityHistogram'
-import { withStyles } from '@material-ui/core'
-import { searchContext } from '../SearchContext'
-import { compose } from 'recompose'
-import { withApi } from '../../api'
-
-class UploadersList extends React.Component {
-  static propTypes = {
-    classes: PropTypes.object.isRequired
-  }
-
-  static styles = theme => ({
-    root: {
-      marginTop: theme.spacing(2)
-    }
-  })
-
-  static contextType = searchContext
-
-  render() {
-    const {state: {usedMetric}} = this.context
-
-    return (
-      <Grid>
-        <Quantity quantity="origin" title="Uploaders/origin" scale={1} metric={usedMetric} />
-      </Grid>
-    )
-  }
-}
-export default compose(withApi(false, false), withStyles(UploadersList.styles))(UploadersList)
diff --git a/gui/src/components/search/input/PeriodicTableData.json b/gui/src/elementData.json
similarity index 100%
rename from gui/src/components/search/input/PeriodicTableData.json
rename to gui/src/elementData.json
-- 
GitLab