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 }