如何用NucliaDB建立自己的人工智能搜索引擎?

332 阅读9分钟

首先,什么是NucliaDB?

让我用几句话来回答: NucliaDB是一个开源的矢量数据库,用于存储、索引和检索AI搜索的矢量。

那么......开始玩一下NLP模型,利用NucliaDB的矢量搜索能力来实现AI搜索功能怎么样?

你可能知道StackExchange,这是一个问答平台,你可以提出问题并从专家那里得到答案。如果你是一名开发者,你可能知道StackOverflow,这是一个专门为开发者服务的StackExchange网站。

通常情况下,开发人员能够用正确的关键词查询StackOverflow,以获得他们正在寻找的答案,因为问题中使用的技术词汇非常精确。通常情况下,如果你想知道如何在display: flex 容器中垂直对齐文本,没有很多方法来表述你的问题。

而这正是全文搜索的优势所在。你只需输入*"垂直对齐flex"*,就能得到你要找的答案。

但是当涉及到更抽象的问题时,就不那么容易了。例如,如果你去Philosophy StackExchange搜索*"what is the meaning of life",你会得到很多结果,但它们仅限于在标题或正文中提到 "生命的意义 "的帖子,而你可能会对一个名为"Is our existence pointless?"* 的帖子感兴趣。

它在这里强调了两件重要的事情:

  • 全文搜索有时是不够好的。
  • 寻找生命的意义比在CSS中垂直对齐文本更难(然而这已经非常难了)。

如果你希望能够搜索*"生命的意义",并得到"我们的存在是无意义的吗?"*这篇文章的结果,你需要AI搜索。你需要搜索引擎理解你查询的含义,而不仅仅是寻找关键词。

所以,让我们用NucliaDB建立一个人工智能搜索引擎吧!

原理

NucliaDB是Nuclia平台的核心组件,它是开源的,可以独立使用。

NucliaDB是一个矢量数据库。这意味着它存储和索引矢量,它接受矢量作为查询参数来检索匹配的结果。
它还提供全文搜索,这意味着它也可以索引和搜索文本中的关键词。

为了实现人工智能搜索引擎,你需要把每个文本的含义联系起来。意义将由一个模型(自然语言处理模型)提供的向量来表示。
一个很好的方法是把它想象成一张地图,一张巨大的地图,所有的句子都根据它们的含义来定位。当两个句子的意思接近时,它们就会在地图上靠近。当两个句子的意思很远时,它们在地图上就会很远。矢量只是句子在地图上的坐标。

当在NucliaDB中索引文本内容时,你需要将每个句子编码为向量并存储在NucliaDB中。
当你想在NucliaDB中搜索一个问题时,你首先使用相同的模型对查询进行编码,然后在地图上搜索最近的句子,你就会得到你要的答案。

在这个例子中,你将使用在HugginFace上可以找到的paraphrase-MiniLM-L6-v2模型。

执行

第一步:获取数据

StackExchange很好,因为它提供了一个很好的查询工具,能够过滤和导出你想要的数据。

例如,如果你想从Philosophy StackExchange网站获得所有的帖子,你可以运行以下查询:

SELECT q.Title, q.Body, (SELECT a.Body FROM Posts a WHERE a.ParentId=q.id) AS Answers
FROM Posts q
WHERE PostTypeId=1

你会得到一个包含所有哲学问题和答案的巨大CSV。

在我自己做的时候,有16000个问题和40000个答案。这是一个很大的数据!但是NucliaDB可以轻松地管理这些数据。让我们来看看如何。

第2步:创建一个知识箱

首先,用Docker运行一个NucliaDB本地实例:

docker run -it \
       -e LOG=INFO \
       -p 8080:8080 \
       -p 8060:8060 \
       -p 8040:8040 \
       -v nucliadb-standalone:/data \
       nuclia/nucliadb:latest

现在你已经运行了NucliaDB,你需要创建一个知识盒。知识库是数据的一个容器。它是一个可以为你的数据建立索引和搜索的地方。

它可以像这样从REST API中创建:

curl 'http://localhost:8080/api/v1/kbs' \
    -X POST \
    -H "X-NUCLIADB-ROLES: MANAGER" \
    -H "Content-Type: application/json" \
    --data-raw '{
  "slug": "philosophy",
  "title": "Philosophy StackExchange"
}'

这个调用将返回给你一个知识箱的ID,需要进一步调用API。

你可以在Python中用nucliadb_client ,实现同样的事情:

from nucliadb_client.client import NucliaDBClient

client = NucliaDBClient(host="localhost", grpc=8060, http=8080, train=8031)
kb = client.create_kb(slug="philosophy", title="Philosophy StackExchange Questions&Answers")

第三步:提取和编码句子

当索引一个问题及其答案时,你需要向NucliaDB传递两件事:

  • 文本内容本身(问题、其标题和答案)
  • 每个句子的向量表示

这意味着你必须从文本中提取句子。

有一些聪明的方法可以做到这一点,但在这个演示中,你将采用一种超级简单的方法:从你的内容中获取第一级HTML标签。这主要是<p> ,有时是<h1><h2> ,或<ul> 。而你将只是假设这些是 "句子"。

这可以用BeautifulSoup来完成:

tree = BeautifulSoup(text, features="html.parser")
sentences = [child.get_text(" ", strip=True) for child in tree.contents]

