You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@couchdb.apache.org by wi...@apache.org on 2019/09/25 12:58:13 UTC

[couchdb-fauxton] 01/02: Import search

This is an automated email from the ASF dual-hosted git repository.

willholley pushed a commit to branch search_addon
in repository https://gitbox.apache.org/repos/asf/couchdb-fauxton.git

commit b2069d491372d73df4667bbb97fe2788165e2608
Author: Will Holley <wi...@gmail.com>
AuthorDate: Tue Sep 24 15:35:21 2019 +0100

    Import search
---
 .gitignore                                         |   1 +
 app/addons/search/__tests__/components.test.js     | 160 ++++++++
 app/addons/search/__tests__/search.actions.test.js |  52 +++
 .../search/__tests__/search.reducers.test.js       | 142 +++++++
 app/addons/search/actions.js                       | 428 +++++++++++++++++++++
 app/addons/search/actiontypes.js                   |  36 ++
 app/addons/search/api.js                           |  32 ++
 app/addons/search/assets/less/search.less          | 159 ++++++++
 app/addons/search/base.js                          | 125 ++++++
 app/addons/search/components/Analyzer.js           | 142 +++++++
 app/addons/search/components/AnalyzerDropdown.js   |  77 ++++
 app/addons/search/components/AnalyzerMultiple.js   |  94 +++++
 app/addons/search/components/AnalyzerRow.js        |  84 ++++
 app/addons/search/components/SearchForm.js         | 126 ++++++
 .../search/components/SearchFormContainer.js       |  36 ++
 app/addons/search/components/SearchIndexEditor.js  | 164 ++++++++
 .../components/SearchIndexEditorContainer.js       |  82 ++++
 app/addons/search/constants.js                     |  18 +
 app/addons/search/layout.js                        | 101 +++++
 app/addons/search/reducers.js                      | 314 +++++++++++++++
 app/addons/search/resources.js                     |  72 ++++
 app/addons/search/routes.js                        | 272 +++++++++++++
 .../search/tests/nightwatch/cloneSearchIndex.js    |  68 ++++
 .../search/tests/nightwatch/createNewSearch.js     | 130 +++++++
 .../search/tests/nightwatch/deleteSearchIndex.js   | 107 ++++++
 .../search/tests/nightwatch/searchPageApiBar.js    |  55 +++
 app/addons/search/tests/nightwatch/sharedSearch.js |  39 ++
 27 files changed, 3116 insertions(+)

