我有一个数据库中存储了许多文章(包括标题和正文)。我正在寻找一种算法,能够找到与某篇文章最相似的X篇文章,就像在Stack Overflow当你提出问题时看到的"Related Questions"那样。
我试着通过谷歌搜索,但是只找到了其他类似的"相似文本"问题页面,例如将每篇文章与所有其他文章进行比较并在某处存储相似性。而SO在我刚刚输入的文本上实时进行了处理。
如何实现呢?
我有一个数据库中存储了许多文章(包括标题和正文)。我正在寻找一种算法,能够找到与某篇文章最相似的X篇文章,就像在Stack Overflow当你提出问题时看到的"Related Questions"那样。
我试着通过谷歌搜索,但是只找到了其他类似的"相似文本"问题页面,例如将每篇文章与所有其他文章进行比较并在某处存储相似性。而SO在我刚刚输入的文本上实时进行了处理。
如何实现呢?
这取决于你对“相似”一词的定义。
编辑距离算法是(拉丁语)字典建议的标准算法,可以处理整个文本。如果两个文本有基本相同的单词(或字母),并且顺序相同,则它们是相似的。因此,以下两篇书评可能会比较相似:
1)“这是一本好书”
2)“这些不是好书”
(将(2)转换为(1)所需的删除、插入、删除或更改字母的数量称为“编辑距离”。)
要实现这个功能,您需要以编程方式访问每个评论。这可能没有听起来那么昂贵,如果太昂贵,您可以将比较作为后台任务执行,并将n个最相似的存储在数据库字段中。
另一种方法是了解(拉丁语)语言的结构。如果剥去短词(非大写或带引号的词),并给常见或独特的单词(或前缀)分配权重,您可以进行类似贝叶斯的比较。下面的两篇书评可能会被简化并发现相似之处:
3)“法国革命是嗯嗯 咆哮与平息嗯嗯 法国。”-> France/French(2)Revolution(1)War(1)Peace(1)(请注意,已使用字典将“France”和“French”组合起来)
4)“这本书是嗯嗯法国菜的一次革命。”-> France(1)Revolution(1)
要实现这个功能,您需要在创建/更新评论时识别评论中的“关键词”,并在查询的where子句中使用这些关键词查找相似的评论(如果数据库支持,则最好使用“全文”搜索),然后对结果集进行后处理以得分。
图书还有分类-在法国的惊悚小说是否类似于关于法国历史的研究等等?除标题和文本之外的元数据可能有助于保持结果相关。
您可以使用以下方法:
(还可参见:http://infolab.stanford.edu/~ullman/mmds/book.pdf Minhash 章节),也可参见http://ann-benchmarks.com/以了解最新技术。
如果您有用户与文章的互动信息(点击/喜欢/查看等),可以使用协同过滤算法:https://en.wikipedia.org/wiki/Collaborative_filtering
使用Word2vec或类似的嵌入技术,在“语义”向量空间中比较文章:https://en.wikipedia.org/wiki/Word2vec
使用潜在语义分析:https://en.wikipedia.org/wiki/Latent_semantic_analysis
使用词袋模型,并应用一些距离度量,如Jaccard系数来计算集合相似度:https://en.wikipedia.org/wiki/Jaccard_index, https://en.wikipedia.org/wiki/Bag-of-words_model
SO只对问题的标题进行比较,不会对正文进行比较,所以仅对短字符串进行比较。
您可以在文章标题和关键字中使用他们的算法(不知道它是什么样子的)。 如果您有更多的CPU时间可用,也可以在文章摘要上使用。
class Statistics {
std::unordered_map<std::string, int64_t> _counts;
int64_t _totWords;
void process(std::string& token);
public:
explicit Statistics(const std::string& text);
double Dist(const Statistics& fellow) const;
bool IsEmpty() const { return _totWords == 0; }
};
namespace {
const std::string gPunctStr = ".,;:!?";
const std::unordered_set<char> gPunctSet(gPunctStr.begin(), gPunctStr.end());
}
Statistics::Statistics(const std::string& text) {
std::string lastToken;
for (size_t i = 0; i < text.size(); i++) {
int ch = static_cast<uint8_t>(text[i]);
if (!isspace(ch)) {
lastToken.push_back(tolower(ch));
continue;
}
process(lastToken);
}
process(lastToken);
}
void Statistics::process(std::string& token) {
do {
if (token.size() == 0) {
break;
}
if (gPunctSet.find(token.back()) != gPunctSet.end()) {
token.pop_back();
}
} while (false);
if (token.size() != 0) {
auto it = _counts.find(token);
if (it == _counts.end()) {
_counts.emplace(token, 1);
}
else {
it->second++;
}
_totWords++;
token.clear();
}
}
double Statistics::Dist(const Statistics& fellow) const {
double sum = 0;
for (const auto& wordInfo : _counts) {
const std::string wordText = wordInfo.first;
const double freq = double(wordInfo.second) / _totWords;
auto it = fellow._counts.find(wordText);
double fellowFreq;
if (it == fellow._counts.end()) {
fellowFreq = 0;
}
else {
fellowFreq = double(it->second) / fellow._totWords;
}
const double d = freq - fellowFreq;
sum += d * d;
}
return std::sqrt(sum);
}