Xipian源码研究报告二——Xipian数据处理

2022-04-27 00:00:00 数据 函数 文件 调用 方法
DataBase类



DataBase类的实现分别写在了omdatabase.cc和dbfactoy.cc中,在omdatabase.cc中主要是使用调用internal类的方法去实现自己的方法,那么核心就在于internal的实例化的地方,Xapian把DataBase类的构造函数放在dbfactoy.cc中,在这里通过判断传入Database中的路径下是否文件名称为iamchert或者iamflint或者iambrass的文件该文件在xapian的概念为version文件,来为internal决定实例化哪个子类。Internal被设计为抽象类,主要由BrassDatabase,ChertDatabase,FlintDatabase等类继承,下面就以BrassDatabase类为例介绍DataBase类。


在BrassDatabase数据库中主要由七张表构成(其实不是表只是表述这种关系的一种数据结构)。这七种分别是:

BrassPositionListTable 保存了每一个term出现在每一个document中的位置,存储类型为不压缩数据。

BrassTermListTable 保存了索引每个document的所有term。存储类型为压缩数据。

BrassRecordTable 保存了每一个document所关联的data,data不能通过query检索,只能通过document来获取。存储类型为压缩数据。

BrassPostListTable 保存了被每一个term索引的document,存储类型为不压缩数据。

BrassValueManager 保存了value的信息,这个结构并不直接存储。主要依赖于BrassPostListTable和BrassTermListTable。

BrassSynonymTable 保存了拼写纠正的数据,存储类型为压缩数据。

BrassSpellingTable 保存术语的字典,例如NBA、C#或C++等,存储类型为压缩数据。

Xapian所有的数据压缩全部基于zlib的数据压缩。

其中BrassPositionListTable,BrassTermListTable,BrassSynonymTable ,BrassSpellingTable继承于BrassLazyTable。BrassRecordTable,BrassPostListTable继承于BrassTable。而BrassLazyTable又继承于BrassTable。BrassTable提供了实际了数据的插入。主要是根据B树算法选择数据插入,查询的位置,以及真正的把内存数据写入和读取到文件系统中,在BrassTable中聚合了一个类叫BrassTable_base这个类调用系统的文件操作API对数据进行读写以及对底层的数据进行序列化和反序列化。文件操作API封装在在io_utl.cc文件中,根据不同的操作系统实现了调用不同的系统本地的API对文件进行操作。

在写数据时,所有的写放在了父类的BrassTable中,这里封装了B+树的相关操作的API。子类写的自己本身特性的部分,共性的部分都写在父类BrassTable中。

WriteDataBase类


WriteDataBase继承于DataBase类,DataBase主要用于数据读,而WriteDataBase主要用于数据的写入,因为在一开始的写入中不能通过数据库的vsersion判断数据库所使用的backends,所以Xapian提供了三个静态方法用于使用者去决定使用什么backends,在dbfactoy.cc中。以类似于Xapian::Brass::open(path,action,block_size)这种方式使用Brass命名空间下的静态方法去产生一个BrassWritableDatabase的对象,在调用BrassWritableDatabase的拷贝构造函数去产生一个BrassWritableDatabase对象。

xapian数据写流程


以上两节交代了xapian的存储数据结构,下面来讲述下数据写入的过程。

1. Xapian::WritableDatabase db(path,  action);调用此方法,会调用WritableDatabase::WritableDatabase(conststd::string &path, int action),然后会调用父类的无参构造函数DataBase(),在DataBase中提供了几种构造函数,在这里使用的是无参构造函数,父类无参构造函数中并未作任何事。然后会根据是否有iamflint,iamchert,iambrass决定new的子类,但是如果都没有定义的话,就会环境变量中是否有”XAPIAN_PREFER_BRASS”的值,如果有就实例化brassWriteDatabase,如果没有的话,按照代码流程就会优先选择chert的存储类型。下面以ChertWritableDatabase为例

2. 由于ChertWritableDatabase继承于ChertDatabase所有先构造父类,在Xapian中写流程产生存储文件的过程也在这一步。该步创建的文件类型如下:

Flintlock该文件Xapian是用来实现文件锁的文件,即单写多读。实现过程在Flintlock类中。

Iamchert该文件是版本文件,记录了uuid。

其中.DB保存了其中实际的数据。.baseA是开始空文件夹时创建的文件,.baseB是发生修改数据时创建文件。


此外在初始化这个数据库时,Xapian已经完成了把所有.DB文件打开然后保存了该文件描述符。之后就要用到document类了

3.

Document在Internal中就已经完成了,下面的继承类的方法不用于document的常用方法,主要实现就在omDocument.cc中。