diff --git a/.gitignore b/.gitignore
index 7865768..e8b3d39 100644
--- a/.gitignore
+++ b/.gitignore
@@ -20,6 +20,7 @@ app/addons/*
 !app/addons/styletests
 !app/addons/cors
 !app/addons/setup
+!app/addons/search
 settings.json*
 i18n.json
 !settings.json.default
diff --git a/app/addons/search/__tests__/components.test.js b/app/addons/search/__tests__/components.test.js
new file mode 100644
index 0000000..6bb6f39
--- /dev/null
+++ b/app/addons/search/__tests__/components.test.js
@@ -0,0 +1,160 @@
+/*
+* Licensed Materials - Property of IBM
+*
+* "Restricted Materials of IBM"
+*
+* (C) Copyright IBM Corp. 2018 All Rights Reserved
+*
+* US Government Users Restricted Rights - Use, duplication or disclosure
+* restricted by GSA ADP Schedule Contract with IBM Corp.
+*/
+
+import {mount} from 'enzyme';
+import React from 'react';
+import sinon from 'sinon';
+import FauxtonAPI from '../../../core/api';
+import AnalyzerDropdown from '../components/AnalyzerDropdown';
+import SearchForm from '../components/SearchForm';
+import SearchIndexEditor from '../components/SearchIndexEditor';
+import '../base';
+
+describe('SearchIndexEditor', () => {
+  const defaultProps = {
+    isLoading: false,
+    isCreatingIndex: false,
+    database: { id: 'my_db' },
+    lastSavedDesignDocName: 'last_ddoc',
+    lastSavedSearchIndexName: 'last_idx',
+    searchIndexFunction: '',
+    saveDoc: {},
+    designDocs: [],
+    searchIndexName: '',
+    ddocPartitioned: false,
+    newDesignDocPartitioned: false,
+    analyzerType: '',
+    singleAnalyzer: '',
+    defaultAnalyzer: '',
+    defaultMultipleAnalyzer: '',
+    analyzerFields: [],
+    setAnalyzerType: () => {},
+    setDefaultMultipleAnalyzer: () => {},
+    setSingleAnalyzer: () => {},
+    addAnalyzerRow: () => {},
+    setSearchIndexName: () => {},
+    saveSearchIndex: () => {},
+    selectDesignDoc: () => {},
+    updateNewDesignDocName: () => {}
+  };
+
+  it('generates the correct cancel link when db, ddoc and views have special chars', () => {
+    const editorEl = mount(<SearchIndexEditor
+      {...defaultProps}
+      database={{ id: 'db%$1' }}
+      lastSavedDesignDocName={'_design/doc/1$2'}
+      lastSavedSearchIndexName={'search?abc/123'}
+    />);
+    const expectedUrl = `/${encodeURIComponent('db%$1')}/_design/${encodeURIComponent('doc/1$2')}/_search/${encodeURIComponent('search?abc/123')}`;
+    expect(editorEl.find('a.index-cancel-link').prop('href')).toMatch(expectedUrl);
+  });
+
+  it('does not save when missing the index name', () => {
+    const spy = sinon.stub();
+    const editorEl = mount(<SearchIndexEditor
+      {...defaultProps}
+      database={{ id: 'test_db' }}
+      designDocs={[{id: '_design/d1'}, {id: '_design/d2'}]}
+      ddocName='_design/d1'
+      searchIndexName={''}
+      saveSearchIndex={spy}
+      saveDoc={{id: '_design/d'}}
+    />);
+
+    editorEl.find('button#save-index').simulate('click', {preventDefault: () => {}});
+    sinon.assert.notCalled(spy);
+  });
+});
+
+describe('AnalyzerDropdown', () => {
+
+  it('check default values and settings', () => {
+    const el = mount(<AnalyzerDropdown />);
+
+    // confirm default label
+    expect(el.find('label').length).toBe(2);
+    expect(el.find('label').first().text()).toBe('Type');
+
+    // confirm default value
+    expect(el.find('select').hasClass('standard')).toBeTruthy();
+  });
+
+  it('omits label element if empty label passed', () => {
+    const el = mount(<AnalyzerDropdown label="" />);
+
+    // (1, because there are normally 2 labels, see prev test)
+    expect(el.find('label').length).toBe(1);
+  });
+
+  it('custom ID works', () => {
+    const customID = 'myCustomID';
+    const el = mount(<AnalyzerDropdown id={customID} />);
+    expect(el.find('select').prop('id')).toBe(customID);
+  });
+
+  it('sets default value', () => {
+    const defaultSelected = 'russian';
+    const el = mount(
+      <AnalyzerDropdown defaultSelected={defaultSelected} />
+    );
+
+    expect(el.find('select').hasClass(defaultSelected)).toBeTruthy();
+  });
+
+  it('custom classes get applied', () => {
+    const el = mount(<AnalyzerDropdown classes="nuthatch vulture" />);
+    expect(el.find('.nuthatch').exists()).toBeTruthy();
+    expect(el.find('.vulture').exists()).toBeTruthy();
+  });
+
+  it('custom change handler gets called', () => {
+    const spy = sinon.spy();
+    const el = mount(<AnalyzerDropdown onChange={spy} />);
+    const newVal = 'whitespace';
+    el.find('select').simulate('change', { target: { value: newVal }});
+    expect(spy.calledOnce).toBeTruthy();
+  });
+
+});
+
+describe('SearchForm', () => {
+  const defaultProps = {
+    searchResults: [{id: 'elephant'}],
+    searchPerformed: true,
+    hasActiveQuery: false,
+    searchQuery: 'a_search',
+    database: { id: 'foo' },
+    querySearch: () => {},
+    setSearchQuery: () => {}
+  };
+
+  beforeEach(() => {
+    sinon.stub(FauxtonAPI, 'urls').returns('/fake/url');
+  });
+
+  afterEach(() => {
+    FauxtonAPI.urls.restore();
+  });
+
+  it('renders docs from the search results', () => {
+    const el = mount(<SearchForm
+      {...defaultProps}
+    />);
+    expect(el.find('pre').first().text('elephant')).toBeTruthy();
+  });
+
+  it('renders with links', () => {
+    const el = mount(<SearchForm
+      {...defaultProps}
+    />);
+    expect(el.find('a')).toBeTruthy();
+  });
+});
diff --git a/app/addons/search/__tests__/search.actions.test.js b/app/addons/search/__tests__/search.actions.test.js
new file mode 100644
index 0000000..99edd28
--- /dev/null
+++ b/app/addons/search/__tests__/search.actions.test.js
@@ -0,0 +1,52 @@
+/*
+* Licensed Materials - Property of IBM
+*
+* "Restricted Materials of IBM"
+*
+* (C) Copyright IBM Corp. 2018 All Rights Reserved
+*
+* US Government Users Restricted Rights - Use, duplication or disclosure
+* restricted by GSA ADP Schedule Contract with IBM Corp.
+*/
+
+import sinon from 'sinon';
+import utils from '../../../../test/mocha/testUtils';
+import FauxtonAPI from '../../../core/api';
+import Actions from '../actions';
+import * as API from '../api';
+import '../base';
+import '../../documents/base';
+
+const {restore} = utils;
+FauxtonAPI.router = new FauxtonAPI.Router([]);
+
+describe('search actions', () => {
+
+  afterEach(() => {
+    restore(FauxtonAPI.navigate);
+    restore(FauxtonAPI.addNotification);
+    restore(API.fetchSearchResults);
+  });
+
+  it("should show a notification and redirect if database doesn't exist", () => {
+    const navigateSpy = sinon.spy(FauxtonAPI, 'navigate');
+    const notificationSpy = sinon.spy(FauxtonAPI, 'addNotification');
+    sinon.stub(API, 'fetchSearchResults').rejects(new Error('db not found'));
+    FauxtonAPI.reduxDispatch = () => {};
+
+    const params = {
+      databaseName: 'safe-id-db',
+      designDoc: 'design-doc',
+      indexName: 'idx1',
+      query: 'a_query'
+    };
+    return Actions.dispatchInitSearchIndex(params)
+      .then(() => {
+        expect(notificationSpy.calledOnce).toBeTruthy();
+        expect(/db not found/.test(notificationSpy.args[0][0].msg)).toBeTruthy();
+        expect(navigateSpy.calledOnce).toBeTruthy();
+        expect(navigateSpy.args[0][0]).toBe('database/safe-id-db/_all_docs');
+      });
+  });
+
+});
diff --git a/app/addons/search/__tests__/search.reducers.test.js b/app/addons/search/__tests__/search.reducers.test.js
new file mode 100644
index 0000000..f08d269
--- /dev/null
+++ b/app/addons/search/__tests__/search.reducers.test.js
@@ -0,0 +1,142 @@
+/*
+* Licensed Materials - Property of IBM
+*
+* "Restricted Materials of IBM"
+*
+* (C) Copyright IBM Corp. 2018 All Rights Reserved
+*
+* US Government Users Restricted Rights - Use, duplication or disclosure
+* restricted by GSA ADP Schedule Contract with IBM Corp.
+*/
+
+import reducer from '../reducers';
+import ActionTypes from '../actiontypes';
+
+describe('Search Reducer', () => {
+
+  it('adds an analyzer row', () => {
+    const action = {
+      type: ActionTypes.SEARCH_INDEX_ADD_ANALYZER_ROW,
+      options: {
+        analyzer: 'sample',
+        fieldName: 'f1'
+      }
+    };
+    let newState = reducer(undefined, { type: 'DO_NOTHING' });
+    expect(newState.analyzerFields).toHaveLength(0);
+    newState = reducer(newState, action);
+    expect(newState.analyzerFields).toHaveLength(1);
+    expect(newState.analyzerFields[0].analyzer).toBe('sample');
+    expect(newState.analyzerFields[0].valid).toBe(true);
+  });
+
+  it('updates field name of an existing row', () => {
+    const action = {
+      type: ActionTypes.SEARCH_INDEX_ADD_ANALYZER_ROW,
+      options: {
+        analyzer: 'sample',
+        fieldName: 'f1'
+      }
+    };
+    let newState = reducer(undefined, action);
+    action.options.fieldName = 'f2';
+    newState = reducer(newState, action);
+    action.options.fieldName = 'f3';
+    newState = reducer(newState, action);
+
+    const removeAction = {
+      type: ActionTypes.SEARCH_INDEX_SET_ANALYZER_ROW_FIELD_NAME,
+      options: { rowIndex: 1, fieldName: 'f100' }
+    };
+    newState = reducer(newState, removeAction);
+    expect(newState.analyzerFields[0].fieldName).toBe('f1');
+    expect(newState.analyzerFields[1].fieldName).toBe('f100');
+    expect(newState.analyzerFields[2].fieldName).toBe('f3');
+  });
+
+  it('updates analyzer of an existing row', () => {
+    const action = {
+      type: ActionTypes.SEARCH_INDEX_ADD_ANALYZER_ROW,
+      options: {
+        analyzer: 'sample',
+        fieldName: 'f1'
+      }
+    };
+    let newState = reducer(undefined, action);
+    action.options.fieldName = 'f2';
+    newState = reducer(newState, action);
+    action.options.fieldName = 'f3';
+    newState = reducer(newState, action);
+
+    const removeAction = {
+      type: ActionTypes.SEARCH_INDEX_SET_ANALYZER_ROW,
+      options: { rowIndex: 1, analyzer: 'keyword' }
+    };
+    newState = reducer(newState, removeAction);
+    expect(newState.analyzerFields[0].analyzer).toBe('sample');
+    expect(newState.analyzerFields[1].analyzer).toBe('keyword');
+    expect(newState.analyzerFields[2].analyzer).toBe('sample');
+  });
+
+  it('removes an analyzer row', () => {
+    const action = {
+      type: ActionTypes.SEARCH_INDEX_ADD_ANALYZER_ROW,
+      options: {
+        analyzer: 'sample',
+        fieldName: 'f1'
+      }
+    };
+    let newState = reducer(undefined, action);
+    action.options.fieldName = 'f2';
+    newState = reducer(newState, action);
+    action.options.fieldName = 'f3';
+    newState = reducer(newState, action);
+
+    const removeAction = {
+      type: ActionTypes.SEARCH_INDEX_REMOVE_ANALYZER_ROW,
+      options: { rowIndex: 1 }
+    };
+    newState = reducer(newState, removeAction);
+    expect(newState.analyzerFields).toHaveLength(2);
+    expect(newState.analyzerFields[0].fieldName).toBe('f1');
+    expect(newState.analyzerFields[1].fieldName).toBe('f3');
+  });
+
+  it('updates search results and resets the hasActiveQuery flag', () => {
+    let newState = reducer(undefined, {
+      type: ActionTypes.SEARCH_INDEX_PREVIEW_REQUEST_MADE
+    });
+    expect(newState.hasActiveQuery).toBe(true);
+
+    const action = {
+      type: ActionTypes.SEARCH_INDEX_PREVIEW_MODEL_UPDATED,
+      options: {
+        searchResults: ['result1', 'result2']
+      }
+    };
+    newState = reducer(newState, action);
+    expect(newState.searchResults).toHaveLength(2);
+    expect(newState.hasActiveQuery).toBe(false);
+  });
+
+  it('resets the search results when the search term is empty', () => {
+    const action = {
+      type: ActionTypes.SEARCH_INDEX_PREVIEW_MODEL_UPDATED,
+      options: {
+        searchResults: ['result1', 'result2']
+      }
+    };
+    let newState = reducer(undefined, action);
+    expect(newState.searchResults).toHaveLength(2);
+
+    const initAction = {
+      type: ActionTypes.SEARCH_INDEX_INIT,
+      options: {
+        searchQuery: ''
+      }
+    };
+    newState = reducer(newState, initAction);
+    expect(newState.searchResults).toBeUndefined();
+  });
+
+});
diff --git a/app/addons/search/actions.js b/app/addons/search/actions.js
new file mode 100644
index 0000000..82ac0f1
--- /dev/null
+++ b/app/addons/search/actions.js
@@ -0,0 +1,428 @@
+/*
+* Licensed Materials - Property of IBM
+*
+* "Restricted Materials of IBM"
+*
+* (C) Copyright IBM Corp. 2018 All Rights Reserved
+*
+* US Government Users Restricted Rights - Use, duplication or disclosure
+* restricted by GSA ADP Schedule Contract with IBM Corp.
+*/
+
+import FauxtonAPI from '../../core/api';
+import ActionTypes from './actiontypes';
+import CloudantSearch from './resources';
+import Documents from '../documents/base';
+import SidebarActions from '../documents/sidebar/actions';
+import IndexEditorActions from '../documents/index-editor/actions';
+import * as API from './api';
+
+const dispatchInitNewSearchIndex = (params) => {
+  // ensure we start with a clear slate
+  FauxtonAPI.reduxDispatch({ type: ActionTypes.SEARCH_INDEX_CLEAR });
+  FauxtonAPI.reduxDispatch({
+    type: ActionTypes.SEARCH_INDEX_SET_LOADING,
+    options: {
+      loading: true
+    }
+  });
+
+  params.designDocs.fetch().then(() => {
+    FauxtonAPI.reduxDispatch({
+      type: ActionTypes.SEARCH_INDEX_DESIGN_DOCS_LOADED,
+      options: {
+        designDocs: params.designDocs,
+        defaultDDoc: params.defaultDDoc,
+        database: params.database
+      }
+    });
+  });
+};
+
+const dispatchInitSearchIndex = (params)  => {
+  FauxtonAPI.reduxDispatch({
+    type: ActionTypes.SEARCH_INDEX_SET_LOADING,
+    options: {
+      loading: true
+    }
+  });
+
+  FauxtonAPI.reduxDispatch({
+    type: ActionTypes.SEARCH_INDEX_INIT,
+    options: {
+      databaseName: params.databaseName,
+      partitionKey: params.partitionKey,
+      ddocName: params.designDoc,
+      indexName: params.indexName,
+      searchQuery: params.query ? params.query : ''
+    }
+  });
+
+  if (params.query) {
+    return executSearchQuery(params.databaseName, params.partitionKey, params.designDoc,
+      params.indexName, params.query, FauxtonAPI.reduxDispatch);
+  }
+};
+
+const dispatchEditSearchIndex = (params) => {
+  var ddocInfo = new Documents.DdocInfo({ _id: params.ddocID }, { database: params.database });
+
+  FauxtonAPI.reduxDispatch({
+    type: ActionTypes.SEARCH_INDEX_SET_LOADING,
+    options: {
+      loading: true
+    }
+  });
+
+  FauxtonAPI.Promise.all([params.designDocs.fetch(), ddocInfo.fetch()]).then(([ddocs]) => {
+    const ddoc = ddocs.rows.find(ddoc => ddoc._id === ddocInfo.id).doc;
+    if (!ddoc.indexes || !ddoc.indexes[params.indexName]) {
+      throw Error(`Index "${params.indexName}" not found`);
+    }
+    FauxtonAPI.reduxDispatch({
+      type: ActionTypes.SEARCH_INDEX_INIT_EDIT_SEARCH_INDEX,
+      options: {
+        indexName: params.indexName,
+        database: params.database,
+        ddocInfo: ddocInfo,
+        designDocs: params.designDocs
+      }
+    });
+  }).catch(err => {
+    const details = err.message ? err.message : '';
+    FauxtonAPI.addNotification({
+      msg: `There was a problem editing the search index "${params.indexName}". ` + details,
+      type: 'error',
+      clear: true
+    });
+  });
+};
+
+const selectTab = (tab) => (dispatch) => {
+  dispatch({
+    type: ActionTypes.SEARCH_INDEX_SELECT_TAB,
+    options: {
+      tab: tab
+    }
+  });
+};
+
+const setSearchIndexName = (str) => (dispatch) => {
+  dispatch({
+    type: ActionTypes.SEARCH_INDEX_SET_NAME,
+    options: {
+      value: str
+    }
+  });
+};
+
+const setAnalyzerType = (type) => (dispatch) => {
+  dispatch({
+    type: ActionTypes.SEARCH_INDEX_SET_ANALYZER_TYPE,
+    options: {
+      value: type
+    }
+  });
+};
+
+const addAnalyzerRow = (analyzer) => (dispatch) => {
+  dispatch({
+    type: ActionTypes.SEARCH_INDEX_ADD_ANALYZER_ROW,
+    options: {
+      analyzer: analyzer
+    }
+  });
+};
+
+const removeAnalyzerRow = (rowIndex) => (dispatch) => {
+  dispatch({
+    type: ActionTypes.SEARCH_INDEX_REMOVE_ANALYZER_ROW,
+    options: {
+      rowIndex: rowIndex
+    }
+  });
+};
+
+const setAnalyzerRowFieldName = (params) => (dispatch) => {
+  dispatch({
+    type: ActionTypes.SEARCH_INDEX_SET_ANALYZER_ROW_FIELD_NAME,
+    options: {
+      rowIndex: params.rowIndex,
+      fieldName: params.fieldName
+    }
+  });
+};
+
+const setAnalyzer = (params) => (dispatch) => {
+  dispatch({
+    type: ActionTypes.SEARCH_INDEX_SET_ANALYZER_ROW,
+    options: {
+      rowIndex: params.rowIndex,
+      analyzer: params.analyzer
+    }
+  });
+};
+
+const setDefaultMultipleAnalyzer = (analyzer) => (dispatch) => {
+  dispatch({
+    type: ActionTypes.SEARCH_INDEX_SET_DEFAULT_MULTIPLE_ANALYZER,
+    options: {
+      analyzer: analyzer
+    }
+  });
+};
+
+const setSingleAnalyzer = (analyzer) => (dispatch) => {
+  dispatch({
+    type: ActionTypes.SEARCH_INDEX_SET_SINGLE_ANALYZER,
+    options: {
+      analyzer: analyzer
+    }
+  });
+};
+
+const saveSearchIndex = (doc, info, navigateToUrl) => {
+  doc.setIndex(info.indexName, info.indexFunction, info.analyzerInfo);
+
+  if (info.lastSavedDesignDocName === doc.id && info.lastSavedSearchIndexName !== info.indexName) {
+    var indexes = doc.get('indexes') || {};
+    delete indexes[info.lastSavedSearchIndexName];
+    doc.set({ indexes: indexes });
+  }
+
+  doc.save().then(() => {
+    info.designDocs.add(doc, { merge: true });
+
+    FauxtonAPI.addNotification({
+      msg: 'The search index has been saved.',
+      type: 'success',
+      clear: true
+    });
+
+    // if the user just saved the view to a different design doc, remove the view from the old design doc and
+    // maybe even delete if it's empty
+    if (!info.isCreatingIndex && info.lastSavedDesignDocName !== doc.id) {
+      const oldDesignDoc = IndexEditorActions.helpers.findDesignDoc(info.designDocs, info.lastSavedDesignDocName);
+      IndexEditorActions.safeDeleteIndex(oldDesignDoc, info.designDocs, 'indexes', info.lastSavedSearchIndexName, {
+        onSuccess: () => {
+          SidebarActions.dispatchUpdateDesignDocs(info.designDocs);
+        }
+      });
+    }
+
+    SidebarActions.dispatchUpdateDesignDocs(info.designDocs);
+    FauxtonAPI.navigate(navigateToUrl, { trigger: true });
+  }, (xhr) => {
+    const responseText = JSON.parse(xhr.responseText).reason;
+    FauxtonAPI.addNotification({
+      msg: 'Save failed: ' + responseText,
+      type: 'error',
+      clear: true
+    });
+  });
+};
+
+
+const deleteSearchIndex = (options) => {
+  const onSuccess = () => {
+
+    // if the user was on the index that was just deleted, redirect them back to all docs
+    if (options.isOnIndex) {
+      const url = FauxtonAPI.urls('allDocs', 'app', options.database.safeID());
+      FauxtonAPI.navigate(url);
+    }
+    SidebarActions.dispatchUpdateDesignDocs(options.designDocs);
+
+    FauxtonAPI.addNotification({
+      msg: 'The <code>' + _.escape(options.indexName) + '</code> search index has been deleted.',
+      type: 'info',
+      escape: false,
+      clear: true
+    });
+    SidebarActions.dispatchHideDeleteIndexModal();
+  };
+
+  IndexEditorActions.safeDeleteIndex(options.designDoc, options.designDocs, 'indexes', options.indexName, { onSuccess });
+};
+
+const cloneSearchIndex = (params) => {
+  const targetDesignDoc = getDesignDoc(params.designDocs, params.targetDesignDocName, params.newDesignDocName, params.database);
+  let indexes = targetDesignDoc.get('indexes');
+  if (indexes && _.has(indexes, params.newIndexName)) {
+    FauxtonAPI.addNotification({
+      msg: 'That index name is already used in this design doc. Please enter a new name.',
+      type: 'error',
+      clear: true
+    });
+    return;
+  }
+  if (!indexes) {
+    indexes = {};
+  }
+  const sourceDesignDoc = IndexEditorActions.helpers.findDesignDoc(params.designDocs, '_design/' + params.sourceDesignDocName);
+  const sourceDesignDocJSON = sourceDesignDoc.toJSON();
+
+  // this sets whatever content is in the source index into the target design doc under the new index name
+  indexes[params.newIndexName] = sourceDesignDocJSON.indexes[params.sourceIndexName];
+  targetDesignDoc.set({ indexes: indexes });
+
+  targetDesignDoc.save().then(function () {
+    params.onComplete();
+    FauxtonAPI.addNotification({
+      msg: 'The search index has been cloned.',
+      type: 'success',
+      clear: true
+    });
+
+    SidebarActions.dispatchUpdateDesignDocs(params.designDocs);
+  },
+  function (xhr) {
+    params.onComplete();
+    var responseText = JSON.parse(xhr.responseText).reason;
+    FauxtonAPI.addNotification({
+      msg: 'Clone failed: ' + responseText,
+      type: 'error',
+      clear: true
+    });
+  });
+};
+
+const gotoEditSearchIndexPage = (databaseName, partitionKey, designDocName, indexName) => {
+  const encodedPartKey = partitionKey ? encodeURIComponent(partitionKey) : '';
+  FauxtonAPI.navigate('#' + FauxtonAPI.urls('search', 'edit', encodeURIComponent(databaseName),
+    encodedPartKey, encodeURIComponent(designDocName), encodeURIComponent(indexName)));
+};
+
+const selectDesignDoc = (designDoc) => (dispatch) => {
+  dispatch({
+    type: ActionTypes.SEARCH_INDEX_SELECT_DESIGN_DOC,
+    options: {
+      value: designDoc
+    }
+  });
+};
+
+// const querySearch = (searchQuery, partitionKey) => {
+const querySearch = (databaseName, partitionKey, ddocName, indexName, searchQuery) => {
+  const baseUrl = partitionKey ?
+    FauxtonAPI.urls('partitioned_search', 'app', encodeURIComponent(databaseName), encodeURIComponent(partitionKey), encodeURIComponent(ddocName), encodeURIComponent(indexName)) :
+    FauxtonAPI.urls('search', 'app', encodeURIComponent(databaseName), encodeURIComponent(ddocName), encodeURIComponent(indexName));
+  FauxtonAPI.navigate(`${baseUrl}${encodeURIComponent(indexName)}?${encodeURIComponent(searchQuery)}`, {trigger: true});
+};
+
+const executSearchQuery = (database, partitionKey, ddoc, index, searchQuery, dispatch) => {
+  dispatch({ type: ActionTypes.SEARCH_INDEX_PREVIEW_REQUEST_MADE });
+
+  return API.fetchSearchResults(database, partitionKey, ddoc, index, searchQuery)
+    .then(rows => {
+      dispatch({
+        type: ActionTypes.SEARCH_INDEX_PREVIEW_MODEL_UPDATED,
+        options: {
+          searchResults: rows
+        }
+      });
+    }).catch(err => {
+      dispatch({ type: ActionTypes.SEARCH_INDEX_PREVIEW_REQUEST_ERROR });
+
+      if (err && err.message.includes('`partition` not supported')) {
+        dispatch(partitionParamNotSupported());
+      } else if (err && err.message.includes('`partition` parameter is mandatory')) {
+        dispatch(partitionParamIsMandatory());
+      } else {
+        FauxtonAPI.addNotification({
+          msg: 'Search failed: ' + err.message,
+          type: 'error',
+          clear: true
+        });
+      }
+
+      if (err.message.includes('not found')) {
+        FauxtonAPI.navigate(FauxtonAPI.urls('allDocsSanitized', 'app', database), {trigger: true});
+      }
+    });
+};
+
+const setSearchQuery = (query) => (dispatch) => {
+  dispatch({
+    type: ActionTypes.SEARCH_INDEX_SET_SEARCH_QUERY,
+    options: {
+      query: query
+    }
+  });
+};
+
+const updateNewDesignDocName = (designDocName) => (dispatch) => {
+  dispatch({
+    type: ActionTypes.SEARCH_INDEX_NEW_DESIGN_DOC_NAME_UPDATED,
+    options: {
+      value: designDocName
+    }
+  });
+};
+
+const updateNewDesignDocPartitioned = (isPartitioned) => (dispatch) => {
+  dispatch({
+    type: ActionTypes.SEARCH_INDEX_NEW_DESIGN_DOC_PARTITONED_UPDATED,
+    options: {
+      value: isPartitioned
+    }
+  });
+};
+
+const partitionParamNotSupported = () => {
+  return {
+    type: ActionTypes.SEARCH_INDEX_PARTITION_PARAM_NOT_SUPPORTED
+  };
+};
+
+const partitionParamIsMandatory = () => {
+  return {
+    type: ActionTypes.SEARCH_INDEX_PARTITION_PARAM_MANDATORY
+  };
+};
+
+
+// helpers
+
+function getDesignDoc (designDocs, targetDesignDocName, newDesignDocName, database) {
+  if (targetDesignDocName === 'new-doc') {
+    var doc = {
+      "_id": "_design/" + newDesignDocName,
+      "indexes": {},
+      "language": "javascript"
+    };
+    return new CloudantSearch.Doc(doc, { database: database });
+  }
+
+  var foundDoc = designDocs.find(function (ddoc) {
+    return ddoc.id === targetDesignDocName;
+  });
+  return (!foundDoc) ? null : foundDoc.dDocModel();
+}
+
+
+export default {
+  dispatchInitNewSearchIndex,
+  dispatchInitSearchIndex,
+  dispatchEditSearchIndex,
+  selectTab,
+  setSearchIndexName,
+  setAnalyzerType,
+  addAnalyzerRow,
+  removeAnalyzerRow,
+  setAnalyzerRowFieldName,
+  setAnalyzer,
+  setDefaultMultipleAnalyzer,
+  selectDesignDoc,
+  saveSearchIndex,
+  cloneSearchIndex,
+  deleteSearchIndex,
+  gotoEditSearchIndexPage,
+  querySearch,
+  setSearchQuery,
+  setSingleAnalyzer,
+  updateNewDesignDocName,
+  updateNewDesignDocPartitioned,
+  partitionParamNotSupported,
+  partitionParamIsMandatory
+};
diff --git a/app/addons/search/actiontypes.js b/app/addons/search/actiontypes.js
new file mode 100644
index 0000000..c5bbb93
--- /dev/null
+++ b/app/addons/search/actiontypes.js
@@ -0,0 +1,36 @@
+/*
+* Licensed Materials - Property of IBM
+*
+* "Restricted Materials of IBM"
+*
+* (C) Copyright IBM Corp. 2018 All Rights Reserved
+*
+* US Government Users Restricted Rights - Use, duplication or disclosure
+* restricted by GSA ADP Schedule Contract with IBM Corp.
+*/
+export default {
+  SEARCH_INDEX_DESIGN_DOCS_LOADED: 'SEARCH_INDEX_DESIGN_DOCS_LOADED',
+  SEARCH_INDEX_INIT: 'SEARCH_INDEX_INIT',
+  SEARCH_INDEX_SELECT_TAB: 'SEARCH_INDEX_SELECT_TAB',
+  SEARCH_INDEX_SET_NAME: 'SEARCH_INDEX_SET_NAME',
+  SEARCH_INDEX_SET_ANALYZER_TYPE: 'SEARCH_INDEX_SET_ANALYZER_TYPE',
+  SEARCH_INDEX_ADD_ANALYZER_ROW: 'SEARCH_INDEX_ADD_ANALYZER_ROW',
+  SEARCH_INDEX_REMOVE_ANALYZER_ROW: 'SEARCH_INDEX_REMOVE_ANALYZER_ROW',
+  SEARCH_INDEX_SET_ANALYZER_ROW_FIELD_NAME: 'SEARCH_INDEX_SET_ANALYZER_ROW_FIELD_NAME',
+  SEARCH_INDEX_SET_ANALYZER_ROW: 'SEARCH_INDEX_SET_ANALYZER_ROW',
+  SEARCH_INDEX_SET_DEFAULT_MULTIPLE_ANALYZER: 'SEARCH_INDEX_SET_DEFAULT_MULTIPLE_ANALYZER',
+  SEARCH_INDEX_INIT_EDIT_SEARCH_INDEX: 'SEARCH_INDEX_INIT_EDIT_SEARCH_INDEX',
+  SEARCH_INDEX_PREVIEW_MODEL_UPDATED: 'SEARCH_INDEX_PREVIEW_MODEL_UPDATED',
+  SEARCH_INDEX_PREVIEW_REQUEST_MADE: 'SEARCH_INDEX_PREVIEW_REQUEST_MADE',
+  SEARCH_INDEX_PREVIEW_REQUEST_ERROR: 'PREVIEW_MODEL_REQUEST_ERROR',
+  SEARCH_INDEX_SELECT_DESIGN_DOC: 'SEARCH_INDEX_SELECT_DESIGN_DOC',
+  SEARCH_INDEX_SET_SINGLE_ANALYZER: 'SEARCH_INDEX_SET_SINGLE_ANALYZER',
+  SEARCH_INDEX_SET_SEARCH_QUERY: 'SEARCH_INDEX_SET_SEARCH_QUERY',
+  SEARCH_INDEX_SET_LOADING: 'SEARCH_INDEX_SET_LOADING',
+  SEARCH_INDEX_CLEAR: 'SEARCH_INDEX_CLEAR',
+  SEARCH_INDEX_UPDATE_INDEX_FUNCTION: 'SEARCH_INDEX_UPDATE_INDEX_FUNCTION',
+  SEARCH_INDEX_NEW_DESIGN_DOC_NAME_UPDATED: 'SEARCH_INDEX_NEW_DESIGN_DOC_NAME_UPDATED',
+  SEARCH_INDEX_NEW_DESIGN_DOC_PARTITONED_UPDATED: 'SEARCH_INDEX_NEW_DESIGN_DOC_PARTITONED_UPDATED',
+  SEARCH_INDEX_PARTITION_PARAM_NOT_SUPPORTED: 'SEARCH_INDEX_PARTITION_PARAM_NOT_SUPPORTED',
+  SEARCH_INDEX_PARTITION_PARAM_MANDATORY: 'SEARCH_INDEX_PARTITION_PARAM_MANDATORY'
+};
diff --git a/app/addons/search/api.js b/app/addons/search/api.js
new file mode 100644
index 0000000..cbfdbcd
--- /dev/null
+++ b/app/addons/search/api.js
@@ -0,0 +1,32 @@
+/*
+* Licensed Materials - Property of IBM
+*
+* "Restricted Materials of IBM"
+*
+* (C) Copyright IBM Corp. 2018 All Rights Reserved
+*
+* US Government Users Restricted Rights - Use, duplication or disclosure
+* restricted by GSA ADP Schedule Contract with IBM Corp.
+*/
+
+import FauxtonAPI from '../../core/api';
+import { get } from '../../core/ajax';
+
+function searchUrl(database, partitionKey, ddoc, index, searchQuery) {
+  //https://[username].cloudant.com/animaldb/_design/views101/_search/animals?q=kookaburra
+  const encodedPartKey = partitionKey ? encodeURIComponent(partitionKey) : '';
+  return FauxtonAPI.urls('search', 'server', encodeURIComponent(database), encodedPartKey,
+    encodeURIComponent(ddoc), encodeURIComponent(index),
+    '?limit=10&q=' + encodeURIComponent(searchQuery));
+}
+
+export const fetchSearchResults = (database, partitionKey, ddoc, index, searchQuery) => {
+  const url = searchUrl(database, partitionKey, ddoc, index, searchQuery);
+  return get(url).then((res) => {
+    if (res.error) {
+      throw new Error(res.reason);
+    }
+
+    return res.rows;
+  });
+};
diff --git a/app/addons/search/assets/less/search.less b/app/addons/search/assets/less/search.less
new file mode 100644
index 0000000..68c7589
--- /dev/null
+++ b/app/addons/search/assets/less/search.less
@@ -0,0 +1,159 @@
+/*
+* Licensed Materials - Property of IBM
+*
+* "Restricted Materials of IBM"
+*
+* (C) Copyright IBM Corp. 2018 All Rights Reserved
+*
+* US Government Users Restricted Rights - Use, duplication or disclosure
+* restricted by GSA ADP Schedule Contract with IBM Corp.
+*/
+@import "../../../style/assets/less/variables.less";
+@import "../../../style/assets/less/mixins.less";
+
+.search-index-page-loading {
+  margin-top: 20px;
+}
+
+.edit-search-index-page-loading {
+  margin-top: 25px;
+}
+
+.index-tabs.dashboard-upper-menu {
+  position: inherit;
+  &.index-tabs {
+    border-bottom: 0;
+    .fonticon:before {
+      margin-right: 6px;
+    }
+  }
+}
+
+#queryText{
+  max-width: 300px;
+}
+
+.ace-editor-section .ace_editor {
+  font-size: 13px;
+  width: 100%;
+  line-height: 22px;
+}
+
+.search-query-save {
+  max-width: 100%;
+  .design-doc-group #new-ddoc-section {
+    margin-top: 24px;
+    label {
+      padding-top: 12px;
+    }
+  }
+}
+
+#analyzer-fields {
+  margin-top: 10px;
+
+  li {
+    margin-bottom: 5px;
+
+    &:first-child {
+      button {
+        margin-top: 27px;
+      }
+    }
+    div:first-child {
+      margin-left: 0;
+    }
+  }
+
+  .span4 {
+    margin-right: 3px;
+    width: 30%;
+
+    .styled-select, select {
+      width: 100%;
+    }
+  }
+}
+
+.delete-analyzer {
+  margin-top: 3px;
+  margin-left: 15px;
+}
+
+.search-index-content>div {
+  margin: 20px 10px 0 20px;
+  .icon-question-sign {
+    margin-left: 4px;
+    &:hover {
+      color: @hoverHighlight;
+    }
+  }
+}
+
+.search-index-function {
+  label {
+    margin-right: 0;
+  }
+  .ace_editor {
+    line-height: 22px;
+  }
+}
+
+#search-query-submit {
+  padding: 12px;
+}
+
+#search-index-preview-form {
+  margin: 5px 0 20px;
+  .input-append {
+    vertical-align: bottom;
+  }
+  .help-link {
+    display: inline-block;
+    margin-left: 12px;
+    padding-bottom: 23px;
+  }
+}
+
+#search-index-help {
+  h4 {
+    margin-top: 18px;
+  }
+
+  ul {
+    margin: 0 0 10px;
+    padding: 0;
+    list-style-type: none;
+  }
+  .search-index-examples {
+    li {
+      line-height: 28px;
+      span {
+        .search-index-code-section;
+      }
+    }
+  }
+}
+
+.search-index-tab-content {
+  a:hover {
+    text-decoration: none;
+  }
+}
+
+.search-index-code-section {
+  font-size: 12px;
+  font-family: Monaco, Menlo, Consolas, "Courier New", monospace;
+  background-color: #ddd;
+  padding: 5px;
+  border-radius: 3px;
+}
+
+#search-index-query-button {
+  height: 46px;
+  .border-right-radius(@radius);
+}
+
+body #dashboard-content #search-index-form {
+  position: relative;
+}
diff --git a/app/addons/search/base.js b/app/addons/search/base.js
new file mode 100644
index 0000000..483b8b9
--- /dev/null
+++ b/app/addons/search/base.js
@@ -0,0 +1,125 @@
+/*
+* Licensed Materials - Property of IBM
+*
+* "Restricted Materials of IBM"
+*
+* (C) Copyright IBM Corp. 2018 All Rights Reserved
+*
+* US Government Users Restricted Rights - Use, duplication or disclosure
+* restricted by GSA ADP Schedule Contract with IBM Corp.
+*/
+import app from '../../app';
+import Helpers from '../../helpers';
+import FauxtonAPI from '../../core/api';
+import Actions from './actions';
+import SearchRoutes from './routes';
+import CloudantDocuments from '../cloudantdocuments/routes';
+import reducers from './reducers';
+import './assets/less/search.less';
+
+SearchRoutes.initialize = function () {
+  FauxtonAPI.registerExtension('sidebar:list', {
+    selector: 'indexes',
+    name: 'Search Indexes',
+    urlNamespace: 'search',
+    indexLabel: 'search index', // used for labels
+    onDelete: Actions.deleteSearchIndex,
+    onClone: Actions.cloneSearchIndex,
+    onEdit: Actions.gotoEditSearchIndexPage
+  });
+  FauxtonAPI.registerExtension('sidebar:links', {
+    title: "New Search Index",
+    url: "new_search",
+    icon: 'fonticon-plus-circled',
+    showForPartitionedDDocs: true
+  });
+  FauxtonAPI.registerExtension('sidebar:newLinks', {
+    url: 'new_search',
+    name: 'Search Index'
+  });
+
+  // this tells Fauxton of the new Search Index type. It's used to determine when a design doc is really empty
+  FauxtonAPI.registerExtension('IndexTypes:propNames', 'indexes');
+};
+
+function partitionUrlComponent(partitionKey) {
+  return partitionKey ? '/_partition/' + partitionKey : '';
+}
+
+var proxyUrl = CloudantDocuments.proxyUrl;
+_.extend(CloudantDocuments.sharedUrlPaths, {
+  search: {
+    server: function (db, partitionKey, designDoc, searchName, query) {
+      query = CloudantDocuments.addRemoteAccount(query);
+      return proxyUrl + db + '/_design/' + designDoc + '/_search/' + searchName + query;
+    },
+
+    //NOTE: partitionKey is included here for compatibility with base registered url functions
+    //but is ignored because it's not supported for shared databases
+    app: function (id, designDoc) {
+      return CloudantDocuments.sharedUrlPaths.userHash() + id + '/_design/' + app.utils.safeURLName(designDoc) + '/_search/';
+    },
+
+    edit: function (database, partitionKey, designDoc, indexName) {
+      return CloudantDocuments.sharedUrlPaths.userHash() + database + '/_design/' + designDoc + '/_search/' + indexName + '/edit';
+    },
+
+    apiurl: function (db, partitionKey, designDoc, searchName) {
+      return CloudantDocuments.sharedUrlPaths.host() + encodeURIComponent(db) + '/_design/' +
+        encodeURIComponent(designDoc) + '/_search/' + encodeURIComponent(searchName);
+    },
+
+    fragment: function (id, partitionKey, designDoc, search) {
+      return CloudantDocuments.sharedUrlPaths.userHash() + id + '/_design/' + designDoc + '/_search/' + search;
+    },
+
+    showIndex: function (id, partitionKey, designDoc, search) {
+      return CloudantDocuments.sharedUrlPaths.host() + id + '/' + designDoc +
+        '/_search/' + search;
+    },
+  },
+  partitioned_search: {
+    app: function (id, partitionKey, designDoc) {
+      return CloudantDocuments.sharedUrlPaths.userHash() + id + '/_design/' + app.utils.safeURLName(designDoc) + '/_search/';
+    }
+  }
+});
+
+FauxtonAPI.registerUrls('partitioned_search', {
+  app: function (id, partitionKey, designDoc) {
+    return 'database/' + id + partitionUrlComponent(partitionKey) + '/_design/' + designDoc + '/_search/';
+  }
+});
+
+FauxtonAPI.registerUrls('search', {
+  server: function (id, partitionKey, designDoc, searchName, query) {
+    return Helpers.getServerUrl('/' + id + partitionUrlComponent(partitionKey) + '/_design/' + designDoc + '/_search/' + searchName + query);
+  },
+
+  app: function (id, designDoc) {
+    return 'database/' + id + '/_design/' + designDoc + '/_search/';
+  },
+
+  edit: function (database, partitionKey, designDoc, indexName) {
+    return 'database/' + database + partitionUrlComponent(partitionKey) + '/_design/' + designDoc + '/_search/' + indexName + '/edit';
+  },
+
+  fragment: function (id, partitionKey, designDoc, search) {
+    return 'database/' + id + partitionUrlComponent(partitionKey) + '/_design/' + designDoc + '/_search/' + search;
+  },
+
+  showIndex: function (id, partitionKey, designDoc, search) {
+    return 'database/' + id + partitionUrlComponent(partitionKey) + '/' + designDoc + '/_search/' + search;
+  },
+
+  apiurl: function (id, partitionKey, designDoc, searchName, query) {
+    return window.location.origin + '/' + encodeURIComponent(id) + '/_design/' + encodeURIComponent(designDoc) +
+      '/_search/' + encodeURIComponent(searchName) + '?q=' + encodeURIComponent(query);
+  }
+});
+
+FauxtonAPI.addReducers({
+  search: reducers
+});
+
+export default SearchRoutes;
diff --git a/app/addons/search/components/Analyzer.js b/app/addons/search/components/Analyzer.js
new file mode 100644
index 0000000..78c8a3c
--- /dev/null
+++ b/app/addons/search/components/Analyzer.js
@@ -0,0 +1,142 @@
+/*
+* Licensed Materials - Property of IBM
+*
+* "Restricted Materials of IBM"
+*
+* (C) Copyright IBM Corp. 2018 All Rights Reserved
+*
+* US Government Users Restricted Rights - Use, duplication or disclosure
+* restricted by GSA ADP Schedule Contract with IBM Corp.
+*/
+
+import PropTypes from 'prop-types';
+import React from 'react';
+import Constants from '../constants';
+import AnalyzerMultiple from './AnalyzerMultiple';
+import AnalyzerDropdown from './AnalyzerDropdown';
+
+// handles the entire Analyzer section: Simple and Multiple analyzers
+export default class Analyzer extends React.Component {
+  static propTypes = {
+    analyzerType: PropTypes.string.isRequired,
+    analyzerFields: PropTypes.array.isRequired,
+    defaultMultipleAnalyzer: PropTypes.string.isRequired,
+    singleAnalyzer: PropTypes.string.isRequired,
+    setAnalyzerType: PropTypes.func.isRequired,
+    setSingleAnalyzer: PropTypes.func.isRequired,
+    setDefaultMultipleAnalyzer: PropTypes.func.isRequired,
+    addAnalyzerRow: PropTypes.func.isRequired
+  };
+
+  constructor(props) {
+    super(props);
+    this.selectAnalyzerType = this.selectAnalyzerType.bind(this);
+    this.selectSingleAnalyzer = this.selectSingleAnalyzer.bind(this);
+    this.selectDefaultMultipleAnalyzer = this.selectDefaultMultipleAnalyzer.bind(this);
+  }
+
+  selectAnalyzerType = (e) => {
+    this.props.setAnalyzerType(e.target.value);
+  };
+
+  validate = () => {
+    if (this.props.analyzerType === Constants.ANALYZER_SINGLE) {
+      return true;
+    }
+    return this.analyzerMultiple.validate();
+  };
+
+  getAnalyzerFieldsAsObject = () => {
+    const obj = {};
+    this.props.analyzerFields.forEach(row => {
+      const fieldName = row.fieldName.replace(/["']/g, '');
+      obj[fieldName] = row.analyzer;
+    });
+    return obj;
+  };
+
+  getInfo = () => {
+    let analyzerInfo;
+    if (this.props.analyzerType === Constants.ANALYZER_SINGLE) {
+      analyzerInfo = this.props.singleAnalyzer;
+    } else {
+      analyzerInfo = {
+        name: 'perfield',
+        default: this.props.defaultMultipleAnalyzer,
+        fields: this.getAnalyzerFieldsAsObject()
+      };
+    }
+    return analyzerInfo;
+  };
+
+  selectSingleAnalyzer = (e) => {
+    this.props.setSingleAnalyzer(e.target.value);
+  };
+
+  selectDefaultMultipleAnalyzer = (e) => {
+    this.props.setDefaultMultipleAnalyzer(e.target.value);
+  };
+
+  getAnalyzerType = () => {
+    if (this.props.analyzerType === Constants.ANALYZER_SINGLE) {
+      return (
+        <AnalyzerDropdown
+          label="Type"
+          defaultSelected={this.props.singleAnalyzer}
+          onChange={this.selectSingleAnalyzer}
+        />
+      );
+    }
+    return (
+      <AnalyzerMultiple
+        ref={node => this.analyzerMultiple = node}
+        defaultAnalyzer={this.props.defaultMultipleAnalyzer}
+        selectDefaultMultipleAnalyzer={this.selectDefaultMultipleAnalyzer}
+        fields={this.props.analyzerFields}
+        addAnalyzerRow={this.props.addAnalyzerRow}
+        removeAnalyzerRow={this.props.removeAnalyzerRow}
+        setAnalyzerRowFieldName={this.props.setAnalyzerRowFieldName}
+        setAnalyzer={this.props.setAnalyzer}
+      />
+    );
+
+  };
+
+  render() {
+    let multipleClasses = 'btn';
+    if (this.props.analyzerType === Constants.ANALYZER_MULTIPLE) {
+      multipleClasses += ' active';
+    }
+    let singleClasses = 'btn';
+    if (this.props.analyzerType === Constants.ANALYZER_SINGLE) {
+      singleClasses += ' active';
+    }
+
+    return (
+      <div className="well">
+        <div className="control-group">
+          <label htmlFor="search-analyzer">Analyzer</label>
+          <div className="btn-group toggle-btns" id="analyzer">
+            <label style={{width: '82px'}}  htmlFor="single-analyzer" className={singleClasses}>Single</label>
+            <input
+              type="radio"
+              id="single-analyzer"
+              name="search-analyzer"
+              value="single"
+              checked={this.props.analyzerType === Constants.ANALYZER_SINGLE}
+              onChange={this.selectAnalyzerType} />
+            <input
+              type="radio"
+              id="multiple-analyzer"
+              name="search-analyzer"
+              value="multiple"
+              checked={this.props.analyzerType === Constants.ANALYZER_MULTIPLE}
+              onChange={this.selectAnalyzerType} />
+            <label style={{width: '82px'}} htmlFor="multiple-analyzer" className={multipleClasses}>Multiple</label>
+          </div>
+        </div>
+        {this.getAnalyzerType()}
+      </div>
+    );
+  }
+}
diff --git a/app/addons/search/components/AnalyzerDropdown.js b/app/addons/search/components/AnalyzerDropdown.js
new file mode 100644
index 0000000..88ac8e5
--- /dev/null
+++ b/app/addons/search/components/AnalyzerDropdown.js
@@ -0,0 +1,77 @@
+/*
+* Licensed Materials - Property of IBM
+*
+* "Restricted Materials of IBM"
+*
+* (C) Copyright IBM Corp. 2018 All Rights Reserved
+*
+* US Government Users Restricted Rights - Use, duplication or disclosure
+* restricted by GSA ADP Schedule Contract with IBM Corp.
+*/
+
+import PropTypes from 'prop-types';
+import React from 'react';
+import GeneralComponents from '../../components/react-components';
+
+const StyledSelect = GeneralComponents.StyledSelect;
+
+export default class AnalyzerDropdown extends React.Component {
+  static defaultProps = {
+    defaultSelected: 'standard',
+    label: 'Type',
+    id: 'analyzer-default',
+    classes: '',
+    onChange: function () { }
+  };
+  static propTypes = {
+    defaultSelected: PropTypes.string.isRequired,
+    label: PropTypes.string.isRequired,
+    id: PropTypes.string.isRequired,
+    classes: PropTypes.string.isRequired,
+    onChange: PropTypes.func.isRequired
+  };
+
+  getAnalyzers = () => {
+    const analyzers = [
+      'Standard', 'Keyword', 'Simple', 'Whitespace', 'Classic', 'Email'
+    ];
+    return analyzers.map((i) => {
+      return (<option value={i.toLowerCase()} key={i}>{i}</option>);
+    });
+  };
+
+  getLanguages = () => {
+    const languages = [
+      'Arabic', 'Armenian', 'Basque', 'Bulgarian', 'Brazilian', 'Catalan', 'Cjk', 'Chinese', 'Czech',
+      'Danish', 'Dutch', 'English', 'Finnish', 'French', 'Galician', 'German', 'Greek', 'Hindi', 'Hungarian',
+      'Indonesian', 'Irish', 'Italian', 'Japanese', 'Latvian', 'Norwegian', 'Persian', 'Polish', 'Portuguese',
+      'Romanian', 'Russian', 'Spanish', 'Swedish', 'Thai', 'Turkish'
+    ];
+    return languages.map((lang) => {
+      return (<option value={lang.toLowerCase()} key={lang}>{lang}</option>);
+    });
+  };
+
+  getLabel = () => {
+    return this.props.label === '' ? null : <label htmlFor={this.props.id}>{this.props.label}</label>;
+  };
+
+  render() {
+    const languages =
+      <optgroup label="Language-specific" key="languages">
+        {this.getLanguages()}
+      </optgroup>;
+
+    return (
+      <div className={this.props.classes}>
+        {this.getLabel()}
+        <StyledSelect
+          selectChange={this.props.onChange}
+          selectValue={this.props.defaultSelected}
+          selectId={this.props.id}
+          selectContent={[this.getAnalyzers(), languages]}
+        />
+      </div>
+    );
+  }
+}
diff --git a/app/addons/search/components/AnalyzerMultiple.js b/app/addons/search/components/AnalyzerMultiple.js
new file mode 100644
index 0000000..6aa4785
--- /dev/null
+++ b/app/addons/search/components/AnalyzerMultiple.js
@@ -0,0 +1,94 @@
+/*
+* Licensed Materials - Property of IBM
+*
+* "Restricted Materials of IBM"
+*
+* (C) Copyright IBM Corp. 2018 All Rights Reserved
+*
+* US Government Users Restricted Rights - Use, duplication or disclosure
+* restricted by GSA ADP Schedule Contract with IBM Corp.
+*/
+
+import FauxtonAPI from '../../../core/api';
+import PropTypes from 'prop-types';
+import React from 'react';
+import AnalyzerRow from './AnalyzerRow';
+import AnalyzerDropdown from './AnalyzerDropdown';
+
+export default class AnalyzerMultiple extends React.Component {
+  static propTypes = {
+    addAnalyzerRow: PropTypes.func.isRequired,
+    defaultAnalyzer: PropTypes.string.isRequired,
+    selectDefaultMultipleAnalyzer: PropTypes.func.isRequired,
+    fields: PropTypes.array.isRequired
+  };
+
+  constructor(props) {
+    super(props);
+    this.state = {
+      showErrors: false
+    };
+  }
+
+  addRow = (e) => {
+    e.preventDefault();
+    this.props.addAnalyzerRow(this.props.defaultAnalyzer);
+  };
+
+  getRows = () => {
+    return this.props.fields.map((row, i) => {
+      return (
+        <AnalyzerRow
+          row={row}
+          key={row.key}
+          rowIndex={i}
+          showErrors={this.state.showErrors}
+          setAnalyzer={this.props.setAnalyzer}
+          setAnalyzerRowFieldName={this.props.setAnalyzerRowFieldName}
+          removeAnalyzerRow={this.props.removeAnalyzerRow}
+        />
+      );
+    });
+  };
+
+  validate = () => {
+    this.setState({ showErrors: true });
+
+    let hasDuplicate = false;
+    const fieldNames = [];
+    const allValid = this.props.fields.every((row) => {
+      if (fieldNames.includes(row.fieldName)) {
+        hasDuplicate = true;
+      }
+      fieldNames.push(row.fieldName);
+      return row.valid;
+    });
+
+    if (!allValid || hasDuplicate) {
+      FauxtonAPI.addNotification({
+        msg: 'Fieldnames cannot be empty and must be unique.',
+        type: 'error',
+        clear: true
+      });
+    }
+    return allValid;
+  };
+
+  render() {
+    return (
+      <div>
+        <AnalyzerDropdown
+          label="Default"
+          id="defaultAnalyzer"
+          defaultSelected={this.props.defaultAnalyzer}
+          onChange={this.props.selectDefaultMultipleAnalyzer}
+          isValidating={this.validate} />
+        <ul id="analyzer-fields" className="unstyled">{this.getRows()}</ul>
+        <button className="addfield btn btn-small btn-primary" onClick={this.addRow}>
+          <i className="cloudant-circle-plus"></i>
+          Add Field
+        </button>
+      </div>
+    );
+  }
+}
diff --git a/app/addons/search/components/AnalyzerRow.js b/app/addons/search/components/AnalyzerRow.js
new file mode 100644
index 0000000..c8ca8d1
--- /dev/null
+++ b/app/addons/search/components/AnalyzerRow.js
@@ -0,0 +1,84 @@
+/*
+* Licensed Materials - Property of IBM
+*
+* "Restricted Materials of IBM"
+*
+* (C) Copyright IBM Corp. 2018 All Rights Reserved
+*
+* US Government Users Restricted Rights - Use, duplication or disclosure
+* restricted by GSA ADP Schedule Contract with IBM Corp.
+*/
+
+import PropTypes from 'prop-types';
+import React from 'react';
+import AnalyzerDropdown from './AnalyzerDropdown';
+
+export default class AnalyzerRow extends React.Component {
+  static propTypes = {
+    rowIndex: PropTypes.number.isRequired,
+    row: PropTypes.object.isRequired,
+    showErrors: PropTypes.bool.isRequired,
+    removeAnalyzerRow: PropTypes.func.isRequired,
+    setAnalyzerRowFieldName: PropTypes.func.isRequired,
+    setAnalyzer: PropTypes.func.isRequired
+  };
+
+  constructor(props) {
+    super(props);
+  }
+
+  deleteRow = (e) => {
+    e.preventDefault();
+    this.props.removeAnalyzerRow(this.props.rowIndex);
+  };
+
+  getFieldNameHeading = (analyzerId) => {
+    return (this.props.rowIndex === 0) ? <label htmlFor={analyzerId}>Fieldname</label> : false;
+  };
+
+  changeFieldName = (e) => {
+    this.props.setAnalyzerRowFieldName({
+      rowIndex: this.props.rowIndex,
+      fieldName: e.target.value
+    });
+  };
+
+  selectAnalyzer = (e) => {
+    this.props.setAnalyzer({
+      rowIndex: this.props.rowIndex,
+      analyzer: e.target.value
+    });
+  };
+
+  render() {
+    const analyzerId = "analyzer-row-" + this.props.rowIndex;
+    const analyzerHeading = (this.props.rowIndex === 0) ? 'Analyzer' : '';
+
+    let fieldNameClasses = 'span12';
+    if (this.props.showErrors && !this.props.row.valid) {
+      fieldNameClasses += ' unhappy';
+    }
+
+    return (
+      <li>
+        <div className="row-fluid">
+          <div className="span4">
+            {this.getFieldNameHeading(analyzerId)}
+            <input type="text" value={this.props.row.fieldName} className={fieldNameClasses} onChange={this.changeFieldName} />
+          </div>
+
+          <AnalyzerDropdown
+            id={analyzerId}
+            label={analyzerHeading}
+            defaultSelected={this.props.row.analyzer}
+            classes="span4"
+            onChange={this.selectAnalyzer} />
+
+          <div className="span4">
+            <button className="btn btn-danger delete-analyzer" onClick={this.deleteRow}>delete</button>
+          </div>
+        </div>
+      </li>
+    );
+  }
+}
diff --git a/app/addons/search/components/SearchForm.js b/app/addons/search/components/SearchForm.js
new file mode 100644
index 0000000..9a85be1
--- /dev/null
+++ b/app/addons/search/components/SearchForm.js
@@ -0,0 +1,126 @@
+/*
+* Licensed Materials - Property of IBM
+*
+* "Restricted Materials of IBM"
+*
+* (C) Copyright IBM Corp. 2018 All Rights Reserved
+*
+* US Government Users Restricted Rights - Use, duplication or disclosure
+* restricted by GSA ADP Schedule Contract with IBM Corp.
+*/
+
+import PropTypes from 'prop-types';
+import React from 'react';
+import FauxtonAPI from '../../../core/api';
+import GeneralComponents from '../../components/react-components';
+
+export default class SearchForm extends React.Component {
+  static propTypes = {
+    hasActiveQuery: PropTypes.bool.isRequired,
+    searchQuery: PropTypes.string.isRequired,
+    searchPerformed: PropTypes.bool.isRequired,
+    querySearch: PropTypes.func.isRequired,
+    setSearchQuery: PropTypes.func.isRequired,
+    searchResults: PropTypes.array
+  };
+
+  componentDidMount() {
+    this.searchInput.focus();
+  }
+
+  querySearch = (e) => {
+    e.preventDefault();
+    if (this.props.searchQuery.trim() === '') {
+      FauxtonAPI.addNotification({
+        msg: 'Please enter a search term.',
+        type: 'error',
+        clear: true
+      });
+      this.searchInput.focus();
+      return;
+    }
+    const {databaseName, partitionKey, ddocName, indexName, searchQuery} = this.props;
+    this.props.querySearch(databaseName, partitionKey, ddocName, indexName, searchQuery);
+  };
+
+  getRows = () => {
+    const database = encodeURIComponent(this.props.databaseName);
+    return this.props.searchResults.map((item) => {
+      const doc = {
+        header: item.id,
+        content: JSON.stringify(item, null, '  '),
+        url: FauxtonAPI.urls('document', 'app', database, encodeURIComponent(item.id))
+      };
+      return <GeneralComponents.Document
+        key={item.id}
+        keylabel={'id:'}
+        doc={doc}
+        header={item.id}
+        isDeletable={false}
+        docContent={doc.content}
+        onClick={this.onClick}
+        docChecked={() => { }}
+        docIdentifier={item.id} />;
+    });
+  };
+
+  onClick = (id, doc) => {
+    if (doc.url) {
+      FauxtonAPI.navigate(doc.url);
+    }
+  };
+
+  onType = (e) => {
+    this.props.setSearchQuery(e.target.value);
+  };
+
+  getResults = () => {
+    if (this.props.hasActiveQuery) {
+      return (<GeneralComponents.LoadLines />);
+    }
+
+    if (this.props.noResultsWarning) {
+      return (<div data-select="search-result-set">{this.props.noResultsWarning}</div>);
+    }
+
+    if (!this.props.searchResults) {
+      return false;
+    }
+
+    if (this.props.searchResults.length === 0) {
+      return (<div data-select="search-result-set">No results found.</div>);
+    }
+
+    return (
+      <div id="doc-list" data-select="search-result-set">
+        {this.getRows()}
+      </div>
+    );
+  };
+
+  render() {
+    const buttonLabel = this.props.hasActiveQuery ? 'Querying...' : 'Query';
+    return (
+      <div>
+        <form id="search-index-preview-form">
+          <span className="input-append">
+            <input
+              className="span4"
+              ref={el => this.searchInput = el}
+              type="text"
+              placeholder="Enter your search query"
+              onChange={this.onType}
+              value={this.props.searchQuery} />
+            <button className="btn btn-primary" id="search-index-query-button" type="submit" disabled={this.props.hasActiveQuery}
+              onClick={this.querySearch}>{buttonLabel}</button>
+          </span>
+          <a className="help-link" data-bypass="true" href={FauxtonAPI.constants.DOC_URLS.SEARCH_INDEX_QUERIES} target="_blank" rel="noopener noreferrer">
+            <i className="icon-question-sign" />
+          </a>
+        </form>
+
+        {this.getResults()}
+      </div>
+    );
+  }
+}
diff --git a/app/addons/search/components/SearchFormContainer.js b/app/addons/search/components/SearchFormContainer.js
new file mode 100644
index 0000000..d5bbba8
--- /dev/null
+++ b/app/addons/search/components/SearchFormContainer.js
@@ -0,0 +1,36 @@
+import { connect } from 'react-redux';
+import SearchForm from './SearchForm';
+import Actions from '../actions';
+
+const mapStateToProps = ({ search }) => {
+  return {
+    databaseName: search.databaseName,
+    partitionKey: search.partitionKey,
+    ddocName: search.ddocName,
+    indexName: search.indexName,
+    hasActiveQuery: search.hasActiveQuery,
+    searchQuery: search.searchQuery,
+    searchPerformed: search.searchPerformed,
+    searchResults: search.searchResults,
+    noResultsWarning: search.noResultsWarning
+  };
+};
+
+const mapDispatchToProps = (dispatch) => {
+  return {
+    querySearch: (databaseName, partitionKey, ddocName, indexName, searchQuery) => {
+      Actions.querySearch(databaseName, partitionKey, ddocName, indexName, searchQuery);
+    },
+
+    setSearchQuery: (query) => {
+      dispatch(Actions.setSearchQuery(query));
+    }
+  };
+};
+
+const SearchFormContainer = connect(
+  mapStateToProps,
+  mapDispatchToProps
+)(SearchForm);
+
+export default SearchFormContainer;
diff --git a/app/addons/search/components/SearchIndexEditor.js b/app/addons/search/components/SearchIndexEditor.js
new file mode 100644
index 0000000..3135ad3
--- /dev/null
+++ b/app/addons/search/components/SearchIndexEditor.js
@@ -0,0 +1,164 @@
+/*
+* Licensed Materials - Property of IBM
+*
+* "Restricted Materials of IBM"
+*
+* (C) Copyright IBM Corp. 2018 All Rights Reserved
+*
+* US Government Users Restricted Rights - Use, duplication or disclosure
+* restricted by GSA ADP Schedule Contract with IBM Corp.
+*/
+
+import FauxtonAPI from '../../../core/api';
+import app from '../../../app';
+import PropTypes from 'prop-types';
+import React from 'react';
+import GeneralComponents from '../../components/react-components';
+import IndexEditorComponents from '../../documents/index-editor/components';
+import Analyzer from './Analyzer';
+
+const DesignDocSelector = IndexEditorComponents.DesignDocSelector;
+
+export default class SearchIndexEditor extends React.Component {
+  static defaultProps = {
+    isCreatingIndex: true,
+    blur: function () { },
+    isLoading: true
+  };
+
+  static propTypes = {
+    isLoading: PropTypes.bool,
+    isCreatingIndex: PropTypes.bool,
+    database: PropTypes.object.isRequired,
+    saveDoc: PropTypes.object.isRequired,
+    newDesignDocName: PropTypes.string,
+    blur: PropTypes.func,
+    setSearchIndexName: PropTypes.func.isRequired,
+    searchIndexFunction: PropTypes.string.isRequired,
+    saveSearchIndex: PropTypes.func.isRequired,
+    selectDesignDoc: PropTypes.func.isRequired,
+    updateNewDesignDocName: PropTypes.func.isRequired
+  };
+
+  updateSearchIndexName = (e) => {
+    this.props.setSearchIndexName(e.target.value);
+  };
+
+  saveIndex = (e) => {
+    e.preventDefault();
+
+    // pass off validation work to the individual form sections
+    if (!this.designDocSelector.validate() || !this.analyzer.validate()) {
+      return;
+    }
+
+    if (!this.props.searchIndexName.trim()) {
+      FauxtonAPI.addNotification({
+        msg: 'Please enter the index name.',
+        type: 'error',
+        clear: true
+      });
+      return;
+    }
+
+    const dDocNameClean = this.props.saveDoc.id.replace(/_design\//, '');
+    const encodedPartKey = this.props.partitionKey ? encodeURIComponent(this.props.partitionKey) : '';
+    const url = FauxtonAPI.urls('search', 'fragment', encodeURIComponent(this.props.database.id), encodedPartKey,
+      encodeURIComponent(dDocNameClean), encodeURIComponent(this.props.searchIndexName));
+
+    this.props.saveSearchIndex(this.props.saveDoc, {
+      isCreatingIndex: this.props.isCreatingIndex,
+      indexName: this.props.searchIndexName,
+      designDocs: this.props.designDocs,
+      database: this.props.database,
+      indexFunction: this.getIndexFunction(),
+      analyzerInfo: this.analyzer.getInfo(),
+      lastSavedSearchIndexName: this.props.lastSavedSearchIndexName,
+      lastSavedDesignDocName: this.props.lastSavedDesignDocName
+    }, url);
+  };
+
+  getIndexFunction = () => {
+    return this.searchIndexEditor.getValue();
+  };
+
+  getDesignDocList = () => {
+    return this.props.designDocs.map(function (doc) {
+      return doc.id;
+    });
+  };
+
+  getCancelLink() {
+    const encodedDatabase = encodeURIComponent(this.props.database.id);
+    const encodedPartitionKey = this.props.partitionKey ? encodeURIComponent(this.props.partitionKey) : '';
+    if (!this.props.lastSavedDesignDocName || this.props.isCreatingIndex) {
+      return '#' + FauxtonAPI.urls('allDocs', 'app', encodedDatabase, encodedPartitionKey);
+    }
+
+    const cleanDDocName = this.props.lastSavedDesignDocName.replace(/^_design\//, '');
+    const encodedDDoc = '_design/' + encodeURIComponent(cleanDDocName);
+    const encodedIndex = encodeURIComponent(this.props.lastSavedSearchIndexName);
+    return '#' + FauxtonAPI.urls('search', 'showIndex', encodedDatabase,
+      encodedPartitionKey, encodedDDoc, encodedIndex);
+  }
+
+  render() {
+    if (this.props.isLoading) {
+      return (
+        <div className="search-index-page-loading">
+          <GeneralComponents.LoadLines />
+        </div>
+      );
+    }
+    // If failed to load
+    if (!this.props.database) {
+      return null;
+    }
+
+    const pageHeader = this.props.isCreatingIndex ? 'New Search Index' : 'Edit Search Index';
+    const btnLabel = this.props.isCreatingIndex ? 'Create Document and Build Index' : 'Save Document and Build Index';
+    return (
+      <form className="form-horizontal search-query-save" id="search-index">
+        <h3 className="simple-header">{pageHeader}</h3>
+
+        <DesignDocSelector
+          ref={node => this.designDocSelector = node}
+          designDocLabel="Save to design document"
+          designDocList={this.getDesignDocList()}
+          isDbPartitioned={this.props.isDbPartitioned}
+          newDesignDocName={this.props.newDesignDocName}
+          newDesignDocPartitioned={this.props.newDesignDocPartitioned}
+          selectedDesignDocName={this.props.ddocName}
+          selectedDesignDocPartitioned={this.props.ddocPartitioned}
+          onSelectDesignDoc={this.props.selectDesignDoc}
+          onChangeNewDesignDocName={this.props.updateNewDesignDocName}
+          onChangeNewDesignDocPartitioned={this.props.updateNewDesignDocPartitioned}
+          docLink={app.helpers.getDocUrl('DOC_URL_DESIGN_DOCS')} />
+
+        <div className="control-group">
+          <label htmlFor="search-name">Index name</label>
+          <input type="text" id="search-name" value={this.props.searchIndexName} onChange={this.updateSearchIndexName} />
+        </div>
+
+        <GeneralComponents.CodeEditorPanel
+          id={'search-function'}
+          className="ace-editor-section"
+          ref={node => this.searchIndexEditor = node}
+          title={"Search index function"}
+          allowZenMode={false}
+          docLink={app.helpers.getDocUrl('SEARCH_INDEXES')}
+          defaultCode={this.props.searchIndexFunction}
+          blur={this.props.blur} />
+
+        <Analyzer ref={node => this.analyzer = node} {...this.props}/>
+
+        <div className="control-group">
+          <button id="save-index" className="btn btn-primary save" onClick={this.saveIndex}>
+            <i className="icon fonticon-ok-circled" />{btnLabel}
+          </button>
+          <a href={this.getCancelLink()} className="index-cancel-link">Cancel</a>
+        </div>
+      </form>
+    );
+  }
+}
diff --git a/app/addons/search/components/SearchIndexEditorContainer.js b/app/addons/search/components/SearchIndexEditorContainer.js
new file mode 100644
index 0000000..ebbde2d
--- /dev/null
+++ b/app/addons/search/components/SearchIndexEditorContainer.js
@@ -0,0 +1,82 @@
+import { connect } from 'react-redux';
+import SearchIndexEditor from './SearchIndexEditor';
+import Actions from '../actions';
+import { getSaveDesignDoc, getSelectedDesignDocPartitioned } from '../reducers';
+
+const mapStateToProps = ({ search, databases }, ownProps) => {
+  const isSelectedDDocPartitioned = getSelectedDesignDocPartitioned(search, databases.isDbPartitioned);
+  return {
+    isCreatingIndex: ownProps.isCreatingIndex,
+    isLoading: search.loading,
+    database: search.database,
+    designDocs: search.designDocs,
+    searchIndexFunction: search.searchIndexFunction,
+    ddocName: search.ddocName,
+    ddocPartitioned: isSelectedDDocPartitioned,
+    lastSavedDesignDocName: search.lastSavedDesignDocName,
+    lastSavedSearchIndexName: search.lastSavedSearchIndexName,
+    searchIndexName: search.searchIndexName,
+    analyzerType: search.analyzerType,
+    analyzerFields: search.analyzerFields,
+    analyzerFieldsObj: search.analyzerFieldsObj,
+    defaultMultipleAnalyzer: search.defaultMultipleAnalyzer,
+    singleAnalyzer: search.singleAnalyzer,
+    saveDoc: getSaveDesignDoc(search, databases.isDbPartitioned),
+    newDesignDocName: search.newDesignDocName,
+    newDesignDocPartitioned: search.newDesignDocPartitioned,
+    isDbPartitioned: databases.isDbPartitioned,
+    partitionKey: ownProps.partitionKey
+  };
+};
+
+const mapDispatchToProps = (dispatch) => {
+  return {
+    setSearchIndexName: (name) => {
+      dispatch(Actions.setSearchIndexName(name));
+    },
+
+    saveSearchIndex: (doc, info, navigateToUrl) => {
+      Actions.saveSearchIndex(doc, info, navigateToUrl);
+    },
+
+    selectDesignDoc: (designDoc) => {
+      dispatch(Actions.selectDesignDoc(designDoc));
+    },
+
+    updateNewDesignDocName: (designDocName) => {
+      dispatch(Actions.updateNewDesignDocName(designDocName));
+    },
+    updateNewDesignDocPartitioned: (isPartitioned) => {
+      dispatch(Actions.updateNewDesignDocPartitioned(isPartitioned));
+    },
+    setAnalyzerType: (type) => {
+      dispatch(Actions.setAnalyzerType(type));
+    },
+    setSingleAnalyzer: (analyzer) => {
+      dispatch(Actions.setSingleAnalyzer(analyzer));
+    },
+    setDefaultMultipleAnalyzer: (analyzer) => {
+      dispatch(Actions.setDefaultMultipleAnalyzer(analyzer));
+    },
+    addAnalyzerRow: (analyzer) => {
+      dispatch(Actions.addAnalyzerRow(analyzer));
+    },
+    removeAnalyzerRow: (rowIndex) => {
+      dispatch(Actions.removeAnalyzerRow(rowIndex));
+    },
+    setAnalyzerRowFieldName: (params) => {
+      dispatch(Actions.setAnalyzerRowFieldName(params));
+    },
+    setAnalyzer: (params) => {
+      dispatch(Actions.setAnalyzer(params));
+    }
+
+  };
+};
+
+const SearchIndexEditorContainer = connect(
+  mapStateToProps,
+  mapDispatchToProps
+)(SearchIndexEditor);
+
+export default SearchIndexEditorContainer;
diff --git a/app/addons/search/constants.js b/app/addons/search/constants.js
new file mode 100644
index 0000000..408a988
--- /dev/null
+++ b/app/addons/search/constants.js
@@ -0,0 +1,18 @@
+/*
+* Licensed Materials - Property of IBM
+*
+* "Restricted Materials of IBM"
+*
+* (C) Copyright IBM Corp. 2018 All Rights Reserved
+*
+* US Government Users Restricted Rights - Use, duplication or disclosure
+* restricted by GSA ADP Schedule Contract with IBM Corp.
+*/
+export default {
+  ANALYZER_SINGLE: 'single',
+  ANALYZER_MULTIPLE: 'multiple',
+  DEFAULT_SEARCH_INDEX_NAME: 'newSearch',
+  DEFAULT_ANALYZER_TYPE: 'single',
+  DEFAULT_ANALYZER: 'standard',
+  DEFAULT_SEARCH_INDEX_FUNCTION: 'function (doc) {\n  index("name", doc.name);\n}'
+};
diff --git a/app/addons/search/layout.js b/app/addons/search/layout.js
new file mode 100644
index 0000000..fb41a92
--- /dev/null
+++ b/app/addons/search/layout.js
@@ -0,0 +1,101 @@
+/*
+* Licensed Materials - Property of IBM
+*
+* "Restricted Materials of IBM"
+*
+* (C) Copyright IBM Corp. 2018 All Rights Reserved
+*
+* US Government Users Restricted Rights - Use, duplication or disclosure
+* restricted by GSA ADP Schedule Contract with IBM Corp.
+*/
+import PropTypes from 'prop-types';
+import React from 'react';
+import {TabsSidebarHeader} from '../documents/layouts';
+import SidebarControllerContainer from '../documents/sidebar/SidebarControllerContainer';
+import SearchFormContainer from './components/SearchFormContainer';
+import SearchIndexEditorContainer from './components/SearchIndexEditorContainer';
+
+
+const getContent = (section, database, indexName,
+  ddocName, designDocs, ddoc, partitionKey) => {
+  if (section === 'create') {
+    return <SearchIndexEditorContainer
+      designDocs={designDocs}
+      ddoc={ddoc}
+      database={database}
+      isCreatingIndex={true}
+      partitionKey={partitionKey}
+    />;
+
+  } else if (section === 'edit') {
+    return <SearchIndexEditorContainer
+      database={database}
+      indexName={indexName}
+      ddocName={ddocName}
+      isCreatingIndex={false}
+      partitionKey={partitionKey}
+    />;
+  }
+
+  return <SearchFormContainer />;
+};
+
+export const SearchLayout = ({
+  section,
+  database,
+  indexName,
+  ddocName,
+  docURL, endpoint,
+  dropDownLinks,
+  designDocs,
+  ddoc,
+  selectedNavItem,
+  partitionKey,
+  onPartitionKeySelected,
+  onGlobalModeSelected,
+  globalMode
+}) => {
+  return (
+    <div id="dashboard" className="with-sidebar">
+      <TabsSidebarHeader
+        hideQueryOptions={true}
+        docURL={docURL}
+        endpoint={endpoint}
+        dbName={database.id}
+        dropDownLinks={dropDownLinks}
+        database={database}
+        showPartitionKeySelector={section === 'search'}
+        partitionKey={partitionKey}
+        onPartitionKeySelected={onPartitionKeySelected}
+        onGlobalModeSelected={onGlobalModeSelected}
+        globalMode={globalMode}
+      />
+      <div className="with-sidebar tabs-with-sidebar content-area">
+        <aside id="sidebar-content" className="scrollable">
+          <SidebarControllerContainer selectedNavItem={selectedNavItem} selectedPartitionKey={partitionKey}/>
+        </aside>
+        <section id="dashboard-content" className="flex-layout flex-col">
+          <div className="flex-body" id='dashboard-lower-content'>
+            <div className="search-index-content">
+              <div className="tab-content search-index-tab-content">
+                {getContent(section, database, indexName, ddocName, designDocs, ddoc, partitionKey)}
+              </div>
+            </div>
+          </div>
+        </section>
+      </div>
+    </div>
+  );
+};
+
+SearchLayout.propTypes = {
+  section: PropTypes.string.isRequired,
+  docURL: PropTypes.string,
+  endpoint: PropTypes.string,
+  ddocName: PropTypes.string,
+  indexName: PropTypes.string,
+  dropDownLinks: PropTypes.array.isRequired,
+  database: PropTypes.object.isRequired,
+};
+
+export default SearchLayout;
diff --git a/app/addons/search/reducers.js b/app/addons/search/reducers.js
new file mode 100644
index 0000000..12f27b5
--- /dev/null
+++ b/app/addons/search/reducers.js
@@ -0,0 +1,314 @@
+/*
+* Licensed Materials - Property of IBM
+*
+* "Restricted Materials of IBM"
+*
+* (C) Copyright IBM Corp. 2018 All Rights Reserved
+*
+* US Government Users Restricted Rights - Use, duplication or disclosure
+* restricted by GSA ADP Schedule Contract with IBM Corp.
+*/
+
+import StyleHelpers from '../style/helpers';
+import ActionTypes from './actiontypes';
+import Constants from './constants';
+import SearchResources from './resources';
+import DocumentsHelpers from '../documents/helpers';
+
+const initialState = {
+  designDocs: new Backbone.Collection(),
+  database: {},
+  loading: true,
+  ddocName: 'new-doc',
+  partitionKey: '',
+  indexName: '',
+  databaseName: '',
+  ...softReset()
+};
+
+// called on first load (e.g. editing a search index) and every time the create search index page loads
+function softReset() {
+  return {
+    noResultsWarning: '',
+    hasActiveQuery: false,
+    searchQuery: '',
+    searchResults: undefined,
+    searchPerformed: false,
+    newDesignDocName: '',
+    newDesignDocPartitioned: true,
+    lastSavedSearchIndexName: '',
+    searchIndexFunction: Constants.DEFAULT_SEARCH_INDEX_FUNCTION,
+    searchIndexName: Constants.DEFAULT_SEARCH_INDEX_NAME,
+    analyzerType: Constants.DEFAULT_ANALYZER_TYPE,
+    analyzerFields: [],
+    singleAnalyzer: Constants.DEFAULT_ANALYZER,
+    defaultMultipleAnalyzer: Constants.DEFAULT_ANALYZER
+  };
+}
+
+function addAnalyzerRow (state, { analyzer, fieldName }) {
+  const newAnalyzerFields = state.analyzerFields.slice();
+  newAnalyzerFields.push({
+    key: StyleHelpers.getUniqueKey(),
+    fieldName: (fieldName) ? fieldName : '',
+    analyzer: analyzer,
+    valid: (fieldName && fieldName.trim().length > 0)
+  });
+  return newAnalyzerFields;
+}
+
+function removeAnalyzerRowByIndex (state, rowIndex) {
+  const newAnalyzerFields = state.analyzerFields.slice();
+  rowIndex = parseInt(rowIndex, 10);
+  newAnalyzerFields.splice(rowIndex, 1);
+  return newAnalyzerFields;
+}
+
+function setAnalyzerRowFieldName (state, { fieldName, rowIndex }) {
+  const newAnalyzerFields = state.analyzerFields.slice();
+  const idx = parseInt(rowIndex, 10);
+  newAnalyzerFields[idx].fieldName = fieldName;
+  newAnalyzerFields[idx].valid = fieldName !== '';
+  return newAnalyzerFields;
+}
+
+function setAnalyzerRow (state, { analyzer, rowIndex }) {
+  const newAnalyzerFields = state.analyzerFields.slice();
+  const idx = parseInt(rowIndex, 10);
+  newAnalyzerFields[idx].analyzer = analyzer;
+  return newAnalyzerFields;
+}
+
+function initEditSearch (state, { database, designDocs, ddocInfo, indexName }) {
+  const ddoc = designDocs.find(ddoc => {
+    return ddoc.id === ddocInfo.id;
+  }).dDocModel();
+
+  // the selected analyzer returned in the ddoc can be applied to both the single analyzer and the default multiple
+  // analyzer. We store them separately in the store so those values don't change when toggling from Single to Multiple
+  const analyzer = ddoc.getAnalyzer(indexName);
+  let newSingleAnalyzer;
+  if (_.isString(analyzer)) {
+    newSingleAnalyzer = analyzer;
+  } else {
+    if (_.has(analyzer, 'default')) {
+      newSingleAnalyzer = analyzer.default;
+    } else {
+      newSingleAnalyzer = Constants.DEFAULT_ANALYZER_TYPE;
+    }
+  }
+  const newAnalyzerFields = [];
+  if (analyzer && analyzer.fields) {
+    Object.keys(analyzer.fields).forEach(fieldName => {
+      newAnalyzerFields.push({
+        key: StyleHelpers.getUniqueKey(),
+        fieldName: (fieldName) ? fieldName : '',
+        analyzer: analyzer.fields[fieldName],
+        valid: !_.isUndefined(fieldName) && !_.isEmpty(fieldName)
+      });
+    });
+  }
+
+  return {
+    loading: false,
+    searchPerformed: false,
+    database: database,
+    designDocs: designDocs,
+    searchIndexName: indexName,
+    ddocName: ddocInfo.id,
+    lastSavedSearchIndexName: indexName,
+    lastSavedDesignDocName: ddocInfo.id,
+    searchIndexFunction: ddoc.getIndex(indexName),
+    analyzerType: ddoc.analyzerType(indexName),
+    // this either returns a simple string (single) or a complex object (multiple)
+    singleAnalyzer: newSingleAnalyzer,
+    defaultMultipleAnalyzer: newSingleAnalyzer,
+    analyzerFields: newAnalyzerFields
+  };
+}
+
+export function getSaveDesignDoc (state, isDbPartitioned) {
+  if (state.ddocName === 'new-doc') {
+    const doc = {
+      _id: '_design/' + state.newDesignDocName,
+      views: {},
+      language: 'javascript'
+    };
+    const dDoc = new SearchResources.Doc(doc, { database: state.database });
+    if (isDbPartitioned) {
+      dDoc.setDDocPartitionedOption(state.newDesignDocPartitioned);
+    }
+    return dDoc;
+  }
+
+  const foundDoc = state.designDocs.find((ddoc) => {
+    return ddoc.id === state.ddocName;
+  });
+  return (!foundDoc) ? null : foundDoc.dDocModel();
+}
+
+export function getSelectedDesignDocPartitioned(state, isDbPartitioned) {
+  const designDoc = state.designDocs.find(ddoc => {
+    return state.ddocName === ddoc.id;
+  });
+  if (designDoc) {
+    return DocumentsHelpers.isDDocPartitioned(designDoc.get('doc'), isDbPartitioned);
+  }
+  return false;
+}
+
+export default function search(state = initialState, action) {
+  const options = action.options;
+  switch (action.type) {
+
+    case ActionTypes.SEARCH_INDEX_SET_LOADING:
+      return {
+        ...state,
+        loading: options.loading
+      };
+
+    case ActionTypes.SEARCH_INDEX_DESIGN_DOCS_LOADED:
+      const newState = {
+        ...state,
+        loading: false,
+        designDocs: options.designDocs,
+        database: options.database,
+        ...softReset()
+      };
+      if (options.defaultDDoc) {
+        newState.ddocName = '_design/' + options.defaultDDoc;
+      }
+      return newState;
+
+    case ActionTypes.SEARCH_INDEX_SET_NAME:
+      return {
+        ...state,
+        searchIndexName: options.value
+      };
+
+    case ActionTypes.SEARCH_INDEX_SET_ANALYZER_TYPE:
+      return {
+        ...state,
+        analyzerType: options.value
+      };
+
+    case ActionTypes.SEARCH_INDEX_SET_SINGLE_ANALYZER:
+      return {
+        ...state,
+        singleAnalyzer: options.analyzer
+      };
+
+    case ActionTypes.SEARCH_INDEX_ADD_ANALYZER_ROW:
+      return {
+        ...state,
+        analyzerFields: addAnalyzerRow(state, options)
+      };
+
+    case ActionTypes.SEARCH_INDEX_PREVIEW_REQUEST_MADE:
+      return {
+        ...state,
+        hasActiveQuery: true
+      };
+
+    case ActionTypes.SEARCH_INDEX_PREVIEW_REQUEST_ERROR:
+      return {
+        ...state,
+        hasActiveQuery: false,
+        searchResults: []
+      };
+
+    case ActionTypes.SEARCH_INDEX_SET_SEARCH_QUERY:
+      return {
+        ...state,
+        searchQuery: options.query
+      };
+
+    case ActionTypes.SEARCH_INDEX_PREVIEW_MODEL_UPDATED:
+      return {
+        ...state,
+        searchResults: options.searchResults,
+        hasActiveQuery: false,
+        searchPerformed: true
+      };
+
+    case ActionTypes.SEARCH_INDEX_REMOVE_ANALYZER_ROW:
+      return {
+        ...state,
+        analyzerFields: removeAnalyzerRowByIndex(state, options.rowIndex)
+      };
+
+    case ActionTypes.SEARCH_INDEX_SET_ANALYZER_ROW_FIELD_NAME:
+      return {
+        ...state,
+        analyzerFields: setAnalyzerRowFieldName(state, options)
+      };
+
+    case ActionTypes.SEARCH_INDEX_SET_ANALYZER_ROW:
+      return {
+        ...state,
+        analyzerFields: setAnalyzerRow(state, options)
+      };
+
+    case ActionTypes.SEARCH_INDEX_SET_DEFAULT_MULTIPLE_ANALYZER:
+      return {
+        ...state,
+        defaultMultipleAnalyzer: options.analyzer
+      };
+
+    case ActionTypes.SEARCH_INDEX_INIT_EDIT_SEARCH_INDEX:
+      return {
+        ...state,
+        ...initEditSearch(state, options)
+      };
+
+    case ActionTypes.SEARCH_INDEX_SELECT_DESIGN_DOC:
+      return {
+        ...state,
+        ddocName: options.value
+      };
+
+    case ActionTypes.SEARCH_INDEX_CLEAR:
+      return {
+        ...initialState,
+        designDocs: new Backbone.Collection()
+      };
+
+    case ActionTypes.SEARCH_INDEX_INIT:
+      return {
+        ...state,
+        loading: false,
+        databaseName: options.databaseName,
+        partitionKey: options.partitionKey,
+        ddocName: options.ddocName,
+        indexName: options.indexName,
+        searchQuery: options.searchQuery,
+        searchResults: options.searchQuery === '' ? undefined : state.searchResults,
+        noResultsWarning: ''
+      };
+
+    case ActionTypes.SEARCH_INDEX_NEW_DESIGN_DOC_NAME_UPDATED:
+      return {
+        ...state,
+        newDesignDocName: options.value
+      };
+
+    case ActionTypes.SEARCH_INDEX_NEW_DESIGN_DOC_PARTITONED_UPDATED:
+      return {
+        ...state,
+        newDesignDocPartitioned: options.value
+      };
+
+    case ActionTypes.SEARCH_INDEX_PARTITION_PARAM_NOT_SUPPORTED:
+      return Object.assign({}, state, {
+        noResultsWarning: 'The selected index does not support partitions. Switch back to global mode.'
+      });
+
+    case ActionTypes.SEARCH_INDEX_PARTITION_PARAM_MANDATORY:
+      return Object.assign({}, state, {
+        noResultsWarning: 'The selected index requires a partition key. Use the selector at the top to enter a partition key.'
+      });
+
+    default:
+      return state;
+  }
+}
diff --git a/app/addons/search/resources.js b/app/addons/search/resources.js
new file mode 100644
index 0000000..c653fbb
--- /dev/null
+++ b/app/addons/search/resources.js
@@ -0,0 +1,72 @@
+/*
+* Licensed Materials - Property of IBM
+*
+* "Restricted Materials of IBM"
+*
+* (C) Copyright IBM Corp. 2018 All Rights Reserved
+*
+* US Government Users Restricted Rights - Use, duplication or disclosure
+* restricted by GSA ADP Schedule Contract with IBM Corp.
+*/
+
+import Constants from './constants';
+import Documents from '../documents/resources';
+
+var CloudantSearch = {};
+
+CloudantSearch.Doc = Documents.Doc.extend({
+  setIndex: function (name, index, analyzer) {
+    if (!this.isDdoc()) {
+      return false;
+    }
+
+    var indexes = this.get('indexes');
+    if (!indexes) {
+      indexes = {};
+    }
+    if (!indexes[name]) {
+      indexes[name] = {};
+    }
+
+    if (analyzer) {
+      indexes[name].analyzer = analyzer;
+    }
+
+    indexes[name].index = index;
+    return this.set({indexes: indexes});
+  },
+
+  getIndex: function (indexName) {
+    return this.get('indexes')[indexName].index;
+  },
+
+  getAnalyzer: function (indexName) {
+    return this.get('indexes')[indexName].analyzer;
+  },
+
+  analyzerType: function (indexName) {
+    if (typeof this.getAnalyzer(indexName) === 'object') {
+      return Constants.ANALYZER_MULTIPLE;
+    }
+    return Constants.ANALYZER_SINGLE;
+  },
+
+  dDocModel: function () {
+    if (!this.isDdoc()) {
+      return false;
+    }
+
+    var doc = this.get('doc');
+    if (doc) {
+      return new CloudantSearch.Doc(doc, { database: this.database });
+    }
+    return false;
+  }
+});
+
+CloudantSearch.AllDocs = Documents.AllDocs.extend({
+  model: CloudantSearch.Doc
+});
+
+
+export default CloudantSearch;
diff --git a/app/addons/search/routes.js b/app/addons/search/routes.js
new file mode 100644
index 0000000..f4ba4d3
--- /dev/null
+++ b/app/addons/search/routes.js
@@ -0,0 +1,272 @@
+/*
+* Licensed Materials - Property of IBM
+*
+* "Restricted Materials of IBM"
+*
+* (C) Copyright IBM Corp. 2018 All Rights Reserved
+*
+* US Government Users Restricted Rights - Use, duplication or disclosure
+* restricted by GSA ADP Schedule Contract with IBM Corp.
+*/
+import app from '../../app';
+import FauxtonAPI from '../../core/api';
+import Resources from './resources';
+import SearchActions from './actions';
+import DatabasesActions from '../databases/actions';
+import Databases from '../databases/base';
+import BaseRoute from '../documents/shared-routes';
+import {SidebarItemSelection} from '../documents/sidebar/helpers';
+import CloudantDocuments from '../cloudantdocuments/resources';
+import CloudantDatabases from '../cloudantdatabases/resources';
+import Account from '../account/resources';
+import SidebarActions from '../documents/sidebar/actions';
+import Actions from './actions';
+import Layout from './layout';
+import React from 'react';
+
+var SearchRouteObject = BaseRoute.extend({
+  routes: {
+    'database/:database/_design/:ddoc/_search/:search(?*searchQuery)': {
+      route: 'searchNoPartition',
+      roles: ['fx_loggedIn']
+    },
+    'database/:database/_partition/:partitionkey/_design/:ddoc/_search/:search(?*searchQuery)': {
+      route: 'search',
+      roles: ['fx_loggedIn']
+    },
+    'database/:database/_design/:ddoc/_indexes/:search(?*searchQuery)': {
+      route: 'searchNoPartition',
+      roles: ['fx_loggedIn']
+    },
+    'database/:database/_partition/:partitionkey/_design/:ddoc/_indexes/:search(?*searchQuery)': {
+      route: 'search',
+      roles: ['fx_loggedIn']
+    },
+    'database/:database/_design/:ddoc/_search/:search/edit': {
+      route: 'editNoPartition',
+      roles: ['fx_loggedIn']
+    },
+    'database/:database/_partition/:partitionkey/_design/:ddoc/_search/:search/edit': {
+      route: 'edit',
+      roles: ['fx_loggedIn']
+    },
+    'database/:database/new_search': 'createNoPartition',
+    'database/:database/_partition/:partitionkey/new_search': 'create',
+    'database/:database/new_search/:designDoc': 'createNoPartition',
+    'database/:database/_partition/:partitionkey/new_search/:designDoc': 'create'
+  },
+
+  initialize: function (route, options) {
+    this.allDatabases = new Databases.List();
+    this.databaseName = options[0];
+    this.database = new Databases.Model({ id: this.databaseName });
+    this.data = {
+      database: new Databases.Model({ id: this.databaseName })
+    };
+
+    this.data.designDocs = new Resources.AllDocs(null, {
+      database: this.data.database,
+      params: {
+        startkey: '"_design"',
+        endkey: '"_design1"',
+        include_docs: true
+      }
+    });
+
+    SidebarActions.dispatchNewOptions({
+      database: this.data.database,
+      designDocs: this.data.designDocs
+    });
+  },
+
+  searchNoPartition: function (databaseName, ddocName, indexName, query) {
+    return this.search(databaseName, '', ddocName, indexName, query);
+  },
+
+  search: function (databaseName, partitionKey, ddocName, indexName, query) {
+    this.databaseName = databaseName;
+    this.ddocName     = ddocName;
+    this.indexName    = indexName;
+
+    const selectedNavItem = new SidebarItemSelection('designDoc', {
+      designDocName: ddocName,
+      designDocSection: 'Search Indexes',
+      indexName: indexName
+    });
+    SidebarActions.dispatchExpandSelectedItem(selectedNavItem);
+
+    const dropDownLinks = this.getCrumbs(this.database);
+    Actions.dispatchInitSearchIndex({
+      databaseName: databaseName,
+      partitionKey: partitionKey,
+      designDoc: ddocName,
+      indexName: indexName,
+      query: query
+    });
+    DatabasesActions.fetchSelectedDatabaseInfo(databaseName);
+
+    const encodedPartKey = partitionKey ? encodeURIComponent(partitionKey) : '';
+    const endpointUrl = FauxtonAPI.urls('search', 'apiurl', this.databaseName, encodedPartKey, this.ddocName,
+      this.indexName, (query ? query : '*:*'));
+
+    const encodedQuery = query ? `?${encodeURIComponent(query)}` : '';
+    const navigateToPartitionedView = (partKey) => {
+      const baseUrl = FauxtonAPI.urls('partitioned_search', 'app', encodeURIComponent(databaseName),
+        encodeURIComponent(partKey), encodeURIComponent(ddocName));
+      FauxtonAPI.navigate('#/' + baseUrl + encodeURIComponent(indexName) + encodedQuery);
+    };
+    const navigateToGlobalView = () => {
+      const baseUrl = FauxtonAPI.urls('search', 'app', encodeURIComponent(databaseName), encodeURIComponent(ddocName));
+      FauxtonAPI.navigate('#/' + baseUrl + encodeURIComponent(indexName) + encodedQuery);
+    };
+    return <Layout
+      section={'search'}
+      dropDownLinks={dropDownLinks}
+      endpoint={endpointUrl}
+      docURL={FauxtonAPI.constants.DOC_URLS.SEARCH_INDEXES}
+      database={this.database}
+      indexName={indexName}
+      selectedNavItem={selectedNavItem}
+      partitionKey={partitionKey}
+      onPartitionKeySelected={navigateToPartitionedView}
+      onGlobalModeSelected={navigateToGlobalView}
+      globalMode={partitionKey === ''}
+    />;
+  },
+
+  editNoPartition: function (database, ddocName, indexName) {
+    return this.edit(database, '', ddocName, indexName);
+  },
+
+  edit: function (database, partitionKey, ddocName, indexName) {
+    const selectedNavItem = new SidebarItemSelection('designDoc', {
+      designDocName: ddocName,
+      designDocSection: 'Search Indexes',
+      indexName: indexName
+    });
+    SidebarActions.dispatchExpandSelectedItem(selectedNavItem);
+
+    SearchActions.dispatchEditSearchIndex({
+      ddocID: '_design/' + ddocName,
+      database: this.database,
+      indexName: indexName,
+      designDocs: this.data.designDocs
+    });
+    DatabasesActions.fetchSelectedDatabaseInfo(database);
+
+    const dropDownLinks = this.getCrumbs(this.database);
+    return <Layout
+      section={'edit'}
+      dropDownLinks={dropDownLinks}
+      database={this.database}
+      indexName={indexName}
+      ddocName={ddocName}
+      selectedNavItem={selectedNavItem}
+      partitionKey={partitionKey}
+    />;
+  },
+
+  createNoPartition: function (database, ddoc) {
+    return this.create(database, '', ddoc);
+  },
+
+  create: function (database, partitionKey, ddoc) {
+    const selectedNavItem = new SidebarItemSelection('');
+
+    SearchActions.dispatchInitNewSearchIndex({
+      database: this.database,
+      designDocs: this.data.designDocs,
+      defaultDDoc: ddoc,
+    });
+    DatabasesActions.fetchSelectedDatabaseInfo(database);
+
+    const dropDownLinks = this.getCrumbs(this.database);
+    return <Layout
+      section={'create'}
+      dropDownLinks={dropDownLinks}
+      database={this.database}
+      designDocs={this.data.designDocs}
+      ddoc={ddoc}
+      selectedNavItem={selectedNavItem}
+      partitionKey={partitionKey}
+    />;
+  },
+
+  getCrumbs: function (database) {
+    return [
+      { type: "back", link: "/_all_dbs"},
+      { name: database.id }
+    ];
+  }
+});
+
+
+var SharedSearchRouteObject = SearchRouteObject.extend({
+  routes: {
+    'database/shared/:user/:database/_design/:ddoc/_search/:search(?*searchQuery)': {
+      route: 'sharedSearch',
+      roles: ['fx_loggedIn']
+    },
+    'database/shared/:user/:database/_design/:ddoc/_indexes/:search(?*searchQuery)': {
+      route: 'sharedSearch',
+      roles: ['fx_loggedIn']
+    },
+    'database/shared/:user/:database/new_search': 'sharedNewSearch',
+    'database/shared/:user/:database/new_search/:designDoc': 'sharedNewSearch',
+    'database/shared/:user/:database/_design/:ddoc/_search/:search/edit': {
+      route: 'sharedEdit',
+      roles: ['fx_loggedIn']
+    }
+  },
+
+  initialize: function (route, options) {
+    var docOptions = app.getParams();
+    docOptions.include_docs = true;
+
+    this.username = CloudantDocuments.sharedUsername = options[0];
+    this.databaseName = options[1];
+    this.database = new CloudantDatabases.SharedModel({
+      id: this.databaseName,
+      name: this.username
+    });
+    this.user = new Account.User();
+
+    this.data = {
+      username: this.username,
+      database: this.database
+    };
+
+    this.data.designDocs = new CloudantDocuments.SharedAllDocs(null, {
+      username: this.username,
+      database: this.database,
+      paging: {
+        pageSize: 500
+      },
+      params: {
+        startkey: '"_design"',
+        endkey: '"_design1"',
+        include_docs: true,
+        limit: 500
+      }
+    });
+
+    var initOptions = options.slice(1);
+    SearchRouteObject.prototype.initialize.call(this, route, initOptions);
+  },
+
+  sharedSearch: function (user, database, ddoc, search, query) {
+    return this.searchNoPartition(database, ddoc, search, query);
+  },
+
+  sharedEdit: function (user, database, ddoc, indexName) {
+    return this.editNoPartition(database, ddoc, indexName);
+  },
+
+  sharedNewSearch: function (user, database, ddoc) {
+    return this.createNoPartition(database, ddoc);
+  }
+});
+
+Resources.RouteObjects = [SearchRouteObject, SharedSearchRouteObject];
+
+export default Resources;
diff --git a/app/addons/search/tests/nightwatch/cloneSearchIndex.js b/app/addons/search/tests/nightwatch/cloneSearchIndex.js
new file mode 100644
index 0000000..69d6748
--- /dev/null
+++ b/app/addons/search/tests/nightwatch/cloneSearchIndex.js
@@ -0,0 +1,68 @@
+/*
+* Licensed Materials - Property of IBM
+*
+* "Restricted Materials of IBM"
+*
+* (C) Copyright IBM Corp. 2018 All Rights Reserved
+*
+* US Government Users Restricted Rights - Use, duplication or disclosure
+* restricted by GSA ADP Schedule Contract with IBM Corp.
+*/
+// 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.
+
+// 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.
+
+module.exports = {
+
+  'Clones a search index': function (client) {
+    var waitTime = client.globals.maxWaitTime,
+        newDatabaseName = client.globals.testDatabaseName,
+        baseUrl = client.globals.test_settings.launch_url;
+
+    client
+      .createDatabase(newDatabaseName)
+      .loginToGUI()
+      .url(baseUrl + '/#/database/' + newDatabaseName + '/_all_docs')
+
+      // create a search index
+      .waitForElementPresent('#new-design-docs-button', waitTime, false)
+      .click('#new-design-docs-button a')
+      .click('#new-design-docs-button a[href="#/database/' + newDatabaseName + '/new_search"]')
+      .waitForElementVisible('#new-ddoc', waitTime, false)
+      .setValue('#new-ddoc', 'test1')
+      .clearValue('#search-name')
+      .setValue('#search-name', 'test1-index')
+      .clickWhenVisible('#save-index')
+      .waitForElementVisible('#global-notifications .alert.alert-success', waitTime, false)
+
+      .clickWhenVisible('.index-list li span', waitTime, true)
+      .clickWhenVisible('.popover-content .fonticon-files-o', waitTime, true)
+      .waitForElementVisible('#new-index-name', waitTime, true)
+      .setValue('#new-index-name', 'cloned-search-index')
+      .clickWhenVisible('.clone-index-modal .btn-primary', waitTime, true)
+
+      // now wait for the sidebar to be updated with the new view
+      .waitForElementVisible('#test1_cloned-search-index', waitTime, true)
+      .end();
+  }
+};
diff --git a/app/addons/search/tests/nightwatch/createNewSearch.js b/app/addons/search/tests/nightwatch/createNewSearch.js
new file mode 100644
index 0000000..6265b67
--- /dev/null
+++ b/app/addons/search/tests/nightwatch/createNewSearch.js
@@ -0,0 +1,130 @@
+/*
+* Licensed Materials - Property of IBM
+*
+* "Restricted Materials of IBM"
+*
+* (C) Copyright IBM Corp. 2018 All Rights Reserved
+*
+* US Government Users Restricted Rights - Use, duplication or disclosure
+* restricted by GSA ADP Schedule Contract with IBM Corp.
+*/
+module.exports = {
+
+  'Creates new Search index for Dash': function (client) {
+    /*jshint multistr: true */
+
+    var newDatabaseName = client.globals.testDatabaseName,
+        baseUrl = client.globals.test_settings.launch_url;
+
+    var searchFunctionString = function (append) {
+      return 'function (doc) {'                  +
+        'index("name", doc.name ' + append + ');' +
+        '}';
+    };
+
+    client
+      .loginToGUI()
+      .populateDatabase(newDatabaseName)
+      .url(baseUrl + '/#/database/' + newDatabaseName + '/_all_docs')
+      .waitForElementPresent('#new-design-docs-button', client.globals.maxWaitTime, false)
+      .click('#new-design-docs-button a')
+      .click('#new-design-docs-button a[href="#/database/' + newDatabaseName + '/new_search"]')
+      .waitForElementVisible('.faux-header__doc-header-title', client.globals.maxWaitTime, true)
+
+      .clickWhenVisible('.styled-select select')
+      .keys(['_design/keyview', '\uE006'])
+
+      .clearValue('#search-name')
+      .setValue('#search-name', 'fancy_search')
+      .execute('\
+        var editor = ace.edit("search-function");\
+        editor.getSession().setValue("' + searchFunctionString(0) + '");\
+      ')
+
+      .execute('document.querySelector("#save-index").scrollIntoView();')
+      .clickWhenVisible('#save-index')
+      .waitForElementPresent('#keyview_fancy_search', client.globals.maxWaitTime, false)
+      .end();
+  },
+
+  'Creating a new index has a clean slate': function (client) {
+    var newDatabaseName = client.globals.testDatabaseName,
+        baseUrl = client.globals.test_settings.launch_url;
+
+    client
+      .loginToGUI()
+      .populateDatabase(newDatabaseName)
+      .url(baseUrl + '/#/database/' + newDatabaseName + '/_all_docs')
+
+      // 1. create a search index in _design/keyview design doc
+      .waitForElementPresent('#new-design-docs-button', client.globals.maxWaitTime, false)
+      .click('#new-design-docs-button a')
+      .click('#new-design-docs-button a[href="#/database/' + newDatabaseName + '/new_search"]')
+      .clickWhenVisible('.styled-select select')
+      .keys(['_design/keyview', '\uE006'])
+
+      .clearValue('#search-name')
+      .setValue('#search-name', 'clean-slate-test')
+      .clickWhenVisible('#save-index')
+      .waitForElementVisible('#global-notifications .alert.alert-success', client.globals.maxWaitTime, false)
+      .url(baseUrl + '/#/database/' + newDatabaseName + '/_all_docs')
+      .waitForElementPresent('.tableview-checkbox-cell', client.globals.maxWaitTime, false)
+      .waitForElementNotPresent('.loading-lines', client.globals.maxWaitTime, false)
+
+      // 2. create a new search index. The "Save to design doc" dropdown should default to New Document
+      .waitForElementPresent('#new-design-docs-button', client.globals.maxWaitTime, false)
+      .click('#new-design-docs-button a')
+      .click('#new-design-docs-button a[href="#/database/' + newDatabaseName + '/new_search"]')
+      .waitForElementVisible('.styled-select select', client.globals.maxWaitTime, false)
+      .assert.value('.styled-select select', 'new-doc')
+      .end();
+  },
+
+  'Adding two indexes in a row does not add multiple indexes': function (client) {
+    var newDatabaseName = client.globals.testDatabaseName,
+        baseUrl = client.globals.test_settings.launch_url;
+
+    client
+      .loginToGUI()
+      .createDatabase(newDatabaseName)
+      .url(baseUrl + '/#/database/' + newDatabaseName + '/_all_docs')
+
+      // 1. create a search index in _design/test1
+      .waitForElementPresent('#new-design-docs-button', client.globals.maxWaitTime, false)
+      .click('#new-design-docs-button a')
+      .click('#new-design-docs-button a[href="#/database/' + newDatabaseName + '/new_search"]')
+      .waitForElementVisible('#new-ddoc', client.globals.maxWaitTime, false)
+      .setValue('#new-ddoc', 'test1')
+      .clearValue('#search-name')
+      .setValue('#search-name', 'test1-index')
+      .clickWhenVisible('#save-index')
+      .waitForElementVisible('#global-notifications .alert.alert-success', client.globals.maxWaitTime, false)
+      .url(baseUrl + '/#/database/' + newDatabaseName + '/_all_docs')
+      .waitForElementPresent('.tableview-checkbox-cell', client.globals.maxWaitTime, false)
+      .waitForElementNotPresent('.loading-lines', client.globals.maxWaitTime, false)
+
+      // 2. create a second index in _design/test2
+      .waitForElementPresent('#new-design-docs-button', client.globals.maxWaitTime, false)
+      .click('#new-design-docs-button a')
+      .click('#new-design-docs-button a[href="#/database/' + newDatabaseName + '/new_search"]')
+      .waitForElementVisible('#new-ddoc', client.globals.maxWaitTime, false)
+      .setValue('#new-ddoc', 'test2')
+      .clearValue('#search-name')
+      .setValue('#search-name', 'test2-index')
+      .clickWhenVisible('#save-index')
+
+      .waitForElementVisible('#global-notifications .alert.alert-success', client.globals.maxWaitTime, false)
+
+      // 3. confirm that the nav bar shows ONLY one search index each:
+      //  _design/test1 has the single _design/test1-index
+      //  _design/test2 has the single _design/test2-index
+      .waitForElementPresent('#test1_test1-index', client.globals.maxWaitTime, false)
+      .waitForElementNotPresent('#test1_test2-index', client.globals.maxWaitTime, false)
+      .waitForElementNotPresent('#test2_test1-index', client.globals.maxWaitTime, false)
+      .waitForElementPresent('#test2_test2-index', client.globals.maxWaitTime, false)
+
+      .end();
+  }
+
+
+};
diff --git a/app/addons/search/tests/nightwatch/deleteSearchIndex.js b/app/addons/search/tests/nightwatch/deleteSearchIndex.js
new file mode 100644
index 0000000..f4cdc86
--- /dev/null
+++ b/app/addons/search/tests/nightwatch/deleteSearchIndex.js
@@ -0,0 +1,107 @@
+/*
+* Licensed Materials - Property of IBM
+*
+* "Restricted Materials of IBM"
+*
+* (C) Copyright IBM Corp. 2018 All Rights Reserved
+*
+* US Government Users Restricted Rights - Use, duplication or disclosure
+* restricted by GSA ADP Schedule Contract with IBM Corp.
+*/
+// 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.
+
+// 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.
+
+module.exports = {
+
+  'Deletes a search index': function (client) {
+    var newDatabaseName = client.globals.testDatabaseName,
+        waitTime = client.globals.maxWaitTime,
+        baseUrl = client.globals.test_settings.launch_url;
+
+    client
+      .loginToGUI()
+      .url(baseUrl + '/#/database/' + newDatabaseName + '/_all_docs')
+
+      // create a search index
+      .waitForElementPresent('#new-design-docs-button', waitTime, false)
+      .click('#new-design-docs-button a')
+      .click('#new-design-docs-button a[href="#/database/' + newDatabaseName + '/new_search"]')
+      .waitForElementVisible('#new-ddoc', waitTime, false)
+      .setValue('#new-ddoc', 'test1')
+      .clearValue('#search-name')
+      .setValue('#search-name', 'test1-index')
+      .clickWhenVisible('#save-index')
+      .waitForElementVisible('#global-notifications .alert.alert-success', waitTime, false)
+      .url(baseUrl + '/#/database/' + newDatabaseName + '/_all_docs')
+      .waitForElementPresent('.tableview-checkbox-cell', client.globals.maxWaitTime, false)
+      .waitForElementNotPresent('.loading-lines', waitTime, false)
+
+      // confirm search index appears in sidebar
+      .waitForElementVisible('#test1_test1-index', waitTime, true)
+
+      // now delete it and confirm that the entire design doc gets removed (because it's the last index)
+      .clickWhenVisible('.index-list li span', waitTime, true)
+      .clickWhenVisible('.popover-content .fonticon-trash', waitTime, true)
+      .clickWhenVisible('.confirmation-modal button.btn.btn-primary')
+
+      // now wait for the sidebar to have removed the design doc
+      .waitForElementNotPresent('#testdesigndoc', waitTime, true)
+      .end();
+  },
+
+  'Deleting view when design doc has search index does not remove design doc': function (client) {
+    var waitTime = client.globals.maxWaitTime,
+        newDatabaseName = client.globals.testDatabaseName,
+        baseUrl = client.globals.test_settings.launch_url;
+
+    client
+      // this creates a view
+      .populateDatabase(newDatabaseName)
+      .loginToGUI()
+
+      // now create a search index
+      .url(baseUrl + '/#/database/' + newDatabaseName + '/new_search/testdesigndoc')
+      .waitForElementVisible('#save-index', waitTime, false)
+      .clearValue('#search-name')
+      .setValue('#search-name', 'search-index1')
+      .clickWhenVisible('#save-index')
+      .waitForElementVisible('#global-notifications .alert.alert-success', waitTime, false)
+      .url(baseUrl + '/#/database/' + newDatabaseName + '/_all_docs')
+      .waitForElementPresent('.tableview-checkbox-cell', client.globals.maxWaitTime, false)
+      .waitForElementNotPresent('.loading-lines', waitTime, false)
+
+      // now delete the search index. Since there's a view in this design doc, the design doc should not be removed
+      .clickWhenVisible('#nav-design-function-testdesigndocindexes .index-list li span', waitTime, true)
+      .clickWhenVisible('.popover-content .fonticon-trash', waitTime, true)
+      .waitForElementVisible('div.confirmation-modal', waitTime, false)
+      .clickWhenVisible('.confirmation-modal button.btn.btn-primary')
+
+      // just assert the search indexes section has been removed, but the design doc still exists
+      .waitForElementNotPresent('#nav-design-function-testdesigndocindexes', waitTime, true)
+      .waitForElementPresent('#testdesigndoc', waitTime, true)
+
+      .end();
+  }
+
+};
diff --git a/app/addons/search/tests/nightwatch/searchPageApiBar.js b/app/addons/search/tests/nightwatch/searchPageApiBar.js
new file mode 100644
index 0000000..ac503c6
--- /dev/null
+++ b/app/addons/search/tests/nightwatch/searchPageApiBar.js
@@ -0,0 +1,55 @@
+/*
+* Licensed Materials - Property of IBM
+*
+* "Restricted Materials of IBM"
+*
+* (C) Copyright IBM Corp. 2018 All Rights Reserved
+*
+* US Government Users Restricted Rights - Use, duplication or disclosure
+* restricted by GSA ADP Schedule Contract with IBM Corp.
+*/
+module.exports = {
+
+  'Check API Bar is present/hidden on appropriate page and is encoded': function (client) {
+    const newDatabaseName = client.globals.testDatabaseName,
+          baseUrl = client.globals.test_settings.launch_url;
+
+    const searchStr = "class:bird";
+    const searchStrEncoded = encodeURIComponent(searchStr);
+    const fullURL = baseUrl + '/' + newDatabaseName + '/_design/keyview/_search/api-bar-test?q=' + searchStrEncoded;
+
+    client
+      .loginToGUI()
+      .populateDatabase(newDatabaseName)
+      .url(baseUrl + '/#/database/' + newDatabaseName + '/_all_docs')
+
+      // start creating a search index in _design/keyview design doc
+      .waitForElementPresent('#new-design-docs-button', client.globals.maxWaitTime, false)
+      .click('#new-design-docs-button a')
+      .click('#new-design-docs-button a[href="#/database/' + newDatabaseName + '/new_search"]')
+      .clickWhenVisible('.styled-select select')
+
+      // confirm there's no API URL field on the create index page
+      .pause(5000)
+      .assert.elementNotPresent('.faux__jsonlink')
+
+      // now create the rest of the index
+      .keys(['_design/keyview', '\uE006'])
+      .clearValue('#search-name')
+      .setValue('#search-name', 'api-bar-test')
+      .clickWhenVisible('#save-index')
+      .waitForElementVisible('#global-notifications .alert.alert-success', client.globals.maxWaitTime, false)
+
+      // confirm the API URL field now shows up (we're on the edit search index page now)
+      .assert.elementPresent('.faux__jsonlink')
+
+      // now enter a search and confirm it's properly encoded in the api URL bar
+      .setValue('#search-index-preview-form input', searchStr)
+      .clickWhenVisible('#search-index-preview-form button')
+
+      .waitForElementNotVisible('#global-notifications', client.globals.maxWaitTime, false)
+      .waitForElementNotPresent('.loading-lines', client.globals.maxWaitTime, false)
+      .assert.attributeContains('.faux__jsonlink-link', 'href', fullURL)
+      .end();
+  }
+};
diff --git a/app/addons/search/tests/nightwatch/sharedSearch.js b/app/addons/search/tests/nightwatch/sharedSearch.js
new file mode 100644
index 0000000..7cae1e2
--- /dev/null
+++ b/app/addons/search/tests/nightwatch/sharedSearch.js
@@ -0,0 +1,39 @@
+/*
+* Licensed Materials - Property of IBM
+*
+* "Restricted Materials of IBM"
+*
+* (C) Copyright IBM Corp. 2018 All Rights Reserved
+*
+* US Government Users Restricted Rights - Use, duplication or disclosure
+* restricted by GSA ADP Schedule Contract with IBM Corp.
+*/
+module.exports = {
+
+  'Edits existing search route works': function (client) {
+    /*jshint multistr: true */
+    const baseUrl = client.globals.test_settings.launch_url;
+
+    client
+      .loginToGUI()
+      .url(baseUrl + '/#database/shared/dashboard-test-account/shareddatabasesrule/_design/for-tests/_search/test-can-edit/edit')
+      // wait for the page to fully load
+      .waitForElementNotPresent('.loading-lines', client.globals.maxWaitTime, false)
+      .waitForElementVisible('#save-index', client.globals.maxWaitTime)
+      .end();
+  },
+
+  'Create new search route works': function (client) {
+    /*jshint multistr: true */
+    const baseUrl = client.globals.test_settings.launch_url;
+
+    client
+      .loginToGUI()
+      .url(baseUrl + '/#database/shared/dashboard-test-account/shareddatabasesrule/new_search')
+      // wait for the page to fully load
+      .waitForElementNotPresent('.loading-lines', client.globals.maxWaitTime, false)
+      .waitForElementVisible('#save-index', client.globals.maxWaitTime)
+      .end();
+  }
+
+};