From d021037bf4ca60f48206e95b898940d5f5aa52c4 Mon Sep 17 00:00:00 2001
From: esikkala <esko.ikkala@aalto.fi>
Date: Thu, 11 Apr 2019 15:37:37 +0300
Subject: [PATCH] Building text search perspective

---
 src/client/actions/index.js                   |  15 +++
 .../components/facet_results/ResultTable2.js  |  28 ++++
 .../components/main_layout/SearchField.js     | 124 ------------------
 src/client/components/main_layout/TopBar.js   |  22 +---
 .../main_layout/TopBarSearchField.js          | 115 ++++++++++++++++
 src/client/components/perspectives/All.js     |  50 +++++++
 src/client/containers/SemanticPortal.js       |  33 ++++-
 src/client/epics/index.js                     |  39 +++++-
 .../reducers/clientSideFacetedSearch.js       |  89 +++++++++++++
 src/client/reducers/index.js                  |   2 +
 src/server/index.js                           |  31 +++++
 src/server/sparql/JenaQuery.js                |  19 +++
 src/server/sparql/SparqlQueriesGeneral.js     |   9 ++
 13 files changed, 435 insertions(+), 141 deletions(-)
 create mode 100644 src/client/components/facet_results/ResultTable2.js
 delete mode 100644 src/client/components/main_layout/SearchField.js
 create mode 100644 src/client/components/main_layout/TopBarSearchField.js
 create mode 100644 src/client/components/perspectives/All.js
 create mode 100644 src/client/reducers/clientSideFacetedSearch.js
 create mode 100644 src/server/sparql/JenaQuery.js

diff --git a/src/client/actions/index.js b/src/client/actions/index.js
index eb01a975..27bf7759 100644
--- a/src/client/actions/index.js
+++ b/src/client/actions/index.js
@@ -1,9 +1,11 @@
 export const FETCH_PAGINATED_RESULTS = 'FETCH_PAGINATED_RESULTS';
 export const FETCH_PAGINATED_RESULTS_FAILED = 'FETCH_PAGINATED_RESULTS_FAILED';
 export const FETCH_RESULTS = 'FETCH_RESULTS';
+export const FETCH_RESULTS_CLIENT_SIDE = 'FETCH_RESULTS_CLIENT_SIDE';
 export const FETCH_RESULTS_FAILED = 'FETCH_RESULTS_FAILED';
 export const UPDATE_PAGINATED_RESULTS = 'UPDATE_PAGINATED_RESULTS';
 export const UPDATE_RESULTS = 'UPDATE_RESULTS';
+export const CLEAR_RESULTS = 'CLEAR_RESULTS';
 export const SORT_RESULTS = 'SORT_RESULTS';
 export const UPDATE_PAGE = 'UPDATE_PAGE';
 export const FETCH_BY_URI = 'FETCH_BY_URI';
@@ -13,6 +15,7 @@ export const FETCH_FACET = 'FETCH_FACET';
 export const FETCH_FACET_FAILED = 'FETCH_FACET_FAILED';
 export const UPDATE_FACET_VALUES = 'UPDATE_FACET_VALUES';
 export const UPDATE_FACET_OPTION = 'UPDATE_FACET_OPTION';
+export const UPDATE_CLIENT_SIDE_FILTER = 'UPDATE_CLIENT_SIDE_FILTER';
 export const OPEN_MARKER_POPUP = 'OPEN_MARKER_POPUP';
 export const SHOW_ERROR = 'SHOW_ERROR';
 
@@ -28,6 +31,10 @@ export const fetchResults = ({ resultClass, facetClass, sortBy, variant }) => ({
   type: FETCH_RESULTS,
   resultClass, facetClass, sortBy, variant
 });
