• Xapian简明教程(未完成)


    第一章 简介

    1.1 简介

    Xapian是一个开源的搜索引擎库,它可以让开发者自定义的开发一些高级的的索引和查找因素应用在他们的应用中。

    通过阅读这篇文档,希望可以帮助你创建第一个你的索引数据库和了解一些基本概念,并提供了了代码作为参看。

    如果你已经安装了Xapian,并且只想看实例代码,可以跳过 “Xapian-核心概念”章节,直接到 “Xapian-Hello World”。

    1.2 安装

    安装Xapian包括两部分,一部分是Xapian核心库自身,还有一部分是你要使用Xapian的编程语言。我们写这个文档大部分是用PHP作为例子。

    这篇文档使用的是Xapian1.2

    1.3 CentOS安装Xapian

    xapian-core

    xapian的一些接口,核心库

    cd xapian-core-1.2.15

    ./configrue -prefix=/opt

    error:

    Neither uuid/uuid.h nor uuid.h found - required for brass

    yum install libuuid-devel

    make && make install

    xapian-bindings

    通过多种语言来调用xapian接口

    cd xapian-bindings-1.2.15

    ./configure --prefix=/usr/local/xapian XAPIAN_CONFIG=/opt/bin/xapian-config --with-php

    注意:这里我的PHP版本是PHP5.4 有可能版本不对导致xapian-bindings检测不到php

    make&&make isntall

    php-config --extension-dir一下,xapian-bindings已经把xapian.so复制到extensions下面了

    修改php.ini

    extension=/usr/local/lib/php/extensions/no-debug-non-zts-20100525/xapian.so

    php -m|grep xapian 一下已经安装了xapian扩展了

    第二章 核心概念

    2.1 并发

    Xapian尽管可以用在多线程的程序中,但是它本身并不直接支持多线程。在将Xapian应用在多线程程序中前确保你已经知道一下描述的细节。

    Xapian没有保持任何全局状态的变量,在线程的程序中并不共享任何对象,所以你可以安全的使用Xapian在多线程编程中。在实践中,这并不算个问题-每个线程可以创建自己的XapianDatabase对象,并且他们可以各自工作的很好。

    如果你真的想访在不同线程中访问同一个Xapian对象,那么你需要确保各个进程不是同时访问(如果你不能保证的话就可能会出问题,比如数据损坏或者数据库Crash)。有种方法防止多线程同时访问,可以当线程访问同一个Xapian对象的时候给线程加一个排他锁在一个互斥量上。

    Xapian不包括线程间锁有关的代码,这样只会增加不必要的开销。在实践中,完全可以让调用者来给那些Xapian指令加锁,而不是让Xapian自身做这些事。

    注意,Xapian对象将保持一些内部的引用--例如,如果你调用XapianDatabase->get_document(),返回的结果XapianDocument对象将会保持一个引用指向XapianDatatbase对象,所以你没法安全的使用XapianDatabase对象在一个线程,同时使用XapianDocument对象在另一个线程。

    2.2 Indexing 标引

    2.2.1 Databases 数据库

    很多Xapian操作是以XapianDatabase对象为中心的。在检索前,被检索的文档要被先放到数据库中;检索进程然后根据给定的查询语句从数据库中高效选择出最匹配的结果。文档被放到数据库里这个过程叫做标引

    大部分信息存储在数据库中都是一个从Term(词项)映射到Document(文档)列表来存储的。为了可以展示信息,也需要存储文档中的文本或者摘要。数据库还可以包括一些比如拼写纠正,同义词扩展,同时开发者甚至可以存储任意的键值对在数据库中。

    XapianDatabase存储数据通常格式化为让检索程序可以检索非常快的格式;Xapian没有用其他数据库作为存储引擎。有很多种Xapian数据库存储引擎,Xapian1.2系列版本使用最主要的存储引擎叫做Chert。Chert存储信息在文件系统中。如果你熟悉数据结构,你可能会对这个后台使用的一种写时复制的B+树感兴趣,如果你不懂数据结构也没事儿。

    多数存储引擎发布以后,至少可以保证原来老版本的Xapian存储引擎还可以用。目前,所有的存储引擎在同一时间只支持单进程写入;如果尝试同时有两个进程在写入数据库将会抛出XapianDabaseLockError异常,说明进程没加锁。但多进程同时读是支持的。

    当一个数据库被一个读进程打开开始读数据的时候,这个进程就指向了一个这个数据库的固定数据库快照(本质上是 Multi-Version Concurrency Control多版本并发控制),发生更新操作的时候,对于这个读进程是不可见的,除非他调用了XapianDatabase->reopen()。

    目前Xapian的磁盘存储受到由于多版本并发控制实现的限制--特别是有两个版本存在并发的情况。所以当一个进程没有任何限制的访问自己的数据库快照的时候,有一个写进程修改了一部分数据并且提交了,然后写进程又修改了一部分数据,如果读进程访问被写进程改变的数据的时候,会接收到XapianDatabaseModifiedError。在这种情况下,读进程可以通过XapianDatabase->reopen()方法将自身快照更新到最新版本。

    这些基于磁盘的Xapian存储引擎,当数据库被打开并开始写入数据的时候,数据库会持有一个数据库的锁来确保不会并发的写入。这个锁当数据库写入进程关闭的时候将被释放(或者是写入进程死亡的时候自动释放锁)。

    一个比较有特色的Xapian锁原理(至少在POSIX系列操作系统)在于Xapian fork一个子进程来持有锁,而不是主进程来持有锁。这样做是避免锁由于 the slightly unhelpful semantics of fcntl locks(这句不大理解)被意外释放。

    Xapian还可以把数据库存储在远程机器上,通常通过网络协议来访问数据库。

    在检索的时候,Xapian可以跨数据库检索。Xapian会将结果合并到一起。这个特性可以和远程数据库相结合来处理数据集合比价大,一台机器处理不了的情况。

    Xapian还支持一个特性,可以在不同机器上的数据库间做同步复制。而且这个复制是增量复制,只复制被修改的数据;这个特性可以用来做冗余备份和负载均衡。

    2.2.2 Documents 文档

    Xapian检索结果返回的都是一个个文档。当你构建一个新的检索系统时,很关键的一件事是决定在你的系统里存储什么样的文档。这将会有很多可以替换的形式。例如,对于你一搜索网页的检索系统,可能会很自然的选择存储整张网页。然而,你可以也可以选择存储一个段落,或者是把几张网页合成一张存储。

    数据库区分不同文档通过一个无符号的32位Int id,也叫Document ID(文档ID)。

    组成文档有3个单元:data(数据),terms(语词),values(值)。我们先来讨论下Term(语词)和Data--Values在用于支持很多高级检索方式的时候很有用。

    Document Data 文档数据

    Document data是一些的二进制数据。Xapian对于这些是完全不透明的,除了存储(通过zlib压缩)这写数据,其他什么都干不了,就只能当访问的时候拿出来。

    data可以用来存储一些文本或者数据库主键的id(这个主键id可以在外部数据库表里查到对应的行),可以理解为卫星数据。

    通常,你需要展示什么给用户(或者是想要的到什么检索结果),就存储什么为Document data。Xapian没要求强制序列化以后再存储到document data:基于你的应用,你可以用 来分割数据,也可也通过什么JSON或者XML序列化方式来存储你的数据。

    2.2.3 Trems 语词

    Terms(语词) 是Xapian检索时候的基础。简单的来说,一个检索过程就是对检索语句中指定的词和每篇文档中的语词作比较,然后返回一个最匹配的列表。

    语词经常就是由一个文本中每个单词通过变形(最常见的比如全部变成小写)生成来的。也还有很多种分词策略。

    经常一个语词在文本出出现很多次。Xapian称这个次数为within document frequency(文档内词频)并且存储在数据库中。这个东西对于检索结果的权重有影响(比如 the a 这种词)

    It is also possible to store a set of positions along with each term; this allows the positions at which words occur to be used when searching, e.g., in a phrase query. These positions are usually stored as word counts (rather than character or byte counts).

    用一个集合来存储每个语词在文档中出现的次序也是很有必要的。对于这段的理解比较模糊。

    数据库还跟踪记录了几对关于语词的频率;term frenquency是在文档中几个指定词出现的频率,coolection frequency是term frenquency的和。

    2.2.4 Stemmers 词根

    把一个文本规范化的过程交过stemming(词根还原)。这个过程将众多词的形式转换为一种形式,例如英文中的将birds转换为bird。

    注意词根并不一定是一个真实存在的单词;而只是接近于单词词义的一种转换,使得搜索的时候可以找到他们。例如,happy 和 happiness 可以都转换为 happi,所以如果一个文档包含happiness,在检索的时候检索happy可以找到这个文档。

    2.2.5 Term Generator 分词器

    Xapian不用让用户写代码来将文本处理成语词用来建索引,Xapian提供了一个XapianTermGenerator类。这个类将文本分片,然后处理得出合适的语词,然后建立语词到文档的索引关系。

    XapianTermGenerator可以通过配置来处理分词。他可以存储词在文本中的次序,还可以指定特定的前缀到分词后的语词,从而允许检索的时候超找特定字段。它甚至可以添加一些信息到数据库中用来处理拼写纠正。

    如果你正在使用XapianTermGenerator来处理文本,你非常有可能使用QueryParser(稍后会介绍)来处理检索。

    2.2.6 使用Xapian的identifiers 标示符

    每个存储在Xapian数据库中的文档都有一个指定的或者自增的不重复id值。也叫docid,但是Xapian是个检索系统,不同于mysql可以通过主键查询,你没法通过docid找到文档,所以你需要对每个文档有个标示符。

    通常文档在你创建索引的时候Xapian已经给它指定了一个docid值,有可能你会重建索引,更新一个已经存在的文档,或者从Xapian索引中删除一个已经过期的文档。有两种方式可以达到你的目的。

    一种方式是创建一个你的标示符和Xapian docid的一对一的映射。这种方式是基于你的标示符是32位的无符号整数。

    另一种方式是把标示符作为一个特殊的term词来存储。可以给这个词加个前缀(通常是Q)来避免和其他词冲突。term有个长度限制(在chert存储引擎中是245 bytes),所以如果你的标示符很长,你需要做些复杂的处理。

    2.2.7 Values 值

    values(文档值)给人的感觉像是一种更灵活的terms(语词)。每个文档可以有很多值,这些文档值都是文档数据的一部分,用于更加高级的检索。文档值是你想对文档数据按照某列排序的值,或者是想检索一个时间范围的一个时间值,或者是一个数字用来影响检索结果现实时候排序的权重。

    每个值被存储在一个数字标号的slot(槽位)上;例如,一个文档,有一个值代表分类在槽位0上,一个值代表价格在槽位1上,一个值代表文档的重要程度在槽位10上。槽位的数据可以使是任何32为无符号整数,除了0xffffffff,这个槽位有个特殊的含义(它是Xapian::BAD_VALUENO)。

    Xapian把槽位上的内容当做不透明的二进制字符串,甚至是数字类型。Xapian提供了一个工具函数sortable_serialise来序列化数字类型的值,转换成二进制字符串以后才能对这个槽位上的值用范围检索或者排序。

    很重要的一点,尽量少的在文档的值上存储数据,因为在检索的时候,文档值可能都被读了一遍,文档值太大会使检索变慢。有些开发这经常写很多文档值,而这些值的数据本来应该存储在文档的data区域;能不这么做就别这样。

    2.2.8 标引限制

    对于文档的数据和标引有些限制;达到这些限制的可能很小,但是还是很值得了解的。

    Term(语词)长度

    term最大长度是245字节,注意0字节在term中内部是用2个字节编码的,所以当term中含有0字节,term最大长度就比245字节小了。

    term特别长的情况很罕见,但是有一种情况,当把URL作为一个term的时候。一个变通的方式来解决term最大长度的限制的方法是可以把URL做哈希。

    文档数据 长度

    文档数据的最大长度取决于操作系统文件块大小和一些其他因素,默认每块是8KB,文档数据的长度限制在100MB。

    文档值 长度

    文档值长度限制和文档数据的长度限制差不多,但是,由于性能原因,最好存储的文档值长度别太大,读100MB+ 的文档值在检索的时候会比较慢。

    文档ID

    文档ID是32位的,2^32-1,接近4.3亿。删除一个文档以后,文档ID在自增方式指定文档ID的情况下不会被重用。你可以通过压缩数据库来回收这些不用的文档ID

    2.3 检索

    2.3.1 查询

    在Xapian中查询的原理是和你所查文档的不同有关。可以使用简单的查找基于文本的term(语词)或者是一个已经设定的文档值,这些查询都可以通过将各种不同的查询方法进行组合最终实现复杂查询。

    简单查询

    最基础的查询是查找一个文本的term(语词)。这将会罗列出所有包含这个term(语词)的文档。例如,查询term(语词)“wood”。

    查询还可以通过指定槽位和值操作符来匹配文档中的文档值。

    当一个查询被执行以后,查询结果是一个和查询匹配成功文档的列表和一个表示文档和查询匹配度的权值。

    逻辑运算符

    有多个查询时,每个查询产生一个文档的列表,每个文档有一个查询匹配度。这些查询可以被组合产生一个查询结构树,查询逻辑运算符就像是树的分支。

    最基础的运算符是逻辑运算符:OR,AND,AND_NOT

    OP_OR-匹配文档列表A和B的并集

    OR_AND-匹配文档列表A和B的交集

    OP_AND_NOT-匹配文档列表A和非B的交集

    操作符对于每个匹配得文档产生一个权值,这个权值是基于两边的子查询有如下规则:

    OP_OR-匹配文档的权值A和B的和

    OP_AND-匹配文档的权值A和B的和

    OP_AND_NOT-匹配文档的权值A

    Maybe

    作为基础逻辑运算符的补充,还有一种逻辑运算符OP_AND_MAYBE,这个运算符匹配文档列表A(不管B批不匹配)。如果只有B匹配,OP_AND_MAYBE不匹配。对于这个操作符,权值是子查询的和,所以:

    1.文档匹配A和B权值是A+B

    2.文档值匹配A权值是A

    过滤器

    一个查询可以被另一个查询过滤。有两种方式用来过滤一个查询是包含还是不包含文档。

    OP_FILTER-匹配两边子查询的文档,但是权值以左边为准

    OP_AND_NOT-匹配左边子查询,但是不匹配右边子查询的文档(权值以左边为准)

    值范围

    当使用文档值的时候,有三个相关的操作符:

    OP_VALUE_LE-匹配给定值比文档值小的

    OP_VALUE_GE-匹配给定值比文档值大的

    OP_VALUE_RANGE-匹配给定值在给定文档值范围中间的。(闭区间)

    在使用这三个操作符时,决定是包含还是不包含文档且不影响文档的权值。

    Near和Pharse

    两个额外的常用操作符,NEAR,

    2.3.2 查询分析器

    为了让查询Xapian数据库更简单,Xapian提供了一个XapianQueryParser类,可以把查询的字符串转换为查询对象。例如:

    apple AND a NEAR word OR "a phrase" NOT(too difficult)+eh

    以上的例子展示了XapianQueryParser如何截获一些基本的修饰符。语句支持的操作符是在之前就已经定义的。例如:

    apple AND pear匹配同时出现apple和pear这两个词的文档。

    apple OR pear匹配任何出现apple或pear一个词的文档。

    apple NOT pear匹配出现apple但是没有pear的文档。

    分词

    查询分析器通过一个内部程序将查询语句转换成语词。这和在建立索引时用分词器将字符串转换成语词差不多。同一个数据库把QueryParser(查询分析器)和TermGenerator(分词器)一起使用比较简单。

    通配符

    通配符可以匹配任意数量在词末尾的字符串;例如:

    wild* 匹配 wild,wildcard,wildcat

    这个特性默认是不可用的;想要开启他,可以参照后面的 “开启特性”章节

    括号

    当一个查询语句同时包含OR和AND操作符的时候,AND的优先级更高。使用括号可以改变这种优先级。例如,一下的查询语句:

    apple OR pear AND dessert

    查询分析器会这样切分这条查询语句:

    apple OR (pear AND dessert)

    所以,为了改变这种优先级需要使用括号。你需要在查询语句的开头这样写:

    (apple OR pear) AND dessert

    默认操作符

    当查询分析器获得一个查询语句的时候,他会链接各各查询语句通过默认的操作符OP_OR。这个默认的操作符可在运行时进行修改 。

    其他操作符

    XapianQueryParser除了提供基础的逻辑操作符还提供了一些新的操作符,例如:

    apple NEAR dessert president "united states" -horse +recipe  +apple dessert

    可以看到新的操作符+和-。

    “race condition” -horse 匹配所有包含语句race condition但是不包括horse的文档;

    +recipe desert 匹配所有包含recipe的文档;然后所有包含recipe的文档根据权重desert排序。

    注意的是所有+/-运算符基于默认运算符,以上例子默认运算符是OP_OR。

    通过前缀检索

    当数据库中的文档已经对语词设置好了前缀以后,查询分析器就可以分析一个更符合人类阅读习惯的语词来应用到前缀字段上。例如:

    author:"whilliam shakespeare" title:juliet

    范围

    查询分析器可以在文档值上做范围查询,按照给定的范围,匹配文档值。有很多种类型的范围查找处理器,在这里讨论的两种时间和数字。

    为了使用范围查询,需要告诉查询分析器哪个文档值是将要被查询的。然后像如下这样查询范围:

    $10..50

    5..10$

    01/01/1970..01/03/1970

    size:3..7

    日期时间范围需要被配置,你可以配置哪种日期格式化方式。

    停用词

    Xapian支持指定一个停用词列表。查询语句在处理前会移除包含停用词列表的词。  

    解析标记

    XapianQueryParser可以设置可用的操作符,设置的结果是按位或下面几个标记:

    FLAG_BOOLEAN:开启AND OR () 表达式

    FLAG_PHRASE: 开启段落表达式

    FLAG_LOVEHATE: 开启 +/- 操作符

    FLAG_BOOLEAN_ANY_CASE: 开启大小写转换操作

    FLAG_WILDCARD: 开启通配符

    默认情况下,XapianQueryParser 开启的是 FLAG_BOOLEAN,FLAG_PHRASE和FLAG_LOVEHATE。

    2.3.3 对结果排序

    当你用Xapian完成一次查询后,你得到的是一个已经排好序的匹配结果。

    每一个匹配结果是一个含有排序权值的Xapian文档。这个权值代表文档与查询语句的匹配度。更好的权值意味着更匹配。每个匹配文档的权值从0开始。也有些其他系统把这个权值叫分数。

    权值是通过一定权值策略计算而来的;Xapian自带了一些不同的策略,尽管默认的已经还不错,你也可以自己实现。(Xapian用一种叫BM25的策略,这种策略会考虑比如这个匹配得词在整个数据库中有多常见,匹配文档的长度差异等。)

    尽管你已经看过如何生成一个简单的匹配列表,你可能还会需要通过一个基准数字和偏移量在这个列表里找一个匹配得子列表。很多检索应用提供给用户分页浏览功能,第一页可能是从0开始后面10个匹配结果,第二页是从10开始后面10个匹配结果等等。

    一页匹配结果在Xapian里叫做MSet(match set的简称)

    可选排序项

    有些情况,不是根据权重排序,而是根据其他排序项来是排序。例如,可能想要根据在文档值里存储的日期字段来排序。

    首先,你需要在文档值存储想要按照排序的字段,这步的做法在之前的建立索引章节讲过。然后在检索的时候告诉Xapian按照这个文档值排序。还有可能是按照多个文档值排序。

    最后,发送查询请求,返回按照设定排序字段排好序的文档。

    第三章 第一次实践

    3.1 索引

    3.1.1 建立一个图书馆目录

    我们要建立一个基于 museum catalogue 数据的简单检索系统。

    数据字段定义

    在CSV文件里,每个字段的含义如下:

    id_NUMBER:一个不重复的标示符

    ITEM_NAME:一个分类

    TITLE:简短的标题

    MARKER:作者的名字

    DATE_MAKE:制作时间,可以是时间段,大概的时间,或者是不知道

    PLACE_MAKE:制作地点

    MATERIALS:制作材料

    MEASUREMENTS:规模

    DESCRIPTION:简介

    COLLECTION:收藏品来自哪里

    3.1.2 用户想检索什么

    我们可以设想一下用户又很多可能想要从博物馆找到的。比如,他们想找Nates做的,或者在1812年以后的,或者是Hurd-Brown公司做的东西。他们还可能想找任何用铜做的,或者不是用木头走的,或者一米长的东西。他们可能只关心在国际铁路博物馆,或者是来自国际铁路博物馆的东西。还有可能他们想找个东西根据标题或者简介有一个明确的词或者短语来描述-一种用户最常用的检索。

    为了支持所有上述这些,我们需要用到很多Xapian的特性,但是在开始之前,我们先看一个最简单的例子:通过标题和简介的文本检索。

    在后面的章节,我们要用到和这章相同的数据。

    3.1.3 索引策略

    为了索引CSV,我们提取每行标题和简介两个字段,并且转化为合适的词。对于简单的文本检索,我们不需要文档值。

    因为我们处理的是文本,并且全部数据库是英文的,我们可以使用stemming(词根还原)来处理"sundial"和"sundials"都可以匹配到同一个文档。这样用户不用担心必须使用确切的语词。

    最后,我们想用一种方式分开检索两个字段。在Xapian里使用前缀来完成这个,最基础的是写一个短的字符串在词的前面来表示哪个字段建了索引。和前缀一样,为了检索文本在任何一个字段上,我们也要生成没有前缀的语词。

    如果想和omega(一个检索系统)或者其他兼容系统交互,有很多习惯性的前缀。用S作为标题的前缀,XD作为简介的描述。一个习惯前缀列表omega前缀

    当你对多个字段建立索引的时候,有前缀和没有前缀的字段词的位置要分开。比如说,有个标题”the saints“,还有个简介”dont like rabbits?keep reading“。如果你对这连个字段建索引时候没有间隔的话,搜索”saints dont like rabbits“会匹配到文档,但是这本不应该匹配。通常每个字段间隔100就够了。

    我们用XapianWriteableDatabase类写数据库,这个类可以创建,更新,或者覆写一个数据库。

    我们用Xapian的内置分词器TermGenerator把文本转换为词。分词器会切分单词,清理语词为词根,然后添加需要的语词前缀。他还可以帮你搞定词的位置,包括不同字段间的间距。

    3.1.4 我们来写点代码

    parsecsv.php

    <?php
    /*
    * Retrieves an array containing name => column associations from open file
    *
    * @param resource $fH Open file resource
    *
    * @return array Associative array of column name => column number
    */
    function get_csv_headers ($fH)
    {
        return fgetcsv($fH);
    }
    
    /**
    * Handles file opening and error reporting if file in unavailable
    *
    * @param string $file Path of file to open
    *
    * @return resource Open file handle
    */
    function open_file ($file)
    {
        // Open the CSV file
        $fH = fopen($file, "r");
        if ($fH === false) {
            die("Failed to open input file {$file} for reading
    ");
        }
    
        return $fH;
    }
    
    /**
    * Reads a row of data from a CSV file and parses into UTF-8
    *
    * @param resource $fH Open file handle
    * @param array $headers Indexed array of column names
    *
    * @return mixed False if EOF; indexed array of data otherwise
    */
    function parse_csv_row ($fH, $headers)
    {
        $row = fgetcsv($fH);
        $data = array();
    
        if (is_array($row) === false)
        {
            return false;
        }
    
        foreach ($row as $key => $value) {
            $data[$headers[$key]] = iconv('ISO-8859-1', 'UTF-8', $value);
        }
    
        return $data;
    }
    ?>

    index.php

    <?php
    require_once("xapian.php");
    require_once("parsecsv.php");
    function index($datapath, $dbpath) {
        $database = new XapianWritableDatabase($dbpath,Xapian::DB_CREATE_OR_OPEN);
        $indexer = new XapianTermGenerator();
        $indexer->set_stemmer(new XapianStem("en"));
        //打开文件
        $fp = open_file($datapath);
        //读取CSV文件头
        $headers = get_csv_headers($fp);
        //一行行读文本
        while (($row = parse_csv_row($fp, $headers)) !== false) {
            $description = $row['DESCRIPTION'];
            $title = $row['TITLE'];
            $identifier = $row['id_NUMBER'];
            
            //创建文档
            $doc = new XapianDocument();
            $indexer->set_document($doc);
    
            //对字段创建索引前缀
            $indexer->index_text($title, 1, 'S');
            $indexer->index_text($description, 1, 'XD');
            //对于普通文本查询
            $indexer->index_text($title);
            $indexer->increase_termpos();
            $indexer->index_text($description);
            //所有字段存储到数据库            
            $doc->set_data(json_encode($row));
            //自己指定docid
            $idterm = "Q".$identifier;
            $doc->add_boolean_term($idterm);
            //添加到数据库
            $database->replace_document($idterm, $doc);
        }
    }
    if ($argc < 2) {
        print "Usage: php index.php <source.csv> <target_db_path>
    ";
        die();
    }
    try {
        index($argv[1], $argv[2]);
    } catch (Exception $e) {
        echo $e->getMessage() . PHP_EOL;
    }

    在shell中运行代码

    php index.php NMSI_object1_20110304.csv db

    3.1.5 验证索引

    Xapian通过一个叫delve的工具来监视数据库,我们可以用这个工具看看刚才建立的数据库。如果你运行delve db,你会得到一个概况:文档数量,平均语词长度,还有一些其他统计信息:

    [root@localhost xapian]# delve db
    UUID = 162eff82-b134-4f4c-accc-a792a8d85a31
    number of documents = 65535
    average document length = 75.8239
    document length lower bound = 4
    document length upper bound = 1484
    highest document id ever used = 65535
    has positional information = true

    你还可以通过Xapian的docid查看某一个文档的信息(-d 表示同时显示数据文档数据):

    [root@localhost xapian]# delve -r 1 -d db
    Data for record #1:
    {"id_NUMBER":"1974-100","ITEM_NAME":"Pocket horizontal sundial","TITLE":"Ansonia Sunwatch (pocket compass dial)","MAKER":"Ansonia Clock Co.","DATE_MADE":"1922-1939","PLACE_MADE":"New York county, New York state, United States","MATERIALS":"","MEASUREMENTS":"","DESCRIPTION":"Ansonia Sunwatch (pocket compass dial)","WHOLE_PART":"WHOLE","COLLECTION":"SCM - Time Measurement"}
    Term List for record #1: Q1974-100 Sansonia Scompass Sdial Spocket Ssunwatch XDansonia XDcompass XDdial XDpocket XDsunwatch ZSansonia ZScompass ZSdial ZSpocket ZSsunwatch ZXDansonia ZXDcompass ZXDdial ZXDpocket ZXDsunwatch Zansonia Zcompass Zdial Zpocket Zsunwatch ansonia compass dial pocket sunwatch

    你还可以通过语词来查看对应的文档和统计信息

    [root@localhost xapian]# delve -t Stime db
    Posting List for term `Stime' (termfreq 76, collfreq 82, wdf_max 24): 41 56 58 65 364 365 414 476 477 547 554 565 604 639 657 666 673 694 699 715 721 745 756 759 761 778 788 887 888 889 890 891 892 2291 4244 4869 7450 8379 9773 9774 13138 14840 16183 16221 16222 17259 18891 19147 20232 20647 20648 21257 21634 22026 22067 22092 22093 22115 23057 23058 23059 25041 25627 27638 29248 29573 30656 30781 30920 31102 37513 38279 38527 41027 48621 64609

    This means you can look documents up by identifier:

    因为文档标识符也是一个语词,所以可以通过文档的标识符来查看文档

    [root@localhost xapian]# delve -t Q1974-100 dbPosting List for term `Q1974-100' (termfreq 1, collfreq 0, wdf_max 0): 1

    delve是个确认数据库是不是有某个文档和语词很有用的工具。

    3.1.6 更新数据库

    如果你回顾验证数据库章节,你可能会注意到,“compass”这个词拼写错了,意味着你需要更新这个文档。

    因为我们使用的是CSV中的id_NUMBER字段外部ID作为文档ID,我们可以传递这个ID到XapianDatabase->replace_document方法就可以更新文档了。对index.php做下修改,只更新数据库中的一部

    分,其他部分是没有被涉及的。

    删除文档

    通过XapianDatabase->delete_document方法作用在XapianWritableDatabase对象上删除文档。这个方法和XapianDatabse->replace_document一样,可以通过Xapian的docid或者是一个唯一的语词来删除文档。

    delete.php

    然后我们运行程序,指定根据id_NUMBER字段上的数据指定标示符:

    最后,我们预期通过delve会看到在数据库中少了两个文档:

    3.2 检索

    现在我们数据库中已经有了一些数据,是时候写点代码来检索了。

    我们想获得用户输入的文本,然后再数据库里检索;为了完成这个目标,我们需要把文本转换成Xapian查询对象,这个查询对象是由语词和像AND,OR这种逻辑操作符组合成的树。

    有很多种方法把用户输入的文本转换为查询对象,但是最简单的方式是使用QueryParser(查询分析器)。

    search.php

    <?php
    include 'xapian.php';
    function search($dbpath, $querystring, $offset = 0, $pagesize = 10){    
        //打开数据库    
        $database = new XapianDatabase($dbpath);
        //创建查询分析器    
        $qp = new XapianQueryParser();
        $qp->set_stemmer(new XapianStem("en"));
        $qp->set_stemming_strategy(XapianQueryParser::STEM_SOME);
        //配置查询分析其
        $qp->add_prefix("title","S");
        $qp->add_prefix("desc","XD");
        //处理查询语句成查询树
        $query = $qp->parse_query($querystring);
        //创建查询对象
        $enquire = new XapianEnquire($database);
        $enquire->set_query($query);
        //获得查询结果
        $matches = $enquire->get_mset($offset, $pagesize);
        $start = $matches->begin();
        $end = $matches->end();
        $index = 0;
        
        while (!($start->equals($end))) {
            $doc = $start->get_document();    
            $docid = $start->get_docid();
            $fields = json_decode($doc->get_data());
            $position = $offset + $index + 1;
            print sprintf("%d: #%03d %s
    ", $position, $docid, $fields->TITLE);  
            
            $start->next();
            $index++;  
        }
    }
    try {
        search($argv[1], $argv[2]);
    } catch (Exception $e) {
        echo $e->getMessage() . PHP_EOL;
    }

    3.3 运行检索

    运行检索

    [root@localhost xapian]# php search.php db watch
    1: #377 Movement of lever watch by the American Watch Co
    2: #353 German watch and watch movement
    3: #472 Watch No
    4: #29397 "Regloscope" electronic watch timer, with microphone
    5: #640 Set of three observatory watches with keyless lever movement
    6: #530 Hamilton Electric Wrist- Watch, 1957
    7: #32561 Combined Watch Pedometer, English, 1795-1805
    8: #505 Waterbury watch movement
    9: #701 Swiss 8-day watch
    10: #8369 Ticka Pocket Watch Camera, 1906-1908

    结果显示了10个匹配数据。

    3.4 检索指定字段

    当我们建立索引的时候,我们通过语词的前缀来区分了标题和简介字段,这样可以检索指定的字段。

    通过查询分析器,可以像title:watch这样检索指定的语词。

    给查询分析器设置查询词和前缀的映射

    $qp->add_prefix("title","S");
    $qp->add_prefix("desc","XD");

    这样可以可以指定在字段内检索了。

    [root@localhost xapian]# php search.php db title:low-loss
    1: #18189 Eddystone low-loss variable capacitor
    2: #21254 Wearite low loss coil unit with carton
    3: #4579 Low-loss electrodynamometer wattmeter, with torsion control,

    还可以通过逻辑操作符组合查询语句。

    [root@localhost xapian]# php search.php db description:"leather case" AND title:sundial
    1: #38525 Circular slide rule, Fowler "Magnum", in leather case with d
    2: #36443 Richardson's slide rule, in leather pocket case, with two de
    3: #36484 Napier's bones, 17th century
    4: #8060 NO DESCRIPTION ON DATABASE.
    5: #10286 No description
    6: #10453 No description
    7: #11145 No description
    8: #11158 No description
    9: #14235 Soldiers of various descriptions
    10: #17462 Mounted photograph of train description transmitter

     

    第四章 实践 

    4.1 过滤查询结果

    之前讨论了建立博物馆数据的索引,我们展示了如何对标题,简介字段分开建立前缀以达到对某一个字段来检索。但是不是所有字段都可以通过文本来检索,有些字段没有包括一个可以约束的文本,例如,字段可能包括一些特殊的值或者标示符。我们希望通过这些字段的值来过滤检索结果,而不是把这种特殊字段看成文本。

    在博物馆数据中,METERIALS字段可以看做一个标示符,而不是一个文本。

    4.1.1 标引

    当对这类字段标引的时候,我们不是想要对这种字段进行词干提取,尽管我们想把字段的数据进行大小写转换,但是这不大重要。我们也不想查找这些语词在字段中出现的次数,所以不需要存储内文档频率信息。像这种类型的字段更适合作为一个boolean term(二值语词),我们使用他是用来直接约束检索结果,而不是作为检索权重的一部分。

    因此我们用add_boolean_term方法直接添加标示符到XapianDocument。

    index_filter.php

    <?php
    require_once("xapian.php");
    require_once("parsecsv.php");
    function index($datapath, $dbpath) {
        $database = new XapianWritableDatabase($dbpath,Xapian::DB_CREATE_OR_OPEN);
        $indexer = new XapianTermGenerator();
        $indexer->set_stemmer(new XapianStem("en"));
        //打开文件
        $fp = open_file($datapath);
        //读取CSV文件头
        $headers = get_csv_headers($fp);
        //一行行读文本
        while (($row = parse_csv_row($fp, $headers)) !== false) {
            $description = $row['DESCRIPTION'];
            $title = $row['TITLE'];
            $identifier = $row['id_NUMBER'];
            
            //创建文档
            $doc = new XapianDocument();
            $indexer->set_document($doc);
    
            //对字段创建索引前缀
            $indexer->index_text($title, 1, 'S');
            $indexer->index_text($description, 1, 'XD');
            //对于普通文本查询
            $indexer->index_text($title);
            $indexer->increase_termpos();
            $indexer->index_text($description);
            //对materials字段建索引
            $materials = explode(";", $row['MATERIALS']);
            foreach ($materials as $material) {
                $material = strtolower(trim($material));
                if ($material != '') {
                    $doc->add_boolean_term('XM'.$material);
                }
            }
            //所有字段存储到数据库            
            $doc->set_data(json_encode($row));
            //自己指定docid
            $idterm = "Q".$identifier;
            $doc->add_boolean_term($idterm);
            //添加到数据库
            $database->replace_document($idterm, $doc);
        }
    }
    if ($argc < 2) {
        print "Usage: php index.php <source.csv> <target_db_path>
    ";
        die();
    }
    try {
        index($argv[1], $argv[2]);
    } catch (Exception $e) {
        echo $e->getMessage() . PHP_EOL;
    }

    使用delve工具查看索引结果

    [root@localhost xapian]# delve -r 3 -1 db
    Term List for record #3:
    ...

    XMglass
    XMmounted
    XMsand
    XMtimer
    XMwood
    ...

    4.1.2 检索

    假设我们提供给用户一个输入框,让用户输入想要检索的文本,然后提供一个复选框选择meterials。我们想返回匹配到检索输入,但是只包含选择的meterials。

    输入框和复选框都是两个Query对象,用一个OP_FILTER操作符把两个Query结合到一起。如果有多个复选框被选择,需要用OP_OR操作符来合成一个Query对象。

    一个复杂的Query树是通过查询分析器和手动构建Query对象组合而成的,这样可以支持更加灵活对查询结果过滤。
    search_filter.php

    <?php
    include 'xapian.php';
    function search($dbpath, $querystring, $material, $offset = 0, $pagesize = 10){    
        //打开数据库    
        $database = new XapianDatabase($dbpath);
        //创建查询分析器    
        $qp = new XapianQueryParser();
        $qp->set_stemmer(new XapianStem("en"));
        $qp->set_stemming_strategy(XapianQueryParser::STEM_SOME);
        //配置查询分析其
        $qp->add_prefix("title","S");
        $qp->add_prefix("desc","XD");
        //处理查询语句成查询树
        $query = $qp->parse_query($querystring);
        //通过字段过滤结果
        if (isset($material)) {
            //根据material创建一个查询对象
            $material_query = new XapianQuery('XM'.strtolower($material));
            //根据material查询 过滤$query的查询结果
            $query = new XapianQuery(XapianQuery::OP_FILTER, $query, $material_query);
        }
        //创建查询对象
        $enquire = new XapianEnquire($database);
        $enquire->set_query($query);
        //获得查询结果
        $matches = $enquire->get_mset($offset, $pagesize);
        $start = $matches->begin();
        $end = $matches->end();
        $index = 0;
        
        while (!($start->equals($end))) {
            $doc = $start->get_document();    
            $docid = $start->get_docid();
            $fields = json_decode($doc->get_data());
            $position = $offset + $index + 1;
            print sprintf("%d: #%03d %s
    ", $position, $docid, $fields->TITLE);  
            
            $start->next();
            $index++;  
        }
    }
    try {
        search($argv[1], $argv[2], $argv[3]);
    } catch (Exception $e) {
        echo $e->getMessage() . PHP_EOL;
    }

    通过material为steel的过滤查询结果

    [root@localhost xapian]# php search_filter.php db watch steel
    1: #29656 Watch fusee cutting engine.
    2: #216 Pocket Watch, late 17th Century
    3: #480 Automatic Wrist-watch
    4: #10342 'Ticka' watch camera  by Houghton Limited, c. 1908, with ins
    5: #160 Cylinder Pocket Watch in Silver and Tortoiseshell Casing, 1800-1810

    4.1.3 使用查询分析器

    前面的代码展示了通过代码层面过滤查询结果。这样做有很好的灵活性,但是有些时候希望用户可以通过输入文本来过滤。

    可以使用XapianQueryParser.add_boolean_prefix()方法。这个方法告诉查询分析器一个用来过滤的字段和对应语词前缀。在materials检索中,我们只需要添加一行代码。

    查询分析器通过解析"material:"

    4.1.4 提供给查询分析器的是什么

    开发者经常会把过滤应用在用户输入中(例如,通过添加像material:steel在末尾)。这是个不好的方法,因为查询分析器还包含一些启发式的处理用户输入,这样对于material:steel就不是很可靠。

    提供给查询分析器的应该是用户直接的查询输入,如果你想应用过滤查询结果,你应该把过滤应该在查询分析器的输出。

    4.2 范围查询

    4.2.1 我只对1980年以后的数据感兴趣

    在博物馆数据中,我们使用了之前的例子,有一个DATE_MADE字段代表东西制作时间,所以有个很自然地事情就是用户可能想查找在一段时间内制作的东西。假设我们想扩展原系统来满足这个需求,我们要:

    1.解析字段成常量;字段包含年,年时间段(1671-1700),灵活的年表示方法(C. 1936)和带有说明的年表示(patented 1885)。

    2.存储字段到XapianDatabase

    3.在查找时指定日期范围

    4.2.2 Xapian如何支持范围查询

    如果你回顾一下之前介绍Xapian中的检索,你会记得一组处理值范围的操作符:OP_VALUE_LE,OP_VALUE_GE和OP_VALUE_RANGE。所以,你可以通过文档值处理范围查询,通过操作符的组合构造合适的查询。

    因为我们通常想暴露这些功能给用户,我们想让用户可以输入一个查询,这个查询包含一个或多个范围约束;查询分析器可以支持做这些,通过使用value range processors,XapianValueRangeProcessor的子类。Xapian自带了一些标准的rang处理类,或者你也可以编写你自己的。

    文档值在Xapian被按照字符串排序后,操作符提供比较字符串的处理,我们需要一种方式转换数字成字符串并存储他们。Xapian提供了一对工具函数:sortable_serialise和sortable_unserialise,这两个函数在浮点数和字符串间转换。

    4.2.3 创建文档值

    我们需要一个新的标引程序。index_ranges.php,通过MEASUREMENTS和DATA_MADE创建文档值。我们可以在槽位0设置最大的规格(幸运的是数据被存储成毫米和千克,所以我们可以假设毫米永远是比重量大的),从DATA_MADE中分离出年存储在槽位1(如果包含多种日期格式,我们选择第一个可以解析的年份)。

    index_ranges.php

    <?php
    require_once("xapian.php");
    require_once("parsecsv.php");
    function index($datapath, $dbpath) {
        $database = new XapianWritableDatabase($dbpath,Xapian::DB_CREATE_OR_OPEN);
        $indexer = new XapianTermGenerator();
        $indexer->set_stemmer(new XapianStem("en"));
        //打开文件
        $fp = open_file($datapath);
        //读取CSV文件头
        $headers = get_csv_headers($fp);
        //一行行读文本
        while (($row = parse_csv_row($fp, $headers)) !== false) {
            $description = $row['DESCRIPTION'];
            $title = $row['TITLE'];
            $identifier = $row['id_NUMBER'];
            $measurements = $row['MEASUREMENTS'];
            $dateMade = $row['DATE_MADE'];
            //创建文档
            $doc = new XapianDocument();
            $indexer->set_document($doc);
    
            //对字段创建索引前缀
            $indexer->index_text($title, 1, 'S');
            $indexer->index_text($description, 1, 'XD');
            //对于普通文本查询
            $indexer->index_text($title);
            $indexer->increase_termpos();
            $indexer->index_text($description);
            //所有字段存储到数据库            
            $doc->set_data(json_encode($row));
            //添加文档值
            if (intval($measurements) > 0) {
                $doc->add_value(0, Xapian::sortable_serialise(intval($measurements)));
            }
            if (intval($dateMade)) {
                $doc->add_value(1, Xapian::sortable_serialise(intval($dateMade)));
            }
            //自己指定docid
            $idterm = "Q".$identifier;
            $doc->add_boolean_term($idterm);
            //添加到数据库
            $database->replace_document($idterm, $doc);
        }
    }
    if ($argc < 2) {
        print "Usage: php index.php <source.csv> <target_db_path>
    ";
        die();
    }
    try {
        index($argv[1], $argv[2]);
    } catch (Exception $e) {
        echo $e->getMessage() . PHP_EOL;
    }

    我们可以执行:

    php index_range.php NMSI_object1_20110304.csv db

    我们可以使用delve检查创建的文档值:

    delve -V0 db

    Value 0 for each document: 314:�� 655:�  1053:� 1114:� 1731:�@ 1812:�@ 2133:� ...

    4.2.4 范围查询

    最简单的值范围处理器是XapianStringValueRangeProcessor,但是我们需要两个XapianNumberValueRangeProcessor实例。

    为了区分不同的范围,我们需要给规格字段指定后缀“mm”,而年份本来就是数字,所以我们需要先告诉查询分析器后缀:

    $qp->add_valuerangeprocessor(
            new XapianNumberValueRangeProcessor(0, 'mm', False)
    ); 
    $qp->add_valuerangeprocessor( new XapianNumberValueRangeProcessor(1, '') );

    第一个调用有一个参数是false代表mm是一个后缀(默认是前缀)。当使用空的字符串作为第二个参数的时候,前缀后缀就不重要了,所以第二个调用的第三个参数可以跳过。

    search_ranges.php

    <?php
    include 'xapian.php';
    function search($dbpath, $querystring, $offset = 0, $pagesize = 10){    
        //打开数据库    
        $database = new XapianDatabase($dbpath);
        //创建查询分析器    
        $qp = new XapianQueryParser();
        $qp->set_stemmer(new XapianStem("en"));
        $qp->set_stemming_strategy(XapianQueryParser::STEM_SOME);
        //配置查询分析其
        $qp->add_prefix("title","S");
        $qp->add_prefix("desc","XD");
        //添加文档值范围处理器
        $qp->add_valuerangeprocessor(
            new XapianNumberValueRangeProcessor(0, 'mm', False)
        )
        $qp->add_valuerangeprocessor(
            new XapianNumberValueRangeProcessor(1, '')
        )
        //处理查询语句成查询树
        $query = $qp->parse_query($querystring);
        //创建查询对象
        $enquire = new XapianEnquire($database);
        $enquire->set_query($query);
        //获得查询结果
        $matches = $enquire->get_mset($offset, $pagesize);
        $start = $matches->begin();
        $end = $matches->end();
        $index = 0;
        
        while (!($start->equals($end))) {
            $doc = $start->get_document();    
            $docid = $start->get_docid();
            $fields = json_decode($doc->get_data());
            $position = $offset + $index + 1;
            print sprintf("%d: #%03d %s
    ", $position, $docid, $fields->TITLE);  
            
            $start->next();
            $index++;  
        }
    }
    try {
        search($argv[1], $argv[2]);
    } catch (Exception $e) {
        echo $e->getMessage() . PHP_EOL;
    }
    View Code

    我们现在可以通过查询“..50mm”来约束规格,通过“1980..1989”来约束年份:

    [root@localhost xapian]# php search_range.php db ..50mm
    1: #1053 Two pieces of glass, partly blown to form contact lenses
    2: #1114 Two plastic plano-convex lenses used for correcting the imag
    3: #1731 Quartz Fibres by Sir Charles Vernon Boys, 1887-1892
    4: #1812 Steel Specimens showing Colours for Tempering, 1801-1900
    5: #2133 Ten millimetre cube of synthetic proustite with two polished
    6: #2137 Two Helical flash tubes used for pumping a ruby laser c
    7: #6017 The Works of Messrs. Perkin & Sons at Greenford Green
    8: #6848 [Mounted sepia photos (2) of the birthplace of John Dalton.
    9: #8899 Comparative Scales, Compiled from Tables of eminent authorities ...
    10: #9099 Ms. laboratory notebook, ca.1956-1959, compiled by Dr. R.P.H

    你可以把普通查询和范围查询组合到一起,比如查询在1960年以后做的clocks

    //shell

    还可以把两个范围查询组合,例如查询规格比较大的并且生产与20世纪以后的:

    //shell

    注意,1000..mm,后缀必须跟在整个查询的后面;也可以跟在前面(比如1000mm..mm)。同理,100mm..200mm或者100..200mm都是合法的,但是100mm..200是不合法的。这个规则同样适用于前缀。

    如果你输入了不合法的查询,查询分析器会抛出一个QueryParserError:

    //shell

    4.2.5 处理日期

    为了约束日期范围,我们需要决定如何储存日期数据到文档值,和如何想要用户输入数据来查询日期范围。XapianDateValueRangeProcessor,通过储存按照"YYYYMMDD"格式化的日期字符串,也可以格式化为美国的日期类型(month/day/year)或者欧洲类型(day/month/year)。

    为了展示他是怎么工作的,我们要用一个不同的数据集,因为图书馆数据指给了每个东西制作的年份;我们已经创建了一些数据,美国50个洲的信息。这个CSV文件states.csv

    我们需要一个新的标引程序

    index_ranges2.php

    //code

    他存储个数字通过sortable_srialise函数:在槽位1存储了这个洲成立的年,槽位3存储了这个洲的人口。在槽位2存储了格式为“YYYYMMDD”的这个洲成立的日期。

    我们用和之前同样的方式创建索引

    //shell

    然后,我们改变值值范围处理器并添加到查询分析器上

    //code

    XapianDateValueRangeProcessor作用在槽位2上,1860是纪元开始(所以两位表示的年份会被认为从1860年开始)。第二个参数代表是否日期是美国日期类型;因为我们看过数据中的US states,都是美国日期类型,所以这里是true,XapianNumberValueRangeProcessor在前面已经介绍了。

    查找所有20世纪成立的洲:

    //shell

    上面例子使用到了作用在槽位1上的XapianNumberValueRangeProcessor。我们下面查找在1889年11月8号到1890年7月10号之间成立的洲:

    //shell

    上面例子使用到了作用在槽位2上的XapianDateValueRangeProcessor;这个范围处理器没法处理年份范围的查找,这也是为什么我们要对槽位1和槽位2分别标引。

    4.2.6 编写你自己的ValueRangeProcessor

    我们还没有对人口字段做什么操作。我们想要做的是对人口字段做类似于XapianNumberValueRangeProcessor的行为。如果我们在作用在槽位1(年份)的XapianNumberValueRangeProcessor前面添加,它会把把任何内容都看做是人口范围,把任何其他的看做年份的范围。

    为了完成这件事,我们需要知道XapianValueRangeProcessor是如何被查询分析器调用的。每个范围处理器轮流被传递开始和结束范围。如果它不理解范围,它会返回Xapian.BAD_VALUENO。如果它理解范围,它会返回XAPIAN.Query.OP_VALUE_RANGE可用的值,它也可以修改开始和结束值(转换开始和结束值为正确的格式,让Xapian.OP_VALUE_RANGE比较使用)。

    我们要做的是写一个XapianValueRangeProcessor的子类来接受数字范围五十万到五千万;这个范围和数据集里年份的范围没有交集,并且可以包含人口数目的全部范围。如果数字不在这个范围,我们会返回Xapian.BAD_VALUENO,然后查询分析器会转到下一个XapianValueRangeProcessor。

    PopulationValueRangeProcessor.class.php

    //code

    我们然后超找人口超过一千万的

    //shell

    或者查询十八世纪80年代人口超过一千万的

    //shell

    想要更完善这个查询,我们还可以支持“..750k”代表75万。

    4.2.7 性能限制

    如果没有其他的语词查询,一个XapianValueRangePrcessor将会引起在整个数据库里查询,这意味着会载入全部在给定槽位上的文档值。在一个小的数据库上,这不是问题,但是对于一个大的数据库,它会有性能影响:这可能是个慢查询。

    如果和合适的语词查询相结合(例如OP_AND 添加一个或多个语词),这个性能问题会被减小,因为范围运算符将会只在已经匹配得结果里执行,这样避免了全数据库查询。

    我们可以把文档值分组,可以提供一个基于语词的查询,尽管用户只关心一个纯范围查询。例如,把人口按照一定范围分组,你可以分配一个语词描述每一组,我们这里使用前缀XP。

    Population range   Term

    0 - 10 million        XP0

    10 - 20 million      XP1

    20 - 30 million      XP2

    30 - 40 million      XP3

    假如用户查询“..15000000”,我们先把两个语词查询"XP0"和"XP1"用OP_AND连接,然后把这个查询和查询分析器生成的查询对象用OP_FILTER组合。

    4.3 分面搜索 

    Xapian提供了可以让你在匹配的文档结果中,在动态的根据筛选项生成结果的功能。例如对于分面,颜色,制造商,尺寸都是候选的筛选项。

    这个功能有很多潜在的用途,但是最常见的用途是提供给用户通过指定筛选值或者类别,缩小他们检索结果的能力。这常被成为分面搜索。

    xapian的分面搜索解决的不是分面以后的筛选结果展示问题,根据筛选结果的展示是之前讲的filter,range解决的问题。这里的分面获取的是每个分面词出现的频率。

    4.3.1 标引

    实现分面搜索标引没有额外的工作需要做,除了确保你希望使用在分面时候的值已经存储为文档值。

    index_factes.php

    //code

    这里我们使用两个文档值,槽位0储存收藏品,槽位1储存制作者的名字。我们已经知道文档数据来自固定的列表,所以我们不需要担心在使用两列的值作为分面前正常化他们的值。下面我们对数据标引:

    //shell

    4.3.2 检索 

    你需要创建一个XapianValueCountMatchSpy对象来,如下陈述了spy添加到XapianEnquire:

    //code

    我们在槽位1,也就是作者上使用分面搜索。你得到MSet之后,可以通过spy查询周到的分面和对应的频率。

    注意,尽管我们通常只显示10个匹配结果,我们对get_mset()用一个叫做checkatleast的参数,为了使整个数据都被考虑,分面的频率是正确。这里spy统计分面频率的时候在10000个记录里统计。输出如下:

    //shell

    注意,spy会按照字母顺序返回结果,而不是按照频率顺序。

    如果你想操作多个分面,你需要在执行get_mset()前注册多个XapianValueCountMatchSpy对象,尽管每个添加的spy会有一些性能影响。

    4.3.3 通过分面约束

    如果你正在使用分面提供给用户选择,来缩小检索结果,你需要应用一个合适的过滤器。

    对于单一值,你需要使用XapianQuery.OP_VALUE_RANGE的开始和结束值一样,或者XpianMatchDecider,但是最高效的是使用一个合适的语词前缀来过滤。

    4.3.4 限制

    Xapian的分面搜索能力决定于Xapian搜索时检查记录的数量。你可以通过指定get_mset的checkatleast的值来控制这个数字。但是很重要的一点是增加这个值会对查询性能有影响。

    4.4 排序

    默认的,xapian的搜过结果是按照相关度递减排列的。但是,也允许按照其他标准排序,或者是其他标准和相关度的组合。

    如果两个和多个结果比较的排序标准相同,则顺序决定于文档ID。默认的,文档ID按照升序排列(小的文档id代表更好,更靠前),但是可以通过使用$enquire->set_docid_order($enquire::DESCENDING)选择降序排列;如果你对排序不关心,你可以使用$enquire->set_docid_order($enquire::DONET_CARE)将会是最高效的。

    也有可能改变相关度的计算公式,了解详细,请转到 文档权重表和打分

    4.4.1 根据值排序

    你可以根据比较指定的文档值来对文档排序。注意比较的文档值是按字节来比较的,所以1<10<2。如果你想对值编码用于对数字排序,请使用Xapian->sortable_serialise()在标引的时候对值编码-以上同样适用于整型和浮点型值:

    $doc->add_value(0, Xapian::sortable_serialise($price));

    有三个方法用于指定文档值如何被用来排序:

    $enquire->set_sort_by_value() 指定相关度一点都不影响排序。

    $enqure->set_sort_by_value_then_relevance() 指定相关度是用于排序那些文档值相同的文档。

    $enquire->set_sort_by_relevance_then_value() 指定文档按照相关度排序,文档值只用于排序相关度值相同的文档(注意:权重需要是完全一样文档值才决定排序,所以这个方法当使用BM25作为默认的参数不是很有用,因为不同文档很少有相同的分数)。

    我们将会用美国州数据来验证,代码使用范围查询时候的标引程序:

    //shell

    这里有三个文档值:槽位1代表州被承认的年份,槽位2是时间(YYYYMMDD格式),槽位3是州人口数。所以,如果我们想根据年份排序,然后根据相关度排序,我们需要再get_mset前添加如下代码:

    //code

    最后的参数false是升序排列,true是降序排列。我们然后执行查询程序:

    //shell

    4.4.2 根据多个值排序

    有一个标准的子类XapianMultiValueKeyMaker可以根据多个文档值排序。

    我们用这个改写之前的检索程序,先根据年份排序,再根据人口降序排序。

    //code

    正如Enquire方法,add_value的第二个参数控制了使用升序或者降序排列。所以现在我们可以执行一个更复杂的排序检索:

    //shell

    4.4.3 生成keys的其他用途

    Xapian::KeyMaker可以被继承来计算排序。例如,“根据地理位置距离排序”,子类可以处理用户的经纬度,然后对文档按照距离用户最近的排序。

    为了完成这些,我们将存储各个州的地理坐标在文档值。states.csv

    //code

    把地理信息存储到文档值:

    //shell

    现在我们需要一个KeyMaker;我们让他返回一个key,这个key是按照距离华盛顿的距离排序的。

    执行示例代码如下:

    //shell

    4.5 对结果去重

    xapian提供从MSet消除复制文档的功能。这个特性被称作"collapsing"-很多副本只保留一个结果。

    去重总是移除最差排名的文档(如果是根据相关度排序,是低权值的;如果是根据值排序,是排序中低的)。

    判断两个文档一个是不是另一个的复制决定于他们的"collapse key"。如果一个文档有一个空的colapse key,他将永远不会被判断为复制,两个有相同colapse kye的文档会被去重。

    collapse key是取自一个你指定槽位的文档值(通过Enquire->set_colapse_key()方法),但是以后你可以通过XapianKeyMaker动态创建colapse key。

    4.5.1 性能

    去重是在匹配运行时被执行的,所以是很高效的,这种方法比生成一个很大的MSet后再去重好很多。

    4.5.2 API

    调用$Enquire->set_collapse_key(),第一个参数传递文档值所在的槽位序号,第二个参数设置相同collapse key保存的文档数量(没有指定默认是1),例如:

    //code

    当你得到XapianMSet对象,你可以获取collapse key通过XapianMSetIterator->get_collapse_key()方法,获取collapse count通过XapianMSetIterator->get_collapse_count()方法。

    注意的是,如果你有可能主动中断查询,collapse count将会出错,是0或者1。

    4.5.3 统计

  • 相关阅读:
    2020软件工程作业02
    2020软件工程作业01
    为什么需要平衡二叉树?
    手机号码和邮箱等联系地址,为什么不明文显示?
    请把重要的事看轻 ——2017年终总结
    万事皆空:随缘而定
    微服务:微服务架构模式译文说明
    Mysql 查询—按位运算
    解决:spring security 登录页停留时间过长 跳转至 403页面
    excel模板解析—桥接模式:分离解析模板和业务校验
  • 原文地址:https://www.cnblogs.com/23lalala/p/3388757.html
Copyright © 2020-2023  润新知