Commit 1ca89ea2 authored by Rene Saarsoo's avatar Rene Saarsoo
Browse files

Integrate guides full text search.

Initial version of the UI - the search results are combined with
API search and shown in the same old dropdown.  This means there
isn't quite enough room to show the full context of full text search
matches and the search is also slower, but it fits nicely to the
current UI.  Plus it was easy to implement.

The most questionable part is how the guide results are placed inside
API search results.  I'm drawing some arbitrary lines between the
scores given by guides search to split the results into boxes like
the API search does.
parent f83bd840
Loading
Loading
Loading
Loading
+25 −1
Original line number Diff line number Diff line
@@ -66,10 +66,12 @@ Ext.define("Docs.ClassRegistry", {
     *     59  middle removed   guide
     *
     * @param {String} text  The query string to search for
     * @param {Object[]} [guides] Results of guides search, to be
     * combined with the results of API search.
     * @return {Object[]} array of the matching items from Docs.search.data
     * ordered by best matches first.
     */
    search: function(text) {
    search: function(text, guides) {
        // Each record has 1 of 5 possible sorting orders,
        var nSort = 5;
        // which is *4 by it being public/deprecated/private/removed,
@@ -93,6 +95,23 @@ Ext.define("Docs.ClassRegistry", {
        var adjPri = nSort * 2;
        var adjRem = nSort * 3;

        // When guides given, populate the result fields with them
        if (guides) {
            var guidePos = 4;
            for (var i=0; i<guides.length; i++) {
                var g = guides[i];
                if (g.score > 5) {
                    results[guidePos + adjPub + adjFul].push(g);
                }
                else if (g.score > 1) {
                    results[guidePos + adjPub + adjBeg].push(g);
                }
                else {
                    results[guidePos + adjPub + adjMid].push(g);
                }
            }
        }

        var searchFull = /[.:]/.test(text);
        var safeText = Ext.escapeRe(text);
        var reFull = new RegExp("^" + safeText + "$", "i");
@@ -103,6 +122,11 @@ Ext.define("Docs.ClassRegistry", {
        for (var i=0, len=searchData.length; i<len; i++) {
            var r = searchData[i];

            // Skip guides when guides search results already provided
            if (guides && r.icon === "icon-guide") {
                continue;
            }

            // when search text has "." or ":" in it, search from the full name
            // (e.g. "Ext.Component.focus" or "xtype: grid")
            // Otherwise search from just the short name (e.g. "focus" or "Component")
+64 −0
Original line number Diff line number Diff line
/**
 * Performs the full-text search of guides contents.
 */
Ext.define("Docs.GuideSearch", {
    singleton: true,

    /**
     * Peforms the search remotely, then calls the given function.
     *
     * @param {String} term  The query string to search for
     * @param {Function} callback Function to call with an array of search results.
     * @param {Object} scope Scope for the function.
     */
    search: function(term, callback, scope) {
        var request = this.currentRequest = Ext.data.JsonP.request({
            url: "http://support-test.sencha.com:8080/docsearch/search",
            params: {
                q: term,
                product: null,
                version: null,
                start: 0,
                limit: 100 // get all guides, there aren't so many.
            },
            callback: function(success, data) {
                // only continue when the finished request is the last
                // one that was started.
                if (success && data.success && this.currentRequest === request) {
                    callback.call(scope, Ext.Array.map(data.docs, this.adaptJson, this));
                }
            },
            scope: this
        });
    },

    adaptJson: function(guide) {
        return {
            icon: "icon-guide",
            name: this.format(guide.title),
            fullName: this.shortenContext(this.format(guide.body)),
            url: this.shortenUrl(guide.url),
            meta: {},
            score: guide.score
        };
    },

    // Extracts string from Array if needed
    // Escapes HTML (not escaped in JSON)
    // Gets rid of newlines (search results view doesn't like them)
    // Replaces supplied highlighting with our highlighting tags.
    format: function(data) {
        var s = Ext.isArray(data) ? data[0] : data;
        var s = Ext.String.htmlEncode(s).replace(/\n/g, " ");
        return s.replace(/&lt;em class=&quot;match&quot;&gt;(.*?)&lt;\/em&gt;/g, "<strong>$1</strong>");
    },

    // Removes search context before the matching text.
    shortenContext: function(s) {
        return s.replace(/^[^<]*/, '');
    },

    shortenUrl: function(url) {
        return url.replace(/^.*#!/, "#!");
    }
});
+9 −3
Original line number Diff line number Diff line
@@ -6,6 +6,7 @@ Ext.define('Docs.controller.Search', {

    requires: [
        'Docs.ClassRegistry',
        'Docs.GuideSearch',
        'Docs.store.Search',
        'Docs.History'
    ],
@@ -129,10 +130,16 @@ Ext.define('Docs.controller.Search', {
        this.getDropdown().hide();
    },

    // First performs guides search, then combines the results with
    // API search.
    search: function(term) {
        // perform search and load results to store
        var results = Docs.ClassRegistry.search(term);
        Docs.GuideSearch.search(term, function(guideResults) {
            this.displayResults(Docs.ClassRegistry.search(term, guideResults));
        }, this);
    },

    // Loads results to store and shows the dropdown.
    displayResults: function(results) {
        // Don't allow paging before first or after the last page.
        if (this.pageIndex < 0) {
            this.pageIndex = 0;
@@ -154,5 +161,4 @@ Ext.define('Docs.controller.Search', {
            this.getDropdown().getSelectionModel().select(0);
        }
    }

});