DataHub 语义搜索架构深度解析:双索引设计、Embedding 数据流与 k-NN 实战配置
【免费下载链接】datahubThe Context Platform for your Data and AI Stack项目地址: https://gitcode.com/GitHub_Trending/da/datahub
语义搜索(Semantic Search)是 DataHub 在传统关键词检索之上新增的向量相似度检索能力:它将文档与查询文本转换为高维向量,通过 OpenSearch k-NN 索引实现"按语义而非字面"的召回。本文以仓库内架构文档 docs/dev-guides/semantic-search/ARCHITECTURE.md 为主线,结合 SemanticContent.pdl、application.yaml 及 ingestion 端 chunking 源码,系统讲解 DataHub 语义搜索的设计哲学、双索引过渡架构、Embedding 生成与查询链路、分块策略与 k-NN 参数调优,帮助你理解该功能如何工作,并能在自己的部署中正确开启与配置。
设计哲学:为什么需要语义搜索
传统关键词搜索(keyword search)存在三类固有局限:
- 词汇不匹配(Vocabulary Mismatch):用户使用的词与文档中的词不一致,例如文档写的是"data governance",用户却搜"metadata compliance";
- 同义词盲区(Synonym Blindness):
access request无法匹配permission request,尽管两者语义相同; - 缺乏上下文理解(Context Ignorance):关键词匹配停留在字符串层面,不理解含义。
语义搜索通过向量嵌入(vector embeddings)——一种能捕捉文本语义相似度的数值表示——来理解文本的含义,从而缓解以上问题。
DataHub 语义搜索的设计遵循四条核心原则(见架构文档 "Core Principles" 一节):
- 非侵入(Non-invasive):语义搜索是增量叠加能力,不会取代或破坏现有关键词搜索;
- 可配置(Configurable):由组织自行决定启用哪些实体类型、使用哪个嵌入模型;
- 可扩展(Extensible):新增嵌入模型无需改动整体架构,通过配置即可挂接;
- 异步处理(Async Processing):Embedding 生成异步进行,不阻塞元数据摄取(ingestion)主流程。
索引架构:双索引(Dual-Index)过渡策略
每个实体类型维护两个索引
对每个开启语义搜索的实体类型,DataHub 同时维护两个 OpenSearch 索引(下文以 document 实体为例):
┌─────────────────────────────────┐ ┌─────────────────────────────────┐ │ documentindex_v2 │ │ documentindex_v2_semantic │ ├─────────────────────────────────┤ ├─────────────────────────────────┤ │ Standard OpenSearch index │ │ OpenSearch index with k-NN │ │ │ │ │ │ Fields: │ │ Fields: │ │ - urn │ │ - urn │ │ - title (text) │ │ - title (text) │ │ - text (text) │ │ - text (text) │ │ - browsePaths │ │ - browsePaths │ │ - tags │ │ - tags │ │ - ... │ │ - ... │ │ │ │ │ │ │ │ + embeddings (nested object): │ │ │ │ - cohere_embed_v3: │ │ │ │ - model_version │ │ │ │ - generated_at │ │ │ │ - chunks[] (nested): │ │ │ │ - position │ │ │ │ - text │ │ │ │ - vector (knn_vector) │ └─────────────────────────────────┘ └─────────────────────────────────┘documentindex_v2:标准 OpenSearch 索引,承载现有关键词检索;documentindex_v2_semantic:启用 k-NN 的语义索引,除常规字段外,还以nested object形式保存embeddings,其中每个模型名下再嵌套chunks[](包含position、text、vector等)。
为什么采用双索引:过渡架构的工程取舍
架构文档明确指出,双索引是过渡性架构(transitional architecture),长期演进分三个阶段:
- 阶段一(当前):过渡期两个索引并行运行;
- 阶段二:将全部搜索流量迁移到语义索引;
- 阶段三:彻底下线
v2索引。
过渡方案带来四个直接收益:
- 零停机迁移:语义能力构建期间,用户可继续使用关键词搜索;
- 渐进验证:可在全量上线前充分验证语义搜索的检索质量;
- 回滚安全:出现问题时可随时回退到关键词搜索;
- 增量生成 Embedding:可离线回填(backfill)向量而不阻塞线上操作。
未来终态:迁移完成后,_semantic索引将成为主(且唯一)搜索索引,同一份索引同时支持:
- 关键词搜索:通过 OpenSearch 标准文本匹配;
- 语义搜索:通过 k-NN 向量相似度。
统一索引既简化了运维,也降低了存储开销。
从源码看,该双索引体系由 metadata-io 模块下的索引构建器落地实现:语义索引的 mapping 与 settings 分别由 V2SemanticSearchMappingsBuilder.java 和 V2SemanticSearchSettingsBuilder.java 负责,索引实际创建由 ESIndexBuilder.java 统一调度。
Embeddings 存储 Schema
语义索引中的向量数据采用嵌套结构存放,示例文档:
{ "urn": "urn:li:document:example-doc", "title": "Data Access Guide", "text": "How to request access to datasets...", "embeddings": { "cohere_embed_v3": { "model_version": "bedrock/cohere.embed-english-v3", "generated_at": "2024-01-15T10:30:00Z", "chunking_strategy": "sentence_boundary_400t", "total_chunks": 3, "total_tokens": 850, "chunks": [ { "position": 0, "text": "How to request access to datasets...", "character_offset": 0, "character_length": 450, "token_count": 95, "vector": [0.023, -0.041, 0.087, ...] }, { "position": 1, "text": "For sensitive data, additional approval...", "character_offset": 450, "character_length": 380, "token_count": 82, "vector": [0.019, -0.055, 0.091, ...] } ] } } }其中vector维度取决于所选模型(文档示例为 Cohere Embed v3 的 1024 维)。
多模型支持
embeddings结构天然支持同时存储多个嵌入模型的结果:
{ "embeddings": { "cohere_embed_v3": { ... }, "openai_text_embedding_3": { ... }, "custom_model": { ... } } }这带来三个能力:
- 不同模型的A/B 测试;
- 模型间的渐进迁移(可新旧模型并存、逐步切换);
- 模型专属优化(不同模型可按需调整索引参数)。
该设计与 SemanticContent.pdl 中的定义完全一致——embeddings字段类型为map[string, EmbeddingModelData],key 即模型标识(如cohere_embed_v3、openai_ada_002)。值得一提的是,PDL 中还预留了skipReason(如EMPTY_TEXT、BELOW_MIN_TEXT_LENGTH、NO_INDEXABLE_CONTENT)与skippedAt两个字段,用于区分"实体本就不可嵌入"与"索引滞后或失败",方便消费方排查问题。
数据流:从摄取到查询
摄取链路(Ingestion Flow)
整体链路为:源系统 → 摄取连接器(ingestion connector)→ GMS → OpenSearch。
┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ Source │ 1. Extract documents │ System │ └──────┬──────┘ │ ▼ ┌─────────────┐ 2. Generate embeddings for document content │ Ingestion │ (using connector's embedding provider) │ Connector │ └──────┬──────┘ │ ▼ ┌─────────────┐ 3. Send document + embeddings to GMS │ GMS │ └──────┬──────┘ │ ▼ ┌─────────────────────────────────────────────────────────────────┐ │ OpenSearch │ │ ┌─────────────────────┐ ┌─────────────────────────────────┐ │ │ │ entityindex_v2 │ │ entityindex_v2_semantic │ │ │ │ (keyword search) │ │ (keyword + vector search) │ │ │ │ - urn / title / ... │ │ - urn / title / text / │ │ │ └─────────────────────┘ │ embeddings.model.chunks[]. │ │ │ │ vector │ │ │ └─────────────────────────────────┘ │ └─────────────────────────────────────────────────────────────────┘关键一步在摄取连接器:它在摄取时即完成文档 Embedding 生成,并通过MCP(Metadata Change Proposal)将文档内容与向量一并提交给 GMS。这样做的好处是:
- 一致性:每个被摄取文档从一开始就携带 Embedding;
- 简单性:无需单独维护回填(backfill)任务;
- 新鲜度:Embedding 始终与文档内容同步更新;
- 审计追踪:向量变化记录在 Metadata Change Log(MCL)中;
- 隐私支持:敏感数据源可在本地生成 Embedding,仅共享向量而不外传原文。
MCP 驱动的 Embedding 流转
┌──────────────┐ ┌──────────────┐ ┌──────────────────────┐ │ Source │───▶│ Ingestion │───▶│ DataHub GMS │ │ System │ │ Connector │ │ │ └──────────────┘ └──────┬───────┘ └──────────┬───────────┘ │ │ ▼ ▼ ┌──────────────────┐ ┌──────────────────┐ │ Generate document│ │ Process MCP and │ │ embeddings │ │ write to semantic │ │ (in connector) │ │ search index │ └────────┬─────────┘ └──────────────────┘ │ ▲ │ MCP with │ └─────SemanticContent─┘ aspectSemanticContent Aspect:第一等公民的元数据
向量并非存于旁路系统,而是以 DataHub 标准的aspect形式落库。SemanticContentaspect 在 SemanticContent.pdl 中定义(@Aspect = { "name": "semanticContent" }),其 MCP 载荷示例:
{ "entityType": "document", "entityUrn": "urn:li:document:my-doc", "aspectName": "semanticContent", "aspect": { "embeddings": { "cohere_embed_v3": { "modelVersion": "bedrock/cohere.embed-english-v3", "generatedAt": 1702234567890, "totalChunks": 2, "chunks": [ { "position": 0, "vector": [...], "text": "..." }, { "position": 1, "vector": [...], "text": "..." } ] } } } }在 ingestion 端,该 aspect 的实际产出位于 chunking_source.py 的process_elements_inline()方法中——它对非结构化文档元素进行分块、生成 Embedding,并以SemanticContentaspect 的 WorkUnit 形式输出(该方法同样服务于 Notion 等外部文档源的 notion_source.py 与 confluence_source.py)。
隐私敏感场景
每个 chunk 的text字段是可选的。这支持以下场景:
- 源数据包含敏感信息(PII、商业秘密);
- 客户只希望把向量存进 DataHub,而不存储源文本;
- Embedding 在数据源本地生成。
注意:Embedding 是单向的——无法从向量反推出原始文本。
查询链路:GMS 侧生成查询向量
与文档 Embedding 相反,查询 Embedding(Query Embedding)由 GMS 在搜索时生成,使用 GMS 配置的嵌入提供者(如 AWS Bedrock):
┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ GraphQL │───▶│ GMS │───▶│ Embedding │───▶│ OpenSearch │ │ Client │ │ │ │ Provider │ │ k-NN Query │ └─────────────┘ └─────────────┘ └─────────────┘ └─────────────┘ │ ▼ Query embedding generated here (for search only)关键点:GMS 的 Embedding Provider只负责查询向量,文档向量一律由摄取连接器负责——这是文档嵌入与查询嵌入在职责上的硬性分工。
完整的查询流程如下:
┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ GraphQL │───▶│ GMS │───▶│ Embedding │───▶│ OpenSearch │ │ Client │ │ │ │ Provider │ │ k-NN Query │ └─────────────┘ └─────────────┘ └─────────────┘ └─────────────┘ │ semanticSearchAcrossEntities( │ query: "how to access data" │ ) │ ▼ ┌─────────────────────────────┐ │ Nested k-NN Query: │ │ { │ │ "nested": { │ │ "path": "embeddings │ │ .cohere_embed_v3 │ │ .chunks", │ │ "query": { │ │ "knn": { │ │ "...chunks.vector":│ │ { "vector": [...], │ │ "k": 10 } │ │ } │ │ } │ │ } │ │ } │ └─────────────────────────────┘查询通过semanticSearchAcrossEntitiesGraphQL 入口发起(如查询 "how to access data"),GMS 生成查询向量后,对语义索引发起nested k-NN 查询:nested路径定位到embeddings.<model>.chunks,knn在该层级的vector字段上执行近似最近邻检索(示例k: 10表示返回 Top-10 命中)。服务端实现位于 SemanticSearchService.java,并针对 OpenSearch 与 Elasticsearch 8 分别提供了查询适配(见 OpenSearchSearchClientShim.java 与 Es8SearchClientShim.java)。
分块策略(Chunking Strategy)
为什么需要分块
嵌入模型有 token 上限(例如 Cohereembed-english-v3.0为 512 tokens),长文档必须拆分为块:
- Token 限制:模型无法处理无限长度的文本;
- 精确性:更小的块允许更精确的匹配;
- 相关性:一篇文档可能只有某个段落与查询高度相关。
分块算法
def chunk_text(text, max_tokens=400): """ Chunk text at sentence boundaries, respecting token limits. 1. Split text into sentences 2. Accumulate sentences until approaching limit 3. Save chunk, start new accumulation 4. Handle oversized sentences by character splitting """参数说明:
max_tokens:目标块大小(默认 400);chars_per_token:字符/token 估算比例(默认约 4 字符 ≈ 1 token)。
在真实 ingestion 实现中,分块参数被抽象为 chunking_config.py 中的ChunkingConfig:
| 配置项 | 默认值 | 说明 |
|---|---|---|
strategy | by_title | 分块策略,可选basic或by_title(按标题/章节切分) |
max_characters | 500 | 每个块的最大字符数 |
overlap | 0 | 块与块之间的字符重叠量 |
combine_text_under_n_chars | 100 | 小于该尺寸的碎片块将合并到相邻块 |
同时,EmbeddingConfig 还提供批量与限流参数:batch_size(默认 25,单次 Embedding API 调用的文档数)、request_timeout(默认 60s)、rate_limit(默认开启)、documents_per_minute(默认 300 篇/分钟)。这些参数让生产环境可以控制外部 Embedding API 的调用压力。另外需要注意:model_embedding_key会被校验为仅含字母、数字与下划线,因为 Elasticsearch 字段名不允许.、:等标点(如 Bedrock Titan 的模型 IDamazon.titan-embed-text-v2:0就不能直接用作字段名,需改用cohere_embed_v3这类下划线形式)。
块元数据(Chunk Metadata)
每个块保存用于调试与分析的元数据:
{ "position": 0, // Order in document "text": "...", // Chunk content "character_offset": 0, // Start position in original "character_length": 450, // Length in characters "token_count": 95, // Estimated tokens "vector": [...] // Embedding vector }k-NN 搜索配置
OpenSearch k-NN 设置
语义索引使用 OpenSearch 的 k-NN 插件,引擎为FAISS,映射示例:
{ "settings": { "index.knn": true }, "mappings": { "properties": { "embeddings": { "type": "nested", "properties": { "cohere_embed_v3": { "type": "nested", "properties": { "chunks": { "type": "nested", "properties": { "vector": { "type": "knn_vector", "dimension": 1024, "method": { "name": "hnsw", "engine": "faiss", "space_type": "cosinesimil", "parameters": { "ef_construction": 128, "m": 16 } } } } } } } } } } } }HNSW 参数说明
| 参数 | 值 | 说明 |
|---|---|---|
ef_construction | 128 | 建图精度(越大越准,建索引越慢) |
m | 16 | 每个节点的连接数(越大越准,内存占用越高) |
space_type | cosinesimil | 相似度度量(余弦相似度) |
在 application.yaml 中如何落地
上述 k-NN 参数在 GMS 配置 application.yaml(elasticsearch.entityIndex.semanticSearch段)中均可通过环境变量覆盖,且索引维度、引擎、度量空间逐模型配置:
elasticsearch: entityIndex: semanticSearch: enabled: ${ELASTICSEARCH_SEMANTIC_SEARCH_ENABLED:false} enabledEntities: ${ELASTICSEARCH_SEMANTIC_SEARCH_ENTITIES:document} models: text_embedding_3_large: vectorDimension: ${ELASTICSEARCH_SEMANTIC_VECTOR_DIMENSION:3072} knnEngine: ${ELASTICSEARCH_SEMANTIC_KNN_ENGINE:faiss} spaceType: ${ELASTICSEARCH_SEMANTIC_SPACE_TYPE:cosinesimil} efConstruction: ${ELASTICSEARCH_SEMANTIC_EF_CONSTRUCTION:128} m: ${ELASTICSEARCH_SEMANTIC_M:16}配置文件还预置了多个常用模型的索引参数模板,包括:nomic_embed_text(Ollama 默认,768 维)、gemini_embedding_001(Vertex AI,默认 3072 维)、snowflake_arctic_embed_s(384 维)、snowflake_arctic_embed_l(1024 维)、bge_base_en_v1_5(768 维)等,均使用faiss+cosinesimil+ef_construction: 128+m: 16。此外 EntityIndexConfiguration.java 与 SearchServiceConfiguration.java 分别承载了语义搜索配置与整体搜索配置的 Java 绑定。
嵌入提供者(Embedding Provider)配置
GMS 查询侧支持六种 Embedding Provider(见 EmbeddingProviderConfiguration.java),在semanticSearch.embeddingProvider段配置:
| Provider 类型 | 说明 | 模型示例(默认) | 关键环境变量 |
|---|---|---|---|
aws-bedrock | AWS Bedrock Runtime API | cohere.embed-english-v3(1024 维) | BEDROCK_EMBEDDING_AWS_REGION(默认us-west-2)、BEDROCK_EMBEDDING_MODEL |
openai | OpenAI Embeddings API | text-embedding-3-large(3072 维) | OPENAI_API_KEY、OPENAI_EMBEDDING_MODEL、OPENAI_EMBEDDING_ENDPOINT |
cohere | Cohere Embed API | embed-english-v3.0(1024 维) | COHERE_API_KEY、COHERE_EMBEDDING_MODEL、COHERE_EMBEDDING_ENDPOINT |
local | 本地 OpenAI 兼容服务(Ollama、LM Studio、llama.cpp 等) | nomic-embed-text(768 维) | LOCAL_EMBEDDING_ENDPOINT(默认http://localhost:11434/v1/embeddings)、LOCAL_EMBEDDING_MODEL |
vertex_ai | Google Vertex AI Embeddings API | gemini-embedding-001 | VERTEX_AI_PROJECT_ID、VERTEX_AI_LOCATION(默认us-east1)、VERTEX_AI_EMBEDDING_MODEL |
onnx | JVM 进程内 ONNX Runtime 推理(无需外部服务) | snowflake_arctic_embed_s/l、bge_base_en_v1_5 | ONNX_EMBEDDING_MODEL_NAME、ONNX_EMBEDDING_MODEL_DIR、ONNX_EMBEDDING_POOLING(默认cls)、ONNX_EMBEDDING_QUERY_INSTRUCTION |
基础配置项(application.yaml L827-834):
semanticSearch: embeddingProvider: type: ${EMBEDDING_PROVIDER_TYPE:openai} maxCharacterLength: ${EMBEDDING_PROVIDER_MAX_CHAR_LENGTH:2048}其中maxCharacterLength默认 2048,对应 Cohere Embed v3 对请求体的 2048 字符硬限制(独立于 token 上下文窗口)。
各提供者的要点与约束(源码注释与工厂实现 EmbeddingProviderFactory.java 中均有明确校验):
- openai:必须提供 API key(
OPENAI_API_KEY或配置项),否则启动即抛异常;endpoint 可指向 Azure OpenAI 部署地址。 - aws-bedrock:依赖共享的
defaultAwsCredentialsProviderBean;bedrock.awsRegion必填,且支持与 Pod 的AWS_REGION不同的跨区域访问。 - cohere:必须提供
COHERE_API_KEY。 - local:兼容任何 OpenAI 兼容端点;Docker Compose 下(quickstart-ai profile)使用
http://ollama:11434/v1/embeddings。 - vertex_ai:必须配置
projectId与location;使用 Application Default Credentials,启动时即验证凭据(fail-fast)。 - onnx:
modelName必须匹配semanticSearch.models中的某个 key;modelDir需包含model.onnx(或model_quantized.onnx)与tokenizer.json;启动时会对模型实际输出维度与配置的vectorDimension做一致性校验,不匹配会直接拒绝启动(避免建出维度错误的索引);pooling必须与文档侧一致(默认cls,适用于 Arctic-embed 与 BGE 系列),否则查询与文档向量不在同一子空间,kNN 召回会失效。
重要:当语义搜索未启用时,工厂会返回 NoOpEmbeddingProvider(一个使用即抛异常的占位实现),确保系统无需嵌入配置也能正常启动。
在摄取侧,连接器默认会通过 AppConfig API 从服务器自动拉取嵌入配置(见 chunking_config.py 的get_semantic_search_config()),确保文档侧与查询侧使用同一模型;若本地显式配置,则会与服务器配置逐项比对(provider、model、model_embedding_key、region 等),不一致时报错并给出修复建议,另有allow_local_embedding_config: true作为"破窗"开关(不推荐)。
安全考虑
数据隐私
- Embedding 存储:向量与文档同库存放,沿用相同的访问控制;
- 外部 API 调用:Embedding Provider 会收到文档文本(查询侧收到查询文本),需确保符合合规要求;
- 凭据管理:API key 与 AWS 凭据必须妥善保管(可通过环境变量注入)。
访问控制
语义搜索遵循 DataHub 现有的访问控制体系:
- 用户只能看到自己有权限查看的结果;
- 返回结果前会强制校验实体级权限。
性能考虑
以下数据为架构文档给出的工程评估量级(实际数值取决于索引规模、硬件与模型,应以实测为准)。
索引性能
- 双写影响:双索引并行写入带来约 10%-20% 的写入延迟增加;
- Embedding 生成:异步执行,不阻塞摄取主流程;
- 批量处理:Embedding 以批量方式生成以提升效率(摄取端默认
batch_size: 25)。
查询性能
- k-NN 开销:每次查询约 50-200ms(取决于索引规模);
- 查询向量生成:约 100-300ms;
- 端到端总延迟:典型 200-500ms。
扩容建议
| 索引规模 | 建议 |
|---|---|
| < 10 万篇文档 | 单节点即可 |
| 10 万 - 100 万篇 | 考虑部署专用 k-NN 节点 |
| > 100 万篇 | 推荐分片(sharding)与副本(replicas) |
未来增强方向
架构文档展望了两个演进方向:
- 混合搜索(Hybrid Search):融合关键词与语义两路打分,提升整体相关性;
- 模型微调(Model Fine-tuning):面向特定领域微调 Embedding 模型,进一步提升准确度。
配合前文所述"迁移完成后统一索引"的终态,这两项增强将共同构成 DataHub 搜索体验的下一步演进。
小结
DataHub 语义搜索是一套设计克制的增量能力:通过双索引过渡架构保证零停机迁移与回滚安全;通过SemanticContent aspect + MCP让向量成为一等公民元数据,天然获得审计追踪与权限控制;通过文档侧由连接器生成、查询侧由 GMS 生成的职责分工,兼顾数据新鲜度与隐私边界;分块策略与 k-NN 参数(ef_construction、m、space_type)则提供了精确性与成本之间的调节旋钮。理解这条链路后,你可以在 application.yaml 中通过semanticSearch配置段与ELASTICSEARCH_SEMANTIC_*、EMBEDDING_PROVIDER_*等环境变量,为自己的部署启用并调优语义搜索。
【免费下载链接】datahubThe Context Platform for your Data and AI Stack项目地址: https://gitcode.com/GitHub_Trending/da/datahub
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考