倒排索引学习

在搜索引擎中每个文件都对应一个文件ID,文件内容被表示为一系列关键词的集合(实际上在搜索引擎索引库中,关键词也已经转换为关键词ID)。例如“文档1”经过分词,提取了20个关键词,每个关键词都会记录它在文档中的出现次数和出现位置。

得到正向索引的结构如下:

       “文档1”的ID > 单词1:出现次数,出现位置列表;单词2:出现次数,出现位置列表;…………。

       “文档2”的ID > 单词1:出现次数,出现位置列表;单词2:出现次数,出现位置列表。

所以,搜索引擎会将正向索引重新构建为倒排索引,即把文件ID对应到关键词的映射转换为关键词到文件ID的映射,每个关键词都对应着一系列的文件,这些文件中都出现这个关键词。

       得到倒排索引的结构如下:

       “关键词1”:“文档1”的ID,“文档2”的ID,…………。

       “关键词2”:“文档1”的ID,“文档2”的ID

  倒排索引(Inverted Index):倒排索引是实现“单词-文档矩阵”的一种具体存储形式,通过倒排索引,可以根据单词快速获取包含这个单词的文档列表。倒排索引主要由两个部分组成:“单词词典”和“倒排文件”。

倒排索引(Inverted Index):倒排索引是实现“单词-文档矩阵”的一种具体存储形式,通过倒排索引,可以根据单词快速获取包含这个单词的文档列表。倒排索引主要由两个部分组成:“单词词典”和“倒排文件”。

       单词词典(Lexicon):搜索引擎的通常索引单位是单词,单词词典是由文档集合中出现过的所有单词构成的字符串集合,单词词典内每条索引项记载单词本身的一些信息以及指向“倒排列表”的指针。

       倒排列表(PostingList):倒排列表记载了出现过某个单词的所有文档的文档列表及单词在该文档中出现的位置信息,每条记录称为一个倒排项(Posting)。根据倒排列表,即可获知哪些文档包含某个单词。

       倒排文件(Inverted File):所有单词的倒排列表往往顺序地存储在磁盘的某个文件里,这个文件即被称之为倒排文件,倒排文件是存储倒排索引的物理文件。

对于一个规模很大的文档集合来说,可能包含几十万甚至上百万的不同单词,能否快速定位某个单词,这直接影响搜索时的响应速度,所以需要高效的数据结构来对单词词典进行构建和查找,常用的数据结构包括哈希加链表结构和树形词典结构。

用哈希加链表结构在建立索引的过程中,词典结构也会相应地被构建出来。比如在解析一个新文档的时候,对于某个在文档中出现的词汇A:

      第1步:首先利用哈希函数获得其哈希值

      第2步:之后根据哈希值对应的哈希表项读取其中保存的指针,找到了对应的冲突链表。

      第3步:如果冲突链表里已经存在这个单词, 说明单词在之前解析的文档里已经出现过。

      第4步:如果在冲突链表里没有发现这个单词,说明该单词是首次碰到,则将其加入冲突链表里。

    通过这种方式,当文档集合内所有文档解析完毕时,相应的词典结构也就建立起来了。

lucene从4开始大量使用的数据结构是FST(Finite State Transducer)。FST有两个优点:1)空间占用小。通过对词典中单词前缀和后缀的重复利用,压缩了存储空间;2)查询速度快。O(len(str))的查询时间复杂度。

template <class TKey>
class InvIndex : public map<TKey, list<int>>
{
public:
    vector<vector<TKey>> docs; //文档正排表

public:
    //向索引中加入一个文档
    void add(vector<TKey>& doc)
    {
        //在正排表里记录该文档
        docs.push_back(doc);
        int curDocID = docs.size();  //现在代码:使得文档编号从1开始 原始代码:int curDocID = docs.size()-1;

        //遍历doc里所有的term
        for (int w = 0; w < doc.size(); w++)
        {
            map<TKey,list<int>>::iterator it;
            it = this->find(doc[w]);

            //如果该term的倒排链不存在,新建倒排链
            if (it == this->end())
            {
                list<int> newList;
                (*this)[doc[w]] = newList;
                it = this->find(doc[w]);
            }

            //在倒排链末尾插入新的文档
            it->second.push_back(curDocID);
        }
    }

    //在索引中进行一次查询
    void retrieve(vector<TKey>& query, set<int>& docIDs)
    {
        int termNum = query.size();

        //合并所有term的倒排链
        docIDs.clear();
        for (int t = 0; t < termNum; t++)
        {
            map<TKey,list<int>>::iterator it;
            //该term倒排链不存在则跳过
            if ((it = this->find(query[t])) != this->end())
                docIDs.insert(it->second.begin(),it->second.end());
        }

    }
};

Trie树,即字典树,又称单词查找树或键树,是一种树形结构,是一种哈希树的变种。典型应用是用于统计和排序大量的字符串(但不仅限于字符串),所以经常被搜索引擎系统用于文本词频统计。它的优点是:最大限度地减少无谓的字符串比较,查询效率比哈希表高。Trie的核心思想是空间换时间。利用字符串的公共前缀来降低查询时间的开销以达到提高效率的目的

根节点不包含字符,除根节点外每一个节点都只包含一个字符。
从根节点到某一节点,路径上经过的字符连接起来,为该节点对应的字符串。
每个节点的所有子节点包含的字符都不相同。

我们可以看到,trie树每一层的节点数是26^i级别的。所以为了节省空间。我们用动态链表,或者用数组来模拟动态。空间的花费,不会超过单词数×单词长度。
struct TrieNode
{
    bool Isword;                //判断是否是单词
    TrieNode* next[Num];
    TrieNode() :Isword(false)   //初始化
    {
        memset(next, NULL, sizeof(next));
    }
};
void Trie::insert(string word)
{
    TrieNode* location = m_root;
    for (int i = 0; i < word.length();i++)
    {
        if (location->next[word[i] - 'a'] == nullptr)
        {
            TrieNode* temp = new TrieNode();
            location->next[word[i] - 'a']=temp;
        }
        location = location->next[word[i] - 'a'];
    }
    location->Isword = true;
}

对于中文 
class trieNode
{
public:
    trieNode() :count(0) {};
    //以当前节点结尾的字符串的个数
    int count;
    map<wchar_t, trieNode*> child;
};
void CTrie::insert_string(const wstring& str)
{
    if (!m_root || str.empty())
        return;
 
    trieNode* currentNode = m_root;
 
    for (auto& chr : str)
    {
        auto Iter = currentNode->child.find(chr);
        if (Iter == currentNode->child.end())
        {
            //如果当前字符不在字典树中,新建一个节点插入
            trieNode* newNode = new trieNode();
            currentNode->child.insert(make_pair(chr, newNode));
            currentNode = newNode;
        }
        else
        {
            //如果当前字符在字典书中,则将当前节点指向它的孩子
            currentNode = Iter->second;
        }
    }
    currentNode->count++;
}

发布了48 篇原创文章 · 获赞 7 · 访问量 4万+

猜你喜欢

转载自blog.csdn.net/wenxinfly/article/details/104017404
今日推荐