目录

whoosh和jieba实现中文全文检索

截止目前(2018-8-5),Whoosh 项目已经整整一年没有更新(最后提交于 2017-07-16),作者可能已经弃坑。

简介

Whoosh 是一个纯 Python 实现的全文检索引擎,虽然不如 Elasticsearch,但好处是纯 Python 实现易于集成,在小项目中应用广泛。

Whoosh 自带的分词器不支持中文分词。jieba 是一个中文分词组件,实现了一个供 Whoosh 调用的中文分词器。两者结合使用即可以实现中文全文检索。

快速上手

环境准备

1
2
pip3 install jieba
pip3 install whoosh

简单示例

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
#!/usr/bin/env python3
## coding=utf-8

import os
from whoosh.index import create_in
from whoosh.fields import TEXT, ID, Schema
from jieba.analyse import ChineseAnalyzer

# 创建 jieba 中文分词器对象
analyzer = ChineseAnalyzer()

# 创建schema,使用 jieba 分词器对象
schema = Schema(
    title=TEXT(stored=True, analyzer=analyzer),
    path=ID(stored=False),
    content=TEXT(stored=True, analyzer=analyzer),
)

# 存储schema信息至 indexdir 目录下
indexdir = "indexdir/"
if not os.path.exists(indexdir):
    os.mkdir(indexdir)
ix = create_in(indexdir, schema)

# 按照schema定义信息,增加需要建立索引的文档
writer = ix.writer()
writer.add_document(
    title="第一篇文档",
    path="/a",
    content="这是我们增加的第一篇文档"
)
writer.add_document(
    title="第二篇文档",
    path="/b",
    content="第二篇文档也很interesting!"
)
writer.commit()

# 创建一个检索器
searcher = ix.searcher()

# 检索标题中出现“文档”的文档
results = searcher.find("title", "文档")

# 检索出来的第一个结果
firstdoc = results[0].fields()

print(firstdoc)                         # 打印出检索出的文档全部内容
print(results[0].highlights("title"))   # 高亮标题中的检索词
print(results[0].score)                 # bm25分数

searcher.close()

输出结果:

1
2
3
{'content': '这是我们增加的第一篇文档', 'title': '第一篇文档'}
第一篇<b class="match term0">文档</b>
0.5945348918918356

创建 Schema

使用 Whoosh 进行检索需要创建索引对象(index object),而索引对象的结构由 Schema 定义。

Schema 会列出索引对象的字段(field),字段是文档(document)中的一条信息,例如标题或者文本内容。字段可以通过参数,设置是否被搜索,是否在搜索结果中与被索引的字段一起返回。

类比 SQL 数据库表:

Whoosh 索引SQL 数据库表
Schema表结构
field 字段字段 field
document 文档记录 record
索引对象整个表

举个例子:

1
2
3
4
5
6
7
8
from whoosh.fields import TEXT, Schema
from jieba.analyse import ChineseAnalyzer

schema = Schema(
    title=TEXT(stored=True, analyzer=ChineseAnalyzer()),
    id=ID(stored=True),
    content=TEXT(stored=True),
)

上面的代码创建了一个 Schema,由 title、id 和 content 三个 filed 组成。title 和 content 的类型为 TEXT,id 的类型为 ID

Whoosh 中为 field 预定义了以下类型:

  • whoosh.fields.TEXT 用于正文。默认使用 whoosh.analysis.StandardAnalyzer 分词并索引,但不在检索结果中返回。 analyzer 参数指定分词器,stored 参数指定是否在检索结果中返回,phrase 参数指定是否允许分词检索。
  • whoosh.fields.KEYWORD 用于以空格或逗号分隔的关键字。默认索引,不在检索结果中返回,不能分词。
  • whoosh.fields.ID 用于日期、路径、URL,分类等等。字段的值作为整体被索引,默认不返回,不能分词。
  • whoosh.fields.STORED 用于在检索结果中展示的信息。不会被索引且无法检索,会在检索结果中返回。
  • whoosh.fields.NUMERIC 用于数字,可以存储整数或浮点数。
  • whoosh.fields.DATETIME 用于 datetime 对象。
  • whoosh.fields.BOOLEAN 用于布尔值。

