Genkit × Vertex AI Vector Search 自定义文档索引与检索实战:基于本地文件的 Indexer / Retriever 示例全解析
Genkit × Vertex AI Vector Search 自定义文档索引与检索实战基于本地文件的 Indexer / Retriever 示例全解析【免费下载链接】genkitOpen-source framework for building agentic apps in JavaScript, Go, Dart, and Python, built and used in production by Google项目地址: https://gitcode.com/GitHub_Trending/ge/genkit本指南以仓库中的js/testapps/vertexai-vector-search-custom示例为核心讲解如何在 Google Genkit 中通过genkit-ai/vertexai/vectorsearch插件为 Vertex AI Vector Search 定义自定义文档索引器DocumentIndexer与文档检索器DocumentRetriever并以本地 JSON 文件作为伪数据库跑通从文档入库、向量化、近邻检索到结果回填的完整链路。读完本文你将掌握该插件vectorSearchOptions的每一项配置含义、ai.index/ai.retrieve两个 Flow 的编写方式以及插件底层如何调用upsertDatapoints与findNeighbors两个 Vertex AI REST 接口。为什么需要自定义 Indexer 与 RetrieverVertex AI Vector Search 的本质是一个向量检索引擎它保存的是数据点datapoint及其特征向量以及可选的 restricts / numericRestricts 等过滤标签并不负责保存你的文档原文与业务元数据。因此在使用 Genkit 接入它时需要你自己回答两个问题文档原文放在哪—— 这是documentIndexer的职责把Document[]存入你自己的存储示例中是本地 JSON 文件并返回每个文档对应的 ID。根据近邻 ID 找回原文的路径是什么—— 这是documentRetriever的职责Vertex AI 返回一批最近的邻居每个邻居带datapointId与distance你需要按这些 ID 从自己的存储里把文档捞回来。官方插件在js/plugins/vertexai/src/vectorsearch/vector_search/indexers.ts与retrievers.ts中预留了documentIndexer/documentRetriever两个扩展点示例正是用这两个扩展点把文档存储层换成了一个本地文件。示例总览与关键组件该示例的完整代码位于 src/index.ts环境变量加载逻辑位于 src/config.ts其 READMEjs/testapps/vertexai-vector-search-custom/README.md将其拆分为以下关键组件自定义文档索引器Custom Document Indexer把文档写入本地documents.json返回新生成的文档 ID自定义文档检索器Custom Document Retriever按近邻 ID 从本地 JSON 文件中取回DocumentGenkit 配置Genkit Configuration装配 Vertex AI 插件与 Vector Search 插件配置项目、位置与索引参数索引 FlowIndexing Flow接收文本数组转为文档后调用ai.index入库查询 FlowQuery Flow接收查询文本与近邻数量k调用ai.retrieve并按距离排序返回结果。整个数据流可以概括为文档 → Document.fromText → ai.index → 本地文件落盘 Embedding 向量 upsert 到 Vertex AI查询时则反向执行查询文本 → Embedding → findNeighbors → 按 datapointId 回查本地文件 → 返回文档与距离。前置条件Prerequisites根据 README 与示例代码运行前需要满足已安装Node.js已安装PNPM包管理器已在Vertex AI Vector Search中完成索引Index的创建、部署到索引端点Index Endpoint并拿到公开域名Public Domain Name——这是通过公开端点执行findNeighbors的前提拥有可用的 Google Cloud 凭据示例通过GOOGLE_APPLICATION_CREDENTIALS环境变量指向服务账号密钥文件并已启用 Vertex AI / AI Platform 相关 API示例索引器仅支持Streaming Update Index流式更新索引这一点在 types.ts 的DocumentIndexer类型注释中有明确说明Only Streaming Update Indexers are supported。注意示例代码在启动时会校验环境变量是否齐全src/index.ts缺失任一变量会直接抛出Missing environment variables. Please check your .env file.并终止进程。安装与构建workspace 依赖的正确姿势本示例通过workspace:*引用 monorepo 内尚未发布的核心包genkit、genkit-ai/vertexai等见 package.json因此需要先克隆仓库并按根目录 README.md 的指引构建核心包再安装依赖。由于依赖是 workspace 内联引用核心包必须在本机可达。安装命令原 README 中目录名写为vertex-vector-search-custom实际仓库目录为vertexai-vector-search-custom以下按仓库真实路径执行cd js/testapps/vertexai-vector-search-custom pnpm i若需要编译 TypeScript可执行pnpm buildpackage.json中提供了compiletsc、build先清空lib再编译与build:watch等脚本。环境变量配置七个变量的作用与来源在示例根目录创建.env文件也可参考同目录.env.example内容如下PROJECT_IDyour-google-cloud-project-id LOCATIONyour-vertex-ai-location LOCAL_DIR./data VECTOR_SEARCH_PUBLIC_DOMAIN_NAMEyour-vector-search-public-domain-name VECTOR_SEARCH_INDEX_ENDPOINT_IDyour-index-endpoint-id VECTOR_SEARCH_INDEX_IDyour-index-id VECTOR_SEARCH_DEPLOYED_INDEX_IDyour-deployed-index-id GOOGLE_APPLICATION_CREDENTIALSpath-to-your-service-account-key.json各变量的含义与在代码中的用途变量用途消费位置PROJECT_IDGoogle Cloud 项目 ID用于 Vertex AI 鉴权与 API 调用vertexAI/vertexAIVectorSearch插件配置LOCATIONVertex AI 区域如us-central1决定 API 端点插件配置、upsertDatapoints与findNeighborsURLLOCAL_DIR本地 JSON 文件所在目录默认./data拼接documents.json路径VECTOR_SEARCH_PUBLIC_DOMAIN_NAMEIndex Endpoint 的公开访问域名检索时构造findNeighborsREST 地址VECTOR_SEARCH_INDEX_ENDPOINT_ID索引端点 ID检索时构造端点 URLVECTOR_SEARCH_INDEX_ID索引 ID索引时upsertDatapoints的目标VECTOR_SEARCH_DEPLOYED_INDEX_ID已部署索引的 ID检索请求体中的deployed_index_idGOOGLE_APPLICATION_CREDENTIALS服务账号密钥文件路径Google 标准 ADC 变量GoogleAuth 自动读取加载机制很简单config.ts 顶部调用dotenv的config()随后逐个读取process.env并导出。示例在 src/index.ts 中对除PROJECT_ID、LOCATION、GOOGLE_APPLICATION_CREDENTIALS外的五个变量做了非空校验。启动 Genkit 服务器依赖安装并配置好.env后在示例目录执行genkit startgenkit start会加载当前目录的 Genkit 应用启动 Dev UI 与 Flow 服务器indexFlow与queryFlow会自动注册可在 Dev UI 中直接调用或通过 HTTP 接口访问。自定义文档索引器把文档落盘并返回 IDlocalDocumentIndexer的类型为DocumentIndexer签名是(docs: Document[], options?) Promisestring[]——返回值必须是每个文档的 ID 列表插件随后会用这些 ID 作为 datapoint ID 构建向量数据点。示例实现如下完整版见 src/index.tsconst localDocumentIndexer: DocumentIndexer async (documents: Document[]) { try { const content await fs.promises.readFile(localFilePath, utf-8); const currentLocalFile JSON.parse(content); const docsWithIds Object.fromEntries( documents.map((doc) [ generateRandomId(), { content: JSON.stringify(doc.content) }, ]) ); const newLocalFile { ...currentLocalFile, ...docsWithIds }; try { await fs.promises.writeFile( localFilePath, JSON.stringify(newLocalFile, null, 2) ); return Object.keys(docsWithIds); } catch (writeError) { console.error(Error writing file:, writeError); throw writeError; } } catch (readError) { console.error(Error reading file:, readError); throw readError; } };实现要点存储结构documents.json是一个以随机 ID → 文档内容为键值对的扁平对象localFilePath path.posix.join(LOCAL_DIR, documents.json)src/index.ts文件不存在时会先写入{}初始化第 62–64 行。ID 生成generateRandomId () Math.random().toString(36).substring(7)第 66 行仅演示用生产环境应使用数据库自增 ID 或 UUID。内容序列化doc.content被JSON.stringify后整体存为字符串检索时再反序列化——这是为了保持与Document 内容可能包含多个 part的通用结构兼容。注意示例在indexFlow中实际调用的是ai.index文档是否真的被写入本地文件取决于 Genkit 插件内部是否调用了你的documentIndexer——详见下文底层调用链。自定义文档检索器按近邻 ID 回查原文localDocumentRetriever的类型为DocumentRetriever签名是(neighbors: Neighbor[], options?: { k?: number }) PromiseDocument[]。Vertex AI 返回的每个Neighbor结构为{ datapoint?: { datapointId?, featureVector?, ... }, distance?, sparseDistance? }定义见 types.ts。示例实现src/index.tsconst localDocumentRetriever: DocumentRetriever async ( neighbors: Neighbor[] ) { try { const content await fs.promises.readFile(localFilePath, utf-8); const currentLocalFile JSON.parse(content); const ids neighbors .map((neighbor) neighbor.datapoint?.datapointId) .filter(Boolean) as string[]; const docs ids .map((id) { const doc currentLocalFile[id]; if (!doc || !doc.content) { console.error(No content found for ID: ${id}); return null; } try { const parsedContent JSON.parse(doc.content); const text parsedContent[0]?.text; if (text) { return Document.fromText(text); } else { console.error(No text found in content for ID: ${id}); return null; } } catch (error) { console.error(Error parsing content for ID: ${id}, error); return null; } }) .filter(Boolean) as Document[]; return docs; } catch (error) { console.error(Error reading file:, error); throw error; } };实现要点ID 提取从每个neighbor.datapoint.datapointId取 ID并过滤空值这正是索引器返回的 ID 集合。原文回填按 ID 从 JSON 中取回content字段反序列化后取第一个 part 的text再通过Document.fromText(text)重建 GenkitDocument——这一步让检索结果与 Genkit 生态如后续的 LLM 生成、Prompt 渲染无缝衔接。容错对缺失 ID、缺失文本、JSON 解析失败分别打印错误并跳过最终filter(Boolean)剔除空项。distance的传递Neighbor.distance由插件层面注入到返回文档的元数据中见queryFlow中doc.metadata?.distance的读取本地文件本身不存距离。插件装配vertexAI 与 vertexAIVectorSearch示例通过genkit()同时注册两个插件src/index.tsconst ai genkit({ plugins: [ vertexAI({ projectId: PROJECT_ID, location: LOCATION, googleAuth: { scopes: [https://www.googleapis.com/auth/cloud-platform], }, }), vertexAIVectorSearch({ location: LOCATION, projectId: PROJECT_ID, vectorSearchOptions: [ { publicDomainName: VECTOR_SEARCH_PUBLIC_DOMAIN_NAME, indexEndpointId: VECTOR_SEARCH_INDEX_ENDPOINT_ID, indexId: VECTOR_SEARCH_INDEX_ID, deployedIndexId: VECTOR_SEARCH_DEPLOYED_INDEX_ID, documentRetriever: localDocumentRetriever, documentIndexer: localDocumentIndexer, embedder: textEmbedding004, }, ], }), ], });vectorSearchOptions数组中的每一项对应一个索引配置字段定义在 types.ts 的VectorSearchOptions接口中字段必填说明publicDomainName是Index Endpoint 公开域名用于构造findNeighborsURLindexEndpointId是索引端点 IDindexId是索引 ID同时决定插件注册的 indexer/retriever 名称vertexai/${indexId}deployedIndexId是已部署索引 IDdocumentRetriever是自定义文档回填函数documentIndexer是自定义文档落盘函数embedder否该索引使用的 Embedding 模型引用不填则回退到插件级embedderembedderOptions否传给 Embedder 的选项示例中embedder: textEmbedding004来自genkit-ai/vertexai主包src/index.ts即 Google 的text-embedding-004模型。若多个索引共用同一个 Embedder也可以在vertexAIVectorSearch的顶层配置embedder作为默认值见 vectorsearch/types.ts 的VectorSearchOptionsConfig与 vectorsearch/index.ts 中defaultEmbedder: options.embedder的传递逻辑。从插件实现看vertexAIVectorSearch返回的GenkitPlugin在初始化时遍历vectorSearchOptions为每一项分别注册一个名为vertexai/${indexId}的 indexer 与 retrievervectorsearch/index.ts而vertexAiIndexerRef({ indexId, displayName })/vertexAiRetrieverRef({ indexId, displayName })生成同名引用indexers.ts、retrievers.tsFlow 中正是通过indexId把调用指向对应索引。定义索引与查询 Flow示例定义了两个 Flowsrc/index.tsexport const indexFlow ai.defineFlow( { name: indexFlow, inputSchema: z.object({ texts: z.array(z.string()), }), outputSchema: z.any(), }, async ({ texts }) { const documents texts.map((text) Document.fromText(text)); await ai.index({ indexer: vertexAiIndexerRef({ indexId: VECTOR_SEARCH_INDEX_ID, displayName: firestore_index, }), documents, }); return { result: success }; } ); export const queryFlow ai.defineFlow( { name: queryFlow, inputSchema: z.object({ query: z.string(), k: z.number(), }), outputSchema: z.object({ result: z.array( z.object({ text: z.string(), distance: z.number(), }) ), length: z.number(), time: z.number(), }), }, async ({ query, k }) { const startTime performance.now(); const queryDocument Document.fromText(query); const res await ai.retrieve({ retriever: vertexAiRetrieverRef({ indexId: VECTOR_SEARCH_INDEX_ID, displayName: firestore_index, }), query: queryDocument, options: { k }, }); const endTime performance.now(); return { result: res .map((doc) ({ text: doc.content[0].text!, distance: doc.metadata?.distance, })) .sort((a, b) b.distance - a.distance), length: res.length, time: endTime - startTime, }; } );要点说明Index Flow输入texts字符串数组 → 逐一Document.fromText→ 调用ai.index传入 indexer 引用与文档。返回固定的{ result: success }。Query Flow输入query与近邻数量k→ 把查询文本转成Document→ 调用ai.retrieveoptions: { k }控制返回条数→ 输出每个文档的text、distance来自doc.metadata?.distance、命中总数与耗时performance.now()差值并按distance降序排序后返回。示例中displayName: firestore_index是引用标签仅用于 Dev UI 展示与实际的 Firestore 无关属于示例中的命名沿用。服务启动调用startFlowsServer()将 Flow 暴露为 Genkit Server 端点配合genkit start使用。底层调用链从 Flow 到 Vertex AI REST API这是自定义 Indexer / Retriever与 Vertex AI 衔接的核心机制值得单独展开。索引侧documentIndexer → embedMany → upsertDatapoints在 indexers.ts 的vertexAiIndexers中插件为每个vectorSearchOptions定义了一个 indexer action执行顺序为先调用你的documentIndexer(docs, options)把文档存入自有存储得到docIds失败会包装为Error storing your document content/metadata调用ai.embedMany({ embedder, content: docs })批量生成向量把docIds[i]与embedding[i]组装成DatapointdatapointIdfeatureVector若文档metadata中带restricts/numericRestricts/crowdingTag也会一并写入数据点调用 upsert_datapoints.ts 中的upsertDatapoints向https://${location}-aiplatform.googleapis.com/v1/projects/${projectId}/locations/${location}/indexes/${indexId}:upsertDatapoints发送 REST 请求请求体为datapoint_id、feature_vector以及可选的restricts、numeric_restricts。因此本地 JSON 文件落盘与向量进入 Vertex AI是同一事务里的两步先存原文拿 ID再用 ID 建向量数据点。检索侧embed → findNeighbors → documentRetriever在 retrievers.ts 的vertexAiRetrievers中执行顺序为用指定 Embedder 对查询文本生成单个 embeddingai.embed获取访问令牌authClient.getAccessToken()并通过getProjectNumber(projectId)把项目 ID 换算成项目编号——这是公开端点 URL 所要求的调用 query_public_endpoint.ts 中的queryPublicEndpoint向https://${publicDomainName}/v1/projects/${projectNumber}/locations/${location}/indexEndpoints/${indexEndpointId}:findNeighbors发送 POST请求体包含deployed_index_id、neighbor_count默认DEFAULT_K 10见 retrievers.ts可由options.k覆盖以及可选的restricts/numeric_restricts从响应中取出nearestNeighbors[0].neighbors即Neighbor[]交给你的documentRetriever(neighbors, options)回填原文返回{ documents }其元数据中携带distance等信息供上层使用。可以看到k的取值优先级是Flow 传入的options.k 插件默认值 10而restricts/numericRestricts既可以来自索引文档的 metadata入库时写入数据点也可以来自查询文档的 metadata检索时作为过滤条件支持LESS、LESS_EQUAL、EQUAL、GREATER_EQUAL、GREATER、NOT_EQUAL等数值比较操作符见 types.ts 的NumericRestrictionOperatorSchema。从示例走向生产内置的 Firestore 与 BigQuery 实现本地 JSON 文件只是演示自定义这一机制的最小实现README 也明确指出它不应直接用于生产Dont use these in production obviously。仓库在同级目录提供了两个可直接落地的变体vertexai-vector-search-firestore通过getFirestoreDocumentIndexer/getFirestoreDocumentRetriever把文档存入 Firestorevertexai-vector-search-bigquery通过getBigQueryDocumentIndexer/getBigQueryDocumentRetriever把文档存入 BigQuery。两者均从genkit-ai/vertexai/vectorsearch导出见 vectorsearch/index.ts与vertexAIVectorSearch的documentIndexer/documentRetriever插槽直接对接。你在生产环境中只需把自己的存储接入换成这两个函数或按同样的接口契约自研其余链路Embedding、向量 upsert、近邻检索全部复用插件实现。总结这个示例完整展示了 Genkit 的检索增强设计哲学向量索引交给 Vertex AI Vector Search原文存储交给自定义的 Indexer / Retriever二者通过 datapoint ID 松散耦合。理解DocumentIndexer存原文、返回 ID与DocumentRetriever按 ID 回填文档这对扩展点以及插件内部upsertDatapoints/findNeighbors的调用链你就能把任何存储文件、Firestore、BigQuery、PostgreSQL……接入 Vertex AI 向量检索构建属于自己的 RAG 应用。进一步阅读建议查看插件源码 indexers.ts 与 retrievers.ts 了解扩展点细节对照 vertexai-vector-search-firestore 与 vertexai-vector-search-bigquery 两个示例确认生产级存储实现方式。本示例基于 Apache License 2.0 开源见 LICENSE。【免费下载链接】genkitOpen-source framework for building agentic apps in JavaScript, Go, Dart, and Python, built and used in production by Google项目地址: https://gitcode.com/GitHub_Trending/ge/genkit创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考