使用 NVIDIA NeMo Retriever、Unstructured 和 Elasticsearch 处理非结构化数据

作者:来自 Elastic Ajay Krishnan Gopalan

了解如何使用 NeMo Retriever、Unstructured Platform 和 Elasticsearch 为 RAG 应用构建可扩展的非结构化文档数据处理流水线。

在本博客中,我们将讨论如何使用 NVIDIA NeMo Retriever 提取模型、Unstructured Platform 和 Elasticsearch 构建一个可扩展的数据处理流水线。该流水线能够将来自数据源的非结构化数据转换为结构化、可搜索的内容,为 RAG 等下游 AI 应用做好准备。 检索增强生成 ( RAG )是一种 AI 技术,它为大型 语言模型 ( LLM )提供外部知识,以生成对用户查询的回答。这使 LLM 能够结合特定上下文生成回答,从而使答案更加准确且更具相关性。

在开始之前,我们先来了解一下支撑这一流水线的关键组件,以及它们各自发挥的作用。

Pipeline 组件

NeMo Retriever extraction 是一组用于将非结构化文档转换为结构化内容和元数据的微服务。它能够大规模处理文档解析、视觉结构识别以及 OCR。RAG NVIDIA AI Blueprint 提供了一个起点,展示了如何在高性能提取流水线中使用 NeMo Retriever 微服务。

Unstructured 是一个 ETL+ 平台,用于编排整个非结构化数据处理流程,包括从多个数据源采集非结构化数据,通过可配置的工作流引擎将原始非结构化文件转换为结构化数据,利用额外的数据转换进行丰富处理,以及最终将结果上传到向量存储、数据库和搜索引擎。它提供可视化 UI、API 和可扩展的后端基础设施,在单一工作流中完成文档解析、数据丰富和嵌入生成。

Elasticsearch 是业界领先的搜索与分析引擎,目前已原生支持向量搜索能力。它既可以作为传统文本数据库,也可以作为 向量数据库 ,支持包括 k-NN 相似度搜索在内的大规模语义搜索。

介绍完这些核心组件之后,我们先来看一下它们在典型工作流中是如何协同工作的,然后再深入具体实现。

使用 NeMo Retriever + Unstructured + Elasticsearch 构建 RAG

这里仅介绍关键内容,完整 Notebook 请参阅对应链接。

本文分为三个部分:

  • 配置源连接器和目标连接器

  • 使用 Unstructured API 配置工作流

  • 基于处理后的数据构建 RAG

Unstructured 工作流采用 DAG(有向无环图)表示,其中的节点称为连接器( connector ),用于控制数据从哪里采集以及处理后的结果上传到哪里。这些节点是任何工作流都必需的。源连接器负责配置从数据源采集原始数据,目标连接器负责将处理后的数据上传到向量存储、搜索引擎或数据库。

在本文中,我们将研究论文存储在 Amazon S3 中,并希望将处理后的数据写入 Elasticsearch,以供下游应用使用。这意味着,在构建数据处理工作流之前,需要先使用 Unstructured API 创建一个 Amazon S3 源连接器,以及一个 Elasticsearch 目标连接器。

步骤 1:配置 S3 源连接器

创建源连接器时,需要为其指定一个唯一名称、连接器类型(例如 S3 或 Google Drive),并提供相应配置。配置通常包括所连接数据源的位置(例如 S3 Bucket URI 或 Google Drive 文件夹)以及认证信息。

