索引和搜索文档层次结构

概述

whoosh的全文索引基本上是一个平面文档数据库。然而,whoosh支持两种模拟层次文档索引和查询的技术,即形成父子层次结构的文档集,例如“章节-部分-段落”或“模块-类-方法”。

可以指定父子关系 在索引时间, 通过将文档分组到同一层次结构中,然后使用 whoosh.query.NestedParent 和/或 whoosh.query.NestedChildren 以子女为基础寻找父母,反之亦然。

或者,您可以使用 查询时间联接, 本质上类似于数据库中的外部键联接,在数据库中执行一次搜索以查找相关文档,然后在该文档上使用存储值(例如, parent 字段)查找另一个文档。

这两种方法都有利弊。

使用嵌套文档索引

索引

此方法通过将“父”文档及其所有“子”文档*索引为“组”*来工作,因此保证它们最终位于同一段中。可以使用返回的上下文管理器 IndexWriter.group() 将文档分组:

with ix.writer() as w:
    with w.group():
        w.add_document(kind="class", name="Index")
        w.add_document(kind="method", name="add document")
        w.add_document(kind="method", name="add reader")
        w.add_document(kind="method", name="close")
    with w.group():
        w.add_document(kind="class", name="Accumulator")
        w.add_document(kind="method", name="add")
        w.add_document(kind="method", name="get result")
    with w.group():
        w.add_document(kind="class", name="Calculator")
        w.add_document(kind="method", name="add")
        w.add_document(kind="method", name="add all")
        w.add_document(kind="method", name="add some")
        w.add_document(kind="method", name="multiply")
        w.add_document(kind="method", name="close")
    with w.group():
        w.add_document(kind="class", name="Deleter")
        w.add_document(kind="method", name="add")
        w.add_document(kind="method", name="delete")

或者,您可以使用 start_group()end_group() 方法::

with ix.writer() as w:
    w.start_group()
    w.add_document(kind="class", name="Index")
    w.add_document(kind="method", name="add document")
    w.add_document(kind="method", name="add reader")
    w.add_document(kind="method", name="close")
    w.end_group()

Each level of the hierarchy should have a query that distinguishes it from other levels (for example, in the above index, you can use kind:classkind:method 以匹配层次结构的不同级别)。

一旦索引了文档的层次结构,就可以使用两种查询类型根据子级查找父级,反之亦然。

(对于嵌套查询,当前在默认查询分析器中不支持。)

嵌套父查询

这个 whoosh.query.NestedParent 查询类型允许您为子文档指定查询,但让查询返回层次结构中较高级别的“祖先”文档:

# First, we need a query that matches all the documents in the "parent"
# level we want of the hierarchy
all_parents = query.Term("kind", "class")

# Then, we need a query that matches the children we want to find
wanted_kids = query.Term("name", "close")

# Now we can make a query that will match documents where "name" is
# "close", but the query will return the "parent" documents of the matching
# children
q = query.NestedParent(all_parents, wanted_kids)
# results = Index, Calculator

请注意,在具有两个以上级别的层次结构中,可以指定与层次结构的任何级别匹配的“父级”查询,以便返回匹配子级或第二级、第三级等的顶级祖先。

查询的工作原理是首先构建一个位向量,表示哪些文档是“父”文档:

Index
|      Calculator
|      |
1000100100000100
    |        |
    |        Deleter
    Accumulator

然后,对于“子”查询的每个匹配项,它根据位向量计算上一个父级,并将其作为匹配项返回(无论子级匹配多少,它只返回每个父级一次)。此父级查找非常有效:

1000100100000100
   |
|<-+ close

NestedChildren查询

相反的 NestedParentwhoosh.query.NestedChildren . 此查询允许您匹配父级,但返回其子级。例如,搜索专辑标题并返回专辑中的歌曲时,这很有用:

# Query that matches all documents in the "parent" level we want to match
# at
all_parents = query.Term("kind", "album")

# Parent documents we want to match
wanted_parents = query.Term("album_title", "heaven")

