dot 快速的未来正在您所在的城市举办的活动中到来。

加入我们参加 Redis 发布会

如何使用 Redis 构建知识库平台

使用 Redis Stack 构建多模型应用程序非常简单!按照本教程,学习如何使用 Python 构建包含强大搜索功能的知识库。

公司每天都会产生大量的数据。组织数据、过滤掉过时信息并使其随时可用是一项巨大挑战。这不是一个简单的问题。

理解数据与存储数据是不同的任务。实现这一目标包括记录数据的组织方式以及创建、编辑、审查和发布数据的流程,并了解编辑者和消费者之间的关系。

什么是知识库系统,为什么要使用它们?

知识库系统充当有关产品、服务、部门或主题的信息库。在这些系统中,文档是动态资产,可以被引用、改进、分类、共享或对未经授权的用户隐藏。典型的例子包括常见问题数据库、操作指南以及新员工入职材料。知识库应该针对公司的需求量身定制,这意味着它需要与现有工具和流程进行交互。

互联网使知识库系统流行起来。使用超文本可以对文档进行结构化和连接,超文本是互联网本身的核心思想。以这种方式组织和链接内容永远改变了生产者和消费者从知识中创造价值的方式。

在这篇文章中,我将演示如何使用 Redis 的 数据结构搜索功能 来构建具有基本功能的网络知识库平台。该项目非常适合使用 Redis 的强大功能进行实时全文搜索和查询,因为它满足了此类系统的主要目的:提供多种方法来检索消费者正在寻找的信息,并且尽可能快地检索信息。

Keybase:知识库项目

本教程将帮助您构建一个可以存储和提供文档以用于典型用例的知识库,例如,客户想要了解新产品功能、解决技术问题,或作为内部部门的单一可靠的真相来源。

该项目的源代码可在 GitHub 存储库 中找到,并根据 MIT 许可证授权。

我将这个项目命名为“Keybase”,这是一个预览。

How Do I Install Redis Stack image

组件

在本教程中,我将使用以下组件来开发一个可工作的原型

  • Redis Stack 服务器:它将开源 Redis 与 RediSearch、RedisJSON、RedisGraph、RedisTimeSeries 和 RedisBloom 结合在一起。Redis Stack 还包括 RedisInsight,这是一个用于理解和优化存储在 Redis 中的数据的可视化工具
  • Redis-py:Redis 的 Python 客户端库
  • Flask:一个轻量级的 Python web 框架
  • Toast UI web 编辑器一个简单而强大的所见即所得 Markdown 编辑器
  • JQueryJQueryUINotify.jsChart.jsJavascript 库和框架
  • Bulma CSS 框架一个 CSS 框架,用于构建精美且响应式的 web 界面
  • NginxGunicorn:与 Flask 一起工作的 web 服务器和 web 服务器网关接口

架构

knowledge base architecture design

流行的 Nginx web 服务器与 Gunicorn 一起使用,Gunicorn 实现 web 服务器网关接口并提供 Flask 应用程序。

知识库后端是作为 Python 应用程序实现的,该应用程序是在流行的 Flask 框架之上开发的。Flask 在视图之间提供了良好的分离。该应用程序是使用 Jinja 模板引擎和 Werkzeug WSGI 工具包提供的控制器来实现的。用户身份验证基于流行的身份验证即服务平台 Okta

前端使用 CSS 框架 Bulma 进行样式化;JQuery 是用于 Ajax 通信和用户界面操作的 Javascript 库,与 JQueryUI 一起使用,而 Notify.js 用于 UI 通知。

作为 cron 作业部署的附加服务会对文档进行索引以进行文档相似性推荐。文档索引与会话方便地分离,以避免影响用户体验。

最后,在数据层中,该项目使用 Redis 数据库以及两个 Redis 模块:RediSearch 和 RedisTimeSeries,在本教程中,我分别使用它们进行文档索引和搜索以及分析。

数据模型

