zoukankan      html  css  js  c++  java
  • Xapian的内存索引-添加文档

        本文主要记录Xapian的内存索引在添加文档过程中,做了哪些事情。

        内容主要为函数执行过程中的流水线。

        demo代码:

        Xapian::WritableDatabase db = Xapian::InMemory::open();
        Xapian::Document doc;
        // 添加文档的,T表示字段名字,TERM内容为世界,position为1
        doc.add_posting("T世界", 1);
        doc.add_posting("T体育", 2);
        doc.add_posting("T比赛", 3);
        // 添加doc的数据
        doc.set_data("世界体育比赛");
        // 添加doc的唯一term
        doc.add_boolean_term(K_DOC_UNIQUE_ID);
        // 采用replace_document,保证拥有K_DOC_UNIQUE_ID的文档在索引库中唯一
        Xapian::docid innerId = db.replace_document(K_DOC_UNIQUE_ID, doc);

    1.创建并填充Document

    定义好文档对象,使用add_posting接口,添加term,以及对应的position、wdfinc;

    内部实现细节:

    1.1 先尝试读取doc已有term数据;如果读取到了,则将term以及positions信息记录到terms中;

    void Xapian::Document::Internal::need_terms() const {
        if (terms_here) {
            return;
        }
        if (database.get()) {
            Xapian::TermIterator t(database->open_term_list(did));
            Xapian::TermIterator tend(NULL);
            for ( ; t != tend; ++t) {
                Xapian::PositionIterator p = t.positionlist_begin();
                OmDocumentTerm term(t.get_wdf());
                for ( ; p != t.positionlist_end(); ++p) {
                    term.append_position(*p);
                }
                terms.insert(make_pair(*t, term));
            }
        }
        termlist_size = terms.size();
        terms_here = true;
    }

    1.2 加入全新term,首先,创建新的term对象,为其添加position信息,最后加入到terms;

    void Xapian::Document::Internal::add_posting(const string & tname, Xapian::termpos tpos, Xapian::termcount wdfinc) {
        need_terms();
        positions_modified = true;
    
        std::map<std::string, OmDocumentTerm>::iterator i = terms.find(tname);
        if (i == terms.end()) {
            ++termlist_size;
            OmDocumentTerm newterm(wdfinc);
            newterm.append_position(tpos);
            terms.insert(make_pair(tname, newterm));
        } else {
            // doc已经有这个term
            if (i->second.add_position(wdfinc, tpos)) {
                ++termlist_size;
            }
        }
    }

    1.3  加入非全新term,调用OmDocumentTerm对象的add_position,为OmDocumentTerm对象的positions添加元素,保证positions是升序的。在非首次插入position时,这里采用分批插入排序小技巧,减少了插入排序时的比较次数,值得阅读。注意:positions信息,在添加完成之后,并不是有序的,而是在把doc添加到DB之前,再做了一次merge。

    技巧:往一个有序数组里添加元素,一般写代码都会采用有序的插入:先定位到插入位置,然后数据往后移,最后插入,时间复杂度是O(n^2)。这里采用的方式,有点多路归并的味道:

    (1)数据分为历史数据和新增数据,在数据添加的过程中,需要保证两个数据组都是升序的,否则就需要对他们做merge合并;

    (2)当新加入的数据适合(符合升序要求,且当前新增数据组为空)放在历史数据组中,则直接在其尾部append;

    (3)否则,判断是否符合新增数据组要求(升序要求),合适则append到新增数据组中;

    (4)如果不合适,则要对历史数据组和新增数据组做merge,把新增数据组合并到历史数据组中,这个合并就是两个升序数组的合并,时间复杂度是O(n+m),合并完成之后,再重复(2)和(3)和(4)这个流程;

    (5)当数据添加完毕之后,可能新增数据组还没有合并到历史数据组中,这个合并的操作延迟到了doc添加到db的时候才做。

    实际代码中,历史数据组和新增数据组是合并在一起存放的,就一个vector,然后有一个变量记录当前历史数据组的位置。

    这个技巧下时间复杂度仍然是n^2,但实际耗时跟每次一个数字的插入排序相比,会降低几倍。

    这种设计思路,跟搜索引擎索引库常见的大小库(静、动库)设计是一样的。

    bool OmDocumentTerm::add_position(Xapian::termcount wdf_inc, Xapian::termpos tpos) {
        LOGCALL(DB, bool, "OmDocumentTerm::add_position", wdf_inc | tpos);
        if (rare(is_deleted())) {
            wdf = wdf_inc;
            split = 0;
            positions.push_back(tpos);
            return true;
        }
    
        wdf += wdf_inc;
    
        // Optimise the common case of adding positions in ascending order.
        if (positions.empty()) {
            positions.push_back(tpos);
            return false;
        }
        if (tpos > positions.back()) {
            if (split) {
                // Check for duplicate before split.
                auto i = lower_bound(positions.cbegin(), positions.cbegin() + split, tpos);
                if (i != positions.cbegin() + split && *i == tpos) {
                    return false;
                }
            }
            positions.push_back(tpos);
            return false;
        }
    
        if (tpos == positions.back()) {
            // Duplicate of last entry.
            return false;
        }
    
        if (split > 0) {
            // We could merge in the new entry at the same time, but that seems to
            // make things much more complex for minor gains.
            merge();
        }
    
        // Search for the position the term occurs at.  Use binary chop to
        // search, since this is a sorted list.
        vector<Xapian::termpos>::iterator i = lower_bound(positions.begin(), positions.end(), tpos);
        if (i == positions.end() || *i != tpos) {
            auto new_split = positions.size();
            if (sizeof(split) < sizeof(Xapian::termpos)) {
                if (rare(new_split > numeric_limits<decltype(split)>::max())) {
                    // The split point would be beyond the size of the type used to
                    // hold it, which is really unlikely if that type is 32-bit.
                    // Just insert the old way in this case.
                    positions.insert(i, tpos);
                    return false;
                }
            } else {
                // This assertion should always be true because we shouldn't have
                // duplicate entries and the split point can't be after the final
                // entry.
                AssertRel(new_split, <=, numeric_limits<decltype(split)>::max());
            }
            split = new_split;
            positions.push_back(tpos);
        }
        return false;
    }

    1.4  添加data信息

    void Xapian::Document::Internal::set_data(const string &data_) {
        data = data_;
        data_here = true;
    }

    2. Document加入到内存DB

        这里为了保证文档唯一,采用replace_document。

        做基本的参数检查之后,判断是否是多子索引库,如果是多子索引库则要判断数据写入到哪个子库中,同时要删除其它子索引库库中可能存在的同unique_term doc;

        判断倒排链里是不是存在这个unique_term,如果不存在则走添加流程;

    Xapian::docid WritableDatabase::replace_document(const std::string & unique_term, const Document & document) {
        LOGCALL(API, Xapian::docid, "WritableDatabase::replace_document", unique_term | document);
        if (unique_term.empty()) {
            throw InvalidArgumentError("Empty termnames are invalid");
        }
        size_t n_dbs = internal.size();
        if (rare(n_dbs == 0)) {
            no_subdatabases();
        }
        if (n_dbs == 1) {
            RETURN(internal[0]->replace_document(unique_term, document));
        }
    
        Xapian::PostingIterator postit = postlist_begin(unique_term);
        // If no unique_term in the database, this is just an add_document().
        if (postit == postlist_end(unique_term)) {
            // Which database will the next never used docid be in?
            Xapian::docid did = get_lastdocid() + 1;
            if (rare(did == 0)) {
                throw Xapian::DatabaseError("Run out of docids - you'll have to use copydatabase to eliminate any gaps before you can add more documents");
            }
            size_t i = sub_db(did, n_dbs);
            RETURN(internal[i]->add_document(document));
        }
    
        Xapian::docid retval = *postit;
        size_t i = sub_db(retval, n_dbs);
        internal[i]->replace_document(sub_docid(retval, n_dbs), document);
    
        // Delete any other occurrences of unique_term.
        while (++postit != postlist_end(unique_term)) {
            Xapian::docid did = *postit;
            i = sub_db(did, n_dbs);
            internal[i]->delete_document(sub_docid(did, n_dbs));
        }
    
        return retval;
    }

    2.1 添加新文档

    这里将添加文档的过程分为make_doc和finish_add_doc,可能是为了在真正的replace文档时,可以复用finish_add_doc的代码;

    Xapian::docid InMemoryDatabase::add_document(const Xapian::Document & document) {
        LOGCALL(DB, Xapian::docid, "InMemoryDatabase::add_document", document);
        if (closed) {
           InMemoryDatabase::throw_database_closed();
        }
        Xapian::docid did = make_doc(document.get_data());
        finish_add_doc(did, document);
    
        RETURN(did);
    }

    2.2  make_doc的实现

    Xapian::docid InMemoryDatabase::make_doc(const string & docdata) {
        termlists.push_back(InMemoryDoc(true));
        doclengths.push_back(0);
        doclists.push_back(docdata);
    
        AssertEqParanoid(termlists.size(), doclengths.size());
    
        return termlists.size();
    }

    2.3 finish_add_doc的实现

        首先添加value、构造term、填充termlist和postlist结构体。

        termlist,即为文章的词列表,含有所有的词信息:词名、词在本文章中出现的次数、词在本文章中出现的位置;

        postlist,即为词的文章列表,包含文章的信息,包括:docid、词在这个doc中出现的位置、词在这个doc中出现的次数;

        也就是说,position信息,要存储两份,termlist一份,postlist一份;

    void InMemoryDatabase::finish_add_doc(Xapian::docid did, const Xapian::Document &document) {
        {
            std::map<Xapian::valueno, string> values;
            Xapian::ValueIterator k = document.values_begin();
            for ( ; k != document.values_end(); ++k) {
                values.insert(make_pair(k.get_valueno(), *k));
                LOGLINE(DB, "InMemoryDatabase::finish_add_doc(): adding value " << k.get_valueno() << " -> " << *k);
            }
            add_values(did, values);
        }
    
        InMemoryDoc doc(true);
        Xapian::TermIterator i = document.termlist_begin();
        for ( ; i != document.termlist_end(); ++i) {
            make_term(*i);
    
            LOGLINE(DB, "InMemoryDatabase::finish_add_doc(): adding term " << *i);
            Xapian::PositionIterator j = i.positionlist_begin();
            if (j == i.positionlist_end()) {
                /* Make sure the posting exists, even without a position. */
                make_posting(&doc, *i, did, 0, i.get_wdf(), false);
            } else {
                positions_present = true;
                for ( ; j != i.positionlist_end(); ++j) {
                    make_posting(&doc, *i, did, *j, i.get_wdf());
                }
            }
    
            Assert(did > 0 && did <= doclengths.size());
            doclengths[did - 1] += i.get_wdf();
            totlen += i.get_wdf();
            postlists[*i].collection_freq += i.get_wdf();
            ++postlists[*i].term_freq;
        }
        swap(termlists[did - 1], doc);
    
        totdocs++;
    }

        在处理position信息的过程中,有些设计上不合理,在填充doc的时候,已经为position信息排序过一次,后面将position信息添加到termlist或者postlist的时候,又重新一个个position单独处理。

        文档添加到DB之后,需要执行commit,而内存索引没有落地磁盘,所以InMemoryDatabase的commit是空函数。

  • 相关阅读:
    软件工程课程总结
    c#代码分析
    运用visual studio进行简单的单元测试
    安装visual studio过程
    忙着,快乐着
    软件工程心得
    session
    XML
    期末团队评价
    黄金点游戏
  • 原文地址:https://www.cnblogs.com/cswuyg/p/10468033.html
Copyright © 2011-2022 走看看