content,internal: delete autocomplete code

Autocomplete no longer works in dev, and the experiment is not active
in any other environment.

Change-Id: I8865539862a4d1b84bf91641e7662d4926d30488
Reviewed-on: https://go-review.googlesource.com/c/pkgsite/+/279133
Run-TryBot: Julie Qiu <julie@golang.org>
TryBot-Result: kokoro <noreply+kokoro@google.com>
Reviewed-by: Jonathan Amsterdam <jba@google.com>
Trust: Julie Qiu <julie@golang.org>
diff --git a/content/static/html/base.tmpl b/content/static/html/base.tmpl
index f55afed..1af51b1 100644
--- a/content/static/html/base.tmpl
+++ b/content/static/html/base.tmpl
@@ -218,10 +218,3 @@
   </iframe>
 </noscript>
 {{end}}
-
-{{if (.Experiments.IsActive "autocomplete")}}
-<script>
-  loadScript("/third_party/autoComplete.js/autoComplete.min.js");
-  loadScript("/static/js/completion.min.js");
-</script>
-{{end}}
diff --git a/content/static/html/helpers/_search_bar.tmpl b/content/static/html/helpers/_search_bar.tmpl
index c624dc5..8a4f8c9 100644
--- a/content/static/html/helpers/_search_bar.tmpl
+++ b/content/static/html/helpers/_search_bar.tmpl
@@ -5,14 +5,11 @@
 -->
 
 {{define "search"}}
-  <div class="SearchForm-container{{if (.Experiments.IsActive "autocomplete")}} Experiment-autoComplete{{end}}">
-    <form class="SearchForm" action="/search" role="search" id="js-AutoComplete-parent" aria-owns="AutoComplete-list">
+  <div class="SearchForm-container">
+    <form class="SearchForm" action="/search" role="search">
       <div class="SearchForm-firstRow">
-        <input class="SearchForm-input js-autoComplete js-searchFocus"
-          id="js-AutoComplete-input"
+        <input class="SearchForm-input js-searchFocus"
           role="textbox"
-          aria-controls="AutoComplete-list"
-          aria-autocomplete="list"
           aria-label="Search for a package"
           type="text"
           name="q"
@@ -38,7 +35,7 @@
     <button class="Header-searchFormSubmit" aria-label="Search for a package">
       <svg class="Header-searchFormSubmitIcon" focusable="false" viewBox="0 0 24 24" aria-hidden="true" role="presentation"><path d="M15.5 14h-.79l-.28-.27C15.41 12.59 16 11.11 16 9.5 16 5.91 13.09 3 9.5 3S3 5.91 3 9.5 5.91 16 9.5 16c1.61 0 3.09-.59 4.23-1.57l.27.28v.79l5 4.99L20.49 19l-4.99-5zm-6 0C7.01 14 5 11.99 5 9.5S7.01 5 9.5 5 14 7.01 14 9.5 11.99 14 9.5 14z"></path><path fill="none" d="M0 0h24v24H0z"></path></svg>
     </button>
-    <input class="Header-searchFormInput js-autoComplete js-searchFocus"
+    <input class="Header-searchFormInput js-searchFocus"
       aria-label="Search for a package"
       type="text"
       name="q"
