tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

UrlbarProviderSemanticHistorySearch.sys.mjs (8841B)


      1 /* This Source Code Form is subject to the terms of the Mozilla Public
      2 * License, v. 2.0. If a copy of the MPL was not distributed with this
      3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
      4 
      5 /**
      6 * This module exports a provider that offers search history suggestions
      7 * based on embeddings and semantic search techniques using semantic
      8 * history
      9 */
     10 
     11 import {
     12  UrlbarProvider,
     13  UrlbarUtils,
     14 } from "moz-src:///browser/components/urlbar/UrlbarUtils.sys.mjs";
     15 
     16 const lazy = {};
     17 
     18 ChromeUtils.defineESModuleGetters(lazy, {
     19  EnrollmentType: "resource://nimbus/ExperimentAPI.sys.mjs",
     20  NimbusFeatures: "resource://nimbus/ExperimentAPI.sys.mjs",
     21  PlacesUtils: "resource://gre/modules/PlacesUtils.sys.mjs",
     22  UrlbarPrefs: "moz-src:///browser/components/urlbar/UrlbarPrefs.sys.mjs",
     23  UrlbarProviderOpenTabs:
     24    "moz-src:///browser/components/urlbar/UrlbarProviderOpenTabs.sys.mjs",
     25  UrlbarResult: "moz-src:///browser/components/urlbar/UrlbarResult.sys.mjs",
     26 });
     27 
     28 ChromeUtils.defineLazyGetter(lazy, "logger", function () {
     29  return UrlbarUtils.getLogger({ prefix: "SemanticHistorySearch" });
     30 });
     31 
     32 /**
     33 * Lazily creates (on first call) and returns the
     34 * {@link PlacesSemanticHistoryManager} instance backing this provider.
     35 */
     36 ChromeUtils.defineLazyGetter(lazy, "semanticManager", function () {
     37  let { getPlacesSemanticHistoryManager } = ChromeUtils.importESModule(
     38    "resource://gre/modules/PlacesSemanticHistoryManager.sys.mjs"
     39  );
     40  const distanceThreshold = Services.prefs.getFloatPref(
     41    "places.semanticHistory.distanceThreshold",
     42    0.6
     43  );
     44  return getPlacesSemanticHistoryManager({
     45    rowLimit: 10000,
     46    samplingAttrib: "frecency",
     47    changeThresholdCount: 3,
     48    distanceThreshold,
     49  });
     50 });
     51 
     52 /**
     53 * @typedef {ReturnType<import("resource://gre/modules/PlacesSemanticHistoryManager.sys.mjs").getPlacesSemanticHistoryManager>} PlacesSemanticHistoryManager
     54 */
     55 
     56 /**
     57 * Class representing the Semantic History Search provider for the URL bar.
     58 *
     59 * This provider queries a semantic database created using history.
     60 * It performs semantic search using embeddings generated
     61 * by an ML model and retrieves results ranked by cosine similarity to the
     62 * query's embedding.
     63 *
     64 * @class
     65 */
     66 export class UrlbarProviderSemanticHistorySearch extends UrlbarProvider {
     67  /** @type {boolean} */
     68  static #exposureRecorded;
     69 
     70  /**
     71   * Provides a shared instance of the semantic manager, so that other consumers
     72   * won't wrongly initialize it with different parameters.
     73   *
     74   * @returns {PlacesSemanticHistoryManager}
     75   *   The semantic manager instance used by this provider.
     76   */
     77  static get semanticManager() {
     78    return lazy.semanticManager;
     79  }
     80 
     81  /**
     82   * @returns {Values<typeof UrlbarUtils.PROVIDER_TYPE>}
     83   */
     84  get type() {
     85    return UrlbarUtils.PROVIDER_TYPE.PROFILE;
     86  }
     87 
     88  /**
     89   * Determines if the provider is active for the given query context.
     90   *
     91   * @param {object} queryContext
     92   *   The context of the query, including the search string.
     93   */
     94  async isActive(queryContext) {
     95    const minSearchStringLength = lazy.UrlbarPrefs.get(
     96      "suggest.semanticHistory.minLength"
     97    );
     98    if (
     99      lazy.UrlbarPrefs.get("suggest.history") &&
    100      queryContext.searchString.length >= minSearchStringLength &&
    101      (!queryContext.searchMode ||
    102        queryContext.searchMode.source == UrlbarUtils.RESULT_SOURCE.HISTORY)
    103    ) {
    104      if (lazy.semanticManager.canUseSemanticSearch) {
    105        // Proceed only if a sufficient number of history entries have
    106        // embeddings calculated.
    107        return lazy.semanticManager.hasSufficientEntriesForSearching();
    108      }
    109    }
    110    return false;
    111  }
    112 
    113  /**
    114   * Starts querying.
    115   *
    116   * @param {UrlbarQueryContext} queryContext
    117   * @param {(provider: UrlbarProvider, result: UrlbarResult) => void} addCallback
    118   *   Callback invoked by the provider to add a new result.
    119   */
    120  async startQuery(queryContext, addCallback) {
    121    let instance = this.queryInstance;
    122    let resultObject = await lazy.semanticManager.infer(queryContext);
    123    this.#maybeRecordExposure();
    124    let results = resultObject.results;
    125    if (!results || instance != this.queryInstance) {
    126      return;
    127    }
    128 
    129    let openTabs = lazy.UrlbarProviderOpenTabs.getOpenTabUrls(
    130      queryContext.isPrivate
    131    );
    132    for (let res of results) {
    133      if (
    134        !this.#addAsSwitchToTab(
    135          openTabs.get(res.url),
    136          queryContext,
    137          res,
    138          addCallback
    139        )
    140      ) {
    141        const result = new lazy.UrlbarResult({
    142          type: UrlbarUtils.RESULT_TYPE.URL,
    143          source: UrlbarUtils.RESULT_SOURCE.HISTORY,
    144          payload: {
    145            title: res.title,
    146            url: res.url,
    147            icon: UrlbarUtils.getIconForUrl(res.url),
    148            isBlockable: true,
    149            blockL10n: { id: "urlbar-result-menu-remove-from-history" },
    150            helpUrl:
    151              Services.urlFormatter.formatURLPref("app.support.baseURL") +
    152              "awesome-bar-result-menu",
    153            frecency: res.frecency,
    154          },
    155        });
    156        addCallback(this, result);
    157      }
    158    }
    159  }
    160 
    161  /**
    162   * Check if the url is open in tabs, and adds one or multiple switch to tab
    163   * results if so.
    164   *
    165   * @param {Set<[number, string]>|undefined} openTabs
    166   *  Tabs open for the result URL, may be undefined.
    167   * @param {object} queryContext
    168   *  The query context, including the search string.
    169   * @param {object} res
    170   * The result object containing the URL.
    171   * @param {Function} addCallback
    172   *  Callback to add results to the URL bar.
    173   * @returns {boolean} True if a switch to tab result was added.
    174   */
    175  #addAsSwitchToTab(openTabs, queryContext, res, addCallback) {
    176    if (!openTabs?.size) {
    177      return false;
    178    }
    179 
    180    let userContextId =
    181      lazy.UrlbarProviderOpenTabs.getUserContextIdForOpenPagesTable(
    182        queryContext.userContextId,
    183        queryContext.isPrivate
    184      );
    185 
    186    let added = false;
    187    for (let [tabUserContextId, tabGroupId] of openTabs) {
    188      // Don't return a switch to tab result for the current page.
    189      if (
    190        res.url == queryContext.currentPage &&
    191        userContextId == tabUserContextId &&
    192        queryContext.tabGroup === tabGroupId
    193      ) {
    194        continue;
    195      }
    196      // Respect the switchTabs.searchAllContainers pref.
    197      if (
    198        !lazy.UrlbarPrefs.get("switchTabs.searchAllContainers") &&
    199        tabUserContextId != userContextId
    200      ) {
    201        continue;
    202      }
    203      let result = new lazy.UrlbarResult({
    204        type: UrlbarUtils.RESULT_TYPE.TAB_SWITCH,
    205        source: UrlbarUtils.RESULT_SOURCE.TABS,
    206        payload: {
    207          url: res.url,
    208          title: res.title,
    209          icon: UrlbarUtils.getIconForUrl(res.url),
    210          userContextId: tabUserContextId,
    211          tabGroup: tabGroupId,
    212          lastVisit: res.lastVisit,
    213          action: lazy.UrlbarPrefs.get("secondaryActions.switchToTab")
    214            ? UrlbarUtils.createTabSwitchSecondaryAction(tabUserContextId)
    215            : undefined,
    216        },
    217      });
    218      addCallback(this, result);
    219      added = true;
    220    }
    221    return added;
    222  }
    223 
    224  /**
    225   * Records an exposure event for the semantic-history feature-gate, but
    226   * **only once per profile**.  Subsequent calls are ignored.
    227   */
    228  #maybeRecordExposure() {
    229    // Skip if we already recorded or if the gate is manually turned off.
    230    if (UrlbarProviderSemanticHistorySearch.#exposureRecorded) {
    231      return;
    232    }
    233 
    234    // Look up our enrollment (experiment or rollout). If no slug, we’re not enrolled.
    235    let metadata =
    236      lazy.NimbusFeatures.urlbar.getEnrollmentMetadata(
    237        lazy.EnrollmentType.EXPERIMENT
    238      ) ||
    239      lazy.NimbusFeatures.urlbar.getEnrollmentMetadata(
    240        lazy.EnrollmentType.ROLLOUT
    241      );
    242    if (!metadata?.slug) {
    243      // Not part of any semantic-history experiment/rollout → nothing to record
    244      return;
    245    }
    246 
    247    try {
    248      // Actually send it once with the slug.
    249      lazy.NimbusFeatures.urlbar.recordExposureEvent({
    250        once: true,
    251        slug: metadata.slug,
    252      });
    253      UrlbarProviderSemanticHistorySearch.#exposureRecorded = true;
    254      lazy.logger.debug(
    255        `Nimbus exposure event sent (semanticHistory: ${metadata.slug}).`
    256      );
    257    } catch (ex) {
    258      lazy.logger.warn("Unable to record semantic-history exposure event:", ex);
    259    }
    260  }
    261 
    262  /**
    263   * Gets the priority of this provider relative to other providers.
    264   *
    265   * @returns {number} The priority of this provider.
    266   */
    267  getPriority() {
    268    return 0;
    269  }
    270 
    271  onEngagement(queryContext, controller, details) {
    272    let { result } = details;
    273    if (details.selType == "dismiss") {
    274      // Remove browsing history entries from Places.
    275      lazy.PlacesUtils.history.remove(result.payload.url).catch(console.error);
    276      controller.removeResult(result);
    277    }
    278  }
    279 }