blob: c557acb835bf13e1b2c4662fd480d883abe10b2c [file] [log] [blame]
// Copyright (c) 2011 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "chrome/browser/history/in_memory_url_index.h"
#include <algorithm>
#include <functional>
#include <iterator>
#include <limits>
#include <numeric>
#include "base/file_util.h"
#include "base/i18n/break_iterator.h"
#include "base/i18n/case_conversion.h"
#include "base/metrics/histogram.h"
#include "base/string_util.h"
#include "base/threading/thread_restrictions.h"
#include "base/time.h"
#include "base/utf_string_conversions.h"
#include "chrome/browser/autocomplete/autocomplete.h"
#include "chrome/browser/autocomplete/history_provider_util.h"
#include "chrome/browser/history/url_database.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/common/url_constants.h"
#include "googleurl/src/url_parse.h"
#include "googleurl/src/url_util.h"
#include "net/base/escape.h"
#include "net/base/net_util.h"
#include "third_party/protobuf/src/google/protobuf/repeated_field.h"
#include "ui/base/l10n/l10n_util.h"
using google::protobuf::RepeatedField;
using google::protobuf::RepeatedPtrField;
using in_memory_url_index::InMemoryURLIndexCacheItem;
namespace history {
typedef imui::InMemoryURLIndexCacheItem_WordListItem WordListItem;
typedef imui::InMemoryURLIndexCacheItem_WordMapItem_WordMapEntry WordMapEntry;
typedef imui::InMemoryURLIndexCacheItem_WordMapItem WordMapItem;
typedef imui::InMemoryURLIndexCacheItem_CharWordMapItem CharWordMapItem;
typedef imui::InMemoryURLIndexCacheItem_CharWordMapItem_CharWordMapEntry
CharWordMapEntry;
typedef imui::InMemoryURLIndexCacheItem_WordIDHistoryMapItem
WordIDHistoryMapItem;
typedef imui::
InMemoryURLIndexCacheItem_WordIDHistoryMapItem_WordIDHistoryMapEntry
WordIDHistoryMapEntry;
typedef imui::InMemoryURLIndexCacheItem_HistoryInfoMapItem HistoryInfoMapItem;
typedef imui::InMemoryURLIndexCacheItem_HistoryInfoMapItem_HistoryInfoMapEntry
HistoryInfoMapEntry;
const size_t InMemoryURLIndex::kNoCachedResultForTerm = -1;
// Score ranges used to get a 'base' score for each of the scoring factors
// (such as recency of last visit, times visited, times the URL was typed,
// and the quality of the string match). There is a matching value range for
// each of these scores for each factor.
const int kScoreRank[] = { 1425, 1200, 900, 400 };
ScoredHistoryMatch::ScoredHistoryMatch()
: raw_score(0),
can_inline(false) {}
ScoredHistoryMatch::ScoredHistoryMatch(const URLRow& url_info)
: HistoryMatch(url_info, 0, false, false),
raw_score(0),
can_inline(false) {}
ScoredHistoryMatch::~ScoredHistoryMatch() {}
// Comparison function for sorting ScoredMatches by their scores.
bool ScoredHistoryMatch::MatchScoreGreater(const ScoredHistoryMatch& m1,
const ScoredHistoryMatch& m2) {
return m1.raw_score >= m2.raw_score;
}
InMemoryURLIndex::SearchTermCacheItem::SearchTermCacheItem(
const WordIDSet& word_id_set,
const HistoryIDSet& history_id_set)
: word_id_set_(word_id_set),
history_id_set_(history_id_set),
used_(true) {}
InMemoryURLIndex::SearchTermCacheItem::SearchTermCacheItem()
: used_(true) {}
InMemoryURLIndex::SearchTermCacheItem::~SearchTermCacheItem() {}
// Comparison function for sorting TermMatches by their offsets.
bool MatchOffsetLess(const TermMatch& m1, const TermMatch& m2) {
return m1.offset < m2.offset;
}
// Comparison function for sorting search terms by descending length.
bool LengthGreater(const string16& string_a, const string16& string_b) {
return string_a.length() > string_b.length();
}
// std::accumulate helper function to add up TermMatches' lengths.
int AccumulateMatchLength(int total, const TermMatch& match) {
return total + match.length;
}
// Converts a raw value for some particular scoring factor into a score
// component for that factor. The conversion function is piecewise linear, with
// input values provided in |value_ranks| and resulting output scores from
// |kScoreRank| (mathematically, f(value_rank[i]) = kScoreRank[i]). A score
// cannot be higher than kScoreRank[0], and drops directly to 0 if lower than
// kScoreRank[3].
//
// For example, take |value| == 70 and |value_ranks| == { 100, 50, 30, 10 }.
// Because 70 falls between ranks 0 (100) and 1 (50), the score is given by the
// linear function:
// score = m * value + b, where
// m = (kScoreRank[0] - kScoreRank[1]) / (value_ranks[0] - value_ranks[1])
// b = value_ranks[1]
// Any value higher than 100 would be scored as if it were 100, and any value
// lower than 10 scored 0.
int ScoreForValue(int value, const int* value_ranks) {
int i = 0;
int rank_count = arraysize(kScoreRank);
while ((i < rank_count) && ((value_ranks[0] < value_ranks[1]) ?
(value > value_ranks[i]) : (value < value_ranks[i])))
++i;
if (i >= rank_count)
return 0;
int score = kScoreRank[i];
if (i > 0) {
score += (value - value_ranks[i]) *
(kScoreRank[i - 1] - kScoreRank[i]) /
(value_ranks[i - 1] - value_ranks[i]);
}
return score;
}
InMemoryURLIndex::InMemoryURLIndex(const FilePath& history_dir)
: history_dir_(history_dir),
history_item_count_(0) {
InMemoryURLIndex::InitializeSchemeWhitelist(&scheme_whitelist_);
}
// Called only by unit tests.
InMemoryURLIndex::InMemoryURLIndex()
: history_item_count_(0) {
InMemoryURLIndex::InitializeSchemeWhitelist(&scheme_whitelist_);
}
InMemoryURLIndex::~InMemoryURLIndex() {}
// static
void InMemoryURLIndex::InitializeSchemeWhitelist(
std::set<std::string>* whitelist) {
DCHECK(whitelist);
whitelist->insert(std::string(chrome::kAboutScheme));
whitelist->insert(std::string(chrome::kChromeUIScheme));
whitelist->insert(std::string(chrome::kFileScheme));
whitelist->insert(std::string(chrome::kFtpScheme));
whitelist->insert(std::string(chrome::kHttpScheme));
whitelist->insert(std::string(chrome::kHttpsScheme));
whitelist->insert(std::string(chrome::kMailToScheme));
}
// Indexing
bool InMemoryURLIndex::Init(history::URLDatabase* history_db,
const std::string& languages) {
// TODO(mrossetti): Register for profile/language change notifications.
languages_ = languages;
return ReloadFromHistory(history_db, false);
}
void InMemoryURLIndex::ShutDown() {
// Write our cache.
SaveToCacheFile();
}
bool InMemoryURLIndex::IndexRow(const URLRow& row) {
const GURL& gurl(row.url());
// Index only URLs with a whitelisted scheme.
if (!InMemoryURLIndex::URLSchemeIsWhitelisted(gurl))
return true;
string16 url(net::FormatUrl(gurl, languages_,
net::kFormatUrlOmitUsernamePassword,
UnescapeRule::SPACES | UnescapeRule::URL_SPECIAL_CHARS,
NULL, NULL, NULL));
HistoryID history_id = static_cast<HistoryID>(row.id());
DCHECK_LT(row.id(), std::numeric_limits<HistoryID>::max());
// Add the row for quick lookup in the history info store.
URLRow new_row(GURL(url), row.id());
new_row.set_visit_count(row.visit_count());
new_row.set_typed_count(row.typed_count());
new_row.set_last_visit(row.last_visit());
new_row.set_title(row.title());
history_info_map_[history_id] = new_row;
// Split URL into individual, unique words then add in the title words.
url = base::i18n::ToLower(url);
String16Set url_words = WordSetFromString16(url);
String16Set title_words = WordSetFromString16(row.title());
String16Set words;
std::set_union(url_words.begin(), url_words.end(),
title_words.begin(), title_words.end(),
std::insert_iterator<String16Set>(words, words.begin()));
for (String16Set::iterator word_iter = words.begin();
word_iter != words.end(); ++word_iter)
AddWordToIndex(*word_iter, history_id);
++history_item_count_;
return true;
}
bool InMemoryURLIndex::ReloadFromHistory(history::URLDatabase* history_db,
bool clear_cache) {
ClearPrivateData();
if (!history_db)
return false;
if (clear_cache || !RestoreFromCacheFile()) {
base::TimeTicks beginning_time = base::TimeTicks::Now();
// The index has to be built from scratch.
URLDatabase::URLEnumerator history_enum;
if (!history_db->InitURLEnumeratorForSignificant(&history_enum))
return false;
URLRow row;
while (history_enum.GetNextURL(&row)) {
if (!IndexRow(row))
return false;
}
UMA_HISTOGRAM_TIMES("History.InMemoryURLIndexingTime",
base::TimeTicks::Now() - beginning_time);
SaveToCacheFile();
}
return true;
}
void InMemoryURLIndex::ClearPrivateData() {
history_item_count_ = 0;
word_list_.clear();
word_map_.clear();
char_word_map_.clear();
word_id_history_map_.clear();
history_info_map_.clear();
search_term_cache_.clear();
}
bool InMemoryURLIndex::RestoreFromCacheFile() {
// TODO(mrossetti): Figure out how to determine if the cache is up-to-date.
// That is: ensure that the database has not been modified since the cache
// was last saved. DB file modification date is inadequate. There are no
// SQLite table checksums automatically stored.
// FIXME(mrossetti): Move File IO to another thread.
base::ThreadRestrictions::ScopedAllowIO allow_io;
base::TimeTicks beginning_time = base::TimeTicks::Now();
FilePath file_path;
if (!GetCacheFilePath(&file_path) || !file_util::PathExists(file_path))
return false;
std::string data;
if (!file_util::ReadFileToString(file_path, &data)) {
LOG(WARNING) << "Failed to read InMemoryURLIndex cache from "
<< file_path.value();
return false;
}
InMemoryURLIndexCacheItem index_cache;
if (!index_cache.ParseFromArray(data.c_str(), data.size())) {
LOG(WARNING) << "Failed to parse InMemoryURLIndex cache data read from "
<< file_path.value();
return false;
}
if (!RestorePrivateData(index_cache)) {
ClearPrivateData(); // Back to square one -- must build from scratch.
return false;
}
UMA_HISTOGRAM_TIMES("History.InMemoryURLIndexRestoreCacheTime",
base::TimeTicks::Now() - beginning_time);
UMA_HISTOGRAM_COUNTS("History.InMemoryURLHistoryItems", history_item_count_);
UMA_HISTOGRAM_COUNTS("History.InMemoryURLCacheSize", data.size());
UMA_HISTOGRAM_COUNTS_10000("History.InMemoryURLWords", word_map_.size());
UMA_HISTOGRAM_COUNTS_10000("History.InMemoryURLChars", char_word_map_.size());
return true;
}
bool InMemoryURLIndex::SaveToCacheFile() {
// TODO(mrossetti): Move File IO to another thread.
base::ThreadRestrictions::ScopedAllowIO allow_io;
base::TimeTicks beginning_time = base::TimeTicks::Now();
InMemoryURLIndexCacheItem index_cache;
SavePrivateData(&index_cache);
std::string data;
if (!index_cache.SerializeToString(&data)) {
LOG(WARNING) << "Failed to serialize the InMemoryURLIndex cache.";
return false;
}
// TODO(mrossetti): Write the cache to a file then swap it for the old cache,
// if any, and delete the old cache.
FilePath file_path;
if (!GetCacheFilePath(&file_path))
return false;
int size = data.size();
if (file_util::WriteFile(file_path, data.c_str(), size) != size) {
LOG(WARNING) << "Failed to write " << file_path.value();
return false;
}
UMA_HISTOGRAM_TIMES("History.InMemoryURLIndexSaveCacheTime",
base::TimeTicks::Now() - beginning_time);
return true;
}
void InMemoryURLIndex::UpdateURL(URLID row_id, const URLRow& row) {
// The row may or may not already be in our index. If it is not already
// indexed and it qualifies then it gets indexed. If it is already
// indexed and still qualifies then it gets updated, otherwise it
// is deleted from the index.
HistoryInfoMap::iterator row_pos = history_info_map_.find(row_id);
if (row_pos == history_info_map_.end()) {
// This new row should be indexed if it qualifies.
if (RowQualifiesAsSignificant(row, base::Time()))
IndexRow(row);
} else if (RowQualifiesAsSignificant(row, base::Time())) {
// This indexed row still qualifies and will be re-indexed.
// The url won't have changed but the title, visit count, etc.
// might have changed.
URLRow& old_row = row_pos->second;
old_row.set_visit_count(row.visit_count());
old_row.set_typed_count(row.typed_count());
old_row.set_last_visit(row.last_visit());
// TODO(mrossetti): When we start indexing the title the next line
// will need attention.
old_row.set_title(row.title());
} else {
// This indexed row no longer qualifies and will be de-indexed.
history_info_map_.erase(row_id);
}
// This invalidates the cache.
search_term_cache_.clear();
// TODO(mrossetti): Record this transaction in the cache.
}
void InMemoryURLIndex::DeleteURL(URLID row_id) {
// Note that this does not remove any reference to this row from the
// word_id_history_map_. That map will continue to contain (and return)
// hits against this row until that map is rebuilt, but since the
// history_info_map_ no longer references the row no erroneous results
// will propagate to the user.
history_info_map_.erase(row_id);
// This invalidates the word cache.
search_term_cache_.clear();
// TODO(mrossetti): Record this transaction in the cache.
}
// Searching
ScoredHistoryMatches InMemoryURLIndex::HistoryItemsForTerms(
const String16Vector& terms) {
ScoredHistoryMatches scored_items;
if (!terms.empty()) {
// Reset used_ flags for search_term_cache_. We use a basic mark-and-sweep
// approach.
ResetSearchTermCache();
// Lowercase the terms.
// TODO(mrossetti): Another opportunity for a transform algorithm.
String16Vector lower_terms;
for (String16Vector::const_iterator term_iter = terms.begin();
term_iter != terms.end(); ++term_iter)
lower_terms.push_back(base::i18n::ToLower(*term_iter));
string16 all_terms(JoinString(lower_terms, ' '));
HistoryIDSet history_id_set = HistoryIDSetFromWords(all_terms);
// Don't perform any scoring (and don't return any matches) if the
// candidate pool is large. (See comments in header.)
const size_t kItemsToScoreLimit = 500;
if (history_id_set.size() <= kItemsToScoreLimit) {
// Pass over all of the candidates filtering out any without a proper
// substring match, inserting those which pass in order by score.
scored_items = std::for_each(history_id_set.begin(), history_id_set.end(),
AddHistoryMatch(*this, lower_terms)).ScoredMatches();
// Select and sort only the top kMaxMatches results.
if (scored_items.size() > AutocompleteProvider::kMaxMatches) {
std::partial_sort(scored_items.begin(),
scored_items.begin() +
AutocompleteProvider::kMaxMatches,
scored_items.end(),
ScoredHistoryMatch::MatchScoreGreater);
scored_items.resize(AutocompleteProvider::kMaxMatches);
} else {
std::sort(scored_items.begin(), scored_items.end(),
ScoredHistoryMatch::MatchScoreGreater);
}
}
}
// Remove any stale SearchTermCacheItems.
for (SearchTermCacheMap::iterator cache_iter = search_term_cache_.begin();
cache_iter != search_term_cache_.end(); ) {
if (!cache_iter->second.used_)
search_term_cache_.erase(cache_iter++);
else
++cache_iter;
}
return scored_items;
}
void InMemoryURLIndex::ResetSearchTermCache() {
for (SearchTermCacheMap::iterator iter = search_term_cache_.begin();
iter != search_term_cache_.end(); ++iter)
iter->second.used_ = false;
}
InMemoryURLIndex::HistoryIDSet InMemoryURLIndex::HistoryIDSetFromWords(
const string16& uni_string) {
// Break the terms down into individual terms (words), get the candidate
// set for each term, and intersect each to get a final candidate list.
// Note that a single 'term' from the user's perspective might be
// a string like "http://www.somewebsite.com" which, from our perspective,
// is four words: 'http', 'www', 'somewebsite', and 'com'.
HistoryIDSet history_id_set;
String16Vector terms = WordVectorFromString16(uni_string, true);
// Sort the terms into the longest first as such are likely to narrow down
// the results quicker. Also, single character terms are the most expensive
// to process so save them for last.
std::sort(terms.begin(), terms.end(), LengthGreater);
for (String16Vector::iterator iter = terms.begin(); iter != terms.end();
++iter) {
string16 uni_word = *iter;
HistoryIDSet term_history_set = HistoryIDsForTerm(uni_word);
if (term_history_set.empty()) {
history_id_set.clear();
break;
}
if (iter == terms.begin()) {
history_id_set.swap(term_history_set);
} else {
HistoryIDSet new_history_id_set;
std::set_intersection(history_id_set.begin(), history_id_set.end(),
term_history_set.begin(), term_history_set.end(),
std::inserter(new_history_id_set,
new_history_id_set.begin()));
history_id_set.swap(new_history_id_set);
}
}
return history_id_set;
}
InMemoryURLIndex::HistoryIDSet InMemoryURLIndex::HistoryIDsForTerm(
const string16& term) {
if (term.empty())
return HistoryIDSet();
// TODO(mrossetti): Consider optimizing for very common terms such as
// 'http[s]', 'www', 'com', etc. Or collect the top 100 more frequently
// occuring words in the user's searches.
size_t term_length = term.length();
InMemoryURLIndex::WordIDSet word_id_set;
if (term_length > 1) {
// See if this term or a prefix thereof is present in the cache.
SearchTermCacheMap::iterator best_prefix(search_term_cache_.end());
for (SearchTermCacheMap::iterator cache_iter = search_term_cache_.begin();
cache_iter != search_term_cache_.end(); ++cache_iter) {
if (StartsWith(term, cache_iter->first, false) &&
(best_prefix == search_term_cache_.end() ||
cache_iter->first.length() > best_prefix->first.length()))
best_prefix = cache_iter;
}
// If a prefix was found then determine the leftover characters to be used
// for further refining the results from that prefix.
Char16Set prefix_chars;
string16 leftovers(term);
if (best_prefix != search_term_cache_.end()) {
// If the prefix is an exact match for the term then grab the cached
// results and we're done.
size_t prefix_length = best_prefix->first.length();
if (prefix_length == term_length) {
best_prefix->second.used_ = true;
return best_prefix->second.history_id_set_;
}
// Otherwise we have a handy starting point.
// If there are no history results for this prefix then we can bail early
// as there will be no history results for the full term.
if (best_prefix->second.history_id_set_.empty()) {
search_term_cache_[term] = SearchTermCacheItem();
return HistoryIDSet();
}
word_id_set = best_prefix->second.word_id_set_;
prefix_chars = Char16SetFromString16(best_prefix->first);
leftovers = term.substr(prefix_length);
}
// Filter for each remaining, unique character in the term.
Char16Set leftover_chars = Char16SetFromString16(leftovers);
Char16Set unique_chars;
std::set_difference(leftover_chars.begin(), leftover_chars.end(),
prefix_chars.begin(), prefix_chars.end(),
std::inserter(unique_chars, unique_chars.begin()));
// Reduce the word set with any leftover, unprocessed characters.
if (!unique_chars.empty()) {
WordIDSet leftover_set(WordIDSetForTermChars(unique_chars));
// We might come up empty on the leftovers.
if (leftover_set.empty()) {
search_term_cache_[term] = SearchTermCacheItem();
return HistoryIDSet();
}
// Or there may not have been a prefix from which to start.
if (prefix_chars.empty()) {
word_id_set.swap(leftover_set);
} else {
WordIDSet new_word_id_set;
std::set_intersection(word_id_set.begin(), word_id_set.end(),
leftover_set.begin(), leftover_set.end(),
std::inserter(new_word_id_set,
new_word_id_set.begin()));
word_id_set.swap(new_word_id_set);
}
}
// We must filter the word list because the resulting word set surely
// contains words which do not have the search term as a proper subset.
for (WordIDSet::iterator word_set_iter = word_id_set.begin();
word_set_iter != word_id_set.end(); ) {
if (word_list_[*word_set_iter].find(term) == string16::npos)
word_id_set.erase(word_set_iter++);
else
++word_set_iter;
}
} else {
word_id_set = WordIDSetForTermChars(Char16SetFromString16(term));
}
// If any words resulted then we can compose a set of history IDs by unioning
// the sets from each word.
HistoryIDSet history_id_set;
if (!word_id_set.empty()) {
for (WordIDSet::iterator word_id_iter = word_id_set.begin();
word_id_iter != word_id_set.end(); ++word_id_iter) {
WordID word_id = *word_id_iter;
WordIDHistoryMap::iterator word_iter = word_id_history_map_.find(word_id);
if (word_iter != word_id_history_map_.end()) {
HistoryIDSet& word_history_id_set(word_iter->second);
history_id_set.insert(word_history_id_set.begin(),
word_history_id_set.end());
}
}
}
// Record a new cache entry for this word if the term is longer than
// a single character.
if (term_length > 1)
search_term_cache_[term] = SearchTermCacheItem(word_id_set, history_id_set);
return history_id_set;
}
// Utility Functions
// static
InMemoryURLIndex::String16Set InMemoryURLIndex::WordSetFromString16(
const string16& uni_string) {
const size_t kMaxWordLength = 64;
String16Vector words = WordVectorFromString16(uni_string, false);
String16Set word_set;
for (String16Vector::const_iterator iter = words.begin(); iter != words.end();
++iter)
word_set.insert(base::i18n::ToLower(*iter).substr(0, kMaxWordLength));
return word_set;
}
// static
InMemoryURLIndex::String16Vector InMemoryURLIndex::WordVectorFromString16(
const string16& uni_string,
bool break_on_space) {
base::i18n::BreakIterator iter(
uni_string,
break_on_space ? base::i18n::BreakIterator::BREAK_SPACE
: base::i18n::BreakIterator::BREAK_WORD);
String16Vector words;
if (!iter.Init())
return words;
while (iter.Advance()) {
if (break_on_space || iter.IsWord()) {
string16 word = iter.GetString();
if (break_on_space)
TrimWhitespace(word, TRIM_ALL, &word);
if (!word.empty())
words.push_back(word);
}
}
return words;
}
// static
InMemoryURLIndex::Char16Set InMemoryURLIndex::Char16SetFromString16(
const string16& term) {
Char16Set characters;
for (string16::const_iterator iter = term.begin(); iter != term.end();
++iter)
characters.insert(*iter);
return characters;
}
void InMemoryURLIndex::AddWordToIndex(const string16& term,
HistoryID history_id) {
WordMap::iterator word_pos = word_map_.find(term);
if (word_pos != word_map_.end())
UpdateWordHistory(word_pos->second, history_id);
else
AddWordHistory(term, history_id);
}
void InMemoryURLIndex::UpdateWordHistory(WordID word_id, HistoryID history_id) {
WordIDHistoryMap::iterator history_pos = word_id_history_map_.find(word_id);
DCHECK(history_pos != word_id_history_map_.end());
HistoryIDSet& history_id_set(history_pos->second);
history_id_set.insert(history_id);
}
// Add a new word to the word list and the word map, and then create a
// new entry in the word/history map.
void InMemoryURLIndex::AddWordHistory(const string16& term,
HistoryID history_id) {
word_list_.push_back(term);
WordID word_id = word_list_.size() - 1;
word_map_[term] = word_id;
HistoryIDSet history_id_set;
history_id_set.insert(history_id);
word_id_history_map_[word_id] = history_id_set;
// For each character in the newly added word (i.e. a word that is not
// already in the word index), add the word to the character index.
Char16Set characters = Char16SetFromString16(term);
for (Char16Set::iterator uni_char_iter = characters.begin();
uni_char_iter != characters.end(); ++uni_char_iter) {
char16 uni_char = *uni_char_iter;
CharWordIDMap::iterator char_iter = char_word_map_.find(uni_char);
if (char_iter != char_word_map_.end()) {
// Update existing entry in the char/word index.
WordIDSet& word_id_set(char_iter->second);
word_id_set.insert(word_id);
} else {
// Create a new entry in the char/word index.
WordIDSet word_id_set;
word_id_set.insert(word_id);
char_word_map_[uni_char] = word_id_set;
}
}
}
InMemoryURLIndex::WordIDSet InMemoryURLIndex::WordIDSetForTermChars(
const Char16Set& term_chars) {
WordIDSet word_id_set;
for (Char16Set::const_iterator c_iter = term_chars.begin();
c_iter != term_chars.end(); ++c_iter) {
CharWordIDMap::iterator char_iter = char_word_map_.find(*c_iter);
if (char_iter == char_word_map_.end()) {
// A character was not found so there are no matching results: bail.
word_id_set.clear();
break;
}
WordIDSet& char_word_id_set(char_iter->second);
// It is possible for there to no longer be any words associated with
// a particular character. Give up in that case.
if (char_word_id_set.empty()) {
word_id_set.clear();
break;
}
if (c_iter == term_chars.begin()) {
// First character results becomes base set of results.
word_id_set = char_word_id_set;
} else {
// Subsequent character results get intersected in.
WordIDSet new_word_id_set;
std::set_intersection(word_id_set.begin(), word_id_set.end(),
char_word_id_set.begin(), char_word_id_set.end(),
std::inserter(new_word_id_set,
new_word_id_set.begin()));
word_id_set.swap(new_word_id_set);
}
}
return word_id_set;
}
// static
TermMatches InMemoryURLIndex::MatchTermInString(const string16& term,
const string16& string,
int term_num) {
const size_t kMaxCompareLength = 2048;
const string16& short_string = (string.length() > kMaxCompareLength) ?
string.substr(0, kMaxCompareLength) : string;
TermMatches matches;
for (size_t location = short_string.find(term); location != string16::npos;
location = short_string.find(term, location + 1)) {
matches.push_back(TermMatch(term_num, location, term.length()));
}
return matches;
}
// static
TermMatches InMemoryURLIndex::SortAndDeoverlap(const TermMatches& matches) {
if (matches.empty())
return matches;
TermMatches sorted_matches = matches;
std::sort(sorted_matches.begin(), sorted_matches.end(), MatchOffsetLess);
TermMatches clean_matches;
TermMatch last_match = sorted_matches[0];
clean_matches.push_back(last_match);
for (TermMatches::const_iterator iter = sorted_matches.begin() + 1;
iter != sorted_matches.end(); ++iter) {
if (iter->offset >= last_match.offset + last_match.length) {
last_match = *iter;
clean_matches.push_back(last_match);
}
}
return clean_matches;
}
// static
std::vector<size_t> InMemoryURLIndex::OffsetsFromTermMatches(
const TermMatches& matches) {
std::vector<size_t> offsets;
for (TermMatches::const_iterator i = matches.begin(); i != matches.end(); ++i)
offsets.push_back(i->offset);
return offsets;
}
// static
TermMatches InMemoryURLIndex::ReplaceOffsetsInTermMatches(
const TermMatches& matches,
const std::vector<size_t>& offsets) {
DCHECK_EQ(matches.size(), offsets.size());
TermMatches new_matches;
std::vector<size_t>::const_iterator offset_iter = offsets.begin();
for (TermMatches::const_iterator term_iter = matches.begin();
term_iter != matches.end(); ++term_iter, ++offset_iter) {
if (*offset_iter != string16::npos) {
TermMatch new_match(*term_iter);
new_match.offset = *offset_iter;
new_matches.push_back(new_match);
}
}
return new_matches;
}
// static
ScoredHistoryMatch InMemoryURLIndex::ScoredMatchForURL(
const URLRow& row,
const String16Vector& terms) {
ScoredHistoryMatch match(row);
GURL gurl = row.url();
if (!gurl.is_valid())
return match;
// Figure out where each search term appears in the URL and/or page title
// so that we can score as well as provide autocomplete highlighting.
string16 url = base::i18n::ToLower(UTF8ToUTF16(gurl.spec()));
string16 title = base::i18n::ToLower(row.title());
int term_num = 0;
for (String16Vector::const_iterator iter = terms.begin(); iter != terms.end();
++iter, ++term_num) {
string16 term = *iter;
TermMatches url_term_matches = MatchTermInString(term, url, term_num);
TermMatches title_term_matches = MatchTermInString(term, title, term_num);
if (url_term_matches.empty() && title_term_matches.empty())
return match; // A term was not found in either URL or title - reject.
match.url_matches.insert(match.url_matches.end(), url_term_matches.begin(),
url_term_matches.end());
match.title_matches.insert(match.title_matches.end(),
title_term_matches.begin(),
title_term_matches.end());
}
// Sort matches by offset and eliminate any which overlap.
match.url_matches = SortAndDeoverlap(match.url_matches);
match.title_matches = SortAndDeoverlap(match.title_matches);
// We should not (currently) inline autocomplete a result unless both of the
// following are true:
// * There is exactly one substring matches in the URL, and
// * The one URL match starts at the beginning of the URL.
match.can_inline =
match.url_matches.size() == 1 && match.url_matches[0].offset == 0;
// Get partial scores based on term matching. Note that the score for
// each of the URL and title are adjusted by the fraction of the
// terms appearing in each.
int url_score = ScoreComponentForMatches(match.url_matches, url.length()) *
match.url_matches.size() / terms.size();
int title_score =
ScoreComponentForMatches(match.title_matches, title.length()) *
match.title_matches.size() / terms.size();
// Arbitrarily pick the best.
// TODO(mrossetti): It might make sense that a term which appears in both the
// URL and the Title should boost the score a bit.
int term_score = std::max(url_score, title_score);
if (term_score == 0)
return match;
// Determine scoring factors for the recency of visit, visit count and typed
// count attributes of the URLRow.
const int kDaysAgoLevel[] = { 0, 10, 20, 30 };
int days_ago_value = ScoreForValue((base::Time::Now() -
row.last_visit()).InDays(), kDaysAgoLevel);
const int kVisitCountLevel[] = { 30, 10, 5, 3 };
int visit_count_value = ScoreForValue(row.visit_count(), kVisitCountLevel);
const int kTypedCountLevel[] = { 10, 5, 3, 1 };
int typed_count_value = ScoreForValue(row.typed_count(), kTypedCountLevel);
// The final raw score is calculated by:
// - accumulating each contributing factor, some of which are added more
// than once giving them more 'influence' on the final score (currently,
// visit_count_value is added twice and typed_count_value three times)
// - dropping the lowest scores (|kInsignificantFactors|)
// - dividing by the remaining significant factors
// This approach allows emphasis on more relevant factors while reducing the
// inordinate impact of low scoring factors.
int factor[] = {term_score, days_ago_value, visit_count_value,
visit_count_value, typed_count_value, typed_count_value,
typed_count_value};
const int kInsignificantFactors = 2;
const int kSignificantFactors = arraysize(factor) - kInsignificantFactors;
std::partial_sort(factor, factor + kSignificantFactors,
factor + arraysize(factor), std::greater<int>());
for (int i = 0; i < kSignificantFactors; ++i)
match.raw_score += factor[i];
match.raw_score /= kSignificantFactors;
return match;
}
int InMemoryURLIndex::ScoreComponentForMatches(const TermMatches& matches,
size_t max_length) {
// TODO(mrossetti): This is good enough for now but must be fine-tuned.
if (matches.empty())
return 0;
// Score component for whether the input terms (if more than one) were found
// in the same order in the match. Start with kOrderMaxValue points divided
// equally among (number of terms - 1); then discount each of those terms that
// is out-of-order in the match.
const int kOrderMaxValue = 250;
int order_value = kOrderMaxValue;
if (matches.size() > 1) {
int max_possible_out_of_order = matches.size() - 1;
int out_of_order = 0;
for (size_t i = 1; i < matches.size(); ++i) {
if (matches[i - 1].term_num > matches[i].term_num)
++out_of_order;
}
order_value = (max_possible_out_of_order - out_of_order) * kOrderMaxValue /
max_possible_out_of_order;
}
// Score component for how early in the match string the first search term
// appears. Start with kStartMaxValue points and discount by
// 1/kMaxSignificantStart points for each character later than the first at
// which the term begins. No points are earned if the start of the match
// occurs at or after kMaxSignificantStart.
const size_t kMaxSignificantStart = 20;
const int kStartMaxValue = 250;
int start_value = (kMaxSignificantStart -
std::min(kMaxSignificantStart, matches[0].offset)) * kStartMaxValue /
kMaxSignificantStart;
// Score component for how much of the matched string the input terms cover.
// kCompleteMaxValue points times the fraction of the URL/page title string
// that was matched.
size_t term_length_total = std::accumulate(matches.begin(), matches.end(),
0, AccumulateMatchLength);
const size_t kMaxSignificantLength = 50;
size_t max_significant_length =
std::min(max_length, std::max(term_length_total, kMaxSignificantLength));
const int kCompleteMaxValue = 500;
int complete_value =
term_length_total * kCompleteMaxValue / max_significant_length;
int raw_score = order_value + start_value + complete_value;
const int kTermScoreLevel[] = { 1000, 650, 500, 200 };
// Scale the sum of the three components above into a single score component
// on the same scale as that used in ScoredMatchForURL().
return ScoreForValue(raw_score, kTermScoreLevel);
}
InMemoryURLIndex::AddHistoryMatch::AddHistoryMatch(
const InMemoryURLIndex& index,
const String16Vector& lower_terms)
: index_(index),
lower_terms_(lower_terms) {
}
InMemoryURLIndex::AddHistoryMatch::~AddHistoryMatch() {}
void InMemoryURLIndex::AddHistoryMatch::operator()(
const InMemoryURLIndex::HistoryID history_id) {
HistoryInfoMap::const_iterator hist_pos =
index_.history_info_map_.find(history_id);
// Note that a history_id may be present in the word_id_history_map_ yet not
// be found in the history_info_map_. This occurs when an item has been
// deleted by the user or the item no longer qualifies as a quick result.
if (hist_pos != index_.history_info_map_.end()) {
const URLRow& hist_item = hist_pos->second;
ScoredHistoryMatch match(ScoredMatchForURL(hist_item, lower_terms_));
if (match.raw_score > 0)
scored_matches_.push_back(match);
}
}
bool InMemoryURLIndex::GetCacheFilePath(FilePath* file_path) {
if (history_dir_.empty())
return false;
*file_path = history_dir_.Append(FILE_PATH_LITERAL("History Provider Cache"));
return true;
}
bool InMemoryURLIndex::URLSchemeIsWhitelisted(const GURL& gurl) const {
return scheme_whitelist_.find(gurl.scheme()) != scheme_whitelist_.end();
}
void InMemoryURLIndex::SavePrivateData(InMemoryURLIndexCacheItem* cache) const {
DCHECK(cache);
cache->set_timestamp(base::Time::Now().ToInternalValue());
cache->set_history_item_count(history_item_count_);
SaveWordList(cache);
SaveWordMap(cache);
SaveCharWordMap(cache);
SaveWordIDHistoryMap(cache);
SaveHistoryInfoMap(cache);
}
bool InMemoryURLIndex::RestorePrivateData(
const InMemoryURLIndexCacheItem& cache) {
last_saved_ = base::Time::FromInternalValue(cache.timestamp());
history_item_count_ = cache.history_item_count();
return (history_item_count_ == 0) || (RestoreWordList(cache) &&
RestoreWordMap(cache) && RestoreCharWordMap(cache) &&
RestoreWordIDHistoryMap(cache) && RestoreHistoryInfoMap(cache));
}
void InMemoryURLIndex::SaveWordList(InMemoryURLIndexCacheItem* cache) const {
if (word_list_.empty())
return;
WordListItem* list_item = cache->mutable_word_list();
list_item->set_word_count(word_list_.size());
for (String16Vector::const_iterator iter = word_list_.begin();
iter != word_list_.end(); ++iter)
list_item->add_word(UTF16ToUTF8(*iter));
}
bool InMemoryURLIndex::RestoreWordList(const InMemoryURLIndexCacheItem& cache) {
if (!cache.has_word_list())
return false;
const WordListItem& list_item(cache.word_list());
uint32 expected_item_count = list_item.word_count();
uint32 actual_item_count = list_item.word_size();
if (actual_item_count == 0 || actual_item_count != expected_item_count)
return false;
const RepeatedPtrField<std::string>& words(list_item.word());
for (RepeatedPtrField<std::string>::const_iterator iter = words.begin();
iter != words.end(); ++iter)
word_list_.push_back(UTF8ToUTF16(*iter));
return true;
}
void InMemoryURLIndex::SaveWordMap(InMemoryURLIndexCacheItem* cache) const {
if (word_map_.empty())
return;
WordMapItem* map_item = cache->mutable_word_map();
map_item->set_item_count(word_map_.size());
for (WordMap::const_iterator iter = word_map_.begin();
iter != word_map_.end(); ++iter) {
WordMapEntry* map_entry = map_item->add_word_map_entry();
map_entry->set_word(UTF16ToUTF8(iter->first));
map_entry->set_word_id(iter->second);
}
}
bool InMemoryURLIndex::RestoreWordMap(const InMemoryURLIndexCacheItem& cache) {
if (!cache.has_word_map())
return false;
const WordMapItem& list_item(cache.word_map());
uint32 expected_item_count = list_item.item_count();
uint32 actual_item_count = list_item.word_map_entry_size();
if (actual_item_count == 0 || actual_item_count != expected_item_count)
return false;
const RepeatedPtrField<WordMapEntry>& entries(list_item.word_map_entry());
for (RepeatedPtrField<WordMapEntry>::const_iterator iter = entries.begin();
iter != entries.end(); ++iter)
word_map_[UTF8ToUTF16(iter->word())] = iter->word_id();
return true;
}
void InMemoryURLIndex::SaveCharWordMap(InMemoryURLIndexCacheItem* cache) const {
if (char_word_map_.empty())
return;
CharWordMapItem* map_item = cache->mutable_char_word_map();
map_item->set_item_count(char_word_map_.size());
for (CharWordIDMap::const_iterator iter = char_word_map_.begin();
iter != char_word_map_.end(); ++iter) {
CharWordMapEntry* map_entry = map_item->add_char_word_map_entry();
map_entry->set_char_16(iter->first);
const WordIDSet& word_id_set(iter->second);
map_entry->set_item_count(word_id_set.size());
for (WordIDSet::const_iterator set_iter = word_id_set.begin();
set_iter != word_id_set.end(); ++set_iter)
map_entry->add_word_id(*set_iter);
}
}
bool InMemoryURLIndex::RestoreCharWordMap(
const InMemoryURLIndexCacheItem& cache) {
if (!cache.has_char_word_map())
return false;
const CharWordMapItem& list_item(cache.char_word_map());
uint32 expected_item_count = list_item.item_count();
uint32 actual_item_count = list_item.char_word_map_entry_size();
if (actual_item_count == 0 || actual_item_count != expected_item_count)
return false;
const RepeatedPtrField<CharWordMapEntry>&
entries(list_item.char_word_map_entry());
for (RepeatedPtrField<CharWordMapEntry>::const_iterator iter =
entries.begin(); iter != entries.end(); ++iter) {
expected_item_count = iter->item_count();
actual_item_count = iter->word_id_size();
if (actual_item_count == 0 || actual_item_count != expected_item_count)
return false;
char16 uni_char = static_cast<char16>(iter->char_16());
WordIDSet word_id_set;
const RepeatedField<int32>& word_ids(iter->word_id());
for (RepeatedField<int32>::const_iterator jiter = word_ids.begin();
jiter != word_ids.end(); ++jiter)
word_id_set.insert(*jiter);
char_word_map_[uni_char] = word_id_set;
}
return true;
}
void InMemoryURLIndex::SaveWordIDHistoryMap(InMemoryURLIndexCacheItem* cache)
const {
if (word_id_history_map_.empty())
return;
WordIDHistoryMapItem* map_item = cache->mutable_word_id_history_map();
map_item->set_item_count(word_id_history_map_.size());
for (WordIDHistoryMap::const_iterator iter = word_id_history_map_.begin();
iter != word_id_history_map_.end(); ++iter) {
WordIDHistoryMapEntry* map_entry =
map_item->add_word_id_history_map_entry();
map_entry->set_word_id(iter->first);
const HistoryIDSet& history_id_set(iter->second);
map_entry->set_item_count(history_id_set.size());
for (HistoryIDSet::const_iterator set_iter = history_id_set.begin();
set_iter != history_id_set.end(); ++set_iter)
map_entry->add_history_id(*set_iter);
}
}
bool InMemoryURLIndex::RestoreWordIDHistoryMap(
const InMemoryURLIndexCacheItem& cache) {
if (!cache.has_word_id_history_map())
return false;
const WordIDHistoryMapItem& list_item(cache.word_id_history_map());
uint32 expected_item_count = list_item.item_count();
uint32 actual_item_count = list_item.word_id_history_map_entry_size();
if (actual_item_count == 0 || actual_item_count != expected_item_count)
return false;
const RepeatedPtrField<WordIDHistoryMapEntry>&
entries(list_item.word_id_history_map_entry());
for (RepeatedPtrField<WordIDHistoryMapEntry>::const_iterator iter =
entries.begin(); iter != entries.end(); ++iter) {
expected_item_count = iter->item_count();
actual_item_count = iter->history_id_size();
if (actual_item_count == 0 || actual_item_count != expected_item_count)
return false;
WordID word_id = iter->word_id();
HistoryIDSet history_id_set;
const RepeatedField<int64>& history_ids(iter->history_id());
for (RepeatedField<int64>::const_iterator jiter = history_ids.begin();
jiter != history_ids.end(); ++jiter)
history_id_set.insert(*jiter);
word_id_history_map_[word_id] = history_id_set;
}
return true;
}
void InMemoryURLIndex::SaveHistoryInfoMap(
InMemoryURLIndexCacheItem* cache) const {
if (history_info_map_.empty())
return;
HistoryInfoMapItem* map_item = cache->mutable_history_info_map();
map_item->set_item_count(history_info_map_.size());
for (HistoryInfoMap::const_iterator iter = history_info_map_.begin();
iter != history_info_map_.end(); ++iter) {
HistoryInfoMapEntry* map_entry = map_item->add_history_info_map_entry();
map_entry->set_history_id(iter->first);
const URLRow& url_row(iter->second);
// Note: We only save information that contributes to the index so there
// is no need to save search_term_cache_ (not persistent),
// languages_, etc.
map_entry->set_visit_count(url_row.visit_count());
map_entry->set_typed_count(url_row.typed_count());
map_entry->set_last_visit(url_row.last_visit().ToInternalValue());
map_entry->set_url(url_row.url().spec());
map_entry->set_title(UTF16ToUTF8(url_row.title()));
}
}
bool InMemoryURLIndex::RestoreHistoryInfoMap(
const InMemoryURLIndexCacheItem& cache) {
if (!cache.has_history_info_map())
return false;
const HistoryInfoMapItem& list_item(cache.history_info_map());
uint32 expected_item_count = list_item.item_count();
uint32 actual_item_count = list_item.history_info_map_entry_size();
if (actual_item_count == 0 || actual_item_count != expected_item_count)
return false;
const RepeatedPtrField<HistoryInfoMapEntry>&
entries(list_item.history_info_map_entry());
for (RepeatedPtrField<HistoryInfoMapEntry>::const_iterator iter =
entries.begin(); iter != entries.end(); ++iter) {
HistoryID history_id = iter->history_id();
GURL url(iter->url());
URLRow url_row(url, history_id);
url_row.set_visit_count(iter->visit_count());
url_row.set_typed_count(iter->typed_count());
url_row.set_last_visit(base::Time::FromInternalValue(iter->last_visit()));
if (iter->has_title()) {
string16 title(UTF8ToUTF16(iter->title()));
url_row.set_title(title);
}
history_info_map_[history_id] = url_row;
}
return true;
}
} // namespace history