diff --git a/content/static/js/completion.js b/content/static/js/completion.js
deleted file mode 100644
index be98027..0000000
--- a/content/static/js/completion.js
+++ /dev/null
@@ -1,188 +0,0 @@
-/**
- * @license
- * Copyright 2019-2020 The Go Authors. All rights reserved.
- * Use of this source code is governed by a BSD-style
- * license that can be found in the LICENSE file.
- */
-
-document.addEventListener('DOMContentLoaded', () => {
-  // To implement autocomplete we use autoComplete.js, but override the
-  // navigation controller to be more accessible.
-  //
-  // Accessibility requirements are based on:
-  // https://www.w3.org/TR/wai-aria-practices/examples/combobox/aria1.1pattern/listbox-combo.html
-  // This defines the interaction of three elements: the 'combobox', which is
-  // the parent of both the text input, and the completion list.
-  //
-  // The autoComplete.js library assumes a single element in its query
-  // selector, unfortunately, so we can't express our autocompletion behavior
-  // in terms of classes rather than IDs. See also
-  // https://github.com/TarekRaafat/autoComplete.js/issues/82
-  const completeInput = document.querySelector('#js-AutoComplete-input');
-  const parentForm = document.querySelector('#js-AutoComplete-parent');
-  const hideCompletion = () => {
-    parentForm.setAttribute('aria-expanded', false);
-    // Without removing aria-activedescendant screenreaders will get confused.
-    completeInput.removeAttribute('aria-activedescendant');
-  };
-  const showCompletion = () => {
-    parentForm.setAttribute('aria-expanded', true);
-  };
-  // Accessibility: hide and show the completion dialog on blur and focus.
-  completeInput.addEventListener('blur', hideCompletion);
-  completeInput.addEventListener('focus', showCompletion);
-
-  /**
-   * See https://tarekraafat.github.io/autoComplete.js/#/?id=api-configuration
-   * for some documentation of the autoComplete.js API. In particular, it
-   * allows overriding the resultsList navigation controller.
-   *
-   * This controller is based on the default implementation here:
-   * https://github.com/TarekRaafat/autoComplete.js/blob/v7.2.0/src/views/autoCompleteView.js#L120
-   *
-   * The primary changes are:
-   * + set aria-expanded, aria-selected, and aria-activedescendant attributes
-   *   where necessary
-   * + use aria attributes for styling, rather than classes
-   * + add id attributes to the list elements
-   * + hide completion on blur, or escape
-   * + simplify the code a bit
-   *
-   * This function is evaluated each time the result list is assembled (i.e.
-   * potentially each keypress).
-   *
-   * @param input is the text input element.
-   * @param resultsList is the ul element being constructed.
-   * @param sendFeedback is the data feedback function invoked on user
-   *   selection. "feedback" is the term used for this in autoComplete.js, so
-   *   we preserve this for consistency.
-   * @param resultsValues is the processed data values from the data source.
-   */
-  const navigation = (evt, input, resultsList, sendFeedback, resultsValues) => {
-    const lis = Array.from(resultsList.childNodes);
-    let liSelected = undefined;
-    const highlightSelection = next => {
-      // Clear the existing selected item.
-      if (liSelected) {
-        liSelected.removeAttribute('aria-selected');
-      }
-      liSelected = next;
-      liSelected.setAttribute('aria-selected', 'true');
-      const idx = lis.findIndex(li => li === liSelected);
-      input.setAttribute('aria-activedescendant', 'AutoComplete-item-' + idx);
-    };
-    const onSelection = (event, elem) => {
-      sendFeedback({
-        event: event,
-        query: input.value,
-        matches: resultsValues.matches,
-        results: resultsValues.list.map(record => record.value),
-        selection: resultsValues.list.find(
-          value => value.index === Number(elem.getAttribute('data-id'))
-        ),
-      });
-      hideCompletion();
-    };
-    input.onkeydown = event => {
-      const keys = {
-        ENTER: 13,
-        ESCAPE: 27,
-        ARROW_UP: 38,
-        ARROW_DOWN: 40,
-      };
-      let next = undefined; // the next item to highlight
-      if (lis.length > 0) {
-        switch (event.keyCode) {
-          case keys.ARROW_UP:
-            showCompletion();
-            // Show the last completion item if none are currently selected, or
-            // we're at the start of the list.
-            next = lis[lis.length - 1];
-            if (liSelected && liSelected.previousSibling) {
-              next = liSelected.previousSibling;
-            }
-            highlightSelection(next);
-            // If we don't preventDefault here, up and down arrows cause us to
-            // also jump to the start or end of the text input.
-            event.preventDefault();
-            break;
-          case keys.ARROW_DOWN:
-            showCompletion();
-            // Show the first completion item if none are currently selected,
-            // or if we're at the end of the list.
-            next = lis[0];
-            if (liSelected && liSelected.nextSibling) {
-              next = liSelected.nextSibling;
-            }
-            highlightSelection(next);
-            // See note above for why this is necessary.
-            event.preventDefault();
-            break;
-          case keys.ENTER:
-            if (liSelected) {
-              event.preventDefault();
-              onSelection(event, liSelected);
-            }
-            break;
-          case keys.ESCAPE:
-            // From the aria guide, escape should both hide completion and
-            // clear the input. Arguably it might be better to leave the input
-            // untouched, which is what Google search does.
-            hideCompletion();
-            input.value = '';
-            break;
-          default:
-            // Because we've hidden the completion ourselves on escape, we need
-            // to show it on any other character.
-            showCompletion();
-        }
-      }
-    };
-    lis.forEach((selection, i) => {
-      // Completion items need id attributes so that they can be referenced by
-      // aria-activedescendant.
-      selection.setAttribute('id', 'AutoComplete-item-' + i);
-      selection.onmousedown = event => {
-        onSelection(event, event.currentTarget);
-        event.preventDefault();
-      };
-    });
-  };
-
-  new autoComplete({
-    data: {
-      src: async () => {
-        const query = completeInput.value;
-        const source = await fetch(`/autocomplete?q=${query}`);
-        return await source.json();
-      },
-      // The string we're completing is stored in the 'PackagePath' field of
-      // the returned JSON array elements.
-      key: ['PackagePath'],
-      cache: false,
-    },
-    threshold: 1, // minimum number of characters before rendering results
-    debounce: 100, // in milliseconds
-    resultsList: {
-      render: true,
-      container: source => {
-        source.setAttribute('id', 'AutoComplete-list');
-        source.classList.add('AutoComplete-list');
-        source.setAttribute('role', 'listbox');
-      },
-      destination: document.querySelector('#js-AutoComplete-parent'),
-      position: 'beforeend',
-      element: 'ul',
-      navigation: navigation,
-    },
-    highlight: true,
-    selector: '#js-AutoComplete-input',
-    onSelection: feedback => {
-      if (feedback.selection.value.PackagePath) {
-        // Navigate directly to the package.
-        // TODO (https://golang.org/issue/42955): update ARIA attributes to reflect this.
-        window.location.href = '/' + feedback.selection.value.PackagePath;
-      }
-    },
-  });
-});
diff --git a/content/static/js/completion.min.js b/content/static/js/completion.min.js
deleted file mode 100644
index 294db73..0000000
--- a/content/static/js/completion.min.js
+++ /dev/null
@@ -1,44 +0,0 @@
-/*
-
- Copyright 2019-2020 The Go Authors. All rights reserved.
- Use of this source code is governed by a BSD-style
- license that can be found in the LICENSE file.
-*/
-var $jscomp=$jscomp||{};$jscomp.scope={};$jscomp.createTemplateTagFirstArg=function(a){return a.raw=a};$jscomp.createTemplateTagFirstArgWithRaw=function(a,b){a.raw=b;return a};$jscomp.ASSUME_ES5=!1;$jscomp.ASSUME_NO_NATIVE_MAP=!1;$jscomp.ASSUME_NO_NATIVE_SET=!1;$jscomp.SIMPLE_FROUND_POLYFILL=!1;$jscomp.ISOLATE_POLYFILLS=!1;
-$jscomp.defineProperty=$jscomp.ASSUME_ES5||"function"==typeof Object.defineProperties?Object.defineProperty:function(a,b,e){if(a==Array.prototype||a==Object.prototype)return a;a[b]=e.value;return a};$jscomp.getGlobal=function(a){a=["object"==typeof globalThis&&globalThis,a,"object"==typeof window&&window,"object"==typeof self&&self,"object"==typeof global&&global];for(var b=0;b<a.length;++b){var e=a[b];if(e&&e.Math==Math)return e}throw Error("Cannot find global object");};$jscomp.global=$jscomp.getGlobal(this);
-$jscomp.IS_SYMBOL_NATIVE="function"===typeof Symbol&&"symbol"===typeof Symbol("x");$jscomp.TRUST_ES6_POLYFILLS=!$jscomp.ISOLATE_POLYFILLS||$jscomp.IS_SYMBOL_NATIVE;$jscomp.polyfills={};$jscomp.propertyToPolyfillSymbol={};$jscomp.POLYFILL_PREFIX="$jscp$";var $jscomp$lookupPolyfilledValue=function(a,b){var e=$jscomp.propertyToPolyfillSymbol[b];if(null==e)return a[b];e=a[e];return void 0!==e?e:a[b]};
-$jscomp.polyfill=function(a,b,e,f){b&&($jscomp.ISOLATE_POLYFILLS?$jscomp.polyfillIsolated(a,b,e,f):$jscomp.polyfillUnisolated(a,b,e,f))};$jscomp.polyfillUnisolated=function(a,b,e,f){e=$jscomp.global;a=a.split(".");for(f=0;f<a.length-1;f++){var d=a[f];if(!(d in e))return;e=e[d]}a=a[a.length-1];f=e[a];b=b(f);b!=f&&null!=b&&$jscomp.defineProperty(e,a,{configurable:!0,writable:!0,value:b})};
-$jscomp.polyfillIsolated=function(a,b,e,f){var d=a.split(".");a=1===d.length;f=d[0];f=!a&&f in $jscomp.polyfills?$jscomp.polyfills:$jscomp.global;for(var k=0;k<d.length-1;k++){var c=d[k];if(!(c in f))return;f=f[c]}d=d[d.length-1];e=$jscomp.IS_SYMBOL_NATIVE&&"es6"===e?f[d]:null;b=b(e);null!=b&&(a?$jscomp.defineProperty($jscomp.polyfills,d,{configurable:!0,writable:!0,value:b}):b!==e&&($jscomp.propertyToPolyfillSymbol[d]=$jscomp.IS_SYMBOL_NATIVE?$jscomp.global.Symbol(d):$jscomp.POLYFILL_PREFIX+d,d=
-$jscomp.propertyToPolyfillSymbol[d],$jscomp.defineProperty(f,d,{configurable:!0,writable:!0,value:b})))};$jscomp.underscoreProtoCanBeSet=function(){var a={a:!0},b={};try{return b.__proto__=a,b.a}catch(e){}return!1};$jscomp.setPrototypeOf=$jscomp.TRUST_ES6_POLYFILLS&&"function"==typeof Object.setPrototypeOf?Object.setPrototypeOf:$jscomp.underscoreProtoCanBeSet()?function(a,b){a.__proto__=b;if(a.__proto__!==b)throw new TypeError(a+" is not extensible");return a}:null;
-$jscomp.arrayIteratorImpl=function(a){var b=0;return function(){return b<a.length?{done:!1,value:a[b++]}:{done:!0}}};$jscomp.arrayIterator=function(a){return{next:$jscomp.arrayIteratorImpl(a)}};$jscomp.makeIterator=function(a){var b="undefined"!=typeof Symbol&&Symbol.iterator&&a[Symbol.iterator];return b?b.call(a):$jscomp.arrayIterator(a)};$jscomp.generator={};
-$jscomp.generator.ensureIteratorResultIsObject_=function(a){if(!(a instanceof Object))throw new TypeError("Iterator result "+a+" is not an object");};$jscomp.generator.Context=function(){this.isRunning_=!1;this.yieldAllIterator_=null;this.yieldResult=void 0;this.nextAddress=1;this.finallyAddress_=this.catchAddress_=0;this.finallyContexts_=this.abruptCompletion_=null};
-$jscomp.generator.Context.prototype.start_=function(){if(this.isRunning_)throw new TypeError("Generator is already running");this.isRunning_=!0};$jscomp.generator.Context.prototype.stop_=function(){this.isRunning_=!1};$jscomp.generator.Context.prototype.jumpToErrorHandler_=function(){this.nextAddress=this.catchAddress_||this.finallyAddress_};$jscomp.generator.Context.prototype.next_=function(a){this.yieldResult=a};
-$jscomp.generator.Context.prototype.throw_=function(a){this.abruptCompletion_={exception:a,isException:!0};this.jumpToErrorHandler_()};$jscomp.generator.Context.prototype.return=function(a){this.abruptCompletion_={return:a};this.nextAddress=this.finallyAddress_};$jscomp.generator.Context.prototype.jumpThroughFinallyBlocks=function(a){this.abruptCompletion_={jumpTo:a};this.nextAddress=this.finallyAddress_};$jscomp.generator.Context.prototype.yield=function(a,b){this.nextAddress=b;return{value:a}};
-$jscomp.generator.Context.prototype.yieldAll=function(a,b){a=$jscomp.makeIterator(a);var e=a.next();$jscomp.generator.ensureIteratorResultIsObject_(e);if(e.done)this.yieldResult=e.value,this.nextAddress=b;else return this.yieldAllIterator_=a,this.yield(e.value,b)};$jscomp.generator.Context.prototype.jumpTo=function(a){this.nextAddress=a};$jscomp.generator.Context.prototype.jumpToEnd=function(){this.nextAddress=0};
-$jscomp.generator.Context.prototype.setCatchFinallyBlocks=function(a,b){this.catchAddress_=a;void 0!=b&&(this.finallyAddress_=b)};$jscomp.generator.Context.prototype.setFinallyBlock=function(a){this.catchAddress_=0;this.finallyAddress_=a||0};$jscomp.generator.Context.prototype.leaveTryBlock=function(a,b){this.nextAddress=a;this.catchAddress_=b||0};
-$jscomp.generator.Context.prototype.enterCatchBlock=function(a){this.catchAddress_=a||0;a=this.abruptCompletion_.exception;this.abruptCompletion_=null;return a};$jscomp.generator.Context.prototype.enterFinallyBlock=function(a,b,e){e?this.finallyContexts_[e]=this.abruptCompletion_:this.finallyContexts_=[this.abruptCompletion_];this.catchAddress_=a||0;this.finallyAddress_=b||0};
-$jscomp.generator.Context.prototype.leaveFinallyBlock=function(a,b){b=this.finallyContexts_.splice(b||0)[0];if(b=this.abruptCompletion_=this.abruptCompletion_||b){if(b.isException)return this.jumpToErrorHandler_();void 0!=b.jumpTo&&this.finallyAddress_<b.jumpTo?(this.nextAddress=b.jumpTo,this.abruptCompletion_=null):this.nextAddress=this.finallyAddress_}else this.nextAddress=a};$jscomp.generator.Context.prototype.forIn=function(a){return new $jscomp.generator.Context.PropertyIterator(a)};
-$jscomp.generator.Context.PropertyIterator=function(a){this.object_=a;this.properties_=[];for(var b in a)this.properties_.push(b);this.properties_.reverse()};$jscomp.generator.Context.PropertyIterator.prototype.getNext=function(){for(;0<this.properties_.length;){var a=this.properties_.pop();if(a in this.object_)return a}return null};$jscomp.generator.Engine_=function(a){this.context_=new $jscomp.generator.Context;this.program_=a};
-$jscomp.generator.Engine_.prototype.next_=function(a){this.context_.start_();if(this.context_.yieldAllIterator_)return this.yieldAllStep_(this.context_.yieldAllIterator_.next,a,this.context_.next_);this.context_.next_(a);return this.nextStep_()};
-$jscomp.generator.Engine_.prototype.return_=function(a){this.context_.start_();var b=this.context_.yieldAllIterator_;if(b)return this.yieldAllStep_("return"in b?b["return"]:function(e){return{value:e,done:!0}},a,this.context_.return);this.context_.return(a);return this.nextStep_()};
-$jscomp.generator.Engine_.prototype.throw_=function(a){this.context_.start_();if(this.context_.yieldAllIterator_)return this.yieldAllStep_(this.context_.yieldAllIterator_["throw"],a,this.context_.next_);this.context_.throw_(a);return this.nextStep_()};
-$jscomp.generator.Engine_.prototype.yieldAllStep_=function(a,b,e){try{var f=a.call(this.context_.yieldAllIterator_,b);$jscomp.generator.ensureIteratorResultIsObject_(f);if(!f.done)return this.context_.stop_(),f;var d=f.value}catch(k){return this.context_.yieldAllIterator_=null,this.context_.throw_(k),this.nextStep_()}this.context_.yieldAllIterator_=null;e.call(this.context_,d);return this.nextStep_()};
-$jscomp.generator.Engine_.prototype.nextStep_=function(){for(;this.context_.nextAddress;)try{var a=this.program_(this.context_);if(a)return this.context_.stop_(),{value:a.value,done:!1}}catch(b){this.context_.yieldResult=void 0,this.context_.throw_(b)}this.context_.stop_();if(this.context_.abruptCompletion_){a=this.context_.abruptCompletion_;this.context_.abruptCompletion_=null;if(a.isException)throw a.exception;return{value:a.return,done:!0}}return{value:void 0,done:!0}};
-$jscomp.generator.Generator_=function(a){this.next=function(b){return a.next_(b)};this.throw=function(b){return a.throw_(b)};this.return=function(b){return a.return_(b)};this[Symbol.iterator]=function(){return this}};$jscomp.generator.createGenerator=function(a,b){b=new $jscomp.generator.Generator_(new $jscomp.generator.Engine_(b));$jscomp.setPrototypeOf&&a.prototype&&$jscomp.setPrototypeOf(b,a.prototype);return b};
-$jscomp.asyncExecutePromiseGenerator=function(a){function b(f){return a.next(f)}function e(f){return a.throw(f)}return new Promise(function(f,d){function k(c){c.done?f(c.value):Promise.resolve(c.value).then(b,e).then(k,d)}k(a.next())})};$jscomp.asyncExecutePromiseGeneratorFunction=function(a){return $jscomp.asyncExecutePromiseGenerator(a())};$jscomp.asyncExecutePromiseGeneratorProgram=function(a){return $jscomp.asyncExecutePromiseGenerator(new $jscomp.generator.Generator_(new $jscomp.generator.Engine_(a)))};
-$jscomp.initSymbol=function(){};$jscomp.polyfill("Symbol",function(a){if(a)return a;var b=function(d,k){this.$jscomp$symbol$id_=d;$jscomp.defineProperty(this,"description",{configurable:!0,writable:!0,value:k})};b.prototype.toString=function(){return this.$jscomp$symbol$id_};var e=0,f=function(d){if(this instanceof f)throw new TypeError("Symbol is not a constructor");return new b("jscomp_symbol_"+(d||"")+"_"+e++,d)};return f},"es6","es3");$jscomp.initSymbolIterator=function(){};
-$jscomp.polyfill("Symbol.iterator",function(a){if(a)return a;a=Symbol("Symbol.iterator");for(var b="Array Int8Array Uint8Array Uint8ClampedArray Int16Array Uint16Array Int32Array Uint32Array Float32Array Float64Array".split(" "),e=0;e<b.length;e++){var f=$jscomp.global[b[e]];"function"===typeof f&&"function"!=typeof f.prototype[a]&&$jscomp.defineProperty(f.prototype,a,{configurable:!0,writable:!0,value:function(){return $jscomp.iteratorPrototype($jscomp.arrayIteratorImpl(this))}})}return a},"es6",
-"es3");$jscomp.initSymbolAsyncIterator=function(){};$jscomp.iteratorPrototype=function(a){a={next:a};a[Symbol.iterator]=function(){return this};return a};$jscomp.FORCE_POLYFILL_PROMISE=!1;
-$jscomp.polyfill("Promise",function(a){function b(){this.batch_=null}function e(c){return c instanceof d?c:new d(function(g,h){g(c)})}if(a&&!$jscomp.FORCE_POLYFILL_PROMISE)return a;b.prototype.asyncExecute=function(c){if(null==this.batch_){this.batch_=[];var g=this;this.asyncExecuteFunction(function(){g.executeBatch_()})}this.batch_.push(c)};var f=$jscomp.global.setTimeout;b.prototype.asyncExecuteFunction=function(c){f(c,0)};b.prototype.executeBatch_=function(){for(;this.batch_&&this.batch_.length;){var c=
-this.batch_;this.batch_=[];for(var g=0;g<c.length;++g){var h=c[g];c[g]=null;try{h()}catch(l){this.asyncThrow_(l)}}}this.batch_=null};b.prototype.asyncThrow_=function(c){this.asyncExecuteFunction(function(){throw c;})};var d=function(c){this.state_=0;this.result_=void 0;this.onSettledCallbacks_=[];var g=this.createResolveAndReject_();try{c(g.resolve,g.reject)}catch(h){g.reject(h)}};d.prototype.createResolveAndReject_=function(){function c(l){return function(m){h||(h=!0,l.call(g,m))}}var g=this,h=!1;
-return{resolve:c(this.resolveTo_),reject:c(this.reject_)}};d.prototype.resolveTo_=function(c){if(c===this)this.reject_(new TypeError("A Promise cannot resolve to itself"));else if(c instanceof d)this.settleSameAsPromise_(c);else{a:switch(typeof c){case "object":var g=null!=c;break a;case "function":g=!0;break a;default:g=!1}g?this.resolveToNonPromiseObj_(c):this.fulfill_(c)}};d.prototype.resolveToNonPromiseObj_=function(c){var g=void 0;try{g=c.then}catch(h){this.reject_(h);return}"function"==typeof g?
-this.settleSameAsThenable_(g,c):this.fulfill_(c)};d.prototype.reject_=function(c){this.settle_(2,c)};d.prototype.fulfill_=function(c){this.settle_(1,c)};d.prototype.settle_=function(c,g){if(0!=this.state_)throw Error("Cannot settle("+c+", "+g+"): Promise already settled in state"+this.state_);this.state_=c;this.result_=g;this.executeOnSettledCallbacks_()};d.prototype.executeOnSettledCallbacks_=function(){if(null!=this.onSettledCallbacks_){for(var c=0;c<this.onSettledCallbacks_.length;++c)k.asyncExecute(this.onSettledCallbacks_[c]);
-this.onSettledCallbacks_=null}};var k=new b;d.prototype.settleSameAsPromise_=function(c){var g=this.createResolveAndReject_();c.callWhenSettled_(g.resolve,g.reject)};d.prototype.settleSameAsThenable_=function(c,g){var h=this.createResolveAndReject_();try{c.call(g,h.resolve,h.reject)}catch(l){h.reject(l)}};d.prototype.then=function(c,g){function h(q,n){return"function"==typeof q?function(p){try{l(q(p))}catch(r){m(r)}}:n}var l,m,t=new d(function(q,n){l=q;m=n});this.callWhenSettled_(h(c,l),h(g,m));return t};
-d.prototype.catch=function(c){return this.then(void 0,c)};d.prototype.callWhenSettled_=function(c,g){function h(){switch(l.state_){case 1:c(l.result_);break;case 2:g(l.result_);break;default:throw Error("Unexpected state: "+l.state_);}}var l=this;null==this.onSettledCallbacks_?k.asyncExecute(h):this.onSettledCallbacks_.push(h)};d.resolve=e;d.reject=function(c){return new d(function(g,h){h(c)})};d.race=function(c){return new d(function(g,h){for(var l=$jscomp.makeIterator(c),m=l.next();!m.done;m=l.next())e(m.value).callWhenSettled_(g,
-h)})};d.all=function(c){var g=$jscomp.makeIterator(c),h=g.next();return h.done?e([]):new d(function(l,m){function t(p){return function(r){q[p]=r;n--;0==n&&l(q)}}var q=[],n=0;do q.push(void 0),n++,e(h.value).callWhenSettled_(t(q.length-1),m),h=g.next();while(!h.done)})};return d},"es6","es3");
-$jscomp.polyfill("Array.from",function(a){return a?a:function(b,e,f){e=null!=e?e:function(g){return g};var d=[],k="undefined"!=typeof Symbol&&Symbol.iterator&&b[Symbol.iterator];if("function"==typeof k){b=k.call(b);for(var c=0;!(k=b.next()).done;)d.push(e.call(f,k.value,c++))}else for(k=b.length,c=0;c<k;c++)d.push(e.call(f,b[c],c));return d}},"es6","es3");
-$jscomp.findInternal=function(a,b,e){a instanceof String&&(a=String(a));for(var f=a.length,d=0;d<f;d++){var k=a[d];if(b.call(e,k,d,a))return{i:d,v:k}}return{i:-1,v:void 0}};$jscomp.polyfill("Array.prototype.findIndex",function(a){return a?a:function(b,e){return $jscomp.findInternal(this,b,e).i}},"es6","es3");$jscomp.polyfill("Array.prototype.find",function(a){return a?a:function(b,e){return $jscomp.findInternal(this,b,e).v}},"es6","es3");
-document.addEventListener("DOMContentLoaded",function(){var a=document.querySelector("#js-AutoComplete-input"),b=document.querySelector("#js-AutoComplete-parent"),e=function(){b.setAttribute("aria-expanded",!1);a.removeAttribute("aria-activedescendant")},f=function(){b.setAttribute("aria-expanded",!0)};a.addEventListener("blur",e);a.addEventListener("focus",f);new autoComplete({data:{src:function(){var d,k;return $jscomp.asyncExecutePromiseGeneratorProgram(function(c){return 1==c.nextAddress?(d=a.value,
-c.yield(fetch("/autocomplete?q="+d),2)):3!=c.nextAddress?(k=c.yieldResult,c.yield(k.json(),3)):c.return(c.yieldResult)})},key:["PackagePath"],cache:!1},threshold:1,debounce:100,resultsList:{render:!0,container:function(d){d.setAttribute("id","AutoComplete-list");d.classList.add("AutoComplete-list");d.setAttribute("role","listbox")},destination:document.querySelector("#js-AutoComplete-parent"),position:"beforeend",element:"ul",navigation:function(d,k,c,g,h){var l=Array.from(c.childNodes),m=void 0,
-t=function(n){m&&m.removeAttribute("aria-selected");m=n;m.setAttribute("aria-selected","true");n=l.findIndex(function(p){return p===m});k.setAttribute("aria-activedescendant","AutoComplete-item-"+n)},q=function(n,p){g({event:n,query:k.value,matches:h.matches,results:h.list.map(function(r){return r.value}),selection:h.list.find(function(r){return r.index===Number(p.getAttribute("data-id"))})});e()};k.onkeydown=function(n){if(0<l.length)switch(n.keyCode){case 38:f();var p=l[l.length-1];m&&m.previousSibling&&
-(p=m.previousSibling);t(p);n.preventDefault();break;case 40:f();p=l[0];m&&m.nextSibling&&(p=m.nextSibling);t(p);n.preventDefault();break;case 13:m&&(n.preventDefault(),q(n,m));break;case 27:e();k.value="";break;default:f()}};l.forEach(function(n,p){n.setAttribute("id","AutoComplete-item-"+p);n.onmousedown=function(r){q(r,r.currentTarget);r.preventDefault()}})}},highlight:!0,selector:"#js-AutoComplete-input",onSelection:function(d){d.selection.value.PackagePath&&(window.location.href="/"+d.selection.value.PackagePath)}})});
diff --git a/go.mod b/go.mod
index 8c954f1..a55f0ff 100644
--- a/go.mod
+++ b/go.mod
@@ -43,7 +43,6 @@
 	golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208
 	golang.org/x/sys v0.0.0-20200922070232-aee5d888a860 // indirect
 	golang.org/x/text v0.3.4 // indirect