在Xapian中常见的数据有三种:data、value、term,Document中就保存了这三种数据结构。他们分别是成员变量:

//这个把所有的slot即value的编号和value对应起来的结构

map<Xapian::valueno,string> document_values;

//每个term和自己的计数器对应一个map,用OmDocumentTerm表示每一个trem独立的计数器,也就是每个term的计数即wdf。Term每出现一次计数器就加1,且每个term的计数互不干扰。

map<string,OmDocumentTerm> document_terms;

//保存了data的信息

string data;

4.Document通过对外的接口完成Document的成员变量赋值。在执行add_document时候,也就是把document的相应的成员变量按照逻辑添加到数据库维护的那7个数据结构里面,后执行commit方法,把数据从内存提交到文件系统,在commit方法中,会分别调用七个数据结构自己的commit方法,也就是chertTable类中的方法flush_db()方法用文件读写的方式把数据写入.DB文件,而文件描述符一开始打开数据库就保存下来的。

WirteDataBase除了手动调用commit方法外,在WirteDataBase的析构函数也会提交数据。

Xapian数据读流程

1.实例化DataBase,Database::Database(const string &path),这里使用这个构造函数,区别于WriteDataBase调用的父类的无参构造函数。在这步的时候,根据了version判断出数据库选择的后端是哪个,同时打开了所有的.DB文件,保存了文件描述符,并且反序列化出了一部分数据,比如该数据库一共有多少document,这个数据是在数据库实例化就获得了。以chert为例,这层永远在chertTable这一层,这个框架可以参考我为DataBase所画的类图。

2.就要介绍Enquire和Query和Mset,先说Query。

个人觉得Query在读的地位和document在写的地位类似。Query提供了各种构造函数,具体的看官方文档。当多条件查询时,每个条件都会被添加到Query::Internal的成员变量subquery_list subqs中。Query还有一个重要概念叫op_t,这个再后来查询的时候会根据这个去实例化各种查询类。他的具体选项可以查官方手册。在这里我们根据需求得到了所需要的一个多条件的Query对象。

3.



 采用了Xapian::Enquire enquire(db)的方式,我们对Enquire完成了实例化。在Enquire中有个成员变量Query query。你把你前面得到的Query对象给这个成员变量赋值。接下来就是Enquire的核心方法get_mset,这个函数的返回值是Mset,数据的查询就依赖于Mset的成员变量vector<Xapian::Internal::MSetItem>items;


首先我们这边主要用的是本地查询不牵扯stub文件也不牵扯远程数据库,也就是get_mset里面涉及到的db个数只为一个。在get_mset中使用MultiMatch类中传入了查询的参数,因为我们使用的是本地查询Xapian::Internal::RefCntPtr<SubMatch> smatch实例化的是 LocalSubMatch。因为这个函数主要返回的类型是Mset,不难分析出构建这个Mset类型主要依赖于PostList *pl,而pl又是通过LocalSubMatch的get_postlist_and_term_info方法获得,跟进这个方法发现pl是由这个类QueryOptimiser构成。接下来是复杂的UML类图!

在QueryOptimiser中进入do_subquery方法,在这里根据了Query的OP选项做了不同的处理。

首先我们ExactPhrasePostList,PhrasePostList,NearPostList这三个继承于SelectPostList,当调用SelectPostList的next方法时,会调用到这三个子类的start_position_list()方法,这里的子类中有成员变量std::vector<PostList*> terms,在这里这个类有三大子类ChertPostList,BrassPostList,FlintPostList这三个类在这里我以chert为例,实例化了ChertPostList这个方法,在ChertPostList聚合了ChertPositionList这个类,而在一层层的嵌套之后,执行next方法其实是进入了这个类ChertPositionList的read_data()方法,在这个方法里面才是使用了文件接口read了文件的数据。


接着一开始的说,我们进入了do_subquery,假设进入了个分支,op的选项为OP_LEAF,即我只根据term的字段查询document,然后进入localsubmatch.postlist_from_op_leaf_query()方法,后在ChertDatabase::open_post_list中我们返回了newChertPostList,也就是根据这个类,一直返回到上层在MutilMatch中的get_mset中的pl,根据这个pl产生的参数够早了了Mset。查询流程完成。

在DataBase初始化的时候,数据已经按照设置好的block_size大小从文件读入了内存,然后get_mset这部按照query再一次设置了内存块的数据。在这之后执行遍历Mset的迭代器,根据了得到了document的ID,然后根据ID然后再去find_block,然后把根据从内存块中查取出来。


相关文章