知识库中的文档被建模为 哈希。以下是数据的存储方式:

HGETALL keybase:kb:9c6c48c2-eb5d-11ec-893a-42010a000a02
 1) "update"
 2) "1655154783"
 3) "processable"
 4) "0"
 5) "tags"
 6) "troubleshooting"
 7) "state"
 8) "draft"
 9) "owner"
10) "00u5brr84hIXBoDRo5d7"
11) "name"
12) "How to Troubleshoot Performance Issues?"
13) "content_embedding"
14) "\xf5\xcc\x1e=D\x82\xd0\xbc\x81\x90\xfa\xbc5&\xab<\x98\xaa\xb0=;`\xf2\xbc\xc1\x00Z\xbb9\xe4\xee\xbc\xee\x88\xce9\xd8^\x80\xbclX\xe0\xbc\a\xaf\x9a\xbdTR\xd8<\xb6Yv=w6^=\x1ftd\xbd M\x19\xbd&BP=\x04\x92\\\xbd@\xb6P=\xd7\xce\x12\xbc\x11" [...]
15) "creation"
16) "1655154783"
17) "content"
18) "Checklist to investigate performance issues.\n\n1. Check slow log looking for\xc2\xa0 `EVALSHA`, `HGETALL`, `HMGET`, `MGET`, and all types of `SCAN` commands. Lower slow log threshold to capture more slow commands. You can configure the threshold using `CONFIG SET slowlog-log-slower-than <THRESHOLD_MICROSECONDS>`\n2. Avoid the" [...]

哈希数据结构存储

  • 文档本身:名称和内容
  • 文档创建和更新时间戳
  • 文档状态:草稿或已发布
  • 一个或多个用于高级搜索的标签
  • 文档的所有者(用户拥有自己的私人草稿)
  • 标志 processable,在创建或更新文档时激活,列出要重新索引以进行相似性搜索的文档
  • 字段 content_embedding 存储文档的模型,用于相似性搜索(稍后讨论)

都明白了吗?我们准备深入了解 Keybase 功能及其实现方式。

探索搜索功能

我们对 Redis 数据库强大的实时搜索功能感到非常自豪;本教程让我们展示了它。

您可能认为搜索是一种蛮力查找,但实际上,当我们使用 RediSearch 索引功能时,不会出现缓慢扫描。我们在这里简要介绍的选择是

  • 全文搜索:返回包含名称和/或内容中一个或多个关键字的文档。
  • 标签字段搜索:返回具有特定关联标签的文档。考虑使用此方法搜索具有特定属性的文档(例如,标记为教程标签的文档)。
  • 向量相似性搜索(VSS):返回与特定文档相似的文档。考虑在开发推荐系统时使用此方法。

有关 Redis 中搜索功能的更深入解释,请参阅 RediSearch 简介 模块。

RediSearch 使用以下语法创建索引

FT.CREATE document_idx ON HASH 
PREFIX 1 "keybase:kb" 
SCHEMA name TEXT 
       content TEXT 
       creation NUMERIC SORTABLE 
       update NUMERIC SORTABLE 
       state TAG 
       owner TEXT 
       processable TAG 
       tags TAG 
       content_embedding VECTOR HNSW 6 TYPE FLOAT32 DIM 768 DISTANCE_METRIC COSINE

索引 document_idx 的模式支持使用相应的字段类型进行三种类型的搜索

  • TEXT 允许对文档名称及其内容进行实时全文搜索(通过倒排索引实现)
  • TAG 提供实时查询功能以过滤文档(例如,按文档状态或自定义标签过滤)并按创建或更新时间戳排序结果
  • VECTOR 允许使用属性 content_embedding 存储的向量搜索相似文档

全文搜索

web 界面中的搜索输入字段用于执行全文搜索,该搜索查看知识库文档的名称和内容。

考虑使用 redis-py 客户端库进行实时全文搜索的示例。它在从客户端接收的 query 字符串上执行搜索。请注意,此命令将排除 draft 状态的文档;此类文档必须保持隐藏,并且不应包含在输出中