-	golang.org/x/time v0.0.0-20191024005414-555d28b269f0
 	google.golang.org/api v0.32.0
 	google.golang.org/genproto v0.0.0-20200923140941-5646d36feee1
 	google.golang.org/grpc v1.32.0
diff --git a/internal/complete/completion.go b/internal/complete/completion.go
deleted file mode 100644
index 01a3987..0000000
--- a/internal/complete/completion.go
+++ /dev/null
@@ -1,127 +0,0 @@
-// Copyright 2019 The Go Authors. All rights reserved.
-// Use of this source code is governed by a BSD-style
-// license that can be found in the LICENSE file.
-
-// Package complete defines a Completion type that is used in auto-completion,
-// along with Encode and Decode methods that can be used for storing this type
-// in redis.
-package complete
-
-import (
-	"fmt"
-	"strconv"
-	"strings"
-
-	"golang.org/x/pkgsite/internal/derrors"
-)
-
-const keySep = "|"
-
-// Redis keys for completion sorted sets ("indexes"). They are in this package
-// so that they can be accessed by both worker and frontend.
-const (
-	KeyPrefix    = "completions"
-	PopularKey   = KeyPrefix + "Popular"
-	RemainingKey = KeyPrefix + "Rest"
-)
-
-// Completion holds package data from an auto-completion match.
-type Completion struct {
-	// Suffix is the path suffix that matched the compltion input, e.g. a query
-	// for "error" would match the suffix "errors" of "github.com/pkg/errors".
-	Suffix string
-	// ModulePath is the module path of the completion match. We may support
-	// matches of the same path in different modules.
-	ModulePath string
-	// Version is the module version of the completion entry.
-	Version string
-	// PackagePath is the full import path.
-	PackagePath string
-	// Importers is the number of importers of this package. It is used for
-	// sorting completion results.
-	Importers int
-}
-
-// Encode string-encodes a completion for storing in the completion index.
-func (c Completion) Encode() string {
-	return strings.Join(c.keyData(), keySep)
-}
-
-func (c Completion) keyData() []string {
-	var suffix string
-	if strings.HasPrefix(c.PackagePath, c.ModulePath) {
-		suffix = strings.TrimPrefix(c.PackagePath, c.ModulePath)
-		suffix = "/" + strings.Trim(suffix, "/")
-	} else {
-		// In the case of the standard library, ModulePath will not be a prefix of
-		// PackagePath.
-		suffix = c.PackagePath
-	}
-	return []string{
-		c.Suffix,
-		strings.TrimRight(c.ModulePath, "/"),
-		c.Version,
-		suffix,
-		// It's important that importers is last in this key, since it is the only
-		// datum that changes. By having it last, we reserve the ability to
-		// selectively update this entry by deleting the prefix corresponding to
-		// the values above.
-		strconv.Itoa(c.Importers),
-	}
-}
-
-// Decode parses a completion entry from the completions index.
-func Decode(entry string) (_ *Completion, err error) {
-	defer derrors.Wrap(&err, "complete.Decode(%q)", entry)
-	parts := strings.Split(entry, "|")
-	if len(parts) != 5 {
-		return nil, fmt.Errorf("got %d parts, want 5", len(parts))
-	}
-	c := &Completion{
-		Suffix:     parts[0],
-		ModulePath: parts[1],
-		Version:    parts[2],
-	}
-	suffix := parts[3]
-	if strings.HasPrefix(suffix, "/") {
-		c.PackagePath = strings.Trim(c.ModulePath+suffix, "/")
-	} else {
-		c.PackagePath = suffix
-	}
-	importers, err := strconv.Atoi(parts[4])
-	if err != nil {
-		return nil, fmt.Errorf("error parsing importers: %v", err)
-	}
-	c.Importers = importers
-	return c, nil
-}
-
-// PathCompletions generates completion entries for all possible suffixes of
-// partial.PackagePath.
-func PathCompletions(partial Completion) []*Completion {
-	suffs := pathSuffixes(partial.PackagePath)
-	var cs []*Completion
-	for _, pref := range suffs {
-		var next = partial
-		next.Suffix = pref
-		cs = append(cs, &next)
-	}
-	return cs
-}
-
-// pathSuffixes returns a slice of all path suffixes of a '/'-separated path,
-// including the full path itself. i.e.
-//   pathSuffixes("foo/bar") = []string{"foo/bar", "bar"}
-func pathSuffixes(path string) []string {
-	path = strings.ToLower(path)
-	var prefs []string
-	for len(path) > 0 {
-		prefs = append(prefs, path)
-		i := strings.Index(path, "/")
-		if i < 0 {
-			break
-		}
-		path = path[i+1:]
-	}
-	return prefs
-}
diff --git a/internal/complete/completion_test.go b/internal/complete/completion_test.go
deleted file mode 100644
index 377386c..0000000
--- a/internal/complete/completion_test.go
+++ /dev/null
@@ -1,87 +0,0 @@
-// Copyright 2019 The Go Authors. All rights reserved.
-// Use of this source code is governed by a BSD-style
-// license that can be found in the LICENSE file.
-
-package complete
-
-import (
-	"sort"
-	"testing"
-
-	"github.com/google/go-cmp/cmp"
-)
-
-func TestEncodeDecode(t *testing.T) {
-	completions := []Completion{
-		{},
-		{Version: "foo"},
-		{Importers: 42},
-		{Suffix: "foo"},
-		{ModulePath: "foo"},
-		{PackagePath: "github.com/foo/bar/baz"},
-		{
-			Suffix:      "github.com/foo",
-			ModulePath:  "github.com/foo/bar",
-			Version:     "v1.2.3",
-			PackagePath: "github.com/foo/bar/baz",
-			Importers:   101,
-		},
-		{
-			Suffix:      "fmt",
-			ModulePath:  "std",
-			Version:     "go1.13",
-			PackagePath: "fmt",
-			Importers:   1234,
-		},
-	}
-	for _, c := range completions {
-		encoded := c.Encode()
-		decoded, err := Decode(encoded)
-		if err != nil {
-			t.Fatal(err)
-		}
-		if diff := cmp.Diff(c, *decoded); diff != "" {
-			t.Errorf("[%#v] decoded mismatch (-initial +decoded):\n%s\nencoded: %q", c, diff, encoded)
-		}
-	}
-}
-
-func TestPathCompletions(t *testing.T) {
-	partial := Completion{
-		ModulePath:  "my.module/foo",
-		PackagePath: "my.module/foo/bar",
-		Version:     "v1.2.3",
-		Importers:   123,
-	}
-	completions := PathCompletions(partial)
-	sort.Slice(completions, func(i, j int) bool {
-		return len(completions[i].Suffix) < len(completions[j].Suffix)
-	})
-	wantSuffixes := []string{"bar", "foo/bar", "my.module/foo/bar"}
-	if got, want := len(completions), len(wantSuffixes); got != want {
-		t.Fatalf("len(pathCompletions(%v)) = %d, want %d", partial, got, want)
-	}
-	for i, got := range completions {
-		want := partial
-		want.Suffix = wantSuffixes[i]
-		if diff := cmp.Diff(want, *got); diff != "" {
-			t.Errorf("completions[%d] mismatch (-want +got)\n%s", i, diff)
-		}
-	}
-}
-
-func TestPathSuffixes(t *testing.T) {
-	tests := []struct {
-		path string
-		want []string
-	}{
-		{"foo/Bar/baz", []string{"foo/bar/baz", "bar/baz", "baz"}},
-		{"foo", []string{"foo"}},
-		{"BAR", []string{"bar"}},
-	}
-	for _, test := range tests {
-		if got := pathSuffixes(test.path); !cmp.Equal(got, test.want) {
-			t.Errorf("prefixes(%q) = %v, want %v", test.path, got, test.want)
-		}
-	}
-}
diff --git a/internal/experiment.go b/internal/experiment.go
index ee2d62d..73abee8 100644
--- a/internal/experiment.go
+++ b/internal/experiment.go
@@ -6,7 +6,6 @@
 package internal
 
 const (
-	ExperimentAutocomplete       = "autocomplete"
 	ExperimentGetUnitMetaQuery   = "get-unit-meta-query"
 	ExperimentGoldmark           = "goldmark"
 	ExperimentReadmeOutline      = "readme-outline"
@@ -16,7 +15,6 @@
 // Experiments represents all of the active experiments in the codebase and
 // a description of each experiment.
 var Experiments = map[string]string{
-	ExperimentAutocomplete:       "Enable autocomplete with search.",
 	ExperimentGetUnitMetaQuery:   "Enable the new get unit meta query, which reads from the paths table.",
 	ExperimentGoldmark:           "Enable the usage of rendering markdown using goldmark instead of blackfriday.",
 	ExperimentReadmeOutline:      "Enable the readme outline in the side nav.",
diff --git a/internal/frontend/completion.go b/internal/frontend/completion.go
deleted file mode 100644
index 9123bf9..0000000
--- a/internal/frontend/completion.go
+++ /dev/null
@@ -1,181 +0,0 @@
-// Copyright 2019 The Go Authors. All rights reserved.
-// Use of this source code is governed by a BSD-style
-// license that can be found in the LICENSE file.
-
-package frontend
-
-import (
-	"bytes"
-	"context"
-	"encoding/json"
-	"io"
-	"net/http"
-	"sort"
-	"strings"
-
-	"github.com/go-redis/redis/v8"
-	"golang.org/x/pkgsite/internal/complete"
-	"golang.org/x/pkgsite/internal/derrors"
-	"golang.org/x/pkgsite/internal/log"
-)
-
-// handleAutoCompletion handles requests for /autocomplete?q=<input prefix>, by
-// querying redis sorted sets indexing package paths.
-func (s *Server) handleAutoCompletion(w http.ResponseWriter, r *http.Request) {
-	ctx := r.Context()
-	var completions []*complete.Completion
-	if s.cmplClient != nil {
-		var err error
-		q := r.FormValue("q")
-		completions, err = doCompletion(r.Context(), s.cmplClient, strings.ToLower(q), 5)
-		if err != nil {
-			code := http.StatusInternalServerError
-			http.Error(w, http.StatusText(code), code)
-			return
-		}
-	}
-	if completions == nil {
-		// autocomplete.js complains if the JSON returned by this endpoint is null,
-		// so we initialize a non-nil empty array to serialize to an empty JSON
-		// array.
-		completions = []*complete.Completion{}
-	}
-	response, err := json.Marshal(completions)
-	if err != nil {
-		log.Errorf(ctx, "error marshalling completion: json.Marshal: %v", err)
-	}
-	w.Header().Set("Content-Type", "application/json")
-	if _, err := io.Copy(w, bytes.NewReader(response)); err != nil {
-		log.Errorf(ctx, "Error copying json buffer to ResponseWriter: %v", err)
-	}
-}
-
-// scoredCompletion wraps Completions with a relevancy score, so that they can
-// be sorted.
-type scoredCompletion struct {
-	c     *complete.Completion
-	score int
-}
-
-// doCompletion executes the completion query against redis. This is inspired
-// by http://oldblog.antirez.com/post/autocomplete-with-redis.html, but
-// improved as follows:
-//  + Use ZRANGEBYLEX to avoid storing each possible prefix, since that was
-//    added to Redis since the original blog post.
-//  + Use an additional sorted set that holds popular packages, to improve
-//    completion relevancy.
-//
-// We autocomplete the query 'q' as follows
-//  1. Query for popular completions starting with q using ZRANGEBYLEX (more
-//     details on this below). We fetch an arbitrary number of results (1000)
-//     to bound the amount of work done by redis.
-//  2. Sort the returned completions by our score (a mix of popularity and
-//     proximity to the end of the import path), and filter to the top
-//     maxResults.
-//  3. If we have maxResults results, we're done. Otherwise do (1) on the index
-//     of remaining (unpopular) package paths, add to our result set, and sort
-//     again (because unpopular packages might actually score higher than
-//     popular packages).
-func doCompletion(ctx context.Context, r *redis.Client, q string, maxResults int) (_ []*complete.Completion, err error) {
-	defer derrors.Wrap(&err, "doCompletion(%q, %d)", q, maxResults)
-	scored, err := completeWithIndex(ctx, r, q, complete.PopularKey, maxResults)
-	if err != nil {
-		return nil, err
-	}
-	if len(scored) < maxResults {
-		unpopular, err := completeWithIndex(ctx, r, q, complete.RemainingKey, maxResults-len(scored))
-		if err != nil {
-			return nil, err
-		}
-		scored = append(scored, unpopular...)
-		// Re-sort, as it is possible that an unpopular completion actually has a
-		// higher score than a popular completion due to the weighting for suffix
-		// length.
-		sort.Slice(scored, func(i, j int) bool {
-			return scored[i].score > scored[j].score
-		})
-	}
-	var completions []*complete.Completion
-	for _, s := range scored {
-		completions = append(completions, s.c)
-	}
-	return completions, nil
-}
-
-func completeWithIndex(ctx context.Context, r *redis.Client, q, indexKey string, maxResults int) (_ []*scoredCompletion, err error) {
-	defer derrors.Wrap(&err, "completeWithIndex(%q, %q, %d)", q, indexKey, maxResults)
-
-	// Query for possible completions using ZRANGEBYLEX. See documentation at
-	// https://redis.io/commands/zrangebylex
-	// Notably, the "(" character in the Min and Max fields means 'exclude this
-	// endpoint'.
-	// We bound our search in two ways: (1) by setting Max to the smallest string
-	// that lexically greater than q but does not start with q, and (2) by
-	// setting an arbitrary limit of 1000 results.
-	entries, err := r.ZRangeByLex(ctx, indexKey, &redis.ZRangeBy{
-		Min:   "(" + q,
-		Max:   "(" + nextPrefix(q),
-		Count: 1000,
-	}).Result()
-	var scored []*scoredCompletion
-	for _, entry := range entries {
-		c, err := complete.Decode(entry)
-		if err != nil {
-			return nil, err
-		}
-		offset := len(strings.Split(entry, "/"))
-		s := &scoredCompletion{
-			c: c,
-			// Weight importers by distance of the matching text from the end of the
-			// import path. This is done in an attempt to make results more relevant
-			// the closer the match is to the end of the import path. For example, if
-			// the user types 'net', we should have some preference for 'net' over
-			// 'net/http'. In this case, it actually works out like so:
-			//  - net has ~68000 importers
-			//  - net/http has ~130000 importers
-			//
-			// So the score of 'net' is ~68000 (offset=1), and the score of
-			// 'net/http' is ~65000 (130K/2, as offset=2), therefore net should be
-			// sorted above 'net/http' in the results.
-			//
-			// This heuristic is a total guess, but since this is just autocomplete
-			// it probably doesn't matter much. In testing, it felt like autocomplete
-			// was completing the packages I wanted.
-			//
-			// The `- offset` term is added to break ties in the case where all
-			// completion results have 0 importers.
-			score: c.Importers/offset - offset,
-		}
-		scored = append(scored, s)
-	}
-	// sort by score descending
-	sort.Slice(scored, func(i, j int) bool {
-		return scored[i].score > scored[j].score
-	})
-	if len(scored) > maxResults {
-		scored = scored[:maxResults]
-	}
-	return scored, nil
-}
-
-// nextPrefix returns the first string (according to lexical sorting) that is
-// greater than prefix but does not start with prefix.
-func nextPrefix(prefix string) string {
-	// redis strings are ASCII. Note that among printing ASCII characters '!' has
-	// the smallest byte value and '~' has the largest byte value. It also so
-	// happens that these are both valid characters in a URL.
-	if prefix == "" {
-		return ""
-	}
-	lastChar := prefix[len(prefix)-1]
-	if lastChar >= '~' {
-		// If the last character is '~', there is no greater ascii character so we
-		// must move to the previous character to find a lexically greater string
-		// that doesn't start with prefix. Note that in the degenerate case where
-		// prefix is nothing but twiddles (e.g. "~~~"), we will recurse until we return "",
-		// which is acceptable: there is no prefix that satisfies our requirements:
-		// all strings greater than "~~~" must also start with "~~~"
-		return nextPrefix(prefix[:len(prefix)-1])
-	}
-	return prefix[:len(prefix)-1] + string(lastChar+1)
-}
diff --git a/internal/frontend/completion_test.go b/internal/frontend/completion_test.go
deleted file mode 100644
index 87403e8..0000000
--- a/internal/frontend/completion_test.go
+++ /dev/null
@@ -1,96 +0,0 @@
-// Copyright 2019 The Go Authors. All rights reserved.
-// Use of this source code is governed by a BSD-style
-// license that can be found in the LICENSE file.
-
-package frontend
-
-import (
-	"context"
-	"testing"
-
-	"github.com/alicebob/miniredis/v2"
-	"github.com/go-redis/redis/v8"
-	"github.com/google/go-cmp/cmp"
-	"golang.org/x/pkgsite/internal/complete"
-)
-
-func TestAutoCompletion(t *testing.T) {
-	ctx := context.Background()
-	s, err := miniredis.Run()
-	if err != nil {
-		t.Fatal(err)
-	}
-	defer s.Close()
-	r := redis.NewClient(&redis.Options{Addr: s.Addr()})
-	// For convenence, populate completion data based on detail path -> imports.
-	pathData := map[string]int{
-		"foo.com/bar@v1.2.3/baz":          123,
-		"foo.com/quux@v1.0.0/bark":        10,
-		"github.com/something@v2.0.0/foo": 80,
-	}
-	for k, v := range pathData {
-		got, err := parseDetailsURLPath(k)
-		if err != nil {
-			t.Fatal(err)
-		}
-		partial := complete.Completion{
-			PackagePath: got.fullPath,
-			ModulePath:  got.modulePath,
-			Version:     got.requestedVersion,
-			Importers:   v,
-		}
-		completions := complete.PathCompletions(partial)
-		var zs []*redis.Z
-		for _, cmpl := range completions {
-			zs = append(zs, &redis.Z{Member: cmpl.Encode()})
-		}
-		if v > 0 {
-			r.ZAdd(ctx, complete.PopularKey, zs...)
-		} else {
-			r.ZAdd(ctx, complete.RemainingKey, zs...)
-		}
-	}
-	ctx, cancel := context.WithTimeout(context.Background(), testTimeout)
-	defer cancel()
-
-	tests := []struct {
-		q    string
-		want []string
-	}{
-		{"baz", []string{"foo.com/bar/baz"}},
-		{"bar", []string{"foo.com/bar/baz", "foo.com/quux/bark"}},
-		{"foo", []string{"github.com/something/foo", "foo.com/bar/baz"}},
-	}
-	for _, test := range tests {
-		t.Run(test.q, func(t *testing.T) {
-			results, err := doCompletion(ctx, r, test.q, 2)
-			if err != nil {
-				t.Fatal(err)
-			}
-			var got []string
-			for _, res := range results {
-				got = append(got, res.PackagePath)
-			}
-			if diff := cmp.Diff(test.want, got); diff != "" {
-				t.Errorf("doCompletion(%q) mismatch (-want +got)\n%s", test.q, diff)
-			}
-		})
-	}
-}
-
-func TestNextPrefix(t *testing.T) {
-	tests := []struct {
-		prefix, want string
-	}{
-		{"", ""},
-		{"~~~", ""},
-		{"aa", "ab"},
-		{"aB", "aC"},
-		{"a~", "b"},
-	}
-	for _, test := range tests {
-		if got := nextPrefix(test.prefix); got != test.want {
-			t.Errorf("nextPrefix(%q) = %q, want %q", test.prefix, got, test.want)
-		}
-	}
-}
diff --git a/internal/frontend/server.go b/internal/frontend/server.go
index c2151b7..f841446 100644
--- a/internal/frontend/server.go
+++ b/internal/frontend/server.go
@@ -147,7 +147,6 @@
 		handle("/detail-stats/",
 			middleware.Stats()(http.StripPrefix("/detail-stats", s.errorHandler(s.serveDetails))))
 	}
-	handle("/autocomplete", http.HandlerFunc(s.handleAutoCompletion))
 	handle("/robots.txt", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
 		w.Header().Set("Content-Type", "text/plain; charset=utf-8")
 		http.ServeContent(w, r, "", time.Time{}, strings.NewReader(`User-agent: *
diff --git a/internal/middleware/secureheaders.go b/internal/middleware/secureheaders.go
index 24b6704..2fe0d23 100644
--- a/internal/middleware/secureheaders.go
+++ b/internal/middleware/secureheaders.go
@@ -13,7 +13,6 @@
 var scriptHashes = []string{
 	// From content/static/html/base.tmpl
 	"'sha256-CgM7SjnSbDyuIteS+D1CQuSnzyKwL0qtXLU6ZW2hB+g='",
-	"'sha256-qPGTOKPn+niRiNKQIEX0Ktwuj+D+iPQWIxnlhPicw58='",
 	"'sha256-LIQd8c4GSueKwR3q2fz3AB92cOdy2Ld7ox8pfvMPHns='",
 	"'sha256-dwce5DnVX7uk6fdvvNxQyLTH/cJrTMDK6zzrdKwdwcg='",
 	// From content/static/html/pages/badge.tmpl
diff --git a/internal/worker/completion.go b/internal/worker/completion.go
deleted file mode 100644
index 2cde613..0000000
--- a/internal/worker/completion.go
+++ /dev/null
@@ -1,185 +0,0 @@
-// Copyright 2019 The Go Authors. All rights reserved.
-// Use of this source code is governed by a BSD-style
-// license that can be found in the LICENSE file.
-
-package worker
-
-import (
-	"context"
-	"database/sql"
-	"errors"
-	"fmt"
-	"net/http"
-	"time"
-
-	"github.com/go-redis/redis/v8"
-	"golang.org/x/pkgsite/internal/complete"
-	"golang.org/x/pkgsite/internal/database"
-	"golang.org/x/pkgsite/internal/derrors"
-	"golang.org/x/pkgsite/internal/log"
-)
-
-const popularCutoff = 50
-
-// handleUpdateRedisIndexes scans recently modified search documents, and
-// updates redis auto completion indexes with data from these documents.
-func (s *Server) handleUpdateRedisIndexes(w http.ResponseWriter, r *http.Request) error {
-	ctx := r.Context()
-	err := updateRedisIndexes(ctx, s.db.Underlying(), s.redisHAClient, popularCutoff)
-	if err != nil {
-		return err
-	}
-	fmt.Fprint(w, "OK")
-	return nil
-}
-
-// updateRedisIndexes updates redisClient with autocompletion data from db.
-// cutoff specifies the number of importers at which a package is considered
-// popular, and is passed-in as an argument to facilitate testing.
-func updateRedisIndexes(ctx context.Context, db *database.DB, redisClient *redis.Client, cutoff int) (err error) {
-	defer derrors.Wrap(&err, "updateRedisIndexes")
-	if redisClient == nil {
-		return errors.New("redis HA client is nil")
-	}
-
-	// For autocompletion, we track two separate "indexes" (sorted sets of
-	// package path suffixes): one for popular packages, and one for the
-	// remainder, as defined by the popularCutoff const.  This allows us to
-	// suggest popular completions, even when the user input is short (i.e. we
-	// want to suggest 'fmt' when the user types 'f', but don't want to scan all
-	// completions that start with the letter 'f').
-	//
-	// This function scans search documents in the database and builds up a
-	// pipeline that writes these two sorted sets to Redis, using timestamped
-	// temporary keys, and then renames them to the keys used by the frontend for
-	// autocompletion.
-	//
-	// See https://redis.io/commands/rename for more information on renaming:
-	// it's unclear whether renaming is atomic, but we don't really care.
-	// Populating these indexes currently takes 1-2 minutes, and renaming takes
-	// 1-2 seconds. Even if completions are broken during this 1-2 seconds, it's
-	// preferable to them being broken for 1-2 minutes. We could do something
-	// more clever, such as updating the completion data in place using
-	// ZREMRANGEBYLEX followed by ZADD, but that would be significantly more
-	// complicated.
-	//
-	// One additional concern of this operation is that we temporary double the
-	// size of our redis database while we're staging the new completion data.
-	// That's fine, but it's dangerous if we ever have a bug and this operation
-	// was either not cleaned up properly, or run concurrently. In light of this,
-	// we first look for evidence of another update operation currently running,
-	// by scanning Redis for keys that match the temporary key pattern.
-
-	// Check for an ongoing update operation, as described above.
-	tempKeyPattern := fmt.Sprintf("%s*-*", complete.KeyPrefix)
-	existing, _, err := redisClient.Scan(ctx, 0, tempKeyPattern, 1).Result()
-	if err != nil {
-		return fmt.Errorf(`redis error: Scan(%q): %v`, tempKeyPattern, err)
-	}
-	if len(existing) > 0 {
-		return fmt.Errorf("found existing in-progress completion index: %v", existing[0])
-	}
-
-	// Use temporary timestamped keys while we write the completion data, as it
-	// can take ~minutes.
-	keyPop := fmt.Sprintf("%s-%s", complete.PopularKey, time.Now().Format(time.RFC3339))
-	keyRem := fmt.Sprintf("%s-%s", complete.RemainingKey, time.Now().Format(time.RFC3339))
-
-	// Always clean up: DEL succeeds even if the keys have been renamed.
-	defer func() {
-		if _, err := redisClient.Del(ctx, keyPop).Result(); err != nil {
-			log.Errorf(ctx, "redisClient.Del(%q): %v", keyPop, err)
-		}
-		if _, err := redisClient.Del(ctx, keyRem).Result(); err != nil {
-			log.Errorf(ctx, "redisClient.Del(%q): %v", keyRem, err)
-		}
-	}()
-
-	// pipeSize tracks the number of ZADD statements in the pipe.
-	pipeSize := 0
-	pipe := redisClient.Pipeline()
-	defer pipe.Close()
-	// flush executes the current pipeline and resets its state.
-	flush := func() error {
-		log.Infof(ctx, "Writing completion data pipeline of size %d.", pipeSize)
-		if _, err := pipe.Exec(ctx); err != nil {
-			return fmt.Errorf("redis error: pipe.Exec: %v", err)
-		}
-		// As of writing this is unnecessary as ExecContext resets the pipeline
-		// commands, but since this is not documented functionality we explicitly
-		// Discard.
-		pipe.Discard()
-		pipeSize = 0
-		return nil
-	}
-	// Track whether or not we have any entries in the popular or remaining
-	// indexes. This is an edge case, but if we don't insert any entries for a
-	// given index the key won't exist and we'll get an error when renaming.
-	var (
-		haveRemaining bool
-		havePopular   bool
-	)
-	// As of writing there were around 5M entries in our index, so writing in
-	// batches of 1M should result in ~6 batches.
-	const batchSize = 1e6
-	// processRow builds up a Redis pipeline as we scan the search_documents
-	// table.
-	processRow := func(rows *sql.Rows) error {
-		var partial complete.Completion
-		if err := rows.Scan(&partial.PackagePath, &partial.ModulePath, &partial.Version, &partial.Importers); err != nil {
-			return fmt.Errorf("rows.Scan: %v", err)
-		}
-		cmpls := complete.PathCompletions(partial)
-		var zs []*redis.Z
-		for _, cmpl := range cmpls {
-			zs = append(zs, &redis.Z{Member: cmpl.Encode()})
-		}
-		switch {
-		case partial.Importers >= cutoff:
-			havePopular = true
-			pipe.ZAdd(ctx, keyPop, zs...)
-		default:
-			haveRemaining = true
-			pipe.ZAdd(ctx, keyRem, zs...)
-		}
-		pipeSize += len(zs)
-		if pipeSize > batchSize {
-			if err := flush(); err != nil {
-				return err
-			}
-		}
-		return nil
-	}
-
-	// Here we use the *database.DB rather than a function on postgres.DB,
-	// so that we can write to our redis pipeline while we stream results from
-	// the DB. Otherwise, we would have to:
-	//  - add a method on postgres.DB for the trivial query above
-	//  - add a type (or reuse SearchResult) to hold the subset of search
-	//    document data used here.
-	//  - hold two copies of all search results in memory while building the
-	//    redis pipeline below.
-	query := `
-		SELECT package_path, module_path, version, imported_by_count
-		FROM search_documents`
-	if err := db.RunQuery(ctx, query, processRow); err != nil {
-		return err
-	}
-	if err := flush(); err != nil {
-		return err
-	}
-	pipe.Close()
-	if havePopular {
-		log.Infof(ctx, "Renaming %q to %q", keyPop, complete.PopularKey)
-		if _, err := redisClient.Rename(ctx, keyPop, complete.PopularKey).Result(); err != nil {
-			return fmt.Errorf(`redis error: Rename(%q, %q): %v`, keyPop, complete.PopularKey, err)
-		}
-	}
-	if haveRemaining {
-		log.Infof(ctx, "Renaming %q to %q", keyRem, complete.RemainingKey)
-		if _, err := redisClient.Rename(ctx, keyRem, complete.RemainingKey).Result(); err != nil {
-			return fmt.Errorf(`redis error: Rename(%q, %q): %v`, keyRem, complete.RemainingKey, err)
-		}
-	}
-	return nil
-}
diff --git a/internal/worker/completion_test.go b/internal/worker/completion_test.go
deleted file mode 100644
index e6388e8..0000000
--- a/internal/worker/completion_test.go
+++ /dev/null
@@ -1,63 +0,0 @@
-// Copyright 2019 The Go Authors. All rights reserved.
-// Use of this source code is governed by a BSD-style
-// license that can be found in the LICENSE file.
-
-package worker
-
-import (
-	"context"
-	"testing"
-
-	"github.com/alicebob/miniredis/v2"
-	"github.com/go-redis/redis/v8"
-	"golang.org/x/pkgsite/internal/complete"
-	"golang.org/x/pkgsite/internal/postgres"
-	"golang.org/x/pkgsite/internal/testing/sample"
-)
-
-func TestUpdateRedisIndexes(t *testing.T) {
-	ctx, cancel := context.WithTimeout(context.Background(), testTimeout)
-	defer cancel()
-	defer postgres.ResetTestDB(testDB, t)
-	mr, err := miniredis.Run()
-	if err != nil {
-		t.Fatal(err)
-	}
-	defer mr.Close()
-
-	// Set up a simple test case with two module versions and two packages. The
-	// package in v2 imports the package in v1. By setting our 'popular cutoff'
-	// to 1, we can force the package in v1 to be considered popular.
-	rc := redis.NewClient(&redis.Options{Addr: mr.Addr()})
-	m1 := sample.Module("github.com/something", sample.VersionString, "apples/bananas")
-	m2 := sample.Module("github.com/something/else", sample.VersionString, "oranges/bananas")
-	m2.Units[1].Imports = []string{m1.Units[1].Path}
-	if err := testDB.InsertModule(ctx, m1); err != nil {
-		t.Fatal(err)
-	}
-	if err := testDB.InsertModule(ctx, m2); err != nil {
-		t.Fatal(err)
-	}
-	if _, err := testDB.UpdateSearchDocumentsImportedByCount(ctx); err != nil {
-		t.Fatal(err)
-	}
-	if err := updateRedisIndexes(ctx, testDB.Underlying(), rc, 1); err != nil {
-		t.Fatal(err)
-	}
-	popCount, err := rc.ZCount(ctx, complete.PopularKey, "0", "0").Result()
-	if err != nil {
-		t.Fatal(err)
-	}
-	// There are 4 path components in github.com/something/apples/bananas
-	if popCount != 4 {
-		t.Errorf("got %d popular autocompletions, want %d", popCount, 4)
-	}
-	remCount, err := rc.ZCount(ctx, complete.RemainingKey, "0", "0").Result()
-	if err != nil {
-		t.Fatal(err)
-	}
-	// There are 5 path components in github.com/something/else/oranges/bananas
-	if remCount != 5 {
-		t.Errorf("got %d remaining autocompletions, want %d", remCount, 5)
-	}
-}
diff --git a/internal/worker/server.go b/internal/worker/server.go
index 52d7ded..08ac50a 100644
--- a/internal/worker/server.go
+++ b/internal/worker/server.go
@@ -138,10 +138,6 @@
 	// This endpoint is intended to be invoked periodically by a scheduler.
 	handle("/update-imported-by-count", rmw(s.errorHandler(s.handleUpdateImportedByCount)))
 
-	// scheduled: download search document data and update the redis sorted
-	// set(s) used in auto-completion.
-	handle("/update-redis-indexes", rmw(s.errorHandler(s.handleUpdateRedisIndexes)))
-
 	// task-queue: fetch fetches a module version from the Module Mirror, and
 	// processes the contents, and inserts it into the database. If a fetch
 	// request fails for any reason other than an http.StatusInternalServerError,