排序和分面

注解

whoosh 3.0中的排序和分面API发生了变化。

概述

whoosh中的排序和分面搜索结果基于 小面. 每个方面都将一个值与搜索结果中的每个文档 关联起来,允许您根据键进行排序或使用 它们对文档进行分组。whoosh包括 facet types 您可以用于排序和分组(见下文)。

分选

默认情况下,搜索结果首先以得分最高的文档排序。你可以使用 sortedby 关键字参数,用于按其他条件(如字段值)对结果排序。

使字段可排序

为了对字段进行排序,应使用 sortable=True 关键字参数:

schema = fields.Schema(title=fields.TEXT(sortable=True),
                       content=fields.TEXT,
                       modified=fields.DATETIME(sortable=True)
                       )

可以在没有的字段上进行排序 sortable=True 但这需要whoosh将字段中的唯一术语加载到内存中。使用 sortable 效率更高。

关于列类型

创建字段时使用 sortable=True ,您告诉whoosh将该字段的每个文档值存储在 柱. 列对象指定用于在磁盘上存储每个文档值的格式。

这个 whoosh.columns 模块包含几个不同的列对象实现。每个字段类型都指定一个合理的默认列类型(例如,文本字段的默认值为 whoosh.columns.VarBytesColumn ,数字字段的默认值为 whoosh.columns.NumericColumn )但是,如果希望最大限度地提高效率,则可能需要对字段使用不同的列类型。

例如,如果字段中的所有文档值都是固定长度,则可以使用 whoosh.columns.FixedBytesColumn . 如果有一个字段,其中许多文档共享相对较少的可能值(例如“category”字段、“month”或其他枚举类型字段),则可能需要使用 whoosh.columns.RefBytesColumn (可以处理可变和固定长度值)。有用于存储每个文档位值、结构、pickled对象和压缩字节值的列类型。

若要为字段指定自定义列对象,请将其作为 sortable 关键字参数而不是 True ::

from whoosh import columns, fields