connection.ft("document_idx")
    .search(Query(query + " -@state:{draft}")
    .return_field("name")
    .return_field("creation")
    .sort_by("creation", asc=False)
    .paging(offset, per_page))

这个整洁的代码示例执行实时全文搜索并返回从指定 offset 开始的一批 per_page 文档(这对在 UI 中对文档进行分页很有用)。它满足指定的搜索条件:所有满足用户在输入字段中提供的查询的文档,它会删除处于 draft 状态的文档,并且会按 creation 时间戳排序文档。

标签字段搜索

Python 中按标签进行实时查询的示例,其中搜索与对标签的过滤相结合,并且还排除了处于 draft 状态的文档,如下所示,它返回所有标记为“troubleshooting”标签的文档

connection.ft("document_idx")
    .search(Query("@tags:{troubleshooting} -@state:{draft}")
    .return_field("name")
    .return_field("creation")
    .sort_by("creation", asc=False)
    .paging(offset, per_page))

这段代码片段是一个纯实时查询,包括使用 troubleshooting keyword 标记的所有文档,但还排除了不是公共文档并且仍然被分类为私人草稿的文档。此语法使用多个字段上的索引来过滤、排序和分页结果。

向量相似性搜索

您一定很熟悉这种搜索,尤其是在电商网站上。个性化推荐通过诸如“您可能还对以下内容感兴趣”或“购买此商品的客户还购买了以下商品”等提示,将用户引导至类似内容。 

此示例应用程序不需要增加产品销量,因为知识库网站没有商品可售,但我们确实希望为用户提供有意义的推荐。 

您可以使用 Redis 实时搜索的 VSS 功能添加推荐。

如果您尚未在项目中使用这种类型的搜索,请从Rediscover Redis for Vector Similarity Search开始。 

简短说明:要使用 VSS 功能,您首先要创建模型,然后存储它,最后查询它。

使用 VSS 进行推荐背后的核心概念是将文档内容转换为其对应的向量嵌入。向量是描述文档的实体,它与其他向量进行比较以返回最佳匹配。向量嵌入存储在哈希数据结构中(在 content_embedding 值中)。 

在此示例中,我们使用 Python SentenceTransformer 库使用 all-distilroberta-v1 模型(来自 Hugging Face 数据科学平台)计算向量嵌入,如下所示。

from sentence_transformers import SentenceTransformer

model = SentenceTransformer('sentence-transformers/all-distilroberta-v1')
embedding = model.encode(content).astype(np.float32).tobytes()

在此示例中,文档内容被向量化并以二进制格式序列化,这是 VSS 接受的格式。此操作在文档创建时和任何后续文档更新时都会触发两次。特别是,当更新文档内容时,需要重新计算向量嵌入,否则索引会变得过时并返回不精确的结果。要触发离线向量嵌入重新计算,如架构中所述,只需激活 processable 标志并定期列出需要刷新向量嵌入的文档,然后对其进行处理。例如

rs = connection.ft("document_idx")
    .search(Query('@processable:{1}')
    .return_field("content")
    .return_field("processable"))
    if not len(rs.docs):
        print("No vector embedding to be processed!")
        sys.exit()

model = SentenceTransformer('sentence-transformers/all-distilroberta-v1')
    for doc in rs.docs:
        # Recalculate the embedding and store it

使用与之前标签搜索示例中相同的语法,此命令执行搜索以包括那些将 processable 标签设置为 1 的文档。此搜索返回的文档将重新计算并存储关联的向量。

存储模型

生成向量嵌入后,您可以将其像往常一样存储在文档哈希数据结构中(在 content_embedding 字段中)。

最后,将 processable 标志设置为零。在文档下次更改之前,无需对向量嵌入进行进一步更新。

doc = { "content_embedding" : embedding, "processable":0}
connection.hmset("keybase:kb:{}".format(key), doc)

