diff --git a/docs/api/v3.rst b/docs/api/v3.rst index d2a91b02d6b..fbcd6704f53 100644 --- a/docs/api/v3.rst +++ b/docs/api/v3.rst @@ -1364,3 +1364,8 @@ Environment Variable delete :requestheader Authorization: token to authenticate. :statuscode 204: Environment variable deleted successfully + +Additional APIs +--------------- + +- :ref:`Server side search API `. diff --git a/docs/server-side-search.rst b/docs/server-side-search.rst index dd6795c1651..9677001960e 100644 --- a/docs/server-side-search.rst +++ b/docs/server-side-search.rst @@ -64,3 +64,123 @@ and then click on :guilabel:`Search Analytics`. Search analytics demo .. _Elasticsearch: https://www.elastic.co/products/elasticsearch + +API +--- + +Search is exposed through our API that's proxied from the domain where your docs are being served. +This is ``https://docs.readthedocs.io/_/api/v2/search`` for the ``docs`` project, for example. + +.. warning:: + + This API isn't stable yet, some small things may change in the future. + +.. http:get:: /_/api/v2/search/ + + Return a list of search results for a project, + including results from its :doc:`/subprojects`. + Results are divided into sections with highlights of the matching term. + + .. Request + + :query q: Search query + :query project: Project slug + :query version: Version slug + + .. Response + + :>json string type: The type of the result, currently page is the only type. + :>json string project: The project slug + :>json string version: The version slug + :>json string title: The title of the page + :>json string link: An absolute URL to the resulting page + :>json object highlights: An object containing a list of substrings with matching terms. + Note that the text is HTML escaped with the matching terms inside a tag. + :>json object blocks: + + A list of block objects containing search results from the page. + Currently, there are two types of blocks: + + - section: A page section with a linkable anchor (``id`` attribute). + - domain: A Sphinx :doc:`domain ` + with a linkable anchor (``id`` attribute). + + + **Example request**: + + .. tabs:: + + .. code-tab:: bash + + $ curl "https://docs.readthedocs.io/_/api/v2/search/?project=docs&version=latest&q=server%20side%20search" + + .. code-tab:: python + + import requests + URL = 'https://docs.readthedocs.io/_/api/v2/search/' + params = { + 'q': 'server side search', + 'project': 'docs', + 'version': 'latest', + } + response = requests.get(URL, params=params) + print(response.json()) + + **Example response**: + + .. sourcecode:: json + + { + "count": 41, + "next": "https://docs.readthedocs.io/api/v2/search/?page=2&project=read-the-docs&q=server+side+search&version=latest", + "previous": null, + "results": [ + { + "type": "page", + "project": "docs", + "version": "latest", + "title": "Server Side Search", + "link": "https://docs.readthedocs.io/en/latest/server-side-search.html", + "highlights": { + "title": [ + "Server Side Search" + ] + }, + "blocks": [ + { + "type": "section", + "id": "server-side-search", + "title": "Server Side Search", + "content": "Read the Docs provides full-text search across all of the pages of all projects, this is powered by Elasticsearch.", + "highlights": { + "title": [ + "Server Side Search" + ], + "content": [ + "You can search all projects at https://readthedocs.org/search/" + ] + } + }, + { + "type": "domain", + "role": "http:get", + "name": "/_/api/v2/search/", + "id": "get--_-api-v2-search-", + "content": "Retrieve search results for docs", + "highlights": { + "name": [""], + "content": ["Retrieve search results for docs"] + } + } + ] + }, + ] + } + +Authentication and authorization +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +If you are using :ref:`private versions `, +users will only be allowed to search projects they have permissions over. +Authentication and authorization is done using the current session, +or any of the valid :doc:`sharing methods `. diff --git a/readthedocs/api/v2/proxied_urls.py b/readthedocs/api/v2/proxied_urls.py index 5ec02bb73fd..d5a027ba543 100644 --- a/readthedocs/api/v2/proxied_urls.py +++ b/readthedocs/api/v2/proxied_urls.py @@ -14,6 +14,7 @@ api_footer_urls = [ url(r'footer_html/', ProxiedFooterHTML.as_view(), name='footer_html'), url(r'docsearch/$', ProxiedPageSearchAPIView.as_view(), name='doc_search'), + url(r'search/$', ProxiedPageSearchAPIView.as_view(new_api=True), name='search_api'), ] urlpatterns = api_footer_urls diff --git a/readthedocs/core/static-src/core/js/doc-embed/search.js b/readthedocs/core/static-src/core/js/doc-embed/search.js index 2d8c6e0b62a..18f9808d207 100644 --- a/readthedocs/core/static-src/core/js/doc-embed/search.js +++ b/readthedocs/core/static-src/core/js/doc-embed/search.js @@ -49,31 +49,27 @@ function attach_elastic_search_query_sphinx(data) { var search_def = $.Deferred(); var search_url = document.createElement('a'); - search_url.href = data.proxied_api_host + '/api/v2/docsearch/'; + search_url.href = data.proxied_api_host + '/api/v2/search/'; search_url.search = '?q=' + $.urlencode(query) + '&project=' + project + '&version=' + version + '&language=' + language; search_def .then(function (data) { - var hit_list = data.results || []; + var results = data.results || []; - if (hit_list.length) { - for (var i = 0; i < hit_list.length; i += 1) { - var doc = hit_list[i]; - var highlight = doc.highlight; - var inner_hits = doc.inner_hits || []; + if (results.length) { + for (var i = 0; i < results.length; i += 1) { + var result = results[i]; + var blocks = result.blocks; var list_item = $('
  • '); - var title = doc.title; - // if highlighted title is present, - // use that. - if (highlight) { - if (highlight.title) { - title = xss(highlight.title[0]); - } + var title = result.title; + // if highlighted title is present, use that. + if (result.highlights.title.length) { + title = xss(result.highlights.title[0]); } - var link = doc.link + "?highlight=" + $.urlencode(query); + var link = result.link + "?highlight=" + $.urlencode(query); var item = $('', {'href': link}); @@ -81,32 +77,19 @@ function attach_elastic_search_query_sphinx(data) { item.find('span').addClass('highlighted'); list_item.append(item); - // If the document is from subproject, add extra information - if (doc.project !== project) { - var text = " (from project " + doc.project + ")"; + // If the document is from a subproject, add extra information + if (result.project !== project) { + var text = " (from project " + result.project + ")"; var extra = $('', {'text': text}); list_item.append(extra); } - for (var j = 0; j < inner_hits.length; j += 1) { + for (var block_index = 0; block_index < blocks.length; block_index += 1) { + var current_block = blocks[block_index]; var contents = $('
    '); - var section = ""; - var section_subtitle = ""; - var section_subtitle_link = ""; - var section_content = ""; - var content = ""; - - var domain = ""; - var domain_role_name = ""; - var domain_subtitle_link = ""; - var domain_name = ""; - var domain_subtitle = ""; - var domain_content = ""; - var domain_docstrings = ""; - - var section_template = '' + + var section_template = '' + '<% } %>'; - var domain_template = '' + + var domain_template = ''; // if the result is page section - if(inner_hits[j].type === "sections") { - - section = inner_hits[j]; - section_subtitle = section._source.title; - section_subtitle_link = link + "#" + section._source.id; - section_content = [section._source.content.substr(0, MAX_SUBSTRING_LIMIT) + " ..."]; - - if (section.highlight) { - if (section.highlight["sections.title"]) { - section_subtitle = xss(section.highlight["sections.title"][0]); - } + if (current_block.type === "section") { + var section = current_block; + var section_subtitle = section.title; + var section_subtitle_link = link + "#" + section.id; + var section_content = [section.content.substr(0, MAX_SUBSTRING_LIMIT) + " ..."]; + + if (section.highlights.title.length) { + section_subtitle = xss(section.highlights.title[0]); + } - if (section.highlight["sections.content"]) { - content = section.highlight["sections.content"]; - section_content = []; - for ( - var k = 0; - k < content.length && k < MAX_RESULT_PER_SECTION; - k += 1 - ) { - section_content.push("... " + xss(content[k]) + " ..."); - } + if (section.highlights.content.length) { + var content = section.highlights.content; + section_content = []; + for ( + var k = 0; + k < content.length && k < MAX_RESULT_PER_SECTION; + k += 1 + ) { + section_content.push("... " + xss(content[k]) + " ..."); } } @@ -166,32 +146,26 @@ function attach_elastic_search_query_sphinx(data) { } // if the result is a sphinx domain object - if (inner_hits[j].type === "domains") { - - domain = inner_hits[j]; - domain_role_name = domain._source.role_name; - domain_subtitle_link = link + "#" + domain._source.anchor; - domain_name = domain._source.name; - domain_subtitle = ""; - domain_content = ""; - domain_docstrings = ""; - - if (domain._source.docstrings !== "") { - domain_docstrings = domain._source.docstrings.substr(0, MAX_SUBSTRING_LIMIT) + " ..."; + if (current_block.type === "domain") { + var domain = current_block; + var domain_role_name = domain.role; + var domain_subtitle_link = link + "#" + domain.id; + var domain_name = domain.name; + var domain_content = ""; + + if (domain.content !== "") { + domain_content = domain.content.substr(0, MAX_SUBSTRING_LIMIT) + " ..."; } - if (domain.highlight) { - if (domain.highlight["domains.docstrings"]) { - domain_docstrings = "... " + xss(domain.highlight["domains.docstrings"][0]) + " ..."; - } + if (domain.highlights.content.length) { + domain_content = "... " + xss(domain.highlights.content[0]) + " ..."; + } - if (domain.highlight["domains.name"]) { - domain_name = xss(domain.highlight["domains.name"][0]); - } + if (domain.highlights.name.length) { + domain_name = xss(domain.highlights.name[0]); } - domain_subtitle = "[" + domain_role_name + "]: " + domain_name; - domain_content = domain_docstrings; + var domain_subtitle = "[" + domain_role_name + "]: " + domain_name; append_html_to_contents( contents, @@ -209,7 +183,7 @@ function attach_elastic_search_query_sphinx(data) { // Create some spacing between the results. // Also, don't add this spacing in the last hit. - if (j !== inner_hits.length - 1) { + if (block_index < blocks.length - 1) { list_item.append($("
    ")); } } @@ -217,21 +191,16 @@ function attach_elastic_search_query_sphinx(data) { Search.output.append(list_item); list_item.slideDown(5); } - } - - if (!hit_list.length) { - // Fallback to Sphinx's indexes - Search.query_fallback(query); - console.log('Read the Docs search failed. Falling back to Sphinx search.'); - } - else { Search.status.text( - _('Search finished, found %s page(s) matching the search query.').replace('%s', hit_list.length) + _('Search finished, found %s page(s) matching the search query.').replace('%s', results.length) ); + } else { + console.log('Read the Docs search failed. Falling back to Sphinx search.'); + Search.query_fallback(query); } }) .fail(function (error) { - // Fallback to Sphinx's indexes + console.debug('Read the Docs search failed. Falling back to Sphinx search.'); Search.query_fallback(query); }) .always(function () { @@ -304,65 +273,64 @@ function attach_elastic_search_query_mkdocs(data) { var search_def = $.Deferred(); var search_url = document.createElement('a'); - search_url.href = data.proxied_api_host + '/api/v2/docsearch/'; + search_url.href = data.proxied_api_host + '/api/v2/search/'; search_url.search = '?q=' + encodeURIComponent(query) + '&project=' + project + '&version=' + version + '&language=' + language; search_def .then(function (data) { - var hit_list = data.results || []; + var results = data.results || []; - if (hit_list.length) { + if (results.length) { var searchResults = $('#mkdocs-search-results'); searchResults.empty(); - for (var i = 0; i < hit_list.length; i += 1) { - var doc = hit_list[i]; - var inner_hits = doc.inner_hits || []; + for (var i = 0; i < results.length; i += 1) { + var result = results[i]; + var blocks = result.blocks; - var result = $('
    '); - result.append( - $('

    ').append($('', {'href': doc.link, 'text': doc.title})) + var item = $('
    '); + item.append( + $('

    ').append($('', {'href': result.link, 'text': result.title})) ); - if (doc.project !== project) { - var text = '(from project ' + doc.project + ')'; - result.append($('', {'text': text})); + if (result.project !== project) { + var text = '(from project ' + result.project + ')'; + item.append($('', {'text': text})); } - for (var j = 0; j < inner_hits.length; j += 1) { - var section = inner_hits[j]; + for (var j = 0; j < blocks.length; j += 1) { + var section = blocks[j]; - if (section.type === 'sections') { - var section_link = doc.link + '#' + section._source.id; - var section_title = section._source.title; - var section_content = section._source.content; + if (section.type === 'section') { + var section_link = result.link + '#' + section.id; + var section_title = section.title; + var section_content = section.content; if (section_content.length > MAX_SUBSTRING_LIMIT) { section_content = section_content.substr(0, MAX_SUBSTRING_LIMIT) + " ..."; } var section_contents = [section_content]; - if (section.highlight) { - if (section.highlight["sections.title"]) { - section_title = section.highlight["sections.title"][0]; - } - if (section.highlight["sections.content"]) { - var contents = section.highlight["sections.content"]; - section_contents = []; - for ( - var k = 0; - k < contents.length && k < MAX_RESULT_PER_SECTION; - k += 1 - ) { - section_contents.push("... " + contents[k] + " ..."); - } + if (section.highlights.title.length) { + section_title = section.highlights.title[0]; + } + + if (section.highlights.content.length) { + var contents = section.highlights.content; + section_contents = []; + for ( + var k = 0; + k < contents.length && k < MAX_RESULT_PER_SECTION; + k += 1 + ) { + section_contents.push("... " + contents[k] + " ..."); } } section_title = xss(section_title) .replace(//g, '') .replace(/<\/span>/g, ''); - result.append( + item.append( $('

    ') .append($('', {'href': section_link}).html(section_title)) ); @@ -371,11 +339,11 @@ function attach_elastic_search_query_mkdocs(data) { content = content .replace(//g, '') .replace(/<\/span>/g, ''); - result.append( + item.append( $('

    ').html(content) ); } - searchResults.append(result); + searchResults.append(item); } } } diff --git a/readthedocs/core/static/core/js/readthedocs-doc-embed.js b/readthedocs/core/static/core/js/readthedocs-doc-embed.js index 9fda423f0f3..644ff4a06b7 100644 --- a/readthedocs/core/static/core/js/readthedocs-doc-embed.js +++ b/readthedocs/core/static/core/js/readthedocs-doc-embed.js @@ -1 +1 @@ -!function o(s,a,l){function c(t,e){if(!a[t]){if(!s[t]){var i="function"==typeof require&&require;if(!e&&i)return i(t,!0);if(d)return d(t,!0);var n=new Error("Cannot find module '"+t+"'");throw n.code="MODULE_NOT_FOUND",n}var r=a[t]={exports:{}};s[t][0].call(r.exports,function(e){return c(s[t][1][e]||e)},r,r.exports,o,s,a,l)}return a[t].exports}for(var d="function"==typeof require&&require,e=0;e

    "),i("table.docutils.footnote").wrap("
    "),i("table.docutils.citation").wrap("
    "),i(".wy-menu-vertical ul").not(".simple").siblings("a").each(function(){var t=i(this);expand=i(''),expand.on("click",function(e){return n.toggleCurrent(t),e.stopPropagation(),!1}),t.prepend(expand)})},reset:function(){var e=encodeURI(window.location.hash)||"#";try{var t=$(".wy-menu-vertical"),i=t.find('[href="'+e+'"]');if(0===i.length){var n=$('.document [id="'+e.substring(1)+'"]').closest("div.section");0===(i=t.find('[href="#'+n.attr("id")+'"]')).length&&(i=t.find('[href="#"]'))}0this.docHeight||(this.navBar.scrollTop(i),this.winPosition=e)},onResize:function(){this.winResize=!1,this.winHeight=this.win.height(),this.docHeight=$(document).height()},hashChange:function(){this.linkScroll=!0,this.win.one("hashchange",function(){this.linkScroll=!1})},toggleCurrent:function(e){var t=e.closest("li");t.siblings("li.current").removeClass("current"),t.siblings().find("li.current").removeClass("current"),t.find("> ul li.current").removeClass("current"),t.toggleClass("current")}},"undefined"!=typeof window&&(window.SphinxRtdTheme={Navigation:t.exports.ThemeNav,StickyNav:t.exports.ThemeNav}),function(){for(var o=0,e=["ms","moz","webkit","o"],t=0;t/g,u=/"/g,h=/"/g,p=/&#([a-zA-Z0-9]*);?/gim,f=/:?/gim,g=/&newline;?/gim,m=/((j\s*a\s*v\s*a|v\s*b|l\s*i\s*v\s*e)\s*s\s*c\s*r\s*i\s*p\s*t\s*|m\s*o\s*c\s*h\s*a)\:/gi,v=/e\s*x\s*p\s*r\s*e\s*s\s*s\s*i\s*o\s*n\s*\(.*/gi,w=/u\s*r\s*l\s*\(.*/gi;function b(e){return e.replace(u,""")}function _(e){return e.replace(h,'"')}function y(e){return e.replace(p,function(e,t){return"x"===t[0]||"X"===t[0]?String.fromCharCode(parseInt(t.substr(1),16)):String.fromCharCode(parseInt(t,10))})}function k(e){return e.replace(f,":").replace(g," ")}function x(e){for(var t="",i=0,n=e.length;i/g;i.whiteList={a:["target","href","title"],abbr:["title"],address:[],area:["shape","coords","href","alt"],article:[],aside:[],audio:["autoplay","controls","loop","preload","src"],b:[],bdi:["dir"],bdo:["dir"],big:[],blockquote:["cite"],br:[],caption:[],center:[],cite:[],code:[],col:["align","valign","span","width"],colgroup:["align","valign","span","width"],dd:[],del:["datetime"],details:["open"],div:[],dl:[],dt:[],em:[],font:["color","size","face"],footer:[],h1:[],h2:[],h3:[],h4:[],h5:[],h6:[],header:[],hr:[],i:[],img:["src","alt","title","width","height"],ins:["datetime"],li:[],mark:[],nav:[],ol:[],p:[],pre:[],s:[],section:[],small:[],span:[],sub:[],sup:[],strong:[],table:["width","border","align","valign"],tbody:["align","valign"],td:["width","rowspan","colspan","align","valign"],tfoot:["align","valign"],th:["width","rowspan","colspan","align","valign"],thead:["align","valign"],tr:["rowspan","align","valign"],tt:[],u:[],ul:[],video:["autoplay","controls","loop","preload","src","height","width"]},i.getDefaultWhiteList=o,i.onTag=function(e,t,i){},i.onIgnoreTag=function(e,t,i){},i.onTagAttr=function(e,t,i){},i.onIgnoreTagAttr=function(e,t,i){},i.safeAttrValue=function(e,t,i,n){if(i=T(i),"href"===t||"src"===t){if("#"===(i=d.trim(i)))return"#";if("http://"!==i.substr(0,7)&&"https://"!==i.substr(0,8)&&"mailto:"!==i.substr(0,7)&&"tel:"!==i.substr(0,4)&&"#"!==i[0]&&"/"!==i[0])return""}else if("background"===t){if(m.lastIndex=0,m.test(i))return""}else if("style"===t){if(v.lastIndex=0,v.test(i))return"";if(w.lastIndex=0,w.test(i)&&(m.lastIndex=0,m.test(i)))return"";!1!==n&&(i=(n=n||s).process(i))}return i=E(i)},i.escapeHtml=a,i.escapeQuote=b,i.unescapeQuote=_,i.escapeHtmlEntities=y,i.escapeDangerHtml5Entities=k,i.clearNonPrintableCharacter=x,i.friendlyAttrValue=T,i.escapeAttrValue=E,i.onIgnoreTagStripAll=function(){return""},i.StripTagBody=function(o,s){"function"!=typeof s&&(s=function(){});var a=!Array.isArray(o),l=[],c=!1;return{onIgnoreTag:function(e,t,i){if(function(e){return a||-1!==d.indexOf(o,e)}(e)){if(i.isClosing){var n="[/removed]",r=i.position+n.length;return l.push([!1!==c?c:i.position,r]),c=!1,n}return c=c||i.position,"[removed]"}return s(e,t,i)},remove:function(t){var i="",n=0;return d.forEach(l,function(e){i+=t.slice(n,e[0]),n=e[1]}),i+=t.slice(n)}}},i.stripCommentTag=function(e){return e.replace(S,"")},i.stripBlankChar=function(e){var t=e.split("");return(t=t.filter(function(e){var t=e.charCodeAt(0);return 127!==t&&(!(t<=31)||(10===t||13===t))})).join("")},i.cssFilter=s,i.getDefaultCSSWhiteList=r},{"./util":5,cssfilter:10}],3:[function(e,t,i){var n=e("./default"),r=e("./parser"),o=e("./xss");for(var s in(i=t.exports=function(e,t){return new o(t).process(e)}).FilterXSS=o,n)i[s]=n[s];for(var s in r)i[s]=r[s];"undefined"!=typeof window&&(window.filterXSS=t.exports)},{"./default":2,"./parser":4,"./xss":6}],4:[function(e,t,i){var d=e("./util");function h(e){var t=d.spaceIndex(e);if(-1===t)var i=e.slice(1,-1);else i=e.slice(1,t+1);return"/"===(i=d.trim(i).toLowerCase()).slice(0,1)&&(i=i.slice(1)),"/"===i.slice(-1)&&(i=i.slice(0,-1)),i}var u=/[^a-zA-Z0-9_:\.\-]/gim;function p(e,t){for(;t"===u){n+=i(e.slice(r,o)),d=h(c=e.slice(o,a+1)),n+=t(o,n.length,d,c,"";var a=function(e){var t=b.spaceIndex(e);if(-1===t)return{html:"",closing:"/"===e[e.length-2]};var i="/"===(e=b.trim(e.slice(t+1,-1)))[e.length-1];return i&&(e=b.trim(e.slice(0,-1))),{html:e,closing:i}}(i),l=d[r],c=w(a.html,function(e,t){var i,n=-1!==b.indexOf(l,e);return _(i=p(r,e,t,n))?n?(t=g(r,e,t,v))?e+'="'+t+'"':e:_(i=f(r,e,t,n))?void 0:i:i});i="<"+r;return c&&(i+=" "+c),a.closing&&(i+=" /"),i+=">"}return _(o=h(r,i,s))?m(i):o},m);return i&&(n=i.remove(n)),n},t.exports=a},{"./default":2,"./parser":4,"./util":5,cssfilter:10}],7:[function(e,t,i){var n,r;n=this,r=function(){var T=!0;function s(i){function e(e){var t=i.match(e);return t&&1t[1][i])return 1;if(t[0][i]!==t[1][i])return-1;if(0===i)return 0}}function o(e,t,i){var n=a;"string"==typeof t&&(i=t,t=void 0),void 0===t&&(t=!1),i&&(n=s(i));var r=""+n.version;for(var o in e)if(e.hasOwnProperty(o)&&n[o]){if("string"!=typeof e[o])throw new Error("Browser version in the minVersion map should be a string: "+o+": "+String(e));return E([r,e[o]])<0}return t}return a.test=function(e){for(var t=0;t");if(s.append($("

    ").append($("",{href:r.link,text:r.title}))),r.project!==b){var a="(from project "+r.project+")";s.append($("",{text:a}))}for(var l=0;lC&&(h=h.substr(0,C)+" ...");var p=[h];if(c.highlight&&(c.highlight["sections.title"]&&(u=c.highlight["sections.title"][0]),c.highlight["sections.content"])){var f=c.highlight["sections.content"];p=[];for(var g=0;g/g,"").replace(/<\/span>/g,""),s.append($("

    ").append($("",{href:d}).html(u)));for(var m=0;m/g,"").replace(/<\/span>/g,""),s.append($("

    ").html(v))}i.append(s)}}}}else console.log("Read the Docs search returned 0 result. Falling back to MkDocs search."),w()}).fail(function(e){console.log("Read the Docs search failed. Falling back to MkDocs search."),w()}),$.ajax({url:t.href,crossDomain:!0,xhrFields:{withCredentials:!0},complete:function(e,t){return"success"!==t||void 0===e.responseJSON||0===e.responseJSON.count?n.reject():n.resolve(e.responseJSON)}}).fail(function(e,t,i){return n.reject()})}function e(){var e=document.getElementById("mkdocs-search-query");e&&e.addEventListener("keyup",n);var t=window.getSearchTermFromLocation();t&&(e.value=t,n())}var b=i.project,r=i.version,o=i.language||"en";$(document).ready(function(){window.doSearchFallback=window.doSearch,window.doSearch=n,(window.initSearch=e)()})}t.exports={init:function(){var e=n.get();e.is_sphinx_builder()?function(t){var A=t.project,i=t.version,r=t.language||"en";if("undefined"!=typeof Search&&A&&i)if(t.features&&t.features.docsearch_disabled)console.log("Server side search is disabled.");else{var e=Search.query;Search.query_fallback=e,Search.query=function(S){var n=$.Deferred(),e=document.createElement("a");e.href=t.proxied_api_host+"/api/v2/docsearch/",e.search="?q="+$.urlencode(S)+"&project="+A+"&version="+i+"&language="+r,n.then(function(e){var t=e.results||[];if(t.length)for(var i=0;i'),a=n.title;r&&r.title&&(a=O(r.title[0]));var l=n.link+"?highlight="+$.urlencode(S),c=$("",{href:l});if(c.html(a),c.find("span").addClass("highlighted"),s.append(c),n.project!==A){var d=" (from project "+n.project+")",u=$("",{text:d});s.append(u)}for(var h=0;h'),f="",g="",m="",v="",w="",b="",y="",k="",x="",T="";if("sections"===o[h].type){if(g=(f=o[h])._source.title,m=l+"#"+f._source.id,v=[f._source.content.substr(0,C)+" ..."],f.highlight&&(f.highlight["sections.title"]&&(g=O(f.highlight["sections.title"][0])),f.highlight["sections.content"])){w=f.highlight["sections.content"],v=[];for(var E=0;E<%= section_subtitle %><% for (var i = 0; i < section_content.length; ++i) { %>

    <%= section_content[i] %>
    <% } %>',{section_subtitle_link:m,section_subtitle:g,section_content:v})}"domains"===o[h].type&&(y=(b=o[h])._source.role_name,k=l+"#"+b._source.anchor,x=b._source.name,(T="")!==b._source.docstrings&&(T=b._source.docstrings.substr(0,C)+" ..."),b.highlight&&(b.highlight["domains.docstrings"]&&(T="... "+O(b.highlight["domains.docstrings"][0])+" ..."),b.highlight["domains.name"]&&(x=O(b.highlight["domains.name"][0]))),M(p,'
    <%= domain_content %>
    ',{domain_subtitle_link:k,domain_subtitle:"["+y+"]: "+x,domain_content:T})),p.find("span").addClass("highlighted"),s.append(p),h!==o.length-1&&s.append($("
    "))}Search.output.append(s),s.slideDown(5)}t.length?Search.status.text(_("Search finished, found %s page(s) matching the search query.").replace("%s",t.length)):(Search.query_fallback(S),console.log("Read the Docs search failed. Falling back to Sphinx search."))}).fail(function(e){Search.query_fallback(S)}).always(function(){$("#search-progress").empty(),Search.stopPulse(),Search.title.text(_("Search Results")),Search.status.fadeIn(500)}),$.ajax({url:e.href,crossDomain:!0,xhrFields:{withCredentials:!0},complete:function(e,t){return"success"!==t||void 0===e.responseJSON||0===e.responseJSON.count?n.reject():n.resolve(e.responseJSON)}}).fail(function(e,t,i){return n.reject()})}}$(document).ready(function(){"undefined"!=typeof Search&&Search.init()})}(e):e.features&&!e.features.docsearch_disabled?r(e):console.log("Server side search is disabled.")}}},{"./../../../../../../bower_components/xss/lib/index":3,"./rtd-data":16}],18:[function(r,e,t){var o=r("./rtd-data");e.exports={init:function(){var e=o.get();if($(document).on("click","[data-toggle='rst-current-version']",function(){var e=$("[data-toggle='rst-versions']").hasClass("shift-up")?"was_open":"was_closed";"undefined"!=typeof ga?ga("rtfd.send","event","Flyout","Click",e):"undefined"!=typeof _gaq&&_gaq.push(["rtfd._setAccount","UA-17997319-1"],["rtfd._trackEvent","Flyout","Click",e])}),void 0===window.SphinxRtdTheme){var t=r("./../../../../../../bower_components/sphinx-rtd-theme/js/theme.js").ThemeNav;if($(document).ready(function(){setTimeout(function(){t.navBar||t.enable()},1e3)}),e.is_rtd_like_theme())if(!$("div.wy-side-scroll:first").length){console.log("Applying theme sidebar fix...");var i=$("nav.wy-nav-side:first"),n=$("
    ").addClass("wy-side-scroll");i.children().detach().appendTo(n),n.prependTo(i),t.navBar=n}}}}},{"./../../../../../../bower_components/sphinx-rtd-theme/js/theme.js":1,"./rtd-data":16}],19:[function(e,t,i){var l,c=e("./constants"),d=e("./rtd-data"),n=e("bowser"),u="#ethical-ad-placement";function h(){var e,t,i="rtd-"+(Math.random()+1).toString(36).substring(4),n=c.PROMO_TYPES.LEFTNAV,r=c.DEFAULT_PROMO_PRIORITY,o=null;return l.is_mkdocs_builder()&&l.is_rtd_like_theme()?(o="nav.wy-nav-side",e="ethical-rtd ethical-dark-theme"):l.is_rtd_like_theme()?(o="nav.wy-nav-side > div.wy-side-scroll",e="ethical-rtd ethical-dark-theme"):l.is_alabaster_like_theme()&&(o="div.sphinxsidebar > div.sphinxsidebarwrapper",e="ethical-alabaster"),o?($("
    ").attr("id",i).addClass(e).appendTo(o),(!(t=$("#"+i).offset())||t.top>$(window).height())&&(r=c.LOW_PROMO_PRIORITY),{div_id:i,display_type:n,priority:r}):null}function p(){var e,t,i="rtd-"+(Math.random()+1).toString(36).substring(4),n=c.PROMO_TYPES.FOOTER,r=c.DEFAULT_PROMO_PRIORITY,o=null;return l.is_rtd_like_theme()?(o=$("
    ").insertAfter("footer hr"),e="ethical-rtd"):l.is_alabaster_like_theme()&&(o="div.bodywrapper .body",e="ethical-alabaster"),o?($("
    ").attr("id",i).addClass(e).appendTo(o),(!(t=$("#"+i).offset())||t.top<$(window).height())&&(r=c.LOW_PROMO_PRIORITY),{div_id:i,display_type:n,priority:r}):null}function f(){var e="rtd-"+(Math.random()+1).toString(36).substring(4),t=c.PROMO_TYPES.FIXED_FOOTER,i=c.DEFAULT_PROMO_PRIORITY;return n&&n.mobile&&(i=c.MAXIMUM_PROMO_PRIORITY),$("
    ").attr("id",e).appendTo("body"),{div_id:e,display_type:t,priority:i}}function g(e){this.id=e.id,this.div_id=e.div_id||"",this.html=e.html||"",this.display_type=e.display_type||"",this.view_tracking_url=e.view_url,this.click_handler=function(){"undefined"!=typeof ga?ga("rtfd.send","event","Promo","Click",e.id):"undefined"!=typeof _gaq&&_gaq.push(["rtfd._setAccount","UA-17997319-1"],["rtfd._trackEvent","Promo","Click",e.id])}}g.prototype.display=function(){var e="#"+this.div_id,t=this.view_tracking_url;$(e).html(this.html),$(e).find('a[href*="/sustainability/click/"]').on("click",this.click_handler);function i(){$.inViewport($(e),-3)&&($("").attr("src",t).css("display","none").appendTo(e),$(window).off(".rtdinview"),$(".wy-side-scroll").off(".rtdinview"))}$(window).on("DOMContentLoaded.rtdinview load.rtdinview scroll.rtdinview resize.rtdinview",i),$(".wy-side-scroll").on("scroll.rtdinview",i),$(".ethical-close").on("click",function(){return $(e).hide(),!1}),this.post_promo_display()},g.prototype.disable=function(){$("#"+this.div_id).hide()},g.prototype.post_promo_display=function(){this.display_type===c.PROMO_TYPES.FOOTER&&($("
    ").insertAfter("#"+this.div_id),$("
    ").insertBefore("#"+this.div_id+".ethical-alabaster .ethical-footer"))},t.exports={Promo:g,init:function(){var e,t,i={format:"jsonp"},n=[],r=[],o=[],s=[p,h,f];if(l=d.get(),t=function(){var e,t="rtd-"+(Math.random()+1).toString(36).substring(4),i=c.PROMO_TYPES.LEFTNAV;return e=l.is_rtd_like_theme()?"ethical-rtd ethical-dark-theme":"ethical-alabaster",0<$(u).length?($("
    ").attr("id",t).addClass(e).appendTo(u),{div_id:t,display_type:i}):null}())n.push(t.div_id),r.push(t.display_type),o.push(t.priority||c.DEFAULT_PROMO_PRIORITY),!0;else{if(!l.show_promo())return;for(var a=0;a").attr("id","rtd-detection").attr("class","ethical-rtd").html(" ").appendTo("body"),0===$("#rtd-detection").height()&&(e=!0),$("#rtd-detection").remove(),e}()&&(console.log("---------------------------------------------------------------------------------------"),console.log("Read the Docs hosts documentation for tens of thousands of open source projects."),console.log("We fund our development (we are open source) and operations through advertising."),console.log("We promise to:"),console.log(" - never let advertisers run 3rd party JavaScript"),console.log(" - never sell user data to advertisers or other 3rd parties"),console.log(" - only show advertisements of interest to developers"),console.log("Read more about our approach to advertising here: https://docs.readthedocs.io/en/latest/advertising/ethical-advertising.html"),console.log("%cPlease allow our Ethical Ads or go ad-free:","font-size: 2em"),console.log("https://docs.readthedocs.io/en/latest/advertising/ad-blocking.html"),console.log("--------------------------------------------------------------------------------------"),function(){var e=h(),t=null;e&&e.div_id&&(t=$("#"+e.div_id).attr("class","keep-us-sustainable"),$("

    ").text("Support Read the Docs!").appendTo(t),$("

    ").html('Please help keep us sustainable by allowing our Ethical Ads in your ad blocker or go ad-free by subscribing.').appendTo(t),$("

    ").text("Thank you! ❤️").appendTo(t))}())}})}}},{"./constants":14,"./rtd-data":16,bowser:7}],20:[function(e,t,i){var o=e("./rtd-data");t.exports={init:function(e){var t=o.get();if(!e.is_highest){var i=window.location.pathname.replace(t.version,e.slug),n=$('

    Note

    You are not reading the most recent version of this documentation. is the latest version available.

    ');n.find("a").attr("href",i).text(e.slug);var r=$("div.body");r.length||(r=$("div.document")),r.prepend(n)}}}},{"./rtd-data":16}],21:[function(e,t,i){var n=e("./doc-embed/sponsorship"),r=e("./doc-embed/footer.js"),o=(e("./doc-embed/rtd-data"),e("./doc-embed/sphinx")),s=e("./doc-embed/search");$.extend(e("verge")),$(document).ready(function(){r.init(),o.init(),s.init(),n.init()})},{"./doc-embed/footer.js":15,"./doc-embed/rtd-data":16,"./doc-embed/search":17,"./doc-embed/sphinx":18,"./doc-embed/sponsorship":19,verge:13}]},{},[21]); \ No newline at end of file +!function o(a,s,l){function c(t,e){if(!s[t]){if(!a[t]){var i="function"==typeof require&&require;if(!e&&i)return i(t,!0);if(d)return d(t,!0);var n=new Error("Cannot find module '"+t+"'");throw n.code="MODULE_NOT_FOUND",n}var r=s[t]={exports:{}};a[t][0].call(r.exports,function(e){return c(a[t][1][e]||e)},r,r.exports,o,a,s,l)}return s[t].exports}for(var d="function"==typeof require&&require,e=0;e
    "),i("table.docutils.footnote").wrap("
    "),i("table.docutils.citation").wrap("
    "),i(".wy-menu-vertical ul").not(".simple").siblings("a").each(function(){var t=i(this);expand=i(''),expand.on("click",function(e){return n.toggleCurrent(t),e.stopPropagation(),!1}),t.prepend(expand)})},reset:function(){var e=encodeURI(window.location.hash)||"#";try{var t=$(".wy-menu-vertical"),i=t.find('[href="'+e+'"]');if(0===i.length){var n=$('.document [id="'+e.substring(1)+'"]').closest("div.section");0===(i=t.find('[href="#'+n.attr("id")+'"]')).length&&(i=t.find('[href="#"]'))}0this.docHeight||(this.navBar.scrollTop(i),this.winPosition=e)},onResize:function(){this.winResize=!1,this.winHeight=this.win.height(),this.docHeight=$(document).height()},hashChange:function(){this.linkScroll=!0,this.win.one("hashchange",function(){this.linkScroll=!1})},toggleCurrent:function(e){var t=e.closest("li");t.siblings("li.current").removeClass("current"),t.siblings().find("li.current").removeClass("current"),t.find("> ul li.current").removeClass("current"),t.toggleClass("current")}},"undefined"!=typeof window&&(window.SphinxRtdTheme={Navigation:t.exports.ThemeNav,StickyNav:t.exports.ThemeNav}),function(){for(var o=0,e=["ms","moz","webkit","o"],t=0;t/g,u=/"/g,h=/"/g,p=/&#([a-zA-Z0-9]*);?/gim,f=/:?/gim,g=/&newline;?/gim,m=/((j\s*a\s*v\s*a|v\s*b|l\s*i\s*v\s*e)\s*s\s*c\s*r\s*i\s*p\s*t\s*|m\s*o\s*c\s*h\s*a)\:/gi,v=/e\s*x\s*p\s*r\s*e\s*s\s*s\s*i\s*o\s*n\s*\(.*/gi,w=/u\s*r\s*l\s*\(.*/gi;function b(e){return e.replace(u,""")}function y(e){return e.replace(h,'"')}function _(e){return e.replace(p,function(e,t){return"x"===t[0]||"X"===t[0]?String.fromCharCode(parseInt(t.substr(1),16)):String.fromCharCode(parseInt(t,10))})}function k(e){return e.replace(f,":").replace(g," ")}function x(e){for(var t="",i=0,n=e.length;i/g;i.whiteList={a:["target","href","title"],abbr:["title"],address:[],area:["shape","coords","href","alt"],article:[],aside:[],audio:["autoplay","controls","loop","preload","src"],b:[],bdi:["dir"],bdo:["dir"],big:[],blockquote:["cite"],br:[],caption:[],center:[],cite:[],code:[],col:["align","valign","span","width"],colgroup:["align","valign","span","width"],dd:[],del:["datetime"],details:["open"],div:[],dl:[],dt:[],em:[],font:["color","size","face"],footer:[],h1:[],h2:[],h3:[],h4:[],h5:[],h6:[],header:[],hr:[],i:[],img:["src","alt","title","width","height"],ins:["datetime"],li:[],mark:[],nav:[],ol:[],p:[],pre:[],s:[],section:[],small:[],span:[],sub:[],sup:[],strong:[],table:["width","border","align","valign"],tbody:["align","valign"],td:["width","rowspan","colspan","align","valign"],tfoot:["align","valign"],th:["width","rowspan","colspan","align","valign"],thead:["align","valign"],tr:["rowspan","align","valign"],tt:[],u:[],ul:[],video:["autoplay","controls","loop","preload","src","height","width"]},i.getDefaultWhiteList=o,i.onTag=function(e,t,i){},i.onIgnoreTag=function(e,t,i){},i.onTagAttr=function(e,t,i){},i.onIgnoreTagAttr=function(e,t,i){},i.safeAttrValue=function(e,t,i,n){if(i=T(i),"href"===t||"src"===t){if("#"===(i=d.trim(i)))return"#";if("http://"!==i.substr(0,7)&&"https://"!==i.substr(0,8)&&"mailto:"!==i.substr(0,7)&&"tel:"!==i.substr(0,4)&&"#"!==i[0]&&"/"!==i[0])return""}else if("background"===t){if(m.lastIndex=0,m.test(i))return""}else if("style"===t){if(v.lastIndex=0,v.test(i))return"";if(w.lastIndex=0,w.test(i)&&(m.lastIndex=0,m.test(i)))return"";!1!==n&&(i=(n=n||a).process(i))}return i=E(i)},i.escapeHtml=s,i.escapeQuote=b,i.unescapeQuote=y,i.escapeHtmlEntities=_,i.escapeDangerHtml5Entities=k,i.clearNonPrintableCharacter=x,i.friendlyAttrValue=T,i.escapeAttrValue=E,i.onIgnoreTagStripAll=function(){return""},i.StripTagBody=function(o,a){"function"!=typeof a&&(a=function(){});var s=!Array.isArray(o),l=[],c=!1;return{onIgnoreTag:function(e,t,i){if(function(e){return s||-1!==d.indexOf(o,e)}(e)){if(i.isClosing){var n="[/removed]",r=i.position+n.length;return l.push([!1!==c?c:i.position,r]),c=!1,n}return c=c||i.position,"[removed]"}return a(e,t,i)},remove:function(t){var i="",n=0;return d.forEach(l,function(e){i+=t.slice(n,e[0]),n=e[1]}),i+=t.slice(n)}}},i.stripCommentTag=function(e){return e.replace(S,"")},i.stripBlankChar=function(e){var t=e.split("");return(t=t.filter(function(e){var t=e.charCodeAt(0);return 127!==t&&(!(t<=31)||(10===t||13===t))})).join("")},i.cssFilter=a,i.getDefaultCSSWhiteList=r},{"./util":5,cssfilter:10}],3:[function(e,t,i){var n=e("./default"),r=e("./parser"),o=e("./xss");for(var a in(i=t.exports=function(e,t){return new o(t).process(e)}).FilterXSS=o,n)i[a]=n[a];for(var a in r)i[a]=r[a];"undefined"!=typeof window&&(window.filterXSS=t.exports)},{"./default":2,"./parser":4,"./xss":6}],4:[function(e,t,i){var d=e("./util");function h(e){var t=d.spaceIndex(e);if(-1===t)var i=e.slice(1,-1);else i=e.slice(1,t+1);return"/"===(i=d.trim(i).toLowerCase()).slice(0,1)&&(i=i.slice(1)),"/"===i.slice(-1)&&(i=i.slice(0,-1)),i}var u=/[^a-zA-Z0-9_:\.\-]/gim;function p(e,t){for(;t"===u){n+=i(e.slice(r,o)),d=h(c=e.slice(o,s+1)),n+=t(o,n.length,d,c,"";var s=function(e){var t=b.spaceIndex(e);if(-1===t)return{html:"",closing:"/"===e[e.length-2]};var i="/"===(e=b.trim(e.slice(t+1,-1)))[e.length-1];return i&&(e=b.trim(e.slice(0,-1))),{html:e,closing:i}}(i),l=d[r],c=w(s.html,function(e,t){var i,n=-1!==b.indexOf(l,e);return y(i=p(r,e,t,n))?n?(t=g(r,e,t,v))?e+'="'+t+'"':e:y(i=f(r,e,t,n))?void 0:i:i});i="<"+r;return c&&(i+=" "+c),s.closing&&(i+=" /"),i+=">"}return y(o=h(r,i,a))?m(i):o},m);return i&&(n=i.remove(n)),n},t.exports=s},{"./default":2,"./parser":4,"./util":5,cssfilter:10}],7:[function(e,t,i){var n,r;n=this,r=function(){var T=!0;function a(i){function e(e){var t=i.match(e);return t&&1t[1][i])return 1;if(t[0][i]!==t[1][i])return-1;if(0===i)return 0}}function o(e,t,i){var n=s;"string"==typeof t&&(i=t,t=void 0),void 0===t&&(t=!1),i&&(n=a(i));var r=""+n.version;for(var o in e)if(e.hasOwnProperty(o)&&n[o]){if("string"!=typeof e[o])throw new Error("Browser version in the minVersion map should be a string: "+o+": "+String(e));return E([r,e[o]])<0}return t}return s.test=function(e){for(var t=0;t");if(a.append($("

    ").append($("",{href:r.link,text:r.title}))),r.project!==b){var s="(from project "+r.project+")";a.append($("",{text:s}))}for(var l=0;lC&&(h=h.substr(0,C)+" ...");var p=[h];if(c.highlights.title.length&&(u=c.highlights.title[0]),c.highlights.content.length){var f=c.highlights.content;p=[];for(var g=0;g/g,"").replace(/<\/span>/g,""),a.append($("

    ").append($("",{href:d}).html(u)));for(var m=0;m/g,"").replace(/<\/span>/g,""),a.append($("

    ").html(v))}i.append(a)}}}}else console.log("Read the Docs search returned 0 result. Falling back to MkDocs search."),w()}).fail(function(e){console.log("Read the Docs search failed. Falling back to MkDocs search."),w()}),$.ajax({url:t.href,crossDomain:!0,xhrFields:{withCredentials:!0},complete:function(e,t){return"success"!==t||void 0===e.responseJSON||0===e.responseJSON.count?n.reject():n.resolve(e.responseJSON)}}).fail(function(e,t,i){return n.reject()})}function e(){var e=document.getElementById("mkdocs-search-query");e&&e.addEventListener("keyup",n);var t=window.getSearchTermFromLocation();t&&(e.value=t,n())}var b=i.project,r=i.version,o=i.language||"en";$(document).ready(function(){window.doSearchFallback=window.doSearch,window.doSearch=n,(window.initSearch=e)()})}t.exports={init:function(){var e=n.get();e.is_sphinx_builder()?function(t){var A=t.project,i=t.version,r=t.language||"en";if("undefined"!=typeof Search&&A&&i)if(t.features&&t.features.docsearch_disabled)console.log("Server side search is disabled.");else{var e=Search.query;Search.query_fallback=e,Search.query=function(S){var n=$.Deferred(),e=document.createElement("a");e.href=t.proxied_api_host+"/api/v2/search/",e.search="?q="+$.urlencode(S)+"&project="+A+"&version="+i+"&language="+r,n.then(function(e){var t=e.results||[];if(t.length){for(var i=0;i'),a=n.title;n.highlights.title.length&&(a=O(n.highlights.title[0]));var s=n.link+"?highlight="+$.urlencode(S),l=$("",{href:s});if(l.html(a),l.find("span").addClass("highlighted"),o.append(l),n.project!==A){var c=" (from project "+n.project+")",d=$("",{text:c});o.append(d)}for(var u=0;u');if("section"===h.type){var f=h,g=f.title,m=s+"#"+f.id,v=[f.content.substr(0,C)+" ..."];if(f.highlights.title.length&&(g=O(f.highlights.title[0])),f.highlights.content.length){var w=f.highlights.content;v=[];for(var b=0;b<%= section_subtitle %>

    <% for (var i = 0; i < section_content.length; ++i) { %>
    <%= section_content[i] %>
    <% } %>',{section_subtitle_link:m,section_subtitle:g,section_content:v})}if("domain"===h.type){var y=h,k=y.role,x=s+"#"+y.id,T=y.name,E="";""!==y.content&&(E=y.content.substr(0,C)+" ..."),y.highlights.content.length&&(E="... "+O(y.highlights.content[0])+" ..."),y.highlights.name.length&&(T=O(y.highlights.name[0])),M(p,'
    <%= domain_content %>
    ',{domain_subtitle_link:x,domain_subtitle:"["+k+"]: "+T,domain_content:E})}p.find("span").addClass("highlighted"),o.append(p),u
    "))}Search.output.append(o),o.slideDown(5)}Search.status.text(_("Search finished, found %s page(s) matching the search query.").replace("%s",t.length))}else console.log("Read the Docs search failed. Falling back to Sphinx search."),Search.query_fallback(S)}).fail(function(e){console.debug("Read the Docs search failed. Falling back to Sphinx search."),Search.query_fallback(S)}).always(function(){$("#search-progress").empty(),Search.stopPulse(),Search.title.text(_("Search Results")),Search.status.fadeIn(500)}),$.ajax({url:e.href,crossDomain:!0,xhrFields:{withCredentials:!0},complete:function(e,t){return"success"!==t||void 0===e.responseJSON||0===e.responseJSON.count?n.reject():n.resolve(e.responseJSON)}}).fail(function(e,t,i){return n.reject()})}}$(document).ready(function(){"undefined"!=typeof Search&&Search.init()})}(e):e.features&&!e.features.docsearch_disabled?r(e):console.log("Server side search is disabled.")}}},{"./../../../../../../bower_components/xss/lib/index":3,"./rtd-data":16}],18:[function(r,e,t){var o=r("./rtd-data");e.exports={init:function(){var e=o.get();if($(document).on("click","[data-toggle='rst-current-version']",function(){var e=$("[data-toggle='rst-versions']").hasClass("shift-up")?"was_open":"was_closed";"undefined"!=typeof ga?ga("rtfd.send","event","Flyout","Click",e):"undefined"!=typeof _gaq&&_gaq.push(["rtfd._setAccount","UA-17997319-1"],["rtfd._trackEvent","Flyout","Click",e])}),void 0===window.SphinxRtdTheme){var t=r("./../../../../../../bower_components/sphinx-rtd-theme/js/theme.js").ThemeNav;if($(document).ready(function(){setTimeout(function(){t.navBar||t.enable()},1e3)}),e.is_rtd_like_theme())if(!$("div.wy-side-scroll:first").length){console.log("Applying theme sidebar fix...");var i=$("nav.wy-nav-side:first"),n=$("
    ").addClass("wy-side-scroll");i.children().detach().appendTo(n),n.prependTo(i),t.navBar=n}}}}},{"./../../../../../../bower_components/sphinx-rtd-theme/js/theme.js":1,"./rtd-data":16}],19:[function(e,t,i){var l,c=e("./constants"),d=e("./rtd-data"),n=e("bowser"),u="#ethical-ad-placement";function h(){var e,t,i="rtd-"+(Math.random()+1).toString(36).substring(4),n=c.PROMO_TYPES.LEFTNAV,r=c.DEFAULT_PROMO_PRIORITY,o=null;return l.is_mkdocs_builder()&&l.is_rtd_like_theme()?(o="nav.wy-nav-side",e="ethical-rtd ethical-dark-theme"):l.is_rtd_like_theme()?(o="nav.wy-nav-side > div.wy-side-scroll",e="ethical-rtd ethical-dark-theme"):l.is_alabaster_like_theme()&&(o="div.sphinxsidebar > div.sphinxsidebarwrapper",e="ethical-alabaster"),o?($("
    ").attr("id",i).addClass(e).appendTo(o),(!(t=$("#"+i).offset())||t.top>$(window).height())&&(r=c.LOW_PROMO_PRIORITY),{div_id:i,display_type:n,priority:r}):null}function p(){var e,t,i="rtd-"+(Math.random()+1).toString(36).substring(4),n=c.PROMO_TYPES.FOOTER,r=c.DEFAULT_PROMO_PRIORITY,o=null;return l.is_rtd_like_theme()?(o=$("
    ").insertAfter("footer hr"),e="ethical-rtd"):l.is_alabaster_like_theme()&&(o="div.bodywrapper .body",e="ethical-alabaster"),o?($("
    ").attr("id",i).addClass(e).appendTo(o),(!(t=$("#"+i).offset())||t.top<$(window).height())&&(r=c.LOW_PROMO_PRIORITY),{div_id:i,display_type:n,priority:r}):null}function f(){var e="rtd-"+(Math.random()+1).toString(36).substring(4),t=c.PROMO_TYPES.FIXED_FOOTER,i=c.DEFAULT_PROMO_PRIORITY;return n&&n.mobile&&(i=c.MAXIMUM_PROMO_PRIORITY),$("
    ").attr("id",e).appendTo("body"),{div_id:e,display_type:t,priority:i}}function g(e){this.id=e.id,this.div_id=e.div_id||"",this.html=e.html||"",this.display_type=e.display_type||"",this.view_tracking_url=e.view_url,this.click_handler=function(){"undefined"!=typeof ga?ga("rtfd.send","event","Promo","Click",e.id):"undefined"!=typeof _gaq&&_gaq.push(["rtfd._setAccount","UA-17997319-1"],["rtfd._trackEvent","Promo","Click",e.id])}}g.prototype.display=function(){var e="#"+this.div_id,t=this.view_tracking_url;$(e).html(this.html),$(e).find('a[href*="/sustainability/click/"]').on("click",this.click_handler);function i(){$.inViewport($(e),-3)&&($("").attr("src",t).css("display","none").appendTo(e),$(window).off(".rtdinview"),$(".wy-side-scroll").off(".rtdinview"))}$(window).on("DOMContentLoaded.rtdinview load.rtdinview scroll.rtdinview resize.rtdinview",i),$(".wy-side-scroll").on("scroll.rtdinview",i),$(".ethical-close").on("click",function(){return $(e).hide(),!1}),this.post_promo_display()},g.prototype.disable=function(){$("#"+this.div_id).hide()},g.prototype.post_promo_display=function(){this.display_type===c.PROMO_TYPES.FOOTER&&($("
    ").insertAfter("#"+this.div_id),$("
    ").insertBefore("#"+this.div_id+".ethical-alabaster .ethical-footer"))},t.exports={Promo:g,init:function(){var e,t,i={format:"jsonp"},n=[],r=[],o=[],a=[p,h,f];if(l=d.get(),t=function(){var e,t="rtd-"+(Math.random()+1).toString(36).substring(4),i=c.PROMO_TYPES.LEFTNAV;return e=l.is_rtd_like_theme()?"ethical-rtd ethical-dark-theme":"ethical-alabaster",0<$(u).length?($("
    ").attr("id",t).addClass(e).appendTo(u),{div_id:t,display_type:i}):null}())n.push(t.div_id),r.push(t.display_type),o.push(t.priority||c.DEFAULT_PROMO_PRIORITY),!0;else{if(!l.show_promo())return;for(var s=0;s").attr("id","rtd-detection").attr("class","ethical-rtd").html(" ").appendTo("body"),0===$("#rtd-detection").height()&&(e=!0),$("#rtd-detection").remove(),e}()&&(console.log("---------------------------------------------------------------------------------------"),console.log("Read the Docs hosts documentation for tens of thousands of open source projects."),console.log("We fund our development (we are open source) and operations through advertising."),console.log("We promise to:"),console.log(" - never let advertisers run 3rd party JavaScript"),console.log(" - never sell user data to advertisers or other 3rd parties"),console.log(" - only show advertisements of interest to developers"),console.log("Read more about our approach to advertising here: https://docs.readthedocs.io/en/latest/advertising/ethical-advertising.html"),console.log("%cPlease allow our Ethical Ads or go ad-free:","font-size: 2em"),console.log("https://docs.readthedocs.io/en/latest/advertising/ad-blocking.html"),console.log("--------------------------------------------------------------------------------------"),function(){var e=h(),t=null;e&&e.div_id&&(t=$("#"+e.div_id).attr("class","keep-us-sustainable"),$("

    ").text("Support Read the Docs!").appendTo(t),$("

    ").html('Please help keep us sustainable by allowing our Ethical Ads in your ad blocker or go ad-free by subscribing.').appendTo(t),$("

    ").text("Thank you! ❤️").appendTo(t))}())}})}}},{"./constants":14,"./rtd-data":16,bowser:7}],20:[function(e,t,i){var o=e("./rtd-data");t.exports={init:function(e){var t=o.get();if(!e.is_highest){var i=window.location.pathname.replace(t.version,e.slug),n=$('

    Note

    You are not reading the most recent version of this documentation. is the latest version available.

    ');n.find("a").attr("href",i).text(e.slug);var r=$("div.body");r.length||(r=$("div.document")),r.prepend(n)}}}},{"./rtd-data":16}],21:[function(e,t,i){var n=e("./doc-embed/sponsorship"),r=e("./doc-embed/footer.js"),o=(e("./doc-embed/rtd-data"),e("./doc-embed/sphinx")),a=e("./doc-embed/search");$.extend(e("verge")),$(document).ready(function(){r.init(),o.init(),a.init(),n.init()})},{"./doc-embed/footer.js":15,"./doc-embed/rtd-data":16,"./doc-embed/search":17,"./doc-embed/sphinx":18,"./doc-embed/sponsorship":19,verge:13}]},{},[21]); \ No newline at end of file diff --git a/readthedocs/projects/urls/public.py b/readthedocs/projects/urls/public.py index d13e52f6d84..da4e4ee067b 100644 --- a/readthedocs/projects/urls/public.py +++ b/readthedocs/projects/urls/public.py @@ -7,8 +7,7 @@ from readthedocs.constants import pattern_opts from readthedocs.projects.views import public from readthedocs.projects.views.public import ProjectDetailView, ProjectTagIndex -from readthedocs.search import views as search_views - +from readthedocs.search.views import SearchView urlpatterns = [ url( @@ -65,7 +64,7 @@ ), url( r'^(?P{project_slug})/search/$'.format(**pattern_opts), - search_views.elastic_search, + SearchView.as_view(), name='elastic_project_search', ), url( diff --git a/readthedocs/search/api.py b/readthedocs/search/api.py index cd5c87fbce3..f5c73ef9466 100644 --- a/readthedocs/search/api.py +++ b/readthedocs/search/api.py @@ -21,6 +21,8 @@ from readthedocs.search import tasks, utils from readthedocs.search.faceted_search import PageSearch +from .serializers import PageSearchSerializer + log = logging.getLogger(__name__) @@ -42,7 +44,7 @@ def has_next(self): return self.number < self.paginator.num_pages def has_previous(self): - return self.number > 0 + return self.number > 1 def next_page_number(self): return self.number + 1 @@ -120,7 +122,7 @@ def paginate_queryset(self, queryset, request, view=None): return result -class PageSearchSerializer(serializers.Serializer): +class OldPageSearchSerializer(serializers.Serializer): """ Serializer for page search results. @@ -192,6 +194,12 @@ class PageSearchAPIView(GenericAPIView): - project - version + Optional params from the view: + + - new_api (true/false): Make use of the new stable API. + Defaults to false. Remove after a couple of days/weeks + and always use the new API. + .. note:: The methods `_get_project` and `_get_version` @@ -201,7 +209,7 @@ class PageSearchAPIView(GenericAPIView): http_method_names = ['get'] permission_classes = [IsAuthorizedToViewVersion] pagination_class = SearchPagination - serializer_class = PageSearchSerializer + new_api = False def _get_project(self): cache_key = '_cached_project' @@ -363,6 +371,11 @@ def get_queryset(self): ) return queryset + def get_serializer_class(self): + if self.new_api: + return PageSearchSerializer + return OldPageSearchSerializer + def get_serializer_context(self): context = super().get_serializer_context() context['projects_data'] = self._get_all_projects_data() diff --git a/readthedocs/search/serializers.py b/readthedocs/search/serializers.py index d01eb901d8e..817d971b844 100644 --- a/readthedocs/search/serializers.py +++ b/readthedocs/search/serializers.py @@ -7,15 +7,22 @@ """ import itertools +import re +from functools import namedtuple from operator import attrgetter from django.shortcuts import get_object_or_404 from rest_framework import serializers from readthedocs.core.resolver import resolve +from readthedocs.projects.constants import MKDOCS, SPHINX_HTMLDIR from readthedocs.projects.models import Project +# Structure used for storing cached data of a project mostly. +ProjectData = namedtuple('ProjectData', ['docs_url', 'version_doctype']) + + class ProjectHighlightSerializer(serializers.Serializer): name = serializers.ListField(child=serializers.CharField(), default=list) @@ -29,7 +36,7 @@ class ProjectSearchSerializer(serializers.Serializer): name = serializers.CharField() slug = serializers.CharField() link = serializers.CharField(source='url') - highlight = ProjectHighlightSerializer(source='meta.highlight', default=dict) + highlights = ProjectHighlightSerializer(source='meta.highlight', default=dict) class PageHighlightSerializer(serializers.Serializer): @@ -39,24 +46,55 @@ class PageHighlightSerializer(serializers.Serializer): class PageSearchSerializer(serializers.Serializer): + """ + Page serializer. + + If ``projects_data`` is passed into the context, the serializer + will try to use that to generate the link before querying the database. + It's a dictionary mapping the project slug to a ProjectData object. + """ + type = serializers.CharField(default='page', source=None, read_only=True) project = serializers.CharField() version = serializers.CharField() title = serializers.CharField() link = serializers.SerializerMethodField() - highlight = PageHighlightSerializer(source='meta.highlight', default=dict) + highlights = PageHighlightSerializer(source='meta.highlight', default=dict) blocks = serializers.SerializerMethodField() def get_link(self, obj): - # TODO: optimize this to not query the db for each result. - # TODO: return an relative URL when this is called from the indoc search. + """ + Get the page link. + + Try to get the link from the ``project_data`` context, + and fallback to get it from the database. + If the result is fetched from the database, + it's cached into ``project_data``. + """ + # TODO: return a relative URL when this is called from the indoc search. + + # First try to build the URL from the context. + project_data = self.context.get('projects_data', {}).get(obj.project) + if project_data: + docs_url, doctype = project_data + path = obj.full_path + + # Generate an appropriate link for the doctypes that use htmldir, + # and always end it with / so it goes directly to proxito. + if doctype in {SPHINX_HTMLDIR, MKDOCS}: + path = re.sub('(^|/)index.html$', '/', path) + + return docs_url.rstrip('/') + '/' + path.lstrip('/') + + # Fallback to build the URL querying the db. project = Project.objects.filter(slug=obj.project).first() if project: - return resolve( - project=project, - version_slug=obj.version, - filename=obj.full_path, - ) + docs_url = project.get_docs_url(version_slug=obj.version) + # cache the project URL + projects_data = self.context.setdefault('projects_data', {}) + projects_data[obj.project] = ProjectData(docs_url, '') + return docs_url + obj.full_path + return None def get_blocks(self, obj): @@ -90,20 +128,16 @@ def get_blocks(self, obj): class DomainHighlightSerializer(serializers.Serializer): - """ - Serializer for domain results. + name = serializers.SerializerMethodField() + content = serializers.SerializerMethodField() - .. note:: + def get_name(self, obj): + name = getattr(obj, 'domains.name', []) + return list(name) - We override the `to_representation` method instead of declaring each field - because serializers don't play nice with keys that include `.`. - """ - - def to_representation(self, instance): - return { - 'name': getattr(instance, 'domains.name', []), - 'docstring': getattr(instance, 'domains.docstrings', []), - } + def get_content(self, obj): + docstring = getattr(obj, 'domains.docstrings', []) + return list(docstring) class DomainSearchSerializer(serializers.Serializer): @@ -112,26 +146,22 @@ class DomainSearchSerializer(serializers.Serializer): role = serializers.CharField(source='_source.role_name') name = serializers.CharField(source='_source.name') id = serializers.CharField(source='_source.anchor') - docstring = serializers.CharField(source='_source.docstrings') - highlight = DomainHighlightSerializer(default=dict) + content = serializers.CharField(source='_source.docstrings') + highlights = DomainHighlightSerializer(source='highlight', default=dict) class SectionHighlightSerializer(serializers.Serializer): - """ - Serializer for section results. + title = serializers.SerializerMethodField() + content = serializers.SerializerMethodField() - .. note:: + def get_title(self, obj): + title = getattr(obj, 'sections.title', []) + return list(title) - We override the `to_representation` method instead of declaring each field - because serializers don't play nice with keys that include `.`. - """ - - def to_representation(self, instance): - return { - 'title': getattr(instance, 'sections.title', []), - 'content': getattr(instance, 'sections.content', []), - } + def get_content(self, obj): + content = getattr(obj, 'sections.content', []) + return list(content) class SectionSearchSerializer(serializers.Serializer): @@ -140,4 +170,4 @@ class SectionSearchSerializer(serializers.Serializer): id = serializers.CharField(source='_source.id') title = serializers.CharField(source='_source.title') content = serializers.CharField(source='_source.content') - highlight = SectionHighlightSerializer(default=dict) + highlights = SectionHighlightSerializer(source='highlight', default=dict) diff --git a/readthedocs/search/tests/test_api.py b/readthedocs/search/tests/test_api.py index c7bf9728fcd..2a57e576c65 100644 --- a/readthedocs/search/tests/test_api.py +++ b/readthedocs/search/tests/test_api.py @@ -23,14 +23,6 @@ get_search_query_from_project_file, ) -OLD_TYPES = { - 'domain': 'domains', - 'section': 'sections', -} -OLD_FIELDS = { - 'docstring': 'docstrings', -} - @pytest.mark.django_db @pytest.mark.search @@ -41,7 +33,7 @@ def setup_method(self, method): # This reverse needs to be inside the ``setup_method`` method because from # the Corporate site we don't define this URL if ``-ext`` module is not # installed - self.url = reverse('doc_search') + self.url = reverse('search_api') def get_search(self, api_client, search_params): return api_client.get(self.url, search_params) @@ -71,7 +63,7 @@ def test_search_works_with_title_query(self, api_client, project, page_num): assert project_data['project'] == project.slug # Check highlight return correct object of first result - title_highlight = project_data['highlight']['title'] + title_highlight = project_data['highlights']['title'] assert len(title_highlight) == 1 assert query.lower() in title_highlight[0].lower() @@ -108,26 +100,24 @@ def test_search_works_with_sections_and_domains_query( project_data = data[0] assert project_data['project'] == project.slug - inner_hits = project_data['inner_hits'] + blocks = project_data['blocks'] # since there was a nested query, - # inner_hits should not be empty - assert len(inner_hits) >= 1 + # blocks should not be empty + assert len(blocks) >= 1 - inner_hit_0 = inner_hits[0] # first inner_hit + block_0 = blocks[0] - old_type = OLD_TYPES.get(type, type) - assert inner_hit_0['type'] == old_type + assert block_0['type'] == type - old_field = old_type + '.' + OLD_FIELDS.get(field, field) - highlight = inner_hit_0['highlight'][old_field] + highlights = block_0['highlights'][field] assert ( - len(highlight) == 1 + len(highlights) == 1 ), 'number_of_fragments is set to 1' # checking highlighting of results highlighted_words = re.findall( # this gets all words inside tag '(.*?)', - highlight[0] + highlights[0] ) assert len(highlighted_words) > 0 @@ -529,8 +519,8 @@ def test_search_custom_ranking(self, api_client): results = resp.data['results'] assert len(results) == 2 - assert results[0]['full_path'] == 'index.html' - assert results[1]['full_path'] == 'guides/index.html' + assert results[0]['link'].endswith('/en/latest/index.html') + assert results[1]['link'].endswith('/en/latest/guides/index.html') # Query with a higher rank over guides/index.html page_guides.rank = 5 @@ -547,8 +537,8 @@ def test_search_custom_ranking(self, api_client): results = resp.data['results'] assert len(results) == 2 - assert results[0]['full_path'] == 'guides/index.html' - assert results[1]['full_path'] == 'index.html' + assert results[0]['link'].endswith('/en/latest/guides/index.html') + assert results[1]['link'].endswith('/en/latest/index.html') # Query with a lower rank over index.html page_index.rank = -2 @@ -568,8 +558,8 @@ def test_search_custom_ranking(self, api_client): results = resp.data['results'] assert len(results) == 2 - assert results[0]['full_path'] == 'guides/index.html' - assert results[1]['full_path'] == 'index.html' + assert results[0]['link'].endswith('/en/latest/guides/index.html') + assert results[1]['link'].endswith('/en/latest/index.html') # Query with a lower rank over index.html page_index.rank = 3 @@ -589,8 +579,8 @@ def test_search_custom_ranking(self, api_client): results = resp.data['results'] assert len(results) == 2 - assert results[0]['full_path'] == 'guides/index.html' - assert results[1]['full_path'] == 'index.html' + assert results[0]['link'].endswith('/en/latest/guides/index.html') + assert results[1]['link'].endswith('/en/latest/index.html') # Query with a same rank over guides/index.html and index.html page_index.rank = -10 @@ -610,8 +600,8 @@ def test_search_custom_ranking(self, api_client): results = resp.data['results'] assert len(results) == 2 - assert results[0]['full_path'] == 'index.html' - assert results[1]['full_path'] == 'guides/index.html' + assert results[0]['link'].endswith('/en/latest/index.html') + assert results[1]['link'].endswith('/en/latest/guides/index.html') class TestDocumentSearch(BaseTestDocumentSearch): diff --git a/readthedocs/search/tests/test_proxied_api.py b/readthedocs/search/tests/test_proxied_api.py index 948ee298775..884cb492a00 100644 --- a/readthedocs/search/tests/test_proxied_api.py +++ b/readthedocs/search/tests/test_proxied_api.py @@ -15,4 +15,6 @@ def setup_settings(self, settings): settings.PUBLIC_DOMAIN = 'readthedocs.io' def get_search(self, api_client, search_params): + # TODO: remove once the api is stable + search_params['new-api'] = 'true' return api_client.get(self.url, search_params, HTTP_HOST=self.host) diff --git a/readthedocs/search/tests/test_search_tasks.py b/readthedocs/search/tests/test_search_tasks.py index 2e13066fef4..b90b009ab8a 100644 --- a/readthedocs/search/tests/test_search_tasks.py +++ b/readthedocs/search/tests/test_search_tasks.py @@ -20,7 +20,7 @@ def setup_class(cls): # This reverse needs to be inside the ``setup_class`` method because from # the Corporate site we don't define this URL if ``-ext`` module is not # installed - cls.url = reverse('doc_search') + cls.url = reverse('search_api') def test_search_query_recorded_when_results_not_zero(self, api_client): """Test if search query is recorded in a database when a search is made.""" diff --git a/readthedocs/search/tests/test_views.py b/readthedocs/search/tests/test_views.py index 609e261af30..ede7034cc5b 100644 --- a/readthedocs/search/tests/test_views.py +++ b/readthedocs/search/tests/test_views.py @@ -106,7 +106,7 @@ def _get_highlight(self, result, field, type=None): # if query is from page title, # highlighted title is present in 'result.meta.highlight.title' if not type and field == 'title': - highlight = result['highlight']['title'] + highlight = result['highlights']['title'] # if result is not from page title, # then results and highlighted results are present inside 'blocks' @@ -117,7 +117,7 @@ def _get_highlight(self, result, field, type=None): # checking first inner_hit inner_hit_0 = blocks[0] assert inner_hit_0['type'] == type - highlight = inner_hit_0['highlight'][field] + highlight = inner_hit_0['highlights'][field] return highlight diff --git a/readthedocs/search/tests/utils.py b/readthedocs/search/tests/utils.py index c55380b7fe8..d6de0091494 100644 --- a/readthedocs/search/tests/utils.py +++ b/readthedocs/search/tests/utils.py @@ -2,8 +2,8 @@ from readthedocs.projects.models import HTMLFile -SECTION_FIELDS = [ 'section.title', 'section.content' ] -DOMAIN_FIELDS = [ 'domain.name', 'domain.docstring' ] +SECTION_FIELDS = ['section.title', 'section.content'] +DOMAIN_FIELDS = ['domain.name', 'domain.content'] DATA_TYPES_VALUES = ['title'] + SECTION_FIELDS + DOMAIN_FIELDS @@ -77,12 +77,12 @@ def get_search_query_from_project_file(project_slug, page_num=0, field='title', else: query = query_data[0]['name'].split()[0] - elif type == 'domain' and field == 'docstring': + elif type == 'domain' and field == 'content': - # generates query from domain docstrings + # generates query from domain content anchor = query_data[0]['anchor'] - docstrings = file_data['domain_data'][anchor] - query_data = docstrings.split() + content = file_data['domain_data'][anchor] + query_data = content.split() start = random.randint(0, 1) # 5 words to generate query to make sure that diff --git a/readthedocs/search/views.py b/readthedocs/search/views.py index 50ba0b65101..47341b5d8f8 100644 --- a/readthedocs/search/views.py +++ b/readthedocs/search/views.py @@ -3,6 +3,7 @@ import logging from django.shortcuts import get_object_or_404, render +from django.views import View from readthedocs.builds.constants import LATEST from readthedocs.projects.models import Project @@ -12,7 +13,11 @@ ProjectSearch, ) -from .serializers import PageSearchSerializer, ProjectSearchSerializer +from .serializers import ( + PageSearchSerializer, + ProjectData, + ProjectSearchSerializer, +) log = logging.getLogger(__name__) LOG_TEMPLATE = '(Elastic Search) [%(user)s:%(type)s] [%(project)s:%(version)s:%(language)s] %(msg)s' @@ -31,7 +36,8 @@ ) -def elastic_search(request, project_slug=None): +class SearchView(View): + """ Global user search on the dashboard. @@ -40,84 +46,117 @@ def elastic_search(request, project_slug=None): :param project_slug: Sent when the view is a project search """ - request_type = None - if project_slug: - queryset = Project.objects.protected(request.user) - project_obj = get_object_or_404(queryset, slug=project_slug) - request_type = request.GET.get('type', 'file') - - user_input = UserInput( - query=request.GET.get('q'), - type=request_type or request.GET.get('type', 'project'), - project=project_slug or request.GET.get('project'), - version=request.GET.get('version', LATEST), - language=request.GET.get('language'), - role_name=request.GET.get('role_name'), - index=request.GET.get('index'), - ) - search_facets = collections.defaultdict( - lambda: ProjectSearch, - { - 'project': ProjectSearch, - 'file': PageSearch, + http_method_names = ['get'] + max_search_results = 50 + + def _get_project(self, project_slug): + # TODO: see if this can be just public(). + queryset = Project.objects.protected(self.request.user) + project = get_object_or_404(queryset, slug=project_slug) + return project + + def _get_project_data(self, project, version_slug): + version_doctype = ( + project.versions + .values_list('documentation_type', flat=True) + .get(slug=version_slug) + ) + docs_url = project.get_docs_url(version_slug=version_slug) + project_data = { + project.slug: ProjectData(docs_url, version_doctype) } - ) + return project_data - results = [] - facets = {} + def get_serializer_context(self, project, version_slug): + context = { + 'projects_data': self._get_project_data(project, version_slug), + } + return context + + def get(self, request, project_slug=None): + request_type = None + if project_slug: + project_obj = self._get_project(project_slug) + request_type = request.GET.get('type', 'file') + + version_slug = request.GET.get('version', LATEST) + + user_input = UserInput( + query=request.GET.get('q'), + type=request_type or request.GET.get('type', 'project'), + project=project_slug or request.GET.get('project'), + version=version_slug, + language=request.GET.get('language'), + role_name=request.GET.get('role_name'), + index=request.GET.get('index'), + ) + results = [] + facets = {} - if user_input.query: - filters = {} + if user_input.query: + filters = {} - for avail_facet in ALL_FACETS: - value = getattr(user_input, avail_facet, None) - if value: - filters[avail_facet] = value + for avail_facet in ALL_FACETS: + value = getattr(user_input, avail_facet, None) + if value: + filters[avail_facet] = value - search = search_facets[user_input.type]( - query=user_input.query, - filters=filters, - user=request.user, - ) - results = search[:50].execute() - facets = results.facets - - log.info( - LOG_TEMPLATE, - { - 'user': request.user, - 'project': user_input.project or '', - 'type': user_input.type or '', - 'version': user_input.version or '', - 'language': user_input.language or '', - 'msg': user_input.query or '', + search_facets = { + 'project': ProjectSearch, + 'file': PageSearch, } + faceted_search_class = search_facets.get( + user_input.type, + ProjectSearch, + ) + search = faceted_search_class( + query=user_input.query, + filters=filters, + user=request.user, + ) + results = search[:self.max_search_results].execute() + facets = results.facets + + log.info( + LOG_TEMPLATE, + { + 'user': request.user, + 'project': user_input.project or '', + 'type': user_input.type or '', + 'version': user_input.version or '', + 'language': user_input.language or '', + 'msg': user_input.query or '', + } + ) + + # Make sure our selected facets are displayed even when they return 0 results + for facet in facets: + value = getattr(user_input, facet, None) + if value and value not in (val[0] for val in facets[facet]): + facets[facet].insert(0, (value, 0, True)) + + serializers = { + 'project': ProjectSearchSerializer, + 'file': PageSearchSerializer, + } + serializer = serializers.get(user_input.type, ProjectSearchSerializer) + if project_slug: + context = self.get_serializer_context(project_obj, version_slug) + else: + context = {} + results = serializer(results, many=True, context=context).data + + template_vars = user_input._asdict() + template_vars.update({ + 'results': results, + 'facets': facets, + }) + + if project_slug: + template_vars.update({'project_obj': project_obj}) + + return render( + request, + 'search/elastic_search.html', + template_vars, ) - - # Make sure our selected facets are displayed even when they return 0 results - for facet in facets: - value = getattr(user_input, facet, None) - if value and value not in (val[0] for val in facets[facet]): - facets[facet].insert(0, (value, 0, True)) - - serializers = { - 'project': ProjectSearchSerializer, - 'file': PageSearchSerializer, - } - serializer = serializers.get(user_input.type, ProjectSearchSerializer) - results = serializer(results, many=True).data - - template_vars = user_input._asdict() - template_vars.update({ - 'results': results, - 'facets': facets, - }) - - if project_slug: - template_vars.update({'project_obj': project_obj}) - - return render( - request, - 'search/elastic_search.html', - template_vars, - ) diff --git a/readthedocs/templates/search/elastic_search.html b/readthedocs/templates/search/elastic_search.html index 64eeee991fa..d0d8db4d0e6 100644 --- a/readthedocs/templates/search/elastic_search.html +++ b/readthedocs/templates/search/elastic_search.html @@ -170,7 +170,7 @@

    {{ result.name }} ({{ result.slug }}) - {% for fragment in result.highlight.description %} + {% for fragment in result.highlights.description %}

    ...{{ fragment|safe }}...

    @@ -184,15 +184,15 @@

    {% elif result.type == 'page' %} - {{ result.project }} - {% if result.highlight.title %} {{ result.highlight.title.0|safe }} {% else %} {{ result.title }} {% endif %} + {{ result.project }} - {% if result.highlights.title %} {{ result.highlights.title.0|safe }} {% else %} {{ result.title }} {% endif %} {% for block in result.blocks %} {% if block.type == 'domain' %}

    - {% if block.highlight.name %} - {% with domain_name=block.highlight.name %} + {% if block.highlights.name %} + {% with domain_name=block.highlights.name %} [{{ block.role }}]: {{ domain_name.0|safe }} {% endwith %} {% else %} @@ -201,13 +201,13 @@

    - {% if block.highlight.docstring %} - {% with domain_docstrings=block.highlight.docstring %} - {{ domain_docstrings.0|safe }} + {% if block.highlights.content %} + {% with domain_content=block.highlights.content %} + {{ domain_content.0|safe }} {% endwith %} {% else %} - {% if block.docstring %} - {{ block.docstring|slice:MAX_SUBSTRING_LIMIT }} ... + {% if block.content %} + {{ block.content|slice:MAX_SUBSTRING_LIMIT }} ... {% endif %} {% endif %}

    @@ -215,8 +215,8 @@

    {% elif block.type == 'section' %}

    - {% if block.highlight.title %} - {% with section_title=block.highlight.title %} + {% if block.highlights.title %} + {% with section_title=block.highlights.title %} {{ section_title.0|safe }} {% endwith %} {% else %} @@ -224,8 +224,8 @@

    {% endif %}

    - {% if block.highlight.content %} - {% with section_content=block.highlight.content %} + {% if block.highlights.content %} + {% with section_content=block.highlights.content %} {% for content in section_content %}

    ... {{ content|safe }} ... diff --git a/readthedocs/urls.py b/readthedocs/urls.py index e19e0b3434f..abfecec9a36 100644 --- a/readthedocs/urls.py +++ b/readthedocs/urls.py @@ -10,14 +10,9 @@ from django.views.generic.base import RedirectView, TemplateView from readthedocs.core.urls import core_urls -from readthedocs.core.views import ( - HomepageView, - do_not_track, - server_error_500, -) -from readthedocs.search import views as search_views +from readthedocs.core.views import HomepageView, do_not_track, server_error_500 from readthedocs.search.api import PageSearchAPIView - +from readthedocs.search.views import SearchView admin.autodiscover() @@ -35,7 +30,7 @@ ] rtd_urls = [ - url(r'^search/$', search_views.elastic_search, name='search'), + url(r'^search/$', SearchView.as_view(), name='search'), url(r'^dashboard/', include('readthedocs.projects.urls.private')), url(r'^profiles/', include('readthedocs.profiles.urls.public')), url(r'^accounts/', include('readthedocs.profiles.urls.private')), @@ -56,6 +51,7 @@ url(r'^api/v2/', include('readthedocs.api.v2.urls')), # Keep the `doc_search` at root level, so the test does not fail for other API url(r'^api/v2/docsearch/$', PageSearchAPIView.as_view(), name='doc_search'), + url(r'^api/v2/search/$', PageSearchAPIView.as_view(new_api=True), name='search_api'), url( r'^api-auth/', include('rest_framework.urls', namespace='rest_framework')