# Now we can make a query that will match parent documents where "album_title"
# contains "heaven", but the query will return the "child" documents of the
# matching parents
q1 = query.NestedChildren(all_parents, wanted_parents)

然后可以将该查询与 AND 子句,例如查找歌曲标题中带有“hell”的歌曲,这些歌曲出现在专辑标题中带有“heaven”的专辑中:

q2 = query.And([q1, query.Term("song_title", "hell")])

删除和更新分层文档

索引时间方法的缺点是 更新和删除. 由于查询的实现依赖于段中相邻的父文档和子文档,因此不能仅更新/删除一个子文档。您只能一次更新/删除整个顶级文档(例如,如果您的层次结构是“章节-章节-段落”,则只能更新或删除整个章节,而不是章节或段落)。如果层次结构的顶层表示非常大的文本块,这可能需要大量的删除和重新索引。

目前 Writer.update_document() 不会自动处理嵌套文档。You must manually delete and re-add document groups to update them.

To delete nested document groups, use the Writer.delete_by_query() 方法与A NestedParent 查询:

# Delete the "Accumulator" class
all_parents = query.Term("kind", "class")
to_delete = query.Term("name", "Accumulator")
q = query.NestedParent(all_parents, to_delete)
with myindex.writer() as w:
    w.delete_by_query(q)

使用查询时间联接

在whoosh中模拟分层文档的第二种技术是在每个文档上使用存储字段指向其父级,然后在查询时使用该字段的值查找父级和子级。

例如,如果我们使用指向父级的指针而不是嵌套来索引类和方法的层次结构:

# Store a pointer to the parent on each "method" document
with ix.writer() as w:
    w.add_document(kind="class", c_name="Index", docstring="...")
    w.add_document(kind="method", m_name="add document", parent="Index")
    w.add_document(kind="method", m_name="add reader", parent="Index")
    w.add_document(kind="method", m_name="close", parent="Index")

    w.add_document(kind="class", c_name="Accumulator", docstring="...")
    w.add_document(kind="method", m_name="add", parent="Accumulator")
    w.add_document(kind="method", m_name="get result", parent="Accumulator")

    w.add_document(kind="class", c_name="Calculator", docstring="...")
    w.add_document(kind="method", m_name="add", parent="Calculator")
    w.add_document(kind="method", m_name="add all", parent="Calculator")
    w.add_document(kind="method", m_name="add some", parent="Calculator")
    w.add_document(kind="method", m_name="multiply", parent="Calculator")
    w.add_document(kind="method", m_name="close", parent="Calculator")

    w.add_document(kind="class", c_name="Deleter", docstring="...")
    w.add_document(kind="method", m_name="add", parent="Deleter")
    w.add_document(kind="method", m_name="delete", parent="Deleter")

# Now do manual joins at query time
with ix.searcher() as s:
    # Tip: Searcher.document() and Searcher.documents() let you look up
    # documents by field values more easily than using Searcher.search()

    # Children to parents:
    # Print the docstrings of classes on which "close" methods occur
    for child_doc in s.documents(m_name="close"):
        # Use the stored value of the "parent" field to look up the parent
        # document
        parent_doc = s.document(c_name=child_doc["parent"])
        # Print the parent document's stored docstring field
        print(parent_doc["docstring"])

    # Parents to children:
    # Find classes with "big" in the docstring and print their methods
    q = query.Term("kind", "class") & query.Term("docstring", "big")
    for hit in s.search(q, limit=None):
        print("Class name=", hit["c_name"], "methods:")
        for child_doc in s.documents(parent=hit["c_name"]):
            print("  Method name=", child_doc["m_name"])

这种技术比索引时间嵌套更灵活,因为您可以逐段删除/更新层次结构中的单个文档,尽管它不支持轻松查找不同的父级。它也比索引时间嵌套慢(可能慢得多),因为您必须对每个找到的文档执行额外的搜索。

未来版本的whoosh可能包括“join”查询,以使这个过程更高效(或者至少更自动)。