执行搜索

每次用户看到文档时,侧边栏都会提供推荐列表。这意味着 Keybase 应用程序必须找出推荐是什么。 

要实现这一点,我们从当前呈现的文档(即用户正在查看的文档)的哈希中获取向量嵌入。

current_doc_embedding = connection.hget("keybase:kb:0373bede-d2c1-11ec-b195-42010a000a02", "content_embedding")

hget 返回与字段 content_embedding 关联的值。

要检索最相似的文档,请将向量与存储的其余向量进行比较。 

q = Query("*=>[KNN 6 @content_embedding $vec]")
    .sort_by("__content_embedding_score")
res = connection.ft("document_idx")
    .search(q, query_params={"vec": current_doc_embedding})
    for doc in res.docs:
        # Show the documents in the box

在此示例中,查询配置为执行强大的向量相似性搜索,通过检索 k 个最近邻 (KNN) 返回六个最相似的文档。 

有关更多 VSS 语法示例,请参阅客户端库 文档。 

建立知识库的用户数据管理

每个业务应用程序都需要确定哪些用户可以访问软件,哪些用户不能访问软件。对于我们的知识库身份验证,我们使用流行的身份即服务平台 Okta。用户存储在 Redis 数据库中,以字符串“keybase:okta:”为前缀的哈希。我测试了与 Okta Developer Edition 的集成。 

以下是一个用户配置文件的示例

127.0.0.1:6380> HGETALL keybase:okta:00u5clwmy9dGg6wPC5d7
 1) "name"
 2) "Test Account"
 3) "given_name"
 4) "Test"
 5) "email"
 6) "test@account.com"
 7) "group"
 8) "viewer"
 9) "signup"
10) "1655510121.2172642"
11) "login"
12) "1655510121.217265"

要搜索知识库用户,我们在用户哈希数据结构上创建索引

FT.CREATE user_idx ON HASH PREFIX 1 "keybase:okta" 
SCHEMA 
    name TEXT 
    group TEXT

该组规范了基于角色的访问控制 (RBAC) 的实施。我为这个示例应用程序创建了三种用户类型,尽管可以添加其他角色。当前角色是

  • 查看者:通常,查看者可以阅读、搜索和收藏文档
  • 编辑者:编辑者可以创建、编辑和标记草稿,并将它们提交以供审核 
  • 管理员:管理员可以发布文档或将其恢复为草稿,归档和删除内容。他们还可以管理网站,例如导入和导出数据,以及禁止用户。

最后,还可以通过将文档 ID 存储在集合中来收藏文档。哈希仍然是最佳选择,因为它具有灵活性;您可以存储文档 ID 以及收藏的个人评论。

以下是一个经过身份验证的用户的集合示例

127.0.0.1:6380> HGETALL keybase:bookmark:00u5brr84hIXBoDRo5d7
1) "efb73056-d2c0-11ec-b195-42010a000a02"
2) "May solve several customer cases"
3) "0373bede-d2c1-11ec-b195-42010a000a02"
4) "TLDR"
5) "2f678872-d2c1-11ec-b195-42010a000a02"
6) ""

使用分析来发现趋势

许多业务场景要求组织监控知识库的活动。例如,您可能希望根据主题领域评估文档受欢迎程度,减少重复问题,或挖掘新功能的想法。

RedisTimeSeries 功能是分析功能的自然选择,因为它经过优化以存储和聚合大量数据。

redistimeseries chart

跟踪知识库的总体访问量的代码(例如)如下所示

connection.ts().add("keybase:visits", "*", 1, duplicate_policy='first')

此代码计算过去一个月的访问量,按天聚合。它以 JSON 格式化,可以提供给 Chart.js 可视化库

bucket = 24*60*60*1000
duration = 2592000000
ts = round(time.time() * 1000)
ts0 = ts - duration
# 86400000 ms in a day
# 3600000 ms in an hour

