Elasticsearch 9.5 · 实战案例
图片、视频、音频,一个 mapping,一条 match 查询。Elasticsearch 9.5 的 semantic 字段让多模态搜索像文本搜索一样简单。
🗓 2026 年 8 月 · 🏷 Elasticsearch 9.5 · Jina v5-omni · 多模态 · ⏱ 阅读约 15 分钟
微信里几年的聊天记录,藏着大量珍贵的照片、语音和视频——但它们一直是"存进去就找不到"的黑洞。本文展示如何用 Elasticsearch 9.5 的新 semantic 字段,配合 Jina Embeddings v5-omni-small 多模态模型,用不到 50 行核心代码,把图片、视频、音频统一纳入语义搜索。
微信本地数据的特征很有代表性:大量媒体文件,却几乎没有任何文本标签。一张照片的文件名是 bd9de7d1.dat,一段视频是 1781612096.mp4,一条语音是 音乐疗愈.m4a——传统关键词搜索对这类数据几乎无能为力。
多模态语义搜索解决这个问题的方式,是把所有媒体映射到同一个向量空间:"海边风景"这个文本短语,和一张海滩照片,会落在空间里的相近位置。你可以用文字找图片,用图片找视频,用一段旋律找相似音频。
传统方案
ES 9.5 semantic 字段(新)
match 查询即可完成文字搜索semantic 字段是 Elasticsearch 9.5 引入的新类型,是 semantic_text(文本专用)的多模态扩展版。它在 ingest 阶段自动调用配置好的推理端点,把传入的媒体内容转化为向量并存储。
💡 与 semantic_text 的关键区别
semantic_text使用text_embeddingtask type,只处理文本。semantic字段使用embeddingtask type,支持图片、视频、音频、PDF 及文本的混合输入。两者共存,各司其职。
模态 | 输入格式 | MIME 示例 |
|---|---|---|
图片 |
| JPEG、PNG、WebP |
视频 |
| MP4(最多 32 帧均匀采样) |
音频 |
| MP3、M4A、WAV |
| 视觉化处理 | |
文本 | 直接传字符串 | — |
⚠️ 1 MB 限制 Elasticsearch 默认对每个二进制输入设置 1 MB 上限
(indices.inference.max_binary_input_size)。大文件必须在索引前压缩或缩小。本文后续会展示具体的压缩方案。
整体数据流
📁 微信本地文件 ⚙️ 预处理脚本 📤 PUT _doc 🧠 Elasticsearch 9.5
图片 / 视频 / 音频 → 压缩 · Base64 编码 → 结构化 JSON → semantic 字段自动 embedding
🔍 用户查询 🌐 搜索 Web App match / knn 查询 📊 结果排序
文字 / 图片 / 视频 / 音频 → Python HTTP Server → ES 自动生成 query vector → 相似度 > 60% 高亮展示
图 1:Kibana 中 wechat-multimodal 索引概览 — 文档数、索引大小、字段列表
Elasticsearch 9.5 内置了 .jina-embeddings-v5-omni-small 推理端点,支持 Elastic Cloud Hosted、Serverless 以及开启 Cloud Connected Mode 的自管集群,无需手动创建。
# 验证推理端点已就绪
GET _inference/embedding/.jina-embeddings-v5-omni-small核心只有一个字段:把 embedding 声明为 semantic 类型,绑定推理端点。其他字段存储元数据(文件路径、媒体类型、时间)。
{
"mappings": {
"properties": {
"embedding": {
"type": "semantic",
"inference_id": ".jina-embeddings-v5-omni-small"
},
"file_path": { "type": "keyword" },
"file_name": { "type": "keyword" },
"media_type": { "type": "keyword" },
"date_folder": { "type": "keyword" },
"file_size": { "type": "long" },
"indexed_at": { "type": "date" }
}
}
}就这些。Elasticsearch 会在每次文档写入时自动调用推理端点并存储向量。
把图片读为字节,Base64 编码后放入结构化对象。大图片先用 PIL 缩小到 900 KB 以内。
def process_image(path: Path) -> tuple[bool, str]:
raw = path.read_bytes()
# 超过 900 KB 自动缩小,保持语义不变
data = raw if len(raw) <= MAX_BINARY else resize_image(path)
b64 = base64.b64encode(data).decode()
mime = "image/png" if path.suffix.lower() == ".png" else "image/jpeg"
doc = {
"embedding": { # ← ES 自动生成向量,无需手动调推理 API
"type": "image",
"value": f"data:{mime};base64,{b64}"
},
"file_path": str(path),
"file_name": path.name,
"media_type": "image",
"indexed_at": datetime.utcnow().isoformat() + "Z",
}
es_request("PUT", f"/{INDEX}/_doc/{path.stem}", doc)视频超过 900 KB 时用 ffmpeg 压缩:降分辨率、提高 CRF、截取前 30 秒。模型内部会均匀抽取最多 32 帧,自动理解视频整体内容。
def compress_video(path: Path) -> bytes:
tmp = tempfile.NamedTemporaryFile(suffix=".mp4", delete=False)
subprocess.run([
"ffmpeg", "-y", "-i", str(path),
"-vf", "scale='min(320,iw)':-2",
"-crf", "40", "-preset", "fast",
"-t", "30", "-an", # 去音频,取前 30 秒
tmp.name,
], capture_output=True, check=True)
return Path(tmp.name).read_bytes()
# 索引时结构与图片完全一致,只改 type
doc["embedding"] = {
"type": "video",
"value": f"data:video/mp4;base64,{b64}"
}音频用 ffmpeg 转为 16 kHz 单声道 MP3,码率 32 kbps。语音、音乐、环境声都可以索引。
def compress_audio(path: Path) -> bytes:
tmp = tempfile.NamedTemporaryFile(suffix=".mp3", delete=False)
subprocess.run([
"ffmpeg", "-y", "-i", str(path),
"-ar", "16000", # 16 kHz 采样率
"-ac", "1", # 单声道
"-b:a", "32k", # 32 kbps 码率
"-t", "120", # 最长 2 分钟
tmp.name,
], capture_output=True, check=True)
return Path(tmp.name).read_bytes()💡 更少代码,更多模态 三种媒体的索引代码结构完全一致——只有
type字段不同。这是semantic字段设计的核心优势:统一的数据模型,统一的 ingest 路径。
semantic 字段最大的易用性提升体现在查询端:文字查询只需一条普通的 match,Elasticsearch 在服务端自动生成 query embedding;媒体查询使用 knn + query_vector_builder,格式与索引时完全对称。
用自然语言描述想找的内容,ES 自动调用推理端点将文字转为向量并执行语义搜索。
GET wechat-multimodal/_search
{
"query": {
"match": {
"embedding": "海边风景,傍晚日落"
}
},
"_source": { "excludes": ["embedding"] }
}
上传一张图片,找语义相似的内容。格式与索引时完全一致。
GET wechat-multimodal/_search
{
"query": {
"knn": {
"field": "embedding",
"query_vector_builder": {
"embedding": {
"input": {
"type": "image",
"value": "data:image/jpeg;base64,<base64-encoded-bytes>"
}
}
},
"num_candidates": 100,
"k": 20
}
}
}用一段视频片段搜索语义相似的视频——只需把 type 改为 "video",结构完全相同。
GET wechat-multimodal/_search
{
"query": {
"knn": {
"field": "embedding",
"query_vector_builder": {
"embedding": {
"input": {
"type": "video",
"value": "data:video/mp4;base64,<base64-encoded-bytes>"
}
}
},
"k": 10
}
}
}以音频片段搜索相似音频——同样只改 type 字段。
GET wechat-multimodal/_search
{
"query": {
"knn": {
"field": "embedding",
"query_vector_builder": {
"embedding": {
"input": {
"type": "audio",
"value": "data:audio/mpeg;base64,<base64-encoded-bytes>"
}
}
},
"k": 10
}
}
}🔑 设计一致性 注意索引格式和查询格式完全对称:索引时
{"type": "image", "value": "data:..."},查询时input: {"type": "image", "value": "data:..."}。切换模态只需改type,其余代码不动。
在上述 API 基础上,我们用 Python 标准库搭建了一个本地搜索 Web App,无需额外框架依赖。
def text_search(query: str, size=20, filter_type=None):
"""文字查询:match 即可,ES 自动生成 embedding"""
body = {
"query": { "match": { "embedding": query } },
"_source": SOURCE_FIELDS,
"size": size,
}
if filter_type:
body["query"] = {
"bool": {
"must": { "match": { "embedding": query } },
"filter": { "term": { "media_type": filter_type } }
}
}
return es_request("POST", f"/{INDEX}/_search", body)
def media_search(media_type: str, data_uri: str, size=20):
"""媒体查询:knn + query_vector_builder,type 动态传入"""
return es_request("POST", f"/{INDEX}/_search", {
"query": {
"knn": {
"field": "embedding",
"query_vector_builder": {
"embedding": {
"input": { "type": media_type, "value": data_uri }
}
},
"num_candidates": size * 5,
"k": size,
}
},
"_source": SOURCE_FIELDS,
"size": size,
})搜索结果按相似度阈值分两组:≥ 60% 的直接展示,< 60% 的折叠显示,让用户一眼区分高相关和低相关结果。

图 3:搜索 Web App 界面 — 文字查询 tab,输入框,筛选下拉,高相关区 + 折叠低相关区

图 4:图片模态查询结果 — 上传一张图片,返回语义相似的图片/视频结果网格
问题 | 建议 |
|---|---|
二进制数据超过 1 MB | 图片用 PIL 缩小质量;视频用 ffmpeg 降分辨率 + 截短;音频降码率到 32 kbps |
响应体积过大 | 搜索时用 |
索引速度慢 | 推理端点有速率限制,并发 Worker 控制在 2,失败后指数退避重试 |
向量存储空间大 | 使用 |
混合搜索(关键词 + 语义) | 用 |
Elasticsearch 9.5 的 semantic 字段把多模态搜索从一个需要大量工程投入的方向,变成了和普通文本搜索同等难度的事情:
"type": "semantic" 和 inference_id,其余交给 ES{"type": "...", "value": "data:..."}match,媒体用 knn,无需手动生成 embedding对于微信数据这样"有媒体无标签"的场景,semantic 字段几乎是零阻力的解决方案。唯一需要关注的工程细节是 1 MB 的二进制上限——做好文件压缩,其余基本是"能跑就能搜"。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。