BM25 检索器¶
在本指南中,我们定义了一个使用 BM25 方法搜索文档的 BM25 检索器。BM25 (Best Matching 25) 是一种排名函数,它通过考虑词频饱和度和文档长度来扩展 TF-IDF。BM25 根据查询词在语料库中的出现频率和稀有度有效地对文档进行排名。
本 notebook 与 RouterQueryEngine notebook 非常相似。
设置¶
如果您在 colab 上打开此 Notebook,您可能需要安装 LlamaIndex 🦙。
%pip install llama-index
%pip install llama-index-retrievers-bm25
import os
os.environ["OPENAI_API_KEY"] = "sk-proj-..."
from llama_index.core import Settings
from llama_index.llms.openai import OpenAI
from llama_index.embeddings.openai import OpenAIEmbedding
Settings.llm = OpenAI(model="gpt-3.5-turbo")
Settings.embed_model = OpenAIEmbedding(model_name="text-embedding-3-small")
下载数据¶
!mkdir -p 'data/paul_graham/'
!wget 'https://raw.githubusercontent.com/run-llama/llama_index/main/docs/docs/examples/data/paul_graham/paul_graham_essay.txt' -O 'data/paul_graham/paul_graham_essay.txt'
--2024-07-05 10:10:09-- https://raw.githubusercontent.com/run-llama/llama_index/main/docs/docs/examples/data/paul_graham/paul_graham_essay.txt Resolving raw.githubusercontent.com (raw.githubusercontent.com)... 185.199.111.133, 185.199.109.133, 185.199.108.133, ... Connecting to raw.githubusercontent.com (raw.githubusercontent.com)|185.199.111.133|:443... connected. HTTP request sent, awaiting response... 200 OK Length: 75042 (73K) [text/plain] Saving to: ‘data/paul_graham/paul_graham_essay.txt’ data/paul_graham/pa 100%[===================>] 73.28K --.-KB/s in 0.05s 2024-07-05 10:10:09 (1.36 MB/s) - ‘data/paul_graham/paul_graham_essay.txt’ saved [75042/75042]
加载数据¶
我们首先展示如何将 Document 转换为一组 Node,并插入到 DocumentStore 中。
from llama_index.core import SimpleDirectoryReader
# load documents
documents = SimpleDirectoryReader("./data/paul_graham").load_data()
from llama_index.core.node_parser import SentenceSplitter
# initialize node parser
splitter = SentenceSplitter(chunk_size=512)
nodes = splitter.get_nodes_from_documents(documents)
BM25 检索器 + 磁盘持久化¶
一种选择是直接从节点创建 BM25Retriever
,并保存到磁盘或从磁盘加载。
from llama_index.retrievers.bm25 import BM25Retriever
import Stemmer
# We can pass in the index, docstore, or list of nodes to create the retriever
bm25_retriever = BM25Retriever.from_defaults(
nodes=nodes,
similarity_top_k=2,
# Optional: We can pass in the stemmer and set the language for stopwords
# This is important for removing stopwords and stemming the query + text
# The default is english for both
stemmer=Stemmer.Stemmer("english"),
language="english",
)
BM25S Count Tokens: 0%| | 0/61 [00:00<?, ?it/s]
BM25S Compute Scores: 0%| | 0/61 [00:00<?, ?it/s]
bm25_retriever.persist("./bm25_retriever")
loaded_bm25_retriever = BM25Retriever.from_persist_dir("./bm25_retriever")
Finding newlines for mmindex: 0%| | 0.00/292k [00:00<?, ?B/s]
BM25 检索器 + 文档存储持久化¶
在这里,我们将介绍如何将 BM25Retriever
与文档存储一起使用来保存节点。这样做的好处是文档存储可以是远程的(mongodb、redis 等)。
# initialize a docstore to store nodes
# also available are mongodb, redis, postgres, etc for docstores
from llama_index.core.storage.docstore import SimpleDocumentStore
docstore = SimpleDocumentStore()
docstore.add_documents(nodes)
from llama_index.retrievers.bm25 import BM25Retriever
import Stemmer
# We can pass in the index, docstore, or list of nodes to create the retriever
bm25_retriever = BM25Retriever.from_defaults(
docstore=docstore,
similarity_top_k=2,
# Optional: We can pass in the stemmer and set the language for stopwords
# This is important for removing stopwords and stemming the query + text
# The default is english for both
stemmer=Stemmer.Stemmer("english"),
language="english",
)
BM25S Count Tokens: 0%| | 0/61 [00:00<?, ?it/s]
BM25S Compute Scores: 0%| | 0/61 [00:00<?, ?it/s]
from llama_index.core.response.notebook_utils import display_source_node
# will retrieve context from specific companies
retrieved_nodes = bm25_retriever.retrieve(
"What happened at Viaweb and Interleaf?"
)
for node in retrieved_nodes:
display_source_node(node, source_length=5000)
节点 ID: a1236ec0-7d41-4b52-950f-27199a1e28de
相似度 1.8383275270462036
文本: 我看到佛罗伦萨的街景在各种状态下,从空无一人的黑暗冬夜到游客挤满街道的酷热夏日。
[4] 当然,如果你愿意并且他们也愿意,你可以把人画成静物。那种肖像画可以说是静物画的顶峰,尽管长时间的坐姿确实会让被画者面露痛苦。
[5] Interleaf 是众多拥有聪明人才并构建了令人印象深刻的技术,但却被摩尔定律压垮的公司之一。在 20 世纪 90 年代,通用(即英特尔)处理器性能的指数级增长就像推土机一样摧毁了高端、专用硬件和软件公司。
[6] RISD 寻求标志性风格的人并非特别功利。在艺术界,金钱和酷紧密相连。任何昂贵的东西都会被视为酷,任何被视为酷的东西很快也会变得同样昂贵。
[7] 从技术上讲,这套公寓不是租金管制,而是租金稳定,但这种细微之处只有纽约人会知道或关心。重点是它真的很便宜,不到市场价格的一半。
[8] 大多数软件一完成就可以发布。但如果软件是一个在线商店构建器,并且你负责托管这些商店,如果你还没有用户,那这一点就会非常明显且令人痛苦。所以在我们能够公开发布之前,我们必须私下发布,这意味着招募一批初始用户并确保他们拥有看起来不错的商店。
[9] 我们在 Viaweb 中有一个代码编辑器,供用户定义自己的页面样式。他们不知道,但他们编辑的是底层的 Lisp 表达式。但这并不是一个应用编辑器,因为代码是在商户网站生成时运行的,而不是购物者访问时运行的。
[10] 这是现在司空见惯的经历的第一个例子,接下来发生的事情也是如此,当我阅读评论时,发现里面充满了愤怒的人。我怎么能声称 Lisp 比其他语言更好?它们不都是图灵完备的吗?
节点 ID: 34259d5b-f0ea-436d-8f44-31d790cfbfb7
相似度 1.5173875093460083
文本: 这个名字没用多久就被“软件即服务”取代了,但它流行的时间够长,我便以此命名了这家新公司:它将被称为 Aspra。
我开始开发应用程序构建器,Dan 负责网络基础设施,那两个本科生负责前两项服务(图像和电话呼叫)。但到了夏天中途,我意识到我真的不想经营一家公司——尤其不是一家大公司,而当时看起来这家公司必须做大。我创办 Viaweb 只是因为我需要钱。现在我不再需要钱了,为什么还要做这件事?如果这个愿景必须通过公司来实现,那就算了。我会构建一个可以作为开源项目完成的子集。
令我惊讶的是,我花在这些事情上的时间最终并没有浪费。在我们创办 Y Combinator 之后,我经常遇到一些正在研究这种新架构部分内容的初创公司,花这么多时间思考甚至尝试编写其中的一部分内容,对我来说非常有益。
我打算作为开源项目构建的子集是新的 Lisp,现在我甚至不必隐藏它的括号。很多 Lisp 黑客梦想构建一种新的 Lisp,部分原因是这种语言的一个独特之处在于它有方言,部分原因,我认为,是因为我们在脑海中有一个柏拉图式的 Lisp 形式,所有现有的方言都未能达到。我当然有。所以夏天结束时,Dan 和我转而在我在剑桥买的一所房子里研究这种新的 Lisp 方言,我称之为 Arc。
次年春天,奇迹发生了。我受邀在一个 Lisp 会议上发表演讲,于是我讲了我们在 Viaweb 如何使用 Lisp。之后我把这次演讲的 postscript 文件放在网上,上传到 paulgraham.com,这个网站是我几年前用 Viaweb 创建的,但一直没用过。一天之内,它获得了 30,000 次页面浏览。到底发生了什么?引用网址显示有人把它发到了 Slashdot 上。
retrieved_nodes = bm25_retriever.retrieve("What did the author do after RISD?")
for node in retrieved_nodes:
display_source_node(node, source_length=5000)
节点 ID: 3aeed631-54d7-4fc9-83cf-804ba393b281
相似度 1.9751536846160889
文本: 而且我非常不负责任。当时编程工作意味着每天在固定的工作时间上班。这对我来说很不自然,而且在这一点上,世界其他地方也开始认同我的想法,但在当时这造成了很大的摩擦。快到年底时,我大部分时间都在偷偷写《On Lisp》,那时我已经拿到了出版合同。
好的一面是我挣了很多钱,尤其对于艺术学生来说。在佛罗伦萨,付完我的那份房租后,我每天其他开销的预算是 7 美元。现在我每小时挣的钱是那时的 4 倍多,即使只是坐在会议里。通过节俭生活,我不仅攒够了回 RISD 的钱,还还清了大学贷款。
我在 Interleaf 学到了一些有用的东西,不过它们主要都是关于不该做什么。我了解到,科技公司最好由产品人员而非销售人员管理(尽管销售是一项真正的技能,而且擅长销售的人确实非常擅长),当代码被太多人编辑时会导致 bug,如果廉价的办公空间令人压抑,那它就不划算,计划好的会议不如走廊里的交谈,大型的、官僚的客户是危险的资金来源,并且传统的办公时间和最佳编程时间之间,或者传统办公室和最佳编程地点之间,并没有太多重叠。
但我学到的最重要的事情,也是我在 Viaweb 和 Y Combinator 都运用到的,是低端吞噬高端:成为“入门级”选项是件好事,尽管这可能不那么有声望,因为如果你不这样做,其他人会,他们会把你挤压到天花板上。这反过来意味着声望是一个危险信号。
第二年秋天,当我离开回 RISD 时,我安排为那个为客户做项目的团队做自由职业,这就是我接下来几年生存的方式。
节点 ID: ea6aabac-ef00-418b-a79b-cc714daf6fb9
相似度 1.91998291015625
文本: 至少不是绘画系。我隔壁邻居所属的纺织系看起来相当严谨。毫无疑问,插画和建筑系也是。但绘画系是后严格的。绘画学生应该表达自己,对那些更世故的人来说,这意味着试图创造某种独特的标志性风格。
标志性风格在视觉上相当于演艺界所称的“噱头”:一种立即识别出作品属于你而非他人的东西。例如,当你看到一幅看起来像某种卡通的画作时,你就知道它是 Roy Lichtenstein 的作品。因此,如果你看到对冲基金经理的公寓里挂着一幅这种类型的大画,你就知道他为此支付了数百万美元。艺术家并非总是出于这个原因拥有标志性风格,但这通常是买家为此类作品支付高价的原因。[6]
也有很多认真的学生:那些在高中时“会画画”的孩子,现在来到了本应是全国最好的艺术学校,学习如何画得更好。他们往往对在 RISD 发现的一切感到困惑和沮丧,但他们坚持下去,因为绘画是他们要做的事。我不是高中时会画画的孩子之一,但在 RISD,我绝对比那些追求标志性风格的人更接近他们那一类。
我在 RISD 上色彩课学到了很多,但除此之外,我基本上是在自学绘画,而且我可以免费做到。所以在 1993 年我辍学了。我在普罗维登斯待了一段时间,然后我的大学朋友 Nancy Parmet 帮了我一个大忙。她母亲在纽约拥有的一栋大楼里有一套租金管制的公寓即将空置。我想要吗?它比我当时住的地方没贵多少,而且纽约本应是艺术家聚集的地方。所以是的,我想要![7]
《高卢英雄传》漫画开头总是聚焦于罗马高卢的一个小角落,那里竟然不受罗马人控制。
混合检索器:BM25 + Chroma¶
现在我们将结合 BM25 和 Chroma 进行稀疏和密集检索。
结果使用 QueryFusionRetriever
进行组合。
有了检索器,我们可以构建一个完整的 RetrieverQueryEngine
。
from llama_index.core import VectorStoreIndex, StorageContext
from llama_index.core.storage.docstore import SimpleDocumentStore
from llama_index.vector_stores.chroma import ChromaVectorStore
import chromadb
docstore = SimpleDocumentStore()
docstore.add_documents(nodes)
db = chromadb.PersistentClient(path="./chroma_db")
chroma_collection = db.get_or_create_collection("dense_vectors")
vector_store = ChromaVectorStore(chroma_collection=chroma_collection)
storage_context = StorageContext.from_defaults(
docstore=docstore, vector_store=vector_store
)
index = VectorStoreIndex(nodes=nodes, storage_context=storage_context)
import nest_asyncio
nest_asyncio.apply()
from llama_index.core.retrievers import QueryFusionRetriever
retriever = QueryFusionRetriever(
[
index.as_retriever(similarity_top_k=2),
BM25Retriever.from_defaults(
docstore=index.docstore, similarity_top_k=2
),
],
num_queries=1,
use_async=True,
)
BM25S Count Tokens: 0%| | 0/61 [00:00<?, ?it/s]
BM25S Compute Scores: 0%| | 0/61 [00:00<?, ?it/s]
nodes = retriever.retrieve("What happened at Viaweb and Interleaf?")
for node in nodes:
display_source_node(node, source_length=5000)
节点 ID: d4b9b0fe-066a-4a43-b0b9-3d981ce09b63
相似度 1.4261349439620972
文本: 这样我们就永远不必编写任何在用户计算机上运行的东西了。我们可以在同一台服务器上生成网站,并从中提供服务。用户只需要一个浏览器。
这种被称为网络应用的软件现在很常见,但在当时还不清楚它是否可行。为了弄清楚,我们决定尝试做一个可以通过浏览器控制的商店构建器版本。几天后,8 月 12 日,我们就有了一个可用的版本。用户界面很糟糕,但这证明了你可以在浏览器中构建整个商店,无需任何客户端软件或在服务器上输入任何命令行。
现在我们感觉自己真的有所发现了。我预想到了全新一代的软件将以这种方式工作。你不需要版本、端口,或者任何那些麻烦的东西。在 Interleaf,有一个叫做发布工程的整个团队,他们的人数似乎至少和实际编写软件的团队一样多。现在你只需直接在服务器上更新软件即可。
我们成立了一家新公司,取名为 Viaweb,因为我们的软件通过网络工作,我们从 Idelle 的丈夫 Julian 那里获得了 10,000 美元的种子资金。作为回报,以及他进行的初步法律工作和提供的商业建议,我们给了他公司 10% 的股份。十年后,这笔交易成为了 Y Combinator 的模式。我们知道创始人需要这样的东西,因为我们自己曾经需要过。
在这个阶段,我的净资产是负的,因为我在银行里的一千美元左右,还不够抵消我欠政府的税款。(我有没有认真地留出为 Interleaf 提供咨询服务赚到的钱的相应比例?没有。)所以虽然 Robert 有他的研究生津贴,我需要那笔种子资金来生活。
我们最初希望在九月发布,但随着我们的开发,对软件的期望越来越高。
节点 ID: 4504224b-1d57-426f-bfb7-d1c1dd6fdae8
相似度 1.3261895179748535
文本: 但从长远来看,增长率会解决绝对数量的问题。如果当时我们是 Y Combinator 里我正在指导的一家初创公司,我会说:别那么焦虑了,因为你们做得很好。你们每年增长 7 倍。只要别再招太多人,很快就会盈利,然后你们就能掌控自己的命运了。
可惜我招了更多人,一部分是因为我们的投资者希望我这样做,一部分是因为互联网泡沫时期初创公司就是这样做的。一家只有少数员工的公司会显得业余。所以我们直到 1998 年夏天雅虎收购我们时才达到收支平衡。这反过来意味着整个公司生命周期我们都受投资者的摆布。而且由于我们和我们的投资者都是初创公司的菜鸟,结果即使按照初创公司的标准来看也是一团糟。
雅虎收购我们时,真是松了一大口气。原则上我们的 Viaweb 股票是有价值的。它是一家盈利且快速增长的公司的股份。但对我来说,它感觉并不值钱;我不知道如何评估一家企业,但我非常清楚我们每隔几个月就会遇到的九死一生般的经历。自从公司成立以来,我的研究生生活方式也没有发生显著变化。所以当雅虎收购我们时,感觉就像从一贫如洗到一夜暴富。既然我们要去加州,我就买了一辆车,一辆黄色的 1998 年款大众 GTI。我记得当时我在想,光是它的真皮座椅就已经是迄今为止我拥有的最奢侈的东西了。
接下来的一年,从 1998 年夏天到 1999 年夏天,一定是我一生中最没有成效的一年。当时我没有意识到,但我已经被经营 Viaweb 的辛劳和压力弄得精疲力尽。到加州后的一段时间里,我试图继续我通常的生活方式,编程到凌晨三点,但疲劳加上雅虎过早老化的文化和圣克拉拉令人沮丧的格子间办公环境,渐渐地把我拖垮了。几个月后,那种感觉令人不安地像是又回到了 Interleaf 工作。
from llama_index.core.query_engine import RetrieverQueryEngine
query_engine = RetrieverQueryEngine(retriever)
response = query_engine.query("What did the author do after RISD?")
print(response)
The author arranged to do freelance work for the group that did projects for customers after leaving RISD.
storage_context.docstore.persist("./docstore.json")
# or, we could ignore the docstore and just persist the bm25 retriever as shown below
# bm25_retriever.persist("./bm25_retriever")
现在,我们可以重新加载并重新创建索引。
db = chromadb.PersistentClient(path="./chroma_db")
chroma_collection = db.get_or_create_collection("dense_vectors")
vector_store = ChromaVectorStore(chroma_collection=chroma_collection)
docstore = SimpleDocumentStore.from_persist_path("./docstore.json")
storage_context = StorageContext.from_defaults(
docstore=docstore, vector_store=vector_store
)
index = VectorStoreIndex(nodes=[], storage_context=storage_context)