activity-stream/lib/PersonalityProvider.jsm

250 строки
8.2 KiB
JavaScript

/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
const {RemoteSettings} = ChromeUtils.import("resource://services-settings/remote-settings.js", {});
const {actionCreators: ac} = ChromeUtils.import("resource://activity-stream/common/Actions.jsm", {});
ChromeUtils.defineModuleGetter(this, "perfService", "resource://activity-stream/common/PerfService.jsm");
const {NaiveBayesTextTagger} = ChromeUtils.import("resource://activity-stream/lib/NaiveBayesTextTagger.jsm", {});
const {NmfTextTagger} = ChromeUtils.import("resource://activity-stream/lib/NmfTextTagger.jsm", {});
const {RecipeExecutor} = ChromeUtils.import("resource://activity-stream/lib/RecipeExecutor.jsm", {});
ChromeUtils.defineModuleGetter(this, "NewTabUtils",
"resource://gre/modules/NewTabUtils.jsm");
/**
* V2 provider builds and ranks an interest profile (also called an “interest vector”) off the browse history.
* This allows Firefox to classify pages into topics, by examining the text found on the page.
* It does this by looking at the history text content, title, and description.
*/
this.PersonalityProvider = class PersonalityProvider {
constructor(
timeSegments,
parameterSets,
maxHistoryQueryResults,
version,
scores,
v2Params) {
this.v2Params = v2Params || {};
this.dispatch = this.v2Params.dispatch || (() => {});
this.modelKeys = this.v2Params.modelKeys;
this.timeSegments = timeSegments;
this.parameterSets = parameterSets;
this.maxHistoryQueryResults = maxHistoryQueryResults;
this.version = version;
this.scores = scores || {};
this.interestConfig = this.scores.interestConfig;
this.interestVector = this.scores.interestVector;
}
async init(callback) {
const perfStart = perfService.absNow();
this.interestConfig = this.interestConfig || await this.getRecipe();
if (!this.interestConfig) {
this.dispatch(ac.PerfEvent({event: "PERSONALIZATION_V2_GET_RECIPE_ERROR"}));
return;
}
this.recipeExecutor = await this.generateRecipeExecutor();
if (!this.recipeExecutor) {
this.dispatch(ac.PerfEvent({event: "PERSONALIZATION_V2_GENERATE_RECIPE_EXECUTOR_ERROR"}));
return;
}
this.interestVector = this.interestVector || await this.createInterestVector();
if (!this.interestVector) {
this.dispatch(ac.PerfEvent({event: "PERSONALIZATION_V2_CREATE_INTEREST_VECTOR_ERROR"}));
return;
}
this.dispatch(ac.PerfEvent({
event: "PERSONALIZATION_V2_TOTAL_DURATION",
value: Math.round(perfService.absNow() - perfStart),
}));
this.initialized = true;
if (callback) {
callback();
}
}
async getFromRemoteSettings(name) {
const result = await RemoteSettings(name).get();
return result;
}
/**
* Returns a Recipe from remote settings to be consumed by a RecipeExecutor.
* A Recipe is a set of instructions on how to processes a RecipeExecutor.
*/
async getRecipe() {
if (!this.recipe || !this.recipe.length) {
const start = perfService.absNow();
this.recipe = await this.getFromRemoteSettings("personality-provider-recipe");
this.dispatch(ac.PerfEvent({
event: "PERSONALIZATION_V2_GET_RECIPE_DURATION",
value: Math.round(perfService.absNow() - start),
}));
}
return this.recipe[0];
}
/**
* Returns a Recipe Executor.
* A Recipe Executor is a set of actions that can be consumed by a Recipe.
* The Recipe determines the order and specifics of which the actions are called.
*/
async generateRecipeExecutor() {
if (!this.taggers) {
const startTaggers = perfService.absNow();
let nbTaggers = [];
let nmfTaggers = {};
const models = await this.getFromRemoteSettings("personality-provider-models");
if (models.length === 0) {
return null;
}
for (let model of models) {
if (!model || !this.modelKeys.includes(model.key)) {
continue;
}
if (model.data.model_type === "nb") {
nbTaggers.push(new NaiveBayesTextTagger(model.data));
} else if (model.data.model_type === "nmf") {
nmfTaggers[model.data.parent_tag] = new NmfTextTagger(model.data);
}
}
this.dispatch(ac.PerfEvent({
event: "PERSONALIZATION_V2_TAGGERS_DURATION",
value: Math.round(perfService.absNow() - startTaggers),
}));
this.taggers = {nbTaggers, nmfTaggers};
}
const startRecipeExecutor = perfService.absNow();
const recipeExecutor = new RecipeExecutor(this.taggers.nbTaggers, this.taggers.nmfTaggers);
this.dispatch(ac.PerfEvent({
event: "PERSONALIZATION_V2_RECIPE_EXECUTOR_DURATION",
value: Math.round(perfService.absNow() - startRecipeExecutor),
}));
return recipeExecutor;
}
/**
* Grabs a slice of browse history for building a interest vector
*/
async fetchHistory(columns, beginTimeSecs, endTimeSecs) {
let sql = `SELECT url, title, visit_count, frecency, last_visit_date, description
FROM moz_places
WHERE last_visit_date >= ${beginTimeSecs * 1000000}
AND last_visit_date < ${endTimeSecs * 1000000}`;
columns.forEach(requiredColumn => {
sql += ` AND IFNULL(${requiredColumn}, "") <> ""`;
});
sql += " LIMIT 30000";
const {activityStreamProvider} = NewTabUtils;
const history = await activityStreamProvider.executePlacesQuery(sql, {
columns,
params: {},
});
return history;
}
/**
* Examines the user's browse history and returns an interest vector that
* describes the topics the user frequently browses.
*/
async createInterestVector() {
let interestVector = {};
let endTimeSecs = ((new Date()).getTime() / 1000);
let beginTimeSecs = endTimeSecs - this.interestConfig.history_limit_secs;
let history = await this.fetchHistory(this.interestConfig.history_required_fields, beginTimeSecs, endTimeSecs);
this.dispatch(ac.PerfEvent({
event: "PERSONALIZATION_V2_HISTORY_SIZE",
value: history.length,
}));
const start = perfService.absNow();
for (let historyRec of history) {
let ivItem = this.recipeExecutor.executeRecipe(
historyRec,
this.interestConfig.history_item_builder);
if (ivItem === null) {
continue;
}
interestVector = this.recipeExecutor.executeCombinerRecipe(
interestVector,
ivItem,
this.interestConfig.interest_combiner);
if (interestVector === null) {
return null;
}
}
const finalResult = this.recipeExecutor.executeRecipe(
interestVector,
this.interestConfig.interest_finalizer);
this.dispatch(ac.PerfEvent({
event: "PERSONALIZATION_V2_CREATE_INTEREST_VECTOR_DURATION",
value: Math.round(perfService.absNow() - start),
}));
return finalResult;
}
/**
* Calculates a score of a Pocket item when compared to the user's interest
* vector. Returns the score. Higher scores are better. Assumes this.interestVector
* is populated.
*/
calculateItemRelevanceScore(pocketItem) {
if (!this.initialized) {
return pocketItem.item_score || 1;
}
let scorableItem = this.recipeExecutor.executeRecipe(
pocketItem,
this.interestConfig.item_to_rank_builder);
if (scorableItem === null) {
return -1;
}
let rankingVector = JSON.parse(JSON.stringify(this.interestVector));
Object.keys(scorableItem).forEach(key => {
rankingVector[key] = scorableItem[key];
});
rankingVector = this.recipeExecutor.executeRecipe(
rankingVector,
this.interestConfig.item_ranker);
if (rankingVector === null) {
return -1;
}
return rankingVector.score;
}
/**
* Returns an object holding the settings and affinity scores of this provider instance.
*/
getAffinities() {
return {
timeSegments: this.timeSegments,
parameterSets: this.parameterSets,
maxHistoryQueryResults: this.maxHistoryQueryResults,
version: this.version,
scores: {
interestConfig: this.interestConfig,
interestVector: this.interestVector,
taggers: this.taggers,
},
};
}
};
const EXPORTED_SYMBOLS = ["PersonalityProvider"];