然后,你需要将这些句子编码为向量。你可以用HuggingFace库来做。首先,你建立一个模型:

from sentence_transformers import SentenceTransformer
model = SentenceTransformer("paraphrase-MiniLM-L6-v2")

然后,你将用它来对每个句子进行编码:

model.encode([sentence])

(你将在下一步中使用它)

第4步:在NucliaDB中对数据进行索引

现在你已经提取了句子并将其编码为向量,你可以在NucliaDB中对所有的内容进行索引。

首先,对于每个问题,你都要创建一个资源:

payload = CreateResourcePayload()

payload.title = title
payload.icon = "text/html"
payload.metadata = InputMetadata()
payload.metadata.language = "en"

field = TextField(body=text)
field.format = TextFormat.HTML
payload.texts["body"] = field

resource = kb.create_resource(payload)

然后你为全文搜索的文本建立索引:

pure_text = " ".join(sentences)
resource.add_text("body", FieldType.TEXT, pure_text)

现在,你为向量建立索引:

vectors = []
index = 0
for sentence in sentences:
    vector = Vector(
        start=index,
        end=index + len(sentence),
        start_paragraph=0,
        end_paragraph=len(pure_text),
    )
    index += len(sentence) + 1
    embeddings = model.encode([sentence])
    vector.vector.extend(embeddings[0])
    vectors.append(vector)

resource.add_vectors(
    "body",
    FieldType.TEXT,
    vectors,
)
resource.sync_commit()

第5步:搜索

一旦你的16,000行被编入索引(是的,这可能需要时间,取决于你的笔记本电脑:)),你就可以开始玩你的知识盒子了

NucliaDB有一个默认的网页,你可以在这里搜索你的本地知识盒。你可以访问它:http://localhost:8080/widget/

好的,它可以工作,如果你输入一个查询,你会得到结果。但不幸的是,它只能检索到全文的结果。

如果你试图搜索 "生命的意义是什么?",你会得到全文的结果:

curl http://localhost:8080/api/v1/kb/f615c8ff-6ad3-42e0-9b24-ba6ae3b73cf3/search\?query\=what+is+the+meaning+of+life \
  -H "X-NUCLIADB-ROLES: READER" | jq ".fulltext.results"

你还会得到段落结果(因为它们是由全文索引产生的):

curl http://localhost:8080/api/v1/kb/f615c8ff-6ad3-42e0-9b24-ba6ae3b73cf3/search\?query\=what+is+the+meaning+of+life \
  -H "X-NUCLIADB-ROLES: READER" | jq ".paragraphs.results"

但没有句子结果(这些结果对应于矢量匹配):

curl http://localhost:8080/api/v1/kb/f615c8ff-6ad3-42e0-9b24-ba6ae3b73cf3/search\?query\=what+is+the+meaning+of+life \
  -H "X-NUCLIADB-ROLES: READER" | jq ".sentences.results"

将返回一个悲伤的[]...

这是为什么呢?

因为 "生命的意义是什么?"不允许匹配任何矢量。要搜索向量,你需要一个向量查询!

所以你首先需要对查询进行编码,然后将相应的向量传递给搜索端点:

curl http://localhost:8080/api/v1/kb/f615c8ff-6ad3-42e0-9b24-ba6ae3b73cf3/search\?query\=what+is+the+meaning+of+life&vector=[0.12242747843265533,-0.1455705165863037,-0.05579487234354019,…] \
  -H "X-NUCLIADB-ROLES: READER" | jq ".sentences.results"

现在你就可以得到语义上接近的结果了!

我做了一些测试,得到的结果如下:

  • 对于*"我们对我们的愿望负责吗?"这个问题,我得到了"我们的必要性是我们有意去做一些至少与我们的情绪和可能与我们的思考相一致的事情"*。
  • 对于*"没有语言我们能思考吗?"这个问题,我得到了"任何人都能在没有语言的情况下体验世界吗? 如果能,程度如何,如果不能,为什么?"*。

很有意义,对吗?可以肯定的是,任何全文搜索都不会检索到这一点

如果你懒得在你的curl 命令中手动输入矢量查询(来吧!这只是384个浮点数!拿出一点勇气来!),你可以用FastAPI设置一个代理,为你做这件事。你会在演示代码中找到实现方法。

为什么不采用简单的方法呢?

NucliaDB是一个非常好的软件,如果你正在寻找一个存储和查询向量的好方法,这绝对是一个好的选择。更不用说,如果你有不同的模型,你可以在你的资源上存储几个向量,只要用特定的向量集名称来标记,然后你就可以根据你想要的向量集进行查询。

但是,如果你只是在寻找一种为你的用户提供人工智能搜索的方法,还有一种更简单的方法:只需使用nuclia.cloud!

Nuclia Cloud是一个在NucliaDB之上实现的SaaS服务,它有非常相同的API,但它为你处理所有痛苦的过程。你只需要上传你的数据(通常是PDF,或MP4,或任何类型的文件),它将提取文本,将其编码为矢量,并在NucliaDB中索引一切。

然后,你可以使用一个简单的文本查询在你的数据中进行搜索,这里同样,Nuclia Cloud会将查询编码为矢量,然后查询NucliaDB以检索出最相关的结果。

此外,NucliaCloud在线仪表板提供了一种方法,可以通过从浏览器上传CSV文件来直接摄取数据。

你可以在这个演示页面上查看结果!