其中 TEXTID 比较常用。

创建索引

有了 Schema 之后,就可以通过 Schema 创建索引。Whoosh 的索引使用文件存储,创建时传入目录名,索引文件就会存储在该目录下。

1
2
3
4
5
6
7
from whoosh.index import create_in, exists_in

indexdir = "indexdir/"
if not os.path.exists(indexdir):
    os.mkdir(indexdir)
if not exists_in(indexdir):
    ix = create_in(indexdir, schema)

exists_in 返回目录下是否存在索引,create_in 创建索引。如果在一个已经存在索引文件的目录下调用 create_in 创建索引,原索引会丢失。

exists_increate_in 函数都可以传入 indexname 参数,指定创建的索引名称。

添加文档

有了索引对象后,就可以添加文档数据到索引中。

首先,获取索引对象:

1
2
3
from whoosh.index import open_dir

ix = index.open_dir(indexdir)

open_dir 函数用于从指定目录中获取索引对象,该函数也可以传入 indexname 参数指定索引名称。

通过索引对象新建一个 IndexWriter 对象,该对象会锁住索引以进行写入,保证同时只有一个进程/线程进行写操作。

1
writer = ix.writer()

通过该 IndexWriter 对象的 add_document 方法添加 document,该方法接受关键字参数,与 Schema 定义保持一致。

1
2
3
4
5
writer.add_document(
    title="第一篇文档",
    path="/a",
    content="这是我们增加的第一篇文档"
)

所有 docment 添加完成后,调用 commit 方法写入索引。

1
writer.commit()

检索结果

首先通过索引对象创建一个 Searcher 对象,该对象使用完成后应该被关闭,可以使用 Python 中的 with 语法。

1
2
3
with ix.searcher() as searcher:
    # 查询操作
    pass

可以直接使用 find 方法进行检索:

1
2
with ix.searcher() as searcher:
    results = searcher.find("title", "文档")

或者先构造 QueryParser 对象,再使用 search 方法检索:

1
2
3
4
5
6
7
from whoosh.qparser import QueryParser

qp = QueryParser("content", schema=myindex.schema)
q = qp.parse(u"hello world")

with ix.searcher() as s:
    results = s.search(q)

search 方法可以使用 limit 参数指定返回结果的个数:

1
results = s.search(q, limit=20)

search_page 方法可以分页返回结果,默认每页 10 条结果,可以通过参数设置:

1
2
# 每页20条结果,返回第 5 页的结果
results = s.search_page(q, 5, pagelen=20)

查询结果 results 对象类似 list,使用索引可以访问单个结果。

1
result = results[0]

单个结果的 score 属性可以得到对该 document 的权重评分,highlights 方法可以对指定 field 中的检索词进行高亮(加 b 标签)。

1
2
result.score
result.highlights("title")

在 Flask 中使用 Whoosh

在 Flask 中使用 Whoosh 最好用的扩展是 Flask-WhooshAlchemy,与 Flask-SQLAlchemy 无缝集成。可以直接索引数据库中的记录并通过数据库查询检索。

但这个项目已经许久没有更新,PyPI 上的版本竟然不支持 Python3,这里推荐使用从 Flask-WhooshAlchemy fork 开发的 Flask-WhooshAlchemyPlus 扩展,使用起来也非常简单,只需要以下步骤:

  1. 在 Flask 配置文件中定义 WHOOSH_BASE,即 Whoosh 索引文件的存储路径;
  2. 在定义数据库模型时指定 whoosh 索引字段和分词器;
  3. 调用扩展的 init_app(app) 方法初始化;
  4. 在查询中使用 whoosh_search 进行检索,返回的结果为 query 对象,可以继续进行筛选,排序等,最后返回数据库记录;

详细请参考 Flask-WhooshAlchemyPlus 的文档。

Jieba 延迟加载机制

jieba 采用延迟加载,import jieba 和 jieba.Tokenizer() 不会立即触发词典的加载,一旦有必要才开始加载词典构建前缀字典。

由于此机制,在初次查询时才会触发词典加载(大约 1 秒钟时间)。我们可以手动加载词典:

1
2
import jieba
jieba.initialize()

在 Flask 应用中,可以在创建 app 时调用上述代码。

参考链接