Solr.js 16.1 KB
define(function(require) {
    'use strict';
    var _ = require('underscore');
    var Backbone = require('backbone');

    var Facets = require('solr/model/Facets');
    var Ordering = require('solr/model/Ordering');
    var Pagination = require('solr/model/Pagination');
    var QueryTextField = require('solr/model/QueryTextField');

    //var module = require('module');

    function mergeStatic(startPtr, endPtr, obj, fieldName, filterFunc) {
        var result = filterFunc(obj);
        var ptr = startPtr;
        while (true) {
            result = _.extend(result, filterFunc(_.result(ptr, fieldName)));
            if (ptr === endPtr) {
                break;
            }
            ptr = ptr.__super__.constructor;
        }
        return result;
    }

    function mergeStaticProps(startPtr, endPtr, obj, fieldName) {
        return mergeStatic(startPtr, endPtr, obj, fieldName, _.identity);
    }

    function mergeStaticSets(startPtr, endPtr, obj, fieldName) {
        return _.keys(mergeStatic(startPtr, endPtr, obj, fieldName, function arrayToMap(array) {
            var result = {};
            _.each(array, function(value) {
                result[value] = true;
            });
            return result;
        }));
    }

    function getItemKeyAccessor(item) {
        return item.get('key');
    }
    var Solr = Pagination.extend({
        url: function url() {
            return this.constructor.selectUrl;
        },
        defaults: function defaults() {
            var constructor = this.constructor;
            var formNameMap = {};
            var facets = new Facets(this.constructor.facets, {search: this});
            _.each(facets.values(), function(facet) {
                var formName = facet.get('formName');
                if (formName) {
                    formNameMap[formName] = facet;
                }
            });
            var queryFields = new Backbone.Model();
            _.each(constructor.queryTextFields, function(definition, queryName) {
                var qtf = new QueryTextField({formName: definition.formName, name: queryName, queries: [], fields: definition.fields, multi: !!definition.multi});
                var formName = qtf.get('formName');
                if (formName) {
                    formNameMap[formName] = qtf;
                }
                queryFields.set(queryName, qtf);
            }, this);
            return _.extend(_.result(Solr.__super__, 'defaults'), {
                initializing: true,
                initialized: false,
                query: '',
                formNameMap: formNameMap,
                results: new Backbone.Collection(),
                ordering: new Ordering({items: constructor.orderingItems}, {parse: true}),
                facets: facets,
                queryFields: queryFields,
            });
        },
        applyQueryParameters: function() {
            var skipOptions = {skipSearch: true};
            var parts = document.location.href.match(/.*\?(.*)/);
            var facets = this.get('facets');
            facets.resetSearch();
            _.each(this.get('queryFields').values(), function(qtf) {
                qtf.set({
                    query: null,
                    queries: [],
                }, skipOptions);
            });

            if (parts) {
                var formNameMap = this.get('formNameMap');
                var keyValueParts = parts[1].split('&');
                _.each(keyValueParts, function(keyValuePart, i) {
                    var keyFieldValue = keyValuePart.match(/^([^.]+)(?:\.([^.]+))?=(.*)$/);
                    if (keyFieldValue) {
                        var key = keyFieldValue[1];
                        var field = keyFieldValue[2];
                        var value = keyFieldValue[3];
                        value = value.replace(/(\+|%20)/g, ' ');
                        var impl = formNameMap[key];
                        if (impl) {
                            if (impl instanceof QueryTextField) {
                                impl.set('query', value, skipOptions);
                            } else if (impl.facetType === 'field' && value) {
                                if (field === 'min') {
                                    impl.set('queryMin', parseInt(value), skipOptions);
                                } else if (field === 'max') {
                                    impl.set('queryMax', parseInt(value), skipOptions);
                                } else if (field === 'items') {
                                    var items = impl.get('items');
                                    var item = items.get(value);
                                    if (!item) {
                                        item = new Facets.Facet.Item({key: value, value: 0});
                                        items.add(item);
                                        item.on('change:checked', function(model, value, options) {
                                            this.trigger('item-change', null, null, options);
                                            impl.trigger('item-change', null, null, options);
                                        }, facets);
                                    }
                                    item.set('checked', true, skipOptions);
                                }
                            }
                        }
                    }
                }, this);
            }
        },
        initialize: function(data, options) {
            options = (options || {});
            this.set('options', new Backbone.Model({
                faceting: !!options.faceting,
                highlighting: !!options.highlighting,
                showAll: !!options.showAll,
            }));
            this._doSearchImmediately = _.bind(function(options) {
                this.fetch(_.extend({}, options, {
                    data: this.toJSON(),
                    merge: false,
                    traditional: true,
                }));
            }, this);
            this._doSearch = _.debounce(this._doSearchImmediately, 250);
            this._doSearch = (function(doSearch) {
                return function(options) {
                    var skipSearch = this._skipSearch || (options || {}).skipSearch;
                    if (!skipSearch) {
                        doSearch(options);
                    }
                };
            })(this._doSearch);
            _.each(this.get('queryFields').values(), function(subQueryModel) {
                subQueryModel.on('change:query change:queries', function(model, value, options) {
                    this.trigger('change:queryFields', null, null, options);
                }, this);
            }, this);
            this.on('change:queryFields', function(model, value, options) {
                this.trigger('change:query', null, null, options);
            }, this);
            this.get('options').on('change:faceting change:highlighting', function(model, value, options) {
                this._doSearch(options);
            }, this);
            this.on('change:query', function(model, value, options) {
                // this is silent, which causes the change events to not fire;
                // this is ok, because the next .on will also see the change
                // event on query, and resend the search
                this._doSearch(_.extend({}, options, {resetCurrentPage: true, resetFacets: true, resetOrdering: true}));
            }, this);
            this.on('change:pageSize', function(model, value, options) {
                this._doSearch(_.extend({}, options, {resetCurrentPage: true}));
            }, this);
            this.on('change:currentPage', function(model, value, options) {
                this._doSearch(options);
            }, this);
            this.get('facets').on('item-change', function(model, value, options) {
                this._doSearch(_.extend({}, options, {resetCurrentPage: true}));
            }, this);
            this.get('ordering').on('change:value', function(model, value, options) {
                this._doSearch(_.extend({}, options, {resetCurrentPage: true}));
            }, this);
            return Solr.__super__.initialize.apply(this, arguments);

        },
        parse: function parse(data, options) {
            if (options.facetSuggestions) {
                return null;
            }
            var skipOptions = _.extend({}, options, {skipSearch: true});
            if (options.resetCurrentPage) {
                this.set('currentPage', 1, skipOptions);
            }
            if (options.resetFacets) {
                this.get('facets').resetSearch(skipOptions);
            }
            if (options.resetOrdering) {
                this.get('ordering').set('value', this.get('ordering').get('items').at(0).get('value'), skipOptions);
            }
            var facets = this.get('facets');
            if (this.get('options').get('faceting')) {
                facets.applyFacetResults(data);
            } else {
                facets.resetSearch(options);
            }
            var list = [];
            var highlighting = this.get('options').get('highlighting') ? data.highlighting : {};

            var self = this;
            _.each(data.response.docs, function(doc, index) {
                var itemHighlighting = highlighting[doc.id];
                if (!itemHighlighting) {
                    itemHighlighting = {};
                }
                list.push(self.searchParseDoc(doc, index, itemHighlighting));
            });

            return {
                initializing: false,
                initialized: true,
                results: new Backbone.Collection(list),
                facets: facets,
                options: this.get('options'),
                totalCount: data.response.numFound,
                queryTime: (data.responseHeader.QTime / 1000).toFixed(2),
                hasResults: list.length > 0,
            };
        },
        searchParseDoc: function searchParseDoc(doc, index, itemHighlighting) {
            var fieldsToParse = mergeStaticProps(this.constructor, Solr, {}, 'parsedFieldMap');
            var result = {};
            _.each(fieldsToParse, function(value, key) {
                var parsed;
                if (_.isFunction(value)) {
                    parsed = value.call(this, doc, index, itemHighlighting, key);
                } else if (_.isArray(value)) {
                    parsed = [];
                    _.each(value, function(item, i) {
                        var rawValue = doc[item.name];
                        if (rawValue) {
                            var parsedValue = item.parse.call(this, doc, index, itemHighlighting, rawValue, item.name);
                            if (parsedValue) {
                                parsed.push(parsedValue);
                            }
                        }
                    }, this);
                } else {
                    parsed = doc[value];
                }
                result[key] = parsed;
            }, this);
            return result;
        },
        toJSON: function(options) {
            if (!options) {
                options = {};
            }
            var facets = this.get('facets');
            var constructor = this.constructor;
            var result = {
                defType: 'edismax',
                qf: constructor.queryField,
                wt: 'json',
            };
            var ordering = this.get('ordering');
            var sort = ordering.get('value');
            if (options.resetOrdering) {
                sort = ordering.get('items').at(0).get('value');
            }
            var currentPage = this.get('currentPage');
            var pageSize = this.get('pageSize');
            if (options.resetCurrentPage) {
                currentPage = 1;
            }
            result.sort = sort;
            result.rows = pageSize;
            result.start = (currentPage - 1) * pageSize;
            result.fl = mergeStaticSets(constructor, Solr, [], 'returnFields');
            if (this.get('options').get('highlighting')) {
                result.hl = true;
                result['hl.fl'] = ['content', 'title'].concat(constructor.extraHighlightFields);
            }

            if (this.get('options').get('faceting')) {
                var facetFormData = facets.getFacetFormData();
                var fq = facetFormData.fq;
                delete(facetFormData.fq);
                result = _.extend(result, facetFormData);
                if (fq.length) {
                    if (result.fq) {
                        result.fq = result.fq.concat(fq);
                    } else {
                        result.fq = fq;
                    }
                }
            }

            var queryParts = [];
            var queryFields = this.get('queryFields');
            _.each(queryFields.keys(), function(queryName) {
                var subQueryModel = queryFields.get(queryName);
                var fields = subQueryModel.get('fields');
                var queries = subQueryModel.get('queries');
                var query = subQueryModel.get('query');
                if (query) {
                    queries = queries.concat([query]);
                }
                var subQuery = [];
                for (var i = 0; i < fields.length; i++) {
                    var fieldName = fields[i];
                    for (var j = 0; j < queries.length; j++) {
                        // FIXME: quote the query
                        subQuery.push(fieldName + ':\'' + queries[j] + '\'');
                    }
                }
                if (subQuery.length) {
                    queryParts.push(subQuery.join(' OR '));
                }
            }, this);

            var query = queryParts.join(' AND ');
            if (!query) {
                query = '*:*';
                if (!this.get('options').get('showAll') && !result.fq) {
                    result.rows = 0;
                }
            }
            var dateBoostField = constructor.dateBoostField;
            var popularityBoostField = constructor.popularityBoostField;
            if (result.rows !== 0 && (dateBoostField || popularityBoostField)) {
                var boostSum = [];
                if (dateBoostField) {
                    result.dateBoost = 'recip(ms(NOW,' + dateBoostField + '),3.16e-11,1,1)';
                    boostSum.push('$dateBoost');
                }
                if (popularityBoostField) {
                    result.popularityBoost = 'def(' + popularityBoostField + ',0)';
                    boostSum.push('$popularityBoost');
                }
                result.qq = query;
                result.q = '{!boost b=sum(' + boostSum.join(',') + ') v=$qq defType=$defType}';
            } else {
                result.q = query;
            }
            return result;
        },
    }, {
        cleanup: function cleanup(txt) {
            if (txt) {
                txt = txt.replace(/&amp;/g, '&');
                txt = txt.replace(/&#39;/g, '\'');
                txt = txt.replace(/[^a-zA-Z0-9 -\.,:;%<>\/'"|]/g, ' ');
            }
            return txt;
        },
        returnFields: [
            'keywords',
            'last_modified',
            'path',
            'score',
            'title',
        ],
        parsedFieldMap: {
            content: function(doc, index, itemHighlighting) {
                var content = itemHighlighting.content;
                if (content && content.constructor === Array) {
                    content = content.join(' ');
                }
                if (!content) {
                    content = '';
                }
                return this.constructor.cleanup(content);
            },
            keywords: 'keywords',
            lastModified: 'last_modified',
            path: 'url',
            score: 'score',
            title: function(doc, index, itemHighlighting) {
                var title;
                if (itemHighlighting.title) {
                    title = itemHighlighting.title.join(' ');
                } else {
                    title = doc.title[0];
                }
                return this.constructor.cleanup(title);
            },
        },
    });
    Solr.Facets = Facets;
    Solr.Pagination = Pagination;
    return Solr;
});