ini 复制代码
`

1.  source_connector_response = unstructured_client.sources.create_source(
2.      request=CreateSourceRequest(
3.          create_source_connector=CreateSourceConnector(
4.              ,
5.              type=SourceConnectorType.S3,
6.              config=S3SourceConnectorConfigInput(
7.                  key=os.environ['S3_AWS_KEY'],
8.                  secret=os.environ['S3_AWS_SECRET'],
9.                  remote_url=os.environ["S3_REMOTE_URL"],
10.                  recursive=False #True/False
11.              )
12.          )
13.      )
14.  )

16.  pretty_print_model(source_connector_response.source_connector_information)

`AI写代码![](https://csdnimg.cn/release/blogv2/dist/pc/img/runCode/icon-arrowwhite.png)

步骤 2:配置 Elasticsearch 目标连接器

接下来,配置 Elasticsearch 目标连接器。你所使用的 Elasticsearch 索引必须具有与 Unstructured 生成文档模式( schema )兼容的映射。有关详细要求,请参阅相关文档

ini 复制代码
`

1.  destination_connector_response = unstructured_client.destinations.create_destination(
2.      request=CreateDestinationRequest(
3.          create_destination_connector=CreateDestinationConnector(
4.              ,
5.              type=DestinationConnectorType.ELASTICSEARCH,
6.              config=ElasticsearchConnectorConfigInput(
7.                  hosts=[os.environ['es_host']],
8.                  es_api_key=os.environ['es_api_key'],
9.                  index_
10.              )
11.          )
12.      )
13.  )

`AI写代码![](https://csdnimg.cn/release/blogv2/dist/pc/img/runCode/icon-arrowwhite.png)

步骤 3:使用 Unstructured 创建工作流

完成源连接器和目标连接器的配置后,就可以创建一个新的数据处理工作流。我们将使用以下节点构建工作流 DAG:

  • 使用 NeMo Retriever 对文档进行分区( partitioning )

  • 使用 Unstructured 的 Image Summarizer、Table Summarizer 和 Named Entity Recognition 节点对内容进行丰富( enrichment )

  • 使用 Chunker 和 Embedder 节点,为内容做好相似性搜索的准备

ini 复制代码
`

1.  from unstructured_client.models.shared import (
2.      WorkflowNode,
3.      WorkflowNodeType,
4.      WorkflowType,
5.      Schedule
6.  )

8.  # Partition the content by using NV-Ingest
9.  parition_node = WorkflowNode(
10.              ,
11.              subtype="nvingest",
12.              type="partition",
13.              settings={"nvingest_host":  userdata.get('NV-Ingest-host-address')},
14.          )

17.  # Summarize each detected image.
18.  image_summarizer_node = WorkflowNode(
19.      ,
20.      subtype="openai_image_description",
21.      type=WorkflowNodeType.PROMPTER,
22.      settings={}
23.  )

25.  # Summarize each detected table.
26.  table_summarizer_node = WorkflowNode(
27.      ,
28.      subtype="anthropic_table_description",
29.      type=WorkflowNodeType.PROMPTER,
30.      settings={}
31.  )

33.  # Label each recognized named entity.
34.  named_entity_recognizer_node = WorkflowNode(
35.      ,
36.      subtype="openai_ner",
37.      type=WorkflowNodeType.PROMPTER,
38.      settings={
39.          "prompt_interface_overrides": None
40.      }
41.  )

43.  # Chunk the partitioned content.
44.  chunk_node = WorkflowNode(
45.      ,
46.      subtype="chunk_by_title",
47.      type=WorkflowNodeType.CHUNK,
48.      settings={
49.          "unstructured_api_url": None,
50.          "unstructured_api_key": None,
51.          "multipage_sections": False,
52.          "combine_text_under_n_chars": 0,
53.          "include_orig_elements": True,
54.          "max_characters": 1537,
55.          "overlap": 160,
56.          "overlap_all": False,
57.          "contextual_chunking_strategy": None
58.      }
59.  )

61.  # Generate vector embeddings.
62.  embed_node = WorkflowNode(
63.      ,
64.      subtype="azure_openai",
65.      type=WorkflowNodeType.EMBED,
66.      settings={
67.          "model_name": "text-embedding-3-large"
68.      }
69.  )

72.  response = unstructured_client.workflows.create_workflow(
73.      request={
74.          "create_workflow": {
75.              "name": f"s3-to-es-NV-Ingest-custom-workflow",
76.              "source_id": source_connector_response.source_connector_information.id,
77.              "destination_id": "a72838a4-bb72-4e93-972d-22dc0403ae9e",
78.              "workflow_type": WorkflowType.CUSTOM,
79.              "workflow_nodes": [
80.                  parition_node,
81.                  image_summarizer_node,
82.                  table_summarizer_node,
83.                  named_entity_recognizer_node,
84.                  chunk_node,
85.                  embed_node
86.              ],
87.          }
88.      }
89.  )

91.  workflow_id = response.workflow_information.id
92.  pretty_print_model(response.workflow_information)

94.  job = unstructured_client.workflows.run_workflow(
95.      request={
96.          "workflow_id": workflow_id,
97.      }
98.  )

100.  pretty_print_model(job.job_information)

`AI写代码![](https://csdnimg.cn/release/blogv2/dist/pc/img/runCode/icon-arrowwhite.png)收起代码块![](https://csdnimg.cn/release/blogv2/dist/pc/img/arrowup-line-top-White.png)

工作流任务完成后,数据将上传到 Elasticsearch,接下来我们就可以开始构建一个基础的 RAG 应用。

步骤 4:配置 RAG

接下来,我们将实现一个简单的检索器( retriever )。它会连接到数据源,接收用户查询,使用与原始数据生成嵌入时相同的模型对查询进行嵌入( embedding ),然后计算余弦相似度( cosine similarity ),检索出最相关的前 3 个文档。

ini 复制代码
`

1.  from langchain_elasticsearch import ElasticsearchStore
2.  from langchain.embeddings import OpenAIEmbeddings
3.  import os

5.  embeddings = OpenAIEmbeddings(
6.      model="text-embedding-3-large",
7.      openai_api_key=os.environ['OPENAI_API_KEY']

9.  )

11.  vector_store = ElasticsearchStore(
12.      es_url=os.environ['es_host'],
13.      index_,
14.      embedding=embeddings,
15.      es_api_key=os.environ['es_api_key'],
16.      query_field="text",
17.      vector_query_field="embeddings",
18.      distance_strategy="COSINE"
19.  )

21.  retriever = vector_store.as_retriever(
22.      search_type="similarity",
23.      search_kwargs={"k": 3}  # Number of results to return
24.  )

`AI写代码![](https://csdnimg.cn/release/blogv2/dist/pc/img/runCode/icon-arrowwhite.png)

接下来,我们将配置一个工作流,用于接收用户查询,从 Elasticsearch 检索相似文档,并将这些文档作为上下文来回答用户的问题。

ini 复制代码
`

1.  from openai import OpenAI

3.  client = OpenAI(api_key=os.getenv("OPENAI_API_KEY"))

5.  def generate_answer(question: str, documents: str):

7.      prompt = """
8.      You are an assistant that can answer user questions given provided context.
9.      Your answer should be thorough and technical.
10.      If you don't know the answer, or no documents are provided, say 'I do not have enough context to answer the question.'
11.      """

13.      augmented_prompt = (
14.          f"{prompt}"
15.          f"User question: {question}\n\n"
16.          f"{documents}"
17.      )
18.      response = client.chat.completions.create(
19.          messages=[
20.              {'role': 'system', 'content': 'You answer users questions.'},
21.              {'role': 'user', 'content': augmented_prompt},
22.          ],
23.          model="gpt-4o-2024-11-20",
24.          temperature=0,
25.      )

27.      return response.choices[0].message.content

30.  def format_docs(docs):
31.      seen_texts = set()
32.      useful_content = [doc.page_content for doc in docs]

34.      return  "\nRetrieved documents:\n" + "".join(
35.          [
36.              f"\n\n===== Document {str(i)} =====\n" + doc
37.              for i, doc in enumerate(useful_content)
38.          ]
39.      )
40.  def rag(query):
41.    docs = retriever.invoke(query)
42.    documents = format_docs(docs)
43.    answer = generate_answer(query, documents)
44.    return documents, answer

`AI写代码![](https://csdnimg.cn/release/blogv2/dist/pc/img/runCode/icon-arrowwhite.png)

将所有步骤组合起来后,我们得到如下流程:

ini 复制代码
`

1.  query = "How did the response lengths change with training?"

3.  docs, answer = rag(query)

5.  print(answer)

`AI写代码

以及一个响应:

vbnet 复制代码
`

1.  Based on the provided context, the response lengths during training for the DeepSeek-R1-Zero model showed a clear trend of increasing as the number of training steps progressed. This is evident from the graphs described in Document 0 and Document 1, which both depict the "average length per response" on the y-axis and training steps on the x-axis.

3.  ### Key Observations:
4.  1. **Increasing Trend**: The average response length consistently increased as training steps advanced. This suggests that the model naturally learned to allocate more "thinking time" (i.e., generate longer responses) as it improved its reasoning capabilities during the reinforcement learning (RL) process.

6.  2. **Variability**: Both graphs include a shaded area around the average response length, indicating some variability in response lengths during training. However, the overall trend remained upward.

8.  3. **Quantitative Range**: The y-axis for response length ranged from 0 to 12,000 tokens, and the graphs show a steady increase in the average response length over the course of training, though specific numerical values at different steps are not provided in the descriptions.

10.  ### Implications:
11.  The increase in response length aligns with the model's goal of solving reasoning tasks more effectively. Longer responses likely reflect the model's ability to provide more detailed and comprehensive reasoning, which is critical for tasks requiring complex problem-solving.

13.  In summary, the response lengths increased during training, indicating that the model adapted to allocate more resources (in terms of response length) to improve its reasoning performance.

`AI写代码![](https://csdnimg.cn/release/blogv2/dist/pc/img/runCode/icon-arrowwhite.png)

Elasticsearch 提供了各种增强搜索的策略,包括混合搜索(Hybrid search),它结合了近似 语义搜索 和基于关键词的搜索。

这种方法可以提高 RAG 架构中作为上下文使用的顶部文档的相关性。要启用它,你需要如下修改 vector_store 初始化:

ini 复制代码
`

1.  from langchain_elasticsearch import DenseVectorStrategy

3.  vector_store = ElasticsearchStore(
4.      es_url=os.environ['es_host'],
5.      index_,
6.      embedding=embeddings,
7.      es_api_key=os.environ['es_api_key'],
8.      query_field="text",
9.      vector_query_field="embeddings",
10.      strategy=DenseVectorStrategy(hybrid=True) // <-- here the change
11.  )

`AI写代码![](https://csdnimg.cn/release/blogv2/dist/pc/img/runCode/icon-arrowwhite.png)

结论

良好的 RAG 始于经过良好准备的数据,而 Unstructured 简化了这一关键的第一步。通过使用 NeMo Retriever 启用分区处理、对非结构化数据进行元数据增强以及高效地将数据摄取到 Elasticsearch 中,它确保你的 RAG 流程建立在坚实的基础之上,为所有下游任务释放其全部潜力。

常见问题

什么是非结构化数据?

非结构化数据指的是没有预定义格式或可搜索结构的原始文件,例如 PDF、研究论文和图像。

NVIDIA NeMo Retriever 做什么?

NVIDIA NeMo Retriever 使用 OCR 和视觉工具来识别页面中的不同部分,例如文本块、表格和图像。

NeMo Retriever 如何与 Elasticsearch 协作?

NeMo Retriever 通过对数据进行分区处理和清理来准备数据,然后将其保存为 Elasticsearch 中的"向量",这样 AI 就可以立即搜索其中的内容。

原文:www.elastic.co/search-labs...

相关推荐
Elasticsearch3 小时前
使用 OpenAI 服务通过 Inference API 实现语义搜索
elasticsearch
Elasticsearch1 天前
跳过编写告警规则:NGINX OTel 集成中内置 6 个现成的 ES|QL 模板
elasticsearch
Elasticsearch1 天前
缩小 AI 差距:下一代知识访问如何为政府解锁任务成果
elasticsearch
Elastic 中国社区官方博客1 天前
Elasticsearch:搜索教程 - 语义搜索(三)
大数据·数据库·人工智能·elasticsearch·搜索引擎·ai·全文检索
西邮彭于晏2 天前
图文详解:Git分支创建、合并与冲突解决|新手零门槛完整教程
大数据·git·elasticsearch
Elastic 中国社区官方博客2 天前
Elastic 和 OpenAI 合作,将前沿智能引入非结构化企业数据
大数据·数据库·人工智能·elasticsearch·搜索引擎·ai
Elastic 中国社区官方博客2 天前
Elasticsearch:搜索教程 - 全文搜索(一)
大数据·python·elasticsearch·搜索引擎·全文检索
西邮彭于晏2 天前
Git 标签(Tag)与版本发布完整指南|附全场景命令速查表
大数据·git·elasticsearch