visits_ts = connection.ts()
   .range("keybase:visits", from_time=ts0, to_time=ts, aggregation_type='sum', bucket_size_msec=bucket)
visits_labels = [datetime.utcfromtimestamp(int(x[0]/1000)).strftime('%b %d') for x in visits_ts]
visits = [x[1] for x in visits_ts]
visits_graph = {}
visits_graph['labels'] = visits_labels
visits_graph['value'] = visits
visits_json = json.dumps(visits_graph)

然后,要在 Jinja 模板中呈现图表,请使用以下 Javascript

var data_js = {{ visits_json|tojson }};
const ctx = $('#visits');
const myChart = new Chart(ctx, {
  type: 'bar',
  data: {
  	labels: JSON.parse(data_js).labels,
  	datasets: [{
      	label: '# of Visits',
      	data: JSON.parse(data_js).value,
      	fill: false,
      	backgroundColor: 'rgba(203, 70, 56, 0.2)',
      	borderColor: 'rgb(203, 70, 56)',
      	borderWidth: 1
  	}]
  },
  options: {
	responsive: true,
	maintainAspectRatio: true,
  	scales: {
      	y: {
          	beginAtZero: true
      	}
  	}
  }
}); 

图表在 HTML canvas 元素中呈现。

<canvas id="visits"></canvas>

此方法使您能够监控其他指标,例如每个文档的查看次数或用户活动。您可以使用 Redis 时间序列聚合功能来计算平均值、方差、标准差以及各种统计信息。

设置管理功能

为了完善示例应用程序,我们需要确保可以管理知识库。

管理员可以使用 RBAC 策略向用户授予或撤销角色。访问控制使用 Python 装饰器和自定义用户类实现。(有关详细信息,请参阅存储库。) 

例如,用于执行备份的受保护方法的签名可能为

@admin.route('/backup', methods=['GET'])
@login_required
@requires_access_level(Role.ADMIN)
def backup(): 
    # Take the backup

您可以使用异步 BGSAVE 命令本机生成 Redis 数据库备份。除了本机备份方法之外,知识库还实现了逻辑数据导入和导出例程。导出数据是通过使用非阻塞 SCAN 命令以 20 个元素的批次迭代 Redis 键空间来实现的。

while True:
    cursor, keys  = conn.scan(cursor, match='keybase*', count=20, _type="HASH")
    for key in keys:
        hash = conn.hgetall(key)
        # Do something with the data
        # ...
            
    if (cursor==0): # Exit condition when the iteration is over
        break

接下来该做什么

此项目是一个概念验证,用于展示 Redis 在用作主数据库时所具有的功能。GitHub 存储库 中的源代码(在 MIT 许可下)出于演示目的而共享,不应用于生产环境。但是,您可以查阅、克隆和/或派生存储库,并重复使用所有示例,以更好地了解本文中讨论的搜索和索引功能。我鼓励您这样做。

我开发 Keybase 来演示 Redis 如何轻松地替换这些类型的 Web 应用程序中的关系数据库。令人惊讶的是,发现对数据进行建模是多么有益。哈希数据结构整洁紧凑!我还很高兴地了解到,Redis 用于相似性搜索和时间序列的独特功能为标准问题提供了完整的开箱即用解决方案,否则这些问题需要多个专门的数据库。使用 Redis Stack 构建多模型应用程序出奇地简单!

您可以添加许多其他功能来补充此项目,例如使用多用户草稿、修订和反馈收集来改善创作体验。此项目还可以与 Slack、Jira、ZenDesk 或任何组织团队工作的平台集成。其他功能,如扫描文档的敏感词、检测断开的链接以及用户审核功能,可以设计用于将此软件从实用程序级别提升到企业级。 

我邀请您克隆或派生 Keybase 存储库 并进行设置。您可以在笔记本电脑上运行它,前提是您拥有本地 Redis Stack 安装(或 免费 Redis Cloud 数据库),以及 Python 环境。希望您玩得开心!