category_col = columns.RefBytesColumn()
schema = fields.Schema(title=fields.TEXT(sortable=True),
                       category=fields.KEYWORD(sortable=category_col)

对自定义排序键使用列字段

添加具有可排序字段的文档时,whoosh使用您为该字段传递的值作为可排序值。例如,如果“标题”是可排序字段,则添加此文档:

writer.add_document(title="Mr. Palomar")

然后…… Mr. Palomar 作为文档的排序键存储在字段列中。

这通常很好,但有时您需要“按摩”可排序键,这样它就不同于用户在界面中搜索和/或看到的值。例如,如果允许用户按标题排序,则可能希望对可见标题和用于排序的值使用不同的值:

# Visible title
title = "The Unbearable Lightness of Being"

# Sortable title: converted to lowercase (to prevent different ordering
# depending on uppercase/lowercase), with initial article moved to the end
sort_title = "unbearable lightness of being, the"

最好的方法是使用一个额外的字段进行排序。你可以使用 whoosh.fields.COLUMN 字段类型要创建未编入索引或存储的字段,它只保留每个文档列的值::

schema = fields.Schema(title=fields.TEXT(stored=True),
                       sort_title=fields.COLUMN(columns.VarBytesColumn())
                       )

唯一的论点 whoosh.fields.COLUMN 初始值设定项是 whoosh.columns.ColumnType 对象。您可以使用 whoosh.columns 模块。

另一个例子是,假设您正在索引与每个文档关联的自定义排序顺序的文档,例如“优先级”编号:

name=Big Wheel
price=100
priority=1

name=Toss Across
price=40
priority=3

name=Slinky
price=25
priority=2
...

可以将列字段与数字列对象一起使用,以保持“优先级”,并将其用于排序:

schema = fields.Schema(name=fields.TEXT(stored=True),
                       price=fields.NUMERIC(stored=True),
                       priority=fields.COLUMN(columns.NumericColumn("i"),
                       )

(注意 columns.NumericColumn 采用类似于python使用的代码的类型代码字符 structarray 模块。

使现有字段可排序

如果在 sortable 在whoosh 3.0中添加了参数,或者您认为不需要字段可排序,但现在您发现需要对其排序,可以使用 whoosh.sorting.add_sortable() 效用函数:

from whoosh import columns, fields, index, sorting

# Say we have an existing index with this schema
schema = fields.Schema(title=fields.TEXT,
                       price=fields.NUMERIC)

# To use add_sortable, first open a writer for the index
ix = index.open_dir("indexdir")
with ix.writer() as w:
    # Add sortable=True to the "price" field using field terms as the
    # sortable values
    sorting.add_sortable(w, "price", sorting.FieldFacet("price"))

    # Add sortable=True to the "title" field using the
    # stored field values as the sortable value
    sorting.add_sortable(w, "title", sorting.StoredFieldFacet("title"))

调用时可以指定自定义列类型 add_sortable 使用 column 关键字参数:

add_sortable(w, "chapter", sorting.FieldFacet("chapter"),
             column=columns.RefBytesColumn())

参见文档 add_sortable() 更多信息。

排序搜索结果

当您告诉whoosh按字段(或字段)排序时,它使用字段列中的每个文档值作为文档的排序键。

通常,搜索结果按相关性得分降序排序。你可以通过传递 sortedby 关键字参数 search() 方法:

from whoosh import fields, index, qparser

schema = fields.Schema(title=fields.TEXT(stored=True),
                       price=fields.NUMERIC(sortable=True))
ix = index.create_in("indexdir", schema)

with ix.writer() as w:
    w.add_document(title="Big Deal", price=20)
    w.add_document(title="Mr. Big", price=10)
    w.add_document(title="Big Top", price=15)

with ix.searcher() as s:
    qp = qparser.QueryParser("big", ix.schema)
    q = qp.parse(user_query_string)

    # Sort search results from lowest to highest price
    results = s.search(q, sortedby="price")
    for hit in results:
        print(hit["title"])

您可以使用以下任何对象作为 sortedby

A FacetType 对象
使用此对象对文档进行排序。有关可用的方面类型,请参见下文。
字段名字符串
将字段名转换为 FieldFacet (见下文)并使用它对文档进行排序。
列表 FacetType 对象和/或字段名字符串
将面组合成一个 MultiFacet 所以可以按多个键排序。请注意,此快捷方式不允许您反转各个方面的排序方向。要做到这一点,您需要构建 MultiFacet 反对你自己。

注解

你可以使用 reverse=True 关键字参数 Searcher.search() 方法反转整个排序方向。这比反转各个方面更有效。

实例

按大小字段的值排序::

results = searcher.search(myquery, sortedby="size")

按“价格”字段的相反(最高到最低)顺序排序:

facet = sorting.FieldFacet("price", reverse=True)
results = searcher.search(myquery, sortedby=facet)

按升序大小排序,然后按降序价格排序:

mf = sorting.MultiFacet()
mf.add_field("size")
mf.add_field("price", reverse=True)
results = searcher.search(myquery, sortedby=mf)

# or...
sizes = sorting.FieldFacet("size")
prices = sorting.FieldFacet("price", reverse=True)
results = searcher.search(myquery, sortedby=[sizes, prices])

按“类别”字段排序,然后按文档分数排序:

cats = sorting.FieldFacet("category")
scores = sorting.ScoreFacet()
results = searcher.search(myquery, sortedby=[cats, scores])

访问列值

每个文档列的值在 Hit 对象就像存储的字段值:

schema = fields.Schema(title=fields.TEXT(stored=True),
                       price=fields.NUMERIC(sortable=True))

...

results = searcher.search(myquery)
for hit in results:
    print(hit["title"], hit["price"])

高级:如果要快速访问每个文档的属性值,可以获取列读取器对象::

with ix.searcher() as s:
    reader = s.reader()

    colreader = s.reader().column_reader("price")
    for docnum in reader.all_doc_ids():
        print(colreader[docnum])

分组

向用户呈现“分面”搜索结果通常非常有用。刻面是将搜索结果动态分组为类别。类别允许用户根据感兴趣的类别查看总结果的一部分。

例如,如果您正在编程一个购物网站,您可能希望显示具有搜索结果的类别,如制造商和价格范围。

制造商 价格
苹果(5) 0美元- 100美元(2)
三洋(1) 101-500美元(10)
索尼(2) 501-1000美元(1)
东芝(5)  

您可以让用户单击不同的方面值,以仅显示给定类别中的结果。

另一个有用的用户界面模式是显示,例如,不同类型的已找到文档的前5个结果,并让用户单击以查看他们感兴趣的类别的更多结果,类似于聚光灯快速结果在Mac OS X上的工作方式。

这个 groupedby 关键字参数

可以使用以下对象作为 groupedby

A FacetType 对象
使用此对象对文档进行分组。有关可用的方面类型,请参见下文。
字段名字符串
将字段名转换为 FieldFacet (见下文)并使用它对文档进行排序。字段的名称用作方面名称。
字段名字符串的列表或元组
设置多个字段分组条件。
将方面名称映射到的字典 FacetType 物体
设置多个分组条件。
A Facets 对象
这个对象很像使用字典,但是有一些方便的方法可以使设置多个分组变得更容易。

实例

按“类别”字段的值分组:

results = searcher.search(myquery, groupedby="category")

按“类别”字段的值以及“标记”字段的值和日期范围分组:

cats = sorting.FieldFacet("category")
tags = sorting.FieldFacet("tags", allow_overlap=True)
results = searcher.search(myquery, groupedby={"category": cats, "tags": tags})

# ...or, using a Facets object has a little less duplication
facets = sorting.Facets()
facets.add_field("category")
facets.add_field("tags", allow_overlap=True)
results = searcher.search(myquery, groupedby=facets)

按 , 使用A MultiFacet 对象(见下文)。例如,如果有两个字段名为 tagsize tagsize 字段,如 ('tag1', 'small')('tag2', 'small')('tag1', 'medium') 等等:

# Generate a grouping from the combination of the "tag" and "size" fields
mf = MultiFacet(["tag", "size"])
results = searcher.search(myquery, groupedby={"tag/size": mf})

获取分面组

这个 Results.groups("facetname") 文档ID: :

myfacets = sorting.Facets().add_field("size").add_field("tag")
results = mysearcher.search(myquery, groupedby=myfacets)
results.groups("size")
# {"small": [8, 5, 1, 2, 4], "medium": [3, 0, 6], "large": [7, 9]}

Results.groups()

results = mysearcher.search(myquery, groupedby=myfunctionfacet)
results.groups()

groups() Searcher 对象的 stored_fields() 方法获取文档编号并将文档的存储字段作为字典返回:

for category_name in categories:
    print "Top 5 documents in the %s category" % category_name
    doclist = categories[category_name]
    for docnum, score in doclist[:5]:
        print "  ", searcher.stored_fields(docnum)
    if len(doclist) > 5:
        print "  (%s more)" % (len(doclist) - 5)

如果需要有关组的不同信息,例如,只需要每个组中文档的计数,或者不需要对组进行排序,则可以指定 whoosh.sorting.FacetMap 类型或实例 maptype 创建时的关键字参数 FacetType ::

# This is the same as the default
myfacet = FieldFacet("size", maptype=sorting.OrderedList)
results = mysearcher.search(myquery, groupedby=myfacet)
results.groups()
# {"small": [8, 5, 1, 2, 4], "medium": [3, 0, 6], "large": [7, 9]}

# Don't sort the groups to match the order of documents in the results
# (faster)
myfacet = FieldFacet("size", maptype=sorting.UnorderedList)
results = mysearcher.search(myquery, groupedby=myfacet)
results.groups()
# {"small": [1, 2, 4, 5, 8], "medium": [0, 3, 6], "large": [7, 9]}

# Only count the documents in each group
myfacet = FieldFacet("size", maptype=sorting.Count)
results = mysearcher.search(myquery, groupedby=myfacet)
results.groups()
# {"small": 5, "medium": 3, "large": 2}

# Only remember the "best" document in each group
myfacet = FieldFacet("size", maptype=sorting.Best)
results = mysearcher.search(myquery, groupedby=myfacet)
results.groups()
# {"small": 8, "medium": 3, "large": 7}

或者,可以指定 maptype 论证中 Searcher.search() 应用于所有方面的方法调用:

results = mysearcher.search(myquery, groupedby=["size", "tag"],
                            maptype=sorting.Count)

(您可以覆盖整个 maptype 通过指定 maptype 为他们辩护。)

刻面类型

FieldFacet

这是最常见的方面类型。它根据每个文档中某个字段中的值对或分组。如果每个文档在字段中只有一个术语(例如ID字段),则此方法通常最有效(或根本有效)::

# Sort search results by the value of the "path" field
facet = sorting.FieldFacet("path")
results = searcher.search(myquery, sortedby=facet)

# Group search results by the value of the "parent" field
facet = sorting.FieldFacet("parent")
results = searcher.search(myquery, groupedby=facet)
parent_groups = results.groups("parent")

默认情况下, FieldFacet 仅支持 non-overlapping 分组,如果一个文档不能同时属于多个方面(每个文档将被任意排序为一个类别)。若要获取具有多值字段的重叠组,请使用 allow_overlap=True 关键字参数:

facet = sorting.FieldFacet(fieldname, allow_overlap=True)

这支持重叠的组成员身份,其中文档在一个字段中有多个术语(例如关键字字段)。如果不需要重叠,请不要使用 allow_overlap 因为它是 much 更慢,使用更多内存(请参见 allow_overlap 下面)。

QueryFacet

您可以设置由任意查询定义的类别。例如,可以使用前缀查询对名称进行分组::

# Use queries to define each category
# (Here I'll assume "price" is a NUMERIC field, so I'll use
# NumericRange)
qdict = {}
qdict["A-D"] = query.TermRange("name", "a", "d")
qdict["E-H"] = query.TermRange("name", "e", "h")
qdict["I-L"] = query.TermRange("name", "i", "l")
# ...

qfacet = sorting.QueryFacet(qdict)
r = searcher.search(myquery, groupedby={"firstltr": qfacet})

默认情况下, QueryFacet 仅支持 non-overlapping 分组,其中一个文档不能同时属于多个方面(每个文档将被任意分类为一个类别)。要获取具有多值字段的重叠组,请使用 allow_overlap=True 关键字参数:

facet = sorting.QueryFacet(querydict, allow_overlap=True)

RangeFacet

这个 RangeFacet 用于数字字段类型。它将一系列可能的值分为若干组。例如,将基于价格的文档分组为100美元宽的存储桶:

pricefacet = sorting.RangeFacet("price", 0, 1000, 100)

第一个参数是字段的名称。接下来的两个参数是要划分的完整范围。超出此范围的值(在本例中,小于0且大于1000的值)将被排序为“缺少”(无)组。第四个参数是“间隙大小”,即范围内划分的大小。

“间隙”可以是一个列表,而不是单个值。在这种情况下,列表中的值将用于设置初始分区的大小,列表中的最后一个值是所有后续分区的大小。例如::

pricefacet = sorting.RangeFacet("price", 0, 1000, [5, 10, 35, 50])

…将设置0-5、5-15、15-50、50-100的分区,然后使用50作为所有后续分区(即100-150、150-200等)的大小。

这个 hardend 关键字参数控制最后一个除法是钳制到范围的末尾,还是允许越过范围的末尾。例如,这个:

facet = sorting.RangeFacet("num", 0, 10, 4, hardend=False)

…给出0-4、4-8和8-12分区,而:

facet = sorting.RangeFacet("num", 0, 10, 4, hardend=True)

…给出0-4、4-8和8-10分区。(默认为 hardend=False

注解

范围/桶总是 inclusive 在开始和 exclusive 最后。

DateRangeFacet

这就像 RangeFacet 但对于日期时间字段。开始值和结束值必须为 datetime.datetime 对象和间隙为 datetime.timedelta 物体。

例如::

from datetime import datetime, timedelta

start = datetime(2000, 1, 1)
end = datetime.now()
gap = timedelta(days=365)
bdayfacet = sorting.DateRangeFacet("birthday", start, end, gap)

和一样 RangeFacet ,可以使用间隙列表和 hardend 关键字参数。

ScoreFacet

这个方面有时对排序有用。

例如,要按“类别”字段排序,然后对于具有相同类别的文档,按文档的分数排序:

cats = sorting.FieldFacet("category")
scores = sorting.ScoreFacet()
results = searcher.search(myquery, sortedby=[cats, scores])

这个 ScoreFacet 总是先高分后低分。

注解

使用时 sortedby=ScoreFacet() 应给出与使用默认评分排序相同的结果( sortedby=None ,使用方面会变慢,因为whoosh在排序时会自动关闭许多优化。

FunctionFacet

这个方面允许您传递一个自定义函数来计算文档的排序/分组键。(使用此方面类型可能比将方面类型和分类程序子类化以设置某些自定义行为更容易。)

将使用索引搜索器和索引文档ID作为参数调用函数。例如,如果索引中包含术语向量:

schema = fields.Schema(id=fields.STORED,
                       text=fields.TEXT(stored=True, vector=True))
ix = RamStorage().create_index(schema)

…您可以使用一个函数对文档进行排序,使其越接近两个术语的相等出现:

def fn(searcher, docnum):
    v = dict(searcher.vector_as("frequency", docnum, "text"))
    # Sort documents that have equal number of "alfa" and "bravo" first
    return 0 - (1.0 / (abs(v.get("alfa", 0) - v.get("bravo", 0)) + 1.0))

facet = sorting.FunctionFacet(fn)
results = searcher.search(myquery, sortedby=facet)

StoredFieldFacet

这个方面允许您使用存储的字段值作为文档的排序/分组键。这通常比使用索引字段慢,但当使用 allow_overlap 实际上,对于大型索引来说,它可以更快,因为它避免了读取发布列表的开销。

StoredFieldFacet 支架 allow_overlap 通过将存储的值拆分为单独的键。默认情况下,它调用值的 split() 方法(因为大多数存储值都是字符串),但可以提供自定义的拆分函数。请参见 allow_overlap 下面。

MultiFacet

此方面类型返回由两个或多个子方面返回的键的组合,允许您按多个方面的相交值排序/分组。

MultiFacet 具有用于添加方面的方法::

myfacet = sorting.RangeFacet(0, 1000, 10)

mf = sorting.MultiFacet()
mf.add_field("category")
mf.add_field("price", reverse=True)
mf.add_facet(myfacet)
mf.add_score()

还可以传递字段名列表和/或 FacetType 初始值设定项的对象::

prices = sorting.FieldFacet("price", reverse=True)
scores = sorting.ScoreFacet()
mf = sorting.MultiFacet(["category", prices, myfacet, scores])

遗漏值

  • 排序时,对于不同方面类型,没有给定字段中任何术语的文档或其他构成“缺少”的内容的文档将始终排序到末尾。
  • 分组时,“丢失”文档将显示在一个带有键的组中。 None .

使用重叠组

用于分组和排序的通用支持工作流是给定字段的 一个文档值, 例如A path 包含原始文档的文件路径的字段。默认情况下,面被设置为支持这种单值方法。

当然,在某些情况下,您希望基于每个文档具有多个术语的字段将文档分为多个组。最常见的例子是 tags 字段。这个 allow_overlap 关键字参数 FieldFacetQueryFacetStoredFieldFacet 允许这种多值方法。

但是,有一个重要的警告:使用 allow_overlap=True 比默认值慢,可能 much 对于非常大的结果集,速度较慢。这是因为whoosh必须阅读字段中每个术语的每篇文章,以创建一个临时的“转发索引”,将文档映射到术语。

如果字段被索引为 项向量, FieldFacet 会用它们加速 allow_overlap 对于较小的结果集,但是对于较大的结果集,whoosh必须为每个匹配的文档打开向量列表,这仍然非常缓慢。

对于非常大的索引和结果集,如果存储了字段,则可以使用 StoredFieldFacet 而不是 FieldFacet . 虽然读取存储值通常比使用索引慢,但在这种情况下,避免打开大量的发帖阅读器的开销可以使其值得。

StoredFieldFacet 支架 allow_overlap 通过加载给定字段的存储值并将其拆分为多个值。默认值是调用值的 split() 方法。

例如,如果您存储了 tags 字段作为字符串 "tag1 tag2 tag3" ::

schema = fields.Schema(name=fields.TEXT(stored=True),
                       tags=fields.KEYWORD(stored=True))
ix = index.create_in("indexdir")
with ix.writer() as w:
    w.add_document(name="A Midsummer Night's Dream", tags="comedy fairies")
    w.add_document(name="Hamlet", tags="tragedy denmark")
    # etc.

…然后您可以使用 StoredFieldFacet 这样地::

ix = index.open_dir("indexdir")
with ix.searcher() as s:
    sff = sorting.StoredFieldFacet("tags", allow_overlap=True)
    results = s.search(myquery, groupedby={"tags": sff})

对于字符串以外的存储python对象,可以提供一个split函数(使用 split_fn 关键字参数 StoredFieldFacet )函数应接受单个参数(存储值),并返回分组键的列表或元组。

使用自定义排序顺序

每次搜索都有一个自定义的排序顺序有时很有用。例如,不同的语言使用不同的排序顺序。如果您有返回给定字段值所需排序顺序的函数,例如Unicode排序算法(UCA)的实现,则可以自定义用户语言的排序顺序。

这个 whoosh.sorting.TranslateFacet lets you apply a function to the value of another facet. 这允许您将字段值“转换”为任意排序键,例如使用UCA::

from pyuca import Collator

# The Collator object has a sort_key() method which takes a unicode
# string and returns a sort key
c = Collator("allkeys.txt")

# Make a facet object for the field you want to sort on
nf = sorting.FieldFacet("name")

# Wrap the facet in a TranslateFacet with the translation function
# (the Collator object's sort_key method)
tf = sorting.TranslateFacet(facet, c.sort_key)

# Use the facet to sort the search results
results = searcher.search(myquery, sortedby=tf)

(您可以将多个“包裹”面传递给 TranslateFacet ,它将使用方面的值作为多个参数调用函数。)

translatefacet还可以非常有用,用于对某些公式的输出进行排序:

# Sort based on the average of two numeric fields
def average(a, b):
    return (a + b) / 2.0

# Create two facets for the fields and pass them with the function to
# TranslateFacet
af = sorting.FieldFacet("age")
wf = sorting.FieldFacet("weight")
facet = sorting.TranslateFacet(average, af, wf)

results = searcher.search(myquery. sortedby=facet)

请记住,您仍然可以按多个方面进行排序。例如,可以先按量化函数转换的数值排序,然后按另一个字段的值排序,如果相等:

# Sort by a quantized size first, then by name
tf = sorting.TranslateFacet(quantize, sorting.FieldFacet("size"))
results = searcher.search(myquery, sortedby=[tf, "name"])

专家:写你自己的方面

TBD。