+export const fetchResultsClientSide = ({ jenaIndex, query }) => ({
+  type: FETCH_RESULTS_CLIENT_SIDE,
+  jenaIndex, query
+});
 export const fetchResultsFailed = (resultClass, error, message) => ({
   type: FETCH_RESULTS_FAILED,
   resultClass, error, message
@@ -44,6 +51,10 @@ export const sortResults = (resultClass, sortBy) => ({
   type: SORT_RESULTS,
   resultClass, sortBy
 });
+export const clearResults = resultClass => ({
+  type: CLEAR_RESULTS,
+  resultClass
+});
 export const updatePage = (resultClass, page) => ({
   type: UPDATE_PAGE,
   resultClass, page
@@ -76,6 +87,10 @@ export const updateFacetOption = ({ facetClass, facetID, option, value }) => ({
   type: UPDATE_FACET_OPTION,
   facetClass, facetID, option, value
 });
+export const updateClientSideFilter = filterObj => ({
+  type: UPDATE_CLIENT_SIDE_FILTER,
+  filterObj
+});
 export const openMarkerPopup = uri => ({
   type: OPEN_MARKER_POPUP,
   uri
diff --git a/src/client/components/facet_results/ResultTable2.js b/src/client/components/facet_results/ResultTable2.js
new file mode 100644
index 00000000..8065033b
--- /dev/null
+++ b/src/client/components/facet_results/ResultTable2.js
@@ -0,0 +1,28 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import MaterialTable from 'material-table';
+
+class ResultTable2 extends React.Component {
+  render() {
+    return (
+      <div style={{ maxWidth: '100%' }}>
+        <MaterialTable
+          columns={[
+            { title: 'Adı', field: 'name' },
+            { title: 'Soyadı', field: 'surname' },
+            { title: 'Doğum Yılı', field: 'birthYear', type: 'numeric' },
+            { title: 'Doğum Yeri', field: 'birthCity', lookup: { 34: 'İstanbul', 63: 'Şanlıurfa' } }
+          ]}
+          data={[{ name: 'Mehmet', surname: 'Baran', birthYear: 1987, birthCity: 63 }]}
+          title="Demo Title"
+        />
+      </div>
+    );
+  }
+}
+
+ResultTable2.propTypes = {
+  data: PropTypes.object,
+};
+
+export default ResultTable2;
diff --git a/src/client/components/main_layout/SearchField.js b/src/client/components/main_layout/SearchField.js
deleted file mode 100644
index 5743237d..00000000
--- a/src/client/components/main_layout/SearchField.js
+++ /dev/null
@@ -1,124 +0,0 @@
-import React from 'react';
-import PropTypes from 'prop-types';
-import { withStyles } from '@material-ui/core/styles';
-import IconButton from '@material-ui/core/IconButton';
-import SearchIcon from '@material-ui/icons/Search';
-import Input from '@material-ui/core/Input';
-import InputLabel from '@material-ui/core/InputLabel';
-import InputAdornment from '@material-ui/core/InputAdornment';
-import FormControl from '@material-ui/core/FormControl';
-import CircularProgress from '@material-ui/core/CircularProgress';
-
-const styles = theme => ({
-  textSearch: {
-    margin: theme.spacing.unit,
-  },
-});
-
-class SearchField extends React.Component {
-  state = {
-    value: '',
-  };
-
-  componentDidUpdate = prevProps => {
-    if (prevProps.search.query != this.props.search.query) {
-      this.setState({
-        value: this.props.search.query
-      });
-    }
-  }
-
-  handleChange = (event) => {
-    this.setState({ value: event.target.value });
-  };
-
-  handleMouseDown = (event) => {
-    event.preventDefault();
-  };
-
-  handleOnKeyDown = (event) => {
-    if (event.key === 'Enter' && this.hasDatasets() && this.hasValidQuery()) {
-      this.props.clearResults();
-      this.props.updateQuery(this.state.value);
-      this.props.fetchResults('text', this.state.value);
-    }
-  };
-
-  handleClick = () => {
-    if (this.hasDatasets() && this.hasValidQuery()) {
-      this.props.clearResults();
-      this.props.updateQuery(this.state.value);
-      this.props.fetchResults('text', this.state.value);
-    }
-  };
-
-  hasDatasets = () => {
-    let hasDs = false;
-    Object.values(this.props.datasets).forEach(value => {
-      if (value.selected) {
-        hasDs = true;
-      }
-    });
-    return hasDs;
-  }
-
-  hasValidQuery = () => {
-    return this.state.value.length > 2;
-  }
-
-  render() {
-    const { classes, strings } = this.props;
-    let searchButton = null;
-    if (this.props.search.textResultsFetching) {
-      searchButton = (
-        <IconButton
-          aria-label="Search places"
-        >
-          <CircularProgress size={24} />
-        </IconButton>
-      );
-    } else {
-      searchButton = (
-        <IconButton
-          aria-label="Search"
-          onClick={this.handleClick}
-          onMouseDown={this.handleMouseDown}
-        >
-          <SearchIcon />
-        </IconButton>
-      );
-    }
-
-    return (
-      <div className={classes.root}>
-        <FormControl className={classes.textSearch}>
-          <InputLabel htmlFor="adornment-search">{strings.searchPlaceNames}</InputLabel>
-          <Input
-            id="adornment-search"
-            type='text'
-            value={this.state.value}
-            onChange={this.handleChange}
-            onKeyDown={this.handleOnKeyDown}
-            endAdornment={
-              <InputAdornment position="end">
-                {searchButton}
-              </InputAdornment>
-            }
-          />
-        </FormControl>
-      </div>
-    );
-  }
-}
-
-SearchField.propTypes = {
-  classes: PropTypes.object.isRequired,
-  search: PropTypes.object.isRequired,
-  fetchResults: PropTypes.func.isRequired,
-  clearResults: PropTypes.func.isRequired,
-  updateQuery: PropTypes.func.isRequired,
-  datasets: PropTypes.object.isRequired,
-  strings: PropTypes.object.isRequired
-};
-
-export default withStyles(styles)(SearchField);
diff --git a/src/client/components/main_layout/TopBar.js b/src/client/components/main_layout/TopBar.js
index 994b1663..4ee67288 100644
--- a/src/client/components/main_layout/TopBar.js
+++ b/src/client/components/main_layout/TopBar.js
@@ -4,15 +4,14 @@ import AppBar from '@material-ui/core/AppBar';
 import Toolbar from '@material-ui/core/Toolbar';
 import IconButton from '@material-ui/core/IconButton';
 import Typography from '@material-ui/core/Typography';
-import InputBase from '@material-ui/core/InputBase';
 import MenuItem from '@material-ui/core/MenuItem';
 import Menu from '@material-ui/core/Menu';
 import { fade } from '@material-ui/core/styles/colorManipulator';
 import { withStyles } from '@material-ui/core/styles';
-import SearchIcon from '@material-ui/icons/Search';
 import MoreIcon from '@material-ui/icons/MoreVert';
 import Button from '@material-ui/core/Button';
 import { Link, NavLink } from 'react-router-dom';
+import TopBarSearchField from './TopBarSearchField';
 
 const styles = theme => ({
   root: {
@@ -154,19 +153,10 @@ class TopBar extends React.Component {
                 MMM
               </Typography>
             </Button>
-            <div className={classes.search}>
-              <div className={classes.searchIcon}>
-                <SearchIcon />
-              </div>
-              <InputBase
-                disabled
-                placeholder="Search…"
-                classes={{
-                  root: classes.inputRoot,
-                  input: classes.inputInput,
-                }}
-              />
-            </div>
+            <TopBarSearchField
+              fetchResultsClientSide={this.props.fetchResultsClientSide}
+              clearResults={this.props.clearResults}
+            />
             <div className={classes.grow} />
             <div className={classes.sectionDesktop}>
               {perspectives.map(perspective =>
@@ -197,6 +187,8 @@ class TopBar extends React.Component {
 
 TopBar.propTypes = {
   classes: PropTypes.object.isRequired,
+  fetchResultsClientSide: PropTypes.func.isRequired,
+  clearResults: PropTypes.func.isRequired,
 };
 
 export default withStyles(styles)(TopBar);
diff --git a/src/client/components/main_layout/TopBarSearchField.js b/src/client/components/main_layout/TopBarSearchField.js
new file mode 100644
index 00000000..5b2dba72
--- /dev/null
+++ b/src/client/components/main_layout/TopBarSearchField.js
@@ -0,0 +1,115 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import { withStyles } from '@material-ui/core/styles';
+import { fade } from '@material-ui/core/styles/colorManipulator';
+import SearchIcon from '@material-ui/icons/Search';
+import InputBase from '@material-ui/core/InputBase';
+//import CircularProgress from '@material-ui/core/CircularProgress';
+
+const styles = theme => ({
+  search: {
+    position: 'relative',
+    borderRadius: theme.shape.borderRadius,
+    backgroundColor: fade(theme.palette.common.white, 0.15),
+    '&:hover': {
+      backgroundColor: fade(theme.palette.common.white, 0.25),
+    },
+    marginRight: theme.spacing.unit * 2,
+    marginLeft: 0,
+    width: '100%',
+    [theme.breakpoints.up('sm')]: {
+      marginLeft: theme.spacing.unit * 3,
+      width: 'auto',
+    },
+  },
+  searchIcon: {
+    width: theme.spacing.unit * 9,
+    height: '100%',
+    position: 'absolute',
+    pointerEvents: 'none',
+    display: 'flex',
+    alignItems: 'center',
+    justifyContent: 'center',
+  },
+  inputRoot: {
+    color: 'inherit',
+    width: '100%',
+  },
+  inputInput: {
+    paddingTop: theme.spacing.unit,
+    paddingRight: theme.spacing.unit,
+    paddingBottom: theme.spacing.unit,
+    paddingLeft: theme.spacing.unit * 10,
+    transition: theme.transitions.create('width'),
+    width: '100%',
+    [theme.breakpoints.up('md')]: {
+      width: 200,
+    },
+  },
+});
+
+class TopBarSearchField extends React.Component {
+  state = {
+    value: '',
+  };
+
+  handleChange = (event) => {
+    this.setState({ value: event.target.value });
+  };
+
+  handleMouseDown = (event) => {
+    event.preventDefault();
+  };
+
+  handleOnKeyDown = (event) => {
+    if (event.key === 'Enter' && this.hasValidQuery()) {
+      this.props.clearResults();
+      this.props.fetchResultsClientSide({
+        jenaIndex: 'text',
+        query: this.state.value
+      });
+    }
+  };
+
+  handleClick = () => {
+    if (this.hasValidQuery()) {
+      this.props.clearResults();
+      this.props.fetchResultsClientSide({
+        jenaIndex: 'text',
+        query: this.state.value
+      });
+    }
+  };
+
+  hasValidQuery = () => {
+    return this.state.value.length > 2;
+  }
+
+  render() {
+    const { classes } = this.props;
+    return (
+      <div className={classes.search}>
+        <div className={classes.searchIcon}>
+          <SearchIcon />
+        </div>
+        <InputBase
+          placeholder="Search everything"
+          classes={{
+            root: classes.inputRoot,
+            input: classes.inputInput,
+          }}
+          onChange={this.handleChange}
+          onKeyDown={this.handleOnKeyDown}
+        />
+      </div>
+    );
+  }
+}
+
+TopBarSearchField.propTypes = {
+  classes: PropTypes.object.isRequired,
+  fetchResultsClientSide: PropTypes.func.isRequired,
+  clearResults: PropTypes.func.isRequired,
+};
+
+export default withStyles(styles)(TopBarSearchField);
diff --git a/src/client/components/perspectives/All.js b/src/client/components/perspectives/All.js
new file mode 100644
index 00000000..74bbc432
--- /dev/null
+++ b/src/client/components/perspectives/All.js
@@ -0,0 +1,50 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import { Route, Redirect } from 'react-router-dom';
+import PerspectiveTabs from '../main_layout/PerspectiveTabs';
+//import ResultTable2 from '../facet_results/ResultTable2';
+import Typography from '@material-ui/core/Typography';
+
+
+let All = props => {
+  const perspectiveUrl = '/all';
+  return (
+    <React.Fragment>
+      <PerspectiveTabs
+        routeProps={props.routeProps}
+        tabs={{
+          [`${perspectiveUrl}/table`]: {
+            label: 'table',
+            value: 0,
+            icon: 'CalendarViewDay',
+          },
+          [`${perspectiveUrl}/map`]: {
+            label: 'map',
+            value: 1,
+            icon: 'AddLocation',
+          },
+        }}
+      />
+      <Route
+        exact path={perspectiveUrl}
+        render={() => <Redirect to={`${perspectiveUrl}/table`} />}
+      />
+      <Route
+        path={`${perspectiveUrl}/table`}
+        render={() =>
+          <Typography>Test</Typography>
+          //<ResultTable2 />
+        }
+      />
+    </React.Fragment>
+  );
+};
+
+All.propTypes = {
+  results: PropTypes.object,
+  updatePage: PropTypes.func,
+  sortResults: PropTypes.func,
+  routeProps: PropTypes.object.isRequired,
+};
+
+export default All;
diff --git a/src/client/containers/SemanticPortal.js b/src/client/containers/SemanticPortal.js
index 0d140a08..9eaf174b 100644
--- a/src/client/containers/SemanticPortal.js
+++ b/src/client/containers/SemanticPortal.js
@@ -18,9 +18,12 @@ import Works from '../components/perspectives/Works';
 import Places from '../components//perspectives/Places';
 import People from '../components//perspectives/People';
 import Organizations from '../components/perspectives/Organizations';
+import All from '../components/perspectives/All';
 import {
   fetchPaginatedResults,
   fetchResults,
+  fetchResultsClientSide,
+  clearResults,
   fetchByURI,
   fetchFacet,
   sortResults,
@@ -82,7 +85,11 @@ let SemanticPortal = (props) => {
       <div className={classes.appFrame}>
         <Message error={error} />
         <React.Fragment>
-          <TopBar />
+          <TopBar
+            search={props.clientSideFacetedSearch}
+            fetchResultsClientSide={props.fetchResultsClientSide}
+            clearResults={props.clearResults}
+          />
           <Grid container spacing={8} className={classes.mainContainer}>
             <Route exact path="/" component={Main} />
             <Route
@@ -243,6 +250,24 @@ let SemanticPortal = (props) => {
                 </React.Fragment>
               }
             />
+            <Route
+              path="/all"
+              render={routeProps =>
+                <React.Fragment>
+                  <Grid item sm={12} md={3} className={classes.facetBarContainer}>
+
+                  </Grid>
+                  <Grid item sm={12} md={9} className={classes.resultsContainer}>
+                    <Paper className={classes.resultsContainerPaper}>
+                      <All
+                        results={props.clientSideFacetedSearch.results}
+                        routeProps={routeProps}
+                      />
+                    </Paper>
+                  </Grid>
+                </React.Fragment>
+              }
+            />
           </Grid>
         </React.Fragment>
         <Footer />
@@ -266,6 +291,7 @@ const mapStateToProps = state => {
     organizationsFacets: state.organizationsFacets,
     places: state.places,
     placesFacets: state.placesFacets,
+    clientSideFacetedSearch: state.clientSideFacetedSearch,
     error: state.error
     //browser: state.browser,
   };
@@ -274,9 +300,11 @@ const mapStateToProps = state => {
 const mapDispatchToProps = ({
   fetchPaginatedResults,
   fetchResults,
+  fetchResultsClientSide,
   fetchByURI,
   fetchFacet,
   sortResults,
+  clearResults,
   updateFacetOption,
   updatePage,
   showError
@@ -297,10 +325,13 @@ SemanticPortal.propTypes = {
   organizationsFacets: PropTypes.object.isRequired,
   places: PropTypes.object.isRequired,
   placesFacets: PropTypes.object.isRequired,
+  clientSideFacetedSearch: PropTypes.object.isRequired,
   fetchResults: PropTypes.func.isRequired,
+  fetchResultsClientSide: PropTypes.func.isRequired,
   fetchPaginatedResults: PropTypes.func.isRequired,
   fetchByURI: PropTypes.func.isRequired,
   sortResults: PropTypes.func.isRequired,
+  clearResults: PropTypes.func.isRequired,
   updatePage: PropTypes.func.isRequired,
   updateFacetOption: PropTypes.func.isRequired,
   fetchFacet: PropTypes.func.isRequired,
diff --git a/src/client/epics/index.js b/src/client/epics/index.js
index e965a8b5..4c33e4fd 100644
--- a/src/client/epics/index.js
+++ b/src/client/epics/index.js
@@ -1,6 +1,13 @@
 import { of } from 'rxjs';
 import { ajax } from 'rxjs/ajax';
-import { mergeMap, map, withLatestFrom, catchError } from 'rxjs/operators';
+import {
+  mergeMap,
+  switchMap,
+  map,
+  withLatestFrom,
+  debounceTime,
+  catchError
+} from 'rxjs/operators';
 import { combineEpics, ofType } from 'redux-observable';
 import querystring from 'querystring';
 import { has } from 'lodash';
@@ -8,6 +15,7 @@ import {
   FETCH_PAGINATED_RESULTS,
   FETCH_PAGINATED_RESULTS_FAILED,
   FETCH_RESULTS,
+  FETCH_RESULTS_CLIENT_SIDE,
   FETCH_RESULTS_FAILED,
   FETCH_BY_URI,
   FETCH_BY_URI_FAILED,
@@ -89,6 +97,34 @@ const fetchResultsEpic = (action$, state$) => action$.pipe(
   })
 );
 
+const fetchResultsClientSideEpic = (action$, state$) => action$.pipe(
+  ofType(FETCH_RESULTS_CLIENT_SIDE),
+  withLatestFrom(state$),
+  debounceTime(500),
+  switchMap(([action, state]) => {
+    const searchUrl = apiUrl + 'search';
+    let requestUrl = '';
+    if (action.jenaIndex === 'text') {
+      requestUrl = `${searchUrl}?q=${action.query}`;
+    } else if (action.jenaIndex === 'spatial') {
+      const { latMin, longMin, latMax, longMax } = state.map;
+      requestUrl = `${searchUrl}?latMin=${latMin}&longMin=${longMin}&latMax=${latMax}&longMax=${longMax}`;
+    }
+    return ajax.getJSON(requestUrl).pipe(
+      map(response => updateResults({ resultClass: 'all', data: response })),
+      catchError(error => of({
+        type: FETCH_RESULTS_FAILED,
+        resultClass: 'all',
+        error: error,
+        message: {
+          text: backendErrorText,
+          title: 'Error'
+        }
+      }))
+    );
+  })
+);
+
 const fetchByURIEpic = (action$, state$) => action$.pipe(
   ofType(FETCH_BY_URI),
   withLatestFrom(state$),
@@ -214,6 +250,7 @@ const boundsToValues = bounds => {
 const rootEpic = combineEpics(
   fetchPaginatedResultsEpic,
   fetchResultsEpic,
+  fetchResultsClientSideEpic,
   fetchByURIEpic,
   fetchFacetEpic,
 );
diff --git a/src/client/reducers/clientSideFacetedSearch.js b/src/client/reducers/clientSideFacetedSearch.js
new file mode 100644
index 00000000..7fbe5710
--- /dev/null
+++ b/src/client/reducers/clientSideFacetedSearch.js
@@ -0,0 +1,89 @@
+import {
+  FETCH_RESULTS_CLIENT_SIDE,
+  UPDATE_RESULTS,
+  CLEAR_RESULTS,
+  UPDATE_CLIENT_SIDE_FILTER,
+  SORT_RESULTS
+} from '../actions';
+
+export const INITIAL_STATE = {
+  results: null,
+  latestFilter: {
+    id: '',
+  },
+  latestFilterValues: [],
+  resultsFilter: {
+    prefLabel: new Set(),
+    type: new Set()
+  },
+  sortBy: 'prefLabel',
+  sortDirection: 'asc',
+  //groupBy: 'broaderTypeLabel',
+  //groupByLabel: 'Paikanlaji',
+  textResultsFetching: false,
+  spatialResultsFetching: false,
+};
+
+const clientSideFacetedSearch = (state = INITIAL_STATE, action) => {
+  if (action.resultClass === 'all') {
+    switch (action.type) {
+      case FETCH_RESULTS_CLIENT_SIDE:
+        return {
+          ...state,
+          //[`${action.jenaIndex}ResultsFetching`]: true
+        };
+      case UPDATE_RESULTS:
+        console.log(action.data)
+        return {
+          ...state,
+          results: action.data,
+          //[`${action.jenaIndex}ResultsFetching`]: false
+        };
+      case CLEAR_RESULTS:
+        return {
+          ...state,
+          results: null,
+          fetchingResults: false,
+          query: '',
+          resultsFilter: {
+            prefLabel: new Set(),
+            type: new Set()
+          },
+        };
+      case UPDATE_CLIENT_SIDE_FILTER:
+        return updateResultsFilter(state, action);
+      case SORT_RESULTS:
+        return {
+          ...state,
+          sortBy: action.options.sortBy,
+          sortDirection: action.options.sortDirection,
+        };
+      default:
+        return state;
+    }
+  } else return state;
+};
+
+const updateResultsFilter = (state, action) => {
+  const { property, value, latestValues } = action.filterObj;
+  let nSet = state.resultsFilter[property];
+  if (nSet.has(value)) {
+    nSet.delete(value);
+  } else {
+    nSet.add(value);
+  }
+  const newFilter = {
+    ...state.resultsFilter,
+    [property]: nSet
+  };
+  return {
+    ...state,
+    resultsFilter: newFilter,
+    latestFilter: {
+      id: property,
+    },
+    latestFilterValues: latestValues
+  };
+};
+
+export default clientSideFacetedSearch;
diff --git a/src/client/reducers/index.js b/src/client/reducers/index.js
index caeb7462..c6d61b5d 100644
--- a/src/client/reducers/index.js
+++ b/src/client/reducers/index.js
@@ -12,6 +12,7 @@ import worksFacets from './worksFacets';
 import peopleFacets from './peopleFacets';
 import organizationsFacets from './organizationsFacets';
 import placesFacets from './placesFacets';
+import clientSideFacetedSearch from './clientSideFacetedSearch';
 
 const reducer = combineReducers({
   manuscripts,
@@ -24,6 +25,7 @@ const reducer = combineReducers({
   organizationsFacets,
   places,
   placesFacets,
+  clientSideFacetedSearch,
   error,
   toastr: toastrReducer,
   browser: createResponsiveStateReducer({
diff --git a/src/server/index.js b/src/server/index.js
index 973cc6b8..7b70aa56 100644
--- a/src/server/index.js
+++ b/src/server/index.js
@@ -1,8 +1,10 @@
 import express from 'express';
 import path from 'path';
 import bodyParser from 'body-parser';
+import { has } from 'lodash';
 import { getPaginatedResults, getAllResults, getByURI } from './sparql/FacetResults';
 import { getFacet } from './sparql/FacetValues';
+import { queryJenaIndex } from './sparql/JenaQuery';
 const DEFAULT_PORT = 3001;
 const app = express();
 app.set('port', process.env.PORT || DEFAULT_PORT);
@@ -91,6 +93,35 @@ app.get(`${apiPath}/:facetClass/facet/:id`, async (req, res, next) => {
   }
 });
 
+app.get(`${apiPath}/search`, async (req, res, next) => {
+  let queryTerm = '';
+  let latMin = 0;
+  let longMin = 0;
+  let latMax = 0;
+  let longMax = 0;
+  if (has(req.query, 'q')) {
+    queryTerm = req.query.q;
+  }
+  if (has(req.query, 'latMin')) {
+    latMin = req.query.latMin;
+    longMin = req.query.longMin;
+    latMax = req.query.latMax;
+    longMax = req.query.longMax;
+  }
+  try {
+    const data = await queryJenaIndex({
+      queryTerm: queryTerm,
+      latMin: latMin,
+      longMin: longMin,
+      latMax: latMax,
+      longMax: longMax,
+    });
+    res.json(data);
+  } catch(error) {
+    next(error);
+  }
+});
+
 /*  Routes are matched to a url in order of their definition
     Redirect all the the rest for react-router to handle */
 app.get('*', function(request, response) {
diff --git a/src/server/sparql/JenaQuery.js b/src/server/sparql/JenaQuery.js
new file mode 100644
index 00000000..d8082f23
--- /dev/null
+++ b/src/server/sparql/JenaQuery.js
@@ -0,0 +1,19 @@
+import { runSelectQuery } from './SparqlApi';
+import { prefixes } from './SparqlQueriesPrefixes';
+import { endpoint, jenaQuery  } from './SparqlQueriesGeneral';
+import { makeObjectList } from './SparqlObjectMapper';
+
+export const queryJenaIndex = async ({
+  queryTerm,
+  latMin,
+  longMin,
+  latMax,
+  longMax,
+}) => {
+  let q = jenaQuery;
+  q = q.replace('<QUERY>', `
+  ?id text:query ('${queryTerm.toLowerCase()}' 10000) .
+  `);
+  const results = await runSelectQuery(prefixes + q, endpoint, makeObjectList);
+  return results;
+};
diff --git a/src/server/sparql/SparqlQueriesGeneral.js b/src/server/sparql/SparqlQueriesGeneral.js
index 00e1ec5e..fb74a535 100644
--- a/src/server/sparql/SparqlQueriesGeneral.js
+++ b/src/server/sparql/SparqlQueriesGeneral.js
@@ -9,6 +9,15 @@ export const countQuery = `
   }
 `;
 
+export const jenaQuery = `
+  SELECT ?id ?prefLabel ?type
+  WHERE {
+    <QUERY>
+    ?id skos:prefLabel ?prefLabel .
+    ?id a ?type .
+  }
+`;
+
 export const facetResultSetQuery = `
   SELECT *
   WHERE {
-- 
GitLab