# Source: src/js/models/Search.js

    /*global define */
    define(["jquery", "underscore", "backbone", "models/SolrResult", "collections/Filters"],
        function($, _, Backbone, SolrResult, Filters) {
            'use strict';

            /**
             * @class Search
             * @classdesc Search filters can be either plain text or a filter object with the following options:
             * filterLabel - text that will be displayed in the filter element in the UI
             * label - text that will be displayed in the autocomplete  list
             * value - the value that will be included in the query
             * description - a longer text description of the filter value
             * Example: {filterLabel: "Creator", label: "Jared Kibele (16)", value: "Kibele", description: "Search for data creators"}
             * @classcategory Models
             * @extends Backbone.Model
             * @constructor
             */
            var Search = Backbone.Model.extend(
              /** @lends Search.prototype */{

                /**
                * @type {object}
                * @property {Filters} filters - The collection of filters used to build a query, an instance of Filters
                */
                defaults: function() {
                    return {
                        all: [],
                        projectText: [],
                        creator: [],
                        taxon: [],
                        isPrivate: null,
                        documents: false,
                        resourceMap: false,
                        yearMin: 1900, //The user-selected minimum year
                        yearMax: new Date().getUTCFullYear(), //The user-selected maximum year
                        pubYear: false,
                        dataYear: false,
                        sortOrder: 'dateUploaded+desc',
                        sortByReads: false, // True if we can sort by reads/popularity
                        east: null,
                        west: null,
                        north: null,
                        south: null,
                        useGeohash: true,
                        geohashes: [],
                        geohashLevel: 9,
                        geohashGroups: {},
                        dataSource: [],
                        username: [],
                        rightsHolder: [],
                        submitter: [],
                        spatial: [],
                        attribute: [],
                        sem_annotation: [],
                        annotation: [],
                        additionalCriteria: [],
                        id: [],
                        seriesId: [],
                        idOnly: [],
                        provFields: [],
                        formatType: [{
                            value: "METADATA",
                            label: "science metadata",
                            description: null
                        }],
                        exclude: [{
                            field: "obsoletedBy",
                            value: "*"
                        },
                        {
                          field: "formatId",
                          value: "*dataone.org/collections*"
                        },
                        {
                          field: "formatId",
                          value: "*dataone.org/portals*"
                        }],
                        filters: null
                    }
                },

                //A list of all the filter names that are related to the spatial/map filter
                spatialFilters: ["useGeohash", "geohashes", "geohashLevel",
                    "geohashGroups", "east", "west", "north", "south"],

                initialize: function() {
                    this.listenTo(this, "change:geohashes", this.groupGeohashes);
                },

                fieldLabels: {
                    attribute: "Data attribute",
                    documents: "Only results with data",
                    annotation: "Annotation",
                    dataSource: "Data source",
                    creator: "Creator",
                    dataYear: "Data coverage",
                    pubYear: "Publish year",
                    id: "Identifier",
                    seriesId: "seriesId",
                    taxon: "Taxon",
                    spatial: "Location",
                    isPrivate: "Private datasets",
                    all: "",
                    projectText: "Project",
                },

                //Map the filter names to their index field names
                fieldNameMap: {
                    attribute: "attribute",
                    annotation: "sem_annotation",
                    dataSource: "datasource",
                    documents: "documents",
                    formatType: "formatType",
                    all: "",
                    creator: "originText",
                    spatial: "siteText",
                    resourceMap: "resourceMap",
                    pubYear: ["datePublished", "dateUploaded"],
                    id: ["id", "identifier", "documents", "resourceMap", "seriesId"],
                    idOnly: ["id", "seriesId"],
                    rightsHolder: "rightsHolder",
                    submitter: "submitter",
                    username: ["rightsHolder", "writePermission", "changePermission"],
                    taxon: ["kingdom", "phylum", "class", "order", "family", "genus", "species"],
                    isPrivate: "isPublic",
                    projectText: "projectText"
                },

                facetNameMap: {
                    "creator": "origin",
                    "attribute": "attribute",
                    "annotation": "sem_annotation",
                    "spatial": "site",
                    "taxon": ["kingdom", "phylum", "class", "order", "family", "genus", "species"],
                    "isPublic": "isPublic",
                    "all": "keywords",
                    "projectText": "project"
                },

                getCurrentFilters: function() {
                    var changedAttr = this.changedAttributes(_.clone(this.defaults())),
                        currentFilters = Object.keys(changedAttr),
                        ignoreAttr = ["sortOrder", "provFields"];

                    if (!changedAttr) return new Array();

                    //Check for changed attributes that should be ignored
                    _.each( Object.keys(changedAttr), function(attr){

                      //If the value is an empty array, but the default value is an empty array too,
                      // then it's not a changed filter attribute
                      if( Array.isArray(this.get(attr)) && this.get(attr).length == 0 &&
                          Array.isArray(this.defaults()[attr]) && this.defaults()[attr].length == 0 ){
                        currentFilters = _.without(currentFilters, attr);
                      }
                      else if( ignoreAttr.includes(attr) ){
                        currentFilters = _.without(currentFilters, attr);
                      }
                    }, this);

                    //Don't count the geohashes or directions as a filter if the geohash filter is turned off
                    if (!this.get("useGeohash")) {
                        currentFilters = _.difference(currentFilters, this.spatialFilters);
                    }

                    return currentFilters;
                },

                filterCount: function() {
                    var currentFilters = this.getCurrentFilters();

                    return currentFilters.length;
                },

                //Function filterIsAvailable will check if a filter is available in this search index -
                //if the filter name if included in the defaults of this model, it is marked as available.
                //Comment out or remove defaults that are not in the index or should not be included in queries
                filterIsAvailable: function(name) {
                    //Get the keys for this model as a way to list the filters that are available
                    var defaults = _.keys(this.defaults());
                    if (_.indexOf(defaults, name) >= 0) {
                        return true;
                    } else {
                        return false;
                    }
                },

                /*
                 * Removes a specified filter from the search model
                 */
                removeFromModel: function(category, filterValueToRemove) {
                    //Remove this filter term from the model
                    if (category) {
                        //Get the current filter terms array
                        var currentFilterValues = this.get(category);

                        //The year filters have special rules
                        //If both year types will be reset/default, then also reset the year min and max values
                        if ((category == "pubYear") || (category == "dataYear")) {
                            var otherType = (category == "pubYear") ? "dataYear" : "pubYear";

                            if (_.contains(this.getCurrentFilters(), otherType))
                                var newFilterValues = this.defaults()[category];
                            else {
                                this.set(category, this.defaults()[category]);
                                this.set("yearMin", this.defaults()["yearMin"]);
                                this.set("yearMax", this.defaults()["yearMax"]);
                                return;
                            }

                        } else if (Array.isArray(currentFilterValues)) {
                            //Remove this filter term from the array
                            var newFilterValues = _.without(currentFilterValues, filterValueToRemove);
                            _.each(currentFilterValues, function(currentFilterValue, key) {
                                var valueString = (typeof currentFilterValue == "object") ? currentFilterValue.value : currentFilterValue;
                                if (valueString == filterValueToRemove) {
                                    newFilterValues = _.without(newFilterValues, currentFilterValue);
                                }
                            });
                        } else {
                            //Get the default value
                            var newFilterValues = this.defaults()[category];
                        }

                        //Set the new value
                        this.set(category, newFilterValues);

                    }
                },

                /**
                 *
                 * @param {Filters|Filter[]} filters The collection of filters to add to this model OR an array of Filter models
                 */
                addFilters: function(filters){
                  try{

                    let currentFilters = this.get("filters");

                    //If the passed collection is the same as the one set already, return
                    if( currentFilters == filters )
                      return;
                    //If the given Filters collec is different than the one set on the model now, combine them
                    else if( Filters.isPrototypeOf(currentFilters) && Filters.isPrototypeOf(filters) ){
                      filters.models.forEach(f => { currentFilters.add(f) });
                      this.set("filters", currentFilters);
                    }
                    else if( Filters.isPrototypeOf(currentFilters) && Array.isArray(filters) ){
                      filters.forEach(f => { currentFilters.add(f) });
                      this.set("filters", currentFilters);
                    }
                    else if( !currentFilters )
                      this.set("filters", new Filters(filters));

                  }
                  catch(e){
                    console.error("Couldn't add Filters to the Search model: ", e);
                  }
                },

                /*
                 * Resets the geoashes and geohashLevel filters to default
                 */
                resetGeohash: function() {
                    this.set("geohashes", this.defaults().geohashes);
                    this.set("geohashLevel", this.defaults().geohashLevel);
                    this.set("geohashGroups", this.defaults().geohashGroups);
                },

                groupGeohashes: function() {
                    //Find out if there are any geohashes that can be combined together, by looking for all 32 geohashes within the same precision level
                    var sortedGeohashes = this.get("geohashes");
                    sortedGeohashes.sort();

                    var groupedGeohashes = _.groupBy(sortedGeohashes, function(n) {
                        return n.substring(0, n.length - 1);
                    });

                    //Find groups of geohashes that makeup a complete geohash tile (32) so we can shorten the query
                    var completeGroups = _.filter(Object.keys(groupedGeohashes), function(n) {
                        return (groupedGeohashes[n].length == 32)
                    });
                    //Find the remaining incomplete geohash groupss
                    var incompleteGroups = [];
                    _.each(_.filter(Object.keys(groupedGeohashes), function(n) {
                        return (groupedGeohashes[n].length < 32)
                    }), function(n) {
                        incompleteGroups.push(groupedGeohashes[n]);
                    });
                    incompleteGroups = _.flatten(incompleteGroups);

                    //Start a geohash group object
                    var geohashGroups = {};
                    if ((typeof incompleteGroups !== "undefined") && (incompleteGroups.length > 0)) {
                        geohashGroups[incompleteGroups[0].length.toString()] = incompleteGroups;
                    }
                    if ((typeof completeGroups !== "undefined") && (completeGroups.length > 0)) {
                        geohashGroups[completeGroups[0].length.toString()] = completeGroups;
                    }
                    //Save it
                    this.set("geohashGroups", geohashGroups);
                    this.trigger("change", "geohashGroups");
                },

                hasGeohashFilter: function(){
                  var currentGeohashFilter = this.get("geohashGroups");
                  return (typeof currentGeohashFilter == "object" && Object.keys(currentGeohashFilter).length > 0);
                },

                /**
                 * Builds the query string to send to the query engine. Goes over each filter specified in this model and adds to the query string.
                 * Some filters have special rules on how to format the query, which are built first, then the remaining filters are tacked on to the
                 * query string as a basic name:value pair. These "other filters" are specified in the otherFilters variable.
                 * @param {string} filter - A single filter to get a query fragment for
                 * @param {object} options - Additional options for this function
                 * @property {boolean} options.forPOST - If true, the query will not be url-encoded, for POST requests
                 */
                getQuery: function(filter, options) {

                    //----All other filters with a basic name:value pair pattern----
                    var otherFilters = ["attribute", "formatType", "rightsHolder", "submitter"];

                    //Start the query string
                    var query = "",
                        forPOST = false;

                    //See if we are looking for a sub-query or a query for all filters
                    if (typeof filter == "undefined") {
                        var filter = null;
                        var getAll = true;
                    } else {
                        var getAll = false;
                    }

                    //Get the options sent to this function via the options object
                    if( typeof options == "object" && options ){
                      forPOST = options.forPOST;
                    }

                    var model = this;

                    //-----Annotation-----
                    if (this.filterIsAvailable("annotation") && ((filter == "annotation") || getAll)) {
                        var annotations = this.get("annotation");
                        _.each(annotations, function(annotationFilter, key, list) {
                            var filterValue = "";

                            //Get the filter value
                            if (typeof annotationFilter == "object") {
                                filterValue = annotationFilter.value || "";
                            } else {
                                filterValue = annotationFilter;
                            }

                            // Trim leading and trailing whitespace just in case
                            filterValue = filterValue.trim();

                            if( forPOST ){
                              // Encode and wrap URI in urlencoded double quote chars
                              filterValue = '"' + filterValue.trim() + '"';
                            }
                            else{
                              // Encode and wrap URI in urlencoded double quote chars
                              filterValue = "%22" + encodeURIComponent(filterValue.trim()) + "%22";
                            }

                            query += model.fieldNameMap["annotation"] + ":" + filterValue;
                        });
                    }

                    //---Identifier---
                    if (this.filterIsAvailable("id") && ((filter == "id") || getAll) && this.get('id').length) {
                        var identifiers = this.get('id');

                        if (Array.isArray(identifiers)) {
                          if( query.length ){
                            query += " AND ";
                          }

                          query += this.getGroupedQuery(this.fieldNameMap["id"], identifiers, {
                            operator: "OR",
                            subtext: true
                          });

                        } else if (identifiers) {
                          if( query.length ){
                            query += " AND ";
                          }

                          if( forPOST ){
                            query += this.fieldNameMap["id"] + ':*' + this.escapeSpecialChar(identifiers) + "*";
                          }
                          else{
                            query += this.fieldNameMap["id"] + ':*' + this.escapeSpecialChar(encodeURIComponent(identifiers)) + "*";
                          }
                        }
                    }

                    //---resourceMap---
                    if (this.filterIsAvailable("resourceMap") && ((filter == "resourceMap") || getAll)) {
                        var resourceMap = this.get('resourceMap');

                        //If the resource map search setting is a list of resource map IDs
                        if (Array.isArray(resourceMap)) {
                          if( query.length ){
                            query += " AND ";
                          }

                          query += this.getGroupedQuery(this.fieldNameMap["resourceMap"], resourceMap, {
                              operator: "OR",
                              forPOST: forPOST
                          });

                        } else if (resourceMap) {
                          if( query.length ){
                            query += " AND ";
                          }
                          //Otherwise, treat it as a binary setting
                          query += this.fieldNameMap["resourceMap"] + ':*';
                        }
                    }

                    //---documents---
                    if (this.filterIsAvailable("documents") && ((filter == "documents") || getAll)) {
                        var documents = this.get('documents');

                        //If the documents search setting is a list ofdocuments IDs
                        if (Array.isArray(documents)) {
                          if( query.length ){
                            query += " AND ";
                          }

                          query += this.getGroupedQuery(this.fieldNameMap["documents"], documents, {
                              operator: "OR",
                              forPOST: forPOST
                          });
                        } else if (documents) {

                          if( query.length ){
                            query += " AND ";
                          }
                          //Otherwise, treat it as a binary setting
                          query += this.fieldNameMap["documents"] + ':*';
                        }
                    }

                    //---Username: search for this username in rightsHolder and submitter ---
                    if (this.filterIsAvailable("username") && ((filter == "username") || getAll) && this.get('username').length) {
                        var username = this.get('username');
                        if (username) {
                          if( query.length ){
                            query += " AND ";
                          }

                          query += this.getGroupedQuery(this.fieldNameMap["username"], username, {
                              operator: "OR",
                              forPOST: forPOST
                          });
                        }
                    }

                    //--- ID Only - searches only the id and seriesId fields ---
                    if (this.filterIsAvailable("idOnly") && ((filter == "idOnly") || getAll) && this.get('idOnly').length) {
                        var idOnly = this.get('idOnly');
                        if (idOnly) {

                            if( query.length ){
                              query += " AND ";
                            }

                            query += this.getGroupedQuery(this.fieldNameMap["idOnly"], idOnly, {
                                operator: "OR",
                                forPOST: forPOST
                            });
                        }
                    }

                    //---Taxon---
                    if (this.filterIsAvailable("taxon") && ((filter == "taxon") || getAll) && this.get('taxon').length) {
                        var taxon = this.get('taxon');

                        for (var i = 0; i < taxon.length; i++) {
                            var value = (typeof taxon == "object") ? taxon[i].value : taxon[i].trim();

                            query += this.getMultiFieldQuery(this.fieldNameMap["taxon"], value, {
                                subtext: true,
                                forPOST: forPOST
                            });
                        }
                    }

                    //------Pub Year-----
                    if (this.filterIsAvailable("pubYear") && ((filter == "pubYear") || getAll)) {
                        //Get the types of year to be searched first
                        var pubYear = this.get('pubYear');
                        if (pubYear) {
                            //Get the minimum and maximum years chosen
                            var yearMin = this.get('yearMin');
                            var yearMax = this.get('yearMax');

                            if( query.length ){
                              query += " AND ";
                            }

                            //Add to the query if we are searching publication year
                            query += this.getMultiFieldQuery(this.fieldNameMap["pubYear"], "[" + yearMin + "-01-01T00:00:00Z TO " + yearMax + "-12-31T00:00:00Z]",
                                                              {
                                                                forPOST: forPOST
                                                              });
                        }
                    }

                    //-----Data year------
                    if (this.filterIsAvailable("dataYear") && ((filter == "dataYear") || getAll)) {
                        var dataYear = this.get('dataYear');

                        if (dataYear) {
                            //Get the minimum and maximum years chosen
                            var yearMin = this.get('yearMin');
                            var yearMax = this.get('yearMax');

                            if( query.length ){
                              query += " AND ";
                            }

                            query += "beginDate:[" + yearMin + "-01-01T00:00:00Z TO *]" +
                                " AND endDate:[* TO " + yearMax + "-12-31T00:00:00Z]";
                        }
                    }

                    //----- public/private ------
                    if (this.filterIsAvailable("isPrivate") && ((filter == "isPrivate") || getAll)) {

                        var isPrivate = this.get('isPrivate');
                        if (isPrivate !== null && isPrivate !== 'undefined') {

                            if( query.length ){
                              query += " AND ";
                            }

                            // Currently, the Solr field 'isPublic' can be set to true or false, or not set.
                            // isPrivate is equivalent to "isPublic:false" or isPublic not set
                            if(isPrivate) {
                                query += "-isPublic:true"
                            }
                        }
                    }

                    //-----Data Source--------
                    if (this.filterIsAvailable("dataSource") && ((filter == "dataSource") || getAll)) {
                        var filterValue = null;
                        var filterValues = [];

                        if (this.get("dataSource").length > 0) {
                            var objectValues = _.filter(this.get("dataSource"), function(v) {
                                return (typeof v == "object")
                            });
                            if (objectValues && objectValues.length) {
                                filterValues.push(_.pluck(objectValues, "value"));
                            }
                        }

                        var stringValues = _.filter(this.get("dataSource"), function(v) {
                            return (typeof v == "string")
                        });
                        if (stringValues && stringValues.length) {
                            filterValues.push(stringValues);
                        }

                        filterValues = _.flatten(filterValues);

                        if( filterValues.length ){
                          if( query.length ){
                            query += " AND ";
                          }

                          query += this.getGroupedQuery(this.fieldNameMap["dataSource"], filterValues, {
                              operator: "OR",
                              forPOST: forPOST
                          });
                        }
                    }

                    //-----Excluded fields-----
                    if (this.filterIsAvailable("exclude") && ((filter == "exclude") || getAll)) {
                        var exclude = this.get("exclude");
                        _.each(exclude, function(excludeField, key, list) {

                            if (model.needsQuotes(excludeField.value)) {
                              if( forPOST ){
                                var filterValue = '"' + excludeField.value + '"';
                              }
                              else{
                                var filterValue = "%22" + encodeURIComponent(excludeField.value) + "%22";
                              }
                            } else {
                              if( forPOST ){
                                var filterValue = excludeField.value;
                              }
                              else{
                                var filterValue = encodeURIComponent(excludeField.value);
                              }
                            }

                            filterValue = model.escapeSpecialChar(filterValue);

                            if( query.length ){
                              query += " AND ";
                            }

                            query += " -" + excludeField.field + ":" + filterValue;
                        });
                    }

                    //-----Additional criteria - both field and value are provided-----
                    if (this.filterIsAvailable("additionalCriteria") && ((filter == "additionalCriteria") || getAll)) {
                        var additionalCriteria = this.get('additionalCriteria');
                        for (var i = 0; i < additionalCriteria.length; i++) {
                            var value;

                            if( forPOST ){
                              value = additionalCriteria[i];
                            }
                            else{
                              //if(this.needsQuotes(additionalCriteria[i])) value = "%22" + encodeURIComponent(additionalCriteria[i]) + "%22";
                              value = encodeURIComponent(additionalCriteria[i]);
                            }

                            if( query.length ){
                              query += " AND ";
                            }

                            query += model.escapeSpecialChar(value);
                        }
                    }

                    //-----All (full text search) -----
                    if (this.filterIsAvailable("all") && ((filter == "all") || getAll)) {
                        var all = this.get('all');
                        for (var i = 0; i < all.length; i++) {
                            var filterValue = all[i];

                            if (typeof filterValue == "object") {
                                filterValue = filterValue.value;
                            }
                            else if( (typeof filterValue == "string" && !filterValue.length) ||
                                      typeof filterValue == "undefined" || filterValue === null){
                              continue;
                            }

                            if (this.needsQuotes(filterValue)) {
                              if( forPOST ){
                                filterValue = '"' + filterValue + '"';
                              }
                              else{
                                filterValue = "%22" + encodeURIComponent(filterValue) + "%22";
                              }
                            } else {
                              if( forPOST ){
                                filterValue = filterValue;
                              }
                              else{
                                filterValue = encodeURIComponent(filterValue);
                              }
                            }

                            if( query.length ){
                              query += " AND ";
                            }

                            query += model.escapeSpecialChar(filterValue);
                        }
                    }

                    //-----Other Filters/Basic Filters-----
                    _.each(otherFilters, function(filterName, key, list) {
                        if (model.filterIsAvailable(filterName) && ((filter == filterName) || getAll)) {
                            var filterValue = null;
                            var filterValues = model.get(filterName);

                            for (var i = 0; i < filterValues.length; i++) {

                                //Trim the spaces off
                                var filterValue = filterValues[i];
                                if (typeof filterValue == "object") {
                                    filterValue = filterValue.value;
                                }
                                filterValue = filterValue.trim();

                                // Does this need to be wrapped in quotes?
                                if (model.needsQuotes(filterValue)) {
                                  if( forPOST ){
                                    filterValue = '"' + filterValue + '"';
                                  }
                                  else{
                                    filterValue = "%22" + encodeURIComponent(filterValue) + "%22";
                                  }
                                } else  {
                                  if( forPOST ){
                                    filterValue = filterValue;
                                  }
                                  else{
                                    filterValue = encodeURIComponent(filterValue);
                                  }
                                }

                                if( query.length ){
                                  query += " AND ";
                                }

                                query += model.fieldNameMap[filterName] + ":" + model.escapeSpecialChar(filterValue);
                            }
                        }
                    });

                    //-----Geohashes-----
                    if (this.filterIsAvailable("geohashLevel") && (((filter == "geohash") || getAll)) && this.get("useGeohash")) {
                        var geohashes = this.get("geohashes");

                        if ((typeof geohashes != undefined) && (geohashes.length > 0)) {
                            var groups = this.get("geohashGroups"),
                                numGroups = (typeof groups == "object")? Object.keys(groups).length : 0;

                            if(numGroups > 0){
                              //Add the AND operator in front of the geohash filter
                              if( query.length ){
                                query += " AND ";
                              }

                              //If there is more than one geohash group/level, wrap them in paranthesis
                              if( numGroups > 1){
                                query += "(";
                              }

                              _.each(Object.keys(groups), function(level, i, allLevels) {
                                  var geohashList = groups[level];

                                  query += "geohash_" + level + ":";

                                  if( geohashList.length > 1 ){
                                    query += "(";
                                  }

                                  _.each(geohashList, function(g, ii, allGeohashes) {
                                      //Keep URI's from getting too long if we are using GET
                                      if( MetacatUI.appModel.get("disableQueryPOSTs") && query.length > 1900){

                                        //Remove the last " OR "
                                        if( query.endsWith(" OR ") ){
                                          query = query.substring(0, query.length-4)
                                        }

                                        return;
                                      }
                                      else{
                                        //Add the geohash value to the query
                                        query += g;

                                        //Add an " OR " operator inbetween geohashes
                                        if( ii < allGeohashes.length-1 ){
                                          query += " OR ";
                                        }
                                      }
                                  });

                                  //Close the paranthesis
                                  if( geohashList.length > 1 ){
                                    query += ")";
                                  }

                                  //Add an " OR " operator inbetween geohash levels
                                  if( i < allLevels.length-1 ){
                                    query += " OR "
                                  }

                              });

                              //Close the paranthesis
                              if(numGroups > 1){
                                query += ")";
                              }
                            }
                        }
                    }

                    //---Spatial---
                    if (this.filterIsAvailable("spatial") && ((filter == "spatial") || getAll)) {
                        var spatial = this.get('spatial');

                        if (Array.isArray(spatial) && spatial.length) {

                          if( query.length ){
                            query += " AND ";
                          }

                          query += this.getGroupedQuery(this.fieldNameMap["spatial"], spatial, {
                              operator: "AND",
                              subtext: false,
                              forPOST: forPOST
                          });

                        } else if( typeof spatial == "string" && spatial.length) {

                          if( query.length ){
                            query += " AND ";
                          }

                          if( forPOST ){
                            query += this.fieldNameMap["spatial"] + ':' + model.escapeSpecialChar(spatial);
                          }
                          else{
                            query += this.fieldNameMap["spatial"] + ':' + model.escapeSpecialChar(encodeURIComponent(spatial));
                          }

                        }
                    }

                    //---Creator---
                    if (this.filterIsAvailable("creator") && ((filter == "creator") || getAll)) {
                        var creator = this.get('creator');

                        if (Array.isArray(creator) && creator.length) {
                          if( query.length ){
                            query += " AND ";
                          }

                          query += this.getGroupedQuery(this.fieldNameMap["creator"], creator, {
                              operator: "AND",
                              subtext: false,
                              forPOST: forPOST
                          });
                        } else if (typeof creator == "string" && creator.length) {
                          if( query.length ){
                            query += " AND ";
                          }

                          if( forPOST ){
                            query += this.fieldNameMap["creator"] + ':' + model.escapeSpecialChar(creator);
                          }
                          else{
                            query += this.fieldNameMap["creator"] + ':' + model.escapeSpecialChar(encodeURIComponent(creator));
                          }
                        }
                    }

                    // Add project filter
                    if (this.filterIsAvailable("projectText") && ((filter == "projectText") || getAll)) {

                        var project = this.get('projectText');
                        if (project && project.length > 0) {

                            if( query.length ){
                                query += " AND ";
                            }
                            query += 'projectText:"' + project[0].value + '"';
                        }
                    }

                    return query;
                },

                getFacetQuery: function(fields) {

                    var facetQuery = "&facet=true" +
                        "&facet.sort=count" +
                        "&facet.mincount=1" +
                        "&facet.limit=-1";

                    //Get the list of fields
                    if (!fields) {
                        var fields = "keywords,origin,family,species,genus,kingdom,phylum,order,class,site";
                        if (this.filterIsAvailable("annotation")) {
                            fields += "," + this.facetNameMap["annotation"];
                        }
                        if (this.filterIsAvailable("attribute")) {
                            fields += ",attributeName,attributeLabel";
                        }
                    }

                    var model = this;
                    //Add the fields to the query string
                    _.each(fields.split(","), function(f) {
                        var fieldNames = model.facetNameMap[f] || f;

                        if (typeof fieldNames == "string") {
                            fieldNames = [fieldNames];
                        }

                        _.each(fieldNames, function(fName) {
                            facetQuery += "&facet.field=" + fName;
                        });
                    });

                    return facetQuery;
                },

                //Check for spaces in a string - we'll use this to url encode the query
                needsQuotes: function(entry) {

                    //Check for spaces
                    var value = "";

                    if (typeof entry == "object") {
                        value = entry.value;
                    } else if (typeof entry == "string") {
                        value = entry;
                    } else {
                        return false;
                    }

                    //Is this a date range search? If so, we don't use quote marks
                    var ISODateRegEx = /\d{4}-[01]\d-[0-3]\dT[0-2]\d:[0-5]\d:[0-5]\d([+-][0-2]\d:[0-5]\d|Z)/;
                    if (ISODateRegEx.exec(value)) {
                        return false;
                    }

                    //Check for a space character
                    if (value.indexOf(" ") > -1) {
                        return true;
                    }

                    //Check if this is an account subject string
                    var LDAPSubjectRegEx = /(uid=|UID=|cn=|CN=).+([a-zA-Z]=).+([a-zA-Z]=).*/,
                        ORCIDRegEx = /^http\:\/\/orcid\.org\/[0-9]{4}-[0-9]{4}-[0-9]{4}-[0-9X]{4}/;

                    if (LDAPSubjectRegEx.exec(value) || ORCIDRegEx.exec(value)) {
                        return true;
                    }

                    return false;
                },

                escapeSpecialChar: function(term) {
                    term = term.replace(/%7B/g, "%5C%7B");
                    term = term.replace(/%7D/g, "%5C%7D");
                    term = term.replace(/%3A/g, "%5C%3A");
                    term = term.replace(/:/g, "%5C:");
                    term = term.replace(/\(/g, "%5C(");
                    term = term.replace(/\)/g, "%5C)");
                    term = term.replace(/\?/g, "%5C?");
                    term = term.replace(/%3F/g, "%5C%3F");
                    term = term.replace(/%2B/g, "%5C%2B");
                    //Remove ampersands (&) for now since they are reserved Solr characters and the Metacat Solr can't seem to handle them even when they are escaped properly for some reason
                    term = term.replace(/%26/g, "");
                    term = term.replace(/%7C%7C/g, "%5C%7C%5C%7C");
                    term = term.replace(/%21/g, "%5C%21");
                    term = term.replace(/%28/g, "%5C%28");
                    term = term.replace(/%29/g, "%5C%29");
                    term = term.replace(/%5B/g, "%5C%5B");
                    term = term.replace(/%5D/g, "%5C%5D");
                    term = term.replace(/%5E/g, "%5C%5E");
                    term = term.replace(/%22/g, "%5C%22");
                    term = term.replace(/~/g, "%5C~");
                    term = term.replace(/-/g, "%5C-");
                    term = term.replace(/%2F/g, "%5C%2F");

                    return term;
                },

                /*
                 * Makes a Solr syntax grouped query using the field name, the field values to search for, and the operator.
                 * Example:  title:(resistance OR salmon OR "pink salmon")
                 */
                getGroupedQuery: function(fieldName, values, options) {
                    if (!values) return "";
                    values = _.compact(values);
                    if (!values.length) return "";

                    var query = "",
                        numValues = values.length,
                        model = this;

                    if (Array.isArray(fieldName) && (fieldName.length > 1)) {
                        return this.getMultiFieldQuery(fieldName, values, options);
                    }

                    if (options && (typeof options == "object")) {
                        var operator = options.operator,
                            subtext  = options.subtext,
                            forPOST  = options.forPOST;
                    }

                    if ((typeof operator === "undefined") || !operator || ((operator != "OR") && (operator != "AND"))) {
                        var operator = "OR";
                    }

                    if (numValues == 1) {
                        var value = values[0],
                            queryAddition;

                        if (!Array.isArray(value) && (typeof value === "object") && value.value) {
                            value = value.value.trim();
                        }

                        if (this.needsQuotes(values[0])) {
                          if( forPOST ){
                            queryAddition = '"' + this.escapeSpecialChar(value) + '"';
                          }
                          else{
                            queryAddition = '%22' + this.escapeSpecialChar(encodeURIComponent(value)) + '%22';
                          }
                        } else if (subtext) {
                          if( forPOST ){
                            queryAddition = "*" + this.escapeSpecialChar(value) + "*";
                          }
                          else{
                            queryAddition = "*" + this.escapeSpecialChar(encodeURIComponent(value)) + "*";
                          }
                        } else {
                          if( forPOST ){
                            queryAddition = this.escapeSpecialChar(value);
                          }
                          else{
                            queryAddition = this.escapeSpecialChar(encodeURIComponent(value));
                          }
                        }
                        query = fieldName + ":" + queryAddition;
                    } else {
                        _.each(values, function(value, i) {
                            //Check for filter objects
                            if (!Array.isArray(value) && (typeof value === "object") && value.value) {
                                value = value.value.trim();
                            }

                            if (model.needsQuotes(value)) {
                              if( forPOST ){
                                value = '"' + value + '"';
                              }
                              else{
                                value = '%22' + encodeURIComponent(value) + '%22';
                              }
                            } else if (subtext) {
                              if( forPOST ){
                                value = "*" + this.escapeSpecialChar(value) + "*";
                              }
                              else{
                                value = "*" + this.escapeSpecialChar(encodeURIComponent(value)) + "*";
                              }
                            } else {
                              if( forPOST ){
                                value = this.escapeSpecialChar(value);
                              }
                              else{
                                value = this.escapeSpecialChar(encodeURIComponent(value));
                              }
                            }

                            if ((i == 0) && (numValues > 1)) {
                                query += fieldName + ":(" + value;
                            } else if ((i > 0) && (i < numValues - 1) && query.length) {
                                query += " " + operator + " " + value;
                            } else if( (i > 0) && (i < numValues - 1) ){
                                query += value;
                            } else if (i == numValues - 1) {
                                query += " " + operator + " " + value + ")";
                            }
                        }, this);
                    }

                    return query;
                },

                /*
                 * Makes a Solr syntax query using multiple field names, one field value to search for, and some options.
                 * Example: (family:*Pin* OR class:*Pin* OR order:*Pin* OR phlyum:*Pin*)
                 * Options:
                 * 		- operator (OR or AND)
                 * 		- subtext (binary) - will surround search value with wildcards to search for partial matches
                 * 		- Example:
                 * 			var options = { operator: "OR", subtext: true }
                 */
                getMultiFieldQuery: function(fieldNames, value, options) {
                    var query = "",
                        numFields = fieldNames.length,
                        model = this;

                    //Catch errors
                    if ((numFields < 1) || !value) return "";

                    //If only one field was given, then use the grouped query function
                    if (numFields == 1) {
                        return this.getGroupedQuery(fieldNames, value, options);
                    }

                    //Get the options
                    if (options && (typeof options == "object")) {
                        var operator = options.operator,
                            subtext  = options.subtext,
                            forPOST  = options.forPOST;
                    }

                    //Default to the OR operator
                    if ((typeof operator === "undefined") || !operator ||
                        ((operator != "OR") && (operator != "AND"))) {
                        var operator = "OR";
                    }
                    if ((typeof subtext === "undefined")) {
                        var subtext = false;
                    }

                    //Create the value string
                    //Trim the spaces off
                    if (!Array.isArray(value) && (typeof value === "object") && value.value) {
                        value = [value.value.trim()];
                    } else if (typeof value == "string") {
                        value = [value.trim()];
                    }

                    var valueString = "";
                    if (Array.isArray(value)) {
                        var model = this;

                        _.each(value, function(v, i) {
                            if ((typeof v == "object") && v.value) {
                                v = v.value;
                            }

                            if ((value.length > 1) && (i == 0)) {
                                valueString += "("
                            }

                            if (model.needsQuotes(v) || _.contains(fieldNames, "id")) {
                              if( forPOST ){
                                valueString += '"' + this.escapeSpecialChar(v.trim()) + '"';
                              }
                              else{
                                valueString += '"' + this.escapeSpecialChar(encodeURIComponent(v.trim())) + '"';
                              }
                            } else if (subtext) {
                              if( forPOST ){
                                valueString += "*" + this.escapeSpecialChar(v.trim()) + "*";
                              }
                              else{
                                valueString += "*" + this.escapeSpecialChar(encodeURIComponent(v.trim())) + "*";
                              }
                            } else {
                              if( forPOST ){
                                valueString += this.escapeSpecialChar(v.trim());
                              }
                              else{
                                valueString += this.escapeSpecialChar(encodeURIComponent(v.trim()));
                              }
                            }

                            if (i < value.length - 1) {
                                valueString += " OR ";
                            } else if ((i == value.length - 1) && (value.length > 1)) {
                                valueString += ")";
                            }

                        }, this);
                    } else valueString = value;

                    query = "(";

                    //Create the query string
                    var last = numFields - 1;
                    _.each(fieldNames, function(field, i) {
                        query += field + ":" + valueString;
                        if (i < last) {
                            query += " " + operator + " ";
                        }
                    });

                    query += ")";

                    return query;
                },

                /**** Provenance-related functions ****/
                // Returns which fields are provenance-related in this model
                // Useful for querying the index and such
                getProvFields: function() {
                  var provFields = this.get("provFields");

                  if( !provFields.length ){
                    var defaultFields = Object.keys(SolrResult.prototype.defaults);
                    provFields = _.filter(defaultFields, function(fieldName) {
                        if (fieldName.indexOf("prov_") == 0) return true;
                    });

                    this.set("provFields", provFields);
                  }

                  return provFields;
                },

                getProvFlList: function() {
                    var provFields = this.getProvFields(),
                        provFl = "";
                    _.each(provFields, function(provField, i) {
                        provFl += provField;
                        if (i < provFields.length - 1) provFl += ",";
                    });

                    return provFl;
                },

                clear: function() {
                    return this.set(_.clone(this.defaults()));
                }

            });
            return Search;
        });