Elasticsearch 完全支持存储和检索向量,这使其成为处理嵌入向量的理想数据库。
字段类型
在本教程的全文搜索章节中,您学习了如何创建包含多个字段的索引。当时提到,Elasticsearch 大部分情况下可以根据数据本身自动确定每个字段的最佳类型。即使 Elasticsearch 8.11 能够自动映射某些向量类型,在本节中,您也将显式定义此类型,以便了解更多关于 Elasticsearch 中类型映射的信息。
检索类型映射
与索引中每个字段关联的类型是在称为映射的过程中确定的,它可以是动态的或显式的。在本教程的全文搜索部分中创建的映射都是由 Elasticsearch动态生成的。
Elasticsearch 客户端提供了一个get_mapping
方法,该方法返回对给定索引有效的类型映射。如果您想自己探索这些映射,请启动一个 Python shell 并输入以下代码。
from app import es
es.es.indices.get_mapping(index='my_documents')
get_mapping()
方法的响应是一个字典,其中包含有关索引中每个字段的信息。为了方便起见,以下是本教程全文搜索部分创建的my_documents
索引的此信息的格式良好的结构。
{
"my_documents": {
"mappings": {
"properties": {
"category": {
"type": "text",
"fields": {
"keyword": {
"type": "keyword",
"ignore_above": 256
}
}
},
"content": {
"type": "text",
"fields": {
"keyword": {
"type": "keyword",
"ignore_above": 256
}
}
},
"created_on": {
"type": "date"
},
"name": {
"type": "text",
"fields": {
"keyword": {
"type": "keyword",
"ignore_above": 256
}
}
},
"rolePermissions": {
"type": "text",
"fields": {
"keyword": {
"type": "keyword",
"ignore_above": 256
}
}
},
"summary": {
"type": "text",
"fields": {
"keyword": {
"type": "keyword",
"ignore_above": 256
}
}
},
"updated_at": {
"type": "date"
},
"url": {
"type": "text",
"fields": {
"keyword": {
"type": "keyword",
"ignore_above": 256
}
}
}
}
}
}
}
由此可见,created_on
和updated_at
字段被自动设置为date
类型,而其他所有字段都被设置为text
类型。在确定类型时,Elasticsearch 首先检查数据的类型,这有助于它将数字、布尔和对象类型分配给字段。当字段数据为字符串时,它还会尝试查看数据是否与日期模式匹配。如果需要,也可以为数字启用基于模式的字符串检测。
文本字段具有一个包含keyword
条目的fields
定义。这称为子字段,是在适当情况下可用的替代或辅助类型。在 Elasticsearch 中,动态类型的text
字段会获得一个keyword
子字段。您已经使用过category.keyword
子字段来对给定类别执行精确搜索。为了避免添加子字段,可以提供text
或keyword
的显式映射,然后这将成为主要且唯一的类型。
向索引添加向量字段
让我们向索引添加一个新字段,用于存储每个文档的嵌入向量。
显式映射的结构与 Elasticsearch 客户端的get_mapping()
方法返回的响应的mappings
键匹配。只需要提供需要显式类型的字段,因为映射中未包含的任何字段将像以前一样继续动态类型。
下面您可以看到Search
类的create_index()
方法的新版本,添加了一个名为embedding
的显式类型字段。请将此方法替换为**search.py**中的方法。
class Search:
# ...
def create_index(self):
self.es.indices.delete(index='my_documents', ignore_unavailable=True)
self.es.indices.create(index='my_documents', mappings={
'properties': {
'embedding': {
'type': 'dense_vector',
}
}
})
如您所见,embedding
字段的类型为dense_vector,这是存储嵌入向量时的适当类型。稍后您将了解另一种向量类型sparse_vector
,它在其他类型的语义搜索应用程序中很有用。
dense_vector
类型接受一些参数,所有这些参数都是可选的。
dims
:将要存储的向量的尺寸。从 8.11 版本开始,在插入第一个文档时会自动分配维度。index
:必须设置为True
才能指示应为搜索索引向量。这是默认值。similarity
:比较向量时使用的距离函数。两个最常见的函数是dot_product
和cosine
。点积效率更高,但它需要对向量进行归一化处理。默认值为cosine
。
向文档添加嵌入向量
在上一节中,您学习了如何使用 SentenceTransformers 框架和all-MiniLM-L6-v2
模型生成嵌入向量。现在是将模型集成到应用程序中的时候了。
首先,可以在Search
类的构造函数中实例化模型。
# ...
from sentence_transformers import SentenceTransformer
# ...
class Search:
def __init__(self):
self.model = SentenceTransformer('all-MiniLM-L6-v2')
self.es = Elasticsearch(cloud_id=os.environ['ELASTIC_CLOUD_ID'],
api_key=os.environ['ELASTIC_API_KEY'])
client_info = self.es.info()
print('Connected to Elasticsearch!')
pprint(client_info.body)
# ...
正如您在本教程的全文搜索部分所回忆的那样,Search
类具有insert_document()
和insert_documents()
方法,分别用于将单个和多个文档插入索引中。这两个方法现在需要生成与每个文档对应的嵌入向量。
下一个代码块显示了这两个方法的新版本,以及一个返回嵌入向量的新get_embedding()
辅助方法。
class Search:
# ...
def get_embedding(self, text):
return self.model.encode(text)
def insert_document(self, document):
return self.es.index(index='my_documents', document={
**document,
'embedding': self.get_embedding(document['summary']),
})
def insert_documents(self, documents):
operations = []
for document in documents:
operations.append({'index': {'_index': 'my_documents'}})
operations.append({
**document,
'embedding': self.get_embedding(document['summary']),
})
return self.es.bulk(operations=operations)
修改后的方法将新的embedding
字段添加到要插入的文档中。嵌入向量是从每个文档的summary
字段生成的。通常,嵌入向量是从句子或短段落生成的,因此在这种情况下,摘要是理想的字段。其他选择可能是包含文档标题的name
字段,或者可能是文档body
中的前几句话。
有了这些更改,就可以重建索引,以便它为每个文档存储一个嵌入向量。要重建索引,请使用以下命令。
flask reindex
如果您需要提醒,flask reindex
命令在**app.py**中的reindex()
函数中实现。它调用Search
类的reindex()
方法,该方法依次调用create_index()
,然后将**data.json**文件中的所有数据传递给insert_documents()
。