首页 > 数据库 > > Xapian索引-文档检索过程分析
Xapian::Query term_one = Xapian::Query("T世界"); Xapian::Query term_two = Xapian::Query("T比赛"); Xapian::Query query = Xapian::Query(Xapian::Query::OP_OR, term_one, term_two); // query组装 std::cout << "query=" << query.get_description() << std::endl; Xapian::Enquire enquire(db); enquire.set_query(query); Xapian::MSet result = enquire.get_mset(, 10); // 执行检索,获取结果 std::cout << "find results count=" << result.get_matches_estimated() << std::endl; for (auto it = result.begin(); it != result.end(); ++it) { Xapian::Document doc = it.get_document(); std::string data = doc.get_data(); double doc_score_weight = it.get_weight(); int doc_score_percent = it.get_percent(); std::cout << "doc=" << data << ",weight=" << doc_score_weight << ",percent=" << doc_score_percent << std::endl; }
Query::Query(const string & term, Xapian::termcount wqf, Xapian::termpos pos) : internal(new Xapian::Internal::QueryTerm(term, wqf, pos)) { LOGCALL_CTOR(API, "Query", term | wqf | pos); }
Query(op op_, const Xapian::Query & a, const Xapian::Query & b) { init(op_, 2); bool positional = (op_ == OP_NEAR || op_ == OP_PHRASE); add_subquery(positional, a); add_subquery(positional, b); done(); }
MSet Enquire::Internal::get_mset(Xapian::doccount first, Xapian::doccount maxitems, Xapian::doccount check_at_least, const RSet *rset, const MatchDecider *mdecider) const { LOGCALL(MATCH, MSet, "Enquire::Internal::get_mset", first | maxitems | check_at_least | rset | mdecider); if (percent_cutoff && (sort_by == VAL || sort_by == VAL_REL)) { throw Xapian::UnimplementedError("Use of a percentage cutoff while sorting primary by value isn't currently supported"); } if (weight == ) { weight = new BM25Weight; // 如果外界没有指定打分策略,采用BM25Weight } Xapian::doccount first_orig = first; { Xapian::doccount docs = db.get_doccount(); first = min(first, docs); maxitems = min(maxitems, docs - first); check_at_least = min(check_at_least, docs); check_at_least = max(check_at_least, first + maxitems); } AutoPtr<Xapian::Weight::Internal> stats(new Xapian::Weight::Internal); // 用于记录打分用的全局信息
// MultiMatch对象的初始化,会执行检索的初始化工作,譬如:填充stats对象,
::MultiMatch match(db, query, qlen, rset, collapse_max, collapse_key, percent_cutoff, weight_cutoff, order, sort_key, sort_by, sort_value_forward, time_limit, *(stats.get()), weight, spies, (sorter.get() != NULL), (mdecider != NULL)); // Run query and put results into supplied Xapian::MSet object. MSet retval; match.get_mset(first, maxitems, check_at_least, retval, *(stats.get()), mdecider, sorter.get()); // 检索 if (first_orig != first && retval.internal.get()) { retval.internal->firstitem = first_orig; } Assert(weight->name() != "bool" || retval.get_max_possible() == ); // The Xapian::MSet needs to have a pointer to ourselves, so that it can // retrieve the documents. This is set here explicitly to avoid having // to pass it into the matcher, which gets messy particularly in the // networked case. retval.internal->enquire = this; if (!retval.internal->stats) { retval.internal->stats = stats.release(); } RETURN(retval); }
static void prepare_sub_matches(vector<intrusive_ptr<SubMatch> > & leaves, Xapian::Weight::Internal & stats) { LOGCALL_STATIC_VOID(MATCH, "prepare_sub_matches", leaves | stats); // We use a vector<bool> to track which SubMatches we're already prepared. vector<bool> prepared; prepared.resize(leaves.size(), false); size_t unprepared = leaves.size(); bool nowait = true; while (unprepared) { for (size_t leaf = ; leaf < leaves.size(); ++leaf) { if (prepared[leaf]) continue; SubMatch * submatch = leaves[leaf].get(); if (!submatch || submatch->prepare_match(nowait, stats)) { prepared[leaf] = true; --unprepared; } } // Use blocking IO on subsequent passes, so that we don't go into // a tight loop. nowait = false; } }

bool LocalSubMatch::prepare_match(bool nowait, Xapian::Weight::Internal & total_stats) { LOGCALL(MATCH, bool, "LocalSubMatch::prepare_match", nowait | total_stats); (void)nowait; Assert(db); total_stats.accumulate_stats(*db, rset); RETURN(true); }

void Weight::Internal::accumulate_stats(const Xapian::Database::Internal &subdb, const Xapian::RSet &rset) { #ifdef XAPIAN_ASSERTIONS Assert(!finalised); ++subdbs; #endif total_length += subdb.get_total_length(); collection_size += subdb.get_doccount(); rset_size += rset.size(); total_term_count += subdb.get_doccount() * subdb.get_total_length(); Xapian::TermIterator t; for (t = query.get_unique_terms_begin(); t != Xapian::TermIterator(); ++t) { const string & term = *t; Xapian::doccount sub_tf; Xapian::termcount sub_cf; subdb.get_freqs(term, &sub_tf, &sub_cf); TermFreqs & tf = termfreqs[term]; tf.termfreq += sub_tf; tf.collfreq += sub_cf; } const set<Xapian::docid> & items(rset.internal->get_items()); set<Xapian::docid>::const_iterator d; for (d = items.begin(); d != items.end(); ++d) { Xapian::docid did = *d; Assert(did); // The query is likely to contain far fewer terms than the documents, // and we can skip the document's termlist, so look for each query term // in the document. AutoPtr<TermList> tl(subdb.open_term_list(did)); map<string, TermFreqs>::iterator i; for (i = termfreqs.begin(); i != termfreqs.end(); ++i) { const string & term = i->first; TermList * ret = tl->skip_to(term); Assert(ret == NULL); (void)ret; if (tl->at_end()) { break; } if (term == tl->get_termname()) { ++i->second.reltermfreq; } } } }
void MultiMatch::get_mset(Xapian::doccount first, Xapian::doccount maxitems, Xapian::doccount check_at_least, Xapian::MSet & mset, Xapian::Weight::Internal & stats, const Xapian::MatchDecider *mdecider, const Xapian::KeyMaker *sorter) { ........ }
PostList * LocalSubMatch::get_postlist(MultiMatch * matcher, Xapian::termcount * total_subqs_ptr) { LOGCALL(MATCH, PostList *, "LocalSubMatch::get_postlist", matcher | total_subqs_ptr); if (query.empty()) { RETURN(new EmptyPostList); // MatchNothing } // Build the postlist tree for the query. This calls // LocalSubMatch::open_post_list() for each term in the query. PostList * pl; { QueryOptimiser opt(*db, *this, matcher); pl = query.internal->postlist(&opt, 1.0); *total_subqs_ptr = opt.get_total_subqs(); } AutoPtr<Xapian::Weight> extra_wt(wt_factory->clone()); // Only uses term-independent stats. extra_wt->init_(*stats, qlen); if (extra_wt->get_maxextra() != 0.0) { // There's a term-independent weight contribution, so we combine the // postlist tree with an ExtraWeightPostList which adds in this // contribution. pl = new ExtraWeightPostList(pl, extra_wt.release(), matcher); } RETURN(pl); }

PostingIterator::Internal * QueryOr::postlist(QueryOptimiser * qopt, double factor) const { LOGCALL(QUERY, PostingIterator::Internal *, "QueryOr::postlist", qopt | factor); OrContext ctx(qopt, subqueries.size()); do_or_like(ctx, qopt, factor); RETURN(ctx.postlist()); }

void QueryBranch::do_or_like(OrContext& ctx, QueryOptimiser * qopt, double factor, Xapian::termcount elite_set_size, size_t first) const { LOGCALL_VOID(MATCH, "QueryBranch::do_or_like", ctx | qopt | factor | elite_set_size); // FIXME: we could optimise by merging OP_ELITE_SET and OP_OR like we do // for AND-like operations. // OP_SYNONYM with a single subquery is only simplified by // QuerySynonym::done() if the single subquery is a term or MatchAll. Assert(subqueries.size() >= 2 || get_op() == Query::OP_SYNONYM); vector<PostList *> postlists; postlists.reserve(subqueries.size() - first); QueryVector::const_iterator q; for (q = subqueries.begin() + first; q != subqueries.end(); ++q) { // MatchNothing subqueries should have been removed by done(). Assert((*q).internal.get()); (*q).internal->postlist_sub_or_like(ctx, qopt, factor); } if (elite_set_size && elite_set_size < subqueries.size()) { ctx.select_elite_set(elite_set_size, subqueries.size()); // FIXME: not right! } }
LeafPostList * LocalSubMatch::open_post_list(const string& term, Xapian::termcount wqf, double factor, bool need_positions, bool in_synonym, QueryOptimiser * qopt, bool lazy_weight) { LOGCALL(MATCH, LeafPostList *, "LocalSubMatch::open_post_list", term | wqf | factor | need_positions | qopt | lazy_weight); bool weighted = (factor != 0.0 && !term.empty()); LeafPostList * pl = NULL; if (!term.empty() && !need_positions) { if ((!weighted && !in_synonym) || !wt_factory->get_sumpart_needs_wdf_()) { Xapian::doccount sub_tf; db->get_freqs(term, &sub_tf, NULL); if (sub_tf == db->get_doccount()) { // If we're not going to use the wdf or term positions, and the // term indexes all documents, we can replace it with the // MatchAll postlist, which is especially efficient if there // are no gaps in the docids. pl = db->open_post_list(string()); // Set the term name so the postlist looks up the correct term // frequencies - this is necessary if the weighting scheme // needs collection frequency or reltermfreq (termfreq would be // correct anyway since it's just the collection size in this // case). pl->set_term(term); } } } if (!pl) { const LeafPostList * hint = qopt->get_hint_postlist(); if (hint) pl = hint->open_nearby_postlist(term); if (!pl) pl = db->open_post_list(term); qopt->set_hint_postlist(pl); } if (lazy_weight) { // Term came from a wildcard, but we may already have that term in the // query anyway, so check before accumulating its TermFreqs. map<string, TermFreqs>::iterator i = stats->termfreqs.find(term); if (i == stats->termfreqs.end()) { Xapian::doccount sub_tf; Xapian::termcount sub_cf; db->get_freqs(term, &sub_tf, &sub_cf); stats->termfreqs.insert(make_pair(term, TermFreqs(sub_tf, , sub_cf))); } } if (weighted) { Xapian::Weight * wt = wt_factory->clone(); if (!lazy_weight) { wt->init_(*stats, qlen, term, wqf, factor); // BM25Weight::init()计算不涉及query跟doc相关性部分的打分(只跟term和query相关) stats->set_max_part(term, wt->get_maxpart()); } else { // Delay initialising the actual weight object, so that we can // gather stats for the terms lazily expanded from a wildcard // (needed for the remote database case). wt = new LazyWeight(pl, wt, stats, qlen, wqf, factor); } pl->set_termweight(wt); } RETURN(pl); }

void BM25Weight::init(double factor) { Xapian::doccount tf = get_termfreq(); double tw = ; if (get_rset_size() != ) { Xapian::doccount reltermfreq = get_reltermfreq(); // There can't be more relevant documents indexed by a term than there // are documents indexed by that term. AssertRel(reltermfreq,<=,tf); // There can't be more relevant documents indexed by a term than there // are relevant documents. AssertRel(reltermfreq,<=,get_rset_size()); Xapian::doccount reldocs_not_indexed = get_rset_size() - reltermfreq; // There can't be more relevant documents not indexed by a term than // there are documents not indexed by that term. AssertRel(reldocs_not_indexed,<=,get_collection_size() - tf); Xapian::doccount Q = get_collection_size() - reldocs_not_indexed; Xapian::doccount nonreldocs_indexed = tf - reltermfreq; double numerator = (reltermfreq + 0.5) * (Q - tf + 0.5); double denom = (reldocs_not_indexed + 0.5) * (nonreldocs_indexed + 0.5); tw = numerator / denom; } else { tw = (get_collection_size() - tf + 0.5) / (tf + 0.5); } AssertRel(tw,>,); // The "official" formula can give a negative termweight in unusual cases // (without an RSet, when a term indexes more than half the documents in // the database). These negative weights aren't actually helpful, and it // is common for implementations to replace them with a small positive // weight or similar. // // Truncating to zero doesn't seem a great approach in practice as it // means that some terms in the query can have no effect at all on the // ranking, and that some results can have zero weight, both of which // are seem surprising. // // Xapian 1.0.x and earlier adjusted the termweight for any term indexing // more than a third of documents, which seems rather "intrusive". That's // what the code currently enabled does, but perhaps it would be better to // do something else. (FIXME) #if 0 if (rare(tw <= 1.0)) { termweight = ; } else { termweight = log(tw) * factor; if (param_k3 != ) { double wqf_double = get_wqf(); termweight *= (param_k3 + 1) * wqf_double / (param_k3 + wqf_double); } } #else if (tw < 2) tw = tw * 0.5 + 1; termweight = log(tw) * factor; if (param_k3 != ) { double wqf_double = get_wqf(); termweight *= (param_k3 + 1) * wqf_double / (param_k3 + wqf_double); } #endif termweight *= (param_k1 + 1); LOGVALUE(WTCALC, termweight); if (param_k2 == && (param_b == || param_k1 == )) { // If k2 is 0, and either param_b or param_k1 is 0 then the document // length doesn't affect the weight. len_factor = ; } else { len_factor = get_average_length(); // len_factor can be zero if all documents are empty (or the database // is empty!) if (len_factor != ) len_factor = 1 / len_factor; } LOGVALUE(WTCALC, len_factor); }
/// 注:在调用这个函数之前会先调用next_helper函数,将第一条链表的位置向前移动一位(如果是首次调用则不移动),
/// find_next_match函数让plist[0]倒排链定位到合适的位置,当能定位到合适的位置(plist[0]和plist[1]有交集)则返回,
/// 否则说明没有交集,设置did=0后返回;调用者会通过判断did==0来确定当前链表交集是否已经做完;
PostList * MultiAndPostList::find_next_match(double w_min) { advanced_plist0: if (plist[]->at_end()) { did = ; return NULL; } did = plist[]->get_docid(); for (size_t i = 1; i < n_kids; ++i) { bool valid; check_helper(i, did, w_min, valid); if (!valid) { next_helper(, w_min); goto advanced_plist0; } if (plist[i]->at_end()) { did = ; return NULL; } Xapian::docid new_did = plist[i]->get_docid(); if (new_did != did) {
/// 两条链表的pos元素不相等,只可能是因为plist[0].pos的元素比较小,需要向前移 skip_to_helper(, new_did, w_min); goto advanced_plist0; } } return NULL; }
double LeafPostList::get_weight() const { if (!weight) return ; Xapian::termcount doclen = , unique_terms = ; // Fetching the document length and number of unique terms is work we can // avoid if the weighting scheme doesn't use them. if (need_doclength) doclen = get_doclength(); if (need_unique_terms) unique_terms = get_unique_terms(); double sumpart = weight->get_sumpart(get_wdf(), doclen, unique_terms); // 这里对某个doc的最终BM25打分做了汇总,利用到了前面计算的第一和第三部分打分 AssertRel(sumpart, <=, weight->get_maxpart()); return sumpart; }
来自 “ ITPUB博客 ” ,链接:https://blog.itpub.net/26692020/viewspace-3125432/ ,如需转载,请注明出处,否则将追究法律责任。