【AI】利用Azure AI的元数据过滤器提升 RAG 性能并增强向量搜索案例
在检索增强生成 (RAG) 设置中,用户指定的过滤器(无论是隐含的还是明确的)通常在向量搜索中被忽视,因为向量搜索主要关注语义相似性。 在某些场景中,确保特定的查询仅使用预定义的(子)文档集来回答至关重要。通过使用“元数据”或标签,您可以强制执行每种类型用户查询应使用的文档类型。这甚至可以变成一种安全覆盖策略,其中每个用户的查询都带有他们的凭据/权限级别标签,以便使用与其权限级别相对应的文档来回答他们的查询。 当 RAG 数据由许多单独的数据对象(例如,文件)组成时,每个数据对象都可以用一组预定义的元数据标记。然后,这些标签可以在向量或混合搜索中用作过滤器。元数据可以与向量嵌入一起集成到搜索索引中,然后用作过滤器。 在本博客中,我们将演示一个示例实现……
推荐超级课程:
@TOC
实施步骤
为了演示目的,在本博客文章中,我们将使用电影维基百科文章作为我们的文档。然后,我们将使用诸如 genre
、releaseYear
和 director
等元数据对这些电影文件进行标记,并稍后使用这些元数据对 RAG 生成进行过滤。
请注意,大型语言模型 (LLM) 也可以在文档上传到搜索索引之前用于“分类”文档,以便在大规模部署。当用户输入提示时,我们可以使用额外的 LLM 调用来对用户提示进行分类(匹配一组元数据),然后稍后使用它来过滤结果。博客文章演示了一个更简单的用例,其中 RAG 文档(保存为 pdf 文件的维基百科页面)带有预先标记的电影元数据…
1. 分类文档并标记元数据
movies = [
{"id": "1", "title": "肖申克的救赎", "genre": "剧情", "releaseYear": 1994, "director": "弗兰克·德拉邦特"},
{"id": "2", "title": "教父", "genre": "犯罪", "releaseYear": 1972, "director": "弗朗西斯·福特·科波拉"},
{"id": "3", "title": "黑暗骑士", "genre": "动作", "releaseYear": 2008, "director": "克里斯托弗·诺兰"},
{"id": "4", "title": "辛德勒的名单", "genre": "传记", "releaseYear": 1993, "director": "史蒂文·斯皮尔伯格"},
{"id": "5", "title": "低俗小说", "genre": "犯罪", "releaseYear": 1994, "director": "昆汀·塔伦蒂诺"},
{"id": "6", "title": "指环王:王者归来", "genre": "奇幻", "releaseYear": 2003, "director": "彼得·杰克逊"},
{"id": "7", "title": "黄金三镖客", "genre": "西部", "releaseYear": 1966, "director": "塞尔吉奥·莱昂内"},
{"id": "8", "title": "搏击俱乐部", "genre": "剧情", "releaseYear": 1999, "director": "大卫·芬奇"},
{"id": "9", "title": "阿甘正传", "genre": "剧情", "releaseYear": 1994, "director": "罗伯特·泽米吉斯"},
{"id": "10", "title": "盗梦空间", "genre": "科幻", "releaseYear": 2010, "director": "克里斯托弗·诺兰"}
]
2. 创建 Azure AI 搜索索引…
我们需要创建一个 Azure AI 搜索索引,该索引将元数据字段作为“可搜索”和“可过滤”字段。下面是我们将使用的模式定义。 首先在 JSON 中定义模式….
{
"name": "movies-index",
"fields": [
{ "name": "id", "type": "Edm.String", "key": true, "filterable": false, "sortable": false },
{ "name": "title", "type": "Edm.String", "filterable": true, "searchable": true },
{ "name": "genre", "type": "Edm.String", "filterable": true, "searchable": true },
{ "name": "releaseYear", "type": "Edm.Int32", "filterable": true, "sortable": true },
{ "name": "director", "type": "Edm.String", "filterable": true, "searchable": true },
{ "name": "content", "type": "Edm.String", "filterable": false, "searchable": true },
{
"name": "contentVector",
"type": "Collection(Edm.Single)",
"searchable": true,
"retrievable": true,
"dimensions": 1536,
"vectorSearchProfile": "my-default-vector-profile"
}
],
"vectorSearch": {
"algorithms": [
{
"name": "my-hnsw-config-1",
"kind": "hnsw",
"hnswParameters": {
"m": 4,
"efConstruction": 400,
"efSearch": 500,
"metric": "cosine"
}
}
],
"profiles": [
{
"name": "my-default-vector-profile",
"algorithm": "my-hnsw-config-1"
}
]
}
}
然后运行以下脚本来使用 REST API 调用创建索引 Azure AI 搜索服务…
RESOURCE_GROUP="[your-resource-group]"
SEARCH_SERVICE_NAME="[search-index-name]"
API_VERSION="2023-11-01"
API_KEY="[your-AI-Search-API-key"
SCHEMA_FILE="movies-index-schema.json"
curl -X POST "https://${SEARCH_SERVICE_NAME}.search.windows.net/indexes?api-version=${API_VERSION}" \
-H "Content-Type: application/json" \
-H "api-key: ${API_KEY}" \
-d @${SCHEMA_FILE}
一旦创建了 Azure AI 搜索索引,请在门户中确认元数据字段已标记为可过滤和可搜索… 
3. 嵌入并将文档块及其元数据上传到 Azure AI 搜索索引
我们将使用的文档是保存为 pdf 文件的电影的维基百科页面。为了将文档集成到 RAG 模式中的 LLM,我们首先将“预处理”文档。下面代码首先使用 extract_text_from_pdf
函数打开指定的 PDF 文件,使用 PdfReader
类读取其内容,并从每页中提取文本,将所有文本合并成一个字符串。normalize_text
函数接受一个文本字符串并删除任何不必要的空白,确保文本被规范化成一个连续的字符串,其中只包含空格。chunk_text
函数然后接受这个规范化文本并将其分成更小的块,每个块的大小不超过指定的大小(默认为 6000 个字符)。这是通过将文本标记成句子并将它们分组到块中,同时确保每个块不超过指定的大小来完成的,这使得文本更容易管理,并以更小的片段进行处理。
# 从 PDF 中提取文本的函数
def extract_text_from_pdf(pdf_path):
text = ""
with open(pdf_path, "rb") as file:
reader = PdfReader(file)
for page in reader.pages:
text += page.extract_text() + "\n"
return text
# 规范化文本的函数
def normalize_text(text):
return ' '.join(text.split())
# 将文本分成更小的块的函数
def chunk_text(text, chunk_size=6000):
sentences = sent_tokenize(text)
chunks = []
current_chunk = []
current_length = 0
for sentence in sentences:
if current_length + len(sentence) > chunk_size:
chunks.append(' '.join(current_chunk))
current_chunk = [sentence]
current_length = len(sentence)
else:
current_chunk.append(sentence)
current_length += len(sentence)
if current_chunk:
chunks.append(' '.join(current_chunk))
return chunks
然后嵌入每个块并将嵌入及其文档元数据上传到之前创建的 Azure AI 搜索索引中。
4. 无过滤向量搜索
首先,让我们用一个相对通用的提示进行向量搜索,该提示将与多个文档块匹配…请注意显式过滤语句“这部电影于 2010 年上映”。请注意,向量搜索无法成功解释所述过滤器,并且搜索结果中也会返回错误的结果(早在 2010 年之前上映的电影)。
# 为情节提示生成嵌入
plot_prompt = "一个人面临无法克服的困难,经历了一段转变性的旅程,揭示了隐藏的力量,并形成了意想不到的同盟。通过坚韧和机智,他们在充满腐败、背叛和正义斗争的世界中导航,最终发现了他们的真正目的。这部电影于 2010 年上映"
prompt_embedding_vector = generate_embeddings(plot_prompt)
payload = {
"count": True,
"select": "title, content, genre",
"vectorQueries": [
{
"kind": "vector",
"vector": prompt_embedding_vector,
"exhaustive": True,
"fields": "contentVector",
"k": 5
}
],
# "filter": "genre eq 'Drama' and releaseYear ge 1990 and director eq 'Christopher Nolan'"
}
response = requests.post(url, headers=headers, data=json.dumps(payload))
if response.status_code == 200:
results = response.json()
print("Results with pre-filter:")
for result in results['value']:
print(result)
else:
print(f"Error: {response.status_code}")
print(response.json())
Results without pre-filter:
{'@search.score': 0.83729386, 'title': '肖申克的救赎', 'genre': '剧情', 'content': '...'}
{'@search.score': 0.83415884, 'title': '肖申克的救赎', 'genre': '剧情', 'content': '...'}
{'@search.score': 0.8314112, 'title': '盗梦空间', 'genre': '科幻', 'content': '...'}
{'@search.score': 0.8308051, 'title': '指环王:王者归来', 'genre': '奇幻', 'content': '...'}
5. (预)过滤向量搜索
接下来,向向量搜索添加一个过滤器…过滤器定义为任何文档块,其 releaseYear
元数据值(int32)大于 2010。在这种情况下,只有正确的搜索结果,即来自电影“盗梦空间”的文档块被返回。
payload_with_release_year_filter = {
"count": True,
"select": "title, content, genre, releaseYear, director",
"filter": "releaseYear eq 2010",
"vectorFilterMode": "preFilter",
"vectorQueries": [
{
"kind": "vector",
"vector": prompt_embedding_vector,
"exhaustive": True,
"fields": "contentVector",
"k": 5
}
]
}
Results with pre-filter:
{'@search.score': 0.8314112, 'title': '盗梦空间', 'genre': '科幻', 'releaseYear': 2010, 'director': '克里斯托弗·诺兰', 'content': '...'}
{'@search.score': 0.83097535, 'title': '盗梦空间', 'genre': '科幻', 'releaseYear': 2010, 'director': '克里斯托弗·诺兰', 'content':'...'}
{'@search.score': 0.83029956, 'title': '盗梦空间', 'genre': '科幻', 'releaseYear': 2010, 'director': '克里斯托弗·诺兰', 'content': '...'}
{'@search.score': 0.82646775, 'title': '盗梦空间', 'genre': '科幻', 'releaseYear': 2010, 'director': '克里斯托弗·诺兰', 'content': '...'}
{'@search.score': 0.8255407, 'title': '盗梦空间', 'genre': '科幻', 'releaseYear': 2010, 'director': '克里斯托弗·诺兰', 'content': '...'}
结论
本博客展示了将文档块嵌入并上传到 Azure 搜索索引中,其中文档元数据作为可搜索和可过滤字段的情况。 这个概念可以扩展,以便可以使用额外的 LLM 查询步骤来“分类”用户提示并推断将应用于预/后过滤向量搜索匹配块的元数据。同样,文档本身可以使用 LLM 调用标记元数据,而不是像本例中演示的那样依赖于静态人工注释。