资讯动态

设备售后资料秒级检索:用Weaviate向量数据库实现文本与图片语义查询

发布时间:2026/10/7 3:47:40 来源:尧图企业网站定制
售后工程师的电脑里往往躺着几百个PDF手册、几十G的故障照片微信聊天记录里还散落着各种上次那个异响的图。客户电话一来说设备报错E014手册里哪有说明你打开文件夹一层层翻找到的却是另一款型号的文档。这个场景我太熟悉了后来我把资料全部扔进Weaviate向量数据库用自然语言直接查E014报警 复位方法液压站漏油 密封圈位置几秒钟就能把对应的说明书片段和故障图片一起捞出来。这篇内容就是围绕这个需求写的给出C#和Python两套简洁的调用示例覆盖设备说明书文本和故障图片的存储、索引与查询适合刚接触向量数据库的售后、设备运维或者上位机开发初学者直接参考。1. 设备售后资料用向量检索解决的是搜不到而不是存不下1.1 传统文件管理在售后场景的三处硬伤很多团队处理设备资料的方式还是文件夹文件名全文搜索。小规模时勉强能用资料一多就暴露问题。第一说明书是PDF或扫描件文件名往往是XX设备说明书_V2_final最终版.pdf正文里的关键内容根本没法通过文件名命中。你想搜如何校准扭矩文件名叫维护手册就搜不到。第二故障照片是纯二进制数据传统数据库只能靠人工打标签。现场拍的照片谁有时间写标签第三售后人员描述故障用的词和手册里的术语经常对不上。手册写的是液压油缸活塞密封圈老化工人说的是油缸漏油。关键词搜索在这里直接失效但语义搜索能把这两句话关联起来。向量数据库解决的就是这些。它把文本和图片各自转成一组几百维的浮点数向量语义相近的内容在向量空间里的距离就近检索时拿你的问题向量去和库里所有向量比相似度排在最前面的就是语义最匹配的结果。这跟传统数据库的精确匹配、模糊匹配是两种完全不同的逻辑。1.2 为什么在这个场景选Weaviate而不是其他向量库选型这件事我踩过不少坑。市面上的向量数据库我接触过Milvus、Qdrant、ChromaDB也用过Elasticsearch加向量插件最后在售后场景里固定用Weaviate理由是它最贴合中小团队、混合数据、快速落地这几个诉求。对比一下几个主流方案方案部署复杂度文本图片混合检索客户端语言支持适合场景Weaviate单个Docker容器即可内置多模态模块如CLIPC#、Python、Go、Java等中小规模、需要快速落地的项目Milvus组件多需要etcd、MinIO等需自建双路召回Python优先大规模、高并发检索平台Qdrant轻量Rust编写需配合外部Embedding官方Python/RustC#社区库纯向量检索场景Elasticsearch重需要调优向量字段支持有限官方各语言已有ES技术栈Weaviate有一个很关键的特性它自带的schema数据模型很直观而且支持你直接定义文本字段和图片字段检索时可以同时做语义搜索和结构化过滤。比如查找 2023年批次 的 油缸漏油 图片这种带条件的查询只需要在向量检索的基础上加一个属性过滤就行不用维护两套系统。部署上也简单。售后系统的服务器通常不大Weaviate单个Docker实例就能跑测试环境甚至可以用嵌入式模式不用单独搭Kafka、etcd这些基础设施。这对设备厂商、售后软件服务商来说落地成本低很多。1.3 一个能跑的向量检索流程分几步抛开概念实际数据流就四步入库前处理说明书PDF抽成文本段落故障图片压缩到合适尺寸。向量化用Embedding模型把文本转成向量用多模态模型或视觉模型把图片转成向量。写入Weaviate把原始内容、业务字段设备型号、故障代码、上传时间等和向量一起存入对象。查询用户输入问题转成同样的向量空间做相似度检索返回Top N结果。后面要给的C#和Python示例都是围绕这四步展开的。C#适合你在上位机系统、设备管理软件里集成Python适合做离线批量导入、模型调试和实验验证。两套代码我都按尽量少、够直观的原则写避免一上来就铺一大堆抽象封装。2. C#调用Weaviate的最短路径从空项目到第一次语义查询2.1 环境准备与版本匹配经验C#这边官方有Weaviate.Client这个NuGet包我用的是基于HTTP的官方客户端。先说明一点Weaviate的客户端版本和服务器版本要匹配否则某些接口会报错。我的建议是服务器用最新的稳定版镜像客户端NuGet包也选对应的大版本不要混搭。创建项目时直接用.NET 8或.NET 6都行控制台应用就够演示。需要安装的包dotnet add package Weaviate.Client dotnet add package System.Text.Json如果你的服务器还没启动先跑一个Weaviate容器。这里我用的是带text2vec-transformers模块的镜像它可以在本地跑文本向量化不依赖外部APIdocker run -d --name weaviate \ -p 8080:8080 \ -e ENABLE_MODULEStext2vec-transformers,multi2vec-clip \ -e DEFAULT_VECTORIZER_MODULEtext2vec-transformers \ -e CLIP_INFERENCE_APIhttp://clip:8000 \ semitechnologies/weaviate:latest注意multi2vec-clip默认需要单独跑一个CLIP推理服务初学阶段可以暂时只用文本模块图片向量化放到后面Python侧处理再导入。别一开始就把整个多模态架构搭起来容易把问题复杂化。2.2 定义Schema设备信息、文档、图片三类对象Weaviate里的Schema类似关系数据库的表设计但更灵活。我要存三类数据设备基本信息、说明书文档段落、故障图片。用一个类名DocumentChunk存说明书段落一个类名FaultImage存故障图片这样查询时可以按类型过滤。using Weaviate.Client; var client new WeaviateClient(http://localhost:8080); var docClass new Class { ClassName DocumentChunk, Vectorizer text2vec-transformers, Properties new ListProperty { new() { Name model, DataType DataType.Text }, new() { Name content, DataType DataType.Text }, new() { Name category, DataType DataType.Text }, new() { Name sourceFile, DataType DataType.Text }, new() { Name pageNo, DataType DataType.Int } } }; var imageClass new Class { ClassName FaultImage, Vectorizer multi2vec-clip, Properties new ListProperty { new() { Name model, DataType DataType.Text }, new() { Name faultCode, DataType DataType.Text }, new() { Name description, DataType DataType.Text }, new() { Name imagePath, DataType DataType.Text } } }; await client.Schema.CreateClassAsync(docClass); await client.Schema.CreateClassAsync(imageClass);这段代码里有几个细节值得展开Vectorizer决定了这一类数据入库时用哪个模型做向量化。文本类用text2vec-transformers图片类如果服务器配了CLIP模块就可以用multi2vec-clip。初学者最容易忽略的是同一个Weaviate实例可以给不同的Class配置不同的向量化模块这不是全局唯一的。属性类型里DataType.Int和DataType.Text是随便起的属性名查询时可以用它们做过滤条件。比如查所有E014故障图片就直接过滤faultCode E014不做向量检索。首次调用CreateClassAsync时如果Class已存在会直接报错。我建议在测试代码里先捕获这个异常或者启动时先检查client.Schema.GetClassAsync(DocumentChunk)是否为空。2.3 写入数据说明书文本拆分与图片路径写入文本数据时有个容易被忽视的问题不要把一整个几十页的PDF当作一条记录写进去。向量检索是基于语义片段的整篇文档的向量会变成一个四不像的平均值查什么都不准。我一般的做法是按段落或按页面拆分每个段落单独存一条记录并把页码、来源文件作为属性保留。这样命中后能精确定位到第几页第几段。下面演示一下。假设PDF解析出的文本已经按段落分好了取前两段var chunks new ListDictionarystring, object { new() { [model] HT-3000, [content] E014报警表示液压油缸压力不足请检查密封圈状态和油路是否堵塞。, [category] 故障排除, [sourceFile] HT-3000操作手册.pdf, [pageNo] 23 }, new() { [model] HT-3000, [content] 复位方法按下控制面板上的复位键3秒等待压力表读数恢复到正常范围。, [category] 故障排除, [sourceFile] HT-3000操作手册.pdf, [pageNo] 24 } }; foreach (var chunk in chunks) { await client.Data.CreateAsync(new DataObject { Class DocumentChunk, Properties chunk }); }写入图片时要注意Weaviate的multi2vec-clip模块接收的是图片的Base64字符串不是服务器上的文件路径。如果你在C#里做图片入库需要读取本地文件并转Base64var imageBytes File.ReadAllBytes(D:\faults\e014_oil_leak.jpg); var b64 Convert.ToBase64String(imageBytes); await client.Data.CreateAsync(new DataObject { Class FaultImage, Properties new Dictionarystring, object { [model] HT-3000, [faultCode] E014, [description] 油缸底部密封圈处明显漏油, [imagePath] e014_oil_leak.jpg }, // 如果Class配置了multi2vec-clipBase64图片需要放在这里关联 });实际上DataObject里没有直接暴露图片字段的强类型属性官方客户端对多模态的支持不如Python方便。如果你在C#里搞不定图片自动向量化有个务实的做法先用Python脚本把图片向量算出来存成浮点数组文件C#读取后通过Vector字段直接写入。我在项目初期就是这么干的。2.4 查询nearText和参数过滤的组合查询是整个环节里最见效果的一步。C#客户端的查询语法是链式调用跟官方GraphQL的语义对齐但写法比JSON简洁。// 语义查询查油缸漏油 密封圈更换 var result await client.Data.SearchAsync(new SearchRequest { Class DocumentChunk, NearText new NearTextQuery { Query 油缸漏油 密封圈更换, Distance 0.6f }, Limit 5 }); foreach (var item in result.Data) { Console.WriteLine($命中{item.Properties[content]}来源{item.Properties[sourceFile]} 第{item.Properties[pageNo]}页); }如果只想查某个型号设备的资料可以在SearchRequest里加Where过滤条件让语义检索和属性过滤同时生效var filtered await client.Data.SearchAsync(new SearchRequest { Class DocumentChunk, NearText new NearTextQuery { Query E014 复位, Distance 0.7f }, Where new WhereFilter { Operator Operator.Equal, Path model, ValueString HT-3000 }, Limit 3 });这里解释一下Distance它表示向量相似度的距离阈值数值越小表示越严格。Weaviate返回的结果都带_additional.distance字段你可以先不设阈值跑一次看实际返回的距离分布再调整。不同Embedding模型的分布差异很大text2vec-transformers算出来的距离一般在0.2到0.8之间设0.6左右通常比较合适。2.5 C#客户端容易卡住的两个细节第一个是异步API的命名。Weaviate.Client的方法基本都有Async后缀忘了await在控制台程序里不会报错但查询结果是Task类型你直接取数据拿到的是空集合排查起来很隐蔽。第二个是对象属性序列化。Dictionarystring, object的value如果是int序列化没问题如果是double有些老版本客户端会默认拒收。建议数值属性统一先转成整数或字符串再入库。3. Python侧更简洁的等价写法别忘了图片向量处理的优势3.1 Python客户端版本那么多选哪个Python是Weaviate支持得最好的语言但也因为版本迭代带来了一些混乱。目前市面上常见的客户端有两代老版的weaviate-client3.x和新版的weaviate4.x。两者API风格差异很大。我这里以新版v4为主因为它在连接管理、错误提示上更友好也是官方文档当前主推的版本。如果你看的老教程是client.schema.create_class(schema)这种写法那大概率是v3直接照着写在新版会报错。安装pip install weaviate-client连接import weaviate client weaviate.connect_to_local()就这么简单。新版客户端会自动读本地的http://localhost:8080不需要再手动拼URL。连接不上时它会直接抛WeaviateConnectionError比老版本直观很多。3.2 同样的SchemaPython代码量少一半Python写同样的Schema大概是这样doc_class { class: DocumentChunk, vectorizer: text2vec-transformers, properties: [ {name: model, dataType: [text]}, {name: content, dataType: [text]}, {name: category, dataType: [text]}, {name: sourceFile, dataType: [text]}, {name: pageNo, dataType: [int]}, ], } # 如果已存在就先删除方便测试重跑 if client.schema.exists(DocumentChunk): client.schema.delete_class(DocumentChunk) client.schema.create_class(doc_class)我在项目里习惯加一个存在则删除的前置判断因为调试Schema定义时经常要改字段不删掉重新建的话会报属性冲突。生产环境当然不能随意删但初学阶段这个习惯能让你的迭代速度翻倍。3.3 批量导入既有文本也有本地图片Python处理批量导入有一个天然优势它可以直接调用Pillow、OpenCV做图片预处理然后把图片转成Base64交给Weaviate整个过程不用离开同一个脚本。文本段落批量入库chunks [ {model: HT-3000, content: E014报警表示液压油缸压力不足请检查密封圈状态和油路是否堵塞。, category: 故障排除, sourceFile: HT-3000操作手册.pdf, pageNo: 23}, {model: HT-3000, content: 复位方法按下控制面板上的复位键3秒等待压力表读数恢复正常。, category: 故障排除, sourceFile: HT-3000操作手册.pdf, pageNo: 24}, ] with client.batch as batch: for chunk in chunks: batch.add_object( collectionDocumentChunk, propertieschunk, )图片入库时如果Class用的是multi2vec-clip可以直接把Base64图片丢给对象import base64 from pathlib import Path def image_to_b64(path: Path) - str: return base64.b64encode(path.read_bytes()).decode(utf-8) image_b64 image_to_b64(Path(faults/e014_oil_leak.jpg)) with client.batch as batch: batch.add_object( collectionFaultImage, properties{ model: HT-3000, faultCode: E014, description: 油缸底部密封圈处明显漏油, imagePath: faults/e014_oil_leak.jpg, }, # v4客户端里图片和其他媒体文件通过references或vector指定 # 如果类配置了multi2vec-clip这里需要把b64传给对应的属性 )这里必须单独说明一下multi2vec-clip在Weaviate里的配置比较复杂官方文档要求先定义multi2vec-clip模块的imageFields参数把某个属性标记为图像字段。比如在Class定义里增加一个imageField属性数据写入时把Base64字符串直接放进去模块就会自动帮你向量化。这个特性在C#客户端里容易被绕开在Python里则可以直接操作。我实际用下来发现如果不想折腾CLIP服务也可以先在外部用Python的SentenceTransformer或者OpenCLIP把图片向量算好然后用add_object时直接传vector参数绕过Weaviate内置的图片模块。这种做法代码稍微多几行但可控性更高也不依赖额外的推理容器。3.4 查询对比Python版更贴近调试习惯Python v4的查询API是遍历式的返回结果直接用objects列表访问不需要纠结序列化问题。response client.collections.get(DocumentChunk).query.near_text( query油缸漏油 密封圈更换, distance0.6, limit5, ) for obj in response.objects: props obj.properties print(f命中{props[content]} f来源{props[sourceFile]} 第{props[pageNo]}页)带过滤条件的查询response client.collections.get(DocumentChunk).query.near_text( queryE014 复位, distance0.7, limit3, filtersweaviate.classes.query.Filter.by_property(model).equal(HT-3000), )Python版最大的优势是可以边查边打印向量距离判断阈值是否合适。查询结果里的distance属性直接暴露出来你可以在循环里输出for obj in response.objects: print(f距离: {obj.metadata.distance:.4f}, 内容: {props[content]})这个调试循环我几乎每个项目都会写一遍因为它能直观告诉你这个模型的语义边界在哪里。4. 故障图片说明书的混合检索设计Schema、过滤条件和召回平衡4.1 文本和图片能不能同时查多模态检索的实际组合方式很多售后场景需要的不是单独搜文本或单独搜图片而是一张故障照片和某段说明书文字之间存在对应关系。比如用户上传一张漏油的照片系统要能自动关联到手册里关于密封圈更换的那一页。Weaviate的多模态能力可以实现这个需求文本和图片都映射到同一个CLIP向量空间你拿一段文字去查图片或者拿图片去查文字都能算相似度。这意味着你可以把故障现场描述和故障照片放进同一个语义空间互相检索。但在工程落地时我不建议一开始就做跨模态互查。原因有两个第一CLIP模型对设备细粒度故障的辨识能力有限。它适合理解漏油、裂纹、磨损这类宏观场景但你让它区分轴承磨损和齿轮点蚀这种相近故障效果不稳定。第二跨模态的向量距离和同模态的距离分布不一样阈值要重新校准调试成本高。更稳妥的设计是分而治之文本和图片各自入库各自检索通过业务属性故障代码、设备型号关联。查询时先向量召回一批候选再用属性过滤锁定正确的设备型号和故障代码最后用业务规则做关联展示。这套逻辑对初学者更友好也更容易排查问题。4.2 一个适合售后场景的Schema组合我自己项目里的Schema设计是三层第一层Device存设备基础信息这里不展开。第二层DocumentChunk存说明书的语义片段字段包含型号、内容、分类、来源文件和页码。第三层FaultImage存故障图片字段包含型号、故障代码、描述文字和图片路径或图片Base64。为什么故障图片里还冗余存一份faultCode和description因为向量检索只能保证语义相近不能保证业务正确。一张磨损的轴承照片可能和手册里检查轴承间隙那段语义相近但这条手册记录是另一个型号的。冗余存储业务字段后查询时把model作为过滤条件就能避免跨型号串数据。4.3 召回策略先向量后过滤还是先过滤后向量Weaviate的查询引擎支持两种执行顺序理解这个对查询性能很重要。第一种是先过滤后向量先把符合modelHT-3000的记录缩小到一个子集再在这个子集里做向量相似度排序。优点是结果更精准、过滤条件能减少向量计算量缺点是如果某个型号的数据量太少向量召回可能找不到足够的候选。第二种是先向量后过滤全库向量相似度排序取Top 200再对这200条做过滤。优点是语义召回能力强适合我不知道这个故障属于哪个型号先帮我找找像不像某个问题的场景缺点是过滤后有可能只剩一两条结果。实际使用中我通常是向量召回 Top N 属性过滤精排的组合。具体到代码就是设置一个较大的Limit比如50或100拿到结果后在内存里做属性过滤或者用Weaviate的Where条件把过滤下推到数据库。售后场景的数据量一般在万级到十万级这个量级下两种顺序性能差异不大优先保证查询结果的准确性更重要。4.4 混合检索的验收标准不要只看看起来像搭建完一个检索demo很多人跑通就完事了但检索质量需要验证。我给自己定的验收标准是这样的用10条真实的售后问题描述做测试集每条手工标注应该命中的资料ID。查询后看命中结果是否出现在Top 3里。如果Top 3没有就算失败。统计召回率目标不低于80%。错误结果要分类是语义理解错描述与内容无关还是过滤条件错型号筛选把正确结果滤掉了这两类的调整方向完全不同。这个测试集不用很复杂Excel表格里维护几十条就行。它能在你换Embedding模型、调整阈值时快速给出量化对比比肉眼抽查可靠得多。5. 新手最容易翻车的五个地方我用实测踩坑换来的经验5.1 嵌入模型与向量空间的维度不一致Weaviate允许你给每个Class配置不同的向量化模块这本身是特性但也容易埋坑。比如DocumentChunk用text2vec-transformers生成的是384维向量FaultImage用CLIP生成的是512维向量。同一实例里不同Class的维度可以不同这没问题。但如果你曾经在物理机上跑过一段代码往DocumentChunk里写入了外部模型生成的768维向量后来又把Class的Vectorizer改成了384维的模块就会出现属性冲突或查询报维度错误。排查这类问题我建议把所有Class的配置集中到一个版本管理文件里每次改动都提交到Git。Weaviate对这种改动的报错信息比较直接你能看到类似Vector dimension mismatch的提示只要学会看这条报错问题就解决一半了。5.2 中文语义搜索的效果取决于Embedding模型text2vec-transformers镜像内置的默认模型大多是英文模型直接拿来搜中文说明书效果会很差经常出现查不到或者返回一堆无关结果。这不是Weaviate的问题是Embedding模型的问题。解决方案有两条路一是改用支持中文的模型。Weaviate的text2vec-transformers支持通过环境变量指定推理模型你可以换成sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2这类多语言模型对中文的语义理解明显更好。但注意下载模型需要时间首次启动时Weaviate会等模型加载完成可能要多等十几分钟。二是外部向量化。用text2vec-transformers的inferenceApi指向你自己部署的中文Embedding服务或者干脆不用Weaviate内置向量化在你自己的Python代码里用text2vec算好向量入库时显式传入vector参数。第二种方式我强烈推荐因为你会发现调试Embedding模型时完全不需要动Weaviate的配置改完模型重跑一个Python脚本就行。5.3 阈值Distance不是越大越好也不是越小越好Distance这个参数是新手最纠结的地方。它的含义是匹配向量与查询向量的距离越小表示越相似。但不同模型的距离尺度不一样有的模型匹配结果距离在0.3左右有的在0.7左右不能套用同一个经验值。我调试阈值的方法是先用一个极宽松的阈值比如0.99跑一批查询把每条结果的distance打出来观察正确结果和错误结果的距离分布区间然后在两者之间找一个分界点。如果你的正确结果集中在0.2到0.5错误结果集中在0.6以上那0.55就是一个合理的阈值。这个过程每个模型都要做一次别嫌麻烦。5.4 图片入库前不压缩查询会越来越慢故障图片经常是手机拍的动辄几MB甚至十几MB。直接Base64写入Weaviate会让数据体积暴涨最直接的后果是查询变慢、备份变大。我算过一笔账一张5MB的图片转Base64后约6.7MB一万张就是67GB。如果压缩到长边800像素、质量80%的JPEG单张往往不到200KB总体积下降95%以上。图片压缩对向量检索准确率的影响非常小因为Embedding模型输入前还会做resize和归一化你提前压缩到合理尺寸方差反而更小。我在Python侧的处理流程是from PIL import Image img Image.open(faults/e014_oil_leak.jpg) img.thumbnail((800, 800)) img.save(faults/e014_oil_leak_thumb.jpg, quality85)然后用压缩后的图片做Base64入库。这个方法对C#同样适用用System.Drawing或者ImageSharp的缩略图功能就行。5.5 把查询结果展示当成检索系统的一部分来设计只返回一堆文本段落售后人员是没法用的。我在实际项目中把查询结果拼装成一个包含原文片段、来源文件、页码、相关图片的卡片视图才真正被一线同事接受。这一步不复杂查询DocumentChunk后拿返回的sourceFile和pageNo去关联展示原文PDF同时用同一个faultCode去查FaultImage把照片一起展示。这个设计逻辑是向量检索负责找到相关片段业务关联负责把上下文补齐。你不需要让向量数据库解决所有问题它只要能精准缩小范围剩下的交给传统代码来处理就行。最后说一个我自己的体会向量数据库现在工具链已经挺成熟了但真正决定一个售后检索系统好不好用的不是数据库本身而是你对数据切的粒度、Embedding模型的选择、阈值调参的方法。每次在实际数据上跑通一个查询都花点时间看看为什么这条排第一、那条没被召回比盲调参数有价值得多。这玩意儿用上之后确实比按文件名找说明书的年代舒服太多了。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