Source: 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"}
         * @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: [],
                    creator: [],
                    taxon: [],
                    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",
                all: ""
            },

            //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"]
            },

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

            getCurrentFilters: function() {
                var changedAttr = this.changedAttributes(_.clone(this.defaults()));

                if (!changedAttr) return new Array();

                var currentFilters = _.keys(changedAttr);

                //Don't count the sort order as a changed filter
                currentFilters = _.without(currentFilters, "sortOrder");

                //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);

                }
            },

            /*
             * 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");
            },

            /**
             * 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]";
                    }
                }

                //-----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));
                      }
                    }
                }

                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;
                }

                return false;
            },

            escapeSpecialChar: function(term) {
                term = term.replace(/%7B/g, "\\%7B");
                term = term.replace(/%7D/g, "\\%7D");
                term = term.replace(/%3A/g, "\\%3A");
                term = term.replace(/:/g, "\\:");
                term = term.replace(/\(/g, "\\(");
                term = term.replace(/\)/g, "\\)");
                term = term.replace(/\?/g, "\\?");
                term = term.replace(/%3F/g, "\\%3